use anyhow::Result as AnyhowResult;
use rust_i18n::t;
use std::io;
use std::time::{Duration, Instant};
use lsp_types::TextDocumentContentChangeEvent;
use crate::model::event::{BufferId, Event};
use crate::primitives::word_navigation::{find_word_end, find_word_start};
use crate::services::lsp::manager::detect_language;
use crate::view::prompt::{Prompt, PromptType};
use super::{uri_to_path, Editor, SemanticTokenRangeRequest};
const SEMANTIC_TOKENS_FULL_DEBOUNCE_MS: u64 = 500;
const SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS: u64 = 50;
const SEMANTIC_TOKENS_RANGE_PADDING_LINES: usize = 10;
impl Editor {
pub(crate) fn handle_completion_response(
&mut self,
request_id: u64,
items: Vec<lsp_types::CompletionItem>,
) -> AnyhowResult<()> {
if self.pending_completion_request != Some(request_id) {
tracing::debug!(
"Ignoring completion response for outdated request {}",
request_id
);
return Ok(());
}
self.pending_completion_request = None;
self.lsp_status.clear();
if items.is_empty() {
tracing::debug!("No completion items received");
return Ok(());
}
use crate::primitives::word_navigation::find_completion_word_start;
let (word_start, cursor_pos) = {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let word_start = find_completion_word_start(&state.buffer, cursor_pos);
(word_start, cursor_pos)
};
let prefix = if word_start < cursor_pos {
self.active_state_mut()
.get_text_range(word_start, cursor_pos)
.to_lowercase()
} else {
String::new()
};
let filtered_items: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
items.iter().collect()
} else {
items
.iter()
.filter(|item| {
item.label.to_lowercase().starts_with(&prefix)
|| item
.filter_text
.as_ref()
.map(|ft| ft.to_lowercase().starts_with(&prefix))
.unwrap_or(false)
})
.collect()
};
if filtered_items.is_empty() {
tracing::debug!("No completion items match prefix '{}'", prefix);
return Ok(());
}
use crate::view::popup::PopupListItem;
let popup_items: Vec<PopupListItem> = filtered_items
.iter()
.map(|item| {
let text = item.label.clone();
let detail = item.detail.clone();
let icon = match item.kind {
Some(lsp_types::CompletionItemKind::FUNCTION)
| Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
Some(lsp_types::CompletionItemKind::STRUCT)
| Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
_ => None,
};
let mut list_item = PopupListItem::new(text);
if let Some(detail) = detail {
list_item = list_item.with_detail(detail);
}
if let Some(icon) = icon {
list_item = list_item.with_icon(icon);
}
let data = item
.insert_text
.clone()
.or_else(|| Some(item.label.clone()));
if let Some(data) = data {
list_item = list_item.with_data(data);
}
list_item
})
.collect();
use crate::model::event::{
PopupContentData, PopupData, PopupListItemData, PopupPositionData,
};
let popup_data = PopupData {
title: Some(t!("lsp.popup_completion").to_string()),
description: None,
transient: false,
content: PopupContentData::List {
items: popup_items
.into_iter()
.map(|item| PopupListItemData {
text: item.text,
detail: item.detail,
icon: item.icon,
data: item.data,
})
.collect(),
selected: 0,
},
position: PopupPositionData::BelowCursor,
width: 50,
max_height: 15,
bordered: true,
};
self.completion_items = Some(items);
self.active_state_mut()
.apply(&crate::model::event::Event::ShowPopup { popup: popup_data });
tracing::info!(
"Showing completion popup with {} items",
self.completion_items.as_ref().map_or(0, |i| i.len())
);
Ok(())
}
pub(crate) fn handle_goto_definition_response(
&mut self,
request_id: u64,
locations: Vec<lsp_types::Location>,
) -> AnyhowResult<()> {
if self.pending_goto_definition_request != Some(request_id) {
tracing::debug!(
"Ignoring go-to-definition response for outdated request {}",
request_id
);
return Ok(());
}
self.pending_goto_definition_request = None;
if locations.is_empty() {
self.status_message = Some(t!("lsp.no_definition").to_string());
return Ok(());
}
let location = &locations[0];
if let Ok(path) = uri_to_path(&location.uri) {
let buffer_id = self.open_file(&path)?;
let line = location.range.start.line as usize;
let character = location.range.start.character as usize;
if let Some(state) = self.buffers.get(&buffer_id) {
let position = state.buffer.line_col_to_position(line, character);
let cursor_id = state.cursors.primary_id();
let old_position = state.cursors.primary().position;
let old_anchor = state.cursors.primary().anchor;
let old_sticky_column = state.cursors.primary().sticky_column;
let event = crate::model::event::Event::MoveCursor {
cursor_id,
old_position,
new_position: position,
old_anchor,
new_anchor: None,
old_sticky_column,
new_sticky_column: 0, };
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state.apply(&event);
}
}
self.status_message = Some(
t!(
"lsp.jumped_to_definition",
path = path.display().to_string(),
line = line + 1
)
.to_string(),
);
} else {
self.status_message = Some(t!("lsp.cannot_open_definition").to_string());
}
Ok(())
}
pub fn has_pending_lsp_requests(&self) -> bool {
self.pending_completion_request.is_some() || self.pending_goto_definition_request.is_some()
}
pub(crate) fn cancel_pending_lsp_requests(&mut self) {
if let Some(request_id) = self.pending_completion_request.take() {
tracing::debug!("Canceling pending LSP completion request {}", request_id);
self.send_lsp_cancel_request(request_id);
self.lsp_status.clear();
}
if let Some(request_id) = self.pending_goto_definition_request.take() {
tracing::debug!(
"Canceling pending LSP goto-definition request {}",
request_id
);
self.send_lsp_cancel_request(request_id);
self.lsp_status.clear();
}
}
fn send_lsp_cancel_request(&mut self, request_id: u64) {
let metadata = self.buffer_metadata.get(&self.active_buffer());
let file_path = metadata.and_then(|meta| meta.file_path());
if let Some(path) = file_path {
if let Some(language) = detect_language(path, &self.config.languages) {
if let Some(lsp) = self.lsp.as_mut() {
if let Some(handle) = lsp.get_handle_mut(&language) {
if let Err(e) = handle.cancel_request(request_id) {
tracing::warn!("Failed to send LSP cancel request: {}", e);
} else {
tracing::debug!("Sent $/cancelRequest for request_id={}", request_id);
}
}
}
}
}
}
pub(crate) fn with_lsp_for_buffer<F, R>(&mut self, buffer_id: BufferId, f: F) -> Option<R>
where
F: FnOnce(&crate::services::lsp::async_handler::LspHandle, &lsp_types::Uri, &str) -> R,
{
use crate::services::lsp::manager::LspSpawnResult;
let (uri, _path, language) = {
let metadata = self.buffer_metadata.get(&buffer_id)?;
if !metadata.lsp_enabled {
return None;
}
let uri = metadata.file_uri()?.clone();
let path = metadata.file_path()?.to_path_buf();
let language = detect_language(&path, &self.config.languages)?;
(uri, path, language)
};
let lsp = self.lsp.as_mut()?;
if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
return None;
}
let handle_id = lsp.get_handle_mut(&language)?.id();
let needs_open = {
let metadata = self.buffer_metadata.get(&buffer_id)?;
!metadata.lsp_opened_with.contains(&handle_id)
};
if needs_open {
let text = self.buffers.get(&buffer_id)?.buffer.to_string()?;
let lsp = self.lsp.as_mut()?;
let handle = lsp.get_handle_mut(&language)?;
if let Err(e) = handle.did_open(uri.clone(), text, language.clone()) {
tracing::warn!("Failed to send didOpen: {}", e);
return None;
}
let metadata = self.buffer_metadata.get_mut(&buffer_id)?;
metadata.lsp_opened_with.insert(handle_id);
tracing::debug!(
"Sent didOpen for {} to LSP handle {} (language: {})",
uri.as_str(),
handle_id,
language
);
}
let lsp = self.lsp.as_mut()?;
let handle = lsp.get_handle_mut(&language)?;
Some(f(handle, &uri, &language))
}
pub(crate) fn request_completion(&mut self) -> AnyhowResult<()> {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result =
handle.completion(request_id, uri.clone(), line as u32, character as u32);
if result.is_ok() {
tracing::info!(
"Requested completion at {}:{}:{}",
uri.as_str(),
line,
character
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_completion_request = Some(request_id);
self.lsp_status = "LSP: completion...".to_string();
}
Ok(())
}
pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
let path = match self.active_state().buffer.file_path() {
Some(p) => p,
None => return, };
let language = match detect_language(path, &self.config.languages) {
Some(lang) => lang,
None => return, };
let is_lsp_trigger = self
.lsp
.as_ref()
.map(|lsp| lsp.is_completion_trigger_char(c, &language))
.unwrap_or(false);
let quick_suggestions_enabled = self.config.editor.quick_suggestions;
let is_word_char = c.is_alphanumeric() || c == '_';
let should_trigger = is_lsp_trigger || (quick_suggestions_enabled && is_word_char);
if should_trigger {
tracing::debug!(
"Character '{}' triggered completion for language {} (lsp_trigger={}, word_char={}, quick_suggestions={})",
c,
language,
is_lsp_trigger,
is_word_char,
quick_suggestions_enabled
);
let _ = self.request_completion();
}
}
pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result =
handle.goto_definition(request_id, uri.clone(), line as u32, character as u32);
if result.is_ok() {
tracing::info!(
"Requested go-to-definition at {}:{}:{}",
uri.as_str(),
line,
character
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_goto_definition_request = Some(request_id);
}
Ok(())
}
pub(crate) fn request_hover(&mut self) -> AnyhowResult<()> {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
tracing::debug!(
"Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
cursor_pos,
pos.line,
pos.column,
character
);
}
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
if result.is_ok() {
tracing::info!(
"Requested hover at {}:{}:{} (byte_pos={})",
uri.as_str(),
line,
character,
cursor_pos
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_hover_request = Some(request_id);
self.lsp_status = "LSP: hover...".to_string();
}
Ok(())
}
pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<()> {
let state = self.active_state();
let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
tracing::trace!(
"Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
byte_pos,
pos.line,
pos.column,
character
);
}
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
if result.is_ok() {
tracing::trace!(
"Mouse hover requested at {}:{}:{} (byte_pos={})",
uri.as_str(),
line,
character,
byte_pos
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_hover_request = Some(request_id);
self.lsp_status = "LSP: hover...".to_string();
}
Ok(())
}
pub(crate) fn handle_hover_response(
&mut self,
request_id: u64,
contents: String,
is_markdown: bool,
range: Option<((u32, u32), (u32, u32))>,
) {
if self.pending_hover_request != Some(request_id) {
tracing::debug!("Ignoring stale hover response: {}", request_id);
return;
}
self.pending_hover_request = None;
self.lsp_status.clear();
if contents.is_empty() {
self.set_status_message(t!("lsp.no_hover").to_string());
self.hover_symbol_range = None;
return;
}
tracing::debug!(
"LSP hover content (markdown={}):\n{}",
is_markdown,
contents
);
if let Some(((start_line, start_char), (end_line, end_char))) = range {
let state = self.active_state();
let start_byte = state
.buffer
.lsp_position_to_byte(start_line as usize, start_char as usize);
let end_byte = state
.buffer
.lsp_position_to_byte(end_line as usize, end_char as usize);
self.hover_symbol_range = Some((start_byte, end_byte));
tracing::debug!(
"Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
start_byte,
end_byte,
start_line,
start_char,
end_line,
end_char
);
if let Some(old_handle) = self.hover_symbol_overlay.take() {
let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
self.apply_event_to_active_buffer(&remove_event);
}
let event = crate::model::event::Event::AddOverlay {
namespace: None,
range: start_byte..end_byte,
face: crate::model::event::OverlayFace::Background {
color: (80, 80, 120), },
priority: 90, message: None,
extend_to_line_end: false,
};
self.apply_event_to_active_buffer(&event);
if let Some(state) = self.buffers.get(&self.active_buffer()) {
self.hover_symbol_overlay = state.overlays.all().last().map(|o| o.handle.clone());
}
} else {
if let Some((hover_byte_pos, _, _, _)) = self.mouse_state.lsp_hover_state {
let state = self.active_state();
let start_byte = find_word_start(&state.buffer, hover_byte_pos);
let end_byte = find_word_end(&state.buffer, hover_byte_pos);
if start_byte < end_byte {
self.hover_symbol_range = Some((start_byte, end_byte));
tracing::debug!(
"Hover symbol range (computed from word boundaries): {}..{}",
start_byte,
end_byte
);
} else {
self.hover_symbol_range = None;
}
} else {
self.hover_symbol_range = None;
}
}
use crate::view::popup::{Popup, PopupPosition};
use ratatui::style::Style;
let mut popup = if is_markdown {
Popup::markdown(&contents, &self.theme, Some(&self.grammar_registry))
} else {
let lines: Vec<String> = contents.lines().map(|s| s.to_string()).collect();
Popup::text(lines, &self.theme)
};
popup.title = Some(t!("lsp.popup_hover").to_string());
popup.transient = true;
popup.position = if let Some((x, y)) = self.mouse_hover_screen_position.take() {
PopupPosition::Fixed { x, y: y + 1 }
} else {
PopupPosition::BelowCursor
};
popup.width = 80;
let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
popup.max_height = dynamic_height;
popup.border_style = Style::default().fg(self.theme.popup_border_fg);
popup.background_style = Style::default().bg(self.theme.popup_bg);
if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
state.popups.show(popup);
tracing::info!("Showing hover popup (markdown={})", is_markdown);
}
self.mouse_state.lsp_hover_request_sent = true;
}
pub(crate) fn apply_inlay_hints_to_state(
state: &mut crate::state::EditorState,
hints: &[lsp_types::InlayHint],
) {
use crate::view::virtual_text::VirtualTextPosition;
use ratatui::style::{Color, Style};
state.virtual_texts.clear(&mut state.marker_list);
if hints.is_empty() {
return;
}
let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
for hint in hints {
let byte_offset = state.buffer.lsp_position_to_byte(
hint.position.line as usize,
hint.position.character as usize,
);
let text = match &hint.label {
lsp_types::InlayHintLabel::String(s) => s.clone(),
lsp_types::InlayHintLabel::LabelParts(parts) => {
parts.iter().map(|p| p.value.as_str()).collect::<String>()
}
};
if state.buffer.is_empty() {
continue;
}
let (byte_offset, position) = if byte_offset >= state.buffer.len() {
(
state.buffer.len().saturating_sub(1),
VirtualTextPosition::AfterChar,
)
} else {
(byte_offset, VirtualTextPosition::BeforeChar)
};
let display_text = text;
state.virtual_texts.add(
&mut state.marker_list,
byte_offset,
display_text,
hint_style,
position,
0, );
}
tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
}
pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let symbol = {
let text = match state.buffer.to_string() {
Some(t) => t,
None => {
self.set_status_message(t!("error.buffer_not_loaded").to_string());
return Ok(());
}
};
let bytes = text.as_bytes();
let buf_len = bytes.len();
if cursor_pos <= buf_len {
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
let mut start = cursor_pos;
while start > 0 {
start -= 1;
while start > 0 && (bytes[start] & 0xC0) == 0x80 {
start -= 1;
}
if let Some(ch) = text[start..].chars().next() {
if !is_word_char(ch) {
start += ch.len_utf8();
break;
}
} else {
break;
}
}
let mut end = cursor_pos;
while end < buf_len {
if let Some(ch) = text[end..].chars().next() {
if is_word_char(ch) {
end += ch.len_utf8();
} else {
break;
}
} else {
break;
}
}
if start < end {
text[start..end].to_string()
} else {
String::new()
}
} else {
String::new()
}
};
let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result =
handle.references(request_id, uri.clone(), line as u32, character as u32);
if result.is_ok() {
tracing::info!(
"Requested find references at {}:{}:{} (byte_pos={})",
uri.as_str(),
line,
character,
cursor_pos
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_references_request = Some(request_id);
self.pending_references_symbol = symbol;
self.lsp_status = "LSP: finding references...".to_string();
}
Ok(())
}
pub(crate) fn request_signature_help(&mut self) -> AnyhowResult<()> {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result =
handle.signature_help(request_id, uri.clone(), line as u32, character as u32);
if result.is_ok() {
tracing::info!(
"Requested signature help at {}:{}:{} (byte_pos={})",
uri.as_str(),
line,
character,
cursor_pos
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_signature_help_request = Some(request_id);
self.lsp_status = "LSP: signature help...".to_string();
}
Ok(())
}
pub(crate) fn handle_signature_help_response(
&mut self,
request_id: u64,
signature_help: Option<lsp_types::SignatureHelp>,
) {
if self.pending_signature_help_request != Some(request_id) {
tracing::debug!("Ignoring stale signature help response: {}", request_id);
return;
}
self.pending_signature_help_request = None;
self.lsp_status.clear();
let signature_help = match signature_help {
Some(help) if !help.signatures.is_empty() => help,
_ => {
tracing::debug!("No signature help available");
return;
}
};
let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
let signature = match signature_help.signatures.get(active_signature_idx) {
Some(sig) => sig,
None => return,
};
let mut lines: Vec<String> = Vec::new();
lines.push(signature.label.clone());
let active_param = signature_help
.active_parameter
.or(signature.active_parameter)
.unwrap_or(0) as usize;
if let Some(params) = &signature.parameters {
if let Some(param) = params.get(active_param) {
let param_label = match ¶m.label {
lsp_types::ParameterLabel::Simple(s) => s.clone(),
lsp_types::ParameterLabel::LabelOffsets(offsets) => {
let start = offsets[0] as usize;
let end = offsets[1] as usize;
if end <= signature.label.len() {
signature.label[start..end].to_string()
} else {
String::new()
}
}
};
if !param_label.is_empty() {
lines.push(format!("> {}", param_label));
}
if let Some(doc) = ¶m.documentation {
let doc_text = match doc {
lsp_types::Documentation::String(s) => s.clone(),
lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
};
if !doc_text.is_empty() {
lines.push(String::new());
lines.push(doc_text);
}
}
}
}
if let Some(doc) = &signature.documentation {
let doc_text = match doc {
lsp_types::Documentation::String(s) => s.clone(),
lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
};
if !doc_text.is_empty() {
if lines.len() > 1 {
lines.push(String::new());
lines.push("---".to_string());
}
lines.push(doc_text);
}
}
use crate::view::popup::{Popup, PopupPosition};
use ratatui::style::Style;
let mut popup = Popup::text(lines, &self.theme);
popup.title = Some(t!("lsp.popup_signature").to_string());
popup.transient = true;
popup.position = PopupPosition::BelowCursor;
popup.width = 60;
popup.max_height = 10;
popup.border_style = Style::default().fg(self.theme.popup_border_fg);
popup.background_style = Style::default().bg(self.theme.popup_bg);
if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
state.popups.show(popup);
tracing::info!(
"Showing signature help popup for {} signatures",
signature_help.signatures.len()
);
}
}
pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
let (start_line, start_char, end_line, end_char) =
if let Some(range) = state.cursors.primary().selection_range() {
let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
(s_line as u32, s_char as u32, e_line as u32, e_char as u32)
} else {
(line as u32, character as u32, line as u32, character as u32)
};
let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result = handle.code_actions(
request_id,
uri.clone(),
start_line,
start_char,
end_line,
end_char,
diagnostics,
);
if result.is_ok() {
tracing::info!(
"Requested code actions at {}:{}:{}-{}:{} (byte_pos={})",
uri.as_str(),
start_line,
start_char,
end_line,
end_char,
cursor_pos
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_code_actions_request = Some(request_id);
self.lsp_status = "LSP: code actions...".to_string();
}
Ok(())
}
pub(crate) fn handle_code_actions_response(
&mut self,
request_id: u64,
actions: Vec<lsp_types::CodeActionOrCommand>,
) {
if self.pending_code_actions_request != Some(request_id) {
tracing::debug!("Ignoring stale code actions response: {}", request_id);
return;
}
self.pending_code_actions_request = None;
self.lsp_status.clear();
if actions.is_empty() {
self.set_status_message(t!("lsp.no_code_actions").to_string());
return;
}
let mut lines: Vec<String> = Vec::new();
lines.push(format!("Code Actions ({}):", actions.len()));
lines.push(String::new());
for (i, action) in actions.iter().enumerate() {
let title = match action {
lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
};
lines.push(format!(" {}. {}", i + 1, title));
}
lines.push(String::new());
lines.push(t!("lsp.code_action_hint").to_string());
use crate::view::popup::{Popup, PopupPosition};
use ratatui::style::Style;
let mut popup = Popup::text(lines, &self.theme);
popup.title = Some(t!("lsp.popup_code_actions").to_string());
popup.position = PopupPosition::BelowCursor;
popup.width = 60;
popup.max_height = 15;
popup.border_style = Style::default().fg(self.theme.popup_border_fg);
popup.background_style = Style::default().bg(self.theme.popup_bg);
if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
state.popups.show(popup);
tracing::info!("Showing code actions popup with {} actions", actions.len());
}
self.set_status_message(
t!("lsp.code_actions_not_implemented", count = actions.len()).to_string(),
);
}
pub(crate) fn handle_references_response(
&mut self,
request_id: u64,
locations: Vec<lsp_types::Location>,
) -> AnyhowResult<()> {
tracing::info!(
"handle_references_response: received {} locations for request_id={}",
locations.len(),
request_id
);
if self.pending_references_request != Some(request_id) {
tracing::debug!("Ignoring stale references response: {}", request_id);
return Ok(());
}
self.pending_references_request = None;
self.lsp_status.clear();
if locations.is_empty() {
self.set_status_message(t!("lsp.no_references").to_string());
return Ok(());
}
let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
.iter()
.map(|loc| {
let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
loc.uri.path().as_str().to_string()
} else {
loc.uri.as_str().to_string()
};
crate::services::plugins::hooks::LspLocation {
file,
line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
})
.collect();
let count = lsp_locations.len();
let symbol = std::mem::take(&mut self.pending_references_symbol);
self.set_status_message(
t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
);
self.plugin_manager.run_hook(
"lsp_references",
crate::services::plugins::hooks::HookArgs::LspReferences {
symbol: symbol.clone(),
locations: lsp_locations,
},
);
tracing::info!(
"Fired lsp_references hook with {} locations for symbol '{}'",
count,
symbol
);
Ok(())
}
pub(crate) fn apply_lsp_text_edits(
&mut self,
buffer_id: BufferId,
mut edits: Vec<lsp_types::TextEdit>,
) -> AnyhowResult<usize> {
if edits.is_empty() {
return Ok(0);
}
edits.sort_by(|a, b| {
b.range
.start
.line
.cmp(&a.range.start.line)
.then(b.range.start.character.cmp(&a.range.start.character))
});
let mut batch_events = Vec::new();
let mut changes = 0;
for edit in edits {
let state = self
.buffers
.get_mut(&buffer_id)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
let start_line = edit.range.start.line as usize;
let start_char = edit.range.start.character as usize;
let end_line = edit.range.end.line as usize;
let end_char = edit.range.end.character as usize;
let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
let buffer_len = state.buffer.len();
let old_text = if start_pos < end_pos && end_pos <= buffer_len {
state.get_text_range(start_pos, end_pos)
} else {
format!(
"<invalid range: start={}, end={}, buffer_len={}>",
start_pos, end_pos, buffer_len
)
};
tracing::debug!(
" Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
start_line, start_char, end_line, end_char,
start_pos, end_pos, old_text, edit.new_text
);
if start_pos < end_pos {
let deleted_text = state.get_text_range(start_pos, end_pos);
let cursor_id = state.cursors.primary_id();
let delete_event = Event::Delete {
range: start_pos..end_pos,
deleted_text,
cursor_id,
};
batch_events.push(delete_event);
}
if !edit.new_text.is_empty() {
let state = self
.buffers
.get(&buffer_id)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
let cursor_id = state.cursors.primary_id();
let insert_event = Event::Insert {
position: start_pos,
text: edit.new_text.clone(),
cursor_id,
};
batch_events.push(insert_event);
}
changes += 1;
}
if !batch_events.is_empty() {
self.apply_events_to_buffer_as_bulk_edit(
buffer_id,
batch_events,
"LSP Rename".to_string(),
)?;
}
Ok(changes)
}
pub fn handle_rename_response(
&mut self,
_request_id: u64,
result: Result<lsp_types::WorkspaceEdit, String>,
) -> AnyhowResult<()> {
self.lsp_status.clear();
match result {
Ok(workspace_edit) => {
tracing::debug!(
"Received WorkspaceEdit: changes={:?}, document_changes={:?}",
workspace_edit.changes.as_ref().map(|c| c.len()),
workspace_edit.document_changes.as_ref().map(|dc| match dc {
lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
lsp_types::DocumentChanges::Operations(o) =>
format!("{} operations", o.len()),
})
);
let mut total_changes = 0;
if let Some(changes) = workspace_edit.changes {
for (uri, edits) in changes {
if let Ok(path) = uri_to_path(&uri) {
let buffer_id = self.open_file(&path)?;
total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
}
}
}
if let Some(document_changes) = workspace_edit.document_changes {
use lsp_types::DocumentChanges;
let text_edits = match document_changes {
DocumentChanges::Edits(edits) => edits,
DocumentChanges::Operations(ops) => {
ops.into_iter()
.filter_map(|op| {
if let lsp_types::DocumentChangeOperation::Edit(edit) = op {
Some(edit)
} else {
None
}
})
.collect()
}
};
for text_doc_edit in text_edits {
let uri = text_doc_edit.text_document.uri;
if let Ok(path) = uri_to_path(&uri) {
let buffer_id = self.open_file(&path)?;
let edits: Vec<lsp_types::TextEdit> = text_doc_edit
.edits
.into_iter()
.map(|one_of| match one_of {
lsp_types::OneOf::Left(text_edit) => text_edit,
lsp_types::OneOf::Right(annotated) => annotated.text_edit,
})
.collect();
tracing::info!(
"Applying {} edits from rust-analyzer for {:?}:",
edits.len(),
path
);
for (i, edit) in edits.iter().enumerate() {
tracing::info!(
" Edit {}: line {}:{}-{}:{} -> {:?}",
i,
edit.range.start.line,
edit.range.start.character,
edit.range.end.line,
edit.range.end.character,
edit.new_text
);
}
total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
}
}
}
self.status_message = Some(t!("lsp.renamed", count = total_changes).to_string());
}
Err(error) => {
if error.contains("content modified") || error.contains("-32801") {
tracing::debug!(
"LSP rename: ContentModified error (expected, ignoring): {}",
error
);
self.status_message = Some(t!("lsp.rename_cancelled").to_string());
} else {
self.status_message = Some(t!("lsp.rename_failed", error = &error).to_string());
}
}
}
Ok(())
}
pub(crate) fn apply_events_to_buffer_as_bulk_edit(
&mut self,
buffer_id: BufferId,
events: Vec<Event>,
description: String,
) -> AnyhowResult<()> {
use crate::model::event::CursorId;
if events.is_empty() {
return Ok(());
}
let batch_for_lsp = Event::Batch {
events: events.clone(),
description: description.clone(),
};
let original_active = self.active_buffer();
self.split_manager.set_active_buffer_id(buffer_id);
let lsp_changes = self.collect_lsp_changes(&batch_for_lsp);
self.split_manager.set_active_buffer_id(original_active);
let state = self
.buffers
.get_mut(&buffer_id)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
let old_cursors: Vec<(CursorId, usize, Option<usize>)> = state
.cursors
.iter()
.map(|(id, c)| (id, c.position, c.anchor))
.collect();
let old_tree = state.buffer.snapshot_piece_tree();
let mut edits: Vec<(usize, usize, String)> = Vec::new();
for event in &events {
match event {
Event::Insert { position, text, .. } => {
edits.push((*position, 0, text.clone()));
}
Event::Delete { range, .. } => {
edits.push((range.start, range.len(), String::new()));
}
_ => {}
}
}
edits.sort_by(|a, b| b.0.cmp(&a.0));
let edit_refs: Vec<(usize, usize, &str)> = edits
.iter()
.map(|(pos, del, text)| (*pos, *del, text.as_str()))
.collect();
let _delta = state.buffer.apply_bulk_edits(&edit_refs);
let mut position_deltas: Vec<(usize, isize)> = Vec::new();
for (pos, del_len, text) in &edits {
let delta = text.len() as isize - *del_len as isize;
position_deltas.push((*pos, delta));
}
position_deltas.sort_by_key(|(pos, _)| *pos);
let calc_shift = |original_pos: usize| -> isize {
let mut shift: isize = 0;
for (edit_pos, delta) in &position_deltas {
if *edit_pos < original_pos {
shift += delta;
}
}
shift
};
let buffer_len = state.buffer.len();
let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
.iter()
.map(|(id, pos, anchor)| {
let shift = calc_shift(*pos);
let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
let new_anchor = anchor.map(|a| {
let anchor_shift = calc_shift(a);
((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
});
(*id, new_pos, new_anchor)
})
.collect();
for (cursor_id, new_pos, new_anchor) in &new_cursors {
if let Some(cursor) = state.cursors.get_mut(*cursor_id) {
cursor.position = *new_pos;
cursor.anchor = *new_anchor;
}
}
let new_tree = state.buffer.snapshot_piece_tree();
state.highlighter.invalidate_all();
let bulk_edit = Event::BulkEdit {
old_tree: Some(old_tree),
new_tree: Some(new_tree),
old_cursors,
new_cursors,
description,
};
if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
event_log.append(bulk_edit);
}
self.send_lsp_changes_for_buffer(buffer_id, lsp_changes);
Ok(())
}
pub(crate) fn send_lsp_changes_for_buffer(
&mut self,
buffer_id: BufferId,
changes: Vec<TextDocumentContentChangeEvent>,
) {
if changes.is_empty() {
return;
}
let metadata = match self.buffer_metadata.get(&buffer_id) {
Some(m) => m,
None => {
tracing::debug!(
"send_lsp_changes_for_buffer: no metadata for buffer {:?}",
buffer_id
);
return;
}
};
if !metadata.lsp_enabled {
tracing::debug!("send_lsp_changes_for_buffer: LSP disabled for this buffer");
return;
}
let uri = match metadata.file_uri() {
Some(u) => u.clone(),
None => {
tracing::debug!(
"send_lsp_changes_for_buffer: no URI for buffer (not a file or URI creation failed)"
);
return;
}
};
let path = match metadata.file_path() {
Some(p) => p,
None => {
tracing::debug!("send_lsp_changes_for_buffer: no file path for buffer");
return;
}
};
let language = match detect_language(path, &self.config.languages) {
Some(l) => l,
None => {
tracing::debug!(
"send_lsp_changes_for_buffer: no language detected for {:?}",
path
);
return;
}
};
tracing::trace!(
"send_lsp_changes_for_buffer: sending {} changes to {} in single didChange notification",
changes.len(),
uri.as_str()
);
use crate::services::lsp::manager::LspSpawnResult;
let Some(lsp) = self.lsp.as_mut() else {
tracing::debug!("send_lsp_changes_for_buffer: no LSP manager available");
return;
};
if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
tracing::debug!(
"send_lsp_changes_for_buffer: LSP not running for {} (auto_start disabled)",
language
);
return;
}
let Some(handle) = lsp.get_handle_mut(&language) else {
return;
};
let handle_id = handle.id();
let needs_open = {
let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
return;
};
!metadata.lsp_opened_with.contains(&handle_id)
};
if needs_open {
let text = match self
.buffers
.get(&buffer_id)
.and_then(|s| s.buffer.to_string())
{
Some(t) => t,
None => {
tracing::debug!(
"send_lsp_changes_for_buffer: buffer text not available for didOpen"
);
return;
}
};
let Some(lsp) = self.lsp.as_mut() else { return };
let Some(handle) = lsp.get_handle_mut(&language) else {
return;
};
if let Err(e) = handle.did_open(uri.clone(), text, language.clone()) {
tracing::warn!("Failed to send didOpen before didChange: {}", e);
return;
}
tracing::debug!(
"Sent didOpen for {} to LSP handle {} before didChange",
uri.as_str(),
handle_id
);
if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
metadata.lsp_opened_with.insert(handle_id);
}
}
let Some(lsp) = self.lsp.as_mut() else { return };
let Some(client) = lsp.get_handle_mut(&language) else {
return;
};
if let Err(e) = client.did_change(uri, changes) {
tracing::warn!("Failed to send didChange to LSP: {}", e);
} else {
tracing::trace!("Successfully sent batched didChange to LSP");
}
}
pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
use crate::primitives::word_navigation::{find_word_end, find_word_start};
let (word_start, word_end) = {
let state = self.active_state();
let cursor_pos = state.cursors.primary().position;
let word_start = find_word_start(&state.buffer, cursor_pos);
let word_end = find_word_end(&state.buffer, cursor_pos);
if word_start >= word_end {
self.status_message = Some(t!("lsp.no_symbol_at_cursor").to_string());
return Ok(());
}
(word_start, word_end)
};
let word_text = self.active_state_mut().get_text_range(word_start, word_end);
let overlay_handle = self.add_overlay(
None,
word_start..word_end,
crate::model::event::OverlayFace::Background {
color: (50, 100, 200), },
100,
Some(t!("lsp.popup_renaming").to_string()),
);
let mut prompt = Prompt::new(
"Rename to: ".to_string(),
PromptType::LspRename {
original_text: word_text.clone(),
start_pos: word_start,
end_pos: word_end,
overlay_handle,
},
);
prompt.set_input(word_text);
self.prompt = Some(prompt);
Ok(())
}
pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
self.remove_overlay(handle.clone());
}
pub(crate) fn perform_lsp_rename(
&mut self,
new_name: String,
original_text: String,
start_pos: usize,
overlay_handle: crate::view::overlay::OverlayHandle,
) {
self.cancel_rename_overlay(&overlay_handle);
if new_name == original_text {
self.status_message = Some(t!("lsp.name_unchanged").to_string());
return;
}
let rename_pos = start_pos;
let state = self.active_state();
let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
let buffer_id = self.active_buffer();
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result = handle.rename(
request_id,
uri.clone(),
line as u32,
character as u32,
new_name.clone(),
);
if result.is_ok() {
tracing::info!(
"Requested rename at {}:{}:{} to '{}'",
uri.as_str(),
line,
character,
new_name
);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.lsp_status = "LSP: rename...".to_string();
} else if self
.buffer_metadata
.get(&buffer_id)
.and_then(|m| m.file_path())
.is_none()
{
self.status_message = Some(t!("lsp.cannot_rename_unsaved").to_string());
}
}
pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
if !self.config.editor.enable_inlay_hints {
return;
}
let buffer_id = self.active_buffer();
let line_count = if let Some(state) = self.buffers.get(&buffer_id) {
state.buffer.line_count().unwrap_or(1000)
} else {
return;
};
let last_line = line_count.saturating_sub(1) as u32;
let request_id = self.next_lsp_request_id;
let sent = self
.with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
let result = handle.inlay_hints(request_id, uri.clone(), 0, 0, last_line, 10000);
if result.is_ok() {
tracing::info!(
"Requested inlay hints for {} (request_id={})",
uri.as_str(),
request_id
);
} else if let Err(e) = &result {
tracing::debug!("Failed to request inlay hints: {}", e);
}
result.is_ok()
})
.unwrap_or(false);
if sent {
self.next_lsp_request_id += 1;
self.pending_inlay_hints_request = Some(request_id);
}
}
pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
if !self.config.editor.enable_semantic_tokens_full {
return;
}
if self.semantic_tokens_in_flight.contains_key(&buffer_id) {
return;
}
let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
return;
};
if !metadata.lsp_enabled {
return;
}
let Some(uri) = metadata.file_uri().cloned() else {
return;
};
let Some(path) = metadata.file_path() else {
return;
};
let Some(language) = detect_language(path, &self.config.languages) else {
return;
};
let Some(lsp) = self.lsp.as_mut() else {
return;
};
if !lsp.semantic_tokens_full_supported(&language) {
return;
}
if lsp.semantic_tokens_legend(&language).is_none() {
return;
}
use crate::services::lsp::manager::LspSpawnResult;
if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
return;
}
let Some(state) = self.buffers.get(&buffer_id) else {
return;
};
let buffer_version = state.buffer.version();
if let Some(store) = state.semantic_tokens.as_ref() {
if store.version == buffer_version {
return; }
}
let previous_result_id = state
.semantic_tokens
.as_ref()
.and_then(|store| store.result_id.clone());
let supports_delta = lsp.semantic_tokens_full_delta_supported(&language);
let use_delta = previous_result_id.is_some() && supports_delta;
let Some(handle) = lsp.get_handle_mut(&language) else {
return;
};
let request_id = self.next_lsp_request_id;
self.next_lsp_request_id += 1;
let request_kind = if use_delta {
super::SemanticTokensFullRequestKind::FullDelta
} else {
super::SemanticTokensFullRequestKind::Full
};
let request_result = if use_delta {
handle.semantic_tokens_full_delta(request_id, uri, previous_result_id.unwrap())
} else {
handle.semantic_tokens_full(request_id, uri)
};
match request_result {
Ok(_) => {
self.pending_semantic_token_requests.insert(
request_id,
super::SemanticTokenFullRequest {
buffer_id,
version: buffer_version,
kind: request_kind,
},
);
self.semantic_tokens_in_flight
.insert(buffer_id, (request_id, buffer_version, request_kind));
}
Err(e) => {
tracing::debug!("Failed to request semantic tokens: {}", e);
}
}
}
pub(crate) fn schedule_semantic_tokens_full_refresh(&mut self, buffer_id: BufferId) {
if !self.config.editor.enable_semantic_tokens_full {
return;
}
let next_time = Instant::now() + Duration::from_millis(SEMANTIC_TOKENS_FULL_DEBOUNCE_MS);
self.semantic_tokens_full_debounce
.insert(buffer_id, next_time);
}
pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
if !self.config.editor.enable_semantic_tokens_full {
self.semantic_tokens_full_debounce.remove(&buffer_id);
return;
}
let Some(ready_at) = self.semantic_tokens_full_debounce.get(&buffer_id).copied() else {
return;
};
if Instant::now() < ready_at {
return;
}
self.semantic_tokens_full_debounce.remove(&buffer_id);
self.maybe_request_semantic_tokens(buffer_id);
}
pub(crate) fn maybe_request_semantic_tokens_range(
&mut self,
buffer_id: BufferId,
start_line: usize,
end_line: usize,
) {
let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
return;
};
if !metadata.lsp_enabled {
return;
}
let Some(uri) = metadata.file_uri().cloned() else {
return;
};
let Some(path) = metadata.file_path() else {
return;
};
let Some(language) = detect_language(path, &self.config.languages) else {
return;
};
let Some(lsp) = self.lsp.as_mut() else {
return;
};
if !lsp.semantic_tokens_range_supported(&language) {
self.maybe_request_semantic_tokens(buffer_id);
return;
}
if lsp.semantic_tokens_legend(&language).is_none() {
return;
}
use crate::services::lsp::manager::LspSpawnResult;
if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
return;
}
let Some(handle) = lsp.get_handle_mut(&language) else {
return;
};
let Some(state) = self.buffers.get(&buffer_id) else {
return;
};
let buffer_version = state.buffer.version();
let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
if let Some(line_count) = state.buffer.line_count() {
if line_count == 0 {
return;
}
let max_line = line_count.saturating_sub(1);
padded_start = padded_start.min(max_line);
padded_end = padded_end.min(max_line);
}
let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
let end_char = state
.buffer
.get_line(padded_end)
.map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
.unwrap_or(0);
let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
state.buffer.lsp_position_to_byte(padded_end, end_char)
} else {
state.buffer.len()
};
if start_byte >= end_byte {
return;
}
let range = start_byte..end_byte;
if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
self.semantic_tokens_range_in_flight.get(&buffer_id)
{
if *in_flight_start == padded_start
&& *in_flight_end == padded_end
&& *in_flight_version == buffer_version
{
return;
}
if let Err(e) = handle.cancel_request(*in_flight_id) {
tracing::debug!("Failed to cancel semantic token range request: {}", e);
}
self.pending_semantic_token_range_requests
.remove(in_flight_id);
self.semantic_tokens_range_in_flight.remove(&buffer_id);
}
if let Some((applied_start, applied_end, applied_version)) =
self.semantic_tokens_range_applied.get(&buffer_id)
{
if *applied_start == padded_start
&& *applied_end == padded_end
&& *applied_version == buffer_version
{
return;
}
}
let now = Instant::now();
if let Some((last_start, last_end, last_version, last_time)) =
self.semantic_tokens_range_last_request.get(&buffer_id)
{
if *last_start == padded_start
&& *last_end == padded_end
&& *last_version == buffer_version
&& now.duration_since(*last_time)
< Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
{
return;
}
}
let lsp_range = lsp_types::Range {
start: lsp_types::Position {
line: padded_start as u32,
character: 0,
},
end: lsp_types::Position {
line: padded_end as u32,
character: end_char as u32,
},
};
let request_id = self.next_lsp_request_id;
self.next_lsp_request_id += 1;
match handle.semantic_tokens_range(request_id, uri, lsp_range) {
Ok(_) => {
self.pending_semantic_token_range_requests.insert(
request_id,
SemanticTokenRangeRequest {
buffer_id,
version: buffer_version,
range: range.clone(),
start_line: padded_start,
end_line: padded_end,
},
);
self.semantic_tokens_range_in_flight.insert(
buffer_id,
(request_id, padded_start, padded_end, buffer_version),
);
self.semantic_tokens_range_last_request
.insert(buffer_id, (padded_start, padded_end, buffer_version, now));
}
Err(e) => {
tracing::debug!("Failed to request semantic token range: {}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::Editor;
use crate::model::buffer::Buffer;
use crate::state::EditorState;
use crate::view::virtual_text::VirtualTextPosition;
use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
InlayHint {
position: Position { line, character },
label: InlayHintLabel::String(label.to_string()),
kind,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}
}
#[test]
fn test_inlay_hint_inserts_before_character() {
let mut state =
EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
state.buffer = Buffer::from_str_test("ab");
if !state.buffer.is_empty() {
state.marker_list.adjust_for_insert(0, state.buffer.len());
}
let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
Editor::apply_inlay_hints_to_state(&mut state, &hints);
let lookup = state
.virtual_texts
.build_lookup(&state.marker_list, 0, state.buffer.len());
let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
assert_eq!(vtexts.len(), 1);
assert_eq!(vtexts[0].text, ": i32");
assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
}
#[test]
fn test_inlay_hint_at_eof_renders_after_last_char() {
let mut state =
EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
state.buffer = Buffer::from_str_test("ab");
if !state.buffer.is_empty() {
state.marker_list.adjust_for_insert(0, state.buffer.len());
}
let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
Editor::apply_inlay_hints_to_state(&mut state, &hints);
let lookup = state
.virtual_texts
.build_lookup(&state.marker_list, 0, state.buffer.len());
let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
assert_eq!(vtexts.len(), 1);
assert_eq!(vtexts[0].text, ": i32");
assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
}
#[test]
fn test_inlay_hint_empty_buffer_is_ignored() {
let mut state =
EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
state.buffer = Buffer::from_str_test("");
let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
Editor::apply_inlay_hints_to_state(&mut state, &hints);
assert!(state.virtual_texts.is_empty());
}
}