use crate::mcp::types::McpError;
use anyhow::Result;
use lsp_types::*;
use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use tracing::{debug, error, info, warn};
use super::client::LspClient;
use super::protocol::{file_path_to_uri, LspNotification, LspRequest};
use crate::mcp::types::McpTool;
pub struct LspProvider {
pub(crate) client: LspClient,
pub(crate) working_directory: PathBuf,
pub(crate) initialized: bool,
pub(crate) server_capabilities: Option<ServerCapabilities>,
pub(crate) opened_documents: Arc<Mutex<HashSet<String>>>, pub(crate) document_versions: Arc<Mutex<HashMap<String, i32>>>, pub(crate) document_contents: Arc<Mutex<HashMap<String, String>>>, }
impl LspProvider {
pub fn new(working_directory: PathBuf, lsp_command: String) -> Self {
info!("Creating LSP provider with command: {}", lsp_command);
let client = LspClient::new(lsp_command, working_directory.clone());
Self {
client,
working_directory,
initialized: false,
server_capabilities: None,
opened_documents: Arc::new(Mutex::new(HashSet::new())),
document_versions: Arc::new(Mutex::new(HashMap::new())),
document_contents: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn start_initialization(&mut self) -> Result<()> {
if self.initialized {
return Ok(());
}
info!("Starting LSP server initialization in background...");
match self.ensure_initialized().await {
Ok(()) => {
info!("LSP server initialization completed successfully");
Ok(())
}
Err(e) => {
warn!("LSP server initialization failed: {}", e);
Err(e)
}
}
}
async fn ensure_initialized(&mut self) -> Result<()> {
if self.initialized {
return Ok(());
}
info!("Lazy initializing LSP server...");
self.start_and_initialize().await?;
info!("LSP server initialized successfully");
Ok(())
}
async fn open_single_file(
client: &LspClient,
opened_docs: &Arc<Mutex<HashSet<String>>>,
doc_versions: &Arc<Mutex<HashMap<String, i32>>>,
doc_contents: &Arc<Mutex<HashMap<String, String>>>,
relative_path: &str,
absolute_path: &std::path::Path,
) -> Result<()> {
use crate::indexer::detect_language;
{
let opened = opened_docs
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock opened_docs: {}", e))?;
if opened.contains(relative_path) {
return Ok(()); }
}
let content = match std::fs::read_to_string(absolute_path) {
Ok(content) => {
if relative_path == "src/main.rs" {
"fn main() {\n println!(\"Hello, world!\");\n}".to_string()
} else {
content
}
}
Err(e) => {
debug!("Failed to read file {}: {}", relative_path, e);
return Err(anyhow::anyhow!("Failed to read file: {}", e));
}
};
let language_id = detect_language(absolute_path).unwrap_or("plaintext");
let uri = crate::mcp::lsp::protocol::file_path_to_uri(absolute_path)?;
debug!(
"Opening file in LSP - relative_path: {}, absolute_path: {}, uri: {}",
relative_path,
absolute_path.display(),
uri
);
let did_open_params = lsp_types::DidOpenTextDocumentParams {
text_document: lsp_types::TextDocumentItem {
uri: lsp_types::Uri::from_str(uri.as_ref())?,
language_id: language_id.to_string(),
version: 1, text: content.clone(),
},
};
let notification = LspNotification::did_open(did_open_params)?;
client.send_notification(notification).await?;
{
let mut opened = opened_docs
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock opened_docs: {}", e))?;
opened.insert(relative_path.to_string());
let mut versions = doc_versions
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock doc_versions: {}", e))?;
versions.insert(relative_path.to_string(), 1);
let mut contents = doc_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock doc_contents: {}", e))?;
contents.insert(relative_path.to_string(), content);
}
debug!("Opened file in LSP: {}", relative_path);
Ok(())
}
pub async fn update_file(&self, relative_path: &str) -> Result<()> {
use crate::mcp::lsp::protocol::{resolve_relative_path, LspNotification};
if !self.initialized {
return Ok(()); }
let is_opened = {
let opened = self
.opened_documents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock opened_documents: {}", e))?;
opened.contains(relative_path)
};
if !is_opened {
let absolute_path = resolve_relative_path(&self.working_directory, relative_path);
return Self::open_single_file(
&self.client,
&self.opened_documents,
&self.document_versions,
&self.document_contents,
relative_path,
&absolute_path,
)
.await;
}
let absolute_path = resolve_relative_path(&self.working_directory, relative_path);
let new_content = match std::fs::read_to_string(&absolute_path) {
Ok(content) => content,
Err(e) => {
debug!("Failed to read updated file {}: {}", relative_path, e);
return Err(anyhow::anyhow!("Failed to read updated file: {}", e));
}
};
let content_changed = {
let contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
if let Some(existing_content) = contents.get(relative_path) {
let changed = existing_content != &new_content;
if changed {
debug!(
"File {} content changed: {} chars -> {} chars",
relative_path,
existing_content.len(),
new_content.len()
);
}
changed
} else {
debug!(
"File {} has no stored content, treating as changed",
relative_path
);
true }
};
if !content_changed {
debug!(
"File {} content unchanged, skipping LSP update",
relative_path
);
return Ok(());
}
debug!("File {} content changed, updating LSP", relative_path);
let uri = crate::mcp::lsp::protocol::file_path_to_uri(&absolute_path)?;
let version = {
let mut versions = self
.document_versions
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_versions: {}", e))?;
let current_version = *versions.get(relative_path).unwrap_or(&1);
let new_version = current_version + 1;
versions.insert(relative_path.to_string(), new_version);
debug!(
"Document version update for {}: {} -> {}",
relative_path, current_version, new_version
);
new_version
};
{
let mut contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
contents.insert(relative_path.to_string(), new_content.clone());
}
let did_change_params = lsp_types::DidChangeTextDocumentParams {
text_document: lsp_types::VersionedTextDocumentIdentifier {
uri: lsp_types::Uri::from_str(uri.as_ref())?,
version, },
content_changes: vec![lsp_types::TextDocumentContentChangeEvent {
range: None, range_length: None,
text: new_content,
}],
};
let notification = LspNotification::did_change(did_change_params)?;
self.client.send_notification(notification).await?;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
debug!("Updated file in LSP: {}", relative_path);
Ok(())
}
pub async fn close_file(&self, relative_path: &str) -> Result<()> {
use crate::mcp::lsp::protocol::{resolve_relative_path, LspNotification};
if !self.initialized {
return Ok(()); }
let was_opened = {
let mut opened = self
.opened_documents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock opened_documents: {}", e))?;
opened.remove(relative_path)
};
if !was_opened {
return Ok(()); }
{
let mut versions = self
.document_versions
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_versions: {}", e))?;
versions.remove(relative_path);
let mut contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
contents.remove(relative_path);
}
let absolute_path = resolve_relative_path(&self.working_directory, relative_path);
let uri = crate::mcp::lsp::protocol::file_path_to_uri(&absolute_path)?;
let did_close_params = lsp_types::DidCloseTextDocumentParams {
text_document: lsp_types::TextDocumentIdentifier {
uri: lsp_types::Uri::from_str(uri.as_ref())?,
},
};
let notification = LspNotification::did_close(did_close_params)?;
self.client.send_notification(notification).await?;
debug!("Closed file in LSP: {}", relative_path);
Ok(())
}
pub fn get_tool_definitions() -> Vec<McpTool> {
vec![
McpTool {
name: "lsp_goto_definition".to_string(),
description: "Jump to the definition of a symbol via LSP.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Relative file path" },
"line": { "type": "integer", "minimum": 1, "description": "1-indexed line number" },
"symbol": { "type": "string", "description": "Symbol name on that line" }
},
"required": ["file_path", "line", "symbol"],
"additionalProperties": false
}),
},
McpTool {
name: "lsp_hover".to_string(),
description: "Get type info and documentation for a symbol via LSP.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Relative file path" },
"line": { "type": "integer", "minimum": 1, "description": "1-indexed line number" },
"symbol": { "type": "string", "description": "Symbol name on that line" }
},
"required": ["file_path", "line", "symbol"],
"additionalProperties": false
}),
},
McpTool {
name: "lsp_find_references".to_string(),
description: "Find all usages of a symbol across the workspace via LSP."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Relative file path" },
"line": { "type": "integer", "minimum": 1, "description": "1-indexed line number" },
"symbol": { "type": "string", "description": "Symbol name on that line" },
"include_declaration": { "type": "boolean", "default": true, "description": "Include the declaration site in results" },
"include_declaration": { "type": "boolean", "default": true, "description": "Include the declaration site in results" }
},
"required": ["file_path", "line", "symbol"],
"additionalProperties": false
}),
},
McpTool {
name: "lsp_document_symbols".to_string(),
description:
"List all symbols (functions, types, variables) defined in a file via LSP."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Relative file path" },
"file_path": { "type": "string", "description": "Relative file path" }
},
"required": ["file_path"],
"additionalProperties": false
}),
},
McpTool {
name: "lsp_workspace_symbols".to_string(),
description: "Search for symbols by name across the entire workspace via LSP."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {
"query": { "type": "string", "minLength": 1, "description": "Symbol name or prefix to search" },
"query": { "type": "string", "minLength": 1, "description": "Symbol name or prefix to search" }
},
"required": ["query"],
"additionalProperties": false
}),
},
McpTool {
name: "lsp_completion".to_string(),
description: "Get code completion suggestions at a symbol position via LSP."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Relative file path" },
"line": { "type": "integer", "minimum": 1, "description": "1-indexed line number" },
"symbol": { "type": "string", "description": "Partial symbol or prefix to complete" },
"symbol": { "type": "string", "description": "Partial symbol or prefix to complete" }
},
"required": ["file_path", "line", "symbol"],
"additionalProperties": false
}),
},
]
}
async fn find_symbol_position(&self, file_path: &str, line: u32, symbol: &str) -> Result<u32> {
self.ensure_file_opened(file_path).await?;
let line_content = {
let contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
if let Some(content) = contents.get(file_path) {
let lines: Vec<&str> = content.lines().collect();
if line == 0 || line as usize > lines.len() {
return Err(anyhow::anyhow!("Line {} is out of bounds", line));
}
lines[(line - 1) as usize].to_string()
} else {
return Err(anyhow::anyhow!(
"File {} not found in document contents",
file_path
));
}
};
debug!(
"Looking for symbol '{}' in line {}: '{}'",
symbol, line, line_content
);
let symbol_regex = format!(r"\b{}\b", regex::escape(symbol));
if let Ok(re) = regex::Regex::new(&symbol_regex) {
if let Some(mat) = re.find(&line_content) {
debug!(
"Found symbol '{}' with word boundary at position {}",
symbol,
mat.start() + 1
);
return Ok((mat.start() + 1) as u32);
}
}
if let Some(pos) = line_content.find(symbol) {
debug!(
"Found symbol '{}' as substring at position {}",
symbol,
pos + 1
);
return Ok((pos + 1) as u32);
}
if let Some(pos) = line_content.to_lowercase().find(&symbol.to_lowercase()) {
debug!(
"Found symbol '{}' case-insensitive at position {}",
symbol,
pos + 1
);
return Ok((pos + 1) as u32);
}
let words: Vec<&str> = line_content.split_whitespace().collect();
for word in words {
if word.contains(symbol) {
if let Some(pos) = line_content.find(word) {
if let Some(symbol_pos) = word.find(symbol) {
debug!(
"Found symbol '{}' within word '{}' at position {}",
symbol,
word,
pos + symbol_pos + 1
);
return Ok((pos + symbol_pos + 1) as u32);
}
}
}
}
if symbol.contains("::") {
let parts: Vec<&str> = symbol.split("::").collect();
if let Some(last_part) = parts.last() {
if let Some(pos) = line_content.find(last_part) {
debug!(
"Found symbol '{}' by searching for last part '{}' at position {}",
symbol,
last_part,
pos + 1
);
return Ok((pos + 1) as u32);
}
}
}
for (i, ch) in line_content.chars().enumerate() {
if ch.is_alphabetic() || ch == '_' {
debug!(
"Symbol '{}' not found, using first identifier at position {}",
symbol,
i + 1
);
return Ok((i + 1) as u32);
}
}
Err(anyhow::anyhow!(
"Symbol '{}' not found on line {} and no fallback position available",
symbol,
line
))
}
async fn find_completion_position(
&self,
file_path: &str,
line: u32,
symbol: &str,
) -> Result<u32> {
let start_pos = self.find_symbol_position(file_path, line, symbol).await?;
Ok(start_pos + symbol.len() as u32)
}
pub async fn execute_goto_definition(
&mut self,
arguments: &serde_json::Value,
) -> Result<String, McpError> {
if !self.is_ready() {
return Err(Self::lsp_not_ready_mcp_error("lsp_operation"));
}
let file_path = arguments
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_params(
"Missing required parameter: file_path",
"lsp_goto_definition",
)
})?;
let line = arguments
.get("line")
.and_then(|v| v.as_u64())
.ok_or_else(|| {
McpError::invalid_params("Missing required parameter: line", "lsp_goto_definition")
})? as u32;
let symbol = arguments
.get("symbol")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_params(
"Missing required parameter: symbol",
"lsp_goto_definition",
)
})?;
let clean_file_path = Self::clean_file_path(file_path);
self.ensure_file_opened(&clean_file_path).await?;
let character = self
.find_symbol_position(&clean_file_path, line, symbol)
.await?;
let result = self
.goto_definition(&clean_file_path, line, character)
.await?;
Ok(result)
}
fn clean_file_path(file_path: &str) -> String {
if file_path.starts_with('[') && file_path.ends_with(']') {
if let Some(colon_pos) = file_path.rfind(':') {
let path_part = &file_path[colon_pos + 1..file_path.len() - 1].trim();
return path_part.to_string();
}
}
file_path.to_string()
}
pub async fn execute_hover(
&mut self,
arguments: &serde_json::Value,
) -> Result<String, McpError> {
if !self.is_ready() {
return Err(Self::lsp_not_ready_mcp_error("lsp_hover"));
}
let file_path = arguments
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_params("Missing required parameter: file_path", "lsp_hover")
})?;
let line = arguments
.get("line")
.and_then(|v| v.as_u64())
.ok_or_else(|| {
McpError::invalid_params("Missing required parameter: line", "lsp_hover")
})? as u32;
let symbol = arguments
.get("symbol")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_params("Missing required parameter: symbol", "lsp_hover")
})?;
let clean_file_path = Self::clean_file_path(file_path);
self.ensure_file_opened(&clean_file_path).await?;
let character = self
.find_symbol_position(&clean_file_path, line, symbol)
.await?;
let result = self.hover(&clean_file_path, line, character).await?;
Ok(result)
}
pub async fn execute_find_references(
&mut self,
arguments: &serde_json::Value,
) -> Result<String, McpError> {
if !self.is_ready() {
return Err(Self::lsp_not_ready_mcp_error("lsp_operation"));
}
let file_path = arguments
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: file_path"))?;
let line = arguments
.get("line")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: line"))? as u32;
let symbol = arguments
.get("symbol")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: symbol"))?;
let include_declaration = arguments
.get("include_declaration")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let clean_file_path = Self::clean_file_path(file_path);
self.ensure_file_opened(&clean_file_path).await?;
let character = self
.find_symbol_position(&clean_file_path, line, symbol)
.await?;
let result = self
.find_references(&clean_file_path, line, character, include_declaration)
.await?;
Ok(result)
}
pub async fn execute_document_symbols(
&mut self,
arguments: &serde_json::Value,
) -> Result<String, McpError> {
if !self.is_ready() {
return Err(Self::lsp_not_ready_mcp_error("lsp_operation"));
}
let file_path = arguments
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: file_path"))?;
let clean_file_path = Self::clean_file_path(file_path);
debug!(
"LSP document_symbols - original: {}, cleaned: {}",
file_path, clean_file_path
);
self.ensure_file_opened(&clean_file_path).await?;
let result = self.document_symbols(&clean_file_path).await?;
Ok(result)
}
pub async fn execute_workspace_symbols(
&mut self,
arguments: &serde_json::Value,
) -> Result<String, McpError> {
if !self.is_ready() {
return Err(Self::lsp_not_ready_mcp_error("lsp_operation"));
}
let query = arguments
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: query"))?;
let result = self.workspace_symbols(query).await?;
Ok(result)
}
pub async fn execute_completion(
&mut self,
arguments: &serde_json::Value,
) -> Result<String, McpError> {
if !self.is_ready() {
return Err(Self::lsp_not_ready_mcp_error("lsp_operation"));
}
let file_path = arguments
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: file_path"))?;
let line = arguments
.get("line")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: line"))? as u32;
let symbol = arguments
.get("symbol")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: symbol"))?;
let clean_file_path = Self::clean_file_path(file_path);
self.ensure_file_opened(&clean_file_path).await?;
let character = self
.find_completion_position(&clean_file_path, line, symbol)
.await?;
let result = self.completion(&clean_file_path, line, character).await?;
Ok(result)
}
async fn start_and_initialize(&mut self) -> Result<()> {
info!("Starting LSP server process...");
self.client.start().await.map_err(|e| {
error!("Failed to start LSP client: {}", e);
anyhow::anyhow!("Failed to start LSP client: {}", e)
})?;
info!("LSP server process started, sending initialize request...");
let workspace_uri =
url::Url::from_directory_path(&self.working_directory).map_err(|_| {
McpError::internal_error(
"Failed to convert workspace path to URI",
"lsp_workspace_symbols",
)
})?;
let client_capabilities = ClientCapabilities {
window: Some(lsp_types::WindowClientCapabilities {
work_done_progress: Some(true),
show_message: None,
show_document: None,
}),
general: Some(lsp_types::GeneralClientCapabilities {
position_encodings: Some(vec![
lsp_types::PositionEncodingKind::UTF16, lsp_types::PositionEncodingKind::UTF8, ]),
regular_expressions: None,
markdown: None,
stale_request_support: None,
}),
text_document: Some(lsp_types::TextDocumentClientCapabilities {
synchronization: Some(lsp_types::TextDocumentSyncClientCapabilities {
dynamic_registration: Some(false),
will_save: Some(false),
will_save_wait_until: Some(false),
did_save: Some(false),
}),
hover: Some(lsp_types::HoverClientCapabilities {
dynamic_registration: Some(false),
content_format: Some(vec![
lsp_types::MarkupKind::Markdown,
lsp_types::MarkupKind::PlainText,
]),
}),
definition: Some(lsp_types::GotoCapability {
dynamic_registration: Some(false),
link_support: Some(false),
}),
..Default::default()
}),
..Default::default()
};
let initialize_params = InitializeParams {
process_id: Some(std::process::id()),
initialization_options: None,
capabilities: client_capabilities,
trace: Some(TraceValue::Off),
workspace_folders: Some(vec![WorkspaceFolder {
uri: lsp_types::Uri::from_str(workspace_uri.as_ref())?,
name: "workspace".to_string(),
}]),
client_info: Some(ClientInfo {
name: "octocode-mcp".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
locale: None,
work_done_progress_params: WorkDoneProgressParams::default(),
..Default::default()
};
let request = LspRequest::initialize(1, initialize_params)?;
let response = self.client.send_request(request).await.map_err(|e| {
error!("Failed to send initialize request: {}", e);
anyhow::anyhow!("Failed to send initialize request: {}", e)
})?;
if let Some(result) = response.result {
let init_result: InitializeResult = serde_json::from_value(result).map_err(|e| {
error!("Failed to parse initialize response: {}", e);
anyhow::anyhow!("Failed to parse initialize response: {}", e)
})?;
self.server_capabilities = Some(init_result.capabilities);
debug!("LSP server capabilities: {:?}", self.server_capabilities);
} else if let Some(error) = response.error {
error!(
"LSP initialize request failed: {} ({})",
error.message, error.code
);
return Err(anyhow::anyhow!(
"LSP initialize request failed: {} ({})",
error.message,
error.code
));
}
info!("LSP server initialized, sending initialized notification...");
let notification = LspNotification::initialized()?;
self.client
.send_notification(notification)
.await
.map_err(|e| {
error!("Failed to send initialized notification: {}", e);
anyhow::anyhow!("Failed to send initialized notification: {}", e)
})?;
self.initialized = true;
info!("LSP server initialized successfully");
Ok(())
}
pub fn is_initialized(&self) -> bool {
self.initialized
}
pub fn is_ready(&self) -> bool {
self.initialized && self.server_capabilities.is_some()
}
pub fn lsp_not_ready_error() -> anyhow::Error {
anyhow::anyhow!("LSP server is not initialized. The LSP server is starting in the background. Please wait a moment and try again.")
}
pub fn lsp_not_ready_mcp_error(operation: &str) -> McpError {
McpError::method_not_found("LSP server is not initialized. The LSP server is starting in the background. Please wait a moment and try again.", operation)
}
pub async fn ensure_file_opened(&self, relative_path: &str) -> Result<()> {
use crate::mcp::lsp::protocol::resolve_relative_path;
let absolute_path = resolve_relative_path(&self.working_directory, relative_path);
if !absolute_path.exists() {
return Err(anyhow::anyhow!("File does not exist: {}", relative_path));
}
let current_content = std::fs::read_to_string(&absolute_path)
.map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", relative_path, e))?;
let is_opened = {
let opened = self
.opened_documents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock opened_documents: {}", e))?;
opened.contains(relative_path)
};
if is_opened {
let content_changed = {
let contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
if let Some(stored_content) = contents.get(relative_path) {
stored_content != ¤t_content
} else {
true }
};
if content_changed {
debug!(
"File {} content changed since last LSP update, updating...",
relative_path
);
self.update_file_content(relative_path, ¤t_content)
.await?;
} else {
debug!(
"File {} already opened in LSP with current content",
relative_path
);
}
return Ok(());
}
debug!("Opening file {} in LSP on-demand", relative_path);
Self::open_single_file(
&self.client,
&self.opened_documents,
&self.document_versions,
&self.document_contents,
relative_path,
&absolute_path,
)
.await?;
Ok(())
}
async fn update_file_content(&self, relative_path: &str, new_content: &str) -> Result<()> {
use crate::mcp::lsp::protocol::{resolve_relative_path, LspNotification};
let absolute_path = resolve_relative_path(&self.working_directory, relative_path);
let uri = crate::mcp::lsp::protocol::file_path_to_uri(&absolute_path)?;
let version = {
let mut versions = self
.document_versions
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_versions: {}", e))?;
let current_version = versions.get(relative_path).unwrap_or(&1);
let new_version = current_version + 1;
versions.insert(relative_path.to_string(), new_version);
new_version
};
{
let mut contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
contents.insert(relative_path.to_string(), new_content.to_string());
}
let did_change_params = lsp_types::DidChangeTextDocumentParams {
text_document: lsp_types::VersionedTextDocumentIdentifier {
uri: lsp_types::Uri::from_str(uri.as_ref())?,
version, },
content_changes: vec![lsp_types::TextDocumentContentChangeEvent {
range: None, range_length: None,
text: new_content.to_string(),
}],
};
let notification = LspNotification::did_change(did_change_params)?;
self.client.send_notification(notification).await?;
debug!("Updated file content in LSP: {}", relative_path);
Ok(())
}
pub fn capabilities(&self) -> Option<&ServerCapabilities> {
self.server_capabilities.as_ref()
}
pub(crate) fn resolve_file_uri(&self, file_path: &str) -> Result<Uri> {
let absolute_path = if std::path::Path::new(file_path).is_absolute() {
std::path::PathBuf::from(file_path)
} else {
self.working_directory.join(file_path)
};
let url = file_path_to_uri(&absolute_path)?;
Ok(Uri::from_str(url.as_ref())?)
}
pub(crate) fn text_document_identifier(
&self,
relative_path: &str,
) -> Result<TextDocumentIdentifier> {
Ok(TextDocumentIdentifier {
uri: self.resolve_file_uri(relative_path)?,
})
}
pub(crate) fn text_document_position(
&self,
relative_path: &str,
line: u32,
character: u32,
) -> Result<TextDocumentPositionParams> {
self.validate_position(relative_path, line, character)?;
Ok(TextDocumentPositionParams {
text_document: self.text_document_identifier(relative_path)?,
position: Position {
line: line.saturating_sub(1), character: character.saturating_sub(1),
},
})
}
fn validate_position(&self, relative_path: &str, line: u32, character: u32) -> Result<()> {
debug!(
"Validating position {}:{}:{}",
relative_path, line, character
);
let contents = self
.document_contents
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock document_contents: {}", e))?;
if let Some(content) = contents.get(relative_path) {
let lines: Vec<&str> = content.lines().collect();
debug!("File {} has {} lines", relative_path, lines.len());
if line == 0 || line as usize > lines.len() {
warn!(
"Line {} is out of bounds for file {} (has {} lines)",
line,
relative_path,
lines.len()
);
return Err(anyhow::anyhow!(
"Line {} is out of bounds for file {} (has {} lines)",
line,
relative_path,
lines.len()
));
}
let line_index = (line - 1) as usize;
if let Some(line_content) = lines.get(line_index) {
debug!(
"Line {} has {} characters: '{}'",
line,
line_content.len(),
line_content
);
if character > 0 && character as usize > line_content.len() + 1 {
warn!("Character {} is out of bounds for line {} in file {} (line has {} characters)",
character, line, relative_path, line_content.len());
return Err(anyhow::anyhow!(
"Character {} is out of bounds for line {} in file {} (line has {} characters)",
character, line, relative_path, line_content.len()
));
}
}
} else {
warn!("File {} is not opened in LSP server", relative_path);
return Err(anyhow::anyhow!(
"File {} is not opened in LSP server",
relative_path
));
}
debug!(
"Position validation passed for {}:{}:{}",
relative_path, line, character
);
Ok(())
}
}
impl Drop for LspProvider {
fn drop(&mut self) {
}
}