use super::{trace, Editor};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Receiver;
use strop_lsp::registry;
use strop_lsp::{LspEvent, ServerId};
use strop_workspace::{Filesystem, ResourceLocation};
pub(crate) mod attach;
mod lifecycle;
mod navigation;
pub(crate) mod remote;
mod routing;
pub(crate) mod state;
#[cfg(test)]
mod tests;
pub struct LspServer {
pub id: ServerId,
pub client: Option<strop_lsp::Client>,
pub rx: Receiver<LspEvent>,
pub ready: bool,
}
impl Editor {
pub(crate) fn open_hover_document(&mut self) {
let Some(text) = self.hover_card.take() else {
return;
};
self.push_jump();
let mut document = super::Document::documentation(strop_core::Buffer::from_text(&text));
document.set_return_point(self.jump_record());
let id = self.docs.insert(document);
self.switch_to(id);
self.set_head(0);
self.view_mut().view_top = 0;
self.message = "documentation: search/scroll normally; Ctrl-O returns".into();
}
}
impl Editor {
pub(super) fn lsp_current_doc_path(&self) -> Option<ResourceLocation> {
if self.cur().remote_metadata().is_some() && !self.remote_window_complete() {
return None;
}
self.lsp_doc_path(self.current())
}
fn lsp_doc_path(&self, document: strop_core::id::DocumentId) -> Option<ResourceLocation> {
let document = self.docs.get(document)?;
match &document.source {
crate::editor::document::DocumentSource::Remote(file) => {
Some(ResourceLocation::remote(
file.file.endpoint().clone(),
file.file.path().to_path_buf(),
))
}
crate::editor::document::DocumentSource::Container { container, path } => {
Some(ResourceLocation {
filesystem: strop_workspace::Filesystem::Container(container.clone()),
path: path.clone(),
})
}
_ => document
.buf
.path
.as_ref()
.map(|path| ResourceLocation::local(self.cwd.join(path))),
}
}
pub(super) fn remote_file_for(
&self,
endpoint: &strop_workspace::RemoteEndpoint,
) -> Option<strop_workspace::RemoteFile> {
self.docs.iter().find_map(|(_, document)| {
match &document.source {
crate::editor::document::DocumentSource::Remote(source) => Some(&source.file),
_ => None,
}
.filter(|file| file.endpoint() == endpoint)
.cloned()
})
}
pub(crate) fn lsp_locations(&mut self, kind: strop_lsp::LocKind) {
self.lsp_request(strop_lsp::RequestKind::Locations(kind));
}
pub(crate) fn lsp_hover(&mut self) {
self.lsp_request(strop_lsp::RequestKind::Hover);
}
pub(crate) fn lsp_goto_definition(&mut self) {
self.lsp_request(strop_lsp::RequestKind::Goto);
}
pub(crate) fn lsp_switch_source_header(&mut self) {
self.lsp_request(strop_lsp::RequestKind::SwitchHeader);
}
pub(crate) fn lsp_format_available(&self) -> bool {
let Some(binding) = self.lsp_state.bindings.get(&self.current()) else {
return false;
};
self.lsp_live_client(binding.server)
.is_some_and(|client| client.caps().formatting())
}
pub(crate) fn continue_after_format(
&mut self,
context: &strop_lsp::ReplyContext,
warning: Option<String>,
) {
if context.kind != strop_lsp::RequestKind::Format
|| !matches!(
self.lsp_state.after_format.as_ref(),
Some(state::AfterFormat::Save { request, .. }) if *request == context.stamp
)
{
return;
}
let Some(state::AfterFormat::Save {
document,
close,
force,
request,
}) = self.lsp_state.after_format.take()
else {
return;
};
if warning.is_some()
&& self
.docs
.get(document)
.is_none_or(|source| source.buf.revision() != request.revision)
{
self.message = "write not started: source changed while formatting — repeat :w to save newer edits".into();
return;
}
let admitted = self.request_save_document(document, None, force, close);
if let Some(warning) = warning {
if admitted {
self.io.format_warnings.insert(document, warning);
} else {
self.message
.push_str(&format!(" — format warning: {warning}"));
}
}
}
pub(crate) fn lsp_format(&mut self) {
self.lsp_change_request(strop_lsp::RequestKind::Format, None);
}
pub(crate) fn lsp_rename(&mut self, new_name: &str) {
self.lsp_change_request(strop_lsp::RequestKind::Rename, Some(new_name.to_string()));
}
pub(crate) fn lsp_document_symbols(&mut self) {
self.lsp_request(strop_lsp::RequestKind::DocumentSymbols);
}
pub(crate) fn lsp_code_actions(&mut self) {
self.lsp_change_request(strop_lsp::RequestKind::CodeAction, None);
}
pub(crate) fn jump_diagnostic(&mut self, forward: bool) {
let Some(diags) = self
.diags_for(self.current())
.filter(|diags| !diags.is_empty())
else {
self.message = "no diagnostics".into();
return;
};
let cur = self.buf().line_of(self.head());
let col = self.buf().col_of(self.head());
let target = if forward {
diags
.iter()
.find(|d| d.line.get() > cur || (d.line.get() == cur && d.col.get() > col))
.or(diags.first())
} else {
diags
.iter()
.rev()
.find(|d| d.line.get() < cur || (d.line.get() == cur && d.col.get() < col))
.or(diags.last())
};
let Some(d) = target else {
return;
};
let (line, col, msg) = (d.line.get(), d.col.get(), d.message.clone());
let start = self
.buf()
.line_start(line.min(self.buf().len_lines().saturating_sub(1)));
self.set_head(self.buf().clamp_boundary(start + col));
self.clamp_cursor();
self.scroll_to_cursor(self.view_rows());
self.message = msg;
}
pub(crate) fn open_diagnostics_picker(&mut self) {
use strop_picker::{Item, Kind, Payload};
let mut by_doc: Vec<_> = self
.diags
.keys()
.filter_map(|&id| Some((self.lsp_doc_path(id)?, self.diags_for(id)?)))
.collect();
by_doc.sort_by(|a, b| {
(a.0.filesystem.label(), &a.0.path).cmp(&(b.0.filesystem.label(), &b.0.path))
});
let mut items: Vec<Item> = Vec::new();
for (doc, diags) in by_doc {
for d in diags {
let line = d.line.get() + 1;
let col = d.col.get() + 1;
match &doc.filesystem {
Filesystem::Local => items.push(Item {
badge: None,
text: format!(
"{}:{} {} {}",
doc.path.display(),
line,
d.severity_char(),
d.message
),
payload: Payload::Grep {
path: doc.path.clone(),
line,
col,
match_len: 1,
line_text: d.message.clone().into(),
},
}),
Filesystem::Remote(endpoint) => items.push(Item {
badge: None,
text: format!(
"{}{}:{} {} {}",
endpoint,
doc.path.display(),
line,
d.severity_char(),
d.message
),
payload: Payload::Remote {
endpoint: endpoint.clone(),
path: doc.path.clone(),
line,
col,
},
}),
Filesystem::Container(_) => {
trace::services::rejected(
"lsp",
"diagnostic in a container namespace (unwired)",
);
}
}
}
}
if items.is_empty() {
self.message = "no diagnostics".into();
return;
}
self.set_picker(super::PickerGlue::diagnostics(strop_picker::Picker::new(
Kind::Diagnostics,
items,
false,
)));
}
pub(crate) fn lsp_goto_definition_pub(&mut self) {
self.lsp_goto_definition();
}
pub(crate) fn lsp_switch_source_header_pub(&mut self) {
self.lsp_switch_source_header();
}
pub(crate) fn lsp_hover_pub(&mut self) {
self.lsp_hover();
}
pub fn lsp_code_actions_pub(&mut self) {
self.lsp_code_actions();
}
pub fn lsp_document_symbols_pub(&mut self) {
self.lsp_document_symbols();
}
pub fn lsp_locations_pub(&mut self, kind: strop_lsp::LocKind) {
self.lsp_locations(kind);
}
pub fn jump_diagnostic_pub(&mut self, forward: bool) {
self.jump_diagnostic(forward);
}
}
pub(crate) fn lsp_language(path: &Path) -> Option<&'static str> {
let ext = path.extension()?.to_str()?;
registry::language_for_extension_name(ext)
}
pub(crate) fn lang_id(path: &Path) -> &'static str {
match path.extension().and_then(|e| e.to_str()) {
Some("rs") => "rust",
Some("py") | Some("pyi") => "python",
Some("go") => "go",
Some("js") | Some("jsx") | Some("mjs") | Some("cjs") => "javascript",
Some("ts") => "typescript",
Some("tsx") => "typescriptreact",
Some("json") => "json",
Some("sh") | Some("bash") => "shellscript",
Some("c") | Some("h") => "c",
Some("cpp") | Some("cc") | Some("cxx") | Some("hpp") | Some("hh") => "cpp",
_ => "plaintext",
}
}