Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-lsp/src/lib.rs')
| -rw-r--r-- | helix-lsp/src/lib.rs | 410 |
1 files changed, 181 insertions, 229 deletions
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 259980dd..ec89e1f8 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -2,17 +2,18 @@ mod client; pub mod file_event; mod file_operations; pub mod jsonrpc; +pub mod snippet; mod transport; use arc_swap::ArcSwap; pub use client::Client; pub use futures_executor::block_on; -pub use helix_lsp_types as lsp; pub use jsonrpc::Call; pub use lsp::{Position, Url}; +pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::syntax::config::{ +use helix_core::syntax::{ LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures, }; use helix_stdx::path; @@ -37,7 +38,7 @@ pub enum Error { #[error("protocol error: {0}")] Rpc(#[from] jsonrpc::Error), #[error("failed to parse: {0}")] - Parse(Box<dyn std::error::Error + Send + Sync>), + Parse(#[from] serde_json::Error), #[error("IO Error: {0}")] IO(#[from] std::io::Error), #[error("request {0} timed out")] @@ -52,18 +53,6 @@ pub enum Error { Other(#[from] anyhow::Error), } -impl From<serde_json::Error> for Error { - fn from(value: serde_json::Error) -> Self { - Self::Parse(Box::new(value)) - } -} - -impl From<sonic_rs::Error> for Error { - fn from(value: sonic_rs::Error) -> Self { - Self::Parse(Box::new(value)) - } -} - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum OffsetEncoding { /// UTF-8 code units aka bytes @@ -78,8 +67,7 @@ pub enum OffsetEncoding { pub mod util { use super::*; use helix_core::line_ending::{line_end_byte_index, line_end_char_index}; - use helix_core::snippets::{RenderedSnippet, Snippet, SnippetRenderCtx}; - use helix_core::{chars, RopeSlice}; + use helix_core::{chars, RopeSlice, SmallVec}; use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction}; /// Converts a diagnostic in the document to [`lsp::Diagnostic`]. @@ -296,8 +284,10 @@ pub mod util { if replace_mode { end += text .chars_at(cursor) + .skip(1) .take_while(|ch| chars::char_is_word(*ch)) - .count(); + .count() + + 1; } (start, end) } @@ -367,17 +357,25 @@ pub mod util { transaction.with_selection(selection) } - /// Creates a [Transaction] from the [Snippet] in a completion response. + /// Creates a [Transaction] from the [snippet::Snippet] in a completion response. /// The transaction applies the edit to all cursors. + #[allow(clippy::too_many_arguments)] pub fn generate_transaction_from_snippet( doc: &Rope, selection: &Selection, edit_offset: Option<(i128, i128)>, replace_mode: bool, - snippet: Snippet, - cx: &mut SnippetRenderCtx, - ) -> (Transaction, RenderedSnippet) { + snippet: snippet::Snippet, + line_ending: &str, + include_placeholder: bool, + tab_width: usize, + indent_width: usize, + ) -> Transaction { let text = doc.slice(..); + + let mut off = 0i128; + let mut mapped_doc = doc.clone(); + let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new(); let (removed_start, removed_end) = completion_range( text, edit_offset, @@ -386,7 +384,8 @@ pub mod util { ) .expect("transaction must be valid for primary selection"); let removed_text = text.slice(removed_start..removed_end); - let (transaction, mapped_selection, snippet) = snippet.render( + + let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping( doc, selection, |range| { @@ -395,15 +394,108 @@ pub mod util { .filter(|(start, end)| text.slice(start..end) == removed_text) .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor)) }, - cx, + |replacement_start, replacement_end| { + let mapped_replacement_start = (replacement_start as i128 + off) as usize; + let mapped_replacement_end = (replacement_end as i128 + off) as usize; + + let line_idx = mapped_doc.char_to_line(mapped_replacement_start); + let indent_level = helix_core::indent::indent_level_for_line( + mapped_doc.line(line_idx), + tab_width, + indent_width, + ) * indent_width; + + let newline_with_offset = format!( + "{line_ending}{blank:indent_level$}", + line_ending = line_ending, + blank = "" + ); + + let (replacement, tabstops) = + snippet::render(&snippet, &newline_with_offset, include_placeholder); + selection_tabstops.push((mapped_replacement_start, tabstops)); + mapped_doc.remove(mapped_replacement_start..mapped_replacement_end); + mapped_doc.insert(mapped_replacement_start, &replacement); + off += + replacement_start as i128 - replacement_end as i128 + replacement.len() as i128; + + Some(replacement) + }, ); - let transaction = transaction.with_selection(snippet.first_selection( - // we keep the direction of the old primary selection in case it changed during mapping - // but use the primary idx from the mapped selection in case ranges had to be merged - selection.primary().direction(), - mapped_selection.primary_index(), - )); - (transaction, snippet) + + let changes = transaction.changes(); + if changes.is_empty() { + return transaction; + } + + // Don't normalize to avoid merging/reording selections which would + // break the association between tabstops and selections. Most ranges + // will be replaced by tabstops anyways and the final selection will be + // normalized anyways + selection = selection.map_no_normalize(changes); + let mut mapped_selection = SmallVec::with_capacity(selection.len()); + let mut mapped_primary_idx = 0; + let primary_range = selection.primary(); + for (range, (tabstop_anchor, tabstops)) in selection.into_iter().zip(selection_tabstops) { + if range == primary_range { + mapped_primary_idx = mapped_selection.len() + } + + let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty()); + let Some(tabstops) = tabstops else { + // no tabstop normal mapping + mapped_selection.push(range); + continue; + }; + + // expand the selection to cover the tabstop to retain the helix selection semantic + // the tabstop closest to the range simply replaces `head` while anchor remains in place + // the remaining tabstops receive their own single-width cursor + if range.head < range.anchor { + let last_idx = tabstops.len() - 1; + let last_tabstop = tabstop_anchor + tabstops[last_idx].0; + + // if selection is forward but was moved to the right it is + // contained entirely in the replacement text, just do a point + // selection (fallback below) + if range.anchor > last_tabstop { + let range = Range::new(range.anchor, last_tabstop); + mapped_selection.push(range); + let rem_tabstops = tabstops[..last_idx] + .iter() + .map(|tabstop| Range::point(tabstop_anchor + tabstop.0)); + mapped_selection.extend(rem_tabstops); + continue; + } + } else { + let first_tabstop = tabstop_anchor + tabstops[0].0; + + // if selection is forward but was moved to the right it is + // contained entirely in the replacement text, just do a point + // selection (fallback below) + if range.anchor < first_tabstop { + // we can't properly compute the the next grapheme + // here because the transaction hasn't been applied yet + // that is not a problem because the range gets grapheme aligned anyway + // tough so just adding one will always cause head to be grapheme + // aligned correctly when applied to the document + let range = Range::new(range.anchor, first_tabstop + 1); + mapped_selection.push(range); + let rem_tabstops = tabstops[1..] + .iter() + .map(|tabstop| Range::point(tabstop_anchor + tabstop.0)); + mapped_selection.extend(rem_tabstops); + continue; + } + }; + + let tabstops = tabstops + .iter() + .map(|tabstop| Range::point(tabstop_anchor + tabstop.0)); + mapped_selection.extend(tabstops); + } + + transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx)) } pub fn generate_transaction_from_edits( @@ -413,7 +505,7 @@ pub mod util { ) -> Transaction { // Sort edits by start range, since some LSPs (Omnisharp) send them // in reverse order. - edits.sort_by_key(|edit| edit.range.start); + edits.sort_unstable_by_key(|edit| edit.range.start); // Generate a diff if the edit is a full document replacement. #[allow(clippy::collapsible_if)] @@ -475,7 +567,6 @@ pub enum MethodCall { RegisterCapability(lsp::RegistrationParams), UnregisterCapability(lsp::UnregistrationParams), ShowDocument(lsp::ShowDocumentParams), - WorkspaceDiagnosticRefresh, } impl MethodCall { @@ -507,7 +598,6 @@ impl MethodCall { let params: lsp::ShowDocumentParams = params.parse()?; Self::ShowDocument(params) } - lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh, _ => { return Err(Error::Unhandled); } @@ -587,7 +677,7 @@ impl Registry { pub fn remove_by_id(&mut self, id: LanguageServerId) { let Some(client) = self.inner.remove(id) else { - log::debug!("client was already removed"); + log::error!("client was already removed"); return; }; self.file_event_handler.remove_client(id); @@ -632,55 +722,51 @@ impl Registry { Ok(self.inner[id].clone()) } - /// If this method is called, all documents that have a reference to the language server have to refresh their language servers, + /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, + /// as it could be that language servers of these documents were stopped by this method. /// See helix_view::editor::Editor::refresh_language_servers - pub fn restart_server( + pub fn restart( &mut self, - name: &str, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Option<Result<Arc<Client>>> { - if let Some(old_clients) = self.inner_by_name.remove(name) { - if old_clients.is_empty() { - log::info!("restarting client for '{name}' which was manually stopped"); - } else { - log::info!("stopping existing clients for '{name}'"); - } - for old_client in old_clients { - self.file_event_handler.remove_client(old_client.id()); - self.inner.remove(old_client.id()); - tokio::spawn(async move { - let _ = old_client.force_shutdown().await; - }); - } - } - let client = match self.start_client( - name.to_string(), - language_config, - doc_path, - root_dirs, - enable_snippets, - ) { - Ok(client) => client, - Err(StartupError::NoRequiredRootFound) => return None, - Err(StartupError::Error(err)) => return Some(Err(err)), - }; - self.inner_by_name - .insert(name.to_owned(), vec![client.clone()]); + ) -> Result<Vec<Arc<Client>>> { + language_config + .language_servers + .iter() + .filter_map(|LanguageServerFeatures { name, .. }| { + if let Some(old_clients) = self.inner_by_name.remove(name) { + for old_client in old_clients { + self.file_event_handler.remove_client(old_client.id()); + self.inner.remove(old_client.id()); + tokio::spawn(async move { + let _ = old_client.force_shutdown().await; + }); + } + } + let client = match self.start_client( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) { + Ok(client) => client, + Err(StartupError::NoRequiredRootFound) => return None, + Err(StartupError::Error(err)) => return Some(Err(err)), + }; + self.inner_by_name + .insert(name.to_owned(), vec![client.clone()]); - Some(Ok(client)) + Some(Ok(client)) + }) + .collect() } pub fn stop(&mut self, name: &str) { - if let Some(clients) = self.inner_by_name.get_mut(name) { - // Drain the clients vec so that the entry in `inner_by_name` remains - // empty. We use the empty vec as a "tombstone" to mean that a server - // has been manually stopped with :lsp-stop and shouldn't be automatically - // restarted by `get`. :lsp-restart can be used to restart the server - // manually. - for client in clients.drain(..) { + if let Some(clients) = self.inner_by_name.remove(name) { + for client in clients { self.file_event_handler.remove_client(client.id()); self.inner.remove(client.id()); tokio::spawn(async move { @@ -700,20 +786,8 @@ impl Registry { language_config.language_servers.iter().filter_map( move |LanguageServerFeatures { name, .. }| { if let Some(clients) = self.inner_by_name.get(name) { - // If the clients vec is empty, do not automatically start a client - // for this server. The empty vec is a tombstone left to mean that a - // server has been manually stopped and shouldn't be started automatically. - // See `stop`. - if clients.is_empty() { - return None; - } - if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| { - let manual_roots = language_config - .workspace_lsp_roots - .as_deref() - .unwrap_or(root_dirs); - client.try_add_doc(&language_config.roots, manual_roots, doc_path, *i == 0) + client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) }) { return Some((name.to_owned(), Ok(client.clone()))); } @@ -747,17 +821,14 @@ impl Registry { #[derive(Debug)] pub enum ProgressStatus { Created, - Started { - title: String, - progress: lsp::WorkDoneProgress, - }, + Started(lsp::WorkDoneProgress), } impl ProgressStatus { pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> { match &self { ProgressStatus::Created => None, - ProgressStatus::Started { title: _, progress } => Some(progress), + ProgressStatus::Started(progress) => Some(progress), } } } @@ -794,13 +865,6 @@ impl LspProgressMap { self.0.get(&id).and_then(|values| values.get(token)) } - pub fn title(&self, id: LanguageServerId, token: &lsp::ProgressToken) -> Option<&String> { - self.progress(id, token).and_then(|p| match p { - ProgressStatus::Created => None, - ProgressStatus::Started { title, .. } => Some(title), - }) - } - /// Checks if progress `token` for server with `id` is created. pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool { self.0 @@ -825,39 +889,17 @@ impl LspProgressMap { self.0.get_mut(&id).and_then(|vals| vals.remove(token)) } - /// Updates the progress of `token` for server with `id` to begin state `status` - pub fn begin( - &mut self, - id: LanguageServerId, - token: lsp::ProgressToken, - status: lsp::WorkDoneProgressBegin, - ) { - self.0.entry(id).or_default().insert( - token, - ProgressStatus::Started { - title: status.title.clone(), - progress: lsp::WorkDoneProgress::Begin(status), - }, - ); - } - - /// Updates the progress of `token` for server with `id` to report state `status`. + /// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`. pub fn update( &mut self, id: LanguageServerId, token: lsp::ProgressToken, - status: lsp::WorkDoneProgressReport, - ) { + status: lsp::WorkDoneProgress, + ) -> Option<ProgressStatus> { self.0 .entry(id) .or_default() - .entry(token) - .and_modify(|e| match e { - ProgressStatus::Created => (), - ProgressStatus::Started { progress, .. } => { - *progress = lsp::WorkDoneProgress::Report(status) - } - }); + .insert(token, ProgressStatus::Started(status)) } } @@ -917,7 +959,7 @@ fn start_client( &ls_config.command, &ls_config.args, ls_config.config.clone(), - &ls_config.environment, + ls_config.environment.clone(), root_path, root_uri, id, @@ -946,7 +988,17 @@ fn start_client( } // next up, notify<initialized> - _client.notify::<lsp::notification::Initialized>(lsp::InitializedParams {}); + let notification_result = _client + .notify::<lsp::notification::Initialized>(lsp::InitializedParams {}) + .await; + + if let Err(e) = notification_result { + log::error!( + "failed to notify language server of its initialization: {}", + e + ); + return; + } initialize_notify.notify_one(); }); @@ -1045,7 +1097,7 @@ mod tests { #[test] fn emoji_format_gh_4791() { - use lsp::{Position, Range, TextEdit}; + use lsp_types::{Position, Range, TextEdit}; let edits = vec![ TextEdit { @@ -1078,107 +1130,7 @@ mod tests { let mut source = Rope::from_str("[\n\"πΊπΈ\",\n\"π\",\n]"); - let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf16); - assert!(transaction.apply(&mut source)); - assert_eq!(source, "[\n \"πΊπΈ\",\n \"π\",\n]"); - } - #[test] - fn rahh() { - use helix_lsp_types::*; - let th = [ - TextEdit { - range: Range { - start: Position { - line: 1, - character: 0, - }, - end: Position { - line: 1, - character: 4, - }, - }, - new_text: "".into(), - }, - TextEdit { - range: Range { - start: Position { - line: 2, - character: 0, - }, - end: Position { - line: 3, - character: 1, - }, - }, - new_text: "".into(), - }, - TextEdit { - range: Range { - start: Position { - line: 3, - character: 9, - }, - end: Position { - line: 3, - character: 9, - }, - }, - new_text: "let new =\n".into(), - }, - TextEdit { - range: Range { - start: Position { - line: 3, - character: 20, - }, - end: Position { - line: 3, - character: 29, - }, - }, - new_text: "".into(), - }, - TextEdit { - range: Range { - start: Position { - line: 3, - character: 56, - }, - end: Position { - line: 4, - character: 24, - }, - }, - new_text: "".into(), - }, - TextEdit { - range: Range { - start: Position { - line: 6, - character: 1, - }, - end: Position { - line: 6, - character: 1, - }, - }, - new_text: "\n".into(), - }, - ]; - let mut source = Rope::from_str( - "impl Editor { // 0 - pub fn open(f: &Path) { // 1 -// 2 - let new = std::fs::read_to_string(f) // 3 - .map_err(anyhow::Error::from)?; // 4 - } -}", - ); - println!("{}", source); - - let transaction = - generate_transaction_from_edits(&source, th.to_vec(), OffsetEncoding::Utf8); + let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf8); assert!(transaction.apply(&mut source)); - println!("{}", source); } } |