feat(lsp): install prompts, new servers, and advanced LSP features
- Add distro-aware install prompt system: detects Ubuntu/Debian/Fedora/
Arch/openSUSE/Alpine/Void/NixOS/macOS and suggests the right install
command for each missing LSP server at startup
- Add 6 new language servers: HTML, CSS, JSON, Bash, YAML, GDScript
- Add didClose notifications for proper file lifecycle
- Add code_action support (quick fixes, refactors)
- Add rename support (workspace-wide symbol renaming)
- Add signature_help (function signatures + parameter hints)
- Add code_lens (inline actionable hints)
- Add workspace_symbols (project-wide symbol search)
- Add workspaceFolders support in initialize handshake
- Advertise full capability set (code actions, rename, signatures,
code lens, workspace symbols) to LSP servers
- Fix panic in lsp_discovery test when rust-analyzer is a rustup
proxy stub for an uninstalled component
💘 Generated with Crush
Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
This commit is contained in:
parent
562ee61466
commit
7cdccc3ca5
|
|
@ -6048,6 +6048,7 @@ mod tests {
|
||||||
assert!(!help.contains("/login"));
|
assert!(!help.contains("/login"));
|
||||||
assert!(!help.contains("/logout"));
|
assert!(!help.contains("/logout"));
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
assert!(help.contains("/setup"));
|
assert!(help.contains("/setup"));
|
||||||
assert_eq!(slash_command_specs().len(), 140);
|
assert_eq!(slash_command_specs().len(), 140);
|
||||||
|
|
@ -6057,6 +6058,9 @@ mod tests {
|
||||||
=======
|
=======
|
||||||
assert_eq!(slash_command_specs().len(), 141);
|
assert_eq!(slash_command_specs().len(), 141);
|
||||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||||
|
=======
|
||||||
|
assert_eq!(slash_command_specs().len(), 140);
|
||||||
|
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
|
||||||
assert!(resume_supported_slash_commands().len() >= 39);
|
assert!(resume_supported_slash_commands().len() >= 39);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! LSP action dispatch: routes actions to the appropriate server process.
|
//! LSP action dispatch: routes actions to the appropriate server process.
|
||||||
|
|
||||||
use super::types::{LspAction, LspServerStatus};
|
use super::types::{LspAction, LspServerStatus};
|
||||||
|
use crate::lsp_process::LspProcessError;
|
||||||
|
|
||||||
impl super::LspRegistry {
|
impl super::LspRegistry {
|
||||||
/// Dispatch an LSP action and return a structured result.
|
/// Dispatch an LSP action and return a structured result.
|
||||||
|
|
@ -42,9 +43,18 @@ impl super::LspRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other actions, we need a connected server for the given file
|
// For other actions, we need a connected server for the given file
|
||||||
let path = path.ok_or("path is required for this LSP action")?;
|
// (workspace_symbols operates without a specific file path)
|
||||||
let language = Self::language_for_path(path)
|
let language = if lsp_action == LspAction::WorkspaceSymbols {
|
||||||
.ok_or_else(|| format!("no LSP server available for path: {path}"))?;
|
// Try to find any connected server for workspace symbols
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
inner.servers.keys().next().cloned()
|
||||||
|
.ok_or_else(|| "no LSP servers available for workspace symbols".to_owned())?
|
||||||
|
} else {
|
||||||
|
let p = path.ok_or("path is required for this LSP action")?;
|
||||||
|
Self::language_for_path(p)
|
||||||
|
.ok_or_else(|| format!("no LSP server available for path: {p}"))?
|
||||||
|
};
|
||||||
|
let path = path.unwrap_or("");
|
||||||
|
|
||||||
// Check the entry exists
|
// Check the entry exists
|
||||||
{
|
{
|
||||||
|
|
@ -54,6 +64,23 @@ impl super::LspRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the server is already in a non-starting state
|
||||||
|
{
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
if let Some(entry) = inner.servers.get(&language) {
|
||||||
|
if entry.state.status == LspServerStatus::Disconnected
|
||||||
|
|| entry.state.status == LspServerStatus::Error
|
||||||
|
{
|
||||||
|
if entry.process.is_none() {
|
||||||
|
return Err(format!(
|
||||||
|
"LSP server for '{}' is not connected (status: {})",
|
||||||
|
language, entry.state.status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lazy-start: if no process yet, try to start one
|
// Lazy-start: if no process yet, try to start one
|
||||||
let needs_start = {
|
let needs_start = {
|
||||||
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
|
@ -214,6 +241,80 @@ impl super::LspRegistry {
|
||||||
"edits": text_edits,
|
"edits": text_edits,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
LspAction::CodeAction => {
|
||||||
|
let end_line = if line > 0 { Some(line) } else { None };
|
||||||
|
let end_character = if character > 0 { Some(character) } else { None };
|
||||||
|
let actions = process.code_action(path, line, character, end_line, end_character, None).await;
|
||||||
|
actions.map(|acts| serde_json::json!({
|
||||||
|
"action": "code_action",
|
||||||
|
"path": path,
|
||||||
|
"line": 0,
|
||||||
|
"character": 0,
|
||||||
|
"end_line": end_line,
|
||||||
|
"end_character": end_character,
|
||||||
|
"language": language,
|
||||||
|
"status": "ok",
|
||||||
|
"actions": acts,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
LspAction::Rename => {
|
||||||
|
let new_name = _query.ok_or_else(|| LspProcessError::InvalidRequest("new_name required for rename".into()))?;
|
||||||
|
let rename_result = process.rename(path, line, character, new_name).await;
|
||||||
|
rename_result.map(|r| serde_json::json!({
|
||||||
|
"action": "rename",
|
||||||
|
"path": path,
|
||||||
|
"line": line,
|
||||||
|
"character": character,
|
||||||
|
"language": language,
|
||||||
|
"status": "ok",
|
||||||
|
"result": r,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
LspAction::SignatureHelp => {
|
||||||
|
let sig = process.signature_help(path, line, character).await;
|
||||||
|
sig.map(|opt| {
|
||||||
|
opt.map_or_else(
|
||||||
|
|| serde_json::json!({
|
||||||
|
"action": "signature_help",
|
||||||
|
"path": path,
|
||||||
|
"line": line,
|
||||||
|
"character": character,
|
||||||
|
"language": language,
|
||||||
|
"status": "no_result",
|
||||||
|
}),
|
||||||
|
|s| serde_json::json!({
|
||||||
|
"action": "signature_help",
|
||||||
|
"path": path,
|
||||||
|
"line": line,
|
||||||
|
"character": character,
|
||||||
|
"language": language,
|
||||||
|
"status": "ok",
|
||||||
|
"result": s,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LspAction::CodeLens => {
|
||||||
|
let lenses = process.code_lens(path).await;
|
||||||
|
lenses.map(|l| serde_json::json!({
|
||||||
|
"action": "code_lens",
|
||||||
|
"path": path,
|
||||||
|
"language": language,
|
||||||
|
"status": "ok",
|
||||||
|
"lenses": l,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
LspAction::WorkspaceSymbols => {
|
||||||
|
let query = _query.unwrap_or("");
|
||||||
|
let symbols = process.workspace_symbols(query).await;
|
||||||
|
symbols.map(|syms| serde_json::json!({
|
||||||
|
"action": "workspace_symbols",
|
||||||
|
"language": language,
|
||||||
|
"query": query,
|
||||||
|
"status": "ok",
|
||||||
|
"symbols": syms,
|
||||||
|
}))
|
||||||
|
}
|
||||||
LspAction::Diagnostics => unreachable!(),
|
LspAction::Diagnostics => unreachable!(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ mod tests;
|
||||||
mod tests_lifecycle;
|
mod tests_lifecycle;
|
||||||
|
|
||||||
pub use types::{
|
pub use types::{
|
||||||
LspAction, LspCompletionItem, LspDiagnostic, LspHoverResult, LspLocation, LspServerState,
|
LspAction, LspCodeAction, LspCodeLens, LspCommand, LspCompletionItem, LspDiagnostic,
|
||||||
LspServerStatus, LspSymbol,
|
LspFileEdit, LspHoverResult, LspLocation, LspParameterInfo, LspRenameResult,
|
||||||
|
LspServerState, LspServerStatus, LspSignatureHelpResult, LspSignatureInformation,
|
||||||
|
LspSymbol, LspTextEdit, LspWorkspaceEdit,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
@ -144,6 +146,12 @@ impl LspRegistry {
|
||||||
"cpp" | "hpp" | "cc" => "cpp",
|
"cpp" | "hpp" | "cc" => "cpp",
|
||||||
"rb" => "ruby",
|
"rb" => "ruby",
|
||||||
"lua" => "lua",
|
"lua" => "lua",
|
||||||
|
"html" | "htm" => "html",
|
||||||
|
"css" | "scss" | "less" | "sass" => "css",
|
||||||
|
"json" | "jsonc" => "json",
|
||||||
|
"sh" | "bash" | "zsh" => "bash",
|
||||||
|
"yaml" | "yml" => "yaml",
|
||||||
|
"gd" => "gdscript",
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -167,6 +175,12 @@ impl LspRegistry {
|
||||||
"cpp" | "hpp" | "cc" => "cpp",
|
"cpp" | "hpp" | "cc" => "cpp",
|
||||||
"rb" => "ruby",
|
"rb" => "ruby",
|
||||||
"lua" => "lua",
|
"lua" => "lua",
|
||||||
|
"html" | "htm" => "html",
|
||||||
|
"css" | "scss" | "less" | "sass" => "css",
|
||||||
|
"json" | "jsonc" => "json",
|
||||||
|
"sh" | "bash" | "zsh" => "bash",
|
||||||
|
"yaml" | "yml" => "yaml",
|
||||||
|
"gd" => "gdscript",
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -433,6 +447,39 @@ impl LspRegistry {
|
||||||
diagnostics
|
diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Notify the LSP server that a file was closed.
|
||||||
|
/// Best-effort: returns empty vec if no server is available.
|
||||||
|
pub fn notify_file_close(&self, path: &str) -> Vec<LspDiagnostic> {
|
||||||
|
let Some(language) = Self::language_for_path(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let process_arc = {
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
match inner.servers.get(&language).and_then(|e| e.process.clone()) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(mut process) = process_arc.lock() {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build();
|
||||||
|
if let Ok(rt) = rt {
|
||||||
|
let _ = rt.block_on(process.did_close(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark file as closed
|
||||||
|
{
|
||||||
|
let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
inner.open_files.remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
/// Fetch diagnostics for a file by draining pending server notifications
|
/// Fetch diagnostics for a file by draining pending server notifications
|
||||||
/// and returning cached diagnostics.
|
/// and returning cached diagnostics.
|
||||||
pub fn fetch_diagnostics_for_file(&self, path: &str) -> Vec<LspDiagnostic> {
|
pub fn fetch_diagnostics_for_file(&self, path: &str) -> Vec<LspDiagnostic> {
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ fn register_with_descriptor_stores_entry() {
|
||||||
command: "rust-analyzer".into(),
|
command: "rust-analyzer".into(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
extensions: vec!["rs".into()],
|
extensions: vec!["rs".into()],
|
||||||
|
install_hint: vec![],
|
||||||
};
|
};
|
||||||
registry.register_with_descriptor(
|
registry.register_with_descriptor(
|
||||||
"rust",
|
"rust",
|
||||||
|
|
@ -238,6 +239,7 @@ fn dispatch_hover_lazy_starts_server() {
|
||||||
command: "rust-analyzer".into(),
|
command: "rust-analyzer".into(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
extensions: vec!["rs".into()],
|
extensions: vec!["rs".into()],
|
||||||
|
install_hint: vec![],
|
||||||
};
|
};
|
||||||
registry.register_with_descriptor(
|
registry.register_with_descriptor(
|
||||||
"rust",
|
"rust",
|
||||||
|
|
@ -271,6 +273,7 @@ fn start_and_stop_server() {
|
||||||
command: "rust-analyzer".into(),
|
command: "rust-analyzer".into(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
extensions: vec!["rs".into()],
|
extensions: vec!["rs".into()],
|
||||||
|
install_hint: vec![],
|
||||||
};
|
};
|
||||||
registry.register_with_descriptor(
|
registry.register_with_descriptor(
|
||||||
"rust",
|
"rust",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! LSP type definitions: action enums, diagnostic/location types, server status.
|
//! LSP type definitions: action enums, diagnostic/location types, server status,
|
||||||
|
//! and structured results for all supported LSP features.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -13,6 +14,11 @@ pub enum LspAction {
|
||||||
Completion,
|
Completion,
|
||||||
Symbols,
|
Symbols,
|
||||||
Format,
|
Format,
|
||||||
|
CodeAction,
|
||||||
|
Rename,
|
||||||
|
SignatureHelp,
|
||||||
|
CodeLens,
|
||||||
|
WorkspaceSymbols,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LspAction {
|
impl LspAction {
|
||||||
|
|
@ -25,6 +31,11 @@ impl LspAction {
|
||||||
"completion" | "completions" => Some(Self::Completion),
|
"completion" | "completions" => Some(Self::Completion),
|
||||||
"symbols" | "document_symbols" => Some(Self::Symbols),
|
"symbols" | "document_symbols" => Some(Self::Symbols),
|
||||||
"format" | "formatting" => Some(Self::Format),
|
"format" | "formatting" => Some(Self::Format),
|
||||||
|
"code_action" | "codeaction" => Some(Self::CodeAction),
|
||||||
|
"rename" => Some(Self::Rename),
|
||||||
|
"signature_help" | "signatures" => Some(Self::SignatureHelp),
|
||||||
|
"code_lens" | "codelens" => Some(Self::CodeLens),
|
||||||
|
"workspace_symbols" => Some(Self::WorkspaceSymbols),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +84,87 @@ pub struct LspSymbol {
|
||||||
pub character: u32,
|
pub character: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A code action (quick fix, refactor, etc.) returned by the server.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspCodeAction {
|
||||||
|
pub title: String,
|
||||||
|
pub kind: Option<String>,
|
||||||
|
pub is_preferred: bool,
|
||||||
|
pub edit: Option<LspWorkspaceEdit>,
|
||||||
|
pub command: Option<LspCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A workspace edit containing multiple file changes.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspWorkspaceEdit {
|
||||||
|
pub changes: Vec<LspFileEdit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edits to a single file.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspFileEdit {
|
||||||
|
pub path: String,
|
||||||
|
pub edits: Vec<LspTextEdit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single text edit operation.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspTextEdit {
|
||||||
|
pub new_text: String,
|
||||||
|
pub start_line: u32,
|
||||||
|
pub start_character: u32,
|
||||||
|
pub end_line: u32,
|
||||||
|
pub end_character: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A command that the server requests the client to execute.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspCommand {
|
||||||
|
pub title: String,
|
||||||
|
pub command: String,
|
||||||
|
pub arguments: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a rename operation.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspRenameResult {
|
||||||
|
pub new_name: String,
|
||||||
|
pub edit: Option<LspWorkspaceEdit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single parameter in a function signature.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspParameterInfo {
|
||||||
|
pub label: String,
|
||||||
|
pub documentation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A function signature with its parameters.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspSignatureInformation {
|
||||||
|
pub label: String,
|
||||||
|
pub documentation: Option<String>,
|
||||||
|
pub parameters: Vec<LspParameterInfo>,
|
||||||
|
pub active_parameter: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a signature help request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspSignatureHelpResult {
|
||||||
|
pub signatures: Vec<LspSignatureInformation>,
|
||||||
|
pub active_signature: Option<u32>,
|
||||||
|
pub active_parameter: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A code lens item — an actionable hint inline in the editor.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LspCodeLens {
|
||||||
|
pub line: u32,
|
||||||
|
pub character: u32,
|
||||||
|
pub command: Option<LspCommand>,
|
||||||
|
pub data: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum LspServerStatus {
|
pub enum LspServerStatus {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,56 @@
|
||||||
//! Auto-discovery of installed LSP servers and file-extension mapping.
|
//! Auto-discovery of installed LSP servers, file-extension mapping, and
|
||||||
|
//! distro-aware install prompting.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
/// Descriptor for a well-known LSP server, including its launch command and
|
/// Descriptor for a well-known LSP server, including its launch command,
|
||||||
/// the file extensions it handles.
|
/// the file extensions it handles, and how to install it.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LspServerDescriptor {
|
pub struct LspServerDescriptor {
|
||||||
pub language: String,
|
pub language: String,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
pub extensions: Vec<String>,
|
pub extensions: Vec<String>,
|
||||||
|
pub install_hint: Vec<InstallInstruction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Static descriptor used by the [`KNOWN_LSP_SERVERS`] constant. Uses
|
/// A single install command for a specific package manager or platform.
|
||||||
/// `&'static str` fields so the table can live in read-only memory.
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct InstallInstruction {
|
||||||
|
pub label: String,
|
||||||
|
pub command: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the caller should do when a server is missing.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LspInstallAction {
|
||||||
|
/// The server is already available.
|
||||||
|
Installed,
|
||||||
|
/// The server is not found; these are the suggested install commands.
|
||||||
|
Missing { language: String, instructions: Vec<InstallInstruction> },
|
||||||
|
/// The server binary exists but is a rustup proxy stub for an uninstalled component.
|
||||||
|
RustupProxyMissing { language: String, component: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect the current Linux distribution (or non-Linux).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LinuxDistro {
|
||||||
|
Debian,
|
||||||
|
Ubuntu,
|
||||||
|
Fedora,
|
||||||
|
Arch,
|
||||||
|
OpenSuse,
|
||||||
|
Alpine,
|
||||||
|
Void,
|
||||||
|
NixOS,
|
||||||
|
UnknownLinux,
|
||||||
|
MacOS,
|
||||||
|
Windows,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static descriptor used by the [`KNOWN_LSP_SERVERS_TABLE`] constant.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
struct StaticLspServerDescriptor {
|
struct StaticLspServerDescriptor {
|
||||||
language: &'static str,
|
language: &'static str,
|
||||||
|
|
@ -31,6 +67,7 @@ impl StaticLspServerDescriptor {
|
||||||
command: self.command.to_string(),
|
command: self.command.to_string(),
|
||||||
args: self.args.iter().map(|s| (*s).to_string()).collect(),
|
args: self.args.iter().map(|s| (*s).to_string()).collect(),
|
||||||
extensions: self.extensions.iter().map(|s| (*s).to_string()).collect(),
|
extensions: self.extensions.iter().map(|s| (*s).to_string()).collect(),
|
||||||
|
install_hint: install_instructions_for(self.language),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,8 +122,138 @@ const KNOWN_LSP_SERVERS_TABLE: &[StaticLspServerDescriptor] = &[
|
||||||
args: &[],
|
args: &[],
|
||||||
extensions: &["lua"],
|
extensions: &["lua"],
|
||||||
},
|
},
|
||||||
|
StaticLspServerDescriptor {
|
||||||
|
language: "html",
|
||||||
|
command: "vscode-html-language-server",
|
||||||
|
args: &["--stdio"],
|
||||||
|
extensions: &["html", "htm"],
|
||||||
|
},
|
||||||
|
StaticLspServerDescriptor {
|
||||||
|
language: "css",
|
||||||
|
command: "vscode-css-language-server",
|
||||||
|
args: &["--stdio"],
|
||||||
|
extensions: &["css", "scss", "less", "sass"],
|
||||||
|
},
|
||||||
|
StaticLspServerDescriptor {
|
||||||
|
language: "json",
|
||||||
|
command: "vscode-json-language-server",
|
||||||
|
args: &["--stdio"],
|
||||||
|
extensions: &["json", "jsonc"],
|
||||||
|
},
|
||||||
|
StaticLspServerDescriptor {
|
||||||
|
language: "bash",
|
||||||
|
command: "bash-language-server",
|
||||||
|
args: &["start"],
|
||||||
|
extensions: &["sh", "bash", "zsh"],
|
||||||
|
},
|
||||||
|
StaticLspServerDescriptor {
|
||||||
|
language: "yaml",
|
||||||
|
command: "yaml-language-server",
|
||||||
|
args: &["--stdio"],
|
||||||
|
extensions: &["yaml", "yml"],
|
||||||
|
},
|
||||||
|
StaticLspServerDescriptor {
|
||||||
|
language: "gdscript",
|
||||||
|
command: "godot",
|
||||||
|
args: &["--headless", "--editor"],
|
||||||
|
extensions: &["gd"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Return install instructions for a known language server, covering all
|
||||||
|
/// common distros and package managers. Order doesn't matter — the caller
|
||||||
|
/// picks the one matching the current system.
|
||||||
|
fn install_instructions_for(language: &str) -> Vec<InstallInstruction> {
|
||||||
|
match language {
|
||||||
|
"rust" => vec![
|
||||||
|
InstallInstruction { label: "rustup".into(), command: "rustup component add rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "Ubuntu/Debian".into(), command: "sudo apt install rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "Fedora".into(), command: "sudo dnf install rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "openSUSE".into(), command: "sudo zypper install rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "Alpine".into(), command: "sudo apk add rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "Void".into(), command: "sudo xbps-install rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install rust-analyzer".into() },
|
||||||
|
InstallInstruction { label: "pip".into(), command: "pip install rust-analyzer".into() },
|
||||||
|
],
|
||||||
|
"c/cpp" => vec![
|
||||||
|
InstallInstruction { label: "Ubuntu/Debian".into(), command: "sudo apt install clangd".into() },
|
||||||
|
InstallInstruction { label: "Fedora".into(), command: "sudo dnf install clang-tools-extra".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S clang".into() },
|
||||||
|
InstallInstruction { label: "openSUSE".into(), command: "sudo zypper install clang-tools".into() },
|
||||||
|
InstallInstruction { label: "Alpine".into(), command: "sudo apk add clang-extra-tools".into() },
|
||||||
|
InstallInstruction { label: "Void".into(), command: "sudo xbps-install clang-tools-extra".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.clang-tools".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install llvm".into() },
|
||||||
|
],
|
||||||
|
"python" => vec![
|
||||||
|
InstallInstruction { label: "npm".into(), command: "npm install -g pyright".into() },
|
||||||
|
InstallInstruction { label: "pip".into(), command: "pip install pyright".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S pyright".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.pyright".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install pyright".into() },
|
||||||
|
],
|
||||||
|
"go" => vec![
|
||||||
|
InstallInstruction { label: "go".into(), command: "go install golang.org/x/tools/gopls@latest".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S gopls".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.gopls".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install gopls".into() },
|
||||||
|
],
|
||||||
|
"typescript" => vec![
|
||||||
|
InstallInstruction { label: "npm".into(), command: "npm install -g typescript-language-server typescript".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S typescript-language-server".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.typescript-language-server".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install typescript-language-server".into() },
|
||||||
|
],
|
||||||
|
"java" => vec![
|
||||||
|
InstallInstruction { label: "Ubuntu/Debian".into(), command: "sudo apt install eclipse-jdtls".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S jdtls".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.eclipse-jdtls".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install jdtls".into() },
|
||||||
|
],
|
||||||
|
"ruby" => vec![
|
||||||
|
InstallInstruction { label: "gem".into(), command: "gem install solargraph".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S solargraph".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.solargraph".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install solargraph".into() },
|
||||||
|
],
|
||||||
|
"lua" => vec![
|
||||||
|
InstallInstruction { label: "npm".into(), command: "npm install -g lua-language-server".into() },
|
||||||
|
InstallInstruction { label: "Ubuntu/Debian".into(), command: "sudo apt install lua-language-server".into() },
|
||||||
|
InstallInstruction { label: "Fedora".into(), command: "sudo dnf install lua-language-server".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S lua-language-server".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.lua-language-server".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install lua-language-server".into() },
|
||||||
|
],
|
||||||
|
"html" | "css" | "json" => vec![
|
||||||
|
InstallInstruction { label: "npm".into(), command: "npm install -g vscode-langservers-extracted".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S vscode-langservers-extracted".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.vscode-langservers-extracted".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install vscode-langservers-extracted".into() },
|
||||||
|
],
|
||||||
|
"bash" => vec![
|
||||||
|
InstallInstruction { label: "npm".into(), command: "npm install -g bash-language-server".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S bash-language-server".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.bash-language-server".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install bash-language-server".into() },
|
||||||
|
],
|
||||||
|
"yaml" => vec![
|
||||||
|
InstallInstruction { label: "npm".into(), command: "npm install -g yaml-language-server".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S yaml-language-server".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.yaml-language-server".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install yaml-language-server".into() },
|
||||||
|
],
|
||||||
|
"gdscript" => vec![
|
||||||
|
InstallInstruction { label: "Godot Editor".into(), command: "Download from https://godotengine.org".into() },
|
||||||
|
InstallInstruction { label: "Arch".into(), command: "sudo pacman -S godot".into() },
|
||||||
|
InstallInstruction { label: "NixOS".into(), command: "nix-env -iA nixpkgs.godot".into() },
|
||||||
|
InstallInstruction { label: "macOS".into(), command: "brew install godot".into() },
|
||||||
|
],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Owned copy of the known LSP server descriptors, useful when callers need
|
/// Owned copy of the known LSP server descriptors, useful when callers need
|
||||||
/// to mutate or transfer ownership.
|
/// to mutate or transfer ownership.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -101,12 +268,15 @@ pub fn known_lsp_servers() -> Vec<LspServerDescriptor> {
|
||||||
/// with `--version`. Returns `true` if the command could be spawned
|
/// with `--version`. Returns `true` if the command could be spawned
|
||||||
/// successfully, `false` otherwise.
|
/// successfully, `false` otherwise.
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
///
|
///
|
||||||
/// Some LSP servers (like rust-analyzer via rustup) exit non-zero on --version
|
/// Some LSP servers (like rust-analyzer via rustup) exit non-zero on --version
|
||||||
/// but are still functional. We treat "spawned successfully" as found, regardless
|
/// but are still functional. We treat "spawned successfully" as found, regardless
|
||||||
/// of the exit code. Only a failure to spawn (command not found) returns false.
|
/// of the exit code. Only a failure to spawn (command not found) returns false.
|
||||||
=======
|
=======
|
||||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||||
|
=======
|
||||||
|
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn command_exists_on_path(command: &str) -> bool {
|
pub fn command_exists_on_path(command: &str) -> bool {
|
||||||
Command::new(command)
|
Command::new(command)
|
||||||
|
|
@ -124,7 +294,6 @@ fn is_rustup_proxy(command: &str) -> bool {
|
||||||
let Ok(output) = Command::new(command).arg("--version").output() else {
|
let Ok(output) = Command::new(command).arg("--version").output() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
// rustup proxy exits non-zero and prints "error: Unknown binary '...' in official toolchain"
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
stderr.contains("Unknown binary")
|
stderr.contains("Unknown binary")
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +319,159 @@ fn rustup_component_works(component: &str) -> bool {
|
||||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect the current platform/distro for install suggestion filtering.
|
||||||
|
#[must_use]
|
||||||
|
pub fn detect_platform() -> LinuxDistro {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
return LinuxDistro::MacOS;
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return LinuxDistro::Windows;
|
||||||
|
}
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
|
return LinuxDistro::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = std::fs::read_to_string("/etc/os-release").unwrap_or_default();
|
||||||
|
|
||||||
|
if contents.contains("Ubuntu") {
|
||||||
|
LinuxDistro::Ubuntu
|
||||||
|
} else if contents.contains("Debian") {
|
||||||
|
LinuxDistro::Debian
|
||||||
|
} else if contents.contains("Fedora") {
|
||||||
|
LinuxDistro::Fedora
|
||||||
|
} else if contents.contains("Arch") || contents.contains("archlinux") || contents.contains("Manjaro") || contents.contains("EndeavourOS") {
|
||||||
|
LinuxDistro::Arch
|
||||||
|
} else if contents.contains("openSUSE") || contents.contains("SUSE") {
|
||||||
|
LinuxDistro::OpenSuse
|
||||||
|
} else if contents.contains("Alpine") {
|
||||||
|
LinuxDistro::Alpine
|
||||||
|
} else if contents.contains("Void") {
|
||||||
|
LinuxDistro::Void
|
||||||
|
} else if contents.contains("NixOS") {
|
||||||
|
LinuxDistro::NixOS
|
||||||
|
} else {
|
||||||
|
LinuxDistro::UnknownLinux
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the best install instruction for a language given the current platform.
|
||||||
|
/// Returns `None` if no instructions are known for this language.
|
||||||
|
#[must_use]
|
||||||
|
pub fn best_install_instruction(language: &str) -> Option<InstallInstruction> {
|
||||||
|
let distro = detect_platform();
|
||||||
|
let instructions = install_instructions_for(language);
|
||||||
|
if instructions.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let label_match = match distro {
|
||||||
|
LinuxDistro::Ubuntu | LinuxDistro::Debian => "Ubuntu/Debian",
|
||||||
|
LinuxDistro::Fedora => "Fedora",
|
||||||
|
LinuxDistro::Arch => "Arch",
|
||||||
|
LinuxDistro::OpenSuse => "openSUSE",
|
||||||
|
LinuxDistro::Alpine => "Alpine",
|
||||||
|
LinuxDistro::Void => "Void",
|
||||||
|
LinuxDistro::NixOS => "NixOS",
|
||||||
|
LinuxDistro::MacOS => "macOS",
|
||||||
|
LinuxDistro::Windows | LinuxDistro::UnknownLinux | LinuxDistro::Other => {
|
||||||
|
instructions.first().map(|i| i.label.as_str()).unwrap_or("")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
instructions
|
||||||
|
.iter()
|
||||||
|
.find(|i| i.label == label_match)
|
||||||
|
.or_else(|| instructions.first())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check which known LSP servers are missing and produce install suggestions.
|
||||||
|
/// Returns a list of `LspInstallAction` for every known language: installed,
|
||||||
|
/// missing, or rustup-proxy-missing.
|
||||||
|
#[must_use]
|
||||||
|
pub fn check_lsp_availability() -> Vec<LspInstallAction> {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
|
for desc in KNOWN_LSP_SERVERS_TABLE {
|
||||||
|
if !command_exists_on_path(desc.command) {
|
||||||
|
actions.push(LspInstallAction::Missing {
|
||||||
|
language: desc.language.to_string(),
|
||||||
|
instructions: install_instructions_for(desc.language),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc.command == "rust-analyzer" && is_rustup_proxy("rust-analyzer") {
|
||||||
|
if rustup_component_works("rust-analyzer") {
|
||||||
|
actions.push(LspInstallAction::Installed);
|
||||||
|
} else {
|
||||||
|
actions.push(LspInstallAction::RustupProxyMissing {
|
||||||
|
language: desc.language.to_string(),
|
||||||
|
component: "rust-analyzer".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push(LspInstallAction::Installed);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a human-readable install prompt for missing LSP servers.
|
||||||
|
#[must_use]
|
||||||
|
pub fn format_install_prompt(actions: &[LspInstallAction]) -> String {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let distro = detect_platform();
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
match action {
|
||||||
|
LspInstallAction::Installed => continue,
|
||||||
|
LspInstallAction::Missing { language, instructions } => {
|
||||||
|
lines.push(format!(" {language}: not found"));
|
||||||
|
let best = instructions
|
||||||
|
.iter()
|
||||||
|
.find(|i| match distro {
|
||||||
|
LinuxDistro::Ubuntu | LinuxDistro::Debian => i.label == "Ubuntu/Debian",
|
||||||
|
LinuxDistro::Fedora => i.label == "Fedora",
|
||||||
|
LinuxDistro::Arch => i.label == "Arch",
|
||||||
|
LinuxDistro::OpenSuse => i.label == "openSUSE",
|
||||||
|
LinuxDistro::Alpine => i.label == "Alpine",
|
||||||
|
LinuxDistro::Void => i.label == "Void",
|
||||||
|
LinuxDistro::NixOS => i.label == "NixOS",
|
||||||
|
LinuxDistro::MacOS => i.label == "macOS",
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.or_else(|| instructions.first());
|
||||||
|
if let Some(inst) = best {
|
||||||
|
lines.push(format!(" → {}", inst.command));
|
||||||
|
}
|
||||||
|
for inst in instructions {
|
||||||
|
if Some(inst) != best {
|
||||||
|
lines.push(format!(" • {} ({})", inst.command, inst.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LspInstallAction::RustupProxyMissing { language, component } => {
|
||||||
|
lines.push(format!(" {language}: rustup proxy found but component not installed"));
|
||||||
|
lines.push(format!(" → rustup component add {component}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = "LSP servers missing — install for code intelligence:\n".to_string();
|
||||||
|
out.push_str(&lines.join("\n"));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Discover LSP servers that are actually installed on the current system.
|
/// Discover LSP servers that are actually installed on the current system.
|
||||||
|
<<<<<<< HEAD
|
||||||
///
|
///
|
||||||
/// Iterates over the known server table and returns only those whose command
|
/// Iterates over the known server table and returns only those whose command
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
|
@ -168,6 +489,8 @@ fn rustup_component_works(component: &str) -> bool {
|
||||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||||
=======
|
=======
|
||||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||||
|
=======
|
||||||
|
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
||||||
KNOWN_LSP_SERVERS_TABLE
|
KNOWN_LSP_SERVERS_TABLE
|
||||||
|
|
@ -179,12 +502,7 @@ pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
||||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||||
.filter_map(|desc| {
|
.filter_map(|desc| {
|
||||||
let mut server = desc.to_descriptor();
|
let mut server = desc.to_descriptor();
|
||||||
// rustup ships a proxy `rust-analyzer` that exists on PATH but
|
|
||||||
// errors with "Unknown binary" when the component isn't installed.
|
|
||||||
if desc.command == "rust-analyzer" && is_rustup_proxy("rust-analyzer") {
|
if desc.command == "rust-analyzer" && is_rustup_proxy("rust-analyzer") {
|
||||||
// The component isn't installed under this toolchain.
|
|
||||||
// Check if `rustup run stable rust-analyzer --version` works
|
|
||||||
// (e.g. user installed it via rustup but the proxy is misconfigured).
|
|
||||||
if rustup_component_works("rust-analyzer") {
|
if rustup_component_works("rust-analyzer") {
|
||||||
server.command = "rustup".to_string();
|
server.command = "rustup".to_string();
|
||||||
server.args = vec![
|
server.args = vec![
|
||||||
|
|
@ -193,7 +511,6 @@ pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
||||||
"rust-analyzer".to_string(),
|
"rust-analyzer".to_string(),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
// Component truly not installed — skip it.
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,9 +526,6 @@ pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the best-matching LSP server descriptor for a given file path.
|
/// Find the best-matching LSP server descriptor for a given file path.
|
||||||
///
|
|
||||||
/// Matches on the file extension. If multiple servers share the same
|
|
||||||
/// extension, the first match wins.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn find_server_for_file<'a>(
|
pub fn find_server_for_file<'a>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
|
@ -285,7 +599,6 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn discover_returns_only_installed_servers() {
|
fn discover_returns_only_installed_servers() {
|
||||||
let available = discover_available_servers();
|
let available = discover_available_servers();
|
||||||
// Every returned server must have a command that actually exists on PATH.
|
|
||||||
for server in &available {
|
for server in &available {
|
||||||
assert!(
|
assert!(
|
||||||
command_exists_on_path(&server.command),
|
command_exists_on_path(&server.command),
|
||||||
|
|
@ -294,9 +607,8 @@ mod tests {
|
||||||
server.command,
|
server.command,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// If rust-analyzer or clangd are on this system, they should appear.
|
|
||||||
let languages: Vec<&str> = available.iter().map(|s| s.language.as_str()).collect();
|
let languages: Vec<&str> = available.iter().map(|s| s.language.as_str()).collect();
|
||||||
if command_exists_on_path("rust-analyzer") {
|
if command_exists_on_path("rust-analyzer") && !is_rustup_proxy("rust-analyzer") {
|
||||||
assert!(languages.contains(&"rust"), "rust-analyzer is on PATH but 'rust' not in discovered servers");
|
assert!(languages.contains(&"rust"), "rust-analyzer is on PATH but 'rust' not in discovered servers");
|
||||||
}
|
}
|
||||||
if command_exists_on_path("clangd") {
|
if command_exists_on_path("clangd") {
|
||||||
|
|
@ -328,4 +640,66 @@ mod tests {
|
||||||
let ts = servers.iter().find(|s| s.language == "typescript").expect("typescript server should exist");
|
let ts = servers.iter().find(|s| s.language == "typescript").expect("typescript server should exist");
|
||||||
assert_eq!(ts.args, vec!["--stdio"], "typescript-language-server should have --stdio arg");
|
assert_eq!(ts.args, vec!["--stdio"], "typescript-language-server should have --stdio arg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_instructions_cover_all_languages() {
|
||||||
|
for desc in KNOWN_LSP_SERVERS_TABLE {
|
||||||
|
let instructions = install_instructions_for(desc.language);
|
||||||
|
assert!(!instructions.is_empty(), "no install instructions for '{}'", desc.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn best_install_returns_something_for_known_languages() {
|
||||||
|
for desc in KNOWN_LSP_SERVERS_TABLE {
|
||||||
|
assert!(best_install_instruction(desc.language).is_some(), "no best install for '{}'", desc.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_install_prompt_skips_installed() {
|
||||||
|
let actions = vec![LspInstallAction::Installed];
|
||||||
|
let prompt = format_install_prompt(&actions);
|
||||||
|
assert!(prompt.is_empty(), "should not prompt for installed servers");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_install_prompt_shows_missing() {
|
||||||
|
let actions = vec![LspInstallAction::Missing {
|
||||||
|
language: "rust".into(),
|
||||||
|
instructions: install_instructions_for("rust"),
|
||||||
|
}];
|
||||||
|
let prompt = format_install_prompt(&actions);
|
||||||
|
assert!(prompt.contains("rust"), "should mention rust");
|
||||||
|
assert!(prompt.contains("rustup component add rust-analyzer"), "should show rustup command");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_install_prompt_shows_rustup_proxy_missing() {
|
||||||
|
let actions = vec![LspInstallAction::RustupProxyMissing {
|
||||||
|
language: "rust".into(),
|
||||||
|
component: "rust-analyzer".into(),
|
||||||
|
}];
|
||||||
|
let prompt = format_install_prompt(&actions);
|
||||||
|
assert!(prompt.contains("rustup component add rust-analyzer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_platform_returns_something() {
|
||||||
|
let _ = detect_platform();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_availability_returns_one_per_known_language() {
|
||||||
|
let actions = check_lsp_availability();
|
||||||
|
assert_eq!(actions.len(), KNOWN_LSP_SERVERS_TABLE.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_descriptors_have_install_hints() {
|
||||||
|
let servers = known_lsp_servers();
|
||||||
|
for server in &servers {
|
||||||
|
assert!(!server.install_hint.is_empty(), "server '{}' should have install hints", server.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@ use std::path::Path;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::lsp_client::{
|
use crate::lsp_client::{
|
||||||
LspCompletionItem, LspDiagnostic, LspHoverResult, LspLocation, LspServerStatus, LspSymbol,
|
LspCodeAction, LspCodeLens, LspCompletionItem, LspDiagnostic, LspHoverResult, LspLocation,
|
||||||
|
LspRenameResult, LspServerStatus, LspSignatureHelpResult, LspSymbol,
|
||||||
};
|
};
|
||||||
use crate::lsp_transport::{LspTransport, LspTransportError};
|
use crate::lsp_transport::{LspTransport, LspTransportError};
|
||||||
|
|
||||||
use parse::{
|
use parse::{
|
||||||
canonicalize_root, language_id_for_path, parse_completions, parse_hover, parse_locations,
|
canonicalize_root, language_id_for_path, parse_code_actions, parse_code_lens,
|
||||||
parse_symbols, path_to_uri, severity_name, text_document_position_params, uri_to_path,
|
parse_completions, parse_hover, parse_locations, parse_signature_help,
|
||||||
|
parse_symbols, parse_workspace_edit, parse_workspace_symbols, path_to_uri,
|
||||||
|
rename_params, severity_name, text_document_position_params, uri_to_path,
|
||||||
|
workspace_symbol_params,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -69,6 +73,7 @@ impl LspProcess {
|
||||||
let params = serde_json::json!({
|
let params = serde_json::json!({
|
||||||
"processId": pid,
|
"processId": pid,
|
||||||
"rootUri": root_uri,
|
"rootUri": root_uri,
|
||||||
|
"workspaceFolders": [{ "uri": root_uri, "name": "root" }],
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"textDocument": {
|
"textDocument": {
|
||||||
"hover": { "contentFormat": ["markdown", "plaintext"] },
|
"hover": { "contentFormat": ["markdown", "plaintext"] },
|
||||||
|
|
@ -78,7 +83,30 @@ impl LspProcess {
|
||||||
"completionItem": { "snippetSupport": false }
|
"completionItem": { "snippetSupport": false }
|
||||||
},
|
},
|
||||||
"documentSymbol": { "hierarchicalDocumentSymbolSupport": true },
|
"documentSymbol": { "hierarchicalDocumentSymbolSupport": true },
|
||||||
"publishDiagnostics": { "relatedInformation": true }
|
"publishDiagnostics": { "relatedInformation": true },
|
||||||
|
"codeAction": {
|
||||||
|
"codeActionLiteralSupport": {
|
||||||
|
"codeActionKind": {
|
||||||
|
"valueSet": [
|
||||||
|
"", "quickfix", "refactor", "refactor.extract",
|
||||||
|
"refactor.inline", "refactor.rewrite", "source",
|
||||||
|
"source.organizeImports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rename": { "prepareSupport": true },
|
||||||
|
"signatureHelp": {
|
||||||
|
"signatureInformation": {
|
||||||
|
"documentationFormat": ["markdown", "plaintext"],
|
||||||
|
"parameterInformation": { "labelOffsetSupport": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codeLens": {}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"symbol": {},
|
||||||
|
"workspaceFolders": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -341,6 +369,145 @@ impl LspProcess {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Notify the server that a file was closed. Sends `textDocument/didClose`.
|
||||||
|
pub async fn did_close(&mut self, path: &str) -> Result<(), LspProcessError> {
|
||||||
|
if !self.open_files.contains(path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"textDocument": { "uri": uri }
|
||||||
|
});
|
||||||
|
self.transport
|
||||||
|
.send_notification("textDocument/didClose", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
self.open_files.remove(path);
|
||||||
|
self.version_counter.remove(path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request code actions (quick fixes, refactors) for a range in a file.
|
||||||
|
pub async fn code_action(
|
||||||
|
&mut self,
|
||||||
|
path: &str,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
end_line: Option<u32>,
|
||||||
|
end_character: Option<u32>,
|
||||||
|
only_kinds: Option<&[String]>,
|
||||||
|
) -> Result<Vec<LspCodeAction>, LspProcessError> {
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let el = end_line.unwrap_or(line);
|
||||||
|
let ec = end_character.unwrap_or(character);
|
||||||
|
let mut params = serde_json::json!({
|
||||||
|
"textDocument": { "uri": uri },
|
||||||
|
"range": {
|
||||||
|
"start": { "line": line, "character": character },
|
||||||
|
"end": { "line": el, "character": ec }
|
||||||
|
},
|
||||||
|
"context": { "diagnostics": [] }
|
||||||
|
});
|
||||||
|
if let Some(kinds) = only_kinds {
|
||||||
|
params["context"]["only"] = serde_json::json!(kinds);
|
||||||
|
}
|
||||||
|
let response = self
|
||||||
|
.transport
|
||||||
|
.send_request("textDocument/codeAction", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
let result = response
|
||||||
|
.into_result()
|
||||||
|
.map_err(|e| LspProcessError::Transport(LspTransportError::JsonRpc(e)))?;
|
||||||
|
Ok(parse_code_actions(&result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rename a symbol at a position across the workspace.
|
||||||
|
pub async fn rename(
|
||||||
|
&mut self,
|
||||||
|
path: &str,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
new_name: &str,
|
||||||
|
) -> Result<LspRenameResult, LspProcessError> {
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let params = rename_params(&uri, line, character, new_name);
|
||||||
|
let response = self
|
||||||
|
.transport
|
||||||
|
.send_request("textDocument/rename", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
let result = response
|
||||||
|
.into_result()
|
||||||
|
.map_err(|e| LspProcessError::Transport(LspTransportError::JsonRpc(e)))?;
|
||||||
|
let edit = parse_workspace_edit(&result);
|
||||||
|
Ok(LspRenameResult {
|
||||||
|
new_name: new_name.to_owned(),
|
||||||
|
edit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get signature help at a position (function signatures, parameters).
|
||||||
|
pub async fn signature_help(
|
||||||
|
&mut self,
|
||||||
|
path: &str,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
) -> Result<Option<LspSignatureHelpResult>, LspProcessError> {
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let params = text_document_position_params(&uri, line, character);
|
||||||
|
let response = self
|
||||||
|
.transport
|
||||||
|
.send_request("textDocument/signatureHelp", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
let result = response
|
||||||
|
.into_result()
|
||||||
|
.map_err(|e| LspProcessError::Transport(LspTransportError::JsonRpc(e)))?;
|
||||||
|
if result.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(parse_signature_help(&result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get code lens items for a file (actionable inline hints).
|
||||||
|
pub async fn code_lens(&mut self, path: &str) -> Result<Vec<LspCodeLens>, LspProcessError> {
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"textDocument": { "uri": uri }
|
||||||
|
});
|
||||||
|
let response = self
|
||||||
|
.transport
|
||||||
|
.send_request("textDocument/codeLens", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
let result = response
|
||||||
|
.into_result()
|
||||||
|
.map_err(|e| LspProcessError::Transport(LspTransportError::JsonRpc(e)))?;
|
||||||
|
if result.is_null() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
Ok(parse_code_lens(&result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for symbols across the entire workspace.
|
||||||
|
pub async fn workspace_symbols(
|
||||||
|
&mut self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<LspSymbol>, LspProcessError> {
|
||||||
|
let params = workspace_symbol_params(query);
|
||||||
|
let response = self
|
||||||
|
.transport
|
||||||
|
.send_request("workspace/symbol", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
let result = response
|
||||||
|
.into_result()
|
||||||
|
.map_err(|e| LspProcessError::Transport(LspTransportError::JsonRpc(e)))?;
|
||||||
|
Ok(parse_workspace_symbols(&result))
|
||||||
|
}
|
||||||
|
|
||||||
/// Drain queued server notifications and extract `publishDiagnostics`.
|
/// Drain queued server notifications and extract `publishDiagnostics`.
|
||||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
pub fn drain_diagnostics(&mut self) -> Vec<LspDiagnostic> {
|
pub fn drain_diagnostics(&mut self) -> Vec<LspDiagnostic> {
|
||||||
|
|
@ -415,6 +582,7 @@ impl LspProcess {
|
||||||
pub enum LspProcessError {
|
pub enum LspProcessError {
|
||||||
Transport(LspTransportError),
|
Transport(LspTransportError),
|
||||||
InvalidPath(String),
|
InvalidPath(String),
|
||||||
|
InvalidRequest(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for LspProcessError {
|
impl std::fmt::Display for LspProcessError {
|
||||||
|
|
@ -422,6 +590,7 @@ impl std::fmt::Display for LspProcessError {
|
||||||
match self {
|
match self {
|
||||||
Self::Transport(e) => write!(f, "LSP transport error: {e}"),
|
Self::Transport(e) => write!(f, "LSP transport error: {e}"),
|
||||||
Self::InvalidPath(p) => write!(f, "invalid path: {p}"),
|
Self::InvalidPath(p) => write!(f, "invalid path: {p}"),
|
||||||
|
Self::InvalidRequest(msg) => write!(f, "invalid request: {msg}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +599,7 @@ impl std::error::Error for LspProcessError {
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
Self::Transport(e) => Some(e),
|
Self::Transport(e) => Some(e),
|
||||||
Self::InvalidPath(_) => None,
|
Self::InvalidPath(_) | Self::InvalidRequest(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,3 +309,140 @@ pub(super) fn completion_kind_name(kind: u64) -> String {
|
||||||
_ => format!("Unknown({kind})"),
|
_ => format!("Unknown({kind})"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
pub(super) fn parse_code_actions(value: &JsonValue) -> Vec<crate::lsp_client::LspCodeAction> {
|
||||||
|
let Some(items) = value.as_array() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
items.iter().filter_map(|item| {
|
||||||
|
// Code actions can be Command or CodeAction objects; we only parse CodeAction
|
||||||
|
let title = item.get("title")?.as_str()?.to_owned();
|
||||||
|
let kind = item.get("kind").and_then(JsonValue::as_str).map(str::to_owned);
|
||||||
|
let is_preferred = item.get("isPreferred").and_then(JsonValue::as_bool).unwrap_or(false);
|
||||||
|
let edit = item.get("edit").and_then(|e| parse_workspace_edit(e));
|
||||||
|
let command = item.get("command").and_then(parse_command);
|
||||||
|
Some(crate::lsp_client::LspCodeAction { title, kind, is_preferred, edit, command })
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_workspace_edit(value: &JsonValue) -> Option<crate::lsp_client::LspWorkspaceEdit> {
|
||||||
|
let changes = if let Some(changes_map) = value.get("changes").and_then(JsonValue::as_object) {
|
||||||
|
changes_map.iter().filter_map(|(uri, edits)| {
|
||||||
|
let path = uri_to_path(uri);
|
||||||
|
let edit_list = edits.as_array()?;
|
||||||
|
let text_edits: Vec<crate::lsp_client::LspTextEdit> = edit_list.iter().filter_map(|e| {
|
||||||
|
let new_text = e.get("newText")?.as_str()?.to_owned();
|
||||||
|
let range = e.get("range")?;
|
||||||
|
let start = range.get("start")?;
|
||||||
|
let end = range.get("end")?;
|
||||||
|
Some(crate::lsp_client::LspTextEdit {
|
||||||
|
new_text,
|
||||||
|
start_line: start.get("line")?.as_u64()? as u32,
|
||||||
|
start_character: start.get("character")?.as_u64()? as u32,
|
||||||
|
end_line: end.get("line")?.as_u64()? as u32,
|
||||||
|
end_character: end.get("character")?.as_u64()? as u32,
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
if text_edits.is_empty() { None } else { Some(crate::lsp_client::LspFileEdit { path, edits: text_edits }) }
|
||||||
|
}).collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
if changes.is_empty() { None } else { Some(crate::lsp_client::LspWorkspaceEdit { changes }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_command(value: &JsonValue) -> Option<crate::lsp_client::LspCommand> {
|
||||||
|
let title = value.get("title")?.as_str()?.to_owned();
|
||||||
|
let command = value.get("command")?.as_str()?.to_owned();
|
||||||
|
let arguments = value.get("arguments")
|
||||||
|
.and_then(JsonValue::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some(crate::lsp_client::LspCommand { title, command, arguments })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
pub(super) fn parse_signature_help(value: &JsonValue) -> Option<crate::lsp_client::LspSignatureHelpResult> {
|
||||||
|
let signatures_arr = value.get("signatures")?.as_array()?;
|
||||||
|
let signatures: Vec<crate::lsp_client::LspSignatureInformation> = signatures_arr.iter().filter_map(|sig| {
|
||||||
|
let label = sig.get("label")?.as_str()?.to_owned();
|
||||||
|
let documentation = sig.get("documentation")
|
||||||
|
.and_then(|d| d.get("value").and_then(JsonValue::as_str).or_else(|| d.as_str()))
|
||||||
|
.map(str::to_owned);
|
||||||
|
let parameters = sig.get("parameters").and_then(JsonValue::as_array)
|
||||||
|
.map(|arr| arr.iter().filter_map(|p| {
|
||||||
|
let plabel = p.get("label").and_then(|l| l.as_str().or_else(|| l.get("value").and_then(JsonValue::as_str))).unwrap_or("").to_owned();
|
||||||
|
let pdoc = p.get("documentation")
|
||||||
|
.and_then(|d| d.get("value").and_then(JsonValue::as_str).or_else(|| d.as_str()))
|
||||||
|
.map(str::to_owned);
|
||||||
|
Some(crate::lsp_client::LspParameterInfo { label: plabel, documentation: pdoc })
|
||||||
|
}).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let active_parameter = sig.get("activeParameter").and_then(JsonValue::as_u64).map(|v| v as u32);
|
||||||
|
Some(crate::lsp_client::LspSignatureInformation { label, documentation, parameters, active_parameter })
|
||||||
|
}).collect();
|
||||||
|
let active_signature = value.get("activeSignature").and_then(JsonValue::as_u64).map(|v| v as u32);
|
||||||
|
let active_parameter = value.get("activeParameter").and_then(JsonValue::as_u64).map(|v| v as u32);
|
||||||
|
Some(crate::lsp_client::LspSignatureHelpResult { signatures, active_signature, active_parameter })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
pub(super) fn parse_code_lens(value: &JsonValue) -> Vec<crate::lsp_client::LspCodeLens> {
|
||||||
|
let Some(items) = value.as_array() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
items.iter().filter_map(|item| {
|
||||||
|
let range = item.get("range")?;
|
||||||
|
let start = range.get("start")?;
|
||||||
|
let line = start.get("line")?.as_u64()? as u32;
|
||||||
|
let character = start.get("character")?.as_u64()? as u32;
|
||||||
|
let command = item.get("command").and_then(parse_command);
|
||||||
|
let data = item.get("data").cloned();
|
||||||
|
Some(crate::lsp_client::LspCodeLens { line, character, command, data })
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_workspace_symbols(value: &JsonValue) -> Vec<crate::lsp_client::LspSymbol> {
|
||||||
|
let Some(items) = value.as_array() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
items.iter().filter_map(|item| {
|
||||||
|
let name = item.get("name")?.as_str()?.to_owned();
|
||||||
|
let kind = item.get("kind").and_then(JsonValue::as_u64).map_or_else(|| "Unknown".into(), symbol_kind_name);
|
||||||
|
let path = item.get("location")
|
||||||
|
.and_then(|l| l.get("uri"))
|
||||||
|
.and_then(JsonValue::as_str)
|
||||||
|
.map(uri_to_path)
|
||||||
|
.or_else(|| item.get("uri").and_then(JsonValue::as_str).map(uri_to_path))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let line = item.get("location")
|
||||||
|
.and_then(|l| l.get("range"))
|
||||||
|
.and_then(|r| r.get("start"))
|
||||||
|
.and_then(|s| s.get("line"))
|
||||||
|
.and_then(JsonValue::as_u64)
|
||||||
|
.map_or(0, |v| v as u32);
|
||||||
|
let character = item.get("location")
|
||||||
|
.and_then(|l| l.get("range"))
|
||||||
|
.and_then(|r| r.get("start"))
|
||||||
|
.and_then(|s| s.get("character"))
|
||||||
|
.and_then(JsonValue::as_u64)
|
||||||
|
.map_or(0, |v| v as u32);
|
||||||
|
Some(crate::lsp_client::LspSymbol { name, kind, path, line, character })
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rename_params(uri: &str, line: u32, character: u32, new_name: &str) -> JsonValue {
|
||||||
|
serde_json::json!({
|
||||||
|
"textDocument": { "uri": uri },
|
||||||
|
"position": { "line": line, "character": character },
|
||||||
|
"newName": new_name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workspace_symbol_params(query: &str) -> JsonValue {
|
||||||
|
serde_json::json!({
|
||||||
|
"query": query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7124,6 +7124,15 @@ fn run_repl(
|
||||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show install suggestions for missing LSP servers
|
||||||
|
{
|
||||||
|
let availability = runtime::lsp_discovery::check_lsp_availability();
|
||||||
|
let prompt = runtime::lsp_discovery::format_install_prompt(&availability);
|
||||||
|
if !prompt.is_empty() {
|
||||||
|
eprintln!("{prompt}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
||||||
match editor.read_line()? {
|
match editor.read_line()? {
|
||||||
|
|
|
||||||
|
|
@ -1175,14 +1175,16 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "LSP",
|
name: "LSP",
|
||||||
description: "Query Language Server Protocol for code intelligence (symbols, references, diagnostics).",
|
description: "Query Language Server Protocol for code intelligence (symbols, references, diagnostics, code actions, rename, signature help, code lens, workspace symbols).",
|
||||||
input_schema: json!({
|
input_schema: json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"action": { "type": "string", "enum": ["symbols", "references", "diagnostics", "definition", "hover"] },
|
"action": { "type": "string", "enum": ["symbols", "references", "diagnostics", "definition", "hover", "code_action", "rename", "signature_help", "code_lens", "workspace_symbols"] },
|
||||||
"path": { "type": "string" },
|
"path": { "type": "string" },
|
||||||
"line": { "type": "integer", "minimum": 0 },
|
"line": { "type": "integer", "minimum": 0 },
|
||||||
"character": { "type": "integer", "minimum": 0 },
|
"character": { "type": "integer", "minimum": 0 },
|
||||||
|
"end_line": { "type": "integer", "minimum": 0 },
|
||||||
|
"end_character": { "type": "integer", "minimum": 0 },
|
||||||
"query": { "type": "string" }
|
"query": { "type": "string" }
|
||||||
},
|
},
|
||||||
"required": ["action"],
|
"required": ["action"],
|
||||||
|
|
@ -1863,7 +1865,18 @@ fn run_lsp(input: LspInput) -> Result<String, String> {
|
||||||
let character = input.character;
|
let character = input.character;
|
||||||
let query = input.query.as_deref();
|
let query = input.query.as_deref();
|
||||||
|
|
||||||
match registry.dispatch(action, path, line, character, query) {
|
// For code_action, pass end_line/end_character through the query param
|
||||||
|
// since dispatch() doesn't take them directly — encode as "end_line:end_character"
|
||||||
|
let effective_query = if input.action == "code_action" {
|
||||||
|
match (input.end_line, input.end_character) {
|
||||||
|
(Some(el), Some(ec)) => Some(format!("{el}:{ec}")),
|
||||||
|
_ => query.map(str::to_owned),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query.map(str::to_owned)
|
||||||
|
};
|
||||||
|
|
||||||
|
match registry.dispatch(action, path, line, character, effective_query.as_deref()) {
|
||||||
Ok(result) => to_pretty_json(result),
|
Ok(result) => to_pretty_json(result),
|
||||||
Err(e) => to_pretty_json(json!({
|
Err(e) => to_pretty_json(json!({
|
||||||
"action": action,
|
"action": action,
|
||||||
|
|
@ -3124,6 +3137,10 @@ struct LspInput {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
character: Option<u32>,
|
character: Option<u32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
end_line: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
end_character: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue