diff --git a/Cargo.lock b/Cargo.lock index c8bf13bf..a26c92e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "arc-swap" @@ -1107,9 +1107,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" dependencies = [ "smawk", "unicode-linebreak", @@ -1118,18 +1118,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -1171,9 +1171,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.0" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" dependencies = [ "autocfg", "bytes", @@ -1278,15 +1278,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "url" diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 3d180853..8343819c 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -44,6 +44,7 @@ | `:show-directory`, `:pwd` | Show the current working directory. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:reload` | Discard changes and reload from the source file. | +| `:lsp-restart` | Restarts the Language Server that is in use by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | | `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. | diff --git a/contrib/completion/hx.bash b/contrib/completion/hx.bash index 8a2d9777..89f3283c 100644 --- a/contrib/completion/hx.bash +++ b/contrib/completion/hx.bash @@ -16,7 +16,7 @@ _hx() { COMPREPLY=($(compgen -W "$languages" -- $2)) ;; *) - COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config" -- $2)) + COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- $2)) ;; esac } && complete -F _hx hx diff --git a/contrib/completion/hx.elv b/contrib/completion/hx.elv index d3d227bc..42c88585 100644 --- a/contrib/completion/hx.elv +++ b/contrib/completion/hx.elv @@ -36,6 +36,11 @@ set edit:completion:arg-completer[hx] = {|@args| edit:complete-filename $args[-1] | each { |v| put $v[stem] } return } + # When we have --log, we need a file + if (has-values "log" $args[-2]) { + edit:complete-filename $args[-1] | each { |v| put $v[stem] } + return + } } edit:complete-filename $args[-1] | each { |v| put $v[stem]} $candidate "--help" "(Prints help information)" @@ -46,4 +51,5 @@ set edit:completion:arg-completer[hx] = {|@args| $candidate "--vsplit" "(Splits all given files vertically)" $candidate "--hsplit" "(Splits all given files horizontally)" $candidate "--config" "(Specifies a file to use for configuration)" -} \ No newline at end of file + $candidate "--log" "(Specifies a file to write log data into)" +} diff --git a/contrib/completion/hx.fish b/contrib/completion/hx.fish index 65f248d4..11977605 100644 --- a/contrib/completion/hx.fish +++ b/contrib/completion/hx.fish @@ -11,4 +11,5 @@ complete -c hx -s v -o vv -o vvv -d "Increases logging verbosity" complete -c hx -s V -l version -d "Prints version information" complete -c hx -l vsplit -d "Splits all given files vertically into different windows" complete -c hx -l hsplit -d "Splits all given files horizontally into different windows" -complete -c hx -s c -l config -d "Specifies a file to use for completion" +complete -c hx -s c -l config -r -d "Specifies a file to use for completion" +complete -c hx -l log -r -d "Specifies a file to write log data into" diff --git a/contrib/completion/hx.zsh b/contrib/completion/hx.zsh index e3375656..aaad6f84 100644 --- a/contrib/completion/hx.zsh +++ b/contrib/completion/hx.zsh @@ -18,6 +18,7 @@ _hx() { "--hsplit[Splits all given files horizontally into different windows]" \ "-c[Specifies a file to use for configuration]" \ "--config[Specifies a file to use for configuration]" \ + "--log[Specifies a file to write log data into]" \ "*:file:_files" case "$state" in diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 3ea7235d..0ec58af9 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -20,7 +20,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" } ropey = { version = "1.5", default-features = false, features = ["simd"] } smallvec = "1.9" smartstring = "1.0.1" -unicode-segmentation = "1.9" +unicode-segmentation = "1.10" unicode-width = "0.1" unicode-general-category = "0.5" # slab = "0.4.2" @@ -42,7 +42,7 @@ encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } etcetera = "0.4" -textwrap = "0.15.0" +textwrap = "0.15.1" [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 48a68dc0..db1f2da9 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -29,6 +29,12 @@ pub enum NumberOrString { String(String), } +#[derive(Debug, Clone)] +pub enum DiagnosticTag { + Unnecessary, + Deprecated, +} + /// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html) #[derive(Debug, Clone)] pub struct Diagnostic { @@ -37,4 +43,6 @@ pub struct Diagnostic { pub message: String, pub severity: Option, pub code: Option, + pub tags: Vec, + pub source: Option, } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index b5f5eace..18cc88fa 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -9,7 +9,8 @@ pub use lsp::{Position, Url}; pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::syntax::LanguageConfiguration; +use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; +use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, @@ -86,15 +87,32 @@ pub mod util { None => None, }; + let new_tags: Vec<_> = diag + .tags + .iter() + .map(|tag| match tag { + helix_core::diagnostic::DiagnosticTag::Unnecessary => { + lsp::DiagnosticTag::UNNECESSARY + } + helix_core::diagnostic::DiagnosticTag::Deprecated => lsp::DiagnosticTag::DEPRECATED, + }) + .collect(); + + let tags = if !new_tags.is_empty() { + Some(new_tags) + } else { + None + }; + // TODO: add support for Diagnostic.data lsp::Diagnostic::new( range_to_lsp_range(doc, range, offset_encoding), severity, code, - None, + diag.source.clone(), diag.message.to_owned(), None, - None, + tags, ) } @@ -320,6 +338,33 @@ impl Registry { .map(|(_, client)| client.as_ref()) } + pub fn restart( + &mut self, + language_config: &LanguageConfiguration, + ) -> Result>> { + let config = match &language_config.language_server { + Some(config) => config, + None => return Ok(None), + }; + + let scope = language_config.scope.clone(); + + match self.inner.entry(scope) { + Entry::Vacant(_) => Ok(None), + Entry::Occupied(mut entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + + let NewClientResult(client, incoming) = start_client(id, language_config, config)?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + + entry.insert((id, client.clone())); + + Ok(Some(client)) + } + } + } + pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -331,43 +376,9 @@ impl Registry { Entry::Vacant(entry) => { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (client, incoming, initialize_notify) = Client::start( - &config.command, - &config.args, - language_config.config.clone(), - &language_config.roots, - id, - config.timeout, - )?; - self.incoming.push(UnboundedReceiverStream::new(incoming)); - let client = Arc::new(client); - - // Initialize the client asynchronously - let _client = client.clone(); - tokio::spawn(async move { - use futures_util::TryFutureExt; - let value = _client - .capabilities - .get_or_try_init(|| { - _client - .initialize() - .map_ok(|response| response.capabilities) - }) - .await; - - if let Err(e) = value { - log::error!("failed to initialize language server: {}", e); - return; - } - - // next up, notify - _client - .notify::(lsp::InitializedParams {}) - .await - .unwrap(); - initialize_notify.notify_one(); - }); + let NewClientResult(client, incoming) = start_client(id, language_config, config)?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); entry.insert((id, client.clone())); Ok(Some(client)) @@ -524,6 +535,56 @@ impl LspProgressMap { } } +struct NewClientResult(Arc, UnboundedReceiver<(usize, Call)>); + +/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that +/// it is only called when it makes sense. +fn start_client( + id: usize, + config: &LanguageConfiguration, + ls_config: &LanguageServerConfiguration, +) -> Result { + let (client, incoming, initialize_notify) = Client::start( + &ls_config.command, + &ls_config.args, + config.config.clone(), + &config.roots, + id, + ls_config.timeout, + )?; + + let client = Arc::new(client); + + // Initialize the client asynchronously + let _client = client.clone(); + tokio::spawn(async move { + use futures_util::TryFutureExt; + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + if let Err(e) = value { + log::error!("failed to initialize language server: {}", e); + return; + } + + // next up, notify + _client + .notify::(lsp::InitializedParams {}) + .await + .unwrap(); + + initialize_notify.notify_one(); + }); + + Ok(NewClientResult(client, incoming)) +} + #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 7ee5b7f1..cd499f1c 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -2,7 +2,7 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; use helix_core::{ config::{default_syntax_loader, user_syntax_loader}, - diagnostic::NumberOrString, + diagnostic::{DiagnosticTag, NumberOrString}, pos_at_coords, syntax, Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; @@ -605,13 +605,28 @@ impl Application { None => None, }; + let tags = if let Some(ref tags) = diagnostic.tags { + let new_tags = tags.iter().filter_map(|tag| { + match *tag { + lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated), + lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary), + _ => None + } + }).collect(); + + new_tags + } else { + Vec::new() + }; + Some(Diagnostic { range: Range { start, end }, line: diagnostic.range.start.line as usize, message: diagnostic.message.clone(), severity, code, - // source + tags, + source: diagnostic.source.clone() }) }) .collect(); diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index d16d7dfd..48c86633 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -14,6 +14,7 @@ pub struct Args { pub build_grammars: bool, pub split: Option, pub verbosity: u64, + pub log_file: Option, pub config_file: Option, pub files: Vec<(PathBuf, Position)>, } @@ -48,6 +49,10 @@ impl Args { Some(path) => args.config_file = Some(path.into()), None => anyhow::bail!("--config must specify a path to read"), }, + "--log" => match argv.next().as_deref() { + Some(path) => args.log_file = Some(path.into()), + None => anyhow::bail!("--log must specify a path to write"), + }, arg if arg.starts_with("--") => { anyhow::bail!("unexpected double dash argument: {}", arg) } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 3e0db661..35f94c87 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -715,7 +715,10 @@ fn theme( cx.editor.unset_theme_preview(); } PromptEvent::Update => { - if let Some(theme_name) = args.first() { + if args.is_empty() { + // Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty. + cx.editor.unset_theme_preview(); + } else if let Some(theme_name) = args.first() { if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); @@ -982,6 +985,40 @@ fn reload( }) } +fn lsp_restart( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let (_view, doc) = current!(cx.editor); + let config = doc + .language_config() + .context("LSP not defined for the current document")?; + + let scope = config.scope.clone(); + cx.editor.language_servers.restart(config)?; + + // This collect is needed because refresh_language_server would need to re-borrow editor. + let document_ids_to_refresh: Vec = cx + .editor + .documents() + .filter_map(|doc| match doc.language_config() { + Some(config) if config.scope.eq(&scope) => Some(doc.id()), + _ => None, + }) + .collect(); + + for document_id in document_ids_to_refresh { + cx.editor.refresh_language_server(document_id); + } + + Ok(()) +} + fn tree_sitter_scopes( cx: &mut compositor::Context, _args: &[Cow], @@ -1844,6 +1881,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: reload, completer: None, }, + TypableCommand { + name: "lsp-restart", + aliases: &[], + doc: "Restarts the Language Server that is in use by the current doc", + fun: lsp_restart, + completer: None, + }, TypableCommand { name: "tree-sitter-scopes", aliases: &[], diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index d21d3e77..726bf9e3 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -67,6 +67,7 @@ FLAGS: -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml -c, --config Specifies a file to use for configuration -v Increases logging verbosity each use for up to 3 times + --log Specifies a file to use for logging (default file: {}) -V, --version Prints version information --vsplit Splits all given files vertically into different windows @@ -114,6 +115,7 @@ FLAGS: return Ok(0); } + let logpath = args.log_file.as_ref().cloned().unwrap_or(logpath); setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; let config_dir = helix_loader::config_dir(); diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index d66e32be..db3bd62d 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -293,6 +293,7 @@ impl Prompt { register: char, direction: CompletionDirection, ) { + (self.callback_fn)(cx, &self.line, PromptEvent::Abort); let register = cx.editor.registers.get_mut(register).read(); if register.is_empty() { @@ -314,6 +315,7 @@ impl Prompt { self.history_pos = Some(index); self.move_end(); + (self.callback_fn)(cx, &self.line, PromptEvent::Update); self.recalculate_completion(cx.editor); } @@ -564,13 +566,11 @@ impl Component for Prompt { ctrl!('p') | key!(Up) => { if let Some(register) = self.history_register { self.change_history(cx, register, CompletionDirection::Backward); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); } } ctrl!('n') | key!(Down) => { if let Some(register) = self.history_register { self.change_history(cx, register, CompletionDirection::Forward); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); } } key!(Tab) => { diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index effad198..b220c64f 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -18,7 +18,7 @@ default = ["crossterm"] [dependencies] bitflags = "1.3" cassowary = "0.3" -unicode-segmentation = "1.9" +unicode-segmentation = "1.10" crossterm = { version = "0.25", optional = true } serde = { version = "1", "optional" = true, features = ["derive"]} helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 8ebc9002..2ef99c6a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -788,6 +788,8 @@ impl Document { diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After); diagnostic.line = self.text.char_to_line(diagnostic.range.start); } + self.diagnostics + .sort_unstable_by_key(|diagnostic| diagnostic.range); // emit lsp notification if let Some(language_server) = self.language_server() {