kconfig-lsp 0.1.0

A language server for Kconfig files used in Linux, Zephyr, U-Boot, and coreboot
Documentation
use std::path::Path;

use tower_lsp::lsp_types::*;

use crate::analysis::WorldIndex;

pub fn complete(index: &WorldIndex, path: &Path, pos: Position) -> Option<CompletionResponse> {
    let fa = index.files.get(path)?;
    let offset = fa.line_index.offset(pos.line, pos.character);
    let prefix = prefix_at_offset(&fa.source, offset);

    let mut items: Vec<CompletionItem> = Vec::new();

    for kw in KEYWORDS {
        if kw.starts_with(&prefix) || prefix.is_empty() {
            items.push(CompletionItem {
                label: kw.to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                ..Default::default()
            });
        }
    }

    for sym in &index.all_symbols {
        if sym.starts_with(&prefix) || (prefix.is_empty() && is_symbol_position(&fa.source, offset))
        {
            items.push(CompletionItem {
                label: sym.clone(),
                kind: Some(CompletionItemKind::CONSTANT),
                detail: index
                    .get_definitions(sym)
                    .first()
                    .and_then(|d| d.prompt.clone()),
                ..Default::default()
            });
        }
    }

    if items.is_empty() {
        None
    } else {
        Some(CompletionResponse::Array(items))
    }
}

fn prefix_at_offset(source: &str, offset: usize) -> String {
    let bytes = source.as_bytes();
    let mut start = offset;
    while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') {
        start -= 1;
    }
    source[start..offset].to_string()
}

fn is_symbol_position(source: &str, offset: usize) -> bool {
    let line_start = source[..offset].rfind('\n').map_or(0, |p| p + 1);
    let before = source[line_start..offset].trim_start();
    matches!(
        before,
        "depends on " | "select " | "imply " | "default " | "def_bool " | "def_tristate "
    ) || before.starts_with("depends on ")
        || before.starts_with("select ")
        || before.starts_with("imply ")
}

const KEYWORDS: &[&str] = &[
    "config",
    "menuconfig",
    "choice",
    "endchoice",
    "comment",
    "menu",
    "endmenu",
    "if",
    "endif",
    "source",
    "mainmenu",
    "bool",
    "tristate",
    "string",
    "hex",
    "int",
    "prompt",
    "default",
    "def_bool",
    "def_tristate",
    "depends",
    "select",
    "imply",
    "visible",
    "range",
    "help",
    "modules",
    "transitional",
    "optional",
];