1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//! LSP change-notification orchestrators on `Editor`.
//!
//! When buffers mutate, the LSP server needs to be notified so its
//! analysis stays in sync. These methods translate Editor `Event`s into
//! `TextDocumentContentChangeEvent`s, compute line-shift metadata for
//! plugin hooks, and send `did_save` notifications.
use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
use crate::model::event::{BufferId, Event};
use super::Editor;
impl Editor {
// === LSP Diagnostics Display ===
// NOTE: Diagnostics are now applied automatically via process_async_messages()
// when received from the LSP server asynchronously. No manual polling needed!
/// Collect all LSP text document changes from an event (recursively for batches)
pub(super) fn collect_lsp_changes(&self, event: &Event) -> Vec<TextDocumentContentChangeEvent> {
match event {
Event::Insert { position, text, .. } => {
tracing::trace!(
"collect_lsp_changes: processing Insert at position {}",
position
);
// For insert: create a zero-width range at the insertion point
let (line, character) = self
.active_state()
.buffer
.position_to_lsp_position(*position);
let lsp_pos = Position::new(line as u32, character as u32);
let lsp_range = LspRange::new(lsp_pos, lsp_pos);
vec![TextDocumentContentChangeEvent {
range: Some(lsp_range),
range_length: None,
text: text.clone(),
}]
}
Event::Delete { range, .. } => {
tracing::trace!("collect_lsp_changes: processing Delete range {:?}", range);
// For delete: create a range from start to end, send empty string
let (start_line, start_char) = self
.active_state()
.buffer
.position_to_lsp_position(range.start);
let (end_line, end_char) = self
.active_state()
.buffer
.position_to_lsp_position(range.end);
let lsp_range = LspRange::new(
Position::new(start_line as u32, start_char as u32),
Position::new(end_line as u32, end_char as u32),
);
vec![TextDocumentContentChangeEvent {
range: Some(lsp_range),
range_length: None,
text: String::new(),
}]
}
Event::Batch { events, .. } => {
// Collect all changes from sub-events into a single vector
// This allows sending all changes in one didChange notification
tracing::trace!(
"collect_lsp_changes: processing Batch with {} events",
events.len()
);
let mut all_changes = Vec::new();
for sub_event in events {
all_changes.extend(self.collect_lsp_changes(sub_event));
}
all_changes
}
_ => Vec::new(), // Ignore cursor movements and other events
}
}
/// Calculate line information for an event (before buffer modification)
/// This provides accurate line numbers for plugin hooks to track changes.
///
/// ## Design Alternatives for Line Tracking
///
/// **Approach 1: Re-diff on every edit (VSCode style)**
/// - Store original file content, re-run diff algorithm after each edit
/// - Simpler conceptually, but O(n) per edit for diff computation
/// - Better for complex scenarios (multi-cursor, large batch edits)
///
/// **Approach 2: Track line shifts (our approach)**
/// - Calculate line info BEFORE applying edit (like LSP does)
/// - Pass `lines_added`/`lines_removed` to plugins via hooks
/// - Plugins shift their stored line numbers accordingly
/// - O(1) per edit, but requires careful bookkeeping
///
/// We use Approach 2 because:
/// - Matches existing LSP infrastructure (`collect_lsp_changes`)
/// - More efficient for typical editing patterns
/// - Plugins can choose to re-diff if they need more accuracy
///
pub(super) fn calculate_event_line_info(&self, event: &Event) -> super::types::EventLineInfo {
match event {
Event::Insert { position, text, .. } => {
// Get line number at insert position (from original buffer)
let start_line = self.active_state().buffer.get_line_number(*position);
// Count newlines in inserted text to determine lines added
let lines_added = text.matches('\n').count();
let end_line = start_line + lines_added;
super::types::EventLineInfo {
start_line,
end_line,
line_delta: lines_added as i32,
}
}
Event::Delete {
range,
deleted_text,
..
} => {
// Get line numbers for the deleted range (from original buffer)
let start_line = self.active_state().buffer.get_line_number(range.start);
let end_line = self.active_state().buffer.get_line_number(range.end);
// Count newlines in deleted text to determine lines removed
let lines_removed = deleted_text.matches('\n').count();
super::types::EventLineInfo {
start_line,
end_line,
line_delta: -(lines_removed as i32),
}
}
Event::Batch { events, .. } => {
// For batches, compute cumulative line info
// This is a simplification - we report the range covering all changes
let mut min_line = usize::MAX;
let mut max_line = 0usize;
let mut total_delta = 0i32;
for sub_event in events {
let info = self.calculate_event_line_info(sub_event);
min_line = min_line.min(info.start_line);
max_line = max_line.max(info.end_line);
total_delta += info.line_delta;
}
if min_line == usize::MAX {
min_line = 0;
}
super::types::EventLineInfo {
start_line: min_line,
end_line: max_line,
line_delta: total_delta,
}
}
_ => super::types::EventLineInfo::default(),
}
}
/// Notify LSP of a file save
pub(super) fn notify_lsp_save(&mut self) {
let buffer_id = self.active_buffer();
self.notify_lsp_save_buffer(buffer_id);
}
/// Notify LSP of a file save for a specific buffer
pub(super) fn notify_lsp_save_buffer(&mut self, buffer_id: BufferId) {
// Check if LSP is enabled for this buffer
let metadata = match self.buffer_metadata.get(&buffer_id) {
Some(m) => m,
None => {
tracing::debug!(
"notify_lsp_save_buffer: no metadata for buffer {:?}",
buffer_id
);
return;
}
};
if !metadata.lsp_enabled {
tracing::debug!(
"notify_lsp_save_buffer: LSP disabled for buffer {:?}",
buffer_id
);
return;
}
// Get file path for LSP spawn
let file_path = metadata.file_path().cloned();
// Get the URI
let uri = match metadata.file_uri() {
Some(u) => u.clone(),
None => {
tracing::debug!("notify_lsp_save_buffer: no URI for buffer {:?}", buffer_id);
return;
}
};
// Get the file path for language detection
// Use buffer's stored language
let language = match self
.buffers
.get(&self.active_buffer())
.map(|s| s.language.clone())
{
Some(l) => l,
None => {
tracing::debug!("notify_lsp_save: no buffer state");
return;
}
};
// Get the full text to send with didSave
let full_text = match self.active_state().buffer.to_string() {
Some(t) => t,
None => {
tracing::debug!("notify_lsp_save: buffer not fully loaded");
return;
}
};
tracing::debug!(
"notify_lsp_save: sending didSave to {} (text length: {} bytes)",
uri.as_str(),
full_text.len()
);
// Only send didSave if LSP is already running (respect auto_start setting)
if let Some(lsp) = &mut self.lsp {
use crate::services::lsp::manager::LspSpawnResult;
if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
tracing::debug!(
"notify_lsp_save: LSP not running for {} (auto_start disabled)",
language
);
return;
}
// Broadcast didSave to all handles for this language
let mut any_sent = false;
for sh in lsp.get_handles_mut(&language) {
if let Err(e) = sh.handle.did_save(uri.clone(), Some(full_text.clone())) {
tracing::warn!("Failed to send didSave to '{}': {}", sh.name, e);
} else {
any_sent = true;
}
}
if any_sent {
tracing::info!("Successfully sent didSave to LSP");
} else {
tracing::warn!("notify_lsp_save: no LSP handles for {}", language);
}
} else {
tracing::debug!("notify_lsp_save: no LSP manager available");
}
}
}