use crate::model::buffer::Buffer;
use crate::model::event::BufferId;
use crate::services::async_bridge::{LspMessageType, LspProgressValue, LspServerStatus};
use crate::services::lsp::manager::detect_language;
use crate::state::{SemanticTokenSpan, SemanticTokenStore};
use crate::view::file_tree::{FileTreeView, NodeId};
use lsp_types::{Diagnostic, InlayHint, SemanticToken, SemanticTokensLegend, SemanticTokensResult};
use rust_i18n::t;
use serde_json::Value;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use super::types::{LspMessageEntry, LspProgressInfo};
use super::Editor;
impl Editor {
pub(super) fn find_buffer_by_uri(&self, uri: &str) -> Option<BufferId> {
let parsed_uri = uri.parse::<lsp_types::Uri>().ok()?;
self.buffer_metadata
.iter()
.find(|(_, m)| m.file_uri() == Some(&parsed_uri))
.map(|(buffer_id, _)| *buffer_id)
}
fn apply_diagnostics_to_buffer(
&mut self,
uri: &str,
diagnostics: &[Diagnostic],
) -> Option<BufferId> {
let buffer_id = self.find_buffer_by_uri(uri)?;
let state = self.buffers.get_mut(&buffer_id)?;
crate::services::lsp::diagnostics::apply_diagnostics_to_state_cached(
state,
diagnostics,
&self.theme,
);
Some(buffer_id)
}
}
impl Editor {
fn store_and_apply_diagnostics(&mut self, uri: String, diagnostics: Vec<Diagnostic>) {
if diagnostics.is_empty() {
self.stored_diagnostics.remove(&uri);
} else {
self.stored_diagnostics
.insert(uri.clone(), diagnostics.clone());
}
if let Some(buffer_id) = self.apply_diagnostics_to_buffer(&uri, &diagnostics) {
tracing::info!(
"Applied {} diagnostics to buffer {:?}",
diagnostics.len(),
buffer_id
);
} else {
tracing::debug!("No buffer found for diagnostic URI: {}", uri);
}
self.plugin_manager.run_hook(
"diagnostics_updated",
crate::services::plugins::hooks::HookArgs::DiagnosticsUpdated {
uri,
count: diagnostics.len(),
},
);
}
pub(super) fn handle_lsp_diagnostics(&mut self, uri: String, diagnostics: Vec<Diagnostic>) {
tracing::debug!(
"Processing {} LSP diagnostics for {}",
diagnostics.len(),
uri
);
self.store_and_apply_diagnostics(uri, diagnostics);
}
pub(super) fn handle_lsp_pulled_diagnostics(
&mut self,
uri: String,
result_id: Option<String>,
diagnostics: Vec<Diagnostic>,
unchanged: bool,
) {
if unchanged {
tracing::debug!(
"Diagnostics unchanged for {} (result_id: {:?})",
uri,
result_id
);
return;
}
tracing::debug!(
"Processing {} pulled diagnostics for {} (result_id: {:?})",
diagnostics.len(),
uri,
result_id
);
if let Some(result_id) = result_id {
self.diagnostic_result_ids.insert(uri.clone(), result_id);
}
self.store_and_apply_diagnostics(uri, diagnostics);
}
}
impl Editor {
pub(super) fn handle_lsp_inlay_hints(
&mut self,
request_id: u64,
uri: String,
hints: Vec<InlayHint>,
) {
if self.pending_inlay_hints_request != Some(request_id) {
tracing::debug!(
"Ignoring stale inlay hints response (request_id={})",
request_id
);
return;
}
self.pending_inlay_hints_request = None;
tracing::info!(
"Received {} inlay hints for {} (request_id={})",
hints.len(),
uri,
request_id
);
if let Some(buffer_id) = self.find_buffer_by_uri(&uri) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
Self::apply_inlay_hints_to_state(state, &hints);
tracing::info!(
"Applied {} inlay hints as virtual text to buffer {:?}",
hints.len(),
buffer_id
);
}
} else {
tracing::warn!("No buffer found for inlay hints URI: {}", uri);
}
}
pub(super) fn handle_lsp_semantic_tokens(
&mut self,
request_id: u64,
uri: String,
result: Result<Option<SemanticTokensResult>, String>,
) {
let Some((buffer_id, target_version)) =
self.take_pending_semantic_token_request(request_id)
else {
tracing::debug!(
"Semantic tokens response {} for {} without pending entry",
request_id,
uri
);
return;
};
let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
return;
};
let Some(path) = metadata.file_path() else {
return;
};
let Some(language) = detect_language(path, &self.config.languages) else {
return;
};
let legend = match self
.lsp
.as_ref()
.and_then(|manager| manager.semantic_tokens_legend(&language).cloned())
{
Some(legend) => legend,
None => {
tracing::debug!("Semantic tokens legend missing for language {}", language);
return;
}
};
let current_version = match self.buffers.get(&buffer_id) {
Some(state) => state.buffer.version(),
None => return,
};
match result {
Err(e) => {
tracing::warn!(
"Semantic tokens request {} for {} failed: {}",
request_id,
uri,
e
);
if current_version != target_version {
self.maybe_request_semantic_tokens(buffer_id);
}
}
Ok(tokens_opt) => {
if current_version != target_version {
self.maybe_request_semantic_tokens(buffer_id);
return;
}
if let Some(state) = self.buffers.get_mut(&buffer_id) {
let (result_id, spans) = match tokens_opt {
Some(SemanticTokensResult::Tokens(tokens)) => {
let spans =
decode_semantic_token_data(&state.buffer, &legend, &tokens.data);
(tokens.result_id.clone(), spans)
}
Some(SemanticTokensResult::Partial(partial)) => {
let spans =
decode_semantic_token_data(&state.buffer, &legend, &partial.data);
(None, spans)
}
None => (None, Vec::new()),
};
state.set_semantic_tokens(SemanticTokenStore {
version: current_version,
result_id,
tokens: spans,
});
}
self.full_redraw_requested = true;
}
}
}
pub(super) fn handle_lsp_server_quiescent(&mut self, language: String) {
tracing::info!(
"LSP ({}) project fully loaded, re-requesting inlay hints",
language
);
if !self.config.editor.enable_inlay_hints {
return;
}
let Some(lsp) = self.lsp.as_mut() else {
return;
};
let Some(client) = lsp.get_handle_mut(&language) else {
return;
};
let buffer_infos: Vec<_> = self
.buffer_metadata
.iter()
.filter_map(|(buffer_id, metadata)| {
metadata.file_uri().map(|uri| {
let line_count = self
.buffers
.get(buffer_id)
.and_then(|s| s.buffer.line_count())
.unwrap_or(1000);
(uri.clone(), line_count)
})
})
.collect();
for (uri, line_count) in buffer_infos {
let request_id = self.next_lsp_request_id;
self.next_lsp_request_id += 1;
self.pending_inlay_hints_request = Some(request_id);
let last_line = line_count.saturating_sub(1) as u32;
if let Err(e) = client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, 10000) {
tracing::debug!(
"Failed to re-request inlay hints for {}: {}",
uri.as_str(),
e
);
} else {
tracing::info!(
"Re-requested inlay hints for {} (request_id={})",
uri.as_str(),
request_id
);
}
}
}
pub(super) fn handle_lsp_progress(
&mut self,
language: String,
token: String,
value: LspProgressValue,
) {
match value {
LspProgressValue::Begin {
title,
message,
percentage,
} => {
self.lsp_progress.insert(
token.clone(),
LspProgressInfo {
language,
title,
message,
percentage,
},
);
self.update_lsp_status_from_progress();
}
LspProgressValue::Report {
message,
percentage,
} => {
if let Some(info) = self.lsp_progress.get_mut(&token) {
info.message = message;
info.percentage = percentage;
self.update_lsp_status_from_progress();
}
}
LspProgressValue::End { .. } => {
self.lsp_progress.remove(&token);
self.update_lsp_status_from_progress();
}
}
}
pub(super) fn handle_lsp_window_message(
&mut self,
language: String,
message_type: LspMessageType,
message: String,
) {
self.lsp_window_messages.push(LspMessageEntry {
language: language.clone(),
message_type,
message: message.clone(),
timestamp: Instant::now(),
});
if self.lsp_window_messages.len() > 100 {
self.lsp_window_messages.remove(0);
}
match message_type {
LspMessageType::Error | LspMessageType::Warning => {
self.status_message = Some(format!("LSP ({}): {}", language, message));
}
_ => {
}
}
}
pub(super) fn handle_lsp_log_message(
&mut self,
language: String,
message_type: LspMessageType,
message: String,
) {
self.lsp_log_messages.push(LspMessageEntry {
language,
message_type,
message,
timestamp: Instant::now(),
});
if self.lsp_log_messages.len() > 500 {
self.lsp_log_messages.remove(0);
}
}
pub(super) fn handle_lsp_status_update(&mut self, language: String, status: LspServerStatus) {
use crate::services::async_bridge::LspServerStatus;
let old_status = self.lsp_server_statuses.get(&language).cloned();
self.lsp_server_statuses
.insert(language.clone(), status.clone());
self.update_lsp_status_from_server_statuses();
self.update_lsp_warning_domain();
if status == LspServerStatus::Error {
let was_running = old_status
.as_ref()
.map(|s| matches!(s, LspServerStatus::Running | LspServerStatus::Initializing))
.unwrap_or(false);
if was_running {
if let Some(lsp) = self.lsp.as_mut() {
let message = lsp.handle_server_crash(&language);
self.status_message = Some(message);
}
}
}
let status_str = match status {
LspServerStatus::Starting => "starting",
LspServerStatus::Initializing => "initializing",
LspServerStatus::Running => "running",
LspServerStatus::Error => "error",
LspServerStatus::Shutdown => "shutdown",
};
let old_status_str = old_status
.map(|s| match s {
LspServerStatus::Starting => "starting",
LspServerStatus::Initializing => "initializing",
LspServerStatus::Running => "running",
LspServerStatus::Error => "error",
LspServerStatus::Shutdown => "shutdown",
})
.unwrap_or("none");
self.emit_event(
crate::model::control_event::events::LSP_STATUS_CHANGED.name,
serde_json::json!({
"language": language,
"old_status": old_status_str,
"status": status_str
}),
);
}
pub(super) fn handle_custom_notification(
&mut self,
language: String,
method: String,
params: Option<Value>,
) {
tracing::debug!("Custom LSP notification {} from {}", method, language);
let payload = serde_json::json!({
"language": language,
"method": method,
"params": params,
});
self.emit_event("lsp/custom_notification", payload);
}
pub(super) fn handle_lsp_server_request(
&mut self,
language: String,
server_command: String,
method: String,
params: Option<Value>,
) {
tracing::debug!(
"LSP server request {} from {} ({})",
method,
language,
server_command
);
let params_str = params.map(|p| p.to_string());
self.plugin_manager.run_hook(
"lsp_server_request",
crate::services::plugins::hooks::HookArgs::LspServerRequest {
language,
method,
server_command,
params: params_str,
},
);
}
pub(super) fn handle_plugin_lsp_response(
&mut self,
request_id: u64,
result: Result<Value, String>,
) {
tracing::debug!("Received plugin LSP response (request_id={})", request_id);
self.send_plugin_response(crate::services::plugins::api::PluginResponse::LspRequest {
request_id,
result,
});
}
pub(super) fn handle_plugin_response(
&mut self,
response: crate::services::plugins::api::PluginResponse,
) {
tracing::debug!("Received plugin response: {:?}", response);
self.send_plugin_response(response);
}
}
impl Editor {
pub(super) fn handle_async_file_changed(&mut self, path: String) -> bool {
const DEBOUNCE_WINDOW: Duration = Duration::from_secs(10);
const RAPID_REVERT_THRESHOLD: u32 = 10;
if !self.auto_revert_enabled {
return false;
}
let path_buf = PathBuf::from(&path);
let is_file_open = self
.buffers
.iter()
.any(|(_, state)| state.buffer.file_path() == Some(&path_buf));
if !is_file_open {
tracing::trace!("Ignoring file change event for non-open file: {}", path);
return false;
}
if let Some((window_start, count)) = self.file_rapid_change_counts.get_mut(&path_buf) {
if self.time_source.elapsed_since(*window_start) < DEBOUNCE_WINDOW {
*count += 1;
if *count >= RAPID_REVERT_THRESHOLD {
self.auto_revert_enabled = false;
self.status_message = Some(format!(
"Auto-revert disabled: {} is updating too frequently (use Ctrl+Shift+R to re-enable)",
path_buf.file_name().unwrap_or_default().to_string_lossy()
));
tracing::info!(
"Auto-revert disabled for {:?} ({} reverts in {:?})",
path_buf,
count,
DEBOUNCE_WINDOW
);
return false;
}
} else {
*count = 1;
*window_start = self.time_source.now();
}
} else {
self.file_rapid_change_counts
.insert(path_buf.clone(), (self.time_source.now(), 1));
}
tracing::info!("File changed externally: {}", path);
self.handle_file_changed(&path);
true
}
}
impl Editor {
pub(super) fn handle_file_explorer_initialized(&mut self, mut view: FileTreeView) {
tracing::info!("File explorer initialized");
let root_id = view.tree().root_id();
let root_path = view.tree().get_node(root_id).map(|n| n.entry.path.clone());
if let Some(root_path) = root_path {
if let Err(e) = view.load_gitignore_for_dir(&root_path) {
tracing::warn!("Failed to load root .gitignore from {:?}: {}", root_path, e);
} else {
tracing::debug!("Loaded root .gitignore from {:?}", root_path);
}
}
self.file_explorer = Some(view);
self.set_status_message(t!("status.file_explorer_ready").to_string());
}
pub(super) fn handle_file_explorer_toggle_node(&mut self, node_id: NodeId) {
tracing::debug!("File explorer toggle completed for node {:?}", node_id);
}
pub(super) fn handle_file_explorer_refresh_node(&mut self, node_id: NodeId) {
tracing::debug!("File explorer refresh completed for node {:?}", node_id);
self.set_status_message(t!("explorer.refreshed_default").to_string());
}
pub(super) fn handle_file_explorer_expanded_to_path(&mut self, mut view: FileTreeView) {
tracing::trace!(
"handle_file_explorer_expanded_to_path: restoring file_explorer after async expand"
);
view.update_scroll_for_selection();
self.file_explorer = Some(view);
self.file_explorer_sync_in_progress = false;
}
}
impl Editor {
pub(super) fn handle_plugin_process_output(
&mut self,
process_id: u64,
stdout: String,
stderr: String,
exit_code: i32,
) {
tracing::debug!(
"Process {} completed: exit_code={}, stdout_len={}, stderr_len={}",
process_id,
exit_code,
stdout.len(),
stderr.len()
);
}
pub(super) fn process_plugin_commands(&mut self) -> bool {
let commands = self.plugin_manager.process_commands();
if commands.is_empty() {
return false;
}
tracing::trace!(
"process_plugin_commands: processing {} commands",
commands.len()
);
for command in commands {
tracing::trace!(
"process_plugin_commands: handling command {:?}",
std::mem::discriminant(&command)
);
if let Err(e) = self.handle_plugin_command(command) {
tracing::error!("Error handling TypeScript plugin command: {}", e);
}
}
true
}
#[cfg(feature = "plugins")]
pub(super) fn process_pending_plugin_actions(&mut self) {
self.pending_plugin_actions
.retain(|(action_name, receiver)| {
match receiver.try_recv() {
Ok(result) => {
match result {
Ok(()) => {
tracing::info!(
"Plugin action '{}' executed successfully",
action_name
);
}
Err(e) => {
tracing::error!("Plugin action '{}' error: {}", action_name, e);
}
}
false }
Err(std::sync::mpsc::TryRecvError::Empty) => {
true }
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
tracing::error!(
"Plugin thread disconnected during action '{}'",
action_name
);
false }
}
});
}
pub(super) fn process_pending_lsp_restarts(&mut self) {
let Some(lsp) = self.lsp.as_mut() else {
return;
};
let restart_results = lsp.process_pending_restarts();
for (language, success, message) in restart_results {
self.status_message = Some(message.clone());
if success {
self.resend_did_open_for_language(&language);
}
}
}
pub(super) fn resend_did_open_for_language(&mut self, language: &str) {
let buffers_for_language: Vec<_> = self
.buffer_metadata
.iter()
.filter_map(|(buf_id, meta)| {
meta.file_path().and_then(|path| {
if crate::services::lsp::manager::detect_language(path, &self.config.languages)
== Some(language.to_string())
{
Some((*buf_id, path.clone()))
} else {
None
}
})
})
.collect();
for (buffer_id, path) in buffers_for_language {
if let Some(state) = self.buffers.get(&buffer_id) {
let content = match state.buffer.to_string() {
Some(c) => c,
None => continue, };
let uri: Option<lsp_types::Uri> = url::Url::from_file_path(&path)
.ok()
.and_then(|u| u.as_str().parse::<lsp_types::Uri>().ok());
if let Some(uri) = uri {
if let Some(lang_id) = crate::services::lsp::manager::detect_language(
&path,
&self.config.languages,
) {
if let Some(lsp) = self.lsp.as_mut() {
if let Some(handle) = lsp.get_handle_mut(&lang_id) {
let _ = handle.did_open(uri, content, lang_id);
}
}
}
}
}
}
}
pub(super) fn request_semantic_tokens_for_language(&mut self, language: &str) {
let buffer_ids: Vec<_> = self
.buffer_metadata
.iter()
.filter_map(|(buffer_id, meta)| {
meta.file_path().and_then(|path| {
if detect_language(path, &self.config.languages).as_deref() == Some(language) {
Some(*buffer_id)
} else {
None
}
})
})
.collect();
for buffer_id in buffer_ids {
self.maybe_request_semantic_tokens(buffer_id);
}
}
}
fn decode_semantic_token_data(
buffer: &Buffer,
legend: &SemanticTokensLegend,
data: &[SemanticToken],
) -> Vec<SemanticTokenSpan> {
let mut result = Vec::with_capacity(data.len());
let mut current_line = 0u32;
let mut current_start = 0u32;
for token in data {
current_line += token.delta_line;
if token.delta_line == 0 {
current_start += token.delta_start;
} else {
current_start = token.delta_start;
}
let start_utf16 = current_start as usize;
let end_utf16 = start_utf16 + token.length as usize;
let start_byte = buffer.lsp_position_to_byte(current_line as usize, start_utf16);
let end_byte = buffer.lsp_position_to_byte(current_line as usize, end_utf16);
let token_type = legend
.token_types
.get(token.token_type as usize)
.map(|ty| ty.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
let mut modifiers = Vec::new();
for (idx, modifier) in legend.token_modifiers.iter().enumerate() {
if (token.token_modifiers_bitset >> idx) & 1 == 1 {
modifiers.push(modifier.as_str().to_string());
}
}
result.push(SemanticTokenSpan {
range: start_byte..end_byte,
token_type,
modifiers,
});
}
result
}