1use std::collections::HashMap;
5use std::path::PathBuf;
6
7use strop_core::id::{BufferRevision, ByteColumn, DocumentId, LineIndex};
8use strop_lsp::{
9 Client, ReplyContext, RequestInput, RequestKind, RequestRefusal, RequestStamp, ServerId,
10};
11
12use super::super::Editor;
13use super::attach::AttachState;
14
15pub(crate) struct Binding {
16 pub server: ServerId,
17 pub path: PathBuf,
18 pub root: PathBuf,
19 pub target: strop_workspace::Filesystem,
22 pub revision: BufferRevision,
23}
24
25#[derive(Debug, serde::Serialize)]
29pub(crate) struct SyncArgs {
30 pub server: ServerId,
31 pub document: DocumentId,
32 pub revision: BufferRevision,
33 #[serde(with = "strop_core::path_serde")]
34 pub path: PathBuf,
35 pub bytes: usize,
36}
37
38#[derive(Debug, serde::Serialize)]
39pub(crate) struct CloseArgs {
40 pub server: ServerId,
41 pub document: DocumentId,
42 #[serde(with = "strop_core::path_serde")]
43 pub path: PathBuf,
44}
45
46pub(crate) struct LspState {
47 pub bindings: HashMap<DocumentId, Binding>,
48 pub hover: Option<RequestStamp>,
49 pub navigation: Option<RequestStamp>,
50 pub attach: AttachState,
51}
52
53impl Default for LspState {
54 fn default() -> Self {
55 Self {
56 bindings: HashMap::new(),
57 hover: None,
58 navigation: None,
59 attach: AttachState::new(),
60 }
61 }
62}
63
64impl Editor {
65 fn lsp_live_client(&self, server: ServerId) -> Option<Client> {
66 self.lsp_servers
67 .iter()
68 .find(|s| s.id == server)
69 .and_then(|s| s.client.clone())
70 }
71
72 pub(super) fn lsp_did_open_current(&mut self) {
73 let document = self.current();
74 let Some(doc) = self.lsp_current_doc_path() else {
75 return;
76 };
77 let Some(language) = super::lsp_language(&doc.path) else {
78 return;
79 };
80 let Some((server, root)) = self.lsp_server_for(&doc.path, language, &doc.filesystem) else {
81 return;
82 };
83 if let Some(binding) = self.lsp_state.bindings.get(&document) {
84 if binding.server == server
85 && binding.path == doc.path
86 && binding.target == doc.filesystem
87 {
88 return;
89 }
90 self.lsp_close_document(document);
91 }
92 let revision = self.buf().revision();
93 let text = self.buf().snapshot();
94 let args = SyncArgs {
95 server,
96 document,
97 revision,
98 path: doc.path.clone(),
99 bytes: text.len_bytes(),
100 };
101 let opened = self.tape.call("lsp.open", &args, || {
104 self.lsp_live_client(server).map(|client| {
105 client.did_open(
106 document,
107 revision,
108 &doc.path,
109 super::lang_id(&doc.path),
110 text,
111 )
112 })
113 });
114 match opened {
115 Ok(Some(true)) => {
116 self.lsp_state.bindings.insert(
117 document,
118 Binding {
119 server,
120 path: doc.path,
121 root,
122 target: doc.filesystem,
123 revision,
124 },
125 );
126 }
127 Ok(_) => {}
129 Err(error) => self.message = format!("lsp open diverged from trace: {error}"),
130 }
131 }
132
133 pub fn lsp_sync_changed(&mut self) {
134 let mut changed: Vec<_> = self
137 .lsp_state
138 .bindings
139 .iter()
140 .filter_map(|(&id, binding)| {
141 let doc = self.docs.get(id)?;
142 let revision = doc.buf.revision();
143 (revision != binding.revision).then(|| {
144 (
145 id,
146 binding.server,
147 binding.path.clone(),
148 revision,
149 doc.buf.snapshot(),
150 )
151 })
152 })
153 .collect();
154 changed.sort_by_key(|(id, _, _, _, _)| *id);
155 for (document, server, path, revision, text) in changed {
156 let args = SyncArgs {
157 server,
158 document,
159 revision,
160 path: path.clone(),
161 bytes: text.len_bytes(),
162 };
163 match self.tape.call("lsp.change", &args, || {
164 self.lsp_live_client(server)
165 .map(|client| client.did_change(document, revision, &path, text))
166 }) {
167 Ok(Some(true)) => {
168 if let Some(binding) = self.lsp_state.bindings.get_mut(&document) {
169 binding.revision = revision;
170 }
171 }
172 Ok(_) => self.message = "lsp: document change refused".into(),
173 Err(error) => self.message = error.to_string(),
174 }
175 }
176 }
177
178 pub(crate) fn lsp_close_document(&mut self, document: DocumentId) {
179 self.diags.remove(&document);
180 if !self.docs.is_empty() && document == self.current() {
181 self.hover_card = None;
182 }
183 if let Some(binding) = self.lsp_state.bindings.remove(&document) {
186 let args = CloseArgs {
187 server: binding.server,
188 document,
189 path: binding.path.clone(),
190 };
191 match self.tape.request("lsp.close", &args) {
192 Ok(true) => {
193 if let Some(client) = self.lsp_live_client(binding.server) {
194 client.did_close(document, &binding.path);
195 }
196 }
197 Ok(false) => {}
198 Err(error) => self.message = format!("lsp close diverged from trace: {error}"),
199 }
200 }
201 if self.lsp_state.hover.is_some_and(|r| r.document == document) {
202 self.lsp_state.hover = None;
203 self.hover_card = None;
204 }
205 if self
206 .lsp_state
207 .navigation
208 .is_some_and(|r| r.document == document)
209 {
210 self.lsp_state.navigation = None;
211 }
212 if self
213 .picker
214 .as_ref()
215 .and_then(|p| p.lsp_context)
216 .is_some_and(|c| c.stamp.document == document)
217 {
218 self.close_picker();
219 }
220 self.lsp_retire_remote_servers();
222 }
223
224 pub(crate) fn lsp_reply_fresh(&self, context: &ReplyContext) -> bool {
225 let stamp = context.stamp;
226 let expected = if context.kind == RequestKind::Hover {
227 self.lsp_state.hover
228 } else {
229 self.lsp_state.navigation
230 };
231 expected == Some(stamp) && self.lsp_context_fresh(context)
232 }
233
234 pub(crate) fn lsp_context_fresh(&self, context: &ReplyContext) -> bool {
238 let stamp = context.stamp;
239 let newer = if context.kind == RequestKind::Hover {
240 self.lsp_state.hover
241 } else {
242 self.lsp_state.navigation
243 };
244 !self.docs.is_empty()
245 && stamp.document == self.current()
246 && newer.is_none_or(|owner| owner == stamp)
247 && self
248 .docs
249 .get(stamp.document)
250 .is_some_and(|d| d.buf.revision() == stamp.revision)
251 && self
252 .lsp_state
253 .bindings
254 .get(&stamp.document)
255 .is_some_and(|b| b.server == stamp.server && b.revision == stamp.revision)
256 }
257
258 pub(super) fn finish_lsp_reply(&mut self, context: &ReplyContext) -> bool {
259 let fresh = self.lsp_reply_fresh(context);
260 let slot = if context.kind == RequestKind::Hover {
261 &mut self.lsp_state.hover
262 } else {
263 &mut self.lsp_state.navigation
264 };
265 if *slot == Some(context.stamp) {
266 *slot = None;
267 }
268 fresh
269 }
270
271 pub(super) fn lsp_request(&mut self, kind: RequestKind) {
272 self.lsp_request_with(kind, None);
273 }
274
275 pub(super) fn lsp_change_request(&mut self, kind: RequestKind, rename_to: Option<String>) {
280 self.lsp_request_with(kind, rename_to);
281 }
282
283 fn lsp_request_with(&mut self, kind: RequestKind, rename_to: Option<String>) {
284 let hover = kind == RequestKind::Hover;
285 if hover {
286 self.lsp_state.hover = None;
287 } else {
288 self.lsp_state.navigation = None;
289 self.cancel_open(strop_core::worker::CancelReason::Superseded);
290 }
291 let Some(doc) = self.lsp_current_doc_path() else {
292 self.message =
293 "language services require a complete file buffer, not a partial/follow view"
294 .into();
295 return;
296 };
297 let Some(language) = super::lsp_language(&doc.path) else {
298 self.message = "no language server for this file type".into();
299 return;
300 };
301 let Some((server, _)) = self.lsp_server_for(&doc.path, language, &doc.filesystem) else {
302 match doc.filesystem {
303 strop_workspace::Filesystem::Local => {
304 self.message = "no language server — install it or fix languages.toml".into()
305 }
306 strop_workspace::Filesystem::Remote(endpoint) => {
307 self.message = format!(
308 "no language server on {endpoint} — install it there or fix languages.toml"
309 )
310 }
311 strop_workspace::Filesystem::Container(_) => {
312 self.message = "language services in containers are not wired yet".into()
313 }
314 }
315 return;
316 };
317 self.lsp_did_open_current();
318 self.lsp_sync_changed();
319 let Some(doc) = self.lsp_current_doc_path() else {
320 return;
321 };
322 let line = self.buf().line_of(self.head());
323 let input = RequestInput {
324 document: self.current(),
325 revision: self.buf().revision(),
326 path: doc.path.clone(),
327 line: LineIndex::new(line),
328 byte_col: ByteColumn::new(self.buf().col_of(self.head())),
329 line_text: strop_lsp::FrozenLine::from_slice(
330 self.buf()
331 .text()
332 .byte_slice(self.buf().line_start(line)..self.buf().line_end(line)),
333 ),
334 kind,
335 rename_to,
336 };
337 let native_input = input.clone();
338 let prepared = self.tape.call("lsp.prepare", &input, || {
339 let client = self
340 .lsp_live_client(server)
341 .ok_or(RequestRefusal::NotOpen)?;
342 client.prepare_request(native_input)
343 });
344 match prepared {
345 Ok(Ok(mut prepared)) => {
346 if kind == RequestKind::Format {
347 prepared.tab_width = Some(self.config.tab_size);
350 }
351 if hover {
354 self.lsp_state.hover = Some(prepared.stamp);
355 } else {
356 self.lsp_state.navigation = Some(prepared.stamp);
357 }
358 if matches!(
359 kind,
360 RequestKind::Locations(_)
361 | RequestKind::Format
362 | RequestKind::Rename
363 | RequestKind::CodeAction
364 ) {
365 let label = match kind {
366 RequestKind::Locations(k) => k.label(),
367 other => other.label(),
368 };
369 self.message = format!("lsp: {label} …");
370 }
371 match self.tape.request("lsp.launch", &prepared) {
372 Ok(true) => {
373 if let Some(client) = self.lsp_live_client(server) {
374 client.launch_request(prepared);
375 }
376 }
377 Ok(false) => {}
378 Err(error) => {
379 if hover {
380 self.lsp_state.hover = None;
381 } else {
382 self.lsp_state.navigation = None;
383 }
384 self.message = format!("lsp request diverged from trace: {error}");
385 }
386 }
387 }
388 Ok(Err(refusal)) => {
389 self.message = match refusal {
390 RequestRefusal::NotOpen => {
391 "lsp: the document is not open on this server".into()
392 }
393 RequestRefusal::StaleRevision => format!(
394 "lsp: buffer changed while syncing — repeat {}",
395 kind.label()
396 ),
397 RequestRefusal::Unsupported => {
398 format!("lsp: {} is not supported by this server", kind.label())
399 }
400 RequestRefusal::IdentityExhausted => "lsp: request identities exhausted".into(),
401 };
402 }
403 Err(error) => self.message = format!("lsp prepare diverged from trace: {error}"),
404 }
405 }
406
407 pub(super) fn lsp_failed(&mut self, server: ServerId) {
408 let mut docs: Vec<_> = self
409 .lsp_state
410 .bindings
411 .iter()
412 .filter_map(|(&id, b)| (b.server == server).then_some(id))
413 .collect();
414 docs.sort();
415 for document in docs {
416 self.lsp_close_document(document);
417 }
418 self.lsp_state
419 .attach
420 .attached
421 .retain(|a| a.server != server);
422 if let Some(index) = self.lsp_servers.iter().position(|s| s.id == server) {
423 let connection = self.lsp_servers.remove(index);
424 if let Some(client) = connection.client {
425 std::thread::spawn(move || {
427 client.shutdown();
428 client.wait(std::time::Duration::from_secs(2));
429 });
430 }
431 }
432 }
433}