1use super::{trace, Editor};
8use std::path::{Path, PathBuf};
9use std::sync::mpsc::Receiver;
10
11use strop_lsp::registry;
12use strop_lsp::{LspEvent, ServerId};
13use strop_workspace::{Filesystem, ResourceLocation};
14
15pub(crate) mod attach;
16mod lifecycle;
17mod navigation;
18pub(crate) mod remote;
19mod routing;
20pub(crate) mod state;
21#[cfg(test)]
22mod tests;
23
24pub struct LspServer {
25 pub id: ServerId,
26 pub client: Option<strop_lsp::Client>,
29 pub rx: Receiver<LspEvent>,
30 pub ready: bool,
31}
32
33impl Editor {
34 pub(crate) fn open_hover_document(&mut self) {
35 let Some(text) = self.hover_card.take() else {
36 return;
37 };
38 self.push_jump();
39 let mut document = super::Document::documentation(strop_core::Buffer::from_text(&text));
40 document.set_return_point(self.jump_record());
41 let id = self.docs.insert(document);
42 self.switch_to(id);
43 self.set_head(0);
44 self.view_mut().view_top = 0;
45 self.message = "documentation: search/scroll normally; Ctrl-O returns".into();
46 }
47}
48
49impl Editor {
50 pub(super) fn lsp_current_doc_path(&self) -> Option<ResourceLocation> {
55 if self.cur().remote_metadata().is_some() && !self.remote_window_complete() {
56 return None;
57 }
58 self.lsp_doc_path(self.current())
59 }
60
61 fn lsp_doc_path(&self, document: strop_core::id::DocumentId) -> Option<ResourceLocation> {
62 let document = self.docs.get(document)?;
63 match &document.source {
64 crate::editor::document::DocumentSource::Remote(file) => {
65 Some(ResourceLocation::remote(
66 file.file.endpoint().clone(),
67 file.file.path().to_path_buf(),
68 ))
69 }
70 crate::editor::document::DocumentSource::Container { container, path } => {
71 Some(ResourceLocation {
72 filesystem: strop_workspace::Filesystem::Container(container.clone()),
73 path: path.clone(),
74 })
75 }
76 _ => document
77 .buf
78 .path
79 .as_ref()
80 .map(|path| ResourceLocation::local(self.cwd.join(path))),
81 }
82 }
83
84 pub(super) fn remote_file_for(
87 &self,
88 endpoint: &strop_workspace::RemoteEndpoint,
89 ) -> Option<strop_workspace::RemoteFile> {
90 self.docs.iter().find_map(|(_, document)| {
91 match &document.source {
92 crate::editor::document::DocumentSource::Remote(source) => Some(&source.file),
93 _ => None,
94 }
95 .filter(|file| file.endpoint() == endpoint)
96 .cloned()
97 })
98 }
99
100 pub(crate) fn lsp_locations(&mut self, kind: strop_lsp::LocKind) {
101 self.lsp_request(strop_lsp::RequestKind::Locations(kind));
102 }
103 pub(crate) fn lsp_hover(&mut self) {
104 self.lsp_request(strop_lsp::RequestKind::Hover);
105 }
106 pub(crate) fn lsp_goto_definition(&mut self) {
107 self.lsp_request(strop_lsp::RequestKind::Goto);
108 }
109 pub(crate) fn lsp_switch_source_header(&mut self) {
110 self.lsp_request(strop_lsp::RequestKind::SwitchHeader);
111 }
112 pub(crate) fn lsp_format_available(&self) -> bool {
116 let Some(binding) = self.lsp_state.bindings.get(&self.current()) else {
117 return false;
118 };
119 self.lsp_live_client(binding.server)
120 .is_some_and(|client| client.caps().formatting())
121 }
122
123 pub(crate) fn continue_after_format(
126 &mut self,
127 context: &strop_lsp::ReplyContext,
128 warning: Option<String>,
129 ) {
130 if context.kind != strop_lsp::RequestKind::Format
131 || !matches!(
132 self.lsp_state.after_format.as_ref(),
133 Some(state::AfterFormat::Save { request, .. }) if *request == context.stamp
134 )
135 {
136 return;
137 }
138 let Some(state::AfterFormat::Save {
139 document,
140 close,
141 force,
142 request,
143 }) = self.lsp_state.after_format.take()
144 else {
145 return;
146 };
147 if warning.is_some()
148 && self
149 .docs
150 .get(document)
151 .is_none_or(|source| source.buf.revision() != request.revision)
152 {
153 self.message = "write not started: source changed while formatting — repeat :w to save newer edits".into();
154 return;
155 }
156 let admitted = self.request_save_document(document, None, force, close);
157 if let Some(warning) = warning {
158 if admitted {
159 self.io.format_warnings.insert(document, warning);
160 } else {
161 self.message
162 .push_str(&format!(" — format warning: {warning}"));
163 }
164 }
165 }
166
167 pub(crate) fn lsp_format(&mut self) {
168 self.lsp_change_request(strop_lsp::RequestKind::Format, None);
169 }
170 pub(crate) fn lsp_rename(&mut self, new_name: &str) {
172 self.lsp_change_request(strop_lsp::RequestKind::Rename, Some(new_name.to_string()));
173 }
174 pub(crate) fn lsp_document_symbols(&mut self) {
176 self.lsp_request(strop_lsp::RequestKind::DocumentSymbols);
177 }
178 pub(crate) fn lsp_code_actions(&mut self) {
180 self.lsp_change_request(strop_lsp::RequestKind::CodeAction, None);
181 }
182
183 pub(crate) fn jump_diagnostic(&mut self, forward: bool) {
184 let Some(diags) = self
185 .diags_for(self.current())
186 .filter(|diags| !diags.is_empty())
187 else {
188 self.message = "no diagnostics".into();
189 return;
190 };
191 let cur = self.buf().line_of(self.head());
192 let col = self.buf().col_of(self.head());
193 let target = if forward {
194 diags
195 .iter()
196 .find(|d| d.line.get() > cur || (d.line.get() == cur && d.col.get() > col))
197 .or(diags.first())
198 } else {
199 diags
200 .iter()
201 .rev()
202 .find(|d| d.line.get() < cur || (d.line.get() == cur && d.col.get() < col))
203 .or(diags.last())
204 };
205 let Some(d) = target else {
206 return;
207 };
208 let (line, col, msg) = (d.line.get(), d.col.get(), d.message.clone());
209 let start = self
210 .buf()
211 .line_start(line.min(self.buf().len_lines().saturating_sub(1)));
212 self.set_head(self.buf().clamp_boundary(start + col));
213 self.clamp_cursor();
214 self.scroll_to_cursor(self.view_rows());
215 self.message = msg;
216 }
217
218 pub(crate) fn open_diagnostics_picker(&mut self) {
219 use strop_picker::{Item, Kind, Payload};
220 let mut by_doc: Vec<_> = self
222 .diags
223 .keys()
224 .filter_map(|&id| Some((self.lsp_doc_path(id)?, self.diags_for(id)?)))
225 .collect();
226 by_doc.sort_by(|a, b| {
227 (a.0.filesystem.label(), &a.0.path).cmp(&(b.0.filesystem.label(), &b.0.path))
228 });
229 let mut items: Vec<Item> = Vec::new();
230 for (doc, diags) in by_doc {
231 for d in diags {
232 let line = d.line.get() + 1;
233 let col = d.col.get() + 1;
234 match &doc.filesystem {
235 Filesystem::Local => items.push(Item {
236 badge: None,
237 text: format!(
238 "{}:{} {} {}",
239 doc.path.display(),
240 line,
241 d.severity_char(),
242 d.message
243 ),
244 payload: Payload::Grep {
245 path: doc.path.clone(),
246 line,
247 col,
248 match_len: 1,
249 line_text: d.message.clone(),
250 },
251 }),
252 Filesystem::Remote(endpoint) => items.push(Item {
256 badge: None,
257 text: format!(
258 "{}{}:{} {} {}",
259 endpoint,
260 doc.path.display(),
261 line,
262 d.severity_char(),
263 d.message
264 ),
265 payload: Payload::Remote {
266 endpoint: endpoint.clone(),
267 path: doc.path.clone(),
268 line,
269 col,
270 },
271 }),
272 Filesystem::Container(_) => {
276 trace::services::rejected(
277 "lsp",
278 "diagnostic in a container namespace (unwired)",
279 );
280 }
281 }
282 }
283 }
284 if items.is_empty() {
285 self.message = "no diagnostics".into();
286 return;
287 }
288 self.set_picker(super::PickerGlue::diagnostics(strop_picker::Picker::new(
289 Kind::Diagnostics,
290 items,
291 false,
292 )));
293 }
294
295 pub(crate) fn lsp_goto_definition_pub(&mut self) {
296 self.lsp_goto_definition();
297 }
298 pub(crate) fn lsp_switch_source_header_pub(&mut self) {
299 self.lsp_switch_source_header();
300 }
301 pub(crate) fn lsp_hover_pub(&mut self) {
302 self.lsp_hover();
303 }
304 pub fn lsp_code_actions_pub(&mut self) {
305 self.lsp_code_actions();
306 }
307 pub fn lsp_document_symbols_pub(&mut self) {
308 self.lsp_document_symbols();
309 }
310 pub fn lsp_locations_pub(&mut self, kind: strop_lsp::LocKind) {
311 self.lsp_locations(kind);
312 }
313 pub fn jump_diagnostic_pub(&mut self, forward: bool) {
314 self.jump_diagnostic(forward);
315 }
316}
317
318pub(crate) fn lsp_language(path: &Path) -> Option<&'static str> {
321 let ext = path.extension()?.to_str()?;
322 registry::language_for_extension_name(ext)
323}
324
325pub(crate) fn lang_id(path: &Path) -> &'static str {
327 match path.extension().and_then(|e| e.to_str()) {
328 Some("rs") => "rust",
329 Some("py") | Some("pyi") => "python",
330 Some("go") => "go",
331 Some("js") | Some("jsx") | Some("mjs") | Some("cjs") => "javascript",
332 Some("ts") => "typescript",
333 Some("tsx") => "typescriptreact",
334 Some("json") => "json",
335 Some("sh") | Some("bash") => "shellscript",
336 Some("c") | Some("h") => "c",
337 Some("cpp") | Some("cc") | Some("cxx") | Some("hpp") | Some("hh") => "cpp",
338 _ => "plaintext",
339 }
340}