From 3cf2a91a253764f0f097249a95e319416e6e4eea Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 7 May 2024 11:27:28 -0400 Subject: [PATCH 001/200] Bump MSRV to 1.74.0 --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e661a1113..7c7cb7f41 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.70.0" +channel = "1.74.0" components = ["rustfmt", "rust-src", "clippy"] From 9dd51e75e0cdff46dad397508573d43d732ffaf8 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 7 May 2024 11:35:39 -0400 Subject: [PATCH 002/200] Resolve new Clippy lints --- helix-core/src/indent.rs | 2 +- helix-core/src/syntax.rs | 13 ++++++++----- helix-core/src/transaction.rs | 9 +++++++-- helix-core/tests/indent.rs | 3 ++- helix-lsp/src/file_event.rs | 2 +- helix-term/src/application.rs | 2 +- helix-view/src/register.rs | 4 ++-- helix-view/src/tree.rs | 2 +- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index ca72fc3a2..fd2b6c959 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -740,7 +740,7 @@ fn init_indent_query<'a, 'b>( crate::syntax::PARSER.with(|ts_parser| { let mut ts_parser = ts_parser.borrow_mut(); - let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); + let mut cursor = ts_parser.cursors.pop().unwrap_or_default(); let query_result = query_indents( query, syntax, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index bbea9bbe7..8fda29352 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -21,7 +21,7 @@ borrow::Cow, cell::RefCell, collections::{HashMap, HashSet, VecDeque}, - fmt::{self, Display}, + fmt::{self, Display, Write}, hash::{Hash, Hasher}, mem::replace, path::{Path, PathBuf}, @@ -729,8 +729,11 @@ pub fn read_query(language: &str, filename: &str) -> String { .replace_all(&query, |captures: ®ex::Captures| { captures[1] .split(',') - .map(|language| format!("\n{}\n", read_query(language, filename))) - .collect::() + .fold(String::new(), |mut output, language| { + // `write!` to a String cannot fail. + write!(output, "\n{}\n", read_query(language, filename)).unwrap(); + output + }) }) .to_string() } @@ -1245,7 +1248,7 @@ fn point_sub(a: Point, b: Point) -> Point { PARSER.with(|ts_parser| { let ts_parser = &mut ts_parser.borrow_mut(); ts_parser.parser.set_timeout_micros(1000 * 500); // half a second is pretty generours - let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); + let mut cursor = ts_parser.cursors.pop().unwrap_or_default(); // TODO: might need to set cursor range cursor.set_byte_range(0..usize::MAX); cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT); @@ -1419,7 +1422,7 @@ pub fn highlight_iter<'a>( // Reuse a cursor from the pool if available. let mut cursor = PARSER.with(|ts_parser| { let highlighter = &mut ts_parser.borrow_mut(); - highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) + highlighter.cursors.pop().unwrap_or_default() }); // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index f5a49cc11..f24f20942 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -378,7 +378,9 @@ pub fn update_positions<'a>(&self, positions: impl Iterator { loop { - let Some((pos, assoc)) = positions.peek_mut() else { return; }; + let Some((pos, assoc)) = positions.peek_mut() else { + return; + }; if **pos < old_pos { // Positions are not sorted, revert to the last Operation that // contains this position and continue iterating from there. @@ -405,7 +407,10 @@ macro_rules! map { debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works"); continue 'outer; } - let Some(new_pos) = $map(**pos, *assoc) else { break; }; + #[allow(clippy::redundant_closure_call)] + let Some(new_pos) = $map(**pos, *assoc) else { + break; + }; **pos = new_pos; positions.next(); } diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index 31946c56e..56b4d2ba9 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -36,7 +36,7 @@ fn test_treesitter_indent_rust_helix() { .unwrap(); let files = String::from_utf8(files.stdout).unwrap(); - let ignored_files = vec![ + let ignored_files = [ // Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting "helix-term/src/health.rs", ]; @@ -45,6 +45,7 @@ fn test_treesitter_indent_rust_helix() { if ignored_files.contains(&file) { continue; } + #[allow(clippy::single_range_in_vec_init)] let ignored_lines: Vec> = match file { "helix-term/src/application.rs" => vec![ // We can't handle complicated indent rules inside macros (`json!` in this case) since diff --git a/helix-lsp/src/file_event.rs b/helix-lsp/src/file_event.rs index 8f4c16756..c7297d67f 100644 --- a/helix-lsp/src/file_event.rs +++ b/helix-lsp/src/file_event.rs @@ -139,7 +139,7 @@ async fn run(mut rx: mpsc::UnboundedReceiver) { registration_id ); - let entry = state.entry(client_id).or_insert_with(ClientState::default); + let entry = state.entry(client_id).or_default(); entry.client = client; let mut builder = GlobSetBuilder::new(); diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 6bdf60bca..f71eed209 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -724,7 +724,7 @@ macro_rules! language_server { } Notification::PublishDiagnostics(mut params) => { let path = match params.uri.to_file_path() { - Ok(path) => helix_stdx::path::normalize(&path), + Ok(path) => helix_stdx::path::normalize(path), Err(_) => { log::error!("Unsupported file URI: {}", params.uri); return; diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index 5592c6afd..a7f356121 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -123,7 +123,7 @@ pub fn push(&mut self, name: char, mut value: String) -> Result<()> { _ => unreachable!(), }; let contents = self.clipboard_provider.get_contents(clipboard_type)?; - let saved_values = self.inner.entry(name).or_insert_with(Vec::new); + let saved_values = self.inner.entry(name).or_default(); if !contents_are_saved(saved_values, &contents) { anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents"); @@ -140,7 +140,7 @@ pub fn push(&mut self, name: char, mut value: String) -> Result<()> { Ok(()) } _ => { - self.inner.entry(name).or_insert_with(Vec::new).push(value); + self.inner.entry(name).or_default().push(value); Ok(()) } } diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 307dbc71d..be8bd4e5b 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -560,7 +560,7 @@ pub fn prev(&self) -> ViewId { id } else { // extremely crude, take the last item - let (key, _) = self.traverse().rev().next().unwrap(); + let (key, _) = self.traverse().next_back().unwrap(); key } } From 5c11af1479a41263496f98096b20614c4b34250f Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 7 May 2024 11:35:49 -0400 Subject: [PATCH 003/200] `cargo fmt` --- helix-lsp/src/lib.rs | 2 +- helix-term/src/handlers/completion/resolve.rs | 4 +++- helix-term/src/handlers/signature_help.rs | 13 ++++++------- helix-vcs/src/git.rs | 4 ++-- helix-view/src/register.rs | 4 +++- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 0a3c6a33d..ec89e1f82 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -678,7 +678,7 @@ pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc> { pub fn remove_by_id(&mut self, id: LanguageServerId) { let Some(client) = self.inner.remove(id) else { log::error!("client was already removed"); - return + return; }; self.file_event_handler.remove_client(id); let instances = self diff --git a/helix-term/src/handlers/completion/resolve.rs b/helix-term/src/handlers/completion/resolve.rs index fb5179e13..6b70e52cd 100644 --- a/helix-term/src/handlers/completion/resolve.rs +++ b/helix-term/src/handlers/completion/resolve.rs @@ -111,7 +111,9 @@ fn handle_event( } fn finish_debounce(&mut self) { - let Some(request) = self.next_request.take() else { return }; + let Some(request) = self.next_request.take() else { + return; + }; let (tx, rx) = helix_event::cancelation(); self.in_flight = Some((tx, request.item.clone())); tokio::spawn(request.execute(rx)); diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 4fc008118..aaa97b9a0 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -119,8 +119,7 @@ pub fn request_signature_help( // Do not show the message if signature help was invoked // automatically on backspace, trigger characters, etc. if invoked == SignatureHelpInvoked::Manual { - editor - .set_error("No configured language server supports signature-help"); + editor.set_error("No configured language server supports signature-help"); } return; }; @@ -300,11 +299,11 @@ fn signature_help_post_insert_char_hook( let (view, doc) = current!(cx.editor); // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow let Some(language_server) = doc - .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .next() - else { - return Ok(()); - }; + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .next() + else { + return Ok(()); + }; let capabilities = language_server.capabilities(); diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 979f8726e..78e582436 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -145,8 +145,8 @@ fn status(repo: &Repository, f: impl Fn(Result) -> bool) -> Result<( for item in status_iter { let Ok(item) = item.map_err(|err| f(Err(err.into()))) else { - continue; - }; + continue; + }; let change = match item { Item::Modification { rela_path, status, .. diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index a7f356121..166d2027d 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -233,7 +233,9 @@ fn read_from_clipboard<'a>( // If we're pasting the same values that we just yanked, re-use // the saved values. This allows pasting multiple selections // even when yanked to a clipboard. - let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) }; + let Some(values) = saved_values else { + return RegisterValues::new(iter::once(contents.into())); + }; if contents_are_saved(values, &contents) { RegisterValues::new(values.iter().map(Cow::from).rev()) From f656b4f3a782786b3cd4f4b457e5f4bc5d710cf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 21:43:40 +0200 Subject: [PATCH 004/200] build(deps): bump the rust-dependencies group across 1 directory with 11 updates (#10715) Bumps the rust-dependencies group with 11 updates in the / directory: | Package | From | To | | --- | --- | --- | | [tree-sitter](https://github.com/tree-sitter/tree-sitter) | `0.22.5` | `0.22.6` | | [unicode-width](https://github.com/unicode-rs/unicode-width) | `0.1.11` | `0.1.12` | | [hashbrown](https://github.com/rust-lang/hashbrown) | `0.14.3` | `0.14.5` | | [serde](https://github.com/serde-rs/serde) | `1.0.198` | `1.0.200` | | [parking_lot](https://github.com/Amanieu/parking_lot) | `0.12.1` | `0.12.2` | | [anyhow](https://github.com/dtolnay/anyhow) | `1.0.82` | `1.0.83` | | [libc](https://github.com/rust-lang/libc) | `0.2.153` | `0.2.154` | | [rustix](https://github.com/bytecodealliance/rustix) | `0.38.32` | `0.38.34` | | [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) | `0.10.0` | `0.10.3` | | [thiserror](https://github.com/dtolnay/thiserror) | `1.0.58` | `1.0.60` | | [cc](https://github.com/rust-lang/cc-rs) | `1.0.95` | `1.0.97` | Updates `tree-sitter` from 0.22.5 to 0.22.6 - [Release notes](https://github.com/tree-sitter/tree-sitter/releases) - [Changelog](https://github.com/tree-sitter/tree-sitter/blob/master/CHANGELOG.md) - [Commits](https://github.com/tree-sitter/tree-sitter/compare/v0.22.5...v0.22.6) Updates `unicode-width` from 0.1.11 to 0.1.12 - [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.11...v0.1.12) Updates `hashbrown` from 0.14.3 to 0.14.5 - [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/hashbrown/compare/v0.14.3...v0.14.5) Updates `serde` from 1.0.198 to 1.0.200 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.198...v1.0.200) Updates `parking_lot` from 0.12.1 to 0.12.2 - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.1...0.12.2) Updates `anyhow` from 1.0.82 to 1.0.83 - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.82...1.0.83) Updates `libc` from 0.2.153 to 0.2.154 - [Release notes](https://github.com/rust-lang/libc/releases) - [Commits](https://github.com/rust-lang/libc/compare/0.2.153...0.2.154) Updates `rustix` from 0.38.32 to 0.38.34 - [Release notes](https://github.com/bytecodealliance/rustix/releases) - [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.32...v0.38.34) Updates `pulldown-cmark` from 0.10.0 to 0.10.3 - [Release notes](https://github.com/raphlinus/pulldown-cmark/releases) - [Commits](https://github.com/raphlinus/pulldown-cmark/compare/v0.10.0...v0.10.3) Updates `thiserror` from 1.0.58 to 1.0.60 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.60) Updates `cc` from 1.0.95 to 1.0.97 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.95...1.0.97) --- updated-dependencies: - dependency-name: tree-sitter dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: unicode-width dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: hashbrown dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: parking_lot dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: libc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rustix dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: pulldown-cmark dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 62 +++++++++++++++++++++--------------------- helix-core/Cargo.toml | 2 +- helix-event/Cargo.toml | 2 +- helix-lsp/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 017a248a8..d48441d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "arc-swap" @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -842,7 +842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" dependencies = [ "gix-hash", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "parking_lot", ] @@ -877,7 +877,7 @@ dependencies = [ "gix-object", "gix-traverse", "gix-utils", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "itoa", "libc", "memmap2", @@ -1282,9 +1282,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -1302,7 +1302,7 @@ dependencies = [ "encoding_rs", "etcetera", "globset", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "helix-loader", "helix-stdx", "imara-diff", @@ -1349,7 +1349,7 @@ dependencies = [ "ahash", "anyhow", "futures-executor", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "log", "once_cell", "parking_lot", @@ -1612,7 +1612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1666,9 +1666,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libloading" @@ -1847,9 +1847,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1909,9 +1909,9 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" [[package]] name = "pulldown-cmark" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" +checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ "bitflags 2.5.0", "memchr", @@ -2054,9 +2054,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2094,18 +2094,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -2325,18 +2325,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", @@ -2473,9 +2473,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688200d842c76dd88f9a7719ecb0483f79f5a766fb1c100756d5d8a059abc71b" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" dependencies = [ "cc", "regex", @@ -2537,9 +2537,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "url" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 16d6b457f..53d4af359 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -32,7 +32,7 @@ arc-swap = "1" regex = "1" bitflags = "2.5" ahash = "0.8.11" -hashbrown = { version = "0.14.3", features = ["raw"] } +hashbrown = { version = "0.14.5", features = ["raw"] } dunce = "1.0" log = "0.4" diff --git a/helix-event/Cargo.toml b/helix-event/Cargo.toml index 616c323dc..e7c877355 100644 --- a/helix-event/Cargo.toml +++ b/helix-event/Cargo.toml @@ -13,7 +13,7 @@ homepage.workspace = true [dependencies] ahash = "0.8.11" -hashbrown = "0.14.0" +hashbrown = "0.14.5" tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } # the event registry is essentially read only but must be an rwlock so we can # setup new events on initialization, hardware-lock-elision hugely benefits this case diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index f87f15f21..b1b267347 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -29,6 +29,6 @@ serde_json = "1.0" thiserror = "1.0" tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.15" -parking_lot = "0.12.1" +parking_lot = "0.12.2" arc-swap = "1" slotmap.workspace = true diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index c34667ab3..e67a03892 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -73,7 +73,7 @@ grep-searcher = "0.1.13" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } -libc = "0.2.153" +libc = "0.2.154" [target.'cfg(target_os = "macos")'.dependencies] crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 9dd8e3cc6..41ac6f527 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -49,7 +49,7 @@ serde_json = "1.0" toml = "0.8" log = "~0.4" -parking_lot = "0.12.1" +parking_lot = "0.12.2" [target.'cfg(windows)'.dependencies] From f4a61006bf70340e57f10842abf47bd88c2cc1ec Mon Sep 17 00:00:00 2001 From: Aral Balkan Date: Wed, 8 May 2024 13:49:44 +0100 Subject: [PATCH 005/200] Remove outdated comment about key binding being disabled (#10717) I just used the `=` binding to format a selection in a CSS file in helix 23.10 (5931a46c) so removing this comment as outdated. --- book/src/keymap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 9a6fecbab..27c5a6b60 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -89,7 +89,7 @@ ### Changes | `"` `` | Select a register to yank to or paste from | `select_register` | | `>` | Indent selection | `indent` | | `<` | Unindent selection | `unindent` | -| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` | +| `=` | Format selection (**LSP**) | `format_selections` | | `d` | Delete selection | `delete_selection` | | `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | From 00e9e5eadef16dd20cd24d303a664faaeb8faa56 Mon Sep 17 00:00:00 2001 From: Felix Richter Date: Thu, 9 May 2024 23:43:07 +0200 Subject: [PATCH 006/200] docs/src/package-managers: update nixos link to official wiki (#10718) This commit updates the the link from the former, unofficial nixos wiki page to the new https://wiki.nixos.org ref: https://github.com/NixOS/foundation/issues/113 --- book/src/package-managers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/package-managers.md b/book/src/package-managers.md index 99d2a3500..3cfd31003 100644 --- a/book/src/package-managers.md +++ b/book/src/package-managers.md @@ -63,7 +63,7 @@ ### NixOS Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute, the unstable channel usually carries the latest release. -Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project +Helix is also available as a [flake](https://wiki.nixos.org/wiki/Flakes) in the project root. Use `nix develop` to spin up a reproducible development shell. Outputs are cached for each push to master using [Cachix](https://www.cachix.org/). The flake is configured to automatically make use of this cache assuming the user From 5fea7cd0ccbcb39f84c6bfc47de78c8889a46fbf Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Mon, 13 May 2024 18:42:34 +0200 Subject: [PATCH 007/200] tree-sitter: Update slint grammar (#10749) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index ded3b656e..2cfa7309b 100644 --- a/languages.toml +++ b/languages.toml @@ -2348,7 +2348,7 @@ language-servers = [ "slint-lsp" ] [[grammar]] name = "slint" -source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "3c82235f41b63f35a01ae3888206e93585cbb84a" } +source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "0701312b74b87fe20e61aa662ba41c5815b5d428" } [[language]] name = "task" From 855568fa341ea352a1064211f20a15ad92c8a633 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 13 May 2024 19:37:35 -0400 Subject: [PATCH 008/200] Synchronize files after writing (#10735) fsync(2) is a somewhat expensive operation that flushes writes to the underlying disk/SSD. It's typically used by databases to ensure that writes survive very hard failure scenarios like your cat kicking the plug out of the wall. Synchronizing isn't automatically done by `flush`ing (from the `std::io::Write` or `tokio::io::AsyncWriteExt` traits). From the [`tokio::fs::File`] moduledocs: > To ensure that a file is closed immediately when it is dropped, you > should call `flush` before dropping it. Note that this does not ensure > that the file has been fully written to disk; the operating system > might keep the changes around in an in-memory buffer. See the > `sync_all` method for telling the OS to write the data to disk. [`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html --- helix-view/src/document.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3393fbed7..b8d318bb8 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -930,6 +930,7 @@ impl Future> + 'static + Send let write_result: anyhow::Result<_> = async { let mut dst = tokio::fs::File::create(&write_path).await?; to_writer(&mut dst, encoding_with_bom_info, &text).await?; + dst.sync_all().await?; Ok(()) } .await; From 380c7adcc0e5eadeebb5f98991034bf68e010656 Mon Sep 17 00:00:00 2001 From: Valentin B <703631+beeb@users.noreply.github.com> Date: Tue, 14 May 2024 10:53:59 +0200 Subject: [PATCH 009/200] feat(solidity): add missing operators for highlights (#10757) * feat(solidity): add missing operators for highlights * fix(solidity): remove invalid operator from highlight queries * Revert "fix(solidity): remove invalid operator from highlight queries" This reverts commit 466f38350c091695dec1729f6fbd6476f2bb7334. * feat(solidity): add highlight query for revert statements with custom errors * feat(solidity): more precise matching for revert statements --- runtime/queries/solidity/highlights.scm | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/runtime/queries/solidity/highlights.scm b/runtime/queries/solidity/highlights.scm index fcc8d3e31..73f354a25 100644 --- a/runtime/queries/solidity/highlights.scm +++ b/runtime/queries/solidity/highlights.scm @@ -75,6 +75,7 @@ ; Invocations (emit_statement . (identifier) @type) +(revert_statement error: (identifier) @type) (modifier_invocation (identifier) @function) (call_expression . (member_expression property: (identifier) @function.method)) @@ -223,6 +224,16 @@ "new" "++" "--" + "+=" + "-=" + "*=" + "/=" + "%=" + "^=" + "&=" + "|=" + "<<=" + ">>=" ] @operator [ From e76020ddb93eeb051de606c24f92189c3fc55547 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 17:54:17 +0900 Subject: [PATCH 010/200] build(deps): bump the rust-dependencies group with 2 updates (#10756) Bumps the rust-dependencies group with 2 updates: [serde](https://github.com/serde-rs/serde) and [serde_json](https://github.com/serde-rs/json). Updates `serde` from 1.0.200 to 1.0.201 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.200...v1.0.201) Updates `serde_json` from 1.0.116 to 1.0.117 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.116...v1.0.117) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d48441d50..58538a1e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2094,18 +2094,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -2114,9 +2114,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", From 889bbefeb387f6984f0a320b64d4a1e5434e768b Mon Sep 17 00:00:00 2001 From: Dmitry Salin Date: Wed, 15 May 2024 17:24:57 +0300 Subject: [PATCH 011/200] Add support for Mojo (#10743) * Add support for Mojo * Update grammar * Fix queries * Fix docs * Use inheritance for some files --- book/src/generated/lang-support.md | 1 + languages.toml | 17 ++ runtime/queries/mojo/highlights.scm | 250 +++++++++++++++++++++++++++ runtime/queries/mojo/indents.scm | 1 + runtime/queries/mojo/injections.scm | 1 + runtime/queries/mojo/locals.scm | 1 + runtime/queries/mojo/textobjects.scm | 1 + 7 files changed, 272 insertions(+) create mode 100644 runtime/queries/mojo/highlights.scm create mode 100644 runtime/queries/mojo/indents.scm create mode 100644 runtime/queries/mojo/injections.scm create mode 100644 runtime/queries/mojo/locals.scm create mode 100644 runtime/queries/mojo/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 27fd583c2..d013f2012 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -125,6 +125,7 @@ | mermaid | ✓ | | | | | meson | ✓ | | ✓ | | | mint | | | | `mint` | +| mojo | ✓ | ✓ | ✓ | `mojo-lsp-server` | | move | ✓ | | | | | msbuild | ✓ | | ✓ | | | nasm | ✓ | ✓ | | | diff --git a/languages.toml b/languages.toml index 2cfa7309b..8776947a8 100644 --- a/languages.toml +++ b/languages.toml @@ -55,6 +55,7 @@ markdown-oxide = { command = "markdown-oxide" } marksman = { command = "marksman", args = ["server"] } metals = { command = "metals", config = { "isHttpEnabled" = true, metals = { inlayHints = { typeParameters = {enable = true} , hintsInPatternMatch = {enable = true} } } } } mint = { command = "mint", args = ["ls"] } +mojo-lsp = { command = "mojo-lsp-server" } nil = { command = "nil" } nimlangserver = { command = "nimlangserver" } nimlsp = { command = "nimlsp" } @@ -372,6 +373,22 @@ block-comment-tokens = { start = "/*", end = "*/" } language-servers = [ "mint" ] indent = { tab-width = 2, unit = " " } +[[language]] +name = "mojo" +scope = "source.mojo" +roots = ["__init__.mojo"] +injection-regex = "mojo" +file-types = ["mojo", "🔥"] +language-servers = [ "mojo-lsp" ] +comment-token = "#" +indent = { tab-width = 4, unit = " " } +auto-format = true +formatter = { command = "mojo", args = ["format", "-q", "-"]} + +[[grammar]] +name = "mojo" +source = { git = "https://github.com/lsh/tree-sitter-mojo", rev = "3d7c53b8038f9ebbb57cd2e61296180aa5c1cf64" } + [[language]] name = "janet" scope = "source.janet" diff --git a/runtime/queries/mojo/highlights.scm b/runtime/queries/mojo/highlights.scm new file mode 100644 index 000000000..7acaf1ead --- /dev/null +++ b/runtime/queries/mojo/highlights.scm @@ -0,0 +1,250 @@ +; Docstrings + +(expression_statement (string) @comment.block.documentation) + +; Imports + +(dotted_name + (identifier)* @namespace) + +(aliased_import + alias: (identifier) @namespace) + +; Builtin functions + +((call + function: (identifier) @function.builtin) + (#match? + @function.builtin + "^(abs|all|always_inline|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|constrained|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unroll|vars|zip|__mlir_attr|__mlir_op|__mlir_type|__import__)$")) + +; Function calls + +[ + "def" + "lambda" + "fn" +] @keyword.function + +(call + function: (attribute attribute: (identifier) @constructor) + (#match? @constructor "^[A-Z]")) + +(call + function: (identifier) @constructor + (#match? @constructor "^[A-Z]")) + +(call + function: (attribute attribute: (identifier) @function.method)) + +(call + function: (identifier) @function) + +; Function definitions + +(function_definition + name: (identifier) @constructor + (#match? @constructor "^(__new__|__init__|__moveinit__|__copyinit__)$")) + +(function_definition + name: (identifier) @function) + +; Decorators + +(decorator) @function +(decorator (identifier) @function) +(decorator (attribute attribute: (identifier) @function)) +(decorator (call + function: (attribute attribute: (identifier) @function))) + +; Parameters + +((identifier) @variable.builtin + (#match? @variable.builtin "^(self|cls)$")) + +(parameters (identifier) @variable.parameter) +(parameters (typed_parameter (identifier) @variable.parameter)) +(parameters (default_parameter name: (identifier) @variable.parameter)) +(parameters (typed_default_parameter name: (identifier) @variable.parameter)) + +(parameters + (list_splat_pattern ; *args + (identifier) @variable.parameter)) + +(parameters + (dictionary_splat_pattern ; **kwargs + (identifier) @variable.parameter)) + +(lambda_parameters + (identifier) @variable.parameter) + +; Types + +((identifier) @type.builtin + (#match? + @type.builtin + "^(bool|bytes|dict|float|frozenset|int|list|set|str|tuple)$")) + +; In type hints make everything types to catch non-conforming identifiers +; (e.g., datetime.datetime) and None +(type [(identifier) (none)] @type) +; Handle [] . and | nesting 4 levels deep +(type + (_ [(identifier) (none)]? @type + (_ [(identifier) (none)]? @type + (_ [(identifier) (none)]? @type + (_ [(identifier) (none)]? @type))))) + +(class_definition name: (identifier) @type) +(class_definition superclasses: (argument_list (identifier) @type)) + +; Variables + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + +((identifier) @type + (#match? @type "^[A-Z]")) + +(attribute attribute: (identifier) @variable.other.member) +(identifier) @variable + +; Literals +(none) @constant.builtin +[ + (true) + (false) +] @constant.builtin.boolean + +(integer) @constant.numeric.integer +(float) @constant.numeric.float +(comment) @comment +(string) @string +(escape_sequence) @constant.character.escape + +["," "." ":" ";" (ellipsis)] @punctuation.delimiter +(interpolation + "{" @punctuation.special + "}" @punctuation.special) @embedded +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +[ + "-" + "-=" + "!=" + "*" + "**" + "**=" + "*=" + "/" + "//" + "//=" + "/=" + "&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "->" + "+=" + "<" + "<<" + "<<=" + "<=" + "<>" + "=" + ":=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "~" + "@=" +] @operator + +[ + "as" + "assert" + "await" + "from" + "pass" + "with" +] @keyword.control + +[ + "if" + "elif" + "else" + "match" + "case" +] @keyword.control.conditional + +[ + "while" + "for" + "break" + "continue" +] @keyword.control.repeat + +[ + "return" + "yield" +] @keyword.control.return + +(yield "from" @keyword.control.return) + +[ + "raise" + "raises" + "try" + "except" + "finally" +] @keyword.control.exception + +(raise_statement "from" @keyword.control.exception) +"import" @keyword.control.import + +(for_statement "in" @keyword.control) +(for_in_clause "in" @keyword.control) + +[ + "alias" + "async" + "class" + "exec" + "global" + "nonlocal" + "print" + "struct" + ; "trait" +] @keyword + +[ + "and" + "or" + "not in" + "in" + "not" + "del" + "is not" + "is" +] @keyword.operator + +"var" @keyword.storage + +[ + "borrowed" + "inout" + "owned" +] @keyword.storage.modifier + +((identifier) @type.builtin + (#match? @type.builtin + "^(BaseException|Exception|ArithmeticError|BufferError|LookupError|AssertionError|AttributeError|EOFError|FloatingPointError|GeneratorExit|ImportError|ModuleNotFoundError|IndexError|KeyError|KeyboardInterrupt|MemoryError|NameError|NotImplementedError|OSError|OverflowError|RecursionError|ReferenceError|RuntimeError|StopIteration|StopAsyncIteration|SyntaxError|IndentationError|TabError|SystemError|SystemExit|TypeError|UnboundLocalError|UnicodeError|UnicodeEncodeError|UnicodeDecodeError|UnicodeTranslateError|ValueError|ZeroDivisionError|EnvironmentError|IOError|WindowsError|BlockingIOError|ChildProcessError|ConnectionError|BrokenPipeError|ConnectionAbortedError|ConnectionRefusedError|ConnectionResetError|FileExistsError|FileNotFoundError|InterruptedError|IsADirectoryError|NotADirectoryError|PermissionError|ProcessLookupError|TimeoutError|Warning|UserWarning|DeprecationWarning|PendingDeprecationWarning|SyntaxWarning|RuntimeWarning|FutureWarning|ImportWarning|UnicodeWarning|BytesWarning|ResourceWarning)$")) + +(ERROR) @error diff --git a/runtime/queries/mojo/indents.scm b/runtime/queries/mojo/indents.scm new file mode 100644 index 000000000..0b920cbf9 --- /dev/null +++ b/runtime/queries/mojo/indents.scm @@ -0,0 +1 @@ +; inherits: python diff --git a/runtime/queries/mojo/injections.scm b/runtime/queries/mojo/injections.scm new file mode 100644 index 000000000..0b920cbf9 --- /dev/null +++ b/runtime/queries/mojo/injections.scm @@ -0,0 +1 @@ +; inherits: python diff --git a/runtime/queries/mojo/locals.scm b/runtime/queries/mojo/locals.scm new file mode 100644 index 000000000..0b920cbf9 --- /dev/null +++ b/runtime/queries/mojo/locals.scm @@ -0,0 +1 @@ +; inherits: python diff --git a/runtime/queries/mojo/textobjects.scm b/runtime/queries/mojo/textobjects.scm new file mode 100644 index 000000000..0b920cbf9 --- /dev/null +++ b/runtime/queries/mojo/textobjects.scm @@ -0,0 +1 @@ +; inherits: python From 2301430e37a98192de7864faa2d8c1b84860f400 Mon Sep 17 00:00:00 2001 From: Krishan <54745129+krish-r@users.noreply.github.com> Date: Fri, 17 May 2024 00:20:01 +0530 Subject: [PATCH 012/200] fix match bracket matching (#10777) --- helix-core/src/match_brackets.rs | 4 ++-- helix-term/tests/test/commands/movement.rs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index 0e08c4898..5351b56dc 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -87,13 +87,13 @@ fn find_pair( (as_char(doc, &open), as_char(doc, &close)) { if PAIRS.contains(&(open, close)) { - if start_pos == pos_ { + if end_pos == pos_ { return Some(start_pos); } // We return the end char if the cursor is either on the start char // or at some arbitrary position between start and end char. - if traverse_parents || end_pos == pos_ { + if traverse_parents || start_pos == pos_ { return Some(end_pos); } } diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index ed6984288..5868fa494 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -895,6 +895,22 @@ fn foo() { } "##}, ), + // named node with 2 or more children + ( + indoc! {r##" + use a::#[{|]# + b::{c, d, e, f, g}, + h, i, j, k, l, m, n, + }; + "##}, + "mm", + indoc! {r##" + use a::{ + b::{c, d, e, f, g}, + h, i, j, k, l, m, n, + #[}|]#; + "##}, + ), ]; let python_tests = vec![ From ff6aca12b73b6f20e99668ea11f604771b6875bf Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 20 May 2024 08:40:55 -0400 Subject: [PATCH 013/200] Reset all changes overlapped by selections in ':reset-diff-change' (#10178) This is useful for resetting multiple changes at once. For example you might use 'maf' or even '%' to select a larger region and reset all changes within. The original behavior of resetting the change on the current line is retained when the primary selection is 1-width since we look for chunks in the line range of each selection. --- helix-core/src/selection.rs | 65 +++++++++++++++++++++++++++++++- helix-term/src/commands/typed.rs | 45 +++++++++++----------- helix-vcs/src/diff.rs | 56 +++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 24 deletions(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 48eaf289c..8995da8f9 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -13,7 +13,7 @@ }; use helix_stdx::rope::{self, RopeSliceExt}; use smallvec::{smallvec, SmallVec}; -use std::borrow::Cow; +use std::{borrow::Cow, iter, slice}; use tree_sitter::Node; /// A single selection range. @@ -503,6 +503,16 @@ pub fn ranges(&self) -> &[Range] { &self.ranges } + /// Returns an iterator over the line ranges of each range in the selection. + /// + /// Adjacent and overlapping line ranges of the [Range]s in the selection are merged. + pub fn line_ranges<'a>(&'a self, text: RopeSlice<'a>) -> LineRangeIter<'a> { + LineRangeIter { + ranges: self.ranges.iter().peekable(), + text, + } + } + pub fn primary_index(&self) -> usize { self.primary_index } @@ -727,6 +737,33 @@ fn from(range: Range) -> Self { } } +pub struct LineRangeIter<'a> { + ranges: iter::Peekable>, + text: RopeSlice<'a>, +} + +impl<'a> Iterator for LineRangeIter<'a> { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + let (start, mut end) = self.ranges.next()?.line_range(self.text); + while let Some((next_start, next_end)) = + self.ranges.peek().map(|range| range.line_range(self.text)) + { + // Merge overlapping and adjacent ranges. + // This subtraction cannot underflow because the ranges are sorted. + if next_start - end <= 1 { + end = next_end; + self.ranges.next(); + } else { + break; + } + } + + Some((start, end)) + } +} + // TODO: checkSelection -> check if valid for doc length && sorted pub fn keep_or_remove_matches( @@ -1165,6 +1202,32 @@ fn test_line_range() { assert_eq!(Range::new(12, 0).line_range(s), (0, 2)); } + #[test] + fn selection_line_ranges() { + let (text, selection) = crate::test::print( + r#" L0 + #[|these]# line #(|ranges)# are #(|merged)# L1 + L2 + single one-line #(|range)# L3 + L4 + single #(|multiline L5 + range)# L6 + L7 + these #(|multiline L8 + ranges)# are #(|also L9 + merged)# L10 + L11 + adjacent #(|ranges)# L12 + are merged #(|the same way)# L13 + "#, + ); + let rope = Rope::from_str(&text); + assert_eq!( + vec![(1, 1), (3, 3), (5, 6), (8, 10), (12, 13)], + selection.line_ranges(rope.slice(..)).collect::>(), + ); + } + #[test] fn test_cursor() { let r = Rope::from_str("\r\nHi\r\nthere!"); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f38ae6bba..b6182f8aa 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2305,37 +2305,36 @@ fn reset_diff_change( let diff = handle.load(); let doc_text = doc.text().slice(..); - let line = doc.selection(view.id).primary().cursor_line(doc_text); - - let Some(hunk_idx) = diff.hunk_at(line as u32, true) else { - bail!("There is no change at the cursor") - }; - let hunk = diff.nth_hunk(hunk_idx); let diff_base = diff.diff_base(); - let before_start = diff_base.line_to_char(hunk.before.start as usize); - let before_end = diff_base.line_to_char(hunk.before.end as usize); - let text: Tendril = diff - .diff_base() - .slice(before_start..before_end) - .chunks() - .collect(); - let anchor = doc_text.line_to_char(hunk.after.start as usize); + let mut changes = 0; + let transaction = Transaction::change( doc.text(), - [( - anchor, - doc_text.line_to_char(hunk.after.end as usize), - (!text.is_empty()).then_some(text), - )] - .into_iter(), + diff.hunks_intersecting_line_ranges(doc.selection(view.id).line_ranges(doc_text)) + .map(|hunk| { + changes += 1; + let start = diff_base.line_to_char(hunk.before.start as usize); + let end = diff_base.line_to_char(hunk.before.end as usize); + let text: Tendril = diff_base.slice(start..end).chunks().collect(); + ( + doc_text.line_to_char(hunk.after.start as usize), + doc_text.line_to_char(hunk.after.end as usize), + (!text.is_empty()).then_some(text), + ) + }), ); + if changes == 0 { + bail!("There are no changes under any selection"); + } + drop(diff); // make borrow check happy doc.apply(&transaction, view.id); - // select inserted text - let text_len = before_end - before_start; - doc.set_selection(view.id, Selection::single(anchor, anchor + text_len)); doc.append_changes_to_history(view); view.ensure_cursor_in_view(doc, scrolloff); + cx.editor.set_status(format!( + "Reset {changes} change{}", + if changes == 1 { "" } else { "s" } + )); Ok(()) } diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index c72deb7ea..634b179b4 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -1,3 +1,4 @@ +use std::iter::Peekable; use std::ops::Range; use std::sync::Arc; @@ -259,6 +260,22 @@ pub fn prev_hunk(&self, line: u32) -> Option { } } + /// Iterates over all hunks that intersect with the given line ranges. + /// + /// Hunks are returned at most once even when intersecting with multiple of the line + /// ranges. + pub fn hunks_intersecting_line_ranges(&self, line_ranges: I) -> impl Iterator + where + I: Iterator, + { + HunksInLineRangesIter { + hunks: &self.diff.hunks, + line_ranges: line_ranges.peekable(), + inverted: self.inverted, + cursor: 0, + } + } + pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option { let hunk_range = if self.inverted { |hunk: &Hunk| hunk.before.clone() @@ -290,3 +307,42 @@ pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option { } } } + +pub struct HunksInLineRangesIter<'a, I: Iterator> { + hunks: &'a [Hunk], + line_ranges: Peekable, + inverted: bool, + cursor: usize, +} + +impl<'a, I: Iterator> Iterator for HunksInLineRangesIter<'a, I> { + type Item = &'a Hunk; + + fn next(&mut self) -> Option { + let hunk_range = if self.inverted { + |hunk: &Hunk| hunk.before.clone() + } else { + |hunk: &Hunk| hunk.after.clone() + }; + + loop { + let (start_line, end_line) = self.line_ranges.peek()?; + let hunk = self.hunks.get(self.cursor)?; + + if (hunk_range(hunk).end as usize) < *start_line { + // If the hunk under the cursor comes before this range, jump the cursor + // ahead to the next hunk that overlaps with the line range. + self.cursor += self.hunks[self.cursor..] + .partition_point(|hunk| (hunk_range(hunk).end as usize) < *start_line); + } else if (hunk_range(hunk).start as usize) <= *end_line { + // If the hunk under the cursor overlaps with this line range, emit it + // and move the cursor up so that the hunk cannot be emitted twice. + self.cursor += 1; + return Some(hunk); + } else { + // Otherwise, go to the next line range. + self.line_ranges.next(); + } + } + } +} From 8444f52e9a53ba770f77b45e4b71b84ba452b627 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 20 May 2024 14:44:53 +0200 Subject: [PATCH 014/200] correctly handle opening helix inside symlinked directory (#10728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * correctly handle opening helix inside symlinked directory * Update helix-stdx/src/env.rs --------- Co-authored-by: Blaž Hrastnik --- helix-stdx/src/env.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/helix-stdx/src/env.rs b/helix-stdx/src/env.rs index 59aba0adc..51450d225 100644 --- a/helix-stdx/src/env.rs +++ b/helix-stdx/src/env.rs @@ -14,13 +14,23 @@ pub fn current_working_dir() -> PathBuf { return path.clone(); } - let path = std::env::current_dir() - .map(crate::path::normalize) - .expect("Couldn't determine current working directory"); - let mut cwd = CWD.write().unwrap(); - *cwd = Some(path.clone()); + // implementation of crossplatform pwd -L + // we want pwd -L so that symlinked directories are handled correctly + let mut cwd = std::env::current_dir().expect("Couldn't determine current working directory"); - path + let pwd = std::env::var_os("PWD"); + #[cfg(windows)] + let pwd = pwd.or_else(|| std::env::var_os("CD")); + + if let Some(pwd) = pwd.map(PathBuf::from) { + if pwd.canonicalize().ok().as_ref() == Some(&cwd) { + cwd = pwd; + } + } + let mut dst = CWD.write().unwrap(); + *dst = Some(cwd.clone()); + + cwd } pub fn set_current_working_dir(path: impl AsRef) -> std::io::Result<()> { From e94735bbd375e623817841f1841e3900c91617df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 21 May 2024 03:50:54 +0900 Subject: [PATCH 015/200] tui: Port https://github.com/ratatui-org/ratatui/pull/1036 --- helix-tui/src/buffer.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 51a864f5f..6126c249c 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -130,22 +130,21 @@ pub struct Buffer { impl Buffer { /// Returns a Buffer with all cells set to the default one + #[must_use] pub fn empty(area: Rect) -> Buffer { - let cell: Cell = Default::default(); - Buffer::filled(area, &cell) + Buffer::filled(area, &Cell::default()) } /// Returns a Buffer with all cells initialized with the attributes of the given Cell + #[must_use] pub fn filled(area: Rect, cell: &Cell) -> Buffer { let size = area.area(); - let mut content = Vec::with_capacity(size); - for _ in 0..size { - content.push(cell.clone()); - } + let content = vec![cell.clone(); size]; Buffer { area, content } } /// Returns a Buffer containing the given lines + #[must_use] pub fn with_lines(lines: Vec) -> Buffer where S: AsRef, From dfcd814389c49a791544530e2ab8b5029d2396a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 21 May 2024 04:34:36 +0900 Subject: [PATCH 016/200] tui: Constify functions, shrink Margin representation --- helix-term/src/ui/completion.rs | 4 +- helix-term/src/ui/info.rs | 7 ++- helix-term/src/ui/lsp.rs | 4 +- helix-term/src/ui/markdown.rs | 2 +- helix-term/src/ui/picker.rs | 20 ++++---- helix-term/src/ui/popup.rs | 6 +-- helix-term/src/ui/prompt.rs | 7 ++- helix-tui/src/layout.rs | 16 +++--- helix-tui/src/widgets/block.rs | 48 +++++++++--------- helix-tui/src/widgets/list.rs | 2 +- helix-tui/src/widgets/paragraph.rs | 2 +- helix-tui/tests/widgets_paragraph.rs | 10 ++-- helix-tui/tests/widgets_table.rs | 12 ++--- helix-view/src/graphics.rs | 74 +++++++++++++--------------- 14 files changed, 102 insertions(+), 112 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index f793dd484..372e3e5ef 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -532,8 +532,8 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { surface.clear_with(doc_area, background); if cx.editor.popup_border() { - use tui::widgets::{Block, Borders, Widget}; - Widget::render(Block::default().borders(Borders::ALL), doc_area, surface); + use tui::widgets::{Block, Widget}; + Widget::render(Block::bordered(), doc_area, surface); } markdown_doc.render(doc_area, surface, cx); diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index 651e5ca93..217cee6b3 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -3,7 +3,7 @@ use helix_view::info::Info; use tui::buffer::Buffer as Surface; use tui::text::Text; -use tui::widgets::{Block, Borders, Paragraph, Widget}; +use tui::widgets::{Block, Paragraph, Widget}; impl Component for Info { fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { @@ -23,13 +23,12 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { )); surface.clear_with(area, popup_style); - let block = Block::default() + let block = Block::bordered() .title(self.title.as_str()) - .borders(Borders::ALL) .border_style(popup_style); let margin = Margin::horizontal(1); - let inner = block.inner(area).inner(&margin); + let inner = block.inner(area).inner(margin); block.render(area, surface); Paragraph::new(&Text::from(self.text.as_str())) diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index d845be4a7..b6491085b 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -126,7 +126,7 @@ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); let sig_text_area = area.clip_top(1).with_height(sig_text_height); - let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area); + let sig_text_area = sig_text_area.inner(margin).intersection(surface.area); let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false }); sig_text_para.render(sig_text_area, surface); @@ -153,7 +153,7 @@ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { let sig_doc_para = Paragraph::new(&sig_doc) .wrap(Wrap { trim: false }) .scroll((cx.scroll.unwrap_or_default() as u16, 0)); - sig_doc_para.render(sig_doc_area.inner(&margin), surface); + sig_doc_para.render(sig_doc_area.inner(margin), surface); } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 81499d039..96614443f 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -351,7 +351,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { .scroll((cx.scroll.unwrap_or_default() as u16, 0)); let margin = Margin::all(1); - par.render(area.inner(&margin), surface); + par.render(area.inner(margin), surface); } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c2728888a..b8ec57d51 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -17,7 +17,7 @@ buffer::Buffer as Surface, layout::Constraint, text::{Span, Spans}, - widgets::{Block, BorderType, Borders, Cell, Table}, + widgets::{Block, BorderType, Cell, Table}, }; use tui::widgets::Widget; @@ -539,13 +539,12 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) let background = cx.editor.theme.get("ui.background"); surface.clear_with(area, background); - // don't like this but the lifetime sucks - let block = Block::default().borders(Borders::ALL); + const BLOCK: Block<'_> = Block::bordered(); // calculate the inner area inside the box - let inner = block.inner(area); + let inner = BLOCK.inner(area); - block.render(area, surface); + BLOCK.render(area, surface); // -- Render the input bar: @@ -690,15 +689,14 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context let text = cx.editor.theme.get("ui.text"); surface.clear_with(area, background); - // don't like this but the lifetime sucks - let block = Block::default().borders(Borders::ALL); + const BLOCK: Block<'_> = Block::bordered(); // calculate the inner area inside the box - let inner = block.inner(area); + let inner = BLOCK.inner(area); // 1 column gap on either side let margin = Margin::horizontal(1); - let inner = inner.inner(&margin); - block.render(area, surface); + let inner = inner.inner(margin); + BLOCK.render(area, surface); if let Some((path, range)) = self.current_file(cx.editor) { let preview = self.get_preview(path, cx.editor); @@ -921,7 +919,7 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { - let block = Block::default().borders(Borders::ALL); + let block = Block::bordered(); // calculate the inner area inside the box let inner = block.inner(area); diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 5362bdc7f..2cefaf61b 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -5,7 +5,7 @@ }; use tui::{ buffer::Buffer as Surface, - widgets::{Block, Borders, Widget}, + widgets::{Block, Widget}, }; use helix_core::Position; @@ -323,8 +323,8 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { let mut inner = area; if render_borders { - inner = area.inner(&Margin::all(1)); - Widget::render(Block::default().borders(Borders::ALL), area, surface); + inner = area.inner(Margin::all(1)); + Widget::render(Block::bordered(), area, surface); } let border = usize::from(render_borders); diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index a6ee7f05d..3e4e8e137 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use std::{borrow::Cow, ops::RangeFrom}; use tui::buffer::Buffer as Surface; -use tui::widgets::{Block, Borders, Widget}; +use tui::widgets::{Block, Widget}; use helix_core::{ unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, @@ -457,12 +457,11 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) let background = theme.get("ui.help"); surface.clear_with(area, background); - let block = Block::default() + let block = Block::bordered() // .title(self.title.as_str()) - .borders(Borders::ALL) .border_style(background); - let inner = block.inner(area).inner(&Margin::horizontal(1)); + let inner = block.inner(area).inner(Margin::horizontal(1)); block.render(area, surface); text.render(inner, surface, cx); diff --git a/helix-tui/src/layout.rs b/helix-tui/src/layout.rs index 1f3ddc6e6..50a16415f 100644 --- a/helix-tui/src/layout.rs +++ b/helix-tui/src/layout.rs @@ -83,24 +83,22 @@ pub fn constraints(mut self, constraints: C) -> Layout self } - pub fn margin(mut self, margin: u16) -> Layout { + pub const fn margin(mut self, margin: u16) -> Layout { self.margin = Margin::all(margin); self } - pub fn horizontal_margin(mut self, horizontal: u16) -> Layout { - self.margin.left = horizontal; - self.margin.right = horizontal; + pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout { + self.margin.horizontal = horizontal; self } - pub fn vertical_margin(mut self, vertical: u16) -> Layout { - self.margin.top = vertical; - self.margin.bottom = vertical; + pub const fn vertical_margin(mut self, vertical: u16) -> Layout { + self.margin.vertical = vertical; self } - pub fn direction(mut self, direction: Direction) -> Layout { + pub const fn direction(mut self, direction: Direction) -> Layout { self.direction = direction; self } @@ -191,7 +189,7 @@ fn split(area: Rect, layout: &Layout) -> Vec { .map(|_| Rect::default()) .collect::>(); - let dest_area = area.inner(&layout.margin); + let dest_area = area.inner(layout.margin); for (i, e) in elements.iter().enumerate() { vars.insert(e.x, (i, 0)); vars.insert(e.y, (i, 1)); diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs index a6fdde4c0..8b8141ea9 100644 --- a/helix-tui/src/widgets/block.rs +++ b/helix-tui/src/widgets/block.rs @@ -1,7 +1,7 @@ use crate::{ buffer::Buffer, symbols::line, - text::{Span, Spans}, + text::Spans, widgets::{Borders, Widget}, }; use helix_view::graphics::{Rect, Style}; @@ -58,6 +58,22 @@ pub struct Block<'a> { } impl<'a> Block<'a> { + pub const fn new() -> Self { + Self { + title: None, + borders: Borders::empty(), + border_style: Style::new(), + border_type: BorderType::Plain, + style: Style::new(), + } + } + + pub const fn bordered() -> Self { + let mut block = Self::new(); + block.borders = Borders::ALL; + block + } + pub fn title(mut self, title: T) -> Block<'a> where T: Into>, @@ -66,34 +82,22 @@ pub fn title(mut self, title: T) -> Block<'a> self } - #[deprecated( - since = "0.10.0", - note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title." - )] - pub fn title_style(mut self, style: Style) -> Block<'a> { - if let Some(t) = self.title { - let title = String::from(&t); - self.title = Some(Spans::from(Span::styled(title, style))); - } - self - } - - pub fn border_style(mut self, style: Style) -> Block<'a> { + pub const fn border_style(mut self, style: Style) -> Block<'a> { self.border_style = style; self } - pub fn style(mut self, style: Style) -> Block<'a> { + pub const fn style(mut self, style: Style) -> Block<'a> { self.style = style; self } - pub fn borders(mut self, flag: Borders) -> Block<'a> { + pub const fn borders(mut self, flag: Borders) -> Block<'a> { self.borders = flag; self } - pub fn border_type(mut self, border_type: BorderType) -> Block<'a> { + pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> { self.border_type = border_type; self } @@ -413,9 +417,7 @@ fn inner_takes_into_account_the_borders() { // All borders assert_eq!( - Block::default() - .borders(Borders::ALL) - .inner(Rect::default()), + Block::bordered().inner(Rect::default()), Rect { x: 0, y: 0, @@ -425,7 +427,7 @@ fn inner_takes_into_account_the_borders() { "all borders, width=0, height=0" ); assert_eq!( - Block::default().borders(Borders::ALL).inner(Rect { + Block::bordered().inner(Rect { x: 0, y: 0, width: 1, @@ -440,7 +442,7 @@ fn inner_takes_into_account_the_borders() { "all borders, width=1, height=1" ); assert_eq!( - Block::default().borders(Borders::ALL).inner(Rect { + Block::bordered().inner(Rect { x: 0, y: 0, width: 2, @@ -455,7 +457,7 @@ fn inner_takes_into_account_the_borders() { "all borders, width=2, height=2" ); assert_eq!( - Block::default().borders(Borders::ALL).inner(Rect { + Block::bordered().inner(Rect { x: 0, y: 0, width: 3, diff --git a/helix-tui/src/widgets/list.rs b/helix-tui/src/widgets/list.rs index e913ce788..4b0fc02f4 100644 --- a/helix-tui/src/widgets/list.rs +++ b/helix-tui/src/widgets/list.rs @@ -72,7 +72,7 @@ pub fn height(&self) -> usize { /// # use helix_tui::style::{Style, Color, Modifier}; /// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")]; /// List::new(items) -/// .block(Block::default().title("List").borders(Borders::ALL)) +/// .block(Block::bordered().title("List")) /// .style(Style::default().fg(Color::White)) /// .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) /// .highlight_symbol(">>"); diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs index 9c8ae127c..79beb0516 100644 --- a/helix-tui/src/widgets/paragraph.rs +++ b/helix-tui/src/widgets/paragraph.rs @@ -37,7 +37,7 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) /// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))), /// ]); /// Paragraph::new(&text) -/// .block(Block::default().title("Paragraph").borders(Borders::ALL)) +/// .block(Block::bordered().title("Paragraph")) /// .style(Style::default().fg(Color::White).bg(Color::Black)) /// .alignment(Alignment::Center) /// .wrap(Wrap { trim: true }); diff --git a/helix-tui/tests/widgets_paragraph.rs b/helix-tui/tests/widgets_paragraph.rs index 3d2ac467b..dcfc6b13f 100644 --- a/helix-tui/tests/widgets_paragraph.rs +++ b/helix-tui/tests/widgets_paragraph.rs @@ -23,7 +23,7 @@ // let size = f.size(); // let text = Text::from(SAMPLE_STRING); // let paragraph = Paragraph::new(&text) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .alignment(alignment) // .wrap(Wrap { trim: true }); // f.render_widget(paragraph, size); @@ -90,7 +90,7 @@ // let size = f.size(); // let text = Text::from(s); // let paragraph = Paragraph::new(&text) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .wrap(Wrap { trim: true }); // f.render_widget(paragraph, size); // }) @@ -122,7 +122,7 @@ // let size = f.size(); // let text = Text::from(s); // let paragraph = Paragraph::new(&text) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .wrap(Wrap { trim: true }); // f.render_widget(paragraph, size); // }) @@ -156,7 +156,7 @@ // .draw(|f| { // let size = f.size(); // let text = Text::from(line); -// let paragraph = Paragraph::new(&text).block(Block::default().borders(Borders::ALL)); +// let paragraph = Paragraph::new(&text).block(Block::bordered()); // f.render_widget(paragraph, size); // }) // .unwrap(); @@ -175,7 +175,7 @@ // "段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line", // ); // let paragraph = Paragraph::new(&text) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .alignment(alignment) // .scroll(scroll); // f.render_widget(paragraph, size); diff --git a/helix-tui/tests/widgets_table.rs b/helix-tui/tests/widgets_table.rs index badf32745..2ec6e3ddb 100644 --- a/helix-tui/tests/widgets_table.rs +++ b/helix-tui/tests/widgets_table.rs @@ -24,7 +24,7 @@ // Row::new(vec!["Row41", "Row42", "Row43"]), // ]) // .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .widths(&[ // Constraint::Length(5), // Constraint::Length(5), @@ -122,7 +122,7 @@ // Row::new(vec!["Row41", "Row42", "Row43"]), // ]) // .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .widths(widths); // f.render_widget(table, size); // }) @@ -210,7 +210,7 @@ // Row::new(vec!["Row41", "Row42", "Row43"]), // ]) // .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .widths(widths) // .column_spacing(0); // f.render_widget(table, size); @@ -316,7 +316,7 @@ // Row::new(vec!["Row41", "Row42", "Row43"]), // ]) // .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .widths(widths); // f.render_widget(table, size); // }) @@ -425,7 +425,7 @@ // Row::new(vec!["Row41", "Row42", "Row43"]), // ]) // .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .widths(widths) // .column_spacing(0); // f.render_widget(table, size); @@ -530,7 +530,7 @@ // Row::new(vec!["Row41", "Row42", "Row43"]).height(2), // ]) // .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) -// .block(Block::default().borders(Borders::ALL)) +// .block(Block::bordered()) // .highlight_symbol(">> ") // .widths(&[ // Constraint::Length(5), diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 046db86a1..a26823b97 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -25,62 +25,52 @@ fn default() -> Self { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct Margin { - pub left: u16, - pub right: u16, - pub top: u16, - pub bottom: u16, + pub horizontal: u16, + pub vertical: u16, } impl Margin { pub fn none() -> Self { Self { - left: 0, - right: 0, - top: 0, - bottom: 0, + horizontal: 0, + vertical: 0, } } /// Set uniform margin for all sides. - pub fn all(value: u16) -> Self { + pub const fn all(value: u16) -> Self { Self { - left: value, - right: value, - top: value, - bottom: value, + horizontal: value, + vertical: value, } } /// Set the margin of left and right sides to specified value. - pub fn horizontal(value: u16) -> Self { + pub const fn horizontal(value: u16) -> Self { Self { - left: value, - right: value, - top: 0, - bottom: 0, + horizontal: value, + vertical: 0, } } /// Set the margin of top and bottom sides to specified value. - pub fn vertical(value: u16) -> Self { + pub const fn vertical(value: u16) -> Self { Self { - left: 0, - right: 0, - top: value, - bottom: value, + horizontal: 0, + vertical: value, } } /// Get the total width of the margin (left + right) - pub fn width(&self) -> u16 { - self.left + self.right + pub const fn width(&self) -> u16 { + self.horizontal * 2 } /// Get the total height of the margin (top + bottom) - pub fn height(&self) -> u16 { - self.top + self.bottom + pub const fn height(&self) -> u16 { + self.vertical * 2 } } @@ -181,13 +171,13 @@ pub fn with_width(self, width: u16) -> Rect { Self::new(self.x, self.y, width, self.height) } - pub fn inner(self, margin: &Margin) -> Rect { + pub fn inner(self, margin: Margin) -> Rect { if self.width < margin.width() || self.height < margin.height() { Rect::default() } else { Rect { - x: self.x + margin.left, - y: self.y + margin.top, + x: self.x + margin.horizontal, + y: self.y + margin.vertical, width: self.width - margin.width(), height: self.height - margin.height(), } @@ -459,7 +449,13 @@ pub struct Style { } impl Default for Style { - fn default() -> Style { + fn default() -> Self { + Self::new() + } +} + +impl Style { + pub const fn new() -> Self { Style { fg: None, bg: None, @@ -469,12 +465,10 @@ fn default() -> Style { sub_modifier: Modifier::empty(), } } -} -impl Style { /// Returns a `Style` resetting all properties. - pub fn reset() -> Style { - Style { + pub const fn reset() -> Self { + Self { fg: Some(Color::Reset), bg: Some(Color::Reset), underline_color: None, @@ -494,7 +488,7 @@ pub fn reset() -> Style { /// let diff = Style::default().fg(Color::Red); /// assert_eq!(style.patch(diff), Style::default().fg(Color::Red)); /// ``` - pub fn fg(mut self, color: Color) -> Style { + pub const fn fg(mut self, color: Color) -> Style { self.fg = Some(color); self } @@ -509,7 +503,7 @@ pub fn fg(mut self, color: Color) -> Style { /// let diff = Style::default().bg(Color::Red); /// assert_eq!(style.patch(diff), Style::default().bg(Color::Red)); /// ``` - pub fn bg(mut self, color: Color) -> Style { + pub const fn bg(mut self, color: Color) -> Style { self.bg = Some(color); self } @@ -524,7 +518,7 @@ pub fn bg(mut self, color: Color) -> Style { /// let diff = Style::default().underline_color(Color::Red); /// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red)); /// ``` - pub fn underline_color(mut self, color: Color) -> Style { + pub const fn underline_color(mut self, color: Color) -> Style { self.underline_color = Some(color); self } @@ -539,7 +533,7 @@ pub fn underline_color(mut self, color: Color) -> Style { /// let diff = Style::default().underline_style(UnderlineStyle::Curl); /// assert_eq!(style.patch(diff), Style::default().underline_style(UnderlineStyle::Curl)); /// ``` - pub fn underline_style(mut self, style: UnderlineStyle) -> Style { + pub const fn underline_style(mut self, style: UnderlineStyle) -> Style { self.underline_style = Some(style); self } From 5b9f5f9fdb7d1729dad6e3ded65ae639878fddd1 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Mon, 20 May 2024 17:46:24 -0400 Subject: [PATCH 017/200] Handle relative symlinks on write (#10790) try again try wip --- helix-term/tests/test/commands/write.rs | 45 +++++++++++++++++++++++++ helix-view/src/document.rs | 10 +++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index 350f34aab..4db98a046 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -604,6 +604,51 @@ async fn test_symlink_write_fail() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_symlink_write_relative() -> anyhow::Result<()> { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(not(unix))] + use std::os::windows::fs::symlink_file as symlink; + + // tempdir + // |- - b + // | |- file + // |- linked (symlink to file) + let dir = tempfile::tempdir()?; + let inner_dir = dir.path().join("b"); + std::fs::create_dir(&inner_dir)?; + + let mut file = tempfile::NamedTempFile::new_in(&inner_dir)?; + let symlink_path = dir.path().join("linked"); + let relative_path = std::path::PathBuf::from("b").join(file.path().file_name().unwrap()); + symlink(relative_path, &symlink_path)?; + + let mut app = helpers::AppBuilder::new() + .with_file(&symlink_path, None) + .build()?; + + test_key_sequence( + &mut app, + Some("ithe gostak distims the doshes:w"), + None, + false, + ) + .await?; + + reload_file(&mut file).unwrap(); + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + + assert_eq!( + LineFeedHandling::Native.apply("the gostak distims the doshes"), + file_content + ); + assert!(symlink_path.is_symlink()); + + Ok(()) +} + async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b8d318bb8..23b597a36 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -895,7 +895,15 @@ impl Future> + 'static + Send } let write_path = tokio::fs::read_link(&path) .await - .unwrap_or_else(|_| path.clone()); + .ok() + .and_then(|p| { + if p.is_relative() { + path.parent().map(|parent| parent.join(p)) + } else { + Some(p) + } + }) + .unwrap_or_else(|| path.clone()); if readonly(&write_path) { bail!(std::io::Error::new( From a789f72a888188ed51923e54a77d4d554b8c4326 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 01:21:53 +0900 Subject: [PATCH 018/200] --- (#10799) updated-dependencies: - dependency-name: cachix/cachix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cachix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 9638137b8..fe192065b 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -17,7 +17,7 @@ jobs: uses: cachix/install-nix-action@v26 - name: Authenticate with Cachix - uses: cachix/cachix-action@v14 + uses: cachix/cachix-action@v15 with: name: helix authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} From 41dec92b0feb77719b07334b7a7f53152866e4e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 01:22:06 +0900 Subject: [PATCH 019/200] --- (#10798) updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cachix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index fe192065b..b8be02e16 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v4 - name: Install nix - uses: cachix/install-nix-action@v26 + uses: cachix/install-nix-action@V27 - name: Authenticate with Cachix uses: cachix/cachix-action@v15 From 4dbb4eeba12aa684a3158a98a97982a55db26257 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 12:08:20 +0900 Subject: [PATCH 020/200] --- (#10804) updated-dependencies: - dependency-name: gix dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 115 ++++++++++++++++++++++--------------------- helix-vcs/Cargo.toml | 2 +- 2 files changed, 60 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58538a1e7..52a0fec87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,9 +422,9 @@ checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fern" @@ -538,9 +538,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gix" -version = "0.62.0" +version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5631c64fb4cd48eee767bf98a3cbc5c9318ef3bb71074d4c099a2371510282b6" +checksum = "984c5018adfa7a4536ade67990b3ebc6e11ab57b3d6cd9968de0947ca99b4b06" dependencies = [ "gix-actor", "gix-attributes", @@ -588,9 +588,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3a3bde455ad2ee8ba8a195745241ce0b770a8a26faae59fcf409d01b28c46" +checksum = "d69c59d392c7e6c94385b6fd6089d6df0fe945f32b4357687989f3aee253cd7f" dependencies = [ "bstr", "gix-date", @@ -637,9 +637,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90009020dc4b3de47beed28e1334706e0a330ddd17f5cfeb097df3b15a54b77" +checksum = "6c22e086314095c43ffe5cdc5c0922d5439da4fd726f3b0438c56147c34dc225" dependencies = [ "bstr", "gix-path", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7580e05996e893347ad04e1eaceb92e1c0e6a3ffe517171af99bf6b6df0ca6e5" +checksum = "53fafe42957e11d98e354a66b6bd70aeea00faf2f62dd11164188224a507c840" dependencies = [ "bstr", "gix-config-value", @@ -697,9 +697,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c" +checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99" dependencies = [ "bstr", "itoa", @@ -709,9 +709,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc24115b957346cd23fb0f47d830eb799c46c89cdcf2f5acc9bf2938c2d01" +checksum = "40b9bd8b2d07b6675a840b56a6c177d322d45fa082672b0dad8f063b25baf0a4" dependencies = [ "bstr", "gix-command", @@ -729,9 +729,9 @@ dependencies = [ [[package]] name = "gix-dir" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6943a1f213ad7a060a0548ece229be53f3c2151534b126446ce3533eaf5f14c" +checksum = "60c99f8c545abd63abe541d20ab6cda347de406c0a3f1c80aadc12d9b0e94974" dependencies = [ "bstr", "gix-discover", @@ -749,9 +749,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bab49087ed3710caf77e473dc0efc54ca33d8ccc6441359725f121211482b1" +checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf" dependencies = [ "bstr", "dunce", @@ -765,9 +765,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.38.1" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4254037d20a247a0367aa79333750146a369719f0c6617fec4f5752cc62b37" +checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69" dependencies = [ "crc32fast", "flate2", @@ -784,9 +784,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0d1f01af62bfd2fb3dd291acc2b29d4ab3e96ad52a679174626508ce98ef12" +checksum = "00ce6ea5ac8fca7adbc63c48a1b9e0492c222c386aa15f513405f1003f2f4ab2" dependencies = [ "bstr", "encoding_rs", @@ -805,10 +805,11 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2184c40e7910529677831c8b481acf788ffd92427ed21fad65b6aa637e631b8" +checksum = "3f78f7d6dcda7a5809efd73a33b145e3dce7421c460df21f32126f9732736b0c" dependencies = [ + "fastrand", "gix-features", "gix-utils", ] @@ -861,9 +862,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3383122cf18655ef4c097c0b935bba5eb56983947959aaf3b0ceb1949d4dd371" +checksum = "2d8c5a5f1c58edcbc5692b174cda2703aba82ed17d7176ff4c1752eb48b1b167" dependencies = [ "bitflags 2.5.0", "bstr", @@ -877,6 +878,7 @@ dependencies = [ "gix-object", "gix-traverse", "gix-utils", + "gix-validate", "hashbrown 0.14.5", "itoa", "libc", @@ -888,9 +890,9 @@ dependencies = [ [[package]] name = "gix-lock" -version = "13.0.0" +version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "651e46174dc5e7d18b7b809d31937b6de3681b1debd78618c99162cc30fcf3e1" +checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" dependencies = [ "gix-tempfile", "gix-utils", @@ -899,9 +901,9 @@ dependencies = [ [[package]] name = "gix-macros" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032" +checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17" dependencies = [ "proc-macro2", "quote", @@ -910,9 +912,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4f8efae72030df1c4a81d02dbe2348e748d9b9a11e108ed6efbd846326e051" +checksum = "1fe2dc4a41191c680c942e6ebd630c8107005983c4679214fdb1007dcf5ae1df" dependencies = [ "bstr", "gix-actor", @@ -929,9 +931,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.60.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8bbb43d2fefdc4701ffdf9224844d05b136ae1b9a73c2f90710c8dd27a93503" +checksum = "e92b9790e2c919166865d0825b26cc440a387c175bed1b43a2fa99c0e9d45e98" dependencies = [ "arc-swap", "gix-date", @@ -949,9 +951,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.50.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58bad27c7677fa6b587aab3a1aca0b6c97373bd371a0a4290677c838c9bcaf1" +checksum = "7a8da51212dbff944713edb2141ed7e002eea326b8992070374ce13a6cb610b3" dependencies = [ "clru", "gix-chunk", @@ -994,9 +996,9 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d479789f3abd10f68a709454ce04cd68b54092ee882c8622ae3aa1bb9bf8496c" +checksum = "a76cab098dc10ba2d89f634f66bf196dea4d7db4bf10b75c7a9c201c55a2ee19" dependencies = [ "bitflags 2.5.0", "bstr", @@ -1020,9 +1022,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd4aba68b925101cb45d6df328979af0681364579db889098a0de75b36c77b65" +checksum = "0b36752b448647acd59c9668fdd830b16d07db1e6d9c3b3af105c1605a6e23d9" dependencies = [ "gix-actor", "gix-date", @@ -1056,9 +1058,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e34196e1969bd5d36e2fbc4467d893999132219d503e23474a8ad2b221cb1e8" +checksum = "63e08f8107ed1f93a83bcfbb4c38084c7cb3f6cd849793f1d5eec235f9b13b2b" dependencies = [ "bstr", "gix-date", @@ -1072,9 +1074,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7d393ae814eeaae41a333c0ff684b243121cc61ccdc5bbe9897094588047d" +checksum = "4181db9cfcd6d1d0fd258e91569dbb61f94cb788b441b5294dd7f1167a3e788f" dependencies = [ "gix-commitgraph", "gix-date", @@ -1099,9 +1101,9 @@ dependencies = [ [[package]] name = "gix-status" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c413bfd2952e4ee92e48438dac3c696f3555e586a34d184a427f6bedd1e4f9" +checksum = "2f4373d989713809554d136f51bc7da565adf45c91aa4d86ef6a79801621bfc8" dependencies = [ "bstr", "filetime", @@ -1121,9 +1123,9 @@ dependencies = [ [[package]] name = "gix-submodule" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb7ea05666362472fecd44c1fc35fe48a5b9b841b431cc4f85b95e6f20c23ec" +checksum = "921cd49924ac14b6611b22e5fb7bbba74d8780dc7ad26153304b64d1272460ac" dependencies = [ "bstr", "gix-config", @@ -1136,9 +1138,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "13.0.0" +version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30" +checksum = "d3b0e276cd08eb2a22e9f286a4f13a222a01be2defafa8621367515375644b99" dependencies = [ "dashmap", "gix-fs", @@ -1156,9 +1158,9 @@ checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e" [[package]] name = "gix-traverse" -version = "0.39.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4029ec209b0cc480d209da3837a42c63801dd8548f09c1f4502c60accb62aeb" +checksum = "f20cb69b63eb3e4827939f42c05b7756e3488ef49c25c412a876691d568ee2a0" dependencies = [ "bitflags 2.5.0", "gix-commitgraph", @@ -1198,9 +1200,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545" +checksum = "82c27dd34a49b1addf193c92070bcbf3beaf6e10f16a78544de6372e146a0acf" dependencies = [ "bstr", "thiserror", @@ -1208,9 +1210,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "359a87dfef695b5f91abb9a424c947edca82768f34acfc269659f66174a510b4" +checksum = "53f6b7de83839274022aff92157d7505f23debf739d257984a300a35972ca94e" dependencies = [ "bstr", "gix-attributes", @@ -1222,6 +1224,7 @@ dependencies = [ "gix-index", "gix-object", "gix-path", + "gix-validate", ] [[package]] diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index a9529ceab..9d6c9a73c 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p parking_lot = "0.12" arc-swap = { version = "1.7.1" } -gix = { version = "0.62.0", features = ["attributes", "status"], default-features = false, optional = true } +gix = { version = "0.63.0", features = ["attributes", "status"], default-features = false, optional = true } imara-diff = "0.1.5" anyhow = "1" From f1c9580e4b636d014fefb61080d8d019c14e37b7 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 24 May 2024 05:09:56 +0200 Subject: [PATCH 021/200] tree-sitter: update Inko grammar (#10805) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 8776947a8..c8da51eb8 100644 --- a/languages.toml +++ b/languages.toml @@ -2721,7 +2721,7 @@ formatter = { command = "inko", args = ["fmt", "-"] } [[grammar]] name = "inko" -source = { git = "https://github.com/inko-lang/tree-sitter-inko", rev = "6983354c13a14bc621d7a3619f1790149e901187" } +source = { git = "https://github.com/inko-lang/tree-sitter-inko", rev = "7860637ce1b43f5f79cfb7cc3311bf3234e9479f" } [[language]] name = "bicep" From 730e684d1dca5736cca41ae336d56962a57b9cfe Mon Sep 17 00:00:00 2001 From: Mark Stosberg Date: Sun, 26 May 2024 21:22:40 -0400 Subject: [PATCH 022/200] Correct typo in "current buffer's directory" (#10814) --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7be2ea095..021b927cf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -326,7 +326,7 @@ pub fn doc(&self) -> &str { append_mode, "Append after selection", command_mode, "Enter command mode", file_picker, "Open file picker", - file_picker_in_current_buffer_directory, "Open file picker at current buffers's directory", + file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", file_picker_in_current_directory, "Open file picker at current working directory", code_action, "Perform code action", buffer_picker, "Open buffer picker", From 179673568df2a519fae2537fbc0053a64ecf3d8b Mon Sep 17 00:00:00 2001 From: Christopher Bayliss Date: Tue, 28 May 2024 21:15:53 +1000 Subject: [PATCH 023/200] avoid cnorm on certain terminals (#10769) using a terminfo's cnorm doesn't reset the cursor for many terminals, see issue: #10089 --- helix-tui/src/backend/crossterm.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index c5c95bff0..1e25b800e 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -23,13 +23,33 @@ fmt, io::{self, Write}, }; +use termini::TermInfo; fn term_program() -> Option { - std::env::var("TERM_PROGRAM").ok() + // Some terminals don't set $TERM_PROGRAM + match std::env::var("TERM_PROGRAM") { + Err(_) => std::env::var("TERM").ok(), + Ok(term_program) => Some(term_program), + } } fn vte_version() -> Option { std::env::var("VTE_VERSION").ok()?.parse().ok() } +fn reset_cursor_approach(terminfo: TermInfo) -> String { + let mut reset_str = "\x1B[0 q".to_string(); + + if let Some(termini::Value::Utf8String(se_str)) = terminfo.extended_cap("Se") { + reset_str.push_str(se_str); + }; + + reset_str.push_str( + terminfo + .utf8_string_cap(termini::StringCapability::CursorNormal) + .unwrap_or(""), + ); + + reset_str +} /// Describes terminal capabilities like extended underline, truecolor, etc. #[derive(Clone, Debug)] @@ -69,10 +89,7 @@ pub fn from_env_or_default(config: &EditorConfig) -> Self { || t.extended_cap("Su").is_some() || vte_version() >= Some(5102) || matches!(term_program().as_deref(), Some("WezTerm")), - reset_cursor_command: t - .utf8_string_cap(termini::StringCapability::CursorNormal) - .unwrap_or("\x1B[0 q") - .to_string(), + reset_cursor_command: reset_cursor_approach(t), }, } } From 972265640d129edf7ab53693628ee6bea470a0e3 Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sun, 2 Jun 2024 17:39:48 +0200 Subject: [PATCH 024/200] fix: correctly reset inlay hints when stopping or restarting LSPs for a document (#10741) --- helix-term/src/commands/typed.rs | 2 ++ helix-view/src/editor.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index b6182f8aa..652106bd2 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1495,6 +1495,8 @@ fn lsp_stop( for doc in cx.editor.documents_mut() { if let Some(client) = doc.remove_language_server_by_name(ls_name) { doc.clear_diagnostics(Some(client.id())); + doc.reset_all_inlay_hints(); + doc.inlay_hints_oudated = true; } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5540c5182..ef4918539 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1356,6 +1356,7 @@ pub fn refresh_doc_language(&mut self, doc_id: DocumentId) { let doc = doc_mut!(self, &doc_id); let diagnostics = Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, doc); doc.replace_diagnostics(diagnostics, &[], None); + doc.reset_all_inlay_hints(); } /// Launch a language server for a given document From 6dbab51f4af34d0a16768ba0aa250c79773bb0ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:08:51 +0900 Subject: [PATCH 025/200] build(deps): bump the rust-dependencies group across 1 directory with 10 updates (#10871) Bumps the rust-dependencies group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [serde](https://github.com/serde-rs/serde) | `1.0.201` | `1.0.203` | | [toml](https://github.com/toml-rs/toml) | `0.8.12` | `0.8.13` | | [parking_lot](https://github.com/Amanieu/parking_lot) | `0.12.2` | `0.12.3` | | [anyhow](https://github.com/dtolnay/anyhow) | `1.0.83` | `1.0.86` | | [tokio](https://github.com/tokio-rs/tokio) | `1.37.0` | `1.38.0` | | [libc](https://github.com/rust-lang/libc) | `0.2.154` | `0.2.155` | | [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) | `0.10.3` | `0.11.0` | | [open](https://github.com/Byron/open-rs) | `5.1.2` | `5.1.3` | | [thiserror](https://github.com/dtolnay/thiserror) | `1.0.60` | `1.0.61` | | [cc](https://github.com/rust-lang/cc-rs) | `1.0.97` | `1.0.98` | Updates `serde` from 1.0.201 to 1.0.203 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.201...v1.0.203) Updates `toml` from 0.8.12 to 0.8.13 - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.12...toml-v0.8.13) Updates `parking_lot` from 0.12.2 to 0.12.3 - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.2...0.12.3) Updates `anyhow` from 1.0.83 to 1.0.86 - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.83...1.0.86) Updates `tokio` from 1.37.0 to 1.38.0 - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.37.0...tokio-1.38.0) Updates `libc` from 0.2.154 to 0.2.155 - [Release notes](https://github.com/rust-lang/libc/releases) - [Commits](https://github.com/rust-lang/libc/compare/0.2.154...0.2.155) Updates `pulldown-cmark` from 0.10.3 to 0.11.0 - [Release notes](https://github.com/raphlinus/pulldown-cmark/releases) - [Commits](https://github.com/raphlinus/pulldown-cmark/compare/v0.10.3...v0.11.0) Updates `open` from 5.1.2 to 5.1.3 - [Release notes](https://github.com/Byron/open-rs/releases) - [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md) - [Commits](https://github.com/Byron/open-rs/compare/v5.1.2...v5.1.3) Updates `thiserror` from 1.0.60 to 1.0.61 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.60...1.0.61) Updates `cc` from 1.0.97 to 1.0.98 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.97...1.0.98) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: parking_lot dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: libc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: pulldown-cmark dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: open dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 64 +++++++++++++++++++++---------------------- helix-lsp/Cargo.toml | 4 +-- helix-term/Cargo.toml | 6 ++-- helix-view/Cargo.toml | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52a0fec87..45f18c8ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arc-swap" @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -1669,9 +1669,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1839,9 +1839,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "5.1.2" +version = "5.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32" +checksum = "2eb49fbd5616580e9974662cb96a3463da4476e649a7e4b258df0de065db0657" dependencies = [ "is-wsl", "libc", @@ -1850,9 +1850,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1912,9 +1912,9 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" [[package]] name = "pulldown-cmark" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" +checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0" dependencies = [ "bitflags 2.5.0", "memchr", @@ -2097,18 +2097,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -2139,9 +2139,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -2328,18 +2328,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -2401,9 +2401,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2420,9 +2420,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -2442,9 +2442,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", @@ -2454,18 +2454,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.8" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap", "serde", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index b1b267347..c6c94b84e 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -27,8 +27,8 @@ lsp-types = { version = "0.95" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.38", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.15" -parking_lot = "0.12.2" +parking_lot = "0.12.3" arc-swap = "1" slotmap.workspace = true diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index e67a03892..ee241c26c 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -53,12 +53,12 @@ log = "0.4" nucleo.workspace = true ignore = "0.4" # markdown doc rendering -pulldown-cmark = { version = "0.10", default-features = false } +pulldown-cmark = { version = "0.11", default-features = false } # file type detection content_inspector = "0.2.4" # opening URLs -open = "5.1.2" +open = "5.1.3" url = "2.5.0" # config @@ -73,7 +73,7 @@ grep-searcher = "0.1.13" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } -libc = "0.2.154" +libc = "0.2.155" [target.'cfg(target_os = "macos")'.dependencies] crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 41ac6f527..903e99ec5 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -49,7 +49,7 @@ serde_json = "1.0" toml = "0.8" log = "~0.4" -parking_lot = "0.12.2" +parking_lot = "0.12.3" [target.'cfg(windows)'.dependencies] From a8010441526714831690b0e920f5776019b98c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Mon, 3 Jun 2024 15:40:30 +0200 Subject: [PATCH 026/200] update tree-sitter-earthfile to 0.5.3 (#10779) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index c8da51eb8..51848b29e 100644 --- a/languages.toml +++ b/languages.toml @@ -3565,7 +3565,7 @@ language-servers = ["earthlyls"] [[grammar]] name = "earthfile" -source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "a079e6c472eeedd6b9a1e03ca0b6c82cd6a112a4" } +source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "dbfb970a59cd87b628d087eb8e3fbe19c8e20601" } [[language]] name = "adl" From 31bcde360c6dbe9931ec3d43964172d0e9d964f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:28:17 +0900 Subject: [PATCH 027/200] build(deps): bump toml in the rust-dependencies group (#10879) Bumps the rust-dependencies group with 1 update: [toml](https://github.com/toml-rs/toml). Updates `toml` from 0.8.13 to 0.8.14 - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.13...toml-v0.8.14) --- updated-dependencies: - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45f18c8ad..4288b8a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2442,9 +2442,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -2463,9 +2463,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", From c39cde8fc2a99871ff1ec9118d65ae404af9e702 Mon Sep 17 00:00:00 2001 From: Marty <58218546+PotatoesFall@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:47:15 +0200 Subject: [PATCH 028/200] Flush pending writes before suspend (#10797) * flush saves before suspending * review suggestion Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com> * review changes --------- Co-authored-by: PotatoesFall Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com> --- helix-term/src/commands.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 021b927cf..9b6140677 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -142,6 +142,17 @@ pub fn callback( pub fn count(&self) -> usize { self.count.map_or(1, |v| v.get()) } + + /// Waits on all pending jobs, and then tries to flush all pending write + /// operations for all documents. + pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { + compositor::Context { + editor: self.editor, + jobs: self.jobs, + scroll: None, + } + .block_try_flush_writes() + } } #[inline] @@ -5828,7 +5839,10 @@ fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBeha fn suspend(_cx: &mut Context) { #[cfg(not(windows))] - signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); + { + _cx.block_try_flush_writes().ok(); + signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); + } } fn add_newline_above(cx: &mut Context) { From 6f1437e9f388f923fadcb3f734e22d2ede298da7 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 5 Jun 2024 23:28:10 -0500 Subject: [PATCH 029/200] LSP: Resolve completion items when any info is missing (#10873) --- helix-term/src/handlers/completion/resolve.rs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/helix-term/src/handlers/completion/resolve.rs b/helix-term/src/handlers/completion/resolve.rs index 6b70e52cd..0b2c90672 100644 --- a/helix-term/src/handlers/completion/resolve.rs +++ b/helix-term/src/handlers/completion/resolve.rs @@ -42,10 +42,30 @@ pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut Completio if item.resolved { return; } - let needs_resolve = item.item.documentation.is_none() - || item.item.detail.is_none() - || item.item.additional_text_edits.is_none(); - if !needs_resolve { + // We consider an item to be fully resolved if it has non-empty, none-`None` details, + // docs and additional text-edits. Ideally we could use `is_some` instead of this + // check but some language servers send values like `Some([])` for additional text + // edits although the items need to be resolved. This is probably a consequence of + // how `null` works in the JavaScript world. + let is_resolved = item + .item + .documentation + .as_ref() + .is_some_and(|docs| match docs { + lsp::Documentation::String(text) => !text.is_empty(), + lsp::Documentation::MarkupContent(markup) => !markup.value.is_empty(), + }) + && item + .item + .detail + .as_ref() + .is_some_and(|detail| !detail.is_empty()) + && item + .item + .additional_text_edits + .as_ref() + .is_some_and(|edits| !edits.is_empty()); + if is_resolved { item.resolved = true; return; } From 886d307b9e145a34a59437055e0021357a9555b5 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 6 Jun 2024 19:24:33 -0400 Subject: [PATCH 030/200] Fix logic to update version when HEAD changes. (#10896) --- helix-loader/build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-loader/build.rs b/helix-loader/build.rs index a4562c00c..ea0689839 100644 --- a/helix-loader/build.rs +++ b/helix-loader/build.rs @@ -50,6 +50,7 @@ fn main() { .ok() .filter(|output| output.status.success()) .and_then(|x| String::from_utf8(x.stdout).ok()) + .map(|x| x.trim().to_string()) else { return; }; @@ -67,6 +68,7 @@ fn main() { .ok() .filter(|output| output.status.success()) .and_then(|x| String::from_utf8(x.stdout).ok()) + .map(|x| x.trim().to_string()) else { return; }; From 3a03109a9971cc8e9501492f2aca234d906ed112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Fern=C3=A1ndez=20Serrata?= <76864299+Rudxain@users.noreply.github.com> Date: Thu, 6 Jun 2024 23:51:40 -0400 Subject: [PATCH 031/200] "it's" -> "its", in `crossterm.rs` (#10860) --- helix-tui/src/backend/crossterm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 1e25b800e..0e60e3866 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -83,7 +83,7 @@ pub fn from_env_or_default(config: &EditorConfig) -> Self { Ok(t) => Capabilities { // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284 // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines - // WezTerm supports underlines but a lot of distros don't properly install it's terminfo + // WezTerm supports underlines but a lot of distros don't properly install its terminfo has_extended_underlines: config.undercurl || t.extended_cap("Smulx").is_some() || t.extended_cap("Su").is_some() From 80e0e98e45d0f6a6ce1bfdd83d8d1fdedd4c702f Mon Sep 17 00:00:00 2001 From: tingerrr Date: Fri, 7 Jun 2024 05:58:26 +0200 Subject: [PATCH 032/200] Add `py`, `hs`, `rs` and `typ` injection regexes (#10785) * Add `py` as valid python injection regex * Add `hs` and `rs` for `haskell` and `rust` * Add `typ` injection regex for `typst` --- languages.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/languages.toml b/languages.toml index 51848b29e..e908ea598 100644 --- a/languages.toml +++ b/languages.toml @@ -223,7 +223,7 @@ mode = "location" [[language]] name = "rust" scope = "source.rust" -injection-regex = "rust" +injection-regex = "rs|rust" file-types = ["rs"] roots = ["Cargo.toml", "Cargo.lock"] shebangs = ["rust-script", "cargo"] @@ -824,7 +824,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53 [[language]] name = "python" scope = "source.python" -injection-regex = "python" +injection-regex = "py(thon)?" file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".python_history" }, { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }] shebangs = ["python"] roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"] @@ -1265,7 +1265,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-yaml", rev = "0e36bed1 [[language]] name = "haskell" scope = "source.haskell" -injection-regex = "haskell" +injection-regex = "hs|haskell" file-types = ["hs", "hs-boot"] roots = ["Setup.hs", "stack.yaml", "cabal.project"] comment-token = "--" @@ -3155,7 +3155,7 @@ grammar = "html" [[language]] name = "typst" scope = "source.typst" -injection-regex = "typst" +injection-regex = "typ(st)?" file-types = ["typst", "typ"] comment-token = "//" language-servers = ["tinymist", "typst-lsp"] From 44504b720b5ad2ac8bf521b678a3fae2370f182d Mon Sep 17 00:00:00 2001 From: Zoey Hewll Date: Fri, 7 Jun 2024 12:02:27 +0800 Subject: [PATCH 033/200] add elisp support (#10644) * add elisp support * update queries for some constants --- book/src/generated/lang-support.md | 1 + languages.toml | 14 ++++++ runtime/queries/elisp/highlights.scm | 72 ++++++++++++++++++++++++++++ runtime/queries/elisp/tags.scm | 5 ++ 4 files changed, 92 insertions(+) create mode 100644 runtime/queries/elisp/highlights.scm create mode 100644 runtime/queries/elisp/tags.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index d013f2012..b9aadc69e 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -42,6 +42,7 @@ | edoc | ✓ | | | | | eex | ✓ | | | | | ejs | ✓ | | | | +| elisp | ✓ | | | | | elixir | ✓ | ✓ | ✓ | `elixir-ls` | | elm | ✓ | ✓ | | `elm-language-server` | | elvish | ✓ | | | `elvish` | diff --git a/languages.toml b/languages.toml index e908ea598..19c5cf9fe 100644 --- a/languages.toml +++ b/languages.toml @@ -3641,6 +3641,20 @@ language-servers = ["pest-language-server"] name = "pest" source = { git = "https://github.com/pest-parser/tree-sitter-pest", rev = "a8a98a824452b1ec4da7f508386a187a2f234b85" } +[[language]] +name = "elisp" +scope = "source.elisp" +file-types = ["el"] +comment-tokens = [";"] + +[language.auto-pairs] +'(' = ')' +'"' = '"' + +[[grammar]] +name = "elisp" +source = { git = "https://github.com/Wilfred/tree-sitter-elisp", rev = "e5524fdccf8c22fc726474a910e4ade976dfc7bb" } + [[language]] name = "gjs" scope = "source.gjs" diff --git a/runtime/queries/elisp/highlights.scm b/runtime/queries/elisp/highlights.scm new file mode 100644 index 000000000..1639168b7 --- /dev/null +++ b/runtime/queries/elisp/highlights.scm @@ -0,0 +1,72 @@ +;; Special forms +[ + "and" + "catch" + "cond" + "condition-case" + "defconst" + "defvar" + "function" + "if" + "interactive" + "lambda" + "let" + "let*" + "or" + "prog1" + "prog2" + "progn" + "quote" + "save-current-buffer" + "save-excursion" + "save-restriction" + "setq" + "setq-default" + "unwind-protect" + "while" +] @keyword + +;; Function definitions +[ + "defun" + "defsubst" + ] @keyword +(function_definition name: (symbol) @function) +(function_definition parameters: (list (symbol) @variable.parameter)) +(function_definition docstring: (string) @comment) + +;; Highlight macro definitions the same way as function definitions. +"defmacro" @keyword +(macro_definition name: (symbol) @function) +(macro_definition parameters: (list (symbol) @variable.parameter)) +(macro_definition docstring: (string) @comment) + +(comment) @comment + +(integer) @constant.numeric.integer +(float) @constant.numeric.float +(char) @constant.character + +(string) @string + +[ + "(" + ")" + "#[" + "[" + "]" +] @punctuation.bracket + +[ + "`" + "#'" + "'" + "," + ",@" +] @operator + +;; Highlight nil and t as constants, unlike other symbols +[ + "nil" + "t" +] @constant.builtin diff --git a/runtime/queries/elisp/tags.scm b/runtime/queries/elisp/tags.scm new file mode 100644 index 000000000..7abcb9a4d --- /dev/null +++ b/runtime/queries/elisp/tags.scm @@ -0,0 +1,5 @@ +;; defun/defsubst +(function_definition name: (symbol) @name) @definition.function + +;; Treat macros as function definitions for the sake of TAGS. +(macro_definition name: (symbol) @name) @definition.function From aa1630a41af774946f12872ffa539f3145eac89e Mon Sep 17 00:00:00 2001 From: Arturs Krumins Date: Fri, 7 Jun 2024 19:29:42 +0200 Subject: [PATCH 034/200] Update Swift Grammar and Queries (#10802) --- book/src/generated/lang-support.md | 2 +- languages.toml | 5 ++-- runtime/queries/swift/highlights.scm | 35 ++++++++++++++++++++++----- runtime/queries/swift/injections.scm | 6 +++++ runtime/queries/swift/locals.scm | 20 ++++++++++++--- runtime/queries/swift/textobjects.scm | 23 ++++++++++++++++++ 6 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 runtime/queries/swift/injections.scm create mode 100644 runtime/queries/swift/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index b9aadc69e..5afde0972 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -190,7 +190,7 @@ | supercollider | ✓ | | | | | svelte | ✓ | | ✓ | `svelteserver` | | sway | ✓ | ✓ | ✓ | `forc` | -| swift | ✓ | | | `sourcekit-lsp` | +| swift | ✓ | ✓ | | `sourcekit-lsp` | | t32 | ✓ | | | | | tablegen | ✓ | ✓ | ✓ | | | tact | ✓ | ✓ | ✓ | | diff --git a/languages.toml b/languages.toml index 19c5cf9fe..1888e8f4e 100644 --- a/languages.toml +++ b/languages.toml @@ -1908,16 +1908,17 @@ language-servers = [ "r" ] name = "swift" scope = "source.swift" injection-regex = "swift" -file-types = ["swift"] +file-types = ["swift", "swiftinterface"] roots = [ "Package.swift" ] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } +formatter = { command = "swift-format", args = [ "--configuration", ".swift-format"] } auto-format = true language-servers = [ "sourcekit-lsp" ] [[grammar]] name = "swift" -source = { git = "https://github.com/alex-pinkus/tree-sitter-swift", rev = "b1b66955d420d5cf5ff268ae552f0d6e43ff66e1" } +source = { git = "https://github.com/alex-pinkus/tree-sitter-swift", rev = "57c1c6d6ffa1c44b330182d41717e6fe37430704" } [[language]] name = "erb" diff --git a/runtime/queries/swift/highlights.scm b/runtime/queries/swift/highlights.scm index e7610e38d..42411d907 100644 --- a/runtime/queries/swift/highlights.scm +++ b/runtime/queries/swift/highlights.scm @@ -1,4 +1,4 @@ -; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/1c586339fb00014b23d6933f2cc32b588a226f3b/queries/highlights.scm +; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/57c1c6d6ffa1c44b330182d41717e6fe37430704/queries/highlights.scm (line_string_literal ["\\(" ")"] @punctuation.special) @@ -10,6 +10,7 @@ (attribute) @variable (type_identifier) @type (self_expression) @variable.builtin +(user_type (type_identifier) @variable.builtin (#eq? @variable.builtin "Self")) ; Declarations "func" @keyword.function @@ -23,7 +24,9 @@ ] @keyword (function_declaration (simple_identifier) @function.method) -(function_declaration "init" @constructor) +(init_declaration ["init" @constructor]) +(deinit_declaration ["deinit" @constructor]) + (throws) @keyword "async" @keyword "await" @keyword @@ -48,10 +51,23 @@ "override" "convenience" "required" - "some" + "mutating" + "associatedtype" + "package" "any" ] @keyword +(opaque_type ["some" @keyword]) +(existential_type ["any" @keyword]) + +(precedence_group_declaration + ["precedencegroup" @keyword] + (simple_identifier) @type) +(precedence_group_attribute + (simple_identifier) @keyword + [(simple_identifier) @type + (boolean_literal) @constant.builtin.boolean]) + [ (getter_specifier) (setter_specifier) @@ -73,6 +89,10 @@ ((navigation_expression (simple_identifier) @type) ; SomeType.method(): highlight SomeType as a type (#match? @type "^[A-Z]")) +(call_expression (simple_identifier) @keyword (#eq? @keyword "defer")) ; defer { ... } + +(try_operator) @operator +(try_operator ["try" @keyword]) (directive) @function.macro (diagnostic) @function.macro @@ -136,10 +156,8 @@ ; Operators [ - "try" - "try?" - "try!" "!" + "?" "+" "-" "*" @@ -171,3 +189,8 @@ "..." (custom_operator) ] @operator + +(value_parameter_pack ["each" @keyword]) +(value_pack_expansion ["repeat" @keyword]) +(type_parameter_pack ["each" @keyword]) +(type_pack_expansion ["repeat" @keyword]) diff --git a/runtime/queries/swift/injections.scm b/runtime/queries/swift/injections.scm new file mode 100644 index 000000000..0ac6cddfe --- /dev/null +++ b/runtime/queries/swift/injections.scm @@ -0,0 +1,6 @@ +; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/57c1c6d6ffa1c44b330182d41717e6fe37430704/queries/injections.scm + +; Parse regex syntax within regex literals + +((regex_literal) @injection.content + (#set! injection.language "regex")) diff --git a/runtime/queries/swift/locals.scm b/runtime/queries/swift/locals.scm index 59cd4c001..31bc9abf1 100644 --- a/runtime/queries/swift/locals.scm +++ b/runtime/queries/swift/locals.scm @@ -1,7 +1,19 @@ +; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/57c1c6d6ffa1c44b330182d41717e6fe37430704/queries/locals.scm +(import_declaration (identifier) @definition.import) +(function_declaration name: (simple_identifier) @definition.function) + +; Scopes [ + (for_statement) + (while_statement) + (repeat_while_statement) + (do_statement) + (if_statement) + (guard_statement) + (switch_statement) + (property_declaration) (function_declaration) + (class_declaration) + (protocol_declaration) + (lambda_literal) ] @local.scope - -(parameter name: (simple_identifier) @local.definition) - -(simple_identifier) @local.reference diff --git a/runtime/queries/swift/textobjects.scm b/runtime/queries/swift/textobjects.scm new file mode 100644 index 000000000..2a9938bf8 --- /dev/null +++ b/runtime/queries/swift/textobjects.scm @@ -0,0 +1,23 @@ +(class_declaration + body: (_) @class.inside) @class.around + +(protocol_declaration + body: (_) @class.inside) @class.around + +(function_declaration + body: (_) @function.inside) @function.around + +(parameter + (_) @parameter.inside) @parameter.around + +(lambda_parameter + (_) @parameter.inside) @parameter.around + +[ + (comment) + (multiline_comment) +] @comment.inside + +(comment)+ @comment.around + +(multiline_comment) @comment.around From 03813bbc2e043d6eb109467fb0617fa4dcbbf482 Mon Sep 17 00:00:00 2001 From: Chris Pyles <40970945+chrispyles@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:07:43 -0700 Subject: [PATCH 035/200] Remove special handling of line ending characters in selection replacement (#10786) * Remove special-casing of line ending characters in selection replacement * Refactor line ending handling and integration test to address code review comments --- helix-term/src/commands.rs | 16 ++++------------ helix-term/tests/test/commands.rs | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9b6140677..5fc832351 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -26,7 +26,7 @@ history::UndoKind, increment, indent, indent::IndentStyle, - line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, + line_ending::{get_line_ending_of_str, line_end_char_index}, match_brackets, movement::{self, move_vertically_visual, Direction}, object, pos_at_coords, @@ -1605,19 +1605,11 @@ fn replace(cx: &mut Context) { if let Some(ch) = ch { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { if !range.is_empty() { - let text: String = + let text: Tendril = RopeGraphemes::new(doc.text().slice(range.from()..range.to())) - .map(|g| { - let cow: Cow = g.into(); - if str_is_line_ending(&cow) { - cow - } else { - ch.into() - } - }) + .map(|_g| ch) .collect(); - - (range.from(), range.to(), Some(text.into())) + (range.from(), range.to(), Some(text)) } else { // No change. (range.from(), range.to(), None) diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 1c5c6196b..7f41a2219 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -722,5 +722,19 @@ fn foo() { )) .await?; + test(( + indoc! {"\ + #[a + b + c + d + e|]# + f + "}, + "s\\nr,", + "a#[,|]#b#(,|)#c#(,|)#d#(,|)#e\nf\n", + )) + .await?; + Ok(()) } From a1cda3c19e510999362f533476dd87e21fc833d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Juul=20Brunsh=C3=B8j?= Date: Mon, 10 Jun 2024 17:08:39 +0200 Subject: [PATCH 036/200] in flake mkShell default RUSTFLAGS to an empty string if unset (#10880) --- flake.nix | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index e113d94b9..c230dc317 100644 --- a/flake.nix +++ b/flake.nix @@ -114,10 +114,7 @@ if pkgs.stdenv.isLinux then pkgs.stdenv else pkgs.clangStdenv; - rustFlagsEnv = - if stdenv.isLinux - then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment'' - else "$RUSTFLAGS"; + rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain; craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default; @@ -183,7 +180,7 @@ shellHook = '' export HELIX_RUNTIME="$PWD/runtime" export RUST_BACKTRACE="1" - export RUSTFLAGS="${rustFlagsEnv}" + export RUSTFLAGS="''${RUSTFLAGS:-""} ${rustFlagsEnv}" ''; }; }) From 265608a3d8d0497fbbec86721fdd548931132839 Mon Sep 17 00:00:00 2001 From: Hendrik Wolff Date: Tue, 11 Jun 2024 00:39:06 +0200 Subject: [PATCH 037/200] Auto Save All Buffers After A Delay (#10899) * auto save after delay * configable * clearer names * init * working with some odd behaviour * working with greater consistency * Apply reviewer suggestions - Remove unneccessary field - Remove blocking save * Improve auto-save configuration Auto save can be configured to trigger on focus loss: ```toml auto-save.focus-lost = true|false ``` and after a time delay (in milli seconds) since last keypress: ```toml auto-save.after-delay.enable = true|false auto-save.after-delay.timeout = [0, u64::MAX] # default: 3000 ``` * Remove boilerplate and unnecessary types * Remove more useless types * Update docs for auto-save.after-delay * Fix wording of (doc) comments relating to auto-save * book: Move auto-save descriptions to separate section --------- Co-authored-by: Miguel Perez Co-authored-by: Miguel Perez --- book/src/editor.md | 11 ++++- helix-term/src/handlers.rs | 7 +++ helix-term/src/handlers/auto_save.rs | 61 +++++++++++++++++++++++++ helix-term/src/ui/editor.rs | 2 +- helix-view/src/editor.rs | 66 ++++++++++++++++++++++++++-- helix-view/src/handlers.rs | 1 + 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 helix-term/src/handlers/auto_save.rs diff --git a/book/src/editor.md b/book/src/editor.md index 88541a7bc..ba03e90e5 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -32,7 +32,6 @@ ### `[editor]` Section | `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | | `auto-completion` | Enable automatic pop up of auto-completion | `true` | | `auto-format` | Enable automatic formatting on save | `true` | -| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` | | `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` | | `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` | @@ -222,6 +221,16 @@ ### `[editor.auto-pairs]` Section '<' = '>' ``` +### `[editor.auto-save]` Section + +Control auto save behavior. + +| Key | Description | Default | +|--|--|---------| +| `focus-lost` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` | +| `after-delay.enable` | Enable automatic saving after `auto-save.after-delay.timeout` milliseconds have passed since last edit. | `false` | +| `after-delay.timeout` | Time in milliseconds since last edit before auto save timer triggers. | `3000` | + ### `[editor.search]` Section Search specific options. diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index d45809d36..fc9273137 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -5,12 +5,14 @@ use crate::config::Config; use crate::events; +use crate::handlers::auto_save::AutoSaveHandler; use crate::handlers::completion::CompletionHandler; use crate::handlers::signature_help::SignatureHelpHandler; pub use completion::trigger_auto_completion; pub use helix_view::handlers::Handlers; +mod auto_save; pub mod completion; mod signature_help; @@ -19,11 +21,16 @@ pub fn setup(config: Arc>) -> Handlers { let completions = CompletionHandler::new(config).spawn(); let signature_hints = SignatureHelpHandler::new().spawn(); + let auto_save = AutoSaveHandler::new().spawn(); + let handlers = Handlers { completions, signature_hints, + auto_save, }; + completion::register_hooks(&handlers); signature_help::register_hooks(&handlers); + auto_save::register_hooks(&handlers); handlers } diff --git a/helix-term/src/handlers/auto_save.rs b/helix-term/src/handlers/auto_save.rs new file mode 100644 index 000000000..d3f7f6fc1 --- /dev/null +++ b/helix-term/src/handlers/auto_save.rs @@ -0,0 +1,61 @@ +use std::time::Duration; + +use anyhow::Ok; +use arc_swap::access::Access; + +use helix_event::{register_hook, send_blocking}; +use helix_view::{events::DocumentDidChange, handlers::Handlers, Editor}; +use tokio::time::Instant; + +use crate::{ + commands, compositor, + job::{self, Jobs}, +}; + +#[derive(Debug)] +pub(super) struct AutoSaveHandler; + +impl AutoSaveHandler { + pub fn new() -> AutoSaveHandler { + AutoSaveHandler + } +} + +impl helix_event::AsyncHook for AutoSaveHandler { + type Event = u64; + + fn handle_event( + &mut self, + timeout: Self::Event, + _: Option, + ) -> Option { + Some(Instant::now() + Duration::from_millis(timeout)) + } + + fn finish_debounce(&mut self) { + job::dispatch_blocking(move |editor, _| request_auto_save(editor)) + } +} + +fn request_auto_save(editor: &mut Editor) { + let context = &mut compositor::Context { + editor, + scroll: Some(0), + jobs: &mut Jobs::new(), + }; + + if let Err(e) = commands::typed::write_all_impl(context, false, false) { + context.editor.set_error(format!("{}", e)); + } +} + +pub(super) fn register_hooks(handlers: &Handlers) { + let tx = handlers.auto_save.clone(); + register_hook!(move |event: &mut DocumentDidChange<'_>| { + let config = event.doc.config.load(); + if config.auto_save.after_delay.enable { + send_blocking(&tx, config.auto_save.after_delay.timeout); + } + Ok(()) + }); +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 97f90f625..d584afbb0 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1451,7 +1451,7 @@ fn handle_event( EventResult::Consumed(None) } Event::FocusLost => { - if context.editor.config().auto_save { + if context.editor.config().auto_save.focus_lost { if let Err(e) = commands::typed::write_all_impl(context, false, false) { context.editor.set_error(format!("{}", e)); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ef4918539..635f72619 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -55,6 +55,8 @@ ArcSwap, }; +pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000; + fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -266,8 +268,11 @@ pub struct Config { pub auto_completion: bool, /// Automatic formatting on save. Defaults to true. pub auto_format: bool, - /// Automatic save on focus lost. Defaults to false. - pub auto_save: bool, + /// Automatic save on focus lost and/or after delay. + /// Time delay in milliseconds since last edit after which auto save timer triggers. + /// Time delay defaults to false with 3000ms delay. Focus lost defaults to false. + #[serde(deserialize_with = "deserialize_auto_save")] + pub auto_save: AutoSave, /// Set a global text_width pub text_width: usize, /// Time in milliseconds since last keypress before idle timers trigger. @@ -771,6 +776,61 @@ pub fn newline(&self) -> WhitespaceRenderValue { } } +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AutoSave { + /// Auto save after a delay in milliseconds. Defaults to disabled. + #[serde(default)] + pub after_delay: AutoSaveAfterDelay, + /// Auto save on focus lost. Defaults to false. + #[serde(default)] + pub focus_lost: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AutoSaveAfterDelay { + #[serde(default)] + /// Enable auto save after delay. Defaults to false. + pub enable: bool, + #[serde(default = "default_auto_save_delay")] + /// Time delay in milliseconds. Defaults to [DEFAULT_AUTO_SAVE_DELAY]. + pub timeout: u64, +} + +impl Default for AutoSaveAfterDelay { + fn default() -> Self { + Self { + enable: false, + timeout: DEFAULT_AUTO_SAVE_DELAY, + } + } +} + +fn default_auto_save_delay() -> u64 { + DEFAULT_AUTO_SAVE_DELAY +} + +fn deserialize_auto_save<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize, Serialize)] + #[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")] + enum AutoSaveToml { + EnableFocusLost(bool), + AutoSave(AutoSave), + } + + match AutoSaveToml::deserialize(deserializer)? { + AutoSaveToml::EnableFocusLost(focus_lost) => Ok(AutoSave { + focus_lost, + ..Default::default() + }), + AutoSaveToml::AutoSave(auto_save) => Ok(auto_save), + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct WhitespaceCharacters { @@ -881,7 +941,7 @@ fn default() -> Self { auto_pairs: AutoPairConfig::default(), auto_completion: true, auto_format: true, - auto_save: false, + auto_save: AutoSave::default(), idle_timeout: Duration::from_millis(250), completion_timeout: Duration::from_millis(250), preview_completion_insert: true, diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs index 724e7b192..352abb881 100644 --- a/helix-view/src/handlers.rs +++ b/helix-view/src/handlers.rs @@ -11,6 +11,7 @@ pub struct Handlers { // only public because most of the actual implementation is in helix-term right now :/ pub completions: Sender, pub signature_hints: Sender, + pub auto_save: Sender, } impl Handlers { From a64dbf825fe99096ab6899551c391b55287d4c94 Mon Sep 17 00:00:00 2001 From: emilylime Date: Tue, 11 Jun 2024 16:16:54 +0300 Subject: [PATCH 038/200] Improve readability of virtual text with 'noctis' theme (#10910) --- runtime/themes/noctis.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/runtime/themes/noctis.toml b/runtime/themes/noctis.toml index db127229e..5846576ed 100644 --- a/runtime/themes/noctis.toml +++ b/runtime/themes/noctis.toml @@ -45,10 +45,13 @@ 'ui.linenr' = { fg = "gray" } # Line numbers. 'ui.linenr.selected' = { fg = "light-green", modifiers = [ "bold" ] } # Current line number. -'ui.virtual' = { fg = "mid-green" } # Namespace for additions to the editing area. +'ui.virtual' = { fg = "autocomp-green" } # Namespace for additions to the editing area. 'ui.virtual.ruler' = { bg = "mid-green" } # Vertical rulers (colored columns in editing area). -'ui.virtual.whitespace' = { fg = "light-gray"} # Whitespace markers in editing area. -'ui.virtual.indent-guide' = { fg = "light-gray" } # Indentation guides. +'ui.virtual.whitespace' = { fg = "mid-green"} # Whitespace markers in editing area. +'ui.virtual.inlay-hint' = { fg = "autocomp-green" } # LSP inlay hints +'ui.virtual.indent-guide' = { fg = "mid-green" } # Indentation guides. +'ui.virtual.wrap' = { fg = "mid-green"} # Soft-wrap indicators +'ui.virtual.jump-label' = { fg = "autocomp-green", modifiers = [ "bold" ] } # Word-jump labels 'ui.statusline' = { fg = "light-green", bg = "autocomp-green"} # Status line. 'ui.statusline.inactive' = { fg = "white", bg = "mid-green"} # Status line in unfocused windows. From 8a549b767b24b14ec1abc3d5502c0ee9dd9e0bd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 01:08:50 +0900 Subject: [PATCH 039/200] build(deps): bump the rust-dependencies group across 1 directory with 5 updates (#10926) * build(deps): bump the rust-dependencies group across 1 directory with 5 updates Bumps the rust-dependencies group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [unicode-width](https://github.com/unicode-rs/unicode-width) | `0.1.12` | `0.1.13` | | [regex](https://github.com/rust-lang/regex) | `1.10.4` | `1.10.5` | | [url](https://github.com/servo/rust-url) | `2.5.0` | `2.5.1` | | [open](https://github.com/Byron/open-rs) | `5.1.3` | `5.1.4` | | [cc](https://github.com/rust-lang/cc-rs) | `1.0.98` | `1.0.99` | Updates `unicode-width` from 0.1.12 to 0.1.13 - [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.12...v0.1.13) Updates `regex` from 1.10.4 to 1.10.5 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.10.4...1.10.5) Updates `url` from 2.5.0 to 2.5.1 - [Release notes](https://github.com/servo/rust-url/releases) - [Commits](https://github.com/servo/rust-url/compare/v2.5.0...v2.5.1) Updates `open` from 5.1.3 to 5.1.4 - [Release notes](https://github.com/Byron/open-rs/releases) - [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md) - [Commits](https://github.com/Byron/open-rs/compare/v5.1.3...v5.1.4) Updates `cc` from 1.0.98 to 1.0.99 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.98...1.0.99) --- updated-dependencies: - dependency-name: unicode-width dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: url dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: open dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] * helix-tui: Use zero-width-space for zero-width grapheme test The update of unicode-width 0.1.13 in the parent commit changed the width of the U+1 codepoint to 1 from 0, causing the test to fail. We can switch to a well known zero-width codepoint of U+200B to fix the behavior. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Michael Davis --- Cargo.lock | 291 +++++++++++++++++++++++++++++++++++++--- helix-term/Cargo.toml | 4 +- helix-tui/src/buffer.rs | 9 +- helix-view/Cargo.toml | 2 +- 4 files changed, 279 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4288b8a05..c9f89cf4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" @@ -351,6 +351,17 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "dunce" version = "1.0.4" @@ -1573,13 +1584,133 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.5.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "idna" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -1698,6 +1829,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.9" @@ -1839,9 +1976,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "5.1.3" +version = "5.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb49fbd5616580e9974662cb96a3463da4476e649a7e4b258df0de065db0657" +checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388" dependencies = [ "is-wsl", "libc", @@ -1999,9 +2136,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2251,6 +2388,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2285,6 +2428,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2384,6 +2538,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2493,12 +2657,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - [[package]] name = "unicode-bom" version = "2.0.2" @@ -2540,15 +2698,15 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", "idna", @@ -2556,6 +2714,18 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "version_check" version = "0.9.4" @@ -2897,6 +3067,18 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "xtask" version = "24.3.0" @@ -2908,6 +3090,30 @@ dependencies = [ "toml", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.31" @@ -2927,3 +3133,46 @@ dependencies = [ "quote", "syn 2.0.48", ] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index ee241c26c..2bad11b87 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -58,8 +58,8 @@ pulldown-cmark = { version = "0.11", default-features = false } content_inspector = "0.2.4" # opening URLs -open = "5.1.3" -url = "2.5.0" +open = "5.1.4" +url = "2.5.1" # config toml = "0.8" diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 6126c249c..d28c32fcc 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -734,13 +734,16 @@ fn buffer_set_string_zero_width() { let area = Rect::new(0, 0, 1, 1); let mut buffer = Buffer::empty(area); + // U+200B is the zero-width space codepoint + assert_eq!("\u{200B}".width(), 0); + // Leading grapheme with zero width - let s = "\u{1}a"; + let s = "\u{200B}a"; buffer.set_stringn(0, 0, s, 1, Style::default()); assert_eq!(buffer, Buffer::with_lines(vec!["a"])); - // Trailing grapheme with zero with - let s = "a\u{1}"; + // Trailing grapheme with zero width + let s = "a\u{200B}"; buffer.set_stringn(0, 0, s, 1, Style::default()); assert_eq!(buffer, Buffer::with_lines(vec!["a"])); } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 903e99ec5..987746a2f 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -32,7 +32,7 @@ tempfile = "3.9" # Conversion traits once_cell = "1.19" -url = "2.5.0" +url = "2.5.1" arc-swap = { version = "1.7.1" } From 9123d3fbb843d778d19569dfee48555584014ee8 Mon Sep 17 00:00:00 2001 From: "Lucas @ StarkWare" <70894690+LucasLvy@users.noreply.github.com> Date: Wed, 12 Jun 2024 02:20:13 +0200 Subject: [PATCH 040/200] feat(cairo): update tree-sitter grammar and queries (#10919) * feat(cairo): update tree-sitter grammar and queries * fix suggestions --- languages.toml | 5 +- runtime/queries/cairo/highlights.scm | 363 +++++++++++++++++++++++++- runtime/queries/cairo/indents.scm | 119 ++++++++- runtime/queries/cairo/injections.scm | 2 +- runtime/queries/cairo/locals.scm | 26 +- runtime/queries/cairo/textobjects.scm | 74 +++++- 6 files changed, 583 insertions(+), 6 deletions(-) diff --git a/languages.toml b/languages.toml index 1888e8f4e..436937cd8 100644 --- a/languages.toml +++ b/languages.toml @@ -2074,9 +2074,12 @@ file-types = ["cairo"] comment-token = "//" indent = { tab-width = 4, unit = " " } # auto-format = true -grammar = "rust" language-servers = [ "cairo-language-server" ] +[[grammar]] +name = "cairo" +source = { git = "https://github.com/starkware-libs/tree-sitter-cairo", rev = "0596baab741ffacdc65c761d5d5ffbbeae97f033" } + [[language]] name = "cpon" scope = "scope.cpon" diff --git a/runtime/queries/cairo/highlights.scm b/runtime/queries/cairo/highlights.scm index ae55c7faf..d2cabd1c5 100644 --- a/runtime/queries/cairo/highlights.scm +++ b/runtime/queries/cairo/highlights.scm @@ -1 +1,362 @@ -; inherits: rust +; ------- +; Tree-Sitter doesn't allow overrides in regards to captures, +; though it is possible to affect the child node of a captured +; node. Thus, the approach here is to flip the order so that +; overrides are unnecessary. +; ------- + +; ------- +; Types +; ------- + +(type_parameters + (type_identifier) @type.parameter) +(constrained_type_parameter + left: (type_identifier) @type.parameter) + +; --- +; Primitives +; --- + +(primitive_type) @type.builtin +(boolean_literal) @constant.builtin.boolean +(numeric_literal) @constant.numeric.integer +[ + (string_literal) + (shortstring_literal) +] @string +[ + (line_comment) +] @comment + +; --- +; Extraneous +; --- + +(enum_variant (identifier) @type.enum.variant) + +(field_initializer + (field_identifier) @variable.other.member) +(shorthand_field_initializer + (identifier) @variable.other.member) +(shorthand_field_identifier) @variable.other.member + + +; --- +; Punctuation +; --- + +[ + "::" + "." + ";" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket +(type_arguments + [ + "<" + ">" + ] @punctuation.bracket) +(type_parameters + [ + "<" + ">" + ] @punctuation.bracket) + +; --- +; Variables +; --- + +(let_declaration + pattern: [ + ((identifier) @variable) + ((tuple_pattern + (identifier) @variable)) + ]) + +; It needs to be anonymous to not conflict with `call_expression` further below. +(_ + value: (field_expression + value: (identifier)? @variable + field: (field_identifier) @variable.other.member)) + +(parameter + pattern: (identifier) @variable.parameter) + +; ------- +; Keywords +; ------- +[ + "match" + "if" + "else" +] @keyword.control.conditional + +[ + "while" + "loop" +] @keyword.control.repeat + +[ + "break" + "continue" + "return" +] @keyword.control.return + +"use" @keyword.control.import +(mod_item "mod" @keyword.control.import !body) +(use_as_clause "as" @keyword.control.import) + + +[ + (crate) + (super) + "as" + "pub" + "mod" + (extern) + (nopanic) + + "impl" + "trait" + "of" + + "default" +] @keyword + +[ + "struct" + "enum" + "type" +] @keyword.storage.type + +"let" @keyword.storage +"fn" @keyword.function + +(mutable_specifier) @keyword.storage.modifier.mut +(ref_specifier) @keyword.storage.modifier.ref + +(snapshot_type "@" @keyword.storage.modifier.ref) + +[ + "const" + "ref" +] @keyword.storage.modifier + +; TODO: variable.mut to highlight mutable identifiers via locals.scm + +; ------- +; Constructors +; ------- +; TODO: this is largely guesswork, remove it once we get actual info from locals.scm or r-a + +(struct_expression + name: (type_identifier) @constructor) + +(tuple_enum_pattern + type: [ + (identifier) @constructor + (scoped_identifier + name: (identifier) @constructor) + ]) +(struct_pattern + type: [ + ((type_identifier) @constructor) + (scoped_type_identifier + name: (type_identifier) @constructor) + ]) +(match_pattern + ((identifier) @constructor) (#match? @constructor "^[A-Z]")) +(or_pattern + ((identifier) @constructor) + ((identifier) @constructor) + (#match? @constructor "^[A-Z]")) + +; ------- +; Guess Other Types +; ------- + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +; --- +; PascalCase identifiers in call_expressions (e.g. `Ok()`) +; are assumed to be enum constructors. +; --- + +(call_expression + function: [ + ((identifier) @constructor + (#match? @constructor "^[A-Z]")) + (scoped_identifier + name: ((identifier) @constructor + (#match? @constructor "^[A-Z]"))) + ]) + +; --- +; PascalCase identifiers under a path which is also PascalCase +; are assumed to be constructors if they have methods or fields. +; --- + +(field_expression + value: (scoped_identifier + path: [ + (identifier) @type + (scoped_identifier + name: (identifier) @type) + ] + name: (identifier) @constructor + (#match? @type "^[A-Z]") + (#match? @constructor "^[A-Z]"))) + +; --- +; Other PascalCase identifiers are assumed to be structs. +; --- + +((identifier) @type + (#match? @type "^[A-Z]")) + +; ------- +; Functions +; ------- + +(call_expression + function: [ + ((identifier) @function) + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function) + ]) +(generic_function + function: [ + ((identifier) @function) + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function.method) + ]) +(function_item + (function + name: (identifier) @function)) + +(function_signature_item + (function + name: (identifier) @function)) + +(external_function_item + (function + name: (identifier) @function)) + +; --- +; Macros +; --- + +(attribute + (identifier) @special + arguments: (token_tree (identifier) @type) + (#eq? @special "derive") +) + +(attribute + (identifier) @function.macro) +(attribute + [ + (identifier) @function.macro + (scoped_identifier + name: (identifier) @function.macro) + ] + (token_tree (identifier) @function.macro)?) + +(inner_attribute_item) @attribute + +(macro_invocation + macro: [ + ((identifier) @function.macro) + (scoped_identifier + name: (identifier) @function.macro) + ] + "!" @function.macro) + + +; ------- +; Operators +; ------- + +[ + "*" + "->" + "=>" + "<=" + "=" + "==" + "!" + "!=" + "%" + "%=" + "@" + "&&" + "|" + "||" + "^" + "*" + "*=" + "-" + "-=" + "+" + "+=" + "/" + "/=" + ">" + "<" + ">=" + ">>" + "<<" +] @operator + +; ------- +; Paths +; ------- + +(use_declaration + argument: (identifier) @namespace) +(use_wildcard + (identifier) @namespace) +(mod_item + name: (identifier) @namespace) +(scoped_use_list + path: (identifier)? @namespace) +(use_list + (identifier) @namespace) +(use_as_clause + path: (identifier)? @namespace + alias: (identifier) @namespace) + +; --- +; Remaining Paths +; --- + +(scoped_identifier + path: (identifier)? @namespace + name: (identifier) @namespace) +(scoped_type_identifier + path: (identifier) @namespace) + +; ------- +; Remaining Identifiers +; ------- + +"?" @special + +(type_identifier) @type +(identifier) @variable +(field_identifier) @variable.other.member diff --git a/runtime/queries/cairo/indents.scm b/runtime/queries/cairo/indents.scm index ae55c7faf..35c162429 100644 --- a/runtime/queries/cairo/indents.scm +++ b/runtime/queries/cairo/indents.scm @@ -1 +1,118 @@ -; inherits: rust +[ + (use_list) + (block) + (match_block) + (arguments) + (parameters) + (declaration_list) + (field_declaration_list) + (field_initializer_list) + (struct_pattern) + (tuple_pattern) + (unit_expression) + (enum_variant_list) + (call_expression) + (binary_expression) + (field_expression) + (tuple_expression) + (array_expression) + + (token_tree) +] @indent + +[ + "}" + "]" + ")" +] @outdent + +; Indent the right side of assignments. +; The #not-same-line? predicate is required to prevent an extra indent for e.g. +; an else-clause where the previous if-clause starts on the same line as the assignment. +(assignment_expression + . + (_) @expr-start + right: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(compound_assignment_expr + . + (_) @expr-start + right: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(let_declaration + "let" @expr-start + value: (_) @indent + alternative: (_)? @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(let_condition + . + (_) @expr-start + value: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(if_expression + . + (_) @expr-start + condition: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(field_pattern + . + (_) @expr-start + pattern: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +; Indent type aliases that span multiple lines, similar to +; regular assignment expressions +(type_item + . + (_) @expr-start + type: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) + +; Some field expressions where the left part is a multiline expression are not +; indented by cargo fmt. +; Because this multiline expression might be nested in an arbitrary number of +; field expressions, this can only be matched using a Regex. +(field_expression + value: (_) @val + "." @outdent + ; Check whether the first line ends with `(`, `{` or `[` (up to whitespace). + (#match? @val "(\\A[^\\n\\r]+(\\(|\\{|\\[)[\\t ]*(\\n|\\r))") +) +; Same as above, but with an additional `call_expression`. This is required since otherwise +; the arguments of the function call won't be outdented. +(call_expression + function: (field_expression + value: (_) @val + "." @outdent + (#match? @val "(\\A[^\\n\\r]+(\\(|\\{|\\[)[\\t ]*(\\n|\\r))") + ) + arguments: (_) @outdent +) + + +; Indent if guards in patterns. +; Since the tree-sitter grammar doesn't create a node for the if expression, +; it's not possible to do this correctly in all cases. Indenting the tail of the +; whole pattern whenever it contains an `if` only fails if the `if` appears after +; the second line of the pattern (which should only rarely be the case) +(match_pattern + . + (_) @expr-start + "if" @pattern-guard + (#not-same-line? @expr-start @pattern-guard) +) @indent + + diff --git a/runtime/queries/cairo/injections.scm b/runtime/queries/cairo/injections.scm index a2358b1ca..e07c83b4d 100644 --- a/runtime/queries/cairo/injections.scm +++ b/runtime/queries/cairo/injections.scm @@ -1,3 +1,3 @@ -([(line_comment) (block_comment)] @injection.content +([(line_comment)] @injection.content (#set! injection.language "comment")) diff --git a/runtime/queries/cairo/locals.scm b/runtime/queries/cairo/locals.scm index ae55c7faf..35acb55c6 100644 --- a/runtime/queries/cairo/locals.scm +++ b/runtime/queries/cairo/locals.scm @@ -1 +1,25 @@ -; inherits: rust +; Scopes + +[ + (function_item) + (struct_item) + (enum_item) + (type_item) + (trait_item) + (impl_item) + (block) +] @local.scope + +; Definitions + +(parameter + (identifier) @local.definition) + +(type_parameters + (type_identifier) @local.definition) +(constrained_type_parameter + left: (type_identifier) @local.definition) + +; References +(identifier) @local.reference +(type_identifier) @local.reference diff --git a/runtime/queries/cairo/textobjects.scm b/runtime/queries/cairo/textobjects.scm index ae55c7faf..4031873de 100644 --- a/runtime/queries/cairo/textobjects.scm +++ b/runtime/queries/cairo/textobjects.scm @@ -1 +1,73 @@ -; inherits: rust +(function_item + body: (_) @function.inside) @function.around + +(struct_item + body: (_) @class.inside) @class.around + +(enum_item + body: (_) @class.inside) @class.around + +(trait_item + body: (_) @class.inside) @class.around + +(impl_item + body: (_) @class.inside) @class.around + +(parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(field_initializer_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +[ + (line_comment) +] @comment.inside + +(line_comment)+ @comment.around + +(; #[test] + (attribute_item + (attribute + (identifier) @_test_attribute)) + ; allow other attributes like #[should_panic] and comments + [ + (attribute_item) + (line_comment) + ]* + ; the test function + (function_item + body: (_) @test.inside) @test.around + (#eq? @_test_attribute "test")) + +(array_expression + (_) @entry.around) + +(tuple_expression + (_) @entry.around) + +(tuple_pattern + (_) @entry.around) + +; Commonly used vec macro intializer is special cased +(macro_invocation + (identifier) @_id (token_tree (_) @entry.around) + (#eq? @_id "array")) + +(enum_variant) @entry.around + +(field_declaration + (_) @entry.inside) @entry.around + +(field_initializer + (_) @entry.inside) @entry.around + +(shorthand_field_initializer) @entry.around From 62655e97f18a78190a87102586dbdc5ac9cbd732 Mon Sep 17 00:00:00 2001 From: Sebastian Poeplau Date: Wed, 12 Jun 2024 16:44:47 +0200 Subject: [PATCH 041/200] Optional history for rename_symbol (#10932) Fix #10560 by accepting an optional history register for the rename_symbol command. --- helix-term/src/commands/lsp.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index e7ba9f0fc..d585e1bed 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1029,11 +1029,12 @@ fn get_prefill_from_lsp_response( fn create_rename_prompt( editor: &Editor, prefill: String, + history_register: Option, language_server_id: Option, ) -> Box { let prompt = ui::Prompt::new( "rename-to:".into(), - None, + history_register, ui::completers::none, move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { @@ -1070,6 +1071,7 @@ fn create_rename_prompt( } let (view, doc) = current_ref!(cx.editor); + let history_register = cx.register; if doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) @@ -1112,14 +1114,14 @@ fn create_rename_prompt( } }; - let prompt = create_rename_prompt(editor, prefill, Some(ls_id)); + let prompt = create_rename_prompt(editor, prefill, history_register, Some(ls_id)); compositor.push(prompt); }, ); } else { let prefill = get_prefill_from_word_boundary(cx.editor); - let prompt = create_rename_prompt(cx.editor, prefill, None); + let prompt = create_rename_prompt(cx.editor, prefill, history_register, None); cx.push_layer(prompt); } } From 9c479e6d2de3bca9dec304f9182cee2b1c0ad766 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Wed, 12 Jun 2024 18:24:24 -0700 Subject: [PATCH 042/200] fix(editor): prevent overflow in count modifier (#10930) --- helix-term/src/ui/editor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index d584afbb0..a071bfaa8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -938,7 +938,11 @@ fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEv // If the count is already started and the input is a number, always continue the count. (key!(i @ '0'..='9'), Some(count)) => { let i = i.to_digit(10).unwrap() as usize; - cxt.editor.count = NonZeroUsize::new(count.get() * 10 + i); + let count = count.get() * 10 + i; + if count > 100_000_000 { + return; + } + cxt.editor.count = NonZeroUsize::new(count); } // A non-zero digit will start the count if that number isn't used by a keymap. (key!(i @ '1'..='9'), None) if !self.keymaps.contains_key(mode, event) => { From 8eda96de6d8df2165c73484c15f35d0fc4ae8767 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 14 Jun 2024 20:58:14 -0500 Subject: [PATCH 043/200] Downgrade unicode-width to 0.1.12 (#10963) unicode-width 0.1.13 contains some fixes that change the widths of line endings, which breaks some assumptions in helix-tui, causing some rendering artifacts. We can downgrade to remove the rendering errors for now. --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9f89cf4f..8c8e97481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2698,9 +2698,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "url" From 43cc30d2256b04c821704a85ea237beefe402758 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Fri, 14 Jun 2024 22:34:33 -0400 Subject: [PATCH 044/200] Sync latest Catppuccin themes changes (#10954) --- runtime/themes/catppuccin_frappe.toml | 6 +---- runtime/themes/catppuccin_latte.toml | 12 +++------ runtime/themes/catppuccin_macchiato.toml | 10 +++----- runtime/themes/catppuccin_mocha.toml | 32 ++++++++++-------------- 4 files changed, 21 insertions(+), 39 deletions(-) diff --git a/runtime/themes/catppuccin_frappe.toml b/runtime/themes/catppuccin_frappe.toml index b63e5270e..2ac76ee7c 100644 --- a/runtime/themes/catppuccin_frappe.toml +++ b/runtime/themes/catppuccin_frappe.toml @@ -1,7 +1,6 @@ inherits = "catppuccin_mocha" [palette] -# catppuccin palette colors rosewater = "#f2d5cf" flamingo = "#eebebe" pink = "#f4b8e4" @@ -16,7 +15,6 @@ sky = "#99d1db" sapphire = "#85c1dc" blue = "#8caaee" lavender = "#babbf1" - text = "#c6d0f5" subtext1 = "#b5bfe2" subtext0 = "#a5adce" @@ -26,13 +24,11 @@ overlay0 = "#737994" surface2 = "#626880" surface1 = "#51576d" surface0 = "#414559" - base = "#303446" mantle = "#292c3c" crust = "#232634" -# derived colors by blending existing palette colors cursorline = "#3b3f52" secondary_cursor = "#b8a5a6" -secondary_cursor_normal = "#9193be" +secondary_cursor_normal = "#9192be" secondary_cursor_insert = "#83a275" diff --git a/runtime/themes/catppuccin_latte.toml b/runtime/themes/catppuccin_latte.toml index 7a015168e..1686f576d 100644 --- a/runtime/themes/catppuccin_latte.toml +++ b/runtime/themes/catppuccin_latte.toml @@ -1,7 +1,6 @@ inherits = "catppuccin_mocha" [palette] -# catppuccin palette colors rosewater = "#dc8a78" flamingo = "#dd7878" pink = "#ea76cb" @@ -16,7 +15,6 @@ sky = "#04a5e5" sapphire = "#209fb5" blue = "#1e66f5" lavender = "#7287fd" - text = "#4c4f69" subtext1 = "#5c5f77" subtext0 = "#6c6f85" @@ -26,13 +24,11 @@ overlay0 = "#9ca0b0" surface2 = "#acb0be" surface1 = "#bcc0cc" surface0 = "#ccd0da" - base = "#eff1f5" mantle = "#e6e9ef" crust = "#dce0e8" -# derived colors by blending existing palette colors -cursorline = "#e9ebf1" -secondary_cursor = "#e2a99e" -secondary_cursor_normal = "#98a7fb" -secondary_cursor_insert = "#75b868" +cursorline = "#e8ecf1" +secondary_cursor = "#e1a99d" +secondary_cursor_normal = "#97a7fb" +secondary_cursor_insert = "#74b867" diff --git a/runtime/themes/catppuccin_macchiato.toml b/runtime/themes/catppuccin_macchiato.toml index 6203eaade..581eb6130 100644 --- a/runtime/themes/catppuccin_macchiato.toml +++ b/runtime/themes/catppuccin_macchiato.toml @@ -1,7 +1,6 @@ inherits = "catppuccin_mocha" [palette] -# catppuccin palette colors rosewater = "#f4dbd6" flamingo = "#f0c6c6" pink = "#f5bde6" @@ -16,7 +15,6 @@ sky = "#91d7e3" sapphire = "#7dc4e4" blue = "#8aadf4" lavender = "#b7bdf8" - text = "#cad3f5" subtext1 = "#b8c0e0" subtext0 = "#a5adcb" @@ -26,13 +24,11 @@ overlay0 = "#6e738d" surface2 = "#5b6078" surface1 = "#494d64" surface0 = "#363a4f" - base = "#24273a" mantle = "#1e2030" crust = "#181926" -# derived colors by blending existing palette colors cursorline = "#303347" -secondary_cursor = "#b6a5a7" -secondary_cursor_normal = "#8b90bf" -secondary_cursor_insert = "#7fa47a" +secondary_cursor = "#b6a6a7" +secondary_cursor_normal = "#8b91bf" +secondary_cursor_insert = "#80a57a" diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 58173dab1..3c030762e 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -1,19 +1,22 @@ # Syntax highlighting # ------------------- +"attribute" = "yellow" + "type" = "yellow" +"type.enum.variant" = "teal" "constructor" = "sapphire" "constant" = "peach" -"constant.builtin" = "peach" "constant.character" = "teal" "constant.character.escape" = "pink" "string" = "green" -"string.regexp" = "peach" +"string.regexp" = "pink" "string.special" = "blue" +"string.special.symbol" = "red" -"comment" = { fg = "overlay1", modifiers = ["italic"] } +"comment" = { fg = "overlay2", modifiers = ["italic"] } "variable" = "text" "variable.parameter" = { fg = "maroon", modifiers = ["italic"] } @@ -26,7 +29,6 @@ "punctuation.special" = "sky" "keyword" = "mauve" -"keyword.storage.modifier.ref" = "teal" "keyword.control.conditional" = { fg = "mauve", modifiers = ["italic"] } "operator" = "sky" @@ -34,10 +36,9 @@ "function" = "blue" "function.macro" = "mauve" -"tag" = "mauve" -"attribute" = "blue" +"tag" = "blue" -"namespace" = { fg = "blue", modifiers = ["italic"] } +"namespace" = { fg = "yellow", modifiers = ["italic"] } "special" = "blue" # fuzzy highlight @@ -51,8 +52,7 @@ "markup.list" = "mauve" "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "rosewater", modifiers = ["underlined"] } +"markup.link.url" = { fg = "blue", modifiers = ["italic", "underlined"] } "markup.link.text" = "blue" "markup.raw" = "flamingo" @@ -70,8 +70,8 @@ "ui.statusline" = { fg = "subtext1", bg = "mantle" } "ui.statusline.inactive" = { fg = "surface2", bg = "mantle" } "ui.statusline.normal" = { fg = "base", bg = "lavender", modifiers = ["bold"] } -"ui.statusline.insert" = { fg = "base", bg = "green", modifiers = ["bold"] } -"ui.statusline.select" = { fg = "base", bg = "flamingo", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "base", bg = "green", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "base", bg = "flamingo", modifiers = ["bold"] } "ui.popup" = { fg = "text", bg = "surface0" } "ui.window" = { fg = "crust" } @@ -88,7 +88,7 @@ "ui.virtual" = "overlay0" "ui.virtual.ruler" = { bg = "surface0" } "ui.virtual.indent-guide" = "surface0" -"ui.virtual.inlay-hint" = { fg = "overlay0", bg = "base" } +"ui.virtual.inlay-hint" = { fg = "surface1", bg = "mantle" } "ui.virtual.jump-label" = { fg = "rosewater", modifiers = ["bold"] } "ui.selection" = { bg = "surface1" } @@ -116,8 +116,6 @@ "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.info" = { underline = { color = "sky", style = "curl" } } "diagnostic.hint" = { underline = { color = "teal", style = "curl" } } -"diagnostic.unnecessary" = { modifiers = ["dim"] } -"diagnostic.deprecated" = { modifiers = ["crossed_out"] } error = "red" warning = "yellow" @@ -125,7 +123,6 @@ info = "sky" hint = "teal" [palette] -# catppuccin palette colors rosewater = "#f5e0dc" flamingo = "#f2cdcd" pink = "#f5c2e7" @@ -140,7 +137,6 @@ sky = "#89dceb" sapphire = "#74c7ec" blue = "#89b4fa" lavender = "#b4befe" - text = "#cdd6f4" subtext1 = "#bac2de" subtext0 = "#a6adc8" @@ -150,13 +146,11 @@ overlay0 = "#6c7086" surface2 = "#585b70" surface1 = "#45475a" surface0 = "#313244" - base = "#1e1e2e" mantle = "#181825" crust = "#11111b" -# derived colors by blending existing palette colors cursorline = "#2a2b3c" secondary_cursor = "#b5a6a8" secondary_cursor_normal = "#878ec0" -secondary_cursor_insert = "#7da87e" +secondary_cursor_insert = "#7ea87f" From dbacaaddcaa4a9dd39c73d0102d03ea7c24ca647 Mon Sep 17 00:00:00 2001 From: slawomirlech Date: Sat, 15 Jun 2024 09:05:04 +0200 Subject: [PATCH 045/200] DAP: Deserialize number IDs (#10943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix deserialization of id * Removing external dependencies This reverts commit 27962afc16c8f047e0c28b181e8a55ba7548cde9. * Fix incorrect import * Adding tests * Moved tests --------- Co-authored-by: Sławomir Lech --- helix-dap/src/types.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs index bbaf53a60..9cec05e65 100644 --- a/helix-dap/src/types.rs +++ b/helix-dap/src/types.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::path::PathBuf; @@ -311,7 +311,8 @@ pub struct Variable { #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Module { - pub id: String, // TODO: || number + #[serde(deserialize_with = "from_number")] + pub id: String, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, @@ -331,6 +332,23 @@ pub struct Module { pub address_range: Option, } +fn from_number<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NumberOrString { + Number(i64), + String(String), + } + + match NumberOrString::deserialize(deserializer)? { + NumberOrString::Number(n) => Ok(n.to_string()), + NumberOrString::String(s) => Ok(s), + } +} + pub mod requests { use super::*; #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] @@ -887,4 +905,18 @@ pub struct Memory { pub offset: usize, pub count: usize, } + + #[test] + fn test_deserialize_module_id_from_number() { + let raw = r#"{"id": 0, "name": "Name"}"#; + let module: super::Module = serde_json::from_str(raw).expect("Error!"); + assert_eq!(module.id, "0"); + } + + #[test] + fn test_deserialize_module_id_from_string() { + let raw = r#"{"id": "0", "name": "Name"}"#; + let module: super::Module = serde_json::from_str(raw).expect("Error!"); + assert_eq!(module.id, "0"); + } } From bc73dd19d35f0d3c69ce26f1c628280d107f77db Mon Sep 17 00:00:00 2001 From: Shaun_Sheep <54682807+GNUSheep@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:38:56 +0200 Subject: [PATCH 046/200] Make prompt use cursor set for Insert mode (#10945) * Resolve issue #10874 * cargo fmt --- helix-term/src/ui/prompt.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 3e4e8e137..14b242df6 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -2,6 +2,7 @@ use crate::{alt, ctrl, key, shift, ui}; use arc_swap::ArcSwap; use helix_core::syntax; +use helix_view::document::Mode; use helix_view::input::KeyEvent; use helix_view::keyboard::KeyCode; use std::sync::Arc; @@ -662,7 +663,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.render_prompt(area, surface, cx) } - fn cursor(&self, area: Rect, _editor: &Editor) -> (Option, CursorKind) { + fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { let line = area.height as usize - 1; ( Some(Position::new( @@ -671,7 +672,7 @@ fn cursor(&self, area: Rect, _editor: &Editor) -> (Option, CursorKind) + self.prompt.len() + UnicodeWidthStr::width(&self.line[..self.cursor]), )), - CursorKind::Block, + editor.config().cursor_shape.from_mode(Mode::Insert), ) } } From afe9049a0ece463b2acb06c0550c949cea138a46 Mon Sep 17 00:00:00 2001 From: Meris Bahtijaragic Date: Tue, 18 Jun 2024 10:39:56 +0200 Subject: [PATCH 047/200] improve jump colors for github_dark themes (#10946) --- runtime/themes/github_dark.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/github_dark.toml b/runtime/themes/github_dark.toml index dafba559d..e98032108 100644 --- a/runtime/themes/github_dark.toml +++ b/runtime/themes/github_dark.toml @@ -61,6 +61,7 @@ label = "scale.red.3" "ui.text.inactive" = "fg.subtle" "ui.virtual" = { fg = "scale.gray.6" } "ui.virtual.ruler" = { bg = "canvas.subtle" } +"ui.virtual.jump-label" = { fg = "scale.red.2", modifiers = ["bold"] } "ui.selection" = { bg = "scale.blue.8" } "ui.selection.primary" = { bg = "scale.blue.7" } From 69acf66cd81c21a2da57a5a3e017aaf64fe22309 Mon Sep 17 00:00:00 2001 From: adiabatic Date: Tue, 18 Jun 2024 01:40:19 -0700 Subject: [PATCH 048/200] Add curly single and double quotes to BRACKETS (#10971) --- helix-core/src/match_brackets.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index 5351b56dc..7520d3e46 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -9,11 +9,13 @@ const MAX_PLAINTEXT_SCAN: usize = 10000; const MATCH_LIMIT: usize = 16; -pub const BRACKETS: [(char, char); 7] = [ +pub const BRACKETS: [(char, char); 9] = [ ('(', ')'), ('{', '}'), ('[', ']'), ('<', '>'), + ('‘', '’'), + ('“', '”'), ('«', '»'), ('「', '」'), ('(', ')'), From 668f1239a94a3a30cc6685094690402a2f0e1943 Mon Sep 17 00:00:00 2001 From: Thomas Schafer <54135831+thomasschafer@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:42:46 +0100 Subject: [PATCH 049/200] Fix jump_backwards behaviour when jumplist is at capacity (#10968) * Fix jump_backwards behaviour when jumplist is at capacity * Decrement self.current while popping from front * Fix issue with conflicting updates to self.current * Realised that truncate is intentional * Use saturating_sub when decrementing current * Fix naming of previous jump, and remove unneeded comment change * Remove unnecessary changes in push * Return num elements removed from front, and use in backward method * Hide num_removed from public interface and tidy up jump location check --- helix-view/src/view.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 5b283b98f..4c0674949 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -38,18 +38,25 @@ pub fn new(initial: Jump) -> Self { Self { jumps, current: 0 } } - pub fn push(&mut self, jump: Jump) { + fn push_impl(&mut self, jump: Jump) -> usize { + let mut num_removed_from_front = 0; self.jumps.truncate(self.current); // don't push duplicates if self.jumps.back() != Some(&jump) { // If the jumplist is full, drop the oldest item. while self.jumps.len() >= JUMP_LIST_CAPACITY { self.jumps.pop_front(); + num_removed_from_front += 1; } self.jumps.push_back(jump); self.current = self.jumps.len(); } + num_removed_from_front + } + + pub fn push(&mut self, jump: Jump) { + self.push_impl(jump); } pub fn forward(&mut self, count: usize) -> Option<&Jump> { @@ -63,13 +70,22 @@ pub fn forward(&mut self, count: usize) -> Option<&Jump> { // Taking view and doc to prevent unnecessary cloning when jump is not required. pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> { - if let Some(current) = self.current.checked_sub(count) { + if let Some(mut current) = self.current.checked_sub(count) { if self.current == self.jumps.len() { let jump = (doc.id(), doc.selection(view_id).clone()); - self.push(jump); + let num_removed = self.push_impl(jump); + current = current.saturating_sub(num_removed); } self.current = current; - self.jumps.get(self.current) + + // Avoid jumping to the current location. + let jump @ (doc_id, selection) = self.jumps.get(self.current)?; + if doc.id() == *doc_id && doc.selection(view_id) == selection { + self.current = self.current.checked_sub(1)?; + self.jumps.get(self.current) + } else { + Some(jump) + } } else { None } From d70f58da10194131bacf596b09b7a057bf7e9425 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:43:36 +0100 Subject: [PATCH 050/200] Fix multiple broken links in the documentation (#10953) * Fix multiple broken links in the documentation * Apply code review suggestion Co-authored-by: Michael Davis --------- Co-authored-by: Michael Davis --- book/src/keymap.md | 5 ++--- book/src/syntax-aware-motions.md | 2 -- book/src/textobjects.md | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 27c5a6b60..402b83ef7 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -25,7 +25,7 @@ ## Keymap > 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type. -> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts. +> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the [wiki](https://github.com/helix-editor/helix/wiki/Terminal-Support) for known conflicts. ## Normal mode @@ -233,8 +233,7 @@ #### Match mode Accessed by typing `m` in [normal mode](#normal-mode). -See the relevant section in [Usage](./usage.md) for an explanation about -[surround](./usage.md#surround) and [textobject](./usage.md#navigating-using-tree-sitter-textobjects) usage. +Please refer to the relevant sections for detailed explanations about [surround](./surround.md) and [textobjects](./textobjects.md). | Key | Description | Command | | ----- | ----------- | ------- | diff --git a/book/src/syntax-aware-motions.md b/book/src/syntax-aware-motions.md index aab52b1b4..17f396fb4 100644 --- a/book/src/syntax-aware-motions.md +++ b/book/src/syntax-aware-motions.md @@ -64,5 +64,3 @@ ## Moving the selection with syntax-aware motions selection to the "func" `identifier`. [lang-support]: ./lang-support.md -[unimpaired-keybinds]: ./keymap.md#unimpaired -[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif diff --git a/book/src/textobjects.md b/book/src/textobjects.md index 92ca5645b..541acab95 100644 --- a/book/src/textobjects.md +++ b/book/src/textobjects.md @@ -27,7 +27,7 @@ ## Selecting and manipulating text with textobjects > 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current document and a special tree-sitter query file to work properly. [Only -some grammars][lang-support] currently have the query file implemented. +some grammars](./lang-support.md) currently have the query file implemented. Contributions are welcome! ## Navigating using tree-sitter textobjects @@ -37,9 +37,9 @@ ## Navigating using tree-sitter textobjects example to move to the next function use `]f`, to move to previous type use `[t`, and so on. -![Tree-sitter-nav-demo][tree-sitter-nav-demo] +![Tree-sitter-nav-demo](https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif) -For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind +For the full reference see the [unimpaired](./keymap.html#unimpaired) section of the key bind documentation. > 💡 This feature relies on tree-sitter textobjects From 94a9c81eb07e03fd7c439e8239041a864106c2b4 Mon Sep 17 00:00:00 2001 From: TiredTumblrina <144416919+TiredTumblrina@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:14:17 -0400 Subject: [PATCH 051/200] Prevent improper files (like /dev/random) from being used as file arguments (#10733) * Implement check before adding path to files * fix problem where directories were removed from args.files * Revert "Implement check before adding path to files" This reverts commit c123944d9b2e125cc0e17e61d53aaffe09234baf. * Dissallow opening of irregular non-symlink files * Fixed issue with creating new file from command line * Fixed linting error. * Optimized regularity check as suggested in review * Created DocumentOpenError Sum Type to switch on in Application * Forgot cargo fmt * Update helix-term/src/application.rs Accept suggestion in review. Co-authored-by: Michael Davis * Moved thiserror version configuration to the workspace instead of the individual packages. --------- Co-authored-by: Michael Davis --- Cargo.lock | 1 + Cargo.toml | 1 + helix-dap/Cargo.toml | 2 +- helix-lsp/Cargo.toml | 2 +- helix-term/src/application.rs | 38 +++++++++++++++++++++++------------ helix-term/src/commands.rs | 1 + helix-term/src/ui/mod.rs | 2 +- helix-view/Cargo.toml | 2 +- helix-view/src/document.rs | 29 ++++++++++++++++++++------ helix-view/src/editor.rs | 6 ++++-- 10 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c8e97481..c2f2735d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,7 @@ dependencies = [ "serde_json", "slotmap", "tempfile", + "thiserror", "tokio", "tokio-stream", "toml", diff --git a/Cargo.toml b/Cargo.toml index 6206281b7..e3ee10319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ package.helix-term.opt-level = 2 tree-sitter = { version = "0.22" } nucleo = "0.2.0" slotmap = "1.0.7" +thiserror = "1.0" [workspace.package] version = "24.3.0" diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index 3521f5890..c37340cc6 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -20,8 +20,8 @@ anyhow = "1.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -thiserror = "1.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } +thiserror.workspace = true [dev-dependencies] fern = "0.6" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index c6c94b84e..ab9251ebe 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -26,9 +26,9 @@ log = "0.4" lsp-types = { version = "0.95" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -thiserror = "1.0" tokio = { version = "1.38", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.15" parking_lot = "0.12.3" arc-swap = "1" slotmap.workspace = true +thiserror.workspace = true diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index f71eed209..9adc764cc 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -9,7 +9,7 @@ use helix_stdx::path::get_relative_path; use helix_view::{ align_view, - document::DocumentSavedEventResult, + document::{DocumentOpenError, DocumentSavedEventResult}, editor::{ConfigEvent, EditorEvent}, graphics::Rect, theme, @@ -186,9 +186,15 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result Action::HorizontalSplit, None => Action::Load, }; - let doc_id = editor - .open(&file, action) - .context(format!("open '{}'", file.to_string_lossy()))?; + let doc_id = match editor.open(&file, action) { + // Ignore irregular files during application init. + Err(DocumentOpenError::IrregularFile) => { + nr_of_files -= 1; + continue; + } + Err(err) => return Err(anyhow::anyhow!(err)), + Ok(doc_id) => doc_id, + }; // with Action::Load all documents have the same view // NOTE: this isn't necessarily true anymore. If // `--vsplit` or `--hsplit` are used, the file which is @@ -199,15 +205,21 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result, } +#[derive(Debug, thiserror::Error)] +pub enum DocumentOpenError { + #[error("path must be a regular file, simlink, or directory")] + IrregularFile, + #[error(transparent)] + IoError(#[from] io::Error), +} + pub struct Document { pub(crate) id: DocumentId, text: Rope, @@ -380,7 +390,7 @@ fn encode_from_utf8( pub fn from_reader( reader: &mut R, encoding: Option<&'static Encoding>, -) -> Result<(Rope, &'static Encoding, bool), Error> { +) -> Result<(Rope, &'static Encoding, bool), io::Error> { // These two buffers are 8192 bytes in size each and are used as // intermediaries during the decoding process. Text read into `buf` // from `reader` is decoded into `buf_out` as UTF-8. Once either @@ -523,7 +533,7 @@ fn read_and_detect_encoding( reader: &mut R, encoding: Option<&'static Encoding>, buf: &mut [u8], -) -> Result<(&'static Encoding, bool, encoding::Decoder, usize), Error> { +) -> Result<(&'static Encoding, bool, encoding::Decoder, usize), io::Error> { let read = reader.read(buf)?; let is_empty = read == 0; let (encoding, has_bom) = encoding @@ -685,11 +695,18 @@ pub fn open( encoding: Option<&'static Encoding>, config_loader: Option>>, config: Arc>, - ) -> Result { + ) -> Result { + // If the path is not a regular file (e.g.: /dev/random) it should not be opened. + if path + .metadata() + .map_or(false, |metadata| !metadata.is_file()) + { + return Err(DocumentOpenError::IrregularFile); + } + // Open the file if it exists, otherwise assume it is a new file (and thus empty). let (rope, encoding, has_bom) = if path.exists() { - let mut file = - std::fs::File::open(path).context(format!("unable to open {:?}", path))?; + let mut file = std::fs::File::open(path)?; from_reader(&mut file, encoding)? } else { let line_ending: LineEnding = config.load().default_line_ending.into(); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 635f72619..3eeb4830e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,6 +1,8 @@ use crate::{ align_view, - document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint}, + document::{ + DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, + }, graphics::{CursorKind, Rect}, handlers::Handlers, info::Info, @@ -1677,7 +1679,7 @@ pub fn new_file_from_stdin(&mut self, action: Action) -> Result Result { + pub fn open(&mut self, path: &Path, action: Action) -> Result { let path = helix_stdx::path::canonicalize(path); let id = self.document_by_path(&path).map(|doc| doc.id); From 0edf60964da1d016f8f6db01cbfffc6648b64365 Mon Sep 17 00:00:00 2001 From: blt-r <63462729+blt-r@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:14:41 +0400 Subject: [PATCH 052/200] Update tree-sitter-rust (#10973) Update to latest commit on master to include fix for a bug that doesn't allow spaces in the shebang line. --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 436937cd8..4a9726385 100644 --- a/languages.toml +++ b/languages.toml @@ -276,7 +276,7 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne [[grammar]] name = "rust" -source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "473634230435c18033384bebaa6d6a17c2523281" } +source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "9c84af007b0f144954adb26b3f336495cbb320a7" } [[language]] name = "sway" From 9b7dffbd613b3ba981890de78712ac0df520f145 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:19:05 -0400 Subject: [PATCH 053/200] Replace unicode-general-category with icu-properties (#10989) --- Cargo.lock | 8 +------- helix-core/Cargo.toml | 2 +- helix-core/src/chars.rs | 4 ++-- helix-core/src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2f2735d3..1e4300a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,6 +1319,7 @@ dependencies = [ "hashbrown 0.14.5", "helix-loader", "helix-stdx", + "icu_properties", "imara-diff", "indoc", "log", @@ -1336,7 +1337,6 @@ dependencies = [ "textwrap", "toml", "tree-sitter", - "unicode-general-category", "unicode-segmentation", "unicode-width", ] @@ -2664,12 +2664,6 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98e90c70c9f0d4d1ee6d0a7d04aa06cb9bbd53d8cfbdd62a0269a7c2eb640552" -[[package]] -name = "unicode-general-category" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" - [[package]] name = "unicode-ident" version = "1.0.8" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 53d4af359..9f9366979 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -24,7 +24,7 @@ smallvec = "1.13" smartstring = "1.0.1" unicode-segmentation = "1.11" unicode-width = "0.1" -unicode-general-category = "0.6" +icu_properties = "1.5" slotmap.workspace = true tree-sitter.workspace = true once_cell = "1.19" diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs index 817bbb86b..220500694 100644 --- a/helix-core/src/chars.rs +++ b/helix-core/src/chars.rs @@ -63,10 +63,10 @@ pub fn char_is_whitespace(ch: char) -> bool { #[inline] pub fn char_is_punctuation(ch: char) -> bool { - use unicode_general_category::{get_general_category, GeneralCategory}; + use icu_properties::{maps::general_category, GeneralCategory}; matches!( - get_general_category(ch), + general_category().get(ch), GeneralCategory::OtherPunctuation | GeneralCategory::OpenPunctuation | GeneralCategory::ClosePunctuation diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 1abd90d10..6b9d359c1 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -30,7 +30,7 @@ pub mod wrap; pub mod unicode { - pub use unicode_general_category as category; + pub use icu_properties as properties; pub use unicode_segmentation as segmentation; pub use unicode_width as width; } From b55cb3aa111c159e2b2f27daa0ff7dd63807056a Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:05:53 -0400 Subject: [PATCH 054/200] Revert "Replace unicode-general-category with icu-properties (#10989)" (#11006) This reverts commit 9b7dffbd613b3ba981890de78712ac0df520f145. --- Cargo.lock | 8 +++++++- helix-core/Cargo.toml | 2 +- helix-core/src/chars.rs | 4 ++-- helix-core/src/lib.rs | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e4300a0a..c2f2735d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,7 +1319,6 @@ dependencies = [ "hashbrown 0.14.5", "helix-loader", "helix-stdx", - "icu_properties", "imara-diff", "indoc", "log", @@ -1337,6 +1336,7 @@ dependencies = [ "textwrap", "toml", "tree-sitter", + "unicode-general-category", "unicode-segmentation", "unicode-width", ] @@ -2664,6 +2664,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98e90c70c9f0d4d1ee6d0a7d04aa06cb9bbd53d8cfbdd62a0269a7c2eb640552" +[[package]] +name = "unicode-general-category" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" + [[package]] name = "unicode-ident" version = "1.0.8" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 9f9366979..53d4af359 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -24,7 +24,7 @@ smallvec = "1.13" smartstring = "1.0.1" unicode-segmentation = "1.11" unicode-width = "0.1" -icu_properties = "1.5" +unicode-general-category = "0.6" slotmap.workspace = true tree-sitter.workspace = true once_cell = "1.19" diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs index 220500694..817bbb86b 100644 --- a/helix-core/src/chars.rs +++ b/helix-core/src/chars.rs @@ -63,10 +63,10 @@ pub fn char_is_whitespace(ch: char) -> bool { #[inline] pub fn char_is_punctuation(ch: char) -> bool { - use icu_properties::{maps::general_category, GeneralCategory}; + use unicode_general_category::{get_general_category, GeneralCategory}; matches!( - general_category().get(ch), + get_general_category(ch), GeneralCategory::OtherPunctuation | GeneralCategory::OpenPunctuation | GeneralCategory::ClosePunctuation diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 6b9d359c1..1abd90d10 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -30,7 +30,7 @@ pub mod wrap; pub mod unicode { - pub use icu_properties as properties; + pub use unicode_general_category as category; pub use unicode_segmentation as segmentation; pub use unicode_width as width; } From 44e113cb76ffeb55c1c852a8833cadb70c64cba3 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 22 Jun 2024 20:06:15 -0500 Subject: [PATCH 055/200] tree-sitter: Update parent links on reused injection layers (#10978) When parsing injections, we skip adding a new layer if there is an existing layer covering the same range. When doing so we did not update the parent layer ID, so some layers could have `parent` layer IDs that pointed to a layer that no longer existed in the `layers` HopSlotMap which could cause a panic when using `A-o`. To fix this we update the `parent` pointer for both newly created injection layers and reused ones. --- helix-core/src/syntax.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 8fda29352..93f618c09 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1363,13 +1363,14 @@ fn point_sub(a: Point, b: Point) -> Point { let depth = layer.depth + 1; // TODO: can't inline this since matches borrows self.layers for (config, ranges) in injections { + let parent = Some(layer_id); let new_layer = LanguageLayer { tree: None, config, depth, ranges, flags: LayerUpdateFlags::empty(), - parent: Some(layer_id), + parent: None, }; // Find an identical existing layer @@ -1381,6 +1382,7 @@ fn point_sub(a: Point, b: Point) -> Point { // ...or insert a new one. let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer)); + self.layers[layer_id].parent = parent; queue.push_back(layer_id); } From 3b5f2e66fc2456a56ff97288593669103eb7a908 Mon Sep 17 00:00:00 2001 From: "J. Dekker" Date: Sun, 23 Jun 2024 03:07:13 +0200 Subject: [PATCH 056/200] base16_default: add styles to newer unthemed features (#10858) * base16_default: add `ui.statusline` for `color-modes` Signed-off-by: J. Dekker * base16_default: add `ui.virtual` default Previously virtual text such as LSP inlay was impossible to distinguish from 'real' text by default. Signed-off-by: J. Dekker --------- Signed-off-by: J. Dekker --- base16_theme.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base16_theme.toml b/base16_theme.toml index 4a071a60b..ce03b9b65 100644 --- a/base16_theme.toml +++ b/base16_theme.toml @@ -28,6 +28,9 @@ "label" = "magenta" "namespace" = "magenta" "ui.help" = { fg = "white", bg = "black" } +"ui.statusline.insert" = { fg = "black", bg = "green" } +"ui.statusline.select" = { fg = "black", bg = "blue" } +"ui.virtual" = { fg = "gray", modifiers = ["italic"] } "ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } "ui.virtual.ruler" = { bg = "black" } From 3706c0dc8535eaa315b31420e9c73533012c0e24 Mon Sep 17 00:00:00 2001 From: tingerrr Date: Sun, 23 Jun 2024 03:07:46 +0200 Subject: [PATCH 057/200] Add block comment tokens for typst (#10955) --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index 4a9726385..4b8299e25 100644 --- a/languages.toml +++ b/languages.toml @@ -3162,6 +3162,7 @@ scope = "source.typst" injection-regex = "typ(st)?" file-types = ["typst", "typ"] comment-token = "//" +block-comment-tokens = { start = "/*", end = "*/" } language-servers = ["tinymist", "typst-lsp"] indent = { tab-width = 2, unit = " " } From b894cf087bd3f92d5b0cb3a8785c43b1d2a935ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gast=C3=B3n=20Alvarez?= Date: Sun, 23 Jun 2024 03:08:20 +0200 Subject: [PATCH 058/200] Add "jsonl" as filetype for JSON lang (#11004) --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index 4b8299e25..eff4ca062 100644 --- a/languages.toml +++ b/languages.toml @@ -431,6 +431,7 @@ file-types = [ "ts.map", "css.map", { glob = ".jslintrc" }, + "jsonl", "jsonld", { glob = ".vuerc" }, { glob = "composer.lock" }, From a982e5ce260e8191a2a24fa62e9e5bdf488bb5d4 Mon Sep 17 00:00:00 2001 From: Ashley Vaughn <59515175+ashl3y-v@users.noreply.github.com> Date: Sat, 22 Jun 2024 18:09:39 -0700 Subject: [PATCH 059/200] add ruler at 101 and text-width at 100 to lean in languages.toml (#10969) --- languages.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/languages.toml b/languages.toml index eff4ca062..7ffc998b8 100644 --- a/languages.toml +++ b/languages.toml @@ -1079,6 +1079,8 @@ comment-token = "--" block-comment-tokens = { start = "/-", end = "-/" } language-servers = [ "lean" ] indent = { tab-width = 2, unit = " " } +rulers = [101] +text-width = 100 [language.auto-pairs] '(' = ')' From b05ed9bf8518be662490fa669bf8b51b0c683fe9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:22:20 +0900 Subject: [PATCH 060/200] build(deps): bump the rust-dependencies group with 4 updates (#11032) Bumps the rust-dependencies group with 4 updates: [bitflags](https://github.com/bitflags/bitflags), [url](https://github.com/servo/rust-url), [cc](https://github.com/rust-lang/cc-rs) and [libloading](https://github.com/nagisa/rust_libloading). Updates `bitflags` from 2.5.0 to 2.6.0 - [Release notes](https://github.com/bitflags/bitflags/releases) - [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md) - [Commits](https://github.com/bitflags/bitflags/compare/2.5.0...2.6.0) Updates `url` from 2.5.1 to 2.5.2 - [Release notes](https://github.com/servo/rust-url/releases) - [Commits](https://github.com/servo/rust-url/compare/v2.5.1...v2.5.2) Updates `cc` from 1.0.99 to 1.0.100 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.99...cc-v1.0.100) Updates `libloading` from 0.8.3 to 0.8.4 - [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.3...0.8.4) --- updated-dependencies: - dependency-name: bitflags dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: url dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: libloading dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 311 +++++------------------------------------- helix-core/Cargo.toml | 2 +- helix-stdx/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- helix-tui/Cargo.toml | 2 +- helix-view/Cargo.toml | 4 +- 6 files changed, 37 insertions(+), 286 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2f2735d3..577298d8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" [[package]] name = "cfg-if" @@ -273,7 +273,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crossterm_winapi", "filedescriptor", "futures-core", @@ -351,17 +351,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "dunce" version = "1.0.4" @@ -699,7 +688,7 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd06203b1a9b33a78c88252a625031b094d9e1b647260070c25b09910c0a804" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bstr", "gix-path", "libc", @@ -831,7 +820,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "682bdc43cb3c00dbedfcc366de2a849b582efd8d886215dbad2ea662ec156bb5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bstr", "gix-features", "gix-path", @@ -877,7 +866,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d8c5a5f1c58edcbc5692b174cda2703aba82ed17d7176ff4c1752eb48b1b167" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bstr", "filetime", "fnv", @@ -1011,7 +1000,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a76cab098dc10ba2d89f634f66bf196dea4d7db4bf10b75c7a9c201c55a2ee19" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bstr", "gix-attributes", "gix-config-value", @@ -1104,7 +1093,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fddc27984a643b20dd03e97790555804f98cf07404e0e552c0ad8133266a79a1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gix-path", "libc", "windows-sys 0.52.0", @@ -1173,7 +1162,7 @@ version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f20cb69b63eb3e4827939f42c05b7756e3488ef49c25c412a876691d568ee2a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gix-commitgraph", "gix-date", "gix-hash", @@ -1310,7 +1299,7 @@ version = "24.3.0" dependencies = [ "ahash", "arc-swap", - "bitflags 2.5.0", + "bitflags 2.6.0", "chrono", "dunce", "encoding_rs", @@ -1421,7 +1410,7 @@ version = "24.3.0" name = "helix-stdx" version = "24.3.0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "dunce", "etcetera", "regex-cursor", @@ -1479,7 +1468,7 @@ dependencies = [ name = "helix-tui" version = "24.3.0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cassowary", "crossterm", "helix-core", @@ -1513,7 +1502,7 @@ version = "24.3.0" dependencies = [ "anyhow", "arc-swap", - "bitflags 2.5.0", + "bitflags 2.6.0", "chardetng", "clipboard-win", "crossterm", @@ -1584,134 +1573,14 @@ dependencies = [ "cxx-build", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -1807,9 +1676,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.0", @@ -1830,12 +1699,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.9" @@ -2054,7 +1917,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "memchr", "unicase", ] @@ -2199,7 +2062,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2389,12 +2252,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -2429,17 +2286,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "tempfile" version = "3.10.1" @@ -2539,16 +2385,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -2658,6 +2494,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-bom" version = "2.0.2" @@ -2705,9 +2547,9 @@ checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2715,18 +2557,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "version_check" version = "0.9.4" @@ -3068,18 +2898,6 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "xtask" version = "24.3.0" @@ -3091,30 +2909,6 @@ dependencies = [ "toml", ] -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.31" @@ -3134,46 +2928,3 @@ dependencies = [ "quote", "syn 2.0.48", ] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 53d4af359..3d5d554fd 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -30,7 +30,7 @@ tree-sitter.workspace = true once_cell = "1.19" arc-swap = "1" regex = "1" -bitflags = "2.5" +bitflags = "2.6" ahash = "0.8.11" hashbrown = { version = "0.14.5", features = ["raw"] } dunce = "1.0" diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index f17e08854..5aef65905 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -17,7 +17,7 @@ etcetera = "0.8" ropey = { version = "1.6.1", default-features = false } which = "6.0" regex-cursor = "0.1.4" -bitflags = "2.4" +bitflags = "2.6" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.52", features = ["Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading"] } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 2bad11b87..99f966d31 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -59,7 +59,7 @@ content_inspector = "0.2.4" # opening URLs open = "5.1.4" -url = "2.5.1" +url = "2.5.2" # config toml = "0.8" diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 9b64a45bd..2bdb494f6 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -18,7 +18,7 @@ default = ["crossterm"] helix-view = { path = "../helix-view", features = ["term"] } helix-core = { path = "../helix-core" } -bitflags = "2.5" +bitflags = "2.6" cassowary = "0.3" unicode-segmentation = "1.11" crossterm = { version = "0.27", optional = true } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index a19357f7f..5de689876 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -24,7 +24,7 @@ helix-lsp = { path = "../helix-lsp" } helix-dap = { path = "../helix-dap" } helix-vcs = { path = "../helix-vcs" } -bitflags = "2.5" +bitflags = "2.6" anyhow = "1" crossterm = { version = "0.27", optional = true } @@ -32,7 +32,7 @@ tempfile = "3.9" # Conversion traits once_cell = "1.19" -url = "2.5.1" +url = "2.5.2" arc-swap = { version = "1.7.1" } From 6ed0d0cd39d40a9ed3a693c0473499a71ea6f152 Mon Sep 17 00:00:00 2001 From: Thomas Schafer <54135831+thomasschafer@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:03:21 +0100 Subject: [PATCH 061/200] Only pluralise "buffer" when required (#11018) * Only pluralise buffer when required * Use == 1 instead of != 1 --- helix-term/src/commands/typed.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 652106bd2..ab12fab10 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -164,9 +164,10 @@ fn buffer_close_by_ids_impl( cx.editor.switch(*first, Action::Replace); } bail!( - "{} unsaved buffer(s) remaining: {:?}", + "{} unsaved buffer{} remaining: {:?}", modified_names.len(), - modified_names + if modified_names.len() == 1 { "" } else { "s" }, + modified_names, ); } @@ -658,9 +659,10 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> editor.switch(*first, Action::Replace); } bail!( - "{} unsaved buffer(s) remaining: {:?}", + "{} unsaved buffer{} remaining: {:?}", modified_names.len(), - modified_names + if modified_names.len() == 1 { "" } else { "s" }, + modified_names, ); } Ok(()) From 0e46f56f30aab12d47eb72c72de693f79f9b3794 Mon Sep 17 00:00:00 2001 From: Michael Jones Date: Wed, 26 Jun 2024 23:04:17 +0200 Subject: [PATCH 062/200] Add new color theme 'iroaseta' (#10381) * Add new color theme 'iroaseta' * Update runtime/themes/iroaseta.toml Co-authored-by: postsolar <120750161+postsolar@users.noreply.github.com> * Update iroaseta.toml Add virtual jump label theme setting * Update runtime/themes/iroaseta.toml Co-authored-by: Michael Davis * Update iroaseta.toml update storage. keyword.storage. according to suggestion, and update color. * Update iroaseta.toml remove unused palette * Update iroaseta.toml add missing setting for bufferline * Update iroaseta.toml update diagnostic fg color * Update iroaseta.toml I made the config more comprehensive and took all available themes settings from the manual. Some are commented out though. * Update iroaseta.toml add missing colors * Update iroaseta.toml Made some final adjustments to the color theme to improve visibility and reduce eye strain. * Update runtime/themes/iroaseta.toml Co-authored-by: Michael Davis * Update runtime/themes/iroaseta.toml Co-authored-by: Michael Davis * Update iroaseta.toml remove redundant settings * Update iroaseta.toml update color name --------- Co-authored-by: postsolar <120750161+postsolar@users.noreply.github.com> Co-authored-by: Michael Davis --- runtime/themes/iroaseta.toml | 162 +++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 runtime/themes/iroaseta.toml diff --git a/runtime/themes/iroaseta.toml b/runtime/themes/iroaseta.toml new file mode 100644 index 000000000..a26293d62 --- /dev/null +++ b/runtime/themes/iroaseta.toml @@ -0,0 +1,162 @@ +# Theme: Iroaseta +# Author: YardQuit + +# SYNTAX HIGHLIGHTING +"attribute" = { fg = "yellow" } +"type" = { fg = "white" } +"type.builtin" = { fg = "white" } +"type.parameter" = { fg = "white" } +"type.enum" = { fg = "white" } +"type.enum.variant" = { fg = "white" } +"constructor" = { fg = "orange" } +"constant" = { fg = "blue" } +"constant.character.escape" = { fg = "yellow" } +"string" = { fg = "blue" } +"string.regexp" = { fg = "yellow" } +"comment" = { fg = "gray" } +"variable" = { fg = "orange" } +"variable.parameter" = { fg = "yellow" } +"variable.other" = { fg = "green" } +"variable.other.member" = { fg = "green" } +"label" = { fg = "blue" } +"punctuation" = { fg = "white" } +"punctuation.bracket" = { fg = "orange" } +"punctuation.special" = { fg = "yellow" } +"keyword" = { fg = "red" } +"keyword.operator" = { fg = "blue" } +"keyword.directive" = { fg = "white" } +"keyword.function" = { fg = "red" } +"keyword.storage" = { fg = "red" } +"keyword.storage.modifier" = { fg = "green" } +"operator" = { fg = "white" } +"function" = { fg = "purple" } +"function.method" = { fg = "green" } +"function.macro" = { fg = "green" } +"function.special" = { fg = "yellow" } +"tag" = { fg = "green" } +"namespace" = { fg = "white" } +"diff" = { fg = "white" } +"diff.minus" = { fg = "red" } +"diff.delta" = { fg = "brown" } + +# MARKUP, SYNAX HIGHLIGHTING AND INTERFACE HYBRID +"markup.heading" = { fg = "blaze_orange" } +"markup.heading.1" = { fg = "crystal_blue", modifiers = ["bold"] } +"markup.heading.2" = { fg = "sky_blue", modifiers = ["bold"] } +"markup.heading.3" = { fg = "dreamy_blue", modifiers = ["bold"] } +"markup.heading.4" = { fg = "crystal_blue" } +"markup.heading.5" = { fg = "sky_blue" } +"markup.heading.6" = { fg = "dreamy_blue" } +"markup.list" = { fg = "blaze_orange" } +"markup.bold" = { fg = "crystal_blue", modifiers = ["bold"] } +"markup.italic" = { fg = "crystal_blue", modifiers = ["italic"] } +"markup.strikethrough" = { fg = "crystal_blue", modifiers = ["crossed_out"] } +"markup.link" = { fg = "crystal_blue", underline = { color = "light_purple", style = "line" } } +"markup.link.url" = { fg = "slate_purple", underline = { color = "slate_purple", style = "line" } } +"markup.link.label" = { fg = "crystal_blue" } +"markup.link.text" = { fg = "crystal_blue", modifiers = ["bold"] } +"markup.quote" = { fg = "misty_white", modifiers = ["italic"] } +"markup.raw" = { fg = "misty_white" } +"markup.raw.block" = { fg = "white" } + +# USER INTERFACE +"ui.background" = { bg = "amber_shadow"} # workspace background +"ui.background.separator" = { fg = "misty_white" } # picker separator below input line (space + j) +"ui.gutter" = { bg = "amber_shadow" } # gutter +"ui.gutter.selected" = { bg = "pitch_black" } # gutter for the line the cursor is on +"ui.linenr" = { fg = "slate_gray" } # line numbers +"ui.linenr.selected" = { fg = "blaze_orange", modifiers = ["bold"] } # line number for the line the cursor is on +"ui.statusline" = { fg = "misty_white", bg = "pitch_black" } # statusline, fucused +"ui.statusline.inactive" = { fg = "slate_gray", bg = "dark_steel" } # statusline, unfocused +"ui.statusline.normal" = { fg = "amber_shadow", bg = "leafy_green", modifiers = ["bold"] } # statusline normal mode (if editor.color-modes is enabled) +"ui.statusline.insert" = { fg = "amber_shadow", bg = "blaze_orange", modifiers = ["bold"] } # statusline insert mode (if editor.color-modes is enabled) +"ui.statusline.select" = { fg = "amber_shadow", bg = "sky_blue", modifiers = ["bold"] } # statusline select mode (if editor.color-modes is enabled) +"ui.statusline.separator" = { fg = "misty_white" } # separator character is statusline +"ui.bufferline" = { fg = "slate_gray", modifiers = ["bold"] } # bufferline inactive tab +"ui.bufferline.active" = { fg = "misty_white", bg = "rustic_red" } # bufferline active tab +"ui.bufferline.background" = { bg = "pitch_black" } # bufferline background +"ui.virtual.ruler" = { bg = "rustic_red" } # ruler columns +"ui.virtual.whitespace" = { fg = "brown" } # whitespace characters +"ui.virtual.indent-guide" = { fg = "brown" } # vertical indent width guides +"ui.virtual.inlay-hint" = { fg = "slate_gray" } # inlay hints of all kinds +"ui.virtual.inlay-hint.parameter" = { fg = "slate_gray" } # inlay hints of kind parameter (lsps are not required to set a kind) +"ui.virtual.inlay-hint.type" = { fg = "slate_gray" } # inlay hints of kind type (lsps are not required to set a kind) +"ui.virtual.wrap" = { fg = "slate_gray" } # soft-wrap indicator +"ui.virtual.jump-label" = { modifiers = ["reversed"] } # virtual jump labels (g + w) +"ui.selection" = { bg = "deep_purple" } # slave selections in the editing area +"ui.selection.primary" = { bg = "light_purple" } # primary selection in the editing area +"ui.cursor" = { modifiers = ["reversed"] } # only if "ui.cursor.primary.normal" isn't set +"ui.cursor.normal" = { modifiers = ["reversed"] } # slave cursor block in normal mode +"ui.cursor.insert" = { bg = "rustic_red" } # slave cursor block in insert mode +"ui.cursor.select" = { bg = "deep_purple" } # slave cursor block in select mode +"ui.cursor.match" = { fg = "amber_shadow", bg = "blaze_orange", modifiers = ["bold"] } # matching bracket etc +"ui.cursor.primary" = { modifiers = ["reversed"] } # cursor with primary selection (has no effect due to "ui.cursor.primary.normal" is set) +"ui.cursor.primary.normal" = { modifiers = ["reversed"] } # cursor block in normal mode +"ui.cursor.primary.insert" = { fg = "amber_shadow", bg = "ruby_glow" } # cursor block in insert mode +"ui.cursor.primary.select" = { fg = "misty_white", bg = "deep_purple" } # cursor block in select mode (not the selected color) +"ui.cursorline.primary" = { bg = "dark_steel" } # line of the primary cursor +"ui.cursorline.secondary" = { bg = "midnight_black"} # lines of secondary cursors +"ui.cursorcolumn.primary" = { bg = "dark_steel" } # column of the primary cursor +"ui.cursorcolumn.secondary" = { bg = "midnight_black" } # columns of secondary cursors + +# USER INTERFACE - MENUS AND POPUP +"ui.popup" = { fg = "misty_white", bg = "midnight_black" } # documentation popups (space + k) +"ui.popup.info" = { fg = "misty_white", bg = "midnight_black" } # prompt for multiple key options, menu border (space, g, z, m, etc) +"ui.window" = { fg = "pitch_black" } # borderlines separating splits +"ui.help" = { fg = "misty_white", bg = "midnight_black" } # description box for commands +"ui.text" = { fg = "white" } # default text style, command prompts, popup text, etc +"ui.text.focus" = { fg = "leafy_green" } # the currently selected line in the picker (space j, space f, space s, etc) +"ui.text.inactive" = { fg = "slate_gray" } # same as ui.text but when the text is inactive e.g. suggestions +"ui.text.info" = { fg = "misty_white", bg = "midnight_black" } # the key: command in ui.popup.info boxes (space, g, z, m, etc) +"ui.menu" = { fg = "ethereal_gray", bg = "pitch_black" } # code and command completion menus ":" +"ui.menu.selected" = { fg = "misty_white", bg = "dark_steel" } # selected autocomplete item +"ui.menu.scroll" = { fg = "slate_gray", bg = "midnight_black" } # scrollbar +"ui.highlight" = { underline = { color = "blaze_orange", style = "line" } } # highlighted lines in the picker preview + +# User Interface - Diagnostics +"warning" = { fg = "lemon_zest" } # diagnostics warning (gutter) +"error" = { fg = "ruby_glow" } # diagnostics error (gutter) +"info" = { fg = "sky_blue" } # diagnostics info (gutter) +"hint" = { fg = "walnut_brown" } # diagnostics hint (gutter) +"diagnostic" = { modifiers = ["reversed"] } # diagnostics fallback style (editing area) +"diagnostic.hint" = { fg = "amber_shadow", bg = "walnut_brown" } # diagnostics hint (editing area) +"diagnostic.info" = { fg = "misty_white", bg = "twilight_slate" } # diagnostics info (editing area) +"diagnostic.warning" = { fg = "misty_white", bg = "rustic_amber" } # diagnostics warning (editing area) +"diagnostic.error" = { fg = "misty_white", bg = "rustic_red" } # diagnostics error (editing area) + +# COLOR NAMES +[palette] +# PALETTE USER INTERFACE +amber_shadow = "#0d1117" +midnight_black = "#010409" +dark_steel = "#161b22" +pitch_black = "#000000" +misty_white = "#f2f0eb" +slate_gray = "#838a97" +ethereal_gray = "#91a3b0" +blaze_orange = "#ff9000" +lemon_zest = "#ffba00" +leafy_green = "#81be83" +dreamy_blue = "#6eb0ff" +crystal_blue = "#99c7ff" +twilight_slate = "#263c57" +sky_blue = "#45b1e8" +rustic_red = "#540b0c" +ruby_glow = "#fa7970" +walnut_brown = "#987654" +rustic_amber = "#9d5800" +slate_purple = "#d2a8ff" +light_purple = "#7533bd" +deep_purple = "#4c1785" + +# PALETTE SYNTAX HIGHLIGHTING +black = "#0d1117" +red = "#fa7970" +green = "#81be83" +yellow = "#ffba00" +orange = "#ff9000" +blue = "#45b1e8" +purple = "#d2a8ff" +brown = "#987654" +gray = "#838a97" +white = "#dadada" From b4811f7d2e117532f7e4b60e8cf1088d283c37f5 Mon Sep 17 00:00:00 2001 From: Chirikumbrah <78883260+Chirikumbrah@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:05:01 +0300 Subject: [PATCH 063/200] Large Gruvbox refactoring (#10773) * gruvbox refactoring * removed unnecessary lines * set purple1 for operators * changed diagnostics colors * removed some unnecessary lines * set diff.delta color to yellow * removed some tag colors --- runtime/themes/gruvbox.toml | 179 +++++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 72 deletions(-) diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index 67ca066f7..220843b50 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -1,89 +1,125 @@ # Author : Jakub Bartodziej # The theme uses the gruvbox dark palette with standard contrast: github.com/morhetz/gruvbox -"attribute" = "aqua1" -"keyword" = { fg = "red1" } -"keyword.directive" = "red0" -"namespace" = "aqua1" -"punctuation" = "orange1" -"punctuation.delimiter" = "orange1" -"operator" = "purple1" -"special" = "purple0" -"variable.other.member" = "blue1" -"variable" = "fg1" -"variable.builtin" = "orange1" -"variable.parameter" = "fg2" -"type" = "yellow1" -"type.builtin" = "yellow1" -"constructor" = { fg = "purple1", modifiers = ["bold"] } -"function" = { fg = "green1", modifiers = ["bold"] } -"function.macro" = "aqua1" -"function.builtin" = "yellow1" -"tag" = "red1" -"comment" = { fg = "gray1", modifiers = ["italic"] } +"annotation" = { fg = "fg1" } + +"attribute" = { fg = "aqua1", modifiers = ["italic"] } + +"comment" = { fg = "gray", modifiers = ["italic"] } + "constant" = { fg = "purple1" } -"constant.builtin" = { fg = "purple1", modifiers = ["bold"] } -"string" = "green1" -"constant.numeric" = "purple1" -"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] } -"label" = "aqua1" -"module" = "aqua1" +"constant.character" = { fg = "aqua1" } +"constant.character.escape" = { fg = "orange1" } +"constant.macro" = { fg = "aqua1" } +"constructor" = { fg = "purple1" } -"diff.plus" = "green1" -"diff.delta" = "orange1" -"diff.minus" = "red1" +"definition" = { underline = { color = "aqua1" } } -"warning" = "yellow1" -"error" = "red1" -"info" = "aqua1" -"hint" = "blue1" +"diagnostic" = { underline = { color = "orange1", style = "curl" } } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"diagnostic.error" = { underline = { color = "red1", style = "curl" } } +"diagnostic.hint" = { underline = { color = "blue1", style = "curl" } } +"diagnostic.info" = { underline = { color = "aqua1", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow1", style = "curl" } } +# "diagnostic.unnecessary" = { modifiers = ["dim"] } # do not remove this for future resolving + +"error" = { fg = "red1" } +"hint" = { fg = "blue1" } +"info" = { fg = "aqua1" } +"warning" = { fg = "yellow1" } + +"diff.delta" = { fg = "yellow1" } +"diff.minus" = { fg = "red1" } +"diff.plus" = { fg = "green1" } + +"function" = { fg = "green1" } +"function.builtin" = { fg = "yellow1" } +"function.macro" = { fg = "blue1" } + +"keyword" = { fg = "red1" } +"keyword.control.import" = { fg = "aqua1" } + +"label" = { fg = "red1" } + +"markup.bold" = { modifiers = ["bold"] } +"markup.heading" = "aqua1" +"markup.italic" = { modifiers = ["italic"] } +"markup.link.text" = "red1" +"markup.link.url" = { fg = "green1", modifiers = ["underlined"] } +"markup.raw" = "red1" +"markup.strikethrough" = { modifiers = ["crossed_out"] } + +"module" = { fg = "aqua1" } + +"namespace" = { fg = "fg1" } + +"operator" = { fg = "purple1" } + +"punctuation" = { fg = "orange1" } + +"special" = { fg = "purple0" } + +"string" = { fg = "green1" } +"string.regexp" = { fg = "orange1" } +"string.special" = { fg = "orange1" } +"string.symbol" = { fg = "yellow1" } + +"tag" = { fg = "aqua1" } + +"type" = { fg = "yellow1" } +"type.enum.variant" = { modifiers = ["italic"] } "ui.background" = { bg = "bg0" } -"ui.linenr" = { fg = "bg4" } -"ui.linenr.selected" = { fg = "yellow1" } -"ui.cursorline" = { bg = "bg1" } -"ui.statusline" = { fg = "fg1", bg = "bg2" } -"ui.statusline.normal" = { fg = "fg1", bg = "bg2" } -"ui.statusline.insert" = { fg = "fg1", bg = "blue0" } -"ui.statusline.select" = { fg = "fg1", bg = "orange0" } -"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" } "ui.bufferline" = { fg = "fg1", bg = "bg1" } "ui.bufferline.active" = { fg = "bg0", bg = "yellow0" } "ui.bufferline.background" = { bg = "bg2" } -"ui.popup" = { bg = "bg1" } -"ui.window" = { bg = "bg1" } + +"ui.cursor" = { fg = "bg1", bg = "bg2" } +"ui.cursor.insert" = { fg = "bg1", bg = "blue0" } +"ui.cursor.normal" = { fg = "bg1", bg = "gray" } +"ui.cursor.select" = { fg = "bg1", bg = "orange0" } +"ui.cursor.match" = { fg = "fg3", bg = "bg3" } + +"ui.cursor.primary" = { bg = "fg3", fg = "bg1" } +"ui.cursor.primary.insert" = { fg = "bg1", bg = "blue1" } +"ui.cursor.primary.normal" = { fg = "bg1", bg = "fg3" } +"ui.cursor.primary.select" = { fg = "bg1", bg = "orange1" } + +"ui.cursorline" = { bg = "bg0_s" } +"ui.cursorline.primary" = { bg = "bg1" } + "ui.help" = { bg = "bg1", fg = "fg1" } -"ui.text" = { fg = "fg1" } -"ui.text.focus" = { fg = "fg1" } -"ui.selection" = { bg = "bg2" } -"ui.selection.primary" = { bg = "bg3" } -"ui.cursor.primary" = { bg = "fg4", fg = "bg1" } -"ui.cursor.match" = { bg = "bg3" } +"ui.linenr" = { fg = "bg3" } +"ui.linenr.selected" = { fg = "yellow1" } "ui.menu" = { fg = "fg1", bg = "bg2" } "ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] } -"ui.virtual.whitespace" = "bg2" -"ui.virtual.ruler" = { bg = "bg1" } -"ui.virtual.inlay-hint" = { fg = "gray1" } -"ui.virtual.wrap" = { fg = "bg2" } +"ui.popup" = { bg = "bg1" } +"ui.selection" = { bg = "bg2" } +"ui.selection.primary" = { bg = "bg3" } + +"ui.statusline" = { fg = "fg1", bg = "bg2" } +"ui.statusline.inactive" = { fg = "fg4", bg = "bg2" } +"ui.statusline.insert" = { fg = "bg1", bg = "blue1", modifiers = ["bold"] } +"ui.statusline.normal" = { fg = "bg1", bg = "fg3", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "bg1", bg = "orange1", modifiers = ["bold"] } + +"ui.text" = { fg = "fg1" } +"ui.virtual.inlay-hint" = { fg = "gray" } "ui.virtual.jump-label" = { fg = "purple0", modifiers = ["bold"] } +"ui.virtual.ruler" = { bg = "bg1" } +"ui.virtual.whitespace" = "bg2" +"ui.virtual.wrap" = { fg = "bg2" } +"ui.window" = { bg = "bg1" } -"diagnostic.warning" = { underline = { color = "yellow1", style = "curl" } } -"diagnostic.error" = { underline = { color = "red1", style = "curl" } } -"diagnostic.info" = { underline = { color = "aqua1", style = "curl" } } -"diagnostic.hint" = { underline = { color = "blue1", style = "curl" } } -"diagnostic.unnecessary" = { modifiers = ["dim"] } -"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"variable" = { fg = "fg1" } +"variable.builtin" = { fg = "orange1", modifiers = ["italic"] } +"variable.other.member" = { fg = "blue1" } +"variable.parameter" = { fg = "blue1", modifiers = ["italic"] } -"markup.heading" = "aqua1" -"markup.bold" = { modifiers = ["bold"] } -"markup.italic" = { modifiers = ["italic"] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "green1", modifiers = ["underlined"] } -"markup.link.text" = "red1" -"markup.raw" = "red1" [palette] -bg0 = "#282828" # main background +bg0 = "#282828" # main background +bg0_s = "#32302f" bg1 = "#3c3836" bg2 = "#504945" bg3 = "#665c54" @@ -93,13 +129,12 @@ fg0 = "#fbf1c7" fg1 = "#ebdbb2" # main foreground fg2 = "#d5c4a1" fg3 = "#bdae93" -fg4 = "#a89984" # gray0 +fg4 = "#a89984" -gray0 = "#a89984" -gray1 = "#928374" +gray = "#928374" -red0 = "#cc241d" # neutral -red1 = "#fb4934" # bright +red0 = "#cc241d" # neutral +red1 = "#fb4934" # bright green0 = "#98971a" green1 = "#b8bb26" yellow0 = "#d79921" From dca952c03ac1d3beab1e4b203648e3a31181dbf1 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 28 Jun 2024 22:08:21 -0400 Subject: [PATCH 064/200] Delay auto-save until exiting insert mode (#11047) Saving while in insert mode causes issues with the modification indicator and this is very easy to reproduce with the current state of the auto-save hook. We can tweak the hook slightly to await the mode switch out of insert mode to perform the save. The debounce is preserved: if you save and then immediately exit insert mode the debounce will be respected. If the debounce lapses while you are in insert mode, the save occurs as you switch out of insert mode immediately. --- helix-term/src/handlers/auto_save.rs | 76 ++++++++++++++++++++++++---- helix-view/src/handlers.rs | 8 ++- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/helix-term/src/handlers/auto_save.rs b/helix-term/src/handlers/auto_save.rs index d3f7f6fc1..4e154df80 100644 --- a/helix-term/src/handlers/auto_save.rs +++ b/helix-term/src/handlers/auto_save.rs @@ -1,39 +1,82 @@ -use std::time::Duration; +use std::{ + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, + time::Duration, +}; use anyhow::Ok; use arc_swap::access::Access; use helix_event::{register_hook, send_blocking}; -use helix_view::{events::DocumentDidChange, handlers::Handlers, Editor}; +use helix_view::{ + document::Mode, + events::DocumentDidChange, + handlers::{AutoSaveEvent, Handlers}, + Editor, +}; use tokio::time::Instant; use crate::{ commands, compositor, + events::OnModeSwitch, job::{self, Jobs}, }; #[derive(Debug)] -pub(super) struct AutoSaveHandler; +pub(super) struct AutoSaveHandler { + save_pending: Arc, +} impl AutoSaveHandler { pub fn new() -> AutoSaveHandler { - AutoSaveHandler + AutoSaveHandler { + save_pending: Default::default(), + } } } impl helix_event::AsyncHook for AutoSaveHandler { - type Event = u64; + type Event = AutoSaveEvent; fn handle_event( &mut self, - timeout: Self::Event, - _: Option, + event: Self::Event, + existing_debounce: Option, ) -> Option { - Some(Instant::now() + Duration::from_millis(timeout)) + match event { + Self::Event::DocumentChanged { save_after } => { + Some(Instant::now() + Duration::from_millis(save_after)) + } + Self::Event::LeftInsertMode => { + if existing_debounce.is_some() { + // If the change happened more recently than the debounce, let the + // debounce run down before saving. + existing_debounce + } else { + // Otherwise if there is a save pending, save immediately. + if self.save_pending.load(atomic::Ordering::Relaxed) { + self.finish_debounce(); + } + None + } + } + } } fn finish_debounce(&mut self) { - job::dispatch_blocking(move |editor, _| request_auto_save(editor)) + let save_pending = self.save_pending.clone(); + job::dispatch_blocking(move |editor, _| { + if editor.mode() == Mode::Insert { + // Avoid saving while in insert mode since this mixes up + // the modification indicator and prevents future saves. + save_pending.store(true, atomic::Ordering::Relaxed); + } else { + request_auto_save(editor); + save_pending.store(false, atomic::Ordering::Relaxed); + } + }) } } @@ -54,7 +97,20 @@ pub(super) fn register_hooks(handlers: &Handlers) { register_hook!(move |event: &mut DocumentDidChange<'_>| { let config = event.doc.config.load(); if config.auto_save.after_delay.enable { - send_blocking(&tx, config.auto_save.after_delay.timeout); + send_blocking( + &tx, + AutoSaveEvent::DocumentChanged { + save_after: config.auto_save.after_delay.timeout, + }, + ); + } + Ok(()) + }); + + let tx = handlers.auto_save.clone(); + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + if event.old_mode == Mode::Insert { + send_blocking(&tx, AutoSaveEvent::LeftInsertMode) } Ok(()) }); diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs index 352abb881..e2848f264 100644 --- a/helix-view/src/handlers.rs +++ b/helix-view/src/handlers.rs @@ -7,11 +7,17 @@ pub mod dap; pub mod lsp; +#[derive(Debug)] +pub enum AutoSaveEvent { + DocumentChanged { save_after: u64 }, + LeftInsertMode, +} + pub struct Handlers { // only public because most of the actual implementation is in helix-term right now :/ pub completions: Sender, pub signature_hints: Sender, - pub auto_save: Sender, + pub auto_save: Sender, } impl Handlers { From c6dbb9c2708a3a224d1ff29758ea54003445ac72 Mon Sep 17 00:00:00 2001 From: Chris44442 <101070356+Chris44442@users.noreply.github.com> Date: Sat, 29 Jun 2024 08:30:38 +0200 Subject: [PATCH 065/200] VHDL highlights.scm improvement (#10845) --- runtime/queries/vhdl/highlights.scm | 446 ++++++++-------------------- 1 file changed, 121 insertions(+), 325 deletions(-) diff --git a/runtime/queries/vhdl/highlights.scm b/runtime/queries/vhdl/highlights.scm index 59cef41cd..1b9742fc8 100644 --- a/runtime/queries/vhdl/highlights.scm +++ b/runtime/queries/vhdl/highlights.scm @@ -1,338 +1,134 @@ -(comment) @comment - -; Keywords [ - ; vhdl 08 - "abs" - "access" - "after" - "alias" - "all" - "and" - "architecture" - "array" - "assert" - "attribute" - "begin" - "block" - "body" - "buffer" - "bus" - "case" - "component" - "configuration" - "constant" - "disconnect" - "downto" - "else" - "elsif" - "end" - "entity" - "exit" - "file" - "for" - "function" - "generic" - "group" - "guarded" - "if" - "impure" - "in" - "inertial" - "inout" - "is" - "label" - "library" - "linkage" - "literal" - "loop" - "map" - "mod" - "nand" - "new" - "next" - "nor" - "not" - "null" - "of" - "on" - "open" - "or" - "others" - "out" - "package" - "port" - "postponed" - "procedure" - "process" - "protected" - "pure" - "range" - "record" - "register" - "reject" - "rem" - "report" - "return" - "rol" - "ror" - "select" - "severity" - "shared" - "signal" - "sla" - "sll" - "sra" - "srl" - "subtype" - "then" - "to" - "transport" - "type" - "unaffected" - "units" - "until" - "use" - "variable" - "wait" - "when" - "while" - "with" - "xnor" - "xor" - ; vhdl 08 - "context" - "force" - "property" - "release" - "sequence" + "alias" "package" "file" "entity" "architecture" "type" "subtype" + "attribute" "to" "downto" "signal" "variable" "record" "array" + "others" "process" "component" "shared" "constant" "port" "generic" + "generate" "range" "map" "in" "inout" "of" "out" "configuration" + "pure" "impure" "is" "begin" "end" "context" "wait" "until" "after" + "report" "open" "exit" "assert" "next" "null" "force" "property" + "release" "sequence" "transport" "unaffected" "select" "severity" + "register" "reject" "postponed" "on" "new" "literal" "linkage" + "inertial" "guarded" "group" "disconnect" "bus" "buffer" "body" + "all" "block" "access" ] @keyword [ - ; vhdl 02 - "boolean" - "bit" - "bit_vector" - ;"character" - ;"severity_level" - ;"integer" - ;"real" - ;"time" - ;"natural" - ;"positive" - "string" - ;"line" - ;"text" - ;"side" - ;"unsigned" - ;"signed" - ;"delay_length" - ;"file_open_kind" - ;"file_open_status" - ;"std_logic" - ;"std_logic_vector" - ;"std_ulogic" - ;"std_ulogic_vector" - ; vhdl 08 - ;"boolean_vector" - ;"integer_vector" - ;"real_vector" - ;"time_vector" - ; math types - ;"complex" - ;"complex_polar" - ;"positive_real" - ;"principal_value" -] @type.builtin + "function" "procedure" +] @keyword.function [ - ; vhdl 02 - "base" - "left" - "right" - "high" - "low" - "pos" - "val" - "succ" - "pred" - "leftof" - "rightof" - "range" - "reverse_range" - "length" - "delayed" - "stable" - "quiet" - "transaction" - "event" - "active" - "last_event" - "last_active" - "last_value" - "driving" - "driving_value" - "ascending" - "value" - "image" - "simple_name" - "instance_name" - "path_name" - ;"foreign" - ; vhdl 08 - "instance_name" - "path_name" -] @attribute - -;[ - ; vhdl 02 - ;"now" - ;"resolved" - ;"rising_edge" - ;"falling_edge" - ;"read" - ;"readline" - ;"hread" - ;"oread" - ;"write" - ;"writeline" - ;"hwrite" - ;"owrite" - ;"endfile" - ;"resize" - ;"is_X" - ;"std_match" - ;"shift_left" - ;"shift_right" - ;"rotate_left" - ;"rotate_right" - ;"to_unsigned" - ;"to_signed" - ;"to_integer" - ;"to_stdLogicVector" - ;"to_stdULogic" - ;"to_stdULogicVector" - ;"to_bit" - ;"to_bitVector" - ;"to_X01" - ;"to_X01Z" - ;"to_UX01" - ;"to_01" - ;"conv_unsigned" - ;"conv_signed" - ;"conv_integer" - ;"conv_std_logic_vector" - ;"shl" - ;"shr" - ;"ext" - ;"sxt" - ;"deallocate" - ; vhdl 08 - ;"finish" - ;"flush" - ;"justify" - ;"maximum" - ;"minimum" - ;"resolution_limit" - ;"stop" - ;"swrite" - ;"tee" - ;"to_binarystring" - ;"to_bstring" - ;"to_hexstring" - ;"to_hstring" - ;"to_octalstring" - ;"to_ostring" - ;"to_string" - ; vhdl math - ;"arccos" - ;"arccosh" - ;"arcsin" - ;"arcsinh" - ;"arctan" - ;"arctanh" - ;"arg" - ;"cbrt" - ;"ceil" - ;"cmplx" - ;"complex_to_polar" - ;"conj" - ;"cos" - ;"cosh" - ;"exp" - ;"floor" - ;"get_principal_value" - ;"log" - ;"log10" - ;"log2" - ;"polar_to_complex" - ;"realmax" - ;"realmin" - ;"round" - ;"sign" - ;"sin" - ;"sinh" - ;"sqrt" - ;"tan" - ;"tanh" - ;"trunc" - ;"uniform" -;] @function.builtin - -; Operators -[ - "+" - "-" - "*" - "/" - "**" - "abs" - "not" - "mod" - "rem" - "&" - "sll" - "srl" - "sla" - "sra" - "rol" - "ror" - "=" - "/=" - "?=" - "?/=" - "?<" - "?<=" - "?>" - "?>=" - "<" - "<=" - ">" - ">=" - "and" - "or" - "nand" - "nor" - "xor" - "xnor" - ":=" - "<=" - "??" -] @operator + "return" +] @keyword.control.return [ - ";" - "," + "for" "loop" "while" +] @keyword.control.repeat + +[ + "if" "elsif" "else" "case" "then" "when" +] @keyword.control.conditional + +[ + "library" "use" +] @keyword.control.import + +(comment) @comment + +(type_mark) @type + +[ + "(" ")" "[" "]" +] @punctuation.bracket + +[ + "." ";" "," ":" ] @punctuation.delimiter [ - "(" - ")" - "'" -] @punctuation.bracket + "=>" "<=" "+" ":=" "=" "/=" "<" ">" "-" "*" + "**" "/" "?>" "?<" "?<=" "?>=" "?=" "?/=" +; "?/" errors, maybe due to escape character + (attribute_name "'") + (index_subtype_definition (any)) +] @operator -(full_type_declaration "type" name: (identifier) @type) -(signal_declaration "signal" (identifier_list) @variable) -(variable_declaration "variable" (identifier_list) @variable) -(constant_declaration "constant" (identifier_list) @variable) +[ + "not" "xor" "xnor" "and" "nand" "or" "nor" "mod" "rem" + (attribute_name "'") + (index_subtype_definition (any)) +] @keyword.operator + +[ + (real_decimal) + (integer_decimal) +] @constant.numeric + +(character_literal) @constant.character + +[ + (string_literal) + (bit_string_literal) +] @string + +(physical_literal + unit: (simple_name) @attribute) + +(attribute_name + prefix: (_) @variable + designator: (_) @attribute) + +((simple_name) @variable.builtin (#any-of? @variable.builtin + "true" "false" "now")) + +(severity_expression) @constant.builtin + +(procedure_call_statement + procedure: (simple_name) @function) + +(ambiguous_name + prefix: (simple_name) @function.builtin (#any-of? @function.builtin + "rising_edge" "falling_edge" "find_rightmost" "find_leftmost" + "maximum" "minimum" "shift_left" "shift_right" "rotate_left" + "rotate_right" "sll" "srl" "rol" "ror" "sla" "sra" "resize" + "mod" "rem" "abs" "saturate" + "to_sfixed" "to_ufixed" "to_signed" "to_unsigned" "to_real" + "to_integer" "sfixed_low" "ufixed_low" "sfixed_high" + "ufixed_high" "to_slv" "to_stdulogicvector" "to_sulv" + "to_float" "std_logic" "std_logic_vector" "integer" "signed" + "unsigned" "real" "std_ulogic_vector" + "std_ulogic" "x01" "x01z" "ux01" "ux01Z" +;math_real + "sign" "ceil" "floor" "round" "fmax" "fmin" "uniform" "srand" + "rand" "get_rand_max" "sqrt" "cbrt" "exp" "log" "log2" "log10" + "sin" "cos" "tan" "asin" "acos" "atan" "atan2" "sinh" "cosh" + "tanh" "asinh" "acosh" "atanh" "realmax" "realmin" "trunc" + "conj" "arg" "polar_to_complex" "complex_to_polar" + "get_principal_value" "cmplx" +;std_textio + "read" "write" "hread" "hwrite" "to_hstring" "to_string" + "from_hstring" "from_string" + "justify" "readline" "sread" "string_read" " bread" + "binary_read" "oread" "octal_read" "hex_read" + "writeline" "swrite" "string_write" "bwrite" + "binary_write" "owrite" "octal_write" "hex_write" + "synthesis_return" +;std_logic_1164 + "resolved" "logic_type_encoding" "is_signed" "to_bit" + "to_bitvector" "to_stdulogic" "to_stdlogicvector" + "to_bit_vector" "to_bv" "to_std_logic_vector" + "to_std_ulogic_vector" "to_01" "to_x01" "to_x01z" "to_ux01" + "is_x" "to_bstring" "to_binary_string" "to_ostring" + "to_octal_string" "to_hex_string" +;float_pkg + "add" "subtract" "multiply" "divide" "remainder" "modulo" + "reciprocal" "dividebyp2" "mac" "eq" "ne" "lt" "gt" "le" "ge" + "to_float32" "to_float64" "to_float128" "realtobits" "bitstoreal" + "break_number" "normalize" "copysign" "scalb" "logb" "nextafter" + "unordered" "finite" "isnan" "zerofp" "nanfp" "qnanfp" + "pos_inffp" "neg_inffp" "neg_zerofp" "from_bstring" + "from_binary_string" "from_ostring" "from_octal_string" + "from_hex_string" +;fixed_pkg + "add_carry" "to_ufix" "to_sfix" "ufix_high" + "ufix_low" "sfix_high" "sfix_low" +)) From 21fd654cc66ce8ba59769b133642f2ca05320847 Mon Sep 17 00:00:00 2001 From: Mark Murphy Date: Sun, 30 Jun 2024 03:07:57 -0400 Subject: [PATCH 066/200] Fix homebrew formula link (#11058) Co-authored-by: Mark Murphy --- docs/releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index e39cfd910..2be14553a 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -31,7 +31,7 @@ ## Checklist * Post to reddit * [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/) -[homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/helix.rb +[homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/h/helix.rb ## Changelog Curation From 9c4c66d41758013fbfbd11f5096166d90c32ec12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:37:33 +0900 Subject: [PATCH 067/200] build(deps): bump the rust-dependencies group with 3 updates (#11072) Bumps the rust-dependencies group with 3 updates: [log](https://github.com/rust-lang/log), [serde_json](https://github.com/serde-rs/json) and [cc](https://github.com/rust-lang/cc-rs). Updates `log` from 0.4.21 to 0.4.22 - [Release notes](https://github.com/rust-lang/log/releases) - [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/log/compare/0.4.21...0.4.22) Updates `serde_json` from 1.0.117 to 1.0.120 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.117...v1.0.120) Updates `cc` from 1.0.100 to 1.0.104 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.0.100...cc-v1.0.104) --- updated-dependencies: - dependency-name: log dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 577298d8b..60d9594e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.100" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" [[package]] name = "cfg-if" @@ -1711,9 +1711,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lsp-types" @@ -2118,9 +2118,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", From 0c6ffe192b718709267b6af082d826e90c4a9099 Mon Sep 17 00:00:00 2001 From: "Lucas @ StarkWare" <70894690+LucasLvy@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:37:49 +0200 Subject: [PATCH 068/200] chore: update cairo tree sitter + queries (#11067) --- languages.toml | 2 +- runtime/queries/cairo/highlights.scm | 6 ++++++ runtime/queries/cairo/indents.scm | 8 +++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index 7ffc998b8..a308e4786 100644 --- a/languages.toml +++ b/languages.toml @@ -2081,7 +2081,7 @@ language-servers = [ "cairo-language-server" ] [[grammar]] name = "cairo" -source = { git = "https://github.com/starkware-libs/tree-sitter-cairo", rev = "0596baab741ffacdc65c761d5d5ffbbeae97f033" } +source = { git = "https://github.com/starkware-libs/tree-sitter-cairo", rev = "e3a0212261c125cb38248458cd856c0ffee2b398" } [[language]] name = "cpon" diff --git a/runtime/queries/cairo/highlights.scm b/runtime/queries/cairo/highlights.scm index d2cabd1c5..16918c141 100644 --- a/runtime/queries/cairo/highlights.scm +++ b/runtime/queries/cairo/highlights.scm @@ -95,6 +95,12 @@ ; ------- ; Keywords ; ------- + +(for_expression + "for" @keyword.control.repeat) + +"in" @keyword.control + [ "match" "if" diff --git a/runtime/queries/cairo/indents.scm b/runtime/queries/cairo/indents.scm index 35c162429..b20317ab0 100644 --- a/runtime/queries/cairo/indents.scm +++ b/runtime/queries/cairo/indents.scm @@ -115,4 +115,10 @@ (#not-same-line? @expr-start @pattern-guard) ) @indent - +(for_expression + "in" @in + . + (_) @indent + (#not-same-line? @in @indent) + (#set! "scope" "all") +) From ed761fbe7c067c7a76151c36c9b90f58af197c3b Mon Sep 17 00:00:00 2001 From: Christopher Smyth <18294397+RossSmyth@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:38:30 -0400 Subject: [PATCH 069/200] Bump time from broken version (0.3.23) (#11065) --- Cargo.lock | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60d9594e0..d225b6650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,6 +351,15 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "dunce" version = "1.0.4" @@ -1795,6 +1804,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.15" @@ -1896,6 +1911,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.76" @@ -2358,13 +2379,16 @@ dependencies = [ [[package]] name = "time" -version = "0.3.23" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", "itoa", "libc", + "num-conv", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -2372,16 +2396,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] From 64f8660d3e4c0f4e8d8d3423257822d25e023b99 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 1 Jul 2024 20:42:51 -0400 Subject: [PATCH 070/200] Tell language servers that Helix can request formatting (#11064) Without providing the formatting capability, the language server might not advertise its ability to format in return, causing the :format command to be broken. --- helix-lsp/src/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 254625a3a..0a822c5b3 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -616,6 +616,9 @@ pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result Date: Tue, 2 Jul 2024 02:56:55 +0200 Subject: [PATCH 071/200] Override far too dark cursorline (#11071) --- runtime/themes/gruvbox_light.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/gruvbox_light.toml b/runtime/themes/gruvbox_light.toml index 59ce99011..ba216020b 100644 --- a/runtime/themes/gruvbox_light.toml +++ b/runtime/themes/gruvbox_light.toml @@ -6,6 +6,7 @@ inherits = "gruvbox" "ui.cursor.primary" = { modifiers = ["reversed"] } "ui.cursor.match" = { bg = "bg2" } +"ui.cursorline" = { bg = "bg1" } [palette] bg0 = "#fbf1c7" # main background From fc97ecc3e3186b9dfe958869178bdb6b8cd7d8df Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 2 Jul 2024 01:36:29 -0700 Subject: [PATCH 072/200] Add hsc filetype to haskell (#11074) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index a308e4786..9c5ce6818 100644 --- a/languages.toml +++ b/languages.toml @@ -1269,7 +1269,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-yaml", rev = "0e36bed1 name = "haskell" scope = "source.haskell" injection-regex = "hs|haskell" -file-types = ["hs", "hs-boot"] +file-types = ["hs", "hs-boot", "hsc"] roots = ["Setup.hs", "stack.yaml", "cabal.project"] comment-token = "--" block-comment-tokens = { start = "{-", end = "-}" } From 06d8fee048ab7964fe2a29d260bd96252e7b102b Mon Sep 17 00:00:00 2001 From: Schmiddiii Date: Sat, 6 Jul 2024 17:34:33 +0200 Subject: [PATCH 073/200] Allow multiple language server with lsp-workspace-command (#10176) This fix allows for multiple language servers at once which support workspace commands. This was previously broken as just the first language server supporting workspace commands was queried when listing allowed worspace commands. The fix is in two parts. Firstly, querying all workspace commands from all language servers available and using them when actually running the command in `lsp_workspace_command`. Secondly, doing the same in `completers::lsp_workspace_command` such that completion still works as expected. The fix has one remaining issue, which I am unsure how to handle in the best way possible, but which I also don't think should happen often: Multiple language servers may register commands with the same name. This will lead to that command being listed in the popup menu and in the completion list multiple times, which can be possibly confusing. One could disambigue them in the popover menu, but I am not sure the same can be done for completion. When running `lsp-workspace-command` with parameters, this behavior is "fixed" by displaying an error in that case. I am unsure if this is the best fix for this issue in that case, but could not find a better one. --- helix-term/src/commands/typed.rs | 95 ++++++++++++++++++++------------ helix-term/src/ui/mod.rs | 14 +++-- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ab12fab10..3a8ed1461 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1370,37 +1370,51 @@ fn lsp_workspace_command( if event != PromptEvent::Validate { return Ok(()); } + + struct LsIdCommand(usize, helix_lsp::lsp::Command); + + impl ui::menu::Item for LsIdCommand { + type Data = (); + + fn format(&self, _data: &Self::Data) -> Row { + self.1.title.as_str().into() + } + } + let doc = doc!(cx.editor); - let Some((language_server_id, options)) = doc + let ls_id_commands = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) - .find_map(|ls| { + .flat_map(|ls| { ls.capabilities() .execute_command_provider - .as_ref() - .map(|options| (ls.id(), options)) - }) - else { - cx.editor - .set_status("No active language servers for this document support workspace commands"); - return Ok(()); - }; + .iter() + .flat_map(|options| options.commands.iter()) + .map(|command| (ls.id(), command)) + }); if args.is_empty() { - let commands = options - .commands - .iter() - .map(|command| helix_lsp::lsp::Command { - title: command.clone(), - command: command.clone(), - arguments: None, + let commands = ls_id_commands + .map(|(ls_id, command)| { + LsIdCommand( + ls_id, + helix_lsp::lsp::Command { + title: command.clone(), + command: command.clone(), + arguments: None, + }, + ) }) .collect::>(); let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::Picker::new(commands, (), move |cx, command, _action| { - execute_lsp_command(cx.editor, language_server_id, command.clone()); - }); + let picker = ui::Picker::new( + commands, + (), + move |cx, LsIdCommand(ls_id, command), _action| { + execute_lsp_command(cx.editor, *ls_id, command.clone()); + }, + ); compositor.push(Box::new(overlaid(picker))) }, )); @@ -1409,21 +1423,32 @@ fn lsp_workspace_command( cx.jobs.callback(callback); } else { let command = args.join(" "); - if options.commands.iter().any(|c| c == &command) { - execute_lsp_command( - cx.editor, - language_server_id, - helix_lsp::lsp::Command { - title: command.clone(), - arguments: None, - command, - }, - ); - } else { - cx.editor.set_status(format!( - "`{command}` is not supported for this language server" - )); - return Ok(()); + let matches: Vec<_> = ls_id_commands + .filter(|(_ls_id, c)| *c == &command) + .collect(); + + match matches.as_slice() { + [(ls_id, _command)] => { + execute_lsp_command( + cx.editor, + *ls_id, + helix_lsp::lsp::Command { + title: command.clone(), + arguments: None, + command, + }, + ); + } + [] => { + cx.editor.set_status(format!( + "`{command}` is not supported for any language server" + )); + } + _ => { + cx.editor.set_status(format!( + "`{command}` supported by multiple language servers" + )); + } } } Ok(()) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6a4655fde..0a65b12b5 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -364,14 +364,16 @@ pub fn language(editor: &Editor, input: &str) -> Vec { } pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { - let Some(options) = doc!(editor) + let commands = doc!(editor) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) - .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) - else { - return vec![]; - }; + .flat_map(|ls| { + ls.capabilities() + .execute_command_provider + .iter() + .flat_map(|options| options.commands.iter()) + }); - fuzzy_match(input, &options.commands, false) + fuzzy_match(input, commands, false) .into_iter() .map(|(name, _)| ((0..), name.to_owned().into())) .collect() From 1fb99ec3b26c833984ef90448d3beb207f56d675 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 6 Jul 2024 12:39:55 -0500 Subject: [PATCH 074/200] Fix language server ID type in lsp_workspace_command (#11105) --- helix-term/src/commands/typed.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 3a8ed1461..032f016f5 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -9,6 +9,7 @@ use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; use helix_core::{line_ending, shellwords::Shellwords}; +use helix_lsp::LanguageServerId; use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; @@ -1371,7 +1372,7 @@ fn lsp_workspace_command( return Ok(()); } - struct LsIdCommand(usize, helix_lsp::lsp::Command); + struct LsIdCommand(LanguageServerId, helix_lsp::lsp::Command); impl ui::menu::Item for LsIdCommand { type Data = (); From 8b7c33d00daa13213de44ebab2c298fd35232412 Mon Sep 17 00:00:00 2001 From: Matt Armstrong Date: Sat, 6 Jul 2024 10:40:23 -0700 Subject: [PATCH 075/200] Minor improvements to comments in selection.rs (#11101) --- helix-core/src/selection.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 8995da8f9..eff1fcd70 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -175,7 +175,7 @@ pub fn contains(&self, pos: usize) -> bool { /// function runs in O(N) (N is number of changes) and can therefore /// cause performance problems if run for a large number of ranges as the /// complexity is then O(MN) (for multicuror M=N usually). Instead use - /// [Selection::map] or [ChangeSet::update_positions] instead + /// [Selection::map] or [ChangeSet::update_positions]. pub fn map(mut self, changes: &ChangeSet) -> Self { use std::cmp::Ordering; if changes.is_empty() { @@ -541,6 +541,8 @@ pub fn point(pos: usize) -> Self { } /// Normalizes a `Selection`. + /// + /// Ranges are sorted by [Range::from], with overlapping ranges merged. fn normalize(mut self) -> Self { if self.len() < 2 { return self; From 0c8d51ee3620470c99863371518f03d5b85351d8 Mon Sep 17 00:00:00 2001 From: Salman Farooq <46742354+SalmanFarooqShiekh@users.noreply.github.com> Date: Sun, 7 Jul 2024 07:24:04 +0500 Subject: [PATCH 076/200] add cursorcolumn and cursorline to base16_transparent theme (#11099) --- runtime/themes/base16_transparent.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/themes/base16_transparent.toml b/runtime/themes/base16_transparent.toml index 70452366f..d5786f89e 100644 --- a/runtime/themes/base16_transparent.toml +++ b/runtime/themes/base16_transparent.toml @@ -24,6 +24,10 @@ "ui.cursor.match" = { fg = "light-yellow", underline = { color = "light-yellow", style = "line" } } "ui.cursor.primary" = { modifiers = ["reversed", "slow_blink"] } "ui.cursor.secondary" = { modifiers = ["reversed"] } +"ui.cursorline.primary" = { underline = { color = "light-gray", style = "line" } } +"ui.cursorline.secondary" = { underline = { color = "light-gray", style = "line" } } +"ui.cursorcolumn.primary" = { bg = "gray" } +"ui.cursorcolumn.secondary" = { bg = "gray" } "ui.virtual.ruler" = { bg = "gray" } "ui.virtual.whitespace" = "gray" "ui.virtual.indent-guide" = "gray" From 6997ee915104a6482c1d94fa7196f3bea6b87a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Fern=C3=A1ndez=20Serrata?= <76864299+Rudxain@users.noreply.github.com> Date: Tue, 9 Jul 2024 05:34:28 -0400 Subject: [PATCH 077/200] `&Option` -> `Option<&T>` (#11091) * refactor `starting_request_args` to only `ref` non-`Copy` * refactor `needs_recompile` to only `ref` non-`Copy` * refactor `add_workspace_folder` to only `ref` `Some` --------- Co-authored-by: Rudxain --- helix-dap/src/client.rs | 4 ++-- helix-loader/src/grammar.rs | 4 ++-- helix-lsp/src/client.rs | 11 +++++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index 2f5b3d0fc..ed4515fe9 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -157,8 +157,8 @@ async fn get_port() -> Option { ) } - pub fn starting_request_args(&self) -> &Option { - &self.starting_request_args + pub fn starting_request_args(&self) -> Option<&Value> { + self.starting_request_args.as_ref() } pub async fn tcp_process( diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 7977c6df8..99e911544 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -422,7 +422,7 @@ fn build_tree_sitter_library( } } - let recompile = needs_recompile(&library_path, &parser_path, &scanner_path) + let recompile = needs_recompile(&library_path, &parser_path, scanner_path.as_ref()) .context("Failed to compare source and binary timestamps")?; if !recompile { @@ -568,7 +568,7 @@ fn build_tree_sitter_library( fn needs_recompile( lib_path: &Path, parser_c_path: &Path, - scanner_path: &Option, + scanner_path: Option<&PathBuf>, ) -> Result { if !lib_path.exists() { return Ok(true); diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 0a822c5b3..643aa9a26 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -123,7 +123,7 @@ pub fn try_add_doc( { client.add_workspace_folder( root_uri, - &workspace_folders_caps.change_notifications, + workspace_folders_caps.change_notifications.as_ref(), ); } }); @@ -136,7 +136,10 @@ pub fn try_add_doc( .and_then(|cap| cap.workspace_folders.as_ref()) .filter(|cap| cap.supported.unwrap_or(false)) { - self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications); + self.add_workspace_folder( + root_uri, + workspace_folders_caps.change_notifications.as_ref(), + ); true } else { // the server doesn't support multi workspaces, we need a new client @@ -147,7 +150,7 @@ pub fn try_add_doc( fn add_workspace_folder( &self, root_uri: Option, - change_notifications: &Option>, + change_notifications: Option<&OneOf>, ) { // root_uri is None just means that there isn't really any LSP workspace // associated with this file. For servers that support multiple workspaces @@ -162,7 +165,7 @@ fn add_workspace_folder( self.workspace_folders .lock() .push(workspace_for_uri(root_uri.clone())); - if &Some(OneOf::Left(false)) == change_notifications { + if Some(&OneOf::Left(false)) == change_notifications { // server specifically opted out of DidWorkspaceChange notifications // let's assume the server will request the workspace folders itself // and that we can therefore reuse the client (but are done now) From 348c0ebeb4ee2696c740d59af4e54b499e441530 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:40:22 +0900 Subject: [PATCH 078/200] build(deps): bump the rust-dependencies group with 5 updates (#11113) Bumps the rust-dependencies group with 5 updates: | Package | From | To | | --- | --- | --- | | [serde](https://github.com/serde-rs/serde) | `1.0.203` | `1.0.204` | | [imara-diff](https://github.com/pascalkuthe/imara-diff) | `0.1.5` | `0.1.6` | | [clipboard-win](https://github.com/DoumanAsh/clipboard-win) | `5.3.1` | `5.4.0` | | [open](https://github.com/Byron/open-rs) | `5.1.4` | `5.2.0` | | [cc](https://github.com/rust-lang/cc-rs) | `1.0.104` | `1.0.106` | Updates `serde` from 1.0.203 to 1.0.204 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.203...v1.0.204) Updates `imara-diff` from 0.1.5 to 0.1.6 - [Release notes](https://github.com/pascalkuthe/imara-diff/releases) - [Changelog](https://github.com/pascalkuthe/imara-diff/blob/master/CHANGELOG.md) - [Commits](https://github.com/pascalkuthe/imara-diff/compare/v0.1.5...v0.1.6) Updates `clipboard-win` from 5.3.1 to 5.4.0 - [Commits](https://github.com/DoumanAsh/clipboard-win/commits) Updates `open` from 5.1.4 to 5.2.0 - [Release notes](https://github.com/Byron/open-rs/releases) - [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md) - [Commits](https://github.com/Byron/open-rs/compare/v5.1.4...v5.2.0) Updates `cc` from 1.0.104 to 1.0.106 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.0.104...cc-v1.0.106) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: imara-diff dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: clipboard-win dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: open dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 26 +++++++++++++------------- helix-core/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- helix-vcs/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d225b6650..7c797aaca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" [[package]] name = "cfg-if" @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -1610,12 +1610,12 @@ dependencies = [ [[package]] name = "imara-diff" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8" +checksum = "af13c8ceb376860ff0c6a66d83a8cdd4ecd9e464da24621bbffcd02b49619434" dependencies = [ "ahash", - "hashbrown 0.12.3", + "hashbrown 0.14.5", ] [[package]] @@ -1855,9 +1855,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "5.1.4" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388" +checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37" dependencies = [ "is-wsl", "libc", @@ -2119,18 +2119,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 3d5d554fd..c6b87e682 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -40,7 +40,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" -imara-diff = "0.1.0" +imara-diff = "0.1.6" encoding_rs = "0.8" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 99f966d31..fb850f7c0 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -58,7 +58,7 @@ pulldown-cmark = { version = "0.11", default-features = false } content_inspector = "0.2.4" # opening URLs -open = "5.1.4" +open = "5.2.0" url = "2.5.2" # config diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 9d6c9a73c..bc8c5b7ba 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -20,7 +20,7 @@ parking_lot = "0.12" arc-swap = { version = "1.7.1" } gix = { version = "0.63.0", features = ["attributes", "status"], default-features = false, optional = true } -imara-diff = "0.1.5" +imara-diff = "0.1.6" anyhow = "1" log = "0.4" diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 5de689876..81729af09 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -53,7 +53,7 @@ parking_lot = "0.12.3" thiserror.workspace = true [target.'cfg(windows)'.dependencies] -clipboard-win = { version = "5.3", features = ["std"] } +clipboard-win = { version = "5.4", features = ["std"] } [target.'cfg(unix)'.dependencies] libc = "0.2" From 71df2428ee050edd2eb2924c467ece4e58e90533 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Wed, 10 Jul 2024 04:37:25 +0100 Subject: [PATCH 079/200] Fix heredoc and add ansi_c_string highlights in bash queries (#11118) --- runtime/queries/bash/highlights.scm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/runtime/queries/bash/highlights.scm b/runtime/queries/bash/highlights.scm index 1aa35aa7c..c3e504f32 100644 --- a/runtime/queries/bash/highlights.scm +++ b/runtime/queries/bash/highlights.scm @@ -1,10 +1,15 @@ [ (string) (raw_string) + (ansi_c_string) (heredoc_body) - (heredoc_start) ] @string +[ + (heredoc_start) + (heredoc_end) +] @label + (command_name) @function (variable_name) @variable.other.member From 86e4b51416b6f966858522890003fa6c58451891 Mon Sep 17 00:00:00 2001 From: kanielrkirby <77940607+kanielrkirby@users.noreply.github.com> Date: Tue, 9 Jul 2024 22:38:50 -0500 Subject: [PATCH 080/200] Add changes for undo in insert mode (#11090) * Add changes before insert mode undo Fixes #11077 * Address edge cases for undo like Kakoune does --------- Co-authored-by: Kaniel Kirby Co-authored-by: Michael Davis --- helix-view/src/document.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index a56cbc2ff..ccf2fa8c3 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1410,6 +1410,11 @@ pub fn apply_temporary(&mut self, transaction: &Transaction, view_id: ViewId) -> } fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool { + if undo { + self.append_changes_to_history(view); + } else if !self.changes.is_empty() { + return false; + } let mut history = self.history.take(); let txn = if undo { history.undo() } else { history.redo() }; let success = if let Some(txn) = txn { @@ -1490,6 +1495,11 @@ pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint, emit_lsp_notif } fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool { + if earlier { + self.append_changes_to_history(view); + } else if !self.changes.is_empty() { + return false; + } let txns = if earlier { self.history.get_mut().earlier(uk) } else { From bfb7023656494738d7c037670112cecb3bb8a9b6 Mon Sep 17 00:00:00 2001 From: q <7078@tuta.io> Date: Wed, 10 Jul 2024 06:40:03 +0300 Subject: [PATCH 081/200] Update fleet_dark.toml (#11046) --- runtime/themes/fleet_dark.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/themes/fleet_dark.toml b/runtime/themes/fleet_dark.toml index 7fd26c1f8..b8f28d1f9 100644 --- a/runtime/themes/fleet_dark.toml +++ b/runtime/themes/fleet_dark.toml @@ -77,7 +77,7 @@ "ui.selection" = { bg = "Gray 50" } # .primary "ui.selection.primary" = { bg = "Blue 40" } -"ui.cursorline" = { bg = "Gray 20" } +"ui.cursorline" = { bg = "Gray 15" } "ui.linenr" = "Gray 70" "ui.linenr.selected" = "Gray 110" @@ -121,6 +121,7 @@ "Gray 40" = "#333333" "Gray 30" = "#2d2d2d" "Gray 20" = "#292929" +"Gray 15" = "#1F1F1F" "Gray 10" = "#181818" "Black" = "#000000" "Blue 110" = "#6daaf7" From 15a26b87c913b48b391ffa3b3df28578879390a7 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 9 Jul 2024 22:40:43 -0500 Subject: [PATCH 082/200] Expand tilde for selected paths in goto_file (#10964) Previously `gf` on `~/.config/helix` for example would error if the entire path was selected but succeed and open a picker for the directory contents if the selection was one one-width cursor. We need to expand tildes for all paths instead of just the auto-detected paths. This also refactors the `goto_file` blocks a little so that we construct `paths` once instead of creating the Vec and immediately clearing it when the selection is one single-width cursor. --- helix-term/src/commands.rs | 40 +++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 67955aec0..5f73a23c1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1203,19 +1203,15 @@ fn goto_file_impl(cx: &mut Context, action: Action) { let (view, doc) = current_ref!(cx.editor); let text = doc.text(); let selections = doc.selection(view.id); + let primary = selections.primary(); let rel_path = doc .relative_path() .map(|path| path.parent().unwrap().to_path_buf()) .unwrap_or_default(); - let mut paths: Vec<_> = selections - .iter() - .map(|r| text.slice(r.from()..r.to()).to_string()) - .collect(); - let primary = selections.primary(); - // Checks whether there is only one selection with a width of 1 - if selections.len() == 1 && primary.len() == 1 { - paths.clear(); + let paths: Vec<_> = if selections.len() == 1 && primary.len() == 1 { + // Secial case: if there is only one one-width selection, try to detect the + // path under the cursor. let is_valid_path_char = |c: &char| { #[cfg(target_os = "windows")] let valid_chars = &[ @@ -1250,29 +1246,29 @@ fn goto_file_impl(cx: &mut Context, action: Action) { .take_while(is_valid_path_char) .count(); - let path: Cow = text + let path: String = text .slice((start_pos - prefix_len)..(start_pos + postfix_len)) .into(); - log::debug!("Goto file path: {}", path); + log::debug!("goto_file auto-detected path: {}", path); - match expand_tilde(PathBuf::from(path.as_ref())).to_str() { - Some(path) => paths.push(path.to_string()), - None => cx.editor.set_error("Couldn't get string out of path."), - }; - } + vec![path] + } else { + // Otherwise use each selection, trimmed. + selections + .fragments(text.slice(..)) + .map(|sel| sel.trim().to_string()) + .filter(|sel| !sel.is_empty()) + .collect() + }; for sel in paths { - let p = sel.trim(); - if p.is_empty() { - continue; - } - - if let Ok(url) = Url::parse(p) { + if let Ok(url) = Url::parse(&sel) { open_url(cx, url, action); continue; } - let path = &rel_path.join(p); + let path = expand_tilde(Cow::from(PathBuf::from(sel))); + let path = &rel_path.join(path); if path.is_dir() { let picker = ui::file_picker(path.into(), &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); From 86982ab33ce95449a9df929f54883d31014760f8 Mon Sep 17 00:00:00 2001 From: Yuntao Zhao <134149794+yuntaz0@users.noreply.github.com> Date: Wed, 10 Jul 2024 03:41:12 +0000 Subject: [PATCH 083/200] feat: improve hx fish completion (#10853) * feat: improve hx fish completion - add -w and --working-dir options - shorten option description - dynamically call hx --health * feat: improve health check completion - remove header - remove check/x characters * feat: use hx --health languages in completion --- contrib/completion/hx.fish | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/contrib/completion/hx.fish b/contrib/completion/hx.fish index 119776057..f05dba8dd 100644 --- a/contrib/completion/hx.fish +++ b/contrib/completion/hx.fish @@ -1,15 +1,18 @@ #!/usr/bin/env fish # Fish completion script for Helix editor -set -l langs (hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g') - complete -c hx -s h -l help -d "Prints help information" complete -c hx -l tutor -d "Loads the tutorial" -complete -c hx -l health -x -a "$langs" -d "Checks for errors in editor setup" -complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetches or builds tree-sitter grammars" +complete -c hx -l health -xa "(__hx_langs_ops)" -d "Checks for errors" +complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetch or build tree-sitter grammars" 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 -r -d "Specifies a file to use for completion" -complete -c hx -l log -r -d "Specifies a file to write log data into" +complete -c hx -l vsplit -d "Splits all given files vertically" +complete -c hx -l hsplit -d "Splits all given files horizontally" +complete -c hx -s c -l config -r -d "Specifies a file to use for config" +complete -c hx -l log -r -d "Specifies a file to use for logging" +complete -c hx -s w -l working-dir -d "Specify initial working directory" -xa "(__fish_complete_directories)" + +function __hx_langs_ops + hx --health languages | tail -n '+2' | string replace -fr '^(\S+) .*' '$1' +end From 649bd4501ee3f292d57964b8a8504fd9a83bd6e8 Mon Sep 17 00:00:00 2001 From: baiyang1919813 <168923710+baiyang1919813@users.noreply.github.com> Date: Thu, 11 Jul 2024 01:28:11 +0800 Subject: [PATCH 084/200] Add basedpyright langserver (#11121) --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index 9c5ce6818..0cd493992 100644 --- a/languages.toml +++ b/languages.toml @@ -73,6 +73,7 @@ prisma-language-server = { command = "prisma-language-server", args = ["--stdio" purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] } pylsp = { command = "pylsp" } pyright = { command = "pyright-langserver", args = ["--stdio"], config = {} } +basedpyright = { command = "basedpyright-langserver", args = ["--stdio"], config = {} } pylyzer = { command = "pylyzer", args = ["--server"] } qmlls = { command = "qmlls" } r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] } From 9d75385062dff480ba94de0a870c15b9b99021b2 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Thu, 11 Jul 2024 01:06:50 +0100 Subject: [PATCH 085/200] Update ZSH completions (#11120) --- contrib/completion/hx.zsh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contrib/completion/hx.zsh b/contrib/completion/hx.zsh index aaad6f844..be380f509 100644 --- a/contrib/completion/hx.zsh +++ b/contrib/completion/hx.zsh @@ -1,4 +1,4 @@ -#compdef _hx hx +compdef _hx hx # Zsh completion script for Helix editor _hx() { @@ -14,16 +14,18 @@ _hx() { "--health[Checks for errors in editor setup]:language:->health" \ "-g[Fetches or builds tree-sitter grammars]:action:->grammar" \ "--grammar[Fetches or builds tree-sitter grammars]:action:->grammar" \ - "--vsplit[Splits all given files vertically into different windows]" \ - "--hsplit[Splits all given files horizontally into different windows]" \ + "--vsplit[Splits all given files vertically]" \ + "--hsplit[Splits all given files horizontally]" \ "-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]" \ + "-w[Specify initial working directory]" \ + "--working-dir[Specify initial working directory]" \ + "--log[Specifies a file to use for logging]" \ "*:file:_files" case "$state" in health) - local languages=($(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g')) + local languages=($(hx --health | tail -n '+11' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g;s/[✘✓]//g')) _values 'language' $languages ;; grammar) From 8229a40da8a6675cbfa394e2437207bcf7863b45 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Thu, 11 Jul 2024 07:47:49 +0100 Subject: [PATCH 086/200] Add space back to main text in the tutor after chapter 11 (#11117) --- runtime/tutor | 388 +++++++++++++++++++++++++------------------------- 1 file changed, 194 insertions(+), 194 deletions(-) diff --git a/runtime/tutor b/runtime/tutor index 055718379..505f88482 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -145,7 +145,7 @@ You can also type wq or write-quit to save and exit. - Note: You can optionally enter a filepath after the w / write + Note: You can optionally enter a file path after the w / write command in order to save to that path. Note: If there are any unsaved changes to a file, a plus [+] will appear next to the file name in the status bar. @@ -1124,14 +1124,14 @@ letters! that is not good grammar. you can fix this. = 11.1 COMMENTING A LINE = ================================================================= -Press Ctrl-c to comment the line under your cursor. -To uncomment the line, press Ctrl-c again. + Press Ctrl-c to comment the line under your cursor. + To uncomment the line, press Ctrl-c again. -1. Move your cursor to the line marked '-->' below. -2. Now comment the line marked with '-->'. -3. Now try uncommenting the line. + 1. Move your cursor to the line marked '-->' below. + 2. Now comment the line marked with '-->'. + 3. Now try uncommenting the line. ---> Comment me please + --> Comment me please @@ -1146,23 +1146,23 @@ To uncomment the line, press Ctrl-c again. = 11.2 COMMENTING MULTIPLE LINES = ================================================================= -Using the selections and multi-cursor functionality, you can -comment multiple lines as long as it is under the selection or -cursors. + Using the selections and multi-cursor functionality, you can + comment multiple lines as long as it is under the selection or + cursors. -1. Move your cursor to the line marked with '-->' below. -2. Now try to select or add more cursors the other lines marked - with '-->'. -3. Comment those lines. + 1. Move your cursor to the line marked with '-->' below. + 2. Now try to select or add more cursors the other lines marked + with '-->'. + 3. Comment those lines. ---> How many are you going to comment? ---> Is this enough for a comment? ---> What are you doing?! ---> Stop commenting me! ---> AAAAaargh!!! + --> How many are you going to comment? + --> Is this enough for a comment? + --> What are you doing?! + --> Stop commenting me! + --> AAAAaargh!!! -Note: If there are already commented lines under selections or -multiple cursors, they won't be uncommented but commented again. + Note: If there are already commented lines under selections or + multiple cursors, they won't be uncommented but commented again. ================================================================= = CHAPTER 11 RECAP = @@ -1190,20 +1190,20 @@ multiple cursors, they won't be uncommented but commented again. = 12.1 USING MATCH MODE JUMP = ================================================================= -To switch to match mode from normal mode, type m. This feature -is particularly useful for handling bracket pairs and their -contents. + To switch to match mode from normal mode, type m. This feature + is particularly useful for handling bracket pairs and their + contents. -There are several actions that can be performed in match mode, -as indicated by the help pop-up. To jump to a matching bracket pair, -simply press mm. For example on the lines below (starting with --->), move the cursor in normal mode to (, and then press mm to jump -to the matching ). You can do the same on the line below: for example -move to ], and press mm to jump to [ . + There are several actions that can be performed in match mode, + as indicated by the help pop-up. To jump to a matching bracket pair, + simply press mm. For example on the lines below (starting with + -->), move the cursor in normal mode to (, and then press mm to jump + to the matching ). You can do the same on the line below: for example + move to ], and press mm to jump to [ . ---> you can (jump between matching parenthesis) ---> or between matching [ square brackets ] ---> now { you know the drill: this works with brackets too } + --> you can (jump between matching parenthesis) + --> or between matching [ square brackets ] + --> now { you know the drill: this works with brackets too } @@ -1212,41 +1212,41 @@ move to ], and press mm to jump to [ . = 12.2 USING MATCH MODE SELECT INSIDE = ================================================================= -Match mode also lets you select the "inside" content between a -pair of brackets or other delimiters. In the lines below: + Match mode also lets you select the "inside" content between a + pair of brackets or other delimiters. In the lines below: -- move to the --> line, put your cursor in normal mode at any -location between the parenthesis, for example at 'x', and press -mi( or mi) to select the whole content inside the parenthesis -(parenthesis excluded). As usual, you can then do anything you want -with the selection (for example, press c to change it) + - move to the --> line, put your cursor in normal mode at any + location between the parenthesis, for example at 'x', and press + mi( or mi) to select the whole content inside the parenthesis + (parenthesis excluded). As usual, you can then do anything you want + with the selection (for example, press c to change it) ---> outside and (inside x parenthesis) - and outside again + --> outside and (inside x parenthesis) - and outside again -Test below that you can do the same with [], or {}, or with -nested combinations of these (this will act on the immediately -surrounding matching pair). This also works with "" and similar + Test below that you can do the same with [], or {}, or with + nested combinations of these (this will act on the immediately + surrounding matching pair). This also works with "" and similar ---> test [ with square brackets ] ! ---> try ( with nested [ pairs of ( parenthesis) and "brackets" ]) + --> test [ with square brackets ] ! + --> try ( with nested [ pairs of ( parenthesis) and "brackets" ]) ================================================================= = 12.3 USING MATCH MODE SELECT AROUND = ================================================================= -You can also select the "around" content, i.e. both the inside -content and the delimiters themselves, by using the ma select. -For example, move to the line under, move your cursor in normal -mode to any position between the (), and select the content of -the (), including the surrounding (), by typing ma( or ma). As -usual, you can do anything you want with the selection, for -example delete it all with ma(d . + You can also select the "around" content, i.e. both the inside + content and the delimiters themselves, by using the ma select. + For example, move to the line under, move your cursor in normal + mode to any position between the (), and select the content of + the (), including the surrounding (), by typing ma( or ma). As + usual, you can do anything you want with the selection, for + example delete it all with ma(d . ---> you ( select x around ) to include delimiters in the select + --> you ( select x around ) to include delimiters in the select -This naturally works with other delimiters too: + This naturally works with other delimiters too: ---> try [ with 'square' brackets ] too! + --> try [ with 'square' brackets ] too! @@ -1256,21 +1256,21 @@ This naturally works with other delimiters too: = 12.4 USING MATCH MODE SURROUND = ================================================================= -The match mode can also be used to add surrounding around the -current selection. For example, move to the line below, then: - * i) select the "select all of this" line segment (for example, -move in normal mode the cursor to the start of select, then enter -selection mode with v , then select the 4 next words with 4e ), - * ii) press ms( or ms) to surround the selection with a pair of -parenthesis. + The match mode can also be used to add surrounding around the + current selection. For example, move to the line below, then: + * i) select the "select all of this" line segment (for example, + move in normal mode the cursor to the start of select, then enter + selection mode with v , then select the 4 next words with 4e ), + * ii) press ms( or ms) to surround the selection with a pair of + parenthesis. ---> so, select all of this, and surround it with () + --> so, select all of this, and surround it with () -You can do the same with other delimiters: for example, ms' on -WORD below to surround it with a pair of ''. You can try also -with adding a surrounding pair of "", or {}, or []. + You can do the same with other delimiters: for example, ms' on + WORD below to surround it with a pair of ''. You can try also + with adding a surrounding pair of "", or {}, or []. ---> surround this WORD ! + --> surround this WORD ! @@ -1278,64 +1278,64 @@ with adding a surrounding pair of "", or {}, or []. = 12.5 USING MATCH MODE DELETE SURROUND = ================================================================= -You can delete surrounding pair of delimiters with the md -command. On the line below, move the cursor anywhere -within the pair of (), for example to the 'x', then from there, -in normal mode, press md( or md) to delete the surrounding -pair of parenthesis. + You can delete surrounding pair of delimiters with the md + command. On the line below, move the cursor anywhere + within the pair of (), for example to the 'x', then from there, + in normal mode, press md( or md) to delete the surrounding + pair of parenthesis. ---> delete (the x pair of parenthesis) from within! + --> delete (the x pair of parenthesis) from within! -You can naturally delete other kinds of surroundings: + You can naturally delete other kinds of surroundings: ---> delete (nested [delimiters]): "this" will delete the nearest -matching surrounding pair. ---> delete "layers "of" quote marks" too: this will delete the -nearest previous and following quote marks + --> delete (nested [delimiters]): "this" will delete the nearest + matching surrounding pair. + --> delete "layers "of" quote marks" too: this will delete the + nearest previous and following quote marks -Trying to delete unexisting surrounding delimiters print an error -at the bottom bar and does nothing. + Trying to delete unexisting surrounding delimiters print an error + at the bottom bar and does nothing. ================================================================= = 12.6 USING MATCH MODE REPLACE SURROUND = ================================================================= -You can replace surrounding pairs of delimiters with the mr -command. On the line below, move the cursor to -anywhere within the pair of (), for example on the 'x', then in -normal mode, press mr([ to replace the pair of () with a pair -of []. + You can replace surrounding pairs of delimiters with the mr + command. On the line below, move the cursor to + anywhere within the pair of (), for example on the 'x', then in + normal mode, press mr([ to replace the pair of () with a pair + of []. ---> replace the (pair from x within), with something else + --> replace the (pair from x within), with something else -This command will act on the closest enclosing pair, so you -can try replacing different surrounding in the following: + This command will act on the closest enclosing pair, so you + can try replacing different surrounding in the following: ---> some (nested surroundings [can be replaced]) ---> this "works with 'other surroundings' too" + --> some (nested surroundings [can be replaced]) + --> this "works with 'other surroundings' too" -You can try to replace a non existing pair: this will show -an error warning at the bottom bar and do nothing. + You can try to replace a non existing pair: this will show + an error warning at the bottom bar and do nothing. ================================================================= = CHAPTER 12 RECAP = ================================================================= -You can enter the match mode with the m key; this will show the -actions available in a popup. This will allow you to: - * jump to matching pair of delimiters with mm (you must have a - delimiter belonging to a pair under your cursor) - * select inside a pair of delimiters surrounding your cursor - (i.e. select the content but not the delimiters) with mi( - and similar - * select around a pair of delimiters surrounding your cursor - (i.e. select the content and the delimiters) with ma( and - similar - * delete surrounding delimiters with md( and similar - * add surrounding delimiters around the selection with ms( - * replace a pair of delimiters surrounding your selection with - mr([ to replace for example surrounding () with [] + You can enter the match mode with the m key; this will show the + actions available in a popup. This will allow you to: + * jump to matching pair of delimiters with mm (you must have a + delimiter belonging to a pair under your cursor) + * select inside a pair of delimiters surrounding your cursor + (i.e. select the content but not the delimiters) with mi( + and similar + * select around a pair of delimiters surrounding your cursor + (i.e. select the content and the delimiters) with ma( and + similar + * delete surrounding delimiters with md( and similar + * add surrounding delimiters around the selection with ms( + * replace a pair of delimiters surrounding your selection with + mr([ to replace for example surrounding () with [] @@ -1344,20 +1344,20 @@ actions available in a popup. This will allow you to: = CHAPTER 13.1 CREATE NEW SPLIT = ================================================================= -In Normal mode, press Ctrl-w to open the Window menu, which displays -a list of available commands. + In Normal mode, press Ctrl-w to open the Window menu, which displays + a list of available commands. -To open a new empty buffer in a vertical split on the right half -of your current window, use Ctrl-w nv (i.e., press Ctrl -and w simultaneously, then press n, followed by v). Your current -window will now split in 2 vertically. A new empty buffer split -will appear on the right half and your cursor will jump to the -new vertical split. + To open a new empty buffer in a vertical split on the right half + of your current window, use Ctrl-w nv (i.e., press Ctrl + and w simultaneously, then press n, followed by v). Your current + window will now split in 2 vertically. A new empty buffer split + will appear on the right half and your cursor will jump to the + new vertical split. -To create a new empty buffer in a horizontal split, press -Ctrl-w ns. This action divides your current window into two -horizontally, creates a new buffer, and moves your cursor to the -new horizontal split. + To create a new empty buffer in a horizontal split, press + Ctrl-w ns. This action divides your current window into two + horizontally, creates a new buffer, and moves your cursor to the + new horizontal split. @@ -1366,33 +1366,33 @@ new horizontal split. = CHAPTER 13.2 MOVE BETWEEN SPLITS = ================================================================= -Use Ctrl-w k to move to the split above your current split. Use -Ctrl-w j to move to the split below. Use Ctrl-w h to move to -the split on the left and Ctrl-w l to move to the split on the -right. To navigate to the next split (in the order they were -opened), press Ctrl-w w. + Use Ctrl-w k to move to the split above your current split. Use + Ctrl-w j to move to the split below. Use Ctrl-w h to move to + the split on the left and Ctrl-w l to move to the split on the + right. To navigate to the next split (in the order they were + opened), press Ctrl-w w. -You can now do whatever you want in your new buffers and splits. -Once you are done with using your new buffer split, -you can close it with Ctrl-w q . Move to the bottom right split -with Ctrl-w l then Ctrl-w j, then press Ctrl-w q to close this -specific split. + You can now do whatever you want in your new buffers and splits. + Once you are done with using your new buffer split, + you can close it with Ctrl-w q . Move to the bottom right split + with Ctrl-w l then Ctrl-w j, then press Ctrl-w q to close this + specific split. -You can also close all splits except the current one with Ctrl-w o . -Open a third vertical split with Ctrl-w nv , then move to the -leftmost split with Ctrl-w h twice, then from inside the split on -the left press Ctrl-w o to close all except this split. + You can also close all splits except the current one with Ctrl-w o . + Open a third vertical split with Ctrl-w nv , then move to the + leftmost split with Ctrl-w h twice, then from inside the split on + the left press Ctrl-w o to close all except this split. ================================================================= = CHAPTER 13.3 SPLIT CURRENT BUFFER = ================================================================= -Use Ctrl-w s to split the view of the current buffer horizontally -and Ctrl-w v to split it vertically with the buffer opened in both -splits. + Use Ctrl-w s to split the view of the current buffer horizontally + and Ctrl-w v to split it vertically with the buffer opened in both + splits. -Close extra splits with Ctrl-w o to return to a single window view. + Close extra splits with Ctrl-w o to return to a single window view. @@ -1410,41 +1410,41 @@ Close extra splits with Ctrl-w o to return to a single window view. = CHAPTER 13.4 USE COMMANDS TO SPLIT = ================================================================= -The :vsplit (or :vs for short) and :hsplit (or :hs) commands can -also be used to split a specific buffer vertically or horizontally. -For example, enter the command: + The :vsplit (or :vs for short) and :hsplit (or :hs) commands can + also be used to split a specific buffer vertically or horizontally. + For example, enter the command: -:vs something + :vs something -to open a new vertical split named "something" to the right. Here, -"something" is not an existing file, so a new buffer with this name -will open; however, you can replace "something" with any file name -to open it in a new buffer. Similarly, you can enter the command: + to open a new vertical split named "something" to the right. Here, + "something" is not an existing file, so a new buffer with this name + will open; however, you can replace "something" with any file name + to open it in a new buffer. Similarly, you can enter the command: -:hs some_more + :hs some_more -to open a new buffer named "some_more" in the lower half. -"some_more" could be any file or path to open this specific file -or path instead of a new empty buffer. + to open a new buffer named "some_more" in the lower half. + "some_more" could be any file or path to open this specific file + or path instead of a new empty buffer. ================================================================= = CHAPTER 13.5 SWAPPING SPLITS = ================================================================= -Open a split on the left with :vs hello1 and then a split below -with :hs hello2. + Open a split on the left with :vs hello1 and then a split below + with :hs hello2. -From hello2, press Ctrl-w K to swap it with the split above. Now -hello2 is at the top while hello1 is at the bottom. + From hello2, press Ctrl-w K to swap it with the split above. Now + hello2 is at the top while hello1 is at the bottom. -Still from hello2, press Ctrl-w H to swap with the split on the -left: now hello2 is on the left and the tutor is on the top -right. After Ctrl-w you can use HJKL to split with the buffer -on the left / below / above / on the right. + Still from hello2, press Ctrl-w H to swap with the split on the + left: now hello2 is on the left and the tutor is on the top + right. After Ctrl-w you can use HJKL to split with the buffer + on the left / below / above / on the right. -Move back to the tutor split, and press Ctrl-w o to only keep -this split. + Move back to the tutor split, and press Ctrl-w o to only keep + this split. @@ -1454,21 +1454,21 @@ this split. = CHAPTER 13.6 TRANSPOSE SPLITS = ================================================================= -Open a split on the left with :vs hello1 and then a split below -with :vs hello2. + Open a split on the left with :vs hello1 and then a split below + with :vs hello2. -Move to the tutor split, then press Ctrl-w t to transpose the -vertical split opened from this window: now, hello1 and -hello2 are below, rather than to the right of, the tutor. Press -Ctrl-w t again to transpose back. + Move to the tutor split, then press Ctrl-w t to transpose the + vertical split opened from this window: now, hello1 and + hello2 are below, rather than to the right of, the tutor. Press + Ctrl-w t again to transpose back. -Move to the hello1 split, then press Ctrl-w t to transpose the -horizontal split that was opened from this window: now hello2 -is on the right, rather than below, hello1. Press Ctrl-w t to -transpose back. + Move to the hello1 split, then press Ctrl-w t to transpose the + horizontal split that was opened from this window: now hello2 + is on the right, rather than below, hello1. Press Ctrl-w t to + transpose back. -Move back to the tutor split and press Ctrl-w o to close all but -the tutor window. + Move back to the tutor split and press Ctrl-w o to close all but + the tutor window. @@ -1476,21 +1476,21 @@ the tutor window. = CHAPTER 13.7 OPEN SPLIT FROM FILEPICKER = ================================================================= -Splits can also be opened directly from the file picker. Press -space f to open the file picker. From there, you can type in text -to perform file lookup with fuzzy matching, and use the arrows -up and down to move the selected file (indicated by the > symbol). -If you want to exit the file picker, press Escape. + Splits can also be opened directly from the file picker. Press + space f to open the file picker. From there, you can type in text + to perform file lookup with fuzzy matching, and use the arrows + up and down to move the selected file (indicated by the > symbol). + If you want to exit the file picker, press Escape. -Select any file you like in the file picker. You could open it in -the current view by pressing enter (do not do this at present). -But you can also open it in a new split. Press Ctrl-v to open -the selected file in a new vertical split. Press space f again, -select any file you want, and press Ctrl-s to open it in a -horizontal split. + Select any file you like in the file picker. You could open it in + the current view by pressing enter (do not do this at present). + But you can also open it in a new split. Press Ctrl-v to open + the selected file in a new vertical split. Press space f again, + select any file you want, and press Ctrl-s to open it in a + horizontal split. -Move back to the tutor split, and press Ctrl-w o to close all -splits except this one. + Move back to the tutor split, and press Ctrl-w o to close all + splits except this one. @@ -1498,18 +1498,18 @@ splits except this one. = CHAPTER 13 RECAP = ================================================================= -Splits can be used to display either the same buffer several times -or several buffers. To access the main windows and splits commands, -press Ctrl-w . You can move between splits with Ctrl-w hjkl , -you can close a split with Ctrl-w q , and you can close all but -the present split with Ctrl-w o . + Splits can be used to display either the same buffer several times + or several buffers. To access the main windows and splits commands, + press Ctrl-w . You can move between splits with Ctrl-w hjkl , + you can close a split with Ctrl-w q , and you can close all but + the present split with Ctrl-w o . -Splits can also be opened by using the :vs FILENAME and -:hs FILENAME commands. + Splits can also be opened by using the :vs FILENAME and + :hs FILENAME commands. -Splits can also be used directly from the file pickers, by using -Ctrl-v to open the file selected in a new vertical split, and -Ctrl-s in a horizontal split. + Splits can also be used directly from the file pickers, by using + Ctrl-v to open the file selected in a new vertical split, and + Ctrl-s in a horizontal split. From 2d1ac0f699423937ccd1b3b1976dbaed262f00a4 Mon Sep 17 00:00:00 2001 From: Branch Vincent Date: Thu, 11 Jul 2024 18:15:40 -0700 Subject: [PATCH 087/200] Add {pdm,uv}.lock, git/ignore, npmrc to languages (#11131) --- languages.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index 0cd493992..6fed9b2df 100644 --- a/languages.toml +++ b/languages.toml @@ -297,7 +297,7 @@ source = { git = "https://github.com/FuelLabs/tree-sitter-sway", rev = "e491a005 name = "toml" scope = "source.toml" injection-regex = "toml" -file-types = ["toml", { glob = "poetry.lock" }, { glob = "Cargo.lock" }] +file-types = ["toml", { glob = "pdm.lock" }, { glob = "poetry.lock" }, { glob = "Cargo.lock" }, { glob = "uv.lock" }] comment-token = "#" language-servers = [ "taplo" ] indent = { tab-width = 2, unit = " " } @@ -1694,7 +1694,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev = [[language]] name = "git-ignore" scope = "source.gitignore" -file-types = [{ glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }] +file-types = [{ glob = ".gitignore_global" }, { glob = "git/ignore" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }] injection-regex = "git-ignore" comment-token = "#" grammar = "gitignore" @@ -2703,6 +2703,8 @@ file-types = [ "kube", "network", { glob = ".editorconfig" }, + { glob = ".npmrc" }, + { glob = "npmrc" }, { glob = "rclone.conf" }, "properties", "cfg", From 501af93c92610bc169c72d0e7c4894c697e19f48 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Fri, 12 Jul 2024 02:16:55 +0100 Subject: [PATCH 088/200] Documentation: Convert links in the `.desktop` file to absolute paths (#11115) --- book/src/building-from-source.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/book/src/building-from-source.md b/book/src/building-from-source.md index 666917ea4..42ed57a27 100644 --- a/book/src/building-from-source.md +++ b/book/src/building-from-source.md @@ -148,6 +148,12 @@ ### Configure the desktop shortcut cp contrib/Helix.desktop ~/.local/share/applications cp contrib/helix.png ~/.icons # or ~/.local/share/icons ``` +It is recommended to convert the links in the `.desktop` file to absolute paths to avoid potential problems: + +```sh +sed -i -e "s|Exec=hx %F|Exec=$(readlink -f ~/.cargo/bin/hx) %F|g" \ + -e "s|Icon=helix|Icon=$(readlink -f ~/.icons/helix.png)|g" ~/.local/share/applications/Helix.desktop +``` To use another terminal than the system default, you can modify the `.desktop` file. For example, to use `kitty`: From a75b1cf51e445eb021ccd875210f529f09cd5f53 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Fri, 12 Jul 2024 03:55:10 +0100 Subject: [PATCH 089/200] Fix ZSH completions (#11133) --- contrib/completion/hx.zsh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contrib/completion/hx.zsh b/contrib/completion/hx.zsh index be380f509..2631d2283 100644 --- a/contrib/completion/hx.zsh +++ b/contrib/completion/hx.zsh @@ -1,4 +1,4 @@ -compdef _hx hx +#compdef _hx hx # Zsh completion script for Helix editor _hx() { @@ -33,4 +33,3 @@ _hx() { ;; esac } - From fd7b1a3e3722ec49d42f795a475806c476799c30 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Fri, 12 Jul 2024 18:44:48 -0700 Subject: [PATCH 090/200] refactor(commands): trim end of `pipe`-like output (#10952) --- helix-term/src/commands.rs | 1 + helix-term/tests/test/commands.rs | 29 ++++++++++------------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5f73a23c1..f25e8254c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5757,6 +5757,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let fragment = range.slice(text); match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { Ok(result) => { + let result = Tendril::from(result.trim_end()); if !pipe { shell_output = Some(result.clone()); } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 7f41a2219..9f196827f 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -209,13 +209,10 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { "}, "|echo foo", indoc! {"\ - #[|foo\n]# - - #(|foo\n)# - - #(|foo\n)# - - "}, + #[|foo]# + #(|foo)# + #(|foo)#" + }, )) .await?; @@ -228,12 +225,9 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { "}, "!echo foo", indoc! {"\ - #[|foo\n]# - lorem - #(|foo\n)# - ipsum - #(|foo\n)# - dolor + #[|foo]#lorem + #(|foo)#ipsum + #(|foo)#dolor "}, )) .await?; @@ -247,12 +241,9 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { "}, "echo foo", indoc! {"\ - lorem#[|foo\n]# - - ipsum#(|foo\n)# - - dolor#(|foo\n)# - + lorem#[|foo]# + ipsum#(|foo)# + dolor#(|foo)# "}, )) .await?; From b0f3fe755671c1bbaac5be773eadef49965f4e3e Mon Sep 17 00:00:00 2001 From: Antonin Date: Sat, 13 Jul 2024 19:58:06 +0200 Subject: [PATCH 091/200] Include .yml files in Helm chart templates (#11135) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 6fed9b2df..1a2efeb3c 100644 --- a/languages.toml +++ b/languages.toml @@ -3516,7 +3516,7 @@ scope = "source.helm" roots = ["Chart.yaml"] comment-token = "#" language-servers = ["helm_ls"] -file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/_*.tpl"}, { glob = "templates/NOTES.txt" } ] +file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/*.yml" }, { glob = "templates/_*.tpl"}, { glob = "templates/NOTES.txt" } ] [[language]] name = "glimmer" From ec0bdb39762265906e2f4e09654f13cd0fa98d43 Mon Sep 17 00:00:00 2001 From: Masanori Ogino <167209+omasanori@users.noreply.github.com> Date: Sun, 14 Jul 2024 02:58:22 +0900 Subject: [PATCH 092/200] Update Hare grammar (#11130) This change uses that is up-to-date and linked from the official documentation. --- languages.toml | 2 +- runtime/queries/hare/highlights.scm | 51 +++++++++-------------------- runtime/queries/hare/locals.scm | 15 ++++----- 3 files changed, 24 insertions(+), 44 deletions(-) diff --git a/languages.toml b/languages.toml index 1a2efeb3c..69b55f178 100644 --- a/languages.toml +++ b/languages.toml @@ -2055,7 +2055,7 @@ indent = { tab-width = 8, unit = "\t" } [[grammar]] name = "hare" -source = { git = "https://git.sr.ht/~ecmma/tree-sitter-hare", rev = "2495958aaf3f93581c87ec020164255e80655331" } +source = { git = "https://git.sr.ht/~ecs/tree-sitter-hare", rev = "07035a248943575444aa0b893ffe306e1444c0ab" } [[language]] name = "devicetree" diff --git a/runtime/queries/hare/highlights.scm b/runtime/queries/hare/highlights.scm index 4b9731488..5115328d4 100644 --- a/runtime/queries/hare/highlights.scm +++ b/runtime/queries/hare/highlights.scm @@ -1,22 +1,5 @@ -[ - "f32" - "f64" - "i16" - "i32" - "i64" - "i8" - "int" - "rune" - "str" - "u16" - "u32" - "u64" - "u8" - "uint" - "uintptr" - "void" -] @type - +(type) @type +(type "const" @type) [ "else" @@ -36,28 +19,23 @@ "break" ] @keyword.control.repeat -[ - "return" - "yield" -] @keyword.control.return +"return" @keyword.control.return [ "abort" "assert" ] @keyword.control.exception -[ - "def" - "fn" -] @keyword.function +"fn" @keyword.function [ "alloc" "append" "as" "bool" - "char" + "case" "const" + "def" "defer" "delete" "enum" @@ -68,13 +46,14 @@ "match" "nullable" "offset" - "size" - "static" "struct" "type" "union" + "yield" ] @keyword +"static" @keyword.storage.modifier + [ "." "!" @@ -137,15 +116,17 @@ "null" "true" ] @constant.builtin +(literal "void") @constant.builtin -(string_constant) @string +(string_literal) @string (escape_sequence) @constant.character.escape -(rune_constant) @string -(integer_constant) @constant.numeric.integer -(floating_constant) @constant.numeric.float +(rune_literal) @string +(integer_literal) @constant.numeric.integer +(floating_literal) @constant.numeric.float (call_expression (postfix_expression) @function) +(size_expression "size" @function.builtin) (function_declaration name: (identifier) @function) @@ -158,4 +139,4 @@ (fndec_attrs) @special (identifier) @variable - +(struct_union_field (name)) @variable diff --git a/runtime/queries/hare/locals.scm b/runtime/queries/hare/locals.scm index b8b9b9f71..b9e0a91b5 100644 --- a/runtime/queries/hare/locals.scm +++ b/runtime/queries/hare/locals.scm @@ -1,20 +1,19 @@ -(unit) @local.scope +(sub_unit) @local.scope (function_declaration) @local.scope +(compound_expression) @local.scope (global_binding (identifier) @local.definition) -(constant_binding +(constant_binding (identifier) @local.definition) -(type_bindings +(type_binding (identifier) @local.definition) (function_declaration - (prototype - (parameter_list - (parameters - (parameter - (name) @local.definition))))) + (identifier) @local.definition) +(function_declaration + (parameter (name) @local.definition)) (identifier) @local.reference From 928e3f0d85cc8be4ebc02d5ee6092a594de277d9 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Sat, 13 Jul 2024 18:59:07 +0100 Subject: [PATCH 093/200] Add regex injections into bash (#11112) --- runtime/queries/bash/injections.scm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/queries/bash/injections.scm b/runtime/queries/bash/injections.scm index 0fddb10ff..d6771e445 100644 --- a/runtime/queries/bash/injections.scm +++ b/runtime/queries/bash/injections.scm @@ -5,4 +5,7 @@ name: (command_name (word) @_command) argument: (raw_string) @injection.content (#match? @_command "^[gnm]?awk$") - (#set! injection.language "awk")) \ No newline at end of file + (#set! injection.language "awk")) + +((regex) @injection.content + (#set! injection.language "regex")) From 35f1c2a55f2cdf396c26cbe5a8433b009e1b9f01 Mon Sep 17 00:00:00 2001 From: Lukas Grassauer Date: Sat, 13 Jul 2024 19:59:21 +0200 Subject: [PATCH 094/200] Update tree-sitter-todotxt (#11097) Update to latest commit that allows any non-whitespace character for projects, and contexts. --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 69b55f178..97ee3e54a 100644 --- a/languages.toml +++ b/languages.toml @@ -3245,7 +3245,7 @@ auto-format = true [[grammar]] name = "todotxt" -source = { git = "https://github.com/arnarg/tree-sitter-todotxt", rev = "0207f6a4ab6aeafc4b091914d31d8235049a2578" } +source = { git = "https://github.com/arnarg/tree-sitter-todotxt", rev = "3937c5cd105ec4127448651a21aef45f52d19609" } [[language]] name = "strace" From 44d2fc2ab3217e50fd23f67ba93135d62c9acd76 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 13 Jul 2024 12:59:47 -0500 Subject: [PATCH 095/200] Commit an undo checkpoint before each write (#11062) This fixes the modification indicator when saving from insert mode with a config such as [keys.insert] C-s = ":write" Previously the modification indicator would be stuck showing modified even if the buffer contents matched the disk contents when writing after some changes in insert mode with this binding. In insert mode we do not eagerly write undo checkpoints so that all changes made become one checkpoint as you exit insert mode. When saving, `Document`s `changes` `ChangeSet` would be non-empty and when there are changes we show the buffer as modified. Then switching to normal mode would append the changes to history, bumping the current revision past what it was when last saved. Since the last saved revision and current revision were then unsynced, the modification indicator would always show modified. This matches [Kakoune's behavior]. Kakoune has a different architecture for writes but a very similar system for history, transactions and undo checkpoints (what it calls "undo groups"). Upon saving Kakoune creates an undo checkpoint if there are any uncommitted changes. It does this after the write has gone through since its writing system is different. For our writing system it's cleaner to make the undo checkpoint before performing the save so that the history revision increments before we send the save event. [Kakoune's behavior]: https://github.com/mawww/kakoune/blob/80fcfebca8c62ace6cf2af9487784486af07d2d5/src/buffer.cc#L565-L566 --- helix-term/src/commands/typed.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 032f016f5..ed1547f1f 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -340,9 +340,12 @@ fn write_impl( let path = path.map(AsRef::as_ref); if config.insert_final_newline { - insert_final_newline(doc, view); + insert_final_newline(doc, view.id); } + // Save an undo checkpoint for any outstanding changes. + doc.append_changes_to_history(view); + let fmt = if config.auto_format { doc.auto_format().map(|fmt| { let callback = make_format_callback( @@ -367,13 +370,12 @@ fn write_impl( Ok(()) } -fn insert_final_newline(doc: &mut Document, view: &mut View) { +fn insert_final_newline(doc: &mut Document, view_id: ViewId) { let text = doc.text(); if line_ending::get_line_ending(&text.slice(..)).is_none() { let eof = Selection::point(text.len_chars()); let insert = Transaction::insert(text, &eof, doc.line_ending.as_str().into()); - doc.apply(&insert, view.id); - doc.append_changes_to_history(view); + doc.apply(&insert, view_id); } } @@ -704,11 +706,15 @@ pub fn write_all_impl( for (doc_id, target_view) in saves { let doc = doc_mut!(cx.editor, &doc_id); + let view = view_mut!(cx.editor, target_view); if config.insert_final_newline { - insert_final_newline(doc, view_mut!(cx.editor, target_view)); + insert_final_newline(doc, target_view); } + // Save an undo checkpoint for any outstanding changes. + doc.append_changes_to_history(view); + let fmt = if config.auto_format { doc.auto_format().map(|fmt| { let callback = make_format_callback( From e6bf97b8434aa9ecc9e9c6a25411ea6f1148360f Mon Sep 17 00:00:00 2001 From: eh Date: Sat, 13 Jul 2024 14:01:58 -0400 Subject: [PATCH 096/200] Theme: Kanagawa Dragon (#10172) Implements the Dragon variant of the Kanagawa theme. https://github.com/rebelot/kanagawa.nvim?tab=readme-ov-file --- runtime/themes/kanagawa-dragon.toml | 125 ++++++++++++++++++++++++++++ runtime/themes/zed_onedark.toml | 3 +- runtime/themes/zed_onelight.toml | 3 +- 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 runtime/themes/kanagawa-dragon.toml diff --git a/runtime/themes/kanagawa-dragon.toml b/runtime/themes/kanagawa-dragon.toml new file mode 100644 index 000000000..e775f2815 --- /dev/null +++ b/runtime/themes/kanagawa-dragon.toml @@ -0,0 +1,125 @@ +# Kanagawa Dragon +# Author: EricHenry + +# Adaptation of https://github.com/rebelot/kanagawa.nvim +# Original author: rebelot +# All credits to the original author, the palette is taken from the README +# because of some theming differences, it's not an exact copy of the original. + +inherits = "kanagawa" + +## User interface +"ui.selection" = { bg = "waveBlue2" } +"ui.selection.primary" = { bg = "waveBlue1" } +"ui.background" = { fg = "dragonWhite", bg = "dragonBlack3" } +"ui.gutter" = { fg = "dragonBlack6", bg = "dragonBlack4" } + +"ui.linenr" = { fg = "dragonBlack6", bg = "dragonBlack4" } +"ui.linenr.selected" = { fg = "roninYellow", modifiers = ["bold"] } + +"ui.virtual" = "dragonBlack4" +"ui.virtual.ruler" = { bg = "dragonBlack5" } +"ui.virtual.inlay-hint" = "dragonGray2" +"ui.virtual.inlay-hint.parameter" = { fg = "dragonYellow", modifiers = ["dim"] } +"ui.virtual.inlay-hint.type" = { fg = "dragonAqua", modifiers = ["dim"] } +"ui.virtual.jump-label" = { fg = "dragonRed", modifiers = ["bold"] } + +"ui.statusline" = { fg = "oldWhite", bg = "dragonBlack0" } + +"ui.bufferline" = { fg = "oldWhite", bg = "dragonBlack0" } +"ui.bufferline.active" = { fg = "oldWhite", bg = "dragonBlack0" } +"ui.bufferline.background" = { bg = "sumiInk0" } + +"ui.popup" = { fg = "oldWhite", bg = "dragonBlack0" } +"ui.window" = { fg = "sumiInk0" } +"ui.help" = { fg = "fujiWhite", bg = "sumiInk0" } +"ui.text" = "dragonWhite" +"ui.text.focus" = { fg = "dragonWhite", bg = "waveBlue1", modifiers = ["bold"] } + +"ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2" } +"ui.cursor.primary" = { fg = "waveBlue1", bg = "fujiWhite" } +"ui.cursor.match" = { fg = "roninYellow", modifiers = ["bold"] } +"ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" } + +"ui.cursorline.primary" = { bg = "dragonBlack5" } +"ui.cursorcolumn.primary" = { bg = "dragonBlack5" } + +"diagnostic.info" = { underline = { color = "dragonBlue", style = "curl" } } +"diagnostic.hint" = { underline = { color = "waveAqua1", style = "curl" } } +"diagnostic.deprecated" = { fg= "katanaGray", modifiers = ["crossed_out"] } + +## Syntax highlighting +"attribute" = "waveRed" +"type" = "dragonAqua" +"type.enum.variant" = "dragonOrange" +"constructor" = "dragonTeal" +"constant" = "dragonOrange" +"constant.numeric" = "dragonPink" +"constant.character.escape" = "dragonBlue2" +"string" = "dragonGreen2" +"string.regexp" = "dragonRed" +"string.special.url" = "dragonTeal" +"string.special.symbol" = "dragonTeal" +"comment" = "dragonAsh" +"variable" = "dragonWhite" +"variable.builtin" = "dragonRed" +"variable.parameter" = "dragonGray" +"variable.other.member" = "dragonYellow" +"label" = "dragonRed" +"punctuation" = "dragonWhite" +"keyword" = "dragonViolet" +"keyword.control.return" = "dragonRed" +"keyword.control.exception" = "dragonRed" +"keyword.directive" = "dragonRed" +"operator" = "dragonRed" +"function" = "dragonBlue2" +"function.builtin" = "dragonBlue2" +"function.macro" = "dragonRed" +"tag" = "dragonYellow" +"namespace" = "dragonWhite" +"special" = "dragonYellow" + +## Markup modifiers +"markup.heading.marker" = "dragonViolet" +"markup.heading.1" = { fg = "dragonOrange", modifiers = ["bold"] } +"markup.heading.2" = { fg = "dragonYellow", modifiers = ["bold"] } +"markup.heading.3" = { fg = "dragonBlue2", modifiers = ["bold"] } +"markup.heading.4" = { fg = "dragonWhite", modifiers = ["bold"] } +"markup.heading.5" = { fg = "dragonRed", modifiers = ["bold"] } +"markup.heading.6" = { fg = "dragonPink", modifiers = ["bold"] } +"markup.list.numbered" = "dragonPink" +"markup.list.unnumbered" = "dragonRed" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.text" = "dragonTeal" +"markup.link.url" = { fg = "dragonPink", underline.style = "line" } +"markup.link.label" = "dragonBlue2" +"markup.quote" = "springViolet1" +"markup.raw" = "dragonGreen2" + +[palette] +dragonBlack0 = "#0d0c0c" +dragonBlack1 = "#12120f" +dragonBlack2 = "#1D1C19" +dragonBlack3 = "#181616" +dragonBlack4 = "#282727" +dragonBlack5 = "#393836" +dragonBlack6 = "#625e5a" + +dragonWhite = "#c5c9c5" +dragonGreen = "#87a987" +dragonGreen2 = "#8a9a7b" +dragonPink = "#a292a3" +dragonOrange = "#b6927b" +dragonOrange2 = "#b98d7b" +dragonGray = "#a6a69c" +dragonGray2 = "#9e9b93" +dragonGray3 = "#7a8382" +dragonBlue2 = "#8ba4b0" +dragonViolet= "#8992a7" +dragonRed = "#c4746e" +dragonAqua = "#8ea4a2" +dragonAsh = "#737c73" +dragonTeal = "#949fb5" +dragonYellow = "#c4b28a" diff --git a/runtime/themes/zed_onedark.toml b/runtime/themes/zed_onedark.toml index 3eeabab30..74db8f75c 100644 --- a/runtime/themes/zed_onedark.toml +++ b/runtime/themes/zed_onedark.toml @@ -1,4 +1,5 @@ -# Author : Eric Correia +# Zed OneDark +# Author : EricHenry "attribute" = { fg = "yellow" } "comment" = { fg = "light-gray", modifiers = ["italic"] } diff --git a/runtime/themes/zed_onelight.toml b/runtime/themes/zed_onelight.toml index 086fce34b..936366250 100644 --- a/runtime/themes/zed_onelight.toml +++ b/runtime/themes/zed_onelight.toml @@ -1,4 +1,5 @@ -# Author : Eric Correia +# Zed OneLight +# Author : EricHenry inherits = "zed_onedark" From c9b484097b045a34b709131fc62e87ba21789d1a Mon Sep 17 00:00:00 2001 From: Masanori Ogino <167209+omasanori@users.noreply.github.com> Date: Sun, 14 Jul 2024 11:31:33 +0900 Subject: [PATCH 097/200] Exclude EOL repos from the Repology badge (#11159) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f166db1b..3b639214d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ # Installation [Installation documentation](https://docs.helix-editor.com/install.html). -[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg?exclude_unsupported=1)](https://repology.org/project/helix/versions) # Contributing From b2cc7d8fea87c39b3bedba46334a4971fa3b5bd4 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 14 Jul 2024 11:17:49 -0500 Subject: [PATCH 098/200] Add changelog notes for 24.07 (#10731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changelog 2024-05-02 checkpoint: 31273c69e0be3b2d14f0c76d3f6a735e1d332e63 * Changelog 2024-05-06 checkpoint: 61818996c63cb752afb817259a90108f884db1cb * Changelog 2024-05-11 checkpoint: 00e9e5eadef16dd20cd24d303a664faaeb8faa56 * Bump version to 24.05 * Add 24.05 release to AppImage metadata * Fix release number in changelog Co-authored-by: Blaž Hrastnik * Update release numbers to 24.07 * Changelog 2024-06-15 * Changelog 2024-07-14 checkpoint: c9b484097b045a34b709131fc62e87ba21789d1a * Linkify --------- Co-authored-by: Blaž Hrastnik --- CHANGELOG.md | 204 ++++++++++++++++++++++++++++++++++++++ Cargo.lock | 24 ++--- Cargo.toml | 2 +- contrib/Helix.appdata.xml | 3 + 4 files changed, 220 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de15f143c..4f82721a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,207 @@ +# 24.07 (2024-07-14) + +Thanks to all of the contributors! This release has changes from 160 contributors. + +Breaking changes: + +Features: + +- Add a textobject for entries/elements of list-like things ([#8150](https://github.com/helix-editor/helix/pull/8150)) +- Add a picker showing files changed in VCS ([#5645](https://github.com/helix-editor/helix/pull/5645)) +- Use a temporary file for writes ([#9236](https://github.com/helix-editor/helix/pull/9236), [#10339](https://github.com/helix-editor/helix/pull/10339), [#10790](https://github.com/helix-editor/helix/pull/10790)) +- Allow cycling through LSP signature-help signatures with `A-n`/`A-p` ([#9974](https://github.com/helix-editor/helix/pull/9974), [#10654](https://github.com/helix-editor/helix/pull/10654), [#10655](https://github.com/helix-editor/helix/pull/10655)) +- Use tree-sitter when finding matching brackets and closest pairs ([#8294](https://github.com/helix-editor/helix/pull/8294), [#10613](https://github.com/helix-editor/helix/pull/10613), [#10777](https://github.com/helix-editor/helix/pull/10777)) +- Auto-save all buffers after a delay ([#10899](https://github.com/helix-editor/helix/pull/10899), [#11047](https://github.com/helix-editor/helix/pull/11047)) + +Commands: + +- `select_all_siblings` (`A-a`) - select all siblings of each selection ([87c4161](https://github.com/helix-editor/helix/commit/87c4161)) +- `select_all_children` (`A-i`) - select all children of each selection ([fa67c5c](https://github.com/helix-editor/helix/commit/fa67c5c)) +- `:read` - insert the contents of the given file at each selection ([#10447](https://github.com/helix-editor/helix/pull/10447)) + +Usability improvements: + +- Support scrolling popup contents using the mouse ([#10053](https://github.com/helix-editor/helix/pull/10053)) +- Sort the jumplist picker so that most recent items come first ([#10095](https://github.com/helix-editor/helix/pull/10095)) +- Improve `goto_file`'s (`gf`) automatic path detection strategy ([#9065](https://github.com/helix-editor/helix/pull/9065)) +- Respect language server definition order in code action menu ([#9590](https://github.com/helix-editor/helix/pull/9590)) +- Allow using a count with `goto_next_buffer` (`gn`) and `goto_previous_buffer` (`gp`) ([#10463](https://github.com/helix-editor/helix/pull/10463)) +- Improve the positioning of popups ([#10257](https://github.com/helix-editor/helix/pull/10257), [#10573](https://github.com/helix-editor/helix/pull/10573)) +- Reset all changes overlapped by selections in `:reset-diff-change` ([#10728](https://github.com/helix-editor/helix/pull/10728)) +- Await pending writes in the `suspend` command (`C-z`) ([#10797](https://github.com/helix-editor/helix/pull/10797)) +- Remove special handling of line ending characters in `replace` (`r`) ([#10786](https://github.com/helix-editor/helix/pull/10786)) +- Use the selected register as a history register for `rename_symbol` (`r`) ([#10932](https://github.com/helix-editor/helix/pull/10932)) +- Use the configured insert-mode cursor for prompt entry ([#10945](https://github.com/helix-editor/helix/pull/10945)) +- Add tilted quotes to the matching brackets list ([#10971](https://github.com/helix-editor/helix/pull/10971)) +- Prevent improper files like `/dev/urandom` from being used as file arguments ([#10733](https://github.com/helix-editor/helix/pull/10733)) +- Allow multiple language servers to provide `:lsp-workspace-command`s ([#10176](https://github.com/helix-editor/helix/pull/10176), [#11105](https://github.com/helix-editor/helix/pull/11105)) +- Trim output of commands executed through `:pipe` ([#10952](https://github.com/helix-editor/helix/pull/10952)) + +Fixes: + +- Use `lldb-dap` instead of `lldb-vscode` in default DAP configuration ([#10091](https://github.com/helix-editor/helix/pull/10091)) +- Fix creation of uneven splits when closing windows ([#10004](https://github.com/helix-editor/helix/pull/10004)) +- Avoid setting a register in `delete_selection_noyank`, fixing the command's use in command sequences ([#10050](https://github.com/helix-editor/helix/pull/10050), [#10148](https://github.com/helix-editor/helix/pull/10148)) +- Fix jump alphabet config resetting when using `:config-reload` ([#10156](https://github.com/helix-editor/helix/pull/10156)) +- Overlay LSP unnecessary/deprecated diagnostic tag highlights onto regular diagnostic highlights ([#10084](https://github.com/helix-editor/helix/pull/10084)) +- Fix crash on LSP text edits with invalid ranges ([#9649](https://github.com/helix-editor/helix/pull/9649)) +- Handle partial failure when sending multiple LSP `textDocument/didSave` notifications ([#10168](https://github.com/helix-editor/helix/pull/10168)) +- Fix off-by-one error for completion-replace option ([#10279](https://github.com/helix-editor/helix/pull/10279)) +- Fix mouse right-click selection behavior ([#10067](https://github.com/helix-editor/helix/pull/10067)) +- Fix scrolling to the end within a popup ([#10181](https://github.com/helix-editor/helix/pull/10181)) +- Fix jump label highlight locations when jumping in non-ascii text ([#10317](https://github.com/helix-editor/helix/pull/10317)) +- Fix crashes from tree-sitter query captures that return non-grapheme aligned ranges ([#10310](https://github.com/helix-editor/helix/pull/10310)) +- Include VCS change in `mi`/`ma` textobject infobox ([#10496](https://github.com/helix-editor/helix/pull/10496)) +- Override crossterm's support for `NO_COLOR` ([#10514](https://github.com/helix-editor/helix/pull/10514)) +- Respect mode when starting a search ([#10505](https://github.com/helix-editor/helix/pull/10505)) +- Simplify first-in-line computation for indent queries ([#10527](https://github.com/helix-editor/helix/pull/10527)) +- Ignore .svn version controlled files in file pickers ([#10536](https://github.com/helix-editor/helix/pull/10536)) +- Fix overloading language servers with `completionItem/resolve` requests ([38ee845](https://github.com/helix-editor/helix/commit/38ee845), [#10873](https://github.com/helix-editor/helix/pull/10873)) +- Specify direction for `select_next_sibling` / `select_prev_sibling` ([#10542](https://github.com/helix-editor/helix/pull/10542)) +- Fix restarting language servers ([#10614](https://github.com/helix-editor/helix/pull/10614)) +- Don't stop at the first URL in `goto_file` ([#10622](https://github.com/helix-editor/helix/pull/10622)) +- Fix overflows in window size calculations for small terminals ([#10620](https://github.com/helix-editor/helix/pull/10620)) +- Allow missing or empty completion lists in DAP ([#10332](https://github.com/helix-editor/helix/pull/10332)) +- Revert statusline refactor that could cause the statusline to blank out on files with long paths ([#10642](https://github.com/helix-editor/helix/pull/10642)) +- Synchronize files after writing ([#10735](https://github.com/helix-editor/helix/pull/10735)) +- Avoid `cnorm` for cursor-type detection in certain terminals ([#10769](https://github.com/helix-editor/helix/pull/10769)) +- Reset inlay hints when stopping or restarting a language server ([#10741](https://github.com/helix-editor/helix/pull/10741)) +- Fix logic for updating `--version` when development VCS HEAD changes ([#10896](https://github.com/helix-editor/helix/pull/10896)) +- Set a max value for the count ([#10930](https://github.com/helix-editor/helix/pull/10930)) +- Deserialize number IDs in DAP module types ([#10943](https://github.com/helix-editor/helix/pull/10943)) +- Fix the behavior of `jump_backwords` when the jumplist is at capacity ([#10968](https://github.com/helix-editor/helix/pull/10968)) +- Fix injection layer heritage tracking for reused tree-sitter injection layers ([#1098](https://github.com/helix-editor/helix/pull/1098)) +- Fix pluralization of "buffers" in the statusline for `:q`, `:q!`, `:wq` ([#11018](https://github.com/helix-editor/helix/pull/11018)) +- Declare LSP formatting client capabilities ([#11064](https://github.com/helix-editor/helix/pull/11064)) +- Commit uncommitted changes before attempting undo/earlier ([#11090](https://github.com/helix-editor/helix/pull/11090)) +- Expand tilde for selected paths in `goto_file` ([#10964](https://github.com/helix-editor/helix/pull/10964)) +- Commit undo checkpoints before `:write[-all]`, fixing the modification indicator ([#11062](https://github.com/helix-editor/helix/pull/11062)) + +Themes: + +- Add jump label styles to `nightfox` ([#10052](https://github.com/helix-editor/helix/pull/10052)) +- Add jump label styles to Solarized themes ([#10056](https://github.com/helix-editor/helix/pull/10056)) +- Add jump label styles to `cyan_light` ([#10058](https://github.com/helix-editor/helix/pull/10058)) +- Add jump label styles to `onelight` ([#10061](https://github.com/helix-editor/helix/pull/10061)) +- Add `flexoki-dark` and `flexoki-light` ([#10002](https://github.com/helix-editor/helix/pull/10002)) +- Add default theme keys for LSP diagnostics tags to existing themes ([#10064](https://github.com/helix-editor/helix/pull/10064)) +- Add jump label styles to base16 themes ([#10076](https://github.com/helix-editor/helix/pull/10076)) +- Dim primary selection in `kanagawa` ([#10094](https://github.com/helix-editor/helix/pull/10094), [#10500](https://github.com/helix-editor/helix/pull/10500)) +- Add jump label styles to tokyonight themes ([#10106](https://github.com/helix-editor/helix/pull/10106)) +- Add jump label styles to papercolor themes ([#10104](https://github.com/helix-editor/helix/pull/10104)) +- Add jump label styles to Darcula themes ([#10116](https://github.com/helix-editor/helix/pull/10116)) +- Add jump label styles to `autumn` ([#10134](https://github.com/helix-editor/helix/pull/10134)) +- Add jump label styles to Ayu themes ([#10133](https://github.com/helix-editor/helix/pull/10133)) +- Add jump label styles to `dark_high_contrast` ([#10133](https://github.com/helix-editor/helix/pull/10133)) +- Update material themes ([#10290](https://github.com/helix-editor/helix/pull/10290)) +- Add jump label styles to `varua` ([#10299](https://github.com/helix-editor/helix/pull/10299)) +- Add ruler style to `adwaita-dark` ([#10260](https://github.com/helix-editor/helix/pull/10260)) +- Remove `ui.highlight` effects from `solarized_dark` ([#10261](https://github.com/helix-editor/helix/pull/10261)) +- Fix statusline color in material themes ([#10308](https://github.com/helix-editor/helix/pull/10308)) +- Brighten `nord` selection highlight ([#10307](https://github.com/helix-editor/helix/pull/10307)) +- Add inlay-hint styles to monokai themes ([#10334](https://github.com/helix-editor/helix/pull/10334)) +- Add bufferline and cursorline colors to `vim_dark_high_contrast` ([#10444](https://github.com/helix-editor/helix/pull/10444)) +- Switch themes with foreground rulers to background ([#10309](https://github.com/helix-editor/helix/pull/10309)) +- Fix statusline colors for `everblush` ([#10394](https://github.com/helix-editor/helix/pull/10394)) +- Use `yellow1` for `gruvbox` warning diagnostics ([#10506](https://github.com/helix-editor/helix/pull/10506)) +- Add jump label styles to Modus themes ([#10538](https://github.com/helix-editor/helix/pull/10538)) +- Refactor `dark_plus` and switch maintainers ([#10543](https://github.com/helix-editor/helix/pull/10543), [#10574](https://github.com/helix-editor/helix/pull/10574)) +- Add debug highlights to `dark_plus` ([#10593](https://github.com/helix-editor/helix/pull/10593)) +- Fix per-mode cursor colors in the default theme ([#10608](https://github.com/helix-editor/helix/pull/10608)) +- Add `tag` and `attribute` highlights to `dark_high_contrast` ([#10705](https://github.com/helix-editor/helix/pull/10705)) +- Improve readability of virtual text with `noctis` theme ([#10910](https://github.com/helix-editor/helix/pull/10910)) +- Sync `catppuccin` themes with upstream ([#10954](https://github.com/helix-editor/helix/pull/10954)) +- Improve jump colors for `github_dark` themes ([#10946](https://github.com/helix-editor/helix/pull/10946)) +- Add modeline and default virtual highlights to `base16_default` ([#10858](https://github.com/helix-editor/helix/pull/10858)) +- Add `iroaseta` ([#10381](https://github.com/helix-editor/helix/pull/10381)) +- Refactor `gruvbox` ([#10773](https://github.com/helix-editor/helix/pull/10773), [#11071](https://github.com/helix-editor/helix/pull/11071)) +- Add cursorcolumn and cursorline to `base16_transparent` ([#11099](https://github.com/helix-editor/helix/pull/11099)) +- Update cursorline color for `fleet_dark` ([#11046](https://github.com/helix-editor/helix/pull/11046)) +- Add `kanagawa-dragon` ([#10172](https://github.com/helix-editor/helix/pull/10172)) + +New languages: + +- BitBake ([#10010](https://github.com/helix-editor/helix/pull/10010)) +- Earthfile ([#10111](https://github.com/helix-editor/helix/pull/10111), [#10489](https://github.com/helix-editor/helix/pull/10489), [#10779](https://github.com/helix-editor/helix/pull/10779)) +- TCL ([#9837](https://github.com/helix-editor/helix/pull/9837)) +- ADL ([#10029](https://github.com/helix-editor/helix/pull/10029)) +- LDIF ([#10330](https://github.com/helix-editor/helix/pull/10330)) +- XTC ([#10448](https://github.com/helix-editor/helix/pull/10448)) +- Move ([f06a166](https://github.com/helix-editor/helix/commit/f06a166)) +- Pest ([#10616](https://github.com/helix-editor/helix/pull/10616)) +- GJS/GTS ([#9940](https://github.com/helix-editor/helix/pull/9940)) +- Inko ([#10656](https://github.com/helix-editor/helix/pull/10656)) +- Mojo ([#10743](https://github.com/helix-editor/helix/pull/10743)) +- Elisp ([#10644](https://github.com/helix-editor/helix/pull/10644)) + +Updated languages and queries: + +- Recognize `mkdn` files as markdown ([#10065](https://github.com/helix-editor/helix/pull/10065)) +- Add comment injections for Gleam ([#10062](https://github.com/helix-editor/helix/pull/10062)) +- Recognize BuildKite commands in YAML injections ([#10090](https://github.com/helix-editor/helix/pull/10090)) +- Add F# block comment token configuration ([#10108](https://github.com/helix-editor/helix/pull/10108)) +- Update tree-sitter-templ and queries ([#10114](https://github.com/helix-editor/helix/pull/10114)) +- Recognize `Tiltfile` as Starlark ([#10072](https://github.com/helix-editor/helix/pull/10072)) +- Remove `todo.txt` from files recognized as todotxt ([5fece00](https://github.com/helix-editor/helix/commit/5fece00)) +- Highlight `type` keyword in Python from PEP695 ([#10165](https://github.com/helix-editor/helix/pull/10165)) +- Update tree-sitter-koka, add language server config ([#10119](https://github.com/helix-editor/helix/pull/10119)) +- Recognize node and Python history files ([#10120](https://github.com/helix-editor/helix/pull/10120)) +- Recognize more shell files as bash ([#10120](https://github.com/helix-editor/helix/pull/10120)) +- Recognize the bun shebang as typescript ([#10120](https://github.com/helix-editor/helix/pull/10120)) +- Add a configuration for the angular language server ([#10166](https://github.com/helix-editor/helix/pull/10166)) +- Add textobject queries for Solidity ([#10318](https://github.com/helix-editor/helix/pull/10318)) +- Recognize `meson.options` as Meson ([#10323](https://github.com/helix-editor/helix/pull/10323)) +- Improve Solidity highlighting ([4fc0a4d](https://github.com/helix-editor/helix/commit/4fc0a4d)) +- Recognize `_.tpl` files as Helm ([#10344](https://github.com/helix-editor/helix/pull/10344)) +- Update tree-sitter-ld and highlights ([#10379](https://github.com/helix-editor/helix/pull/10379)) +- Add `lldb-dap` configuration for Odin ([#10175](https://github.com/helix-editor/helix/pull/10175)) +- Update tree-sitter-rust ([#10365](https://github.com/helix-editor/helix/pull/10365)) +- Update tree-sitter-typst ([#10321](https://github.com/helix-editor/helix/pull/10321)) +- Recognize `hyprpaper.conf`, `hypridle.conf` and `hyprlock.conf` as Hyprlang ([#10383](https://github.com/helix-editor/helix/pull/10383)) +- Improve HTML highlighting ([#10503](https://github.com/helix-editor/helix/pull/10503)) +- Add `rust-script` and `cargo` as shebangs for Rust ([#10484](https://github.com/helix-editor/helix/pull/10484)) +- Fix precedence of tag highlights in Svelte ([#10487](https://github.com/helix-editor/helix/pull/10487)) +- Update tree-sitter-bash ([#10526](https://github.com/helix-editor/helix/pull/10526)) +- Recognize `*.ignore` files as ignore ([#10579](https://github.com/helix-editor/helix/pull/10579)) +- Add configuration to enable inlay hints in metals ([#10597](https://github.com/helix-editor/helix/pull/10597)) +- Enable highlighting private members in ECMA languages ([#10554](https://github.com/helix-editor/helix/pull/10554)) +- Add comment injection to typst queries ([#10628](https://github.com/helix-editor/helix/pull/10628)) +- Add textobject queries for Hurl ([#10594](https://github.com/helix-editor/helix/pull/10594)) +- Add `try` keyword to Rust ([#10641](https://github.com/helix-editor/helix/pull/10641)) +- Add `is not` and `not in` to Python highlights ([#10647](https://github.com/helix-editor/helix/pull/10647)) +- Remove ' and ⟨⟩ from Lean autopair configuration ([#10688](https://github.com/helix-editor/helix/pull/10688)) +- Match TOML/YAML highlights for JSON keys ([#10676](https://github.com/helix-editor/helix/pull/10676)) +- Recognize WORKSPACE files as Starlark ([#10713](https://github.com/helix-editor/helix/pull/10713)) +- Switch Odin tree-sitter grammar and highlights ([#10698](https://github.com/helix-editor/helix/pull/10698)) +- Update `tree-sitter-slint` ([#10749](https://github.com/helix-editor/helix/pull/10749)) +- Add missing operators for Solidity highlights ([#10735](https://github.com/helix-editor/helix/pull/10735)) +- Update `tree-sitter-inko` ([#10805](https://github.com/helix-editor/helix/pull/10805)) +- Add `py`, `hs`, `rs` and `typ` injection regexes ([#10785](https://github.com/helix-editor/helix/pull/10785)) +- Update Swift grammar and queries ([#10802](https://github.com/helix-editor/helix/pull/10802)) +- Update Cairo grammar and queries ([#10919](https://github.com/helix-editor/helix/pull/10919), [#11067](https://github.com/helix-editor/helix/pull/11067)) +- Update Rust grammar ([#10973](https://github.com/helix-editor/helix/pull/10973)) +- Add block comment tokens for typst ([#10955](https://github.com/helix-editor/helix/pull/10955)) +- Recognize `jsonl` as JSON ([#11004](https://github.com/helix-editor/helix/pull/11004)) +- Add rulers and text-width at 100 columns for Lean language ([#10969](https://github.com/helix-editor/helix/pull/10969)) +- Improve VDHL highlights ([#10845](https://github.com/helix-editor/helix/pull/10845)) +- Recognize `hsc` as Haskell ([#11074](https://github.com/helix-editor/helix/pull/11074)) +- Fix heredoc and `$''` highlights in Bash ([#11118](https://github.com/helix-editor/helix/pull/11118)) +- Add LSP configuration for `basedpyright` ([#11121](https://github.com/helix-editor/helix/pull/11121)) +- Recognize `npmrc` and `.nmprc` files as INI ([#11131](https://github.com/helix-editor/helix/pull/11131)) +- Recognize `~/.config/git/ignore` as git-ignore ([#11131](https://github.com/helix-editor/helix/pull/11131)) +- Recognize `pdm.lock` and `uv.lock` as TOML ([#11131](https://github.com/helix-editor/helix/pull/11131)) +- Recognize `.yml` as well as `.yaml` for Helm chart templates ([#11135](https://github.com/helix-editor/helix/pull/11135)) +- Add regex injections for Bash ([#11112](https://github.com/helix-editor/helix/pull/11112)) +- Update tree-sitter-todo ([#11097](https://github.com/helix-editor/helix/pull/11097)) + +Packaging: + +- Make `Helix.appdata.xml` spec-compliant ([#10051](https://github.com/helix-editor/helix/pull/10051)) +- Expose all flake outputs through flake-compat ([#10673](https://github.com/helix-editor/helix/pull/10673)) +- Bump the MSRV to 1.74.0 ([#10714](https://github.com/helix-editor/helix/pull/10714)) +- Improve FiSH completions ([#10853](https://github.com/helix-editor/helix/pull/10853)) +- Improve ZSH completions ([#10853](https://github.com/helix-editor/helix/pull/10853)) + # 24.03 (2024-03-30) As always, a big thank you to all of the contributors! This release saw changes from 125 contributors. diff --git a/Cargo.lock b/Cargo.lock index 7c797aaca..c614cadf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "helix-core" -version = "24.3.0" +version = "24.7.0" dependencies = [ "ahash", "arc-swap", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "helix-dap" -version = "24.3.0" +version = "24.7.0" dependencies = [ "anyhow", "fern", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "helix-event" -version = "24.3.0" +version = "24.7.0" dependencies = [ "ahash", "anyhow", @@ -1370,7 +1370,7 @@ dependencies = [ [[package]] name = "helix-loader" -version = "24.3.0" +version = "24.7.0" dependencies = [ "anyhow", "cc", @@ -1389,7 +1389,7 @@ dependencies = [ [[package]] name = "helix-lsp" -version = "24.3.0" +version = "24.7.0" dependencies = [ "anyhow", "arc-swap", @@ -1413,11 +1413,11 @@ dependencies = [ [[package]] name = "helix-parsec" -version = "24.3.0" +version = "24.7.0" [[package]] name = "helix-stdx" -version = "24.3.0" +version = "24.7.0" dependencies = [ "bitflags 2.6.0", "dunce", @@ -1432,7 +1432,7 @@ dependencies = [ [[package]] name = "helix-term" -version = "24.3.0" +version = "24.7.0" dependencies = [ "anyhow", "arc-swap", @@ -1475,7 +1475,7 @@ dependencies = [ [[package]] name = "helix-tui" -version = "24.3.0" +version = "24.7.0" dependencies = [ "bitflags 2.6.0", "cassowary", @@ -1491,7 +1491,7 @@ dependencies = [ [[package]] name = "helix-vcs" -version = "24.3.0" +version = "24.7.0" dependencies = [ "anyhow", "arc-swap", @@ -1507,7 +1507,7 @@ dependencies = [ [[package]] name = "helix-view" -version = "24.3.0" +version = "24.7.0" dependencies = [ "anyhow", "arc-swap", @@ -2925,7 +2925,7 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "xtask" -version = "24.3.0" +version = "24.7.0" dependencies = [ "helix-core", "helix-loader", diff --git a/Cargo.toml b/Cargo.toml index e3ee10319..9be265fc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ slotmap = "1.0.7" thiserror = "1.0" [workspace.package] -version = "24.3.0" +version = "24.7.0" edition = "2021" authors = ["Blaž Hrastnik "] categories = ["editor"] diff --git a/contrib/Helix.appdata.xml b/contrib/Helix.appdata.xml index a46ef796b..90b3d415a 100644 --- a/contrib/Helix.appdata.xml +++ b/contrib/Helix.appdata.xml @@ -47,6 +47,9 @@ + + https://github.com/helix-editor/helix/releases/tag/24.07 + https://helix-editor.com/news/release-24-03-highlights/ From 079f544260f4f5eaff08104bf07abd57bfb7b611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 15 Jul 2024 01:19:09 +0900 Subject: [PATCH 099/200] Adjust the ruler color of the default theme --- theme.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.toml b/theme.toml index 81c1a91d7..c1e5883d0 100644 --- a/theme.toml +++ b/theme.toml @@ -56,7 +56,7 @@ label = "honey" "ui.text.focus" = { fg = "white" } "ui.text.inactive" = "sirocco" "ui.virtual" = { fg = "comet" } -"ui.virtual.ruler" = { bg = "revolver" } +"ui.virtual.ruler" = { bg = "bossanova" } "ui.virtual.jump-label" = { fg = "apricot", modifiers = ["bold"] } "ui.virtual.indent-guide" = { fg = "comet" } From 1bad3b0dd4eca1cf85b1892eebc0d21699ee062d Mon Sep 17 00:00:00 2001 From: arcofx <105167958+arcofx@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:27:42 +0300 Subject: [PATCH 100/200] Make `format_selections` respect document configuration (#11169) --- helix-term/src/commands.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f25e8254c..69496eb61 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4523,7 +4523,11 @@ fn format_selections(cx: &mut Context) { .text_document_range_formatting( doc.identifier(), range, - lsp::FormattingOptions::default(), + lsp::FormattingOptions { + tab_size: doc.tab_width() as u32, + insert_spaces: matches!(doc.indent_style, IndentStyle::Spaces(_)), + ..Default::default() + }, None, ) .unwrap(); From 7f77d95c795a91f0c2b584c6f2f0d223d59b9c25 Mon Sep 17 00:00:00 2001 From: Masanori Ogino <167209+omasanori@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:28:23 +0900 Subject: [PATCH 101/200] Inject the comment grammar into Hare (#11173) --- runtime/queries/hare/injections.scm | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 runtime/queries/hare/injections.scm diff --git a/runtime/queries/hare/injections.scm b/runtime/queries/hare/injections.scm new file mode 100644 index 000000000..321c90add --- /dev/null +++ b/runtime/queries/hare/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) From 702a96d417c4654c066238f2ae214a58ab6fed9a Mon Sep 17 00:00:00 2001 From: Emran Ramezan Date: Mon, 15 Jul 2024 14:29:14 +0100 Subject: [PATCH 102/200] Update highlights.scm and injections.scm for blade.php files (#11138) * Update highlights.scm for blade.php files * Update injections.scm to add tree-sitter-comment injection * Fixed the injection issues regarding blade parameters --- runtime/queries/blade/highlights.scm | 4 ++++ runtime/queries/blade/injections.scm | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/runtime/queries/blade/highlights.scm b/runtime/queries/blade/highlights.scm index b3d442a05..04a4b9657 100644 --- a/runtime/queries/blade/highlights.scm +++ b/runtime/queries/blade/highlights.scm @@ -2,3 +2,7 @@ (directive_start) @tag (directive_end) @tag (comment) @comment +[ + (bracket_start) + (bracket_end) +] @punctuation.bracket diff --git a/runtime/queries/blade/injections.scm b/runtime/queries/blade/injections.scm index 4c6367349..77cf9eef8 100644 --- a/runtime/queries/blade/injections.scm +++ b/runtime/queries/blade/injections.scm @@ -1,9 +1,14 @@ ((text) @injection.content (#set! injection.combined) - (#set! injection.language php)) + (#set! injection.language "php")) + +((comment) @injection.content + (#set! injection.language "comment")) ((php_only) @injection.content - (#set! injection.language php-only)) -((parameter) @injection.content - (#set! injection.language php-only)) + (#set! injection.language "php-only")) + +((parameter) @injection.content + (#set! injection.include-children) + (#set! injection.language "php-only")) From dae3841a75fc521c9e5d63ff8f3ffa32be4e41e1 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 15 Feb 2024 15:13:44 -0500 Subject: [PATCH 103/200] Use an AsyncHook for picker preview highlighting The picker previously used the IdleTimeout event as a trigger for syntax-highlighting the currently selected document in the preview pane. This is a bit ad-hoc now that the event system has landed and we can refactor towards an AsyncHook (like those used for LSP completion and signature-help). This should resolve some odd scenarios where the preview did not highlight because of a race between the idle timeout and items appearing in the picker. --- helix-term/src/ui/picker.rs | 147 ++++++++------------------- helix-term/src/ui/picker/handlers.rs | 117 +++++++++++++++++++++ 2 files changed, 161 insertions(+), 103 deletions(-) create mode 100644 helix-term/src/ui/picker/handlers.rs diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index b8ec57d51..cc86a4fad 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,3 +1,5 @@ +mod handlers; + use crate::{ alt, compositor::{self, Component, Compositor, Context, Event, EventResult}, @@ -10,9 +12,11 @@ EditorView, }, }; -use futures_util::{future::BoxFuture, FutureExt}; +use futures_util::future::BoxFuture; +use helix_event::AsyncHook; use nucleo::pattern::CaseMatching; use nucleo::{Config, Nucleo, Utf32String}; +use tokio::sync::mpsc::Sender; use tui::{ buffer::Buffer as Surface, layout::Constraint, @@ -25,7 +29,7 @@ use std::{ collections::HashMap, io::Read, - path::PathBuf, + path::{Path, PathBuf}, sync::{ atomic::{self, AtomicBool}, Arc, @@ -36,7 +40,6 @@ use helix_core::{ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction, text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position, - Syntax, }; use helix_view::{ editor::Action, @@ -46,9 +49,12 @@ Document, DocumentId, Editor, }; -pub const ID: &str = "picker"; +use self::handlers::PreviewHighlightHandler; + use super::{menu::Item, overlay::Overlay}; +pub const ID: &str = "picker"; + pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; /// Biggest file size to preview in bytes pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; @@ -56,14 +62,14 @@ #[derive(PartialEq, Eq, Hash)] pub enum PathOrId { Id(DocumentId), - Path(PathBuf), + Path(Arc), } impl PathOrId { fn get_canonicalized(self) -> Self { use PathOrId::*; match self { - Path(path) => Path(helix_stdx::path::canonicalize(path)), + Path(path) => Path(helix_stdx::path::canonicalize(&path).into()), Id(id) => Id(id), } } @@ -71,7 +77,7 @@ fn get_canonicalized(self) -> Self { impl From for PathOrId { fn from(v: PathBuf) -> Self { - Self::Path(v) + Self::Path(v.as_path().into()) } } @@ -197,10 +203,12 @@ pub struct Picker { pub truncate_start: bool, /// Caches paths to documents - preview_cache: HashMap, + preview_cache: HashMap, CachedPreview>, read_buffer: Vec, /// Given an item in the picker, return the file path and line number to display. file_fn: Option>, + /// An event handler for syntax highlighting the currently previewed file. + preview_highlight_handler: Sender>, } impl Picker { @@ -280,6 +288,7 @@ fn with( preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: None, + preview_highlight_handler: PreviewHighlightHandler::::default().spawn(), } } @@ -403,16 +412,26 @@ fn get_preview<'picker, 'editor>( ) -> Preview<'picker, 'editor> { match path_or_id { PathOrId::Path(path) => { - let path = &path; - if let Some(doc) = editor.document_by_path(path) { + if let Some(doc) = editor.document_by_path(&path) { return Preview::EditorDocument(doc); } - if self.preview_cache.contains_key(path) { - return Preview::Cached(&self.preview_cache[path]); + if self.preview_cache.contains_key(&path) { + let preview = &self.preview_cache[&path]; + match preview { + // If the document isn't highlighted yet, attempt to highlight it. + CachedPreview::Document(doc) if doc.language_config().is_none() => { + helix_event::send_blocking( + &self.preview_highlight_handler, + path.clone(), + ); + } + _ => (), + } + return Preview::Cached(preview); } - let data = std::fs::File::open(path).and_then(|file| { + let data = std::fs::File::open(&path).and_then(|file| { let metadata = file.metadata()?; // Read up to 1kb to detect the content type let n = file.take(1024).read_to_end(&mut self.read_buffer)?; @@ -427,14 +446,21 @@ fn get_preview<'picker, 'editor>( (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => { CachedPreview::LargeFile } - _ => Document::open(path, None, None, editor.config.clone()) - .map(|doc| CachedPreview::Document(Box::new(doc))) + _ => Document::open(&path, None, None, editor.config.clone()) + .map(|doc| { + // Asynchronously highlight the new document + helix_event::send_blocking( + &self.preview_highlight_handler, + path.clone(), + ); + CachedPreview::Document(Box::new(doc)) + }) .unwrap_or(CachedPreview::NotFound), }, ) .unwrap_or(CachedPreview::NotFound); - self.preview_cache.insert(path.to_owned(), preview); - Preview::Cached(&self.preview_cache[path]) + self.preview_cache.insert(path.clone(), preview); + Preview::Cached(&self.preview_cache[&path]) } PathOrId::Id(id) => { let doc = editor.documents.get(&id).unwrap(); @@ -443,84 +469,6 @@ fn get_preview<'picker, 'editor>( } } - fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult { - let Some((current_file, _)) = self.current_file(cx.editor) else { - return EventResult::Consumed(None); - }; - - // Try to find a document in the cache - let doc = match ¤t_file { - PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id), - PathOrId::Path(path) => match self.preview_cache.get_mut(path) { - Some(CachedPreview::Document(ref mut doc)) => doc, - _ => return EventResult::Consumed(None), - }, - }; - - let mut callback: Option = None; - - // Then attempt to highlight it if it has no language set - if doc.language_config().is_none() { - if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader.load()) - { - doc.language = Some(language_config.clone()); - let text = doc.text().clone(); - let loader = cx.editor.syn_loader.clone(); - let job = tokio::task::spawn_blocking(move || { - let syntax = language_config - .highlight_config(&loader.load().scopes()) - .and_then(|highlight_config| { - Syntax::new(text.slice(..), highlight_config, loader) - }); - let callback = move |editor: &mut Editor, compositor: &mut Compositor| { - let Some(syntax) = syntax else { - log::info!("highlighting picker item failed"); - return; - }; - let picker = match compositor.find::>() { - Some(Overlay { content, .. }) => Some(content), - None => compositor - .find::>>() - .map(|overlay| &mut overlay.content.file_picker), - }; - let Some(picker) = picker else { - log::info!("picker closed before syntax highlighting finished"); - return; - }; - // Try to find a document in the cache - let doc = match current_file { - PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id), - PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) { - Some(CachedPreview::Document(ref mut doc)) => { - let diagnostics = Editor::doc_diagnostics( - &editor.language_servers, - &editor.diagnostics, - doc, - ); - doc.replace_diagnostics(diagnostics, &[], None); - doc - } - _ => return, - }, - }; - doc.syntax = Some(syntax); - }; - Callback::EditorCompositor(Box::new(callback)) - }); - let tmp: compositor::Callback = Box::new(move |_, ctx| { - ctx.jobs - .callback(job.map(|res| res.map_err(anyhow::Error::from))) - }); - callback = Some(Box::new(tmp)) - } - } - - // QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now - // but it could be interesting in the future - - EventResult::Consumed(callback) - } - fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let status = self.matcher.tick(10); let snapshot = self.matcher.snapshot(); @@ -826,9 +774,6 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { } fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { - if let Event::IdleTimeout = event { - return self.handle_idle_timeout(ctx); - } // TODO: keybinds for scrolling preview let key_event = match event { @@ -861,9 +806,6 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { EventResult::Consumed(Some(callback)) }; - // So that idle timeout retriggers - ctx.editor.reset_idle_timer(); - match key_event { shift!(Tab) | key!(Up) | ctrl!('p') => { self.move_by(1, Direction::Backward); @@ -989,7 +931,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { cx.jobs.callback(async move { let new_options = new_options.await?; - let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { + let callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { // Wrapping of pickers in overlay is done outside the picker code, // so this is fragile and will break if wrapped in some other widget. let picker = match compositor.find_id::>>(ID) { @@ -997,7 +939,6 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { None => return, }; picker.set_options(new_options); - editor.reset_idle_timer(); })); anyhow::Ok(callback) }); diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs new file mode 100644 index 000000000..7a77efa44 --- /dev/null +++ b/helix-term/src/ui/picker/handlers.rs @@ -0,0 +1,117 @@ +use std::{path::Path, sync::Arc, time::Duration}; + +use helix_event::AsyncHook; +use tokio::time::Instant; + +use crate::{ + job, + ui::{menu::Item, overlay::Overlay}, +}; + +use super::{CachedPreview, DynamicPicker, Picker}; + +pub(super) struct PreviewHighlightHandler { + trigger: Option>, + phantom_data: std::marker::PhantomData, +} + +impl Default for PreviewHighlightHandler { + fn default() -> Self { + Self { + trigger: None, + phantom_data: Default::default(), + } + } +} + +impl AsyncHook for PreviewHighlightHandler { + type Event = Arc; + + fn handle_event( + &mut self, + path: Self::Event, + timeout: Option, + ) -> Option { + if self + .trigger + .as_ref() + .is_some_and(|trigger| trigger == &path) + { + // If the path hasn't changed, don't reset the debounce + timeout + } else { + self.trigger = Some(path); + Some(Instant::now() + Duration::from_millis(150)) + } + } + + fn finish_debounce(&mut self) { + let Some(path) = self.trigger.take() else { + return; + }; + + job::dispatch_blocking(move |editor, compositor| { + let picker = match compositor.find::>>() { + Some(Overlay { content, .. }) => content, + None => match compositor.find::>>() { + Some(Overlay { content, .. }) => &mut content.file_picker, + None => return, + }, + }; + + let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) + else { + return; + }; + + if doc.language_config().is_some() { + return; + } + + let Some(language_config) = doc.detect_language_config(&editor.syn_loader.load()) + else { + return; + }; + doc.language = Some(language_config.clone()); + let text = doc.text().clone(); + let loader = editor.syn_loader.clone(); + + tokio::task::spawn_blocking(move || { + let Some(syntax) = language_config + .highlight_config(&loader.load().scopes()) + .and_then(|highlight_config| { + helix_core::Syntax::new(text.slice(..), highlight_config, loader) + }) + else { + log::info!("highlighting picker item failed"); + return; + }; + + job::dispatch_blocking(move |editor, compositor| { + let picker = match compositor.find::>>() { + Some(Overlay { content, .. }) => Some(content), + None => compositor + .find::>>() + .map(|overlay| &mut overlay.content.file_picker), + }; + let Some(picker) = picker else { + log::info!("picker closed before syntax highlighting finished"); + return; + }; + let Some(CachedPreview::Document(ref mut doc)) = + picker.preview_cache.get_mut(&path) + else { + return; + }; + let diagnostics = helix_view::Editor::doc_diagnostics( + &editor.language_servers, + &editor.diagnostics, + doc, + ); + doc.replace_diagnostics(diagnostics, &[], None); + doc.syntax = Some(syntax); + }); + }); + }); + } +} From f40fca88e00968a74ae17b8ae83f9ff9680671f1 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 16 Feb 2024 10:55:02 -0500 Subject: [PATCH 104/200] Refactor Picker in terms of columns `menu::Item` is replaced with column configurations for each picker which control how a column is displayed and whether it is passed to nucleo for filtering. (This is used for dynamic pickers so that we can filter those items with the dynamic picker callback rather than nucleo.) The picker has a new lucene-like syntax that can be used to filter the picker only on certain criteria. If a filter is not specified, the text in the prompt applies to the picker's configured "primary" column. Adding column configurations for each picker is left for the child commit. --- book/src/themes.md | 1 + helix-term/src/commands.rs | 14 +- helix-term/src/commands/dap.rs | 18 +- helix-term/src/commands/lsp.rs | 35 ++- helix-term/src/commands/typed.rs | 18 +- helix-term/src/ui/mod.rs | 33 ++- helix-term/src/ui/picker.rs | 344 +++++++++++++++++---------- helix-term/src/ui/picker/handlers.rs | 23 +- helix-term/src/ui/prompt.rs | 6 +- 9 files changed, 309 insertions(+), 183 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index e3b95c0a7..a59df2fd7 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -297,6 +297,7 @@ #### Interface | `ui.bufferline.background` | Style for bufferline background | | `ui.popup` | Documentation popups (e.g. Space + k) | | `ui.popup.info` | Prompt for multiple key options | +| `ui.picker.header` | Column names in pickers with multiple columns | | `ui.window` | Borderlines separating splits | | `ui.help` | Description box for commands | | `ui.text` | Default text style, command prompts, popup text, etc. | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 69496eb61..5600a1e49 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2317,7 +2317,9 @@ fn format(&self, current_path: &Self::Data) -> Row { return; } - let (picker, injector) = Picker::stream(current_path); + // TODO + let columns = vec![]; + let (picker, injector) = Picker::stream(columns, current_path); let dedup_symlinks = file_picker_config.deduplicate_links; let absolute_root = search_root @@ -2420,6 +2422,7 @@ fn format(&self, current_path: &Self::Data) -> Row { let call = move |_: &mut Editor, compositor: &mut Compositor| { let picker = Picker::with_stream( picker, + 0, injector, move |cx, FileResult { path, line_num }, action| { let doc = match cx.editor.open(path, action) { @@ -2937,7 +2940,8 @@ fn format(&self, _data: &Self::Data) -> Row { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let picker = Picker::new(items, (), |cx, meta, action| { + let columns = vec![]; + let picker = Picker::new(columns, 0, items, (), |cx, meta, action| { cx.editor.switch(meta.id, action); }) .with_preview(|editor, meta| { @@ -3014,7 +3018,10 @@ fn format(&self, _data: &Self::Data) -> Row { } }; + let columns = vec![]; let picker = Picker::new( + columns, + 0, cx.editor .tree .views() @@ -3180,7 +3187,8 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let columns = vec![]; + let picker = Picker::new(columns, 0, commands, keymap, move |cx, command, _action| { let mut ctx = Context { register, count, diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 0e50377ac..da2b60dae 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -73,9 +73,14 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let picker = Picker::new(threads, thread_states, move |cx, thread, _action| { - callback_fn(cx.editor, thread) - }) + let columns = vec![]; + let picker = Picker::new( + columns, + 0, + threads, + thread_states, + move |cx, thread, _action| callback_fn(cx.editor, thread), + ) .with_preview(move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; let frame = frames.first()?; @@ -268,7 +273,11 @@ pub fn dap_launch(cx: &mut Context) { let templates = config.templates.clone(); + let columns = vec![]; + cx.push_layer(Box::new(overlaid(Picker::new( + columns, + 0, templates, (), |cx, template, _action| { @@ -736,7 +745,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let picker = Picker::new(frames, (), move |cx, frame, _action| { + let columns = vec![]; + let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| { let debugger = debugger!(cx.editor); // TODO: this should be simpler to find let pos = debugger.stack_frames[&thread_id] diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index d585e1bed..bf9747a4c 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -241,18 +241,25 @@ fn jump_to_position( } } -type SymbolPicker = Picker; +type SymbolPicker = Picker>; fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - Picker::new(symbols, current_path, move |cx, item, action| { - jump_to_location( - cx.editor, - &item.symbol.location, - item.offset_encoding, - action, - ); - }) + let columns = vec![]; + Picker::new( + columns, + 0, + symbols, + current_path, + move |cx, item, action| { + jump_to_location( + cx.editor, + &item.symbol.location, + item.offset_encoding, + action, + ); + }, + ) .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location))) .truncate_start(false) } @@ -263,11 +270,13 @@ enum DiagnosticsFormat { HideSourcePath, } +type DiagnosticsPicker = Picker; + fn diag_picker( cx: &Context, diagnostics: BTreeMap>, format: DiagnosticsFormat, -) -> Picker { +) -> DiagnosticsPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs @@ -293,7 +302,10 @@ fn diag_picker( error: cx.editor.theme.get("error"), }; + let columns = vec![]; Picker::new( + columns, + 0, flat_diag, (styles, format), move |cx, @@ -817,7 +829,8 @@ fn goto_impl( } [] => unreachable!("`locations` should be non-empty for `goto_impl`"), _locations => { - let picker = Picker::new(locations, cwdir, move |cx, location, action| { + let columns = vec![]; + let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) }) .with_preview(move |_editor, location| Some(location_to_file_location(location))); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ed1547f1f..232e5846b 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -9,7 +9,6 @@ use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; use helix_core::{line_ending, shellwords::Shellwords}; -use helix_lsp::LanguageServerId; use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; @@ -1378,16 +1377,6 @@ fn lsp_workspace_command( return Ok(()); } - struct LsIdCommand(LanguageServerId, helix_lsp::lsp::Command); - - impl ui::menu::Item for LsIdCommand { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - self.1.title.as_str().into() - } - } - let doc = doc!(cx.editor); let ls_id_commands = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) @@ -1402,7 +1391,7 @@ fn format(&self, _data: &Self::Data) -> Row { if args.is_empty() { let commands = ls_id_commands .map(|(ls_id, command)| { - LsIdCommand( + ( ls_id, helix_lsp::lsp::Command { title: command.clone(), @@ -1415,10 +1404,13 @@ fn format(&self, _data: &Self::Data) -> Row { let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { + let columns = vec![]; let picker = ui::Picker::new( + columns, + 0, commands, (), - move |cx, LsIdCommand(ls_id, command), _action| { + move |cx, (ls_id, command), _action| { execute_lsp_command(cx.editor, *ls_id, command.clone()); }, ); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0a65b12b5..01b718d45 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -21,7 +21,7 @@ use helix_stdx::rope; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{DynamicPicker, FileLocation, Picker}; +pub use picker::{Column as PickerColumn, DynamicPicker, FileLocation, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -170,7 +170,9 @@ pub fn raw_regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker { +type FilePicker = Picker; + +pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -217,16 +219,23 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker }); log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }) + let columns = vec![]; + let picker = Picker::new( + columns, + 0, + Vec::new(), + root, + move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }, + ) .with_preview(|_editor, path| Some((path.clone().into(), None))); let injector = picker.injector(); let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index cc86a4fad..ab8e4e155 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -21,12 +21,13 @@ buffer::Buffer as Surface, layout::Constraint, text::{Span, Spans}, - widgets::{Block, BorderType, Cell, Table}, + widgets::{Block, BorderType, Cell, Row, Table}, }; use tui::widgets::Widget; use std::{ + borrow::Cow, collections::HashMap, io::Read, path::{Path, PathBuf}, @@ -49,9 +50,9 @@ Document, DocumentId, Editor, }; -use self::handlers::PreviewHighlightHandler; +use super::overlay::Overlay; -use super::{menu::Item, overlay::Overlay}; +use self::handlers::PreviewHighlightHandler; pub const ID: &str = "picker"; @@ -129,38 +130,36 @@ fn placeholder(&self) -> &str { } } -fn item_to_nucleo(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> { - let row = item.format(editor_data); - let mut cells = row.cells.iter(); - let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum()); - let cell = cells.next()?; - if let Some(cell) = cell.content.lines.first() { - for span in &cell.0 { - text.push_str(&span.content); +fn inject_nucleo_item( + injector: &nucleo::Injector, + columns: &[Column], + item: T, + editor_data: &D, +) { + let column_texts: Vec = columns + .iter() + .filter(|column| column.filter) + .map(|column| column.format_text(&item, editor_data).into()) + .collect(); + injector.push(item, |dst| { + for (i, text) in column_texts.into_iter().enumerate() { + dst[i] = text; } - } - - for cell in cells { - text.push(' '); - if let Some(cell) = cell.content.lines.first() { - for span in &cell.0 { - text.push_str(&span.content); - } - } - } - Some((item, text.into())) + }); } -pub struct Injector { +pub struct Injector { dst: nucleo::Injector, - editor_data: Arc, + columns: Arc<[Column]>, + editor_data: Arc, shutown: Arc, } -impl Clone for Injector { +impl Clone for Injector { fn clone(&self) -> Self { Injector { dst: self.dst.clone(), + columns: self.columns.clone(), editor_data: self.editor_data.clone(), shutown: self.shutown.clone(), } @@ -169,21 +168,56 @@ fn clone(&self) -> Self { pub struct InjectorShutdown; -impl Injector { +impl Injector { pub fn push(&self, item: T) -> Result<(), InjectorShutdown> { if self.shutown.load(atomic::Ordering::Relaxed) { return Err(InjectorShutdown); } - if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) { - self.dst.push(item, |dst| dst[0] = matcher_text); - } + inject_nucleo_item(&self.dst, &self.columns, item, &self.editor_data); Ok(()) } } -pub struct Picker { - editor_data: Arc, +type ColumnFormatFn = for<'a> fn(&'a T, &'a D) -> Cell<'a>; + +pub struct Column { + name: Arc, + format: ColumnFormatFn, + /// Whether the column should be passed to nucleo for matching and filtering. + /// `DynamicPicker` uses this so that the dynamic column (for example regex in + /// global search) is not used for filtering twice. + filter: bool, +} + +impl Column { + pub fn new(name: impl Into>, format: ColumnFormatFn) -> Self { + Self { + name: name.into(), + format, + filter: true, + } + } + + pub fn without_filtering(mut self) -> Self { + self.filter = false; + self + } + + fn format<'a>(&self, item: &'a T, data: &'a D) -> Cell<'a> { + (self.format)(item, data) + } + + fn format_text<'a>(&self, item: &'a T, data: &'a D) -> Cow<'a, str> { + let text: String = self.format(item, data).content.into(); + text.into() + } +} + +pub struct Picker { + columns: Arc<[Column]>, + primary_column: usize, + editor_data: Arc, shutdown: Arc, matcher: Nucleo, @@ -211,16 +245,19 @@ pub struct Picker { preview_highlight_handler: Sender>, } -impl Picker { - pub fn stream(editor_data: T::Data) -> (Nucleo, Injector) { +impl Picker { + pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injector) { + let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32; + assert!(matcher_columns > 0); let matcher = Nucleo::new( Config::DEFAULT, Arc::new(helix_event::request_redraw), None, - 1, + matcher_columns, ); let streamer = Injector { dst: matcher.injector(), + columns: columns.into(), editor_data: Arc::new(editor_data), shutown: Arc::new(AtomicBool::new(false)), }; @@ -228,24 +265,28 @@ pub fn stream(editor_data: T::Data) -> (Nucleo, Injector) { } pub fn new( + columns: Vec>, + primary_column: usize, options: Vec, - editor_data: T::Data, + editor_data: D, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { + let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32; + assert!(matcher_columns > 0); let matcher = Nucleo::new( Config::DEFAULT, Arc::new(helix_event::request_redraw), None, - 1, + matcher_columns, ); let injector = matcher.injector(); for item in options { - if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) { - injector.push(item, |dst| dst[0] = matcher_text); - } + inject_nucleo_item(&injector, &columns, item, &editor_data); } Self::with( matcher, + columns.into(), + primary_column, Arc::new(editor_data), Arc::new(AtomicBool::new(false)), callback_fn, @@ -254,18 +295,30 @@ pub fn new( pub fn with_stream( matcher: Nucleo, - injector: Injector, + primary_column: usize, + injector: Injector, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { - Self::with(matcher, injector.editor_data, injector.shutown, callback_fn) + Self::with( + matcher, + injector.columns, + primary_column, + injector.editor_data, + injector.shutown, + callback_fn, + ) } fn with( matcher: Nucleo, - editor_data: Arc, + columns: Arc<[Column]>, + default_column: usize, + editor_data: Arc, shutdown: Arc, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { + assert!(!columns.is_empty()); + let prompt = Prompt::new( "".into(), None, @@ -273,7 +326,14 @@ fn with( |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, ); + let widths = columns + .iter() + .map(|column| Constraint::Length(column.name.chars().count() as u16)) + .collect(); + Self { + columns, + primary_column: default_column, matcher, editor_data, shutdown, @@ -284,17 +344,18 @@ fn with( show_preview: true, callback_fn: Box::new(callback_fn), completion_height: 0, - widths: Vec::new(), + widths, preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: None, - preview_highlight_handler: PreviewHighlightHandler::::default().spawn(), + preview_highlight_handler: PreviewHighlightHandler::::default().spawn(), } } - pub fn injector(&self) -> Injector { + pub fn injector(&self) -> Injector { Injector { dst: self.matcher.injector(), + columns: self.columns.clone(), editor_data: self.editor_data.clone(), shutown: self.shutdown.clone(), } @@ -316,13 +377,17 @@ pub fn with_preview( self } + pub fn with_line(mut self, line: String, editor: &Editor) -> Self { + self.prompt.set_line(line, editor); + self.handle_prompt_change(); + self + } + pub fn set_options(&mut self, new_options: Vec) { self.matcher.restart(false); let injector = self.matcher.injector(); for item in new_options { - if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) { - injector.push(item, |dst| dst[0] = matcher_text); - } + inject_nucleo_item(&injector, &self.columns, item, &self.editor_data); } } @@ -376,27 +441,39 @@ pub fn selection(&self) -> Option<&T> { .map(|item| item.data) } + fn header_height(&self) -> u16 { + if self.columns.len() > 1 { + 1 + } else { + 0 + } + } + pub fn toggle_preview(&mut self) { self.show_preview = !self.show_preview; } fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { - let pattern = self.prompt.line(); - // TODO: better track how the pattern has changed - if pattern != &self.previous_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - pattern.starts_with(&self.previous_pattern), - ); - self.previous_pattern = pattern.clone(); - } + self.handle_prompt_change(); } EventResult::Consumed(None) } + fn handle_prompt_change(&mut self) { + let pattern = self.prompt.line(); + // TODO: better track how the pattern has changed + if pattern != &self.previous_pattern { + self.matcher.pattern.reparse( + 0, + pattern, + CaseMatching::Smart, + pattern.starts_with(&self.previous_pattern), + ); + self.previous_pattern = pattern.clone(); + } + } + fn current_file(&self, editor: &Editor) -> Option { self.selection() .and_then(|current| (self.file_fn.as_ref()?)(editor, current)) @@ -526,7 +603,7 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) // -- Render the contents: // subtract area of prompt from top let inner = inner.clip_top(2); - let rows = inner.height as u32; + let rows = inner.height.saturating_sub(self.header_height()) as u32; let offset = self.cursor - (self.cursor % std::cmp::max(1, rows)); let cursor = self.cursor.saturating_sub(offset); let end = offset @@ -540,83 +617,94 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) } let options = snapshot.matched_items(offset..end).map(|item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut indices, - ); - indices.sort_unstable(); - indices.dedup(); - let mut row = item.data.format(&self.editor_data); - - let mut grapheme_idx = 0u32; - let mut indices = indices.drain(..); - let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX); - if self.widths.len() < row.cells.len() { - self.widths.resize(row.cells.len(), Constraint::Length(0)); - } let mut widths = self.widths.iter_mut(); - for cell in &mut row.cells { + let mut matcher_index = 0; + + Row::new(self.columns.iter().map(|column| { let Some(Constraint::Length(max_width)) = widths.next() else { unreachable!(); }; + let mut cell = column.format(item.data, &self.editor_data); + let width = if column.filter { + snapshot.pattern().column_pattern(matcher_index).indices( + item.matcher_columns[matcher_index].slice(..), + &mut matcher, + &mut indices, + ); + indices.sort_unstable(); + indices.dedup(); + let mut indices = indices.drain(..); + let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX); + let mut span_list = Vec::new(); + let mut current_span = String::new(); + let mut current_style = Style::default(); + let mut grapheme_idx = 0u32; + let mut width = 0; - // merge index highlights on top of existing hightlights - let mut span_list = Vec::new(); - let mut current_span = String::new(); - let mut current_style = Style::default(); - let mut width = 0; - - let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice()); - for span in spans { - // this looks like a bug on first glance, we are iterating - // graphemes but treating them as char indices. The reason that - // this is correct is that nucleo will only ever consider the first char - // of a grapheme (and discard the rest of the grapheme) so the indices - // returned by nucleo are essentially grapheme indecies - for grapheme in span.content.graphemes(true) { - let style = if grapheme_idx == next_highlight_idx { - next_highlight_idx = indices.next().unwrap_or(u32::MAX); - span.style.patch(highlight_style) - } else { - span.style - }; - if style != current_style { - if !current_span.is_empty() { - span_list.push(Span::styled(current_span, current_style)) + let spans: &[Span] = + cell.content.lines.first().map_or(&[], |it| it.0.as_slice()); + for span in spans { + // this looks like a bug on first glance, we are iterating + // graphemes but treating them as char indices. The reason that + // this is correct is that nucleo will only ever consider the first char + // of a grapheme (and discard the rest of the grapheme) so the indices + // returned by nucleo are essentially grapheme indecies + for grapheme in span.content.graphemes(true) { + let style = if grapheme_idx == next_highlight_idx { + next_highlight_idx = indices.next().unwrap_or(u32::MAX); + span.style.patch(highlight_style) + } else { + span.style + }; + if style != current_style { + if !current_span.is_empty() { + span_list.push(Span::styled(current_span, current_style)) + } + current_span = String::new(); + current_style = style; } - current_span = String::new(); - current_style = style; + current_span.push_str(grapheme); + grapheme_idx += 1; } - current_span.push_str(grapheme); - grapheme_idx += 1; + width += span.width(); } - width += span.width(); - } - span_list.push(Span::styled(current_span, current_style)); + span_list.push(Span::styled(current_span, current_style)); + cell = Cell::from(Spans::from(span_list)); + matcher_index += 1; + width + } else { + cell.content + .lines + .first() + .map(|line| line.width()) + .unwrap_or_default() + }; + if width as u16 > *max_width { *max_width = width as u16; } - *cell = Cell::from(Spans::from(span_list)); - // spacer - if grapheme_idx == next_highlight_idx { - next_highlight_idx = indices.next().unwrap_or(u32::MAX); - } - grapheme_idx += 1; - } - - row + cell + })) }); - let table = Table::new(options) + let mut table = Table::new(options) .style(text_style) .highlight_style(selected) .highlight_symbol(" > ") .column_spacing(1) .widths(&self.widths); + // -- Header + if self.columns.len() > 1 { + let header_style = cx.editor.theme.get("ui.picker.header"); + + table = table.header(Row::new(self.columns.iter().map(|column| { + Cell::from(Span::styled(Cow::from(&*column.name), header_style)) + }))); + } + use tui::widgets::TableState; table.render_table( @@ -746,7 +834,7 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context } } -impl Component for Picker { +impl Component for Picker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | @@ -872,7 +960,7 @@ fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) } fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { - self.completion_height = height.saturating_sub(4); + self.completion_height = height.saturating_sub(4 + self.header_height()); Some((width, height)) } @@ -880,7 +968,7 @@ fn id(&self) -> Option<&'static str> { Some(ID) } } -impl Drop for Picker { +impl Drop for Picker { fn drop(&mut self) { // ensure we cancel any ongoing background threads streaming into the picker self.shutdown.store(true, atomic::Ordering::Relaxed) @@ -896,14 +984,14 @@ fn drop(&mut self) { /// A picker that updates its contents via a callback whenever the /// query string changes. Useful for live grep, workspace symbols, etc. -pub struct DynamicPicker { - file_picker: Picker, +pub struct DynamicPicker { + file_picker: Picker, query_callback: DynQueryCallback, query: String, } -impl DynamicPicker { - pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { +impl DynamicPicker { + pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { Self { file_picker, query_callback, @@ -912,20 +1000,22 @@ pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self } } -impl Component for DynamicPicker { +impl Component for DynamicPicker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.file_picker.render(area, surface, cx); } fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { let event_result = self.file_picker.handle_event(event, cx); - let current_query = self.file_picker.prompt.line(); + let Some(current_query) = self.file_picker.primary_query() else { + return event_result; + }; if !matches!(event, Event::IdleTimeout) || self.query == *current_query { return event_result; } - self.query.clone_from(current_query); + self.query = current_query.to_string(); let new_options = (self.query_callback)(current_query.to_owned(), cx.editor); @@ -934,7 +1024,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { let callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { // Wrapping of pickers in overlay is done outside the picker code, // so this is fragile and will break if wrapped in some other widget. - let picker = match compositor.find_id::>>(ID) { + let picker = match compositor.find_id::>(ID) { Some(overlay) => &mut overlay.content.file_picker, None => return, }; diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs index 7a77efa44..f01c982a7 100644 --- a/helix-term/src/ui/picker/handlers.rs +++ b/helix-term/src/ui/picker/handlers.rs @@ -3,19 +3,16 @@ use helix_event::AsyncHook; use tokio::time::Instant; -use crate::{ - job, - ui::{menu::Item, overlay::Overlay}, -}; +use crate::{job, ui::overlay::Overlay}; use super::{CachedPreview, DynamicPicker, Picker}; -pub(super) struct PreviewHighlightHandler { +pub(super) struct PreviewHighlightHandler { trigger: Option>, - phantom_data: std::marker::PhantomData, + phantom_data: std::marker::PhantomData<(T, D)>, } -impl Default for PreviewHighlightHandler { +impl Default for PreviewHighlightHandler { fn default() -> Self { Self { trigger: None, @@ -24,7 +21,9 @@ fn default() -> Self { } } -impl AsyncHook for PreviewHighlightHandler { +impl AsyncHook + for PreviewHighlightHandler +{ type Event = Arc; fn handle_event( @@ -51,9 +50,9 @@ fn finish_debounce(&mut self) { }; job::dispatch_blocking(move |editor, compositor| { - let picker = match compositor.find::>>() { + let picker = match compositor.find::>>() { Some(Overlay { content, .. }) => content, - None => match compositor.find::>>() { + None => match compositor.find::>>() { Some(Overlay { content, .. }) => &mut content.file_picker, None => return, }, @@ -88,10 +87,10 @@ fn finish_debounce(&mut self) { }; job::dispatch_blocking(move |editor, compositor| { - let picker = match compositor.find::>>() { + let picker = match compositor.find::>>() { Some(Overlay { content, .. }) => Some(content), None => compositor - .find::>>() + .find::>>() .map(|overlay| &mut overlay.content.file_picker), }; let Some(picker) = picker else { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 14b242df6..19183470c 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -93,11 +93,15 @@ pub fn new( } pub fn with_line(mut self, line: String, editor: &Editor) -> Self { + self.set_line(line, editor); + self + } + + pub fn set_line(&mut self, line: String, editor: &Editor) { let cursor = line.len(); self.line = line; self.cursor = cursor; self.recalculate_completion(editor); - self } pub fn with_language( From c4c17c693de839ab48fc7c43f46aa25c5473c583 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 16 Feb 2024 10:57:38 -0500 Subject: [PATCH 105/200] Add a special query syntax for Pickers to select columns Now that the picker is defined as a table, we need a way to provide input for each field in the picker. We introduce a small query syntax that supports multiple columns without being too verbose. Fields are specified as `%field pattern`. The default column for a picker doesn't need the `%field` prefix. The field name may be selected by a prefix of the field, for example `%p foo.rs` rather than `%path foo.rs`. Co-authored-by: ItsEthra <107059409+ItsEthra@users.noreply.github.com> --- helix-term/src/ui/picker.rs | 53 ++++-- helix-term/src/ui/picker/query.rs | 282 ++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 11 deletions(-) create mode 100644 helix-term/src/ui/picker/query.rs diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ab8e4e155..5e7bda21b 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,4 +1,5 @@ mod handlers; +mod query; use crate::{ alt, @@ -9,6 +10,7 @@ ui::{ self, document::{render_document, LineDecoration, LinePos, TextRenderer}, + picker::query::PickerQuery, EditorView, }, }; @@ -226,7 +228,7 @@ pub struct Picker { cursor: u32, prompt: Prompt, - previous_pattern: String, + query: PickerQuery, /// Whether to show the preview panel (default true) show_preview: bool, @@ -331,6 +333,8 @@ fn with( .map(|column| Constraint::Length(column.name.chars().count() as u16)) .collect(); + let query = PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column); + Self { columns, primary_column: default_column, @@ -339,7 +343,7 @@ fn with( shutdown, cursor: 0, prompt, - previous_pattern: String::new(), + query, truncate_start: true, show_preview: true, callback_fn: Box::new(callback_fn), @@ -441,6 +445,13 @@ pub fn selection(&self) -> Option<&T> { .map(|item| item.data) } + fn primary_query(&self) -> Arc { + self.query + .get(&self.columns[self.primary_column].name) + .cloned() + .unwrap_or_else(|| "".into()) + } + fn header_height(&self) -> u16 { if self.columns.len() > 1 { 1 @@ -461,16 +472,36 @@ fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResul } fn handle_prompt_change(&mut self) { - let pattern = self.prompt.line(); // TODO: better track how the pattern has changed - if pattern != &self.previous_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - pattern.starts_with(&self.previous_pattern), - ); - self.previous_pattern = pattern.clone(); + let line = self.prompt.line(); + let old_query = self.query.parse(line); + if self.query == old_query { + return; + } + // Have nucleo reparse each changed column. + for (i, column) in self + .columns + .iter() + .filter(|column| column.filter) + .enumerate() + { + let pattern = self + .query + .get(&column.name) + .map(|f| &**f) + .unwrap_or_default(); + let old_pattern = old_query + .get(&column.name) + .map(|f| &**f) + .unwrap_or_default(); + // Fastlane: most columns will remain unchanged after each edit. + if pattern == old_pattern { + continue; + } + let is_append = pattern.starts_with(old_pattern); + self.matcher + .pattern + .reparse(i, pattern, CaseMatching::Smart, is_append); } } diff --git a/helix-term/src/ui/picker/query.rs b/helix-term/src/ui/picker/query.rs new file mode 100644 index 000000000..89ade95ff --- /dev/null +++ b/helix-term/src/ui/picker/query.rs @@ -0,0 +1,282 @@ +use std::{collections::HashMap, mem, sync::Arc}; + +#[derive(Debug)] +pub(super) struct PickerQuery { + /// The column names of the picker. + column_names: Box<[Arc]>, + /// The index of the primary column in `column_names`. + /// The primary column is selected by default unless another + /// field is specified explicitly with `%fieldname`. + primary_column: usize, + /// The mapping between column names and input in the query + /// for those columns. + inner: HashMap, Arc>, +} + +impl PartialEq, Arc>> for PickerQuery { + fn eq(&self, other: &HashMap, Arc>) -> bool { + self.inner.eq(other) + } +} + +impl PickerQuery { + pub(super) fn new>>( + column_names: I, + primary_column: usize, + ) -> Self { + let column_names: Box<[_]> = column_names.collect(); + let inner = HashMap::with_capacity(column_names.len()); + Self { + column_names, + primary_column, + inner, + } + } + + pub(super) fn get(&self, column: &str) -> Option<&Arc> { + self.inner.get(column) + } + + pub(super) fn parse(&mut self, input: &str) -> HashMap, Arc> { + let mut fields: HashMap, String> = HashMap::new(); + let primary_field = &self.column_names[self.primary_column]; + let mut escaped = false; + let mut in_field = false; + let mut field = None; + let mut text = String::new(); + + macro_rules! finish_field { + () => { + let key = field.take().unwrap_or(primary_field); + + if let Some(pattern) = fields.get_mut(key) { + pattern.push(' '); + pattern.push_str(text.trim()); + } else { + fields.insert(key.clone(), text.trim().to_string()); + } + text.clear(); + }; + } + + for ch in input.chars() { + match ch { + // Backslash escaping + _ if escaped => { + // '%' is the only character that is special cased. + // You can escape it to prevent parsing the text that + // follows it as a field name. + if ch != '%' { + text.push('\\'); + } + text.push(ch); + escaped = false; + } + '\\' => escaped = !escaped, + '%' => { + if !text.is_empty() { + finish_field!(); + } + in_field = true; + } + ' ' if in_field => { + // Go over all columns and their indices, find all that starts with field key, + // select a column that fits key the most. + field = self + .column_names + .iter() + .filter(|col| col.starts_with(&text)) + // select "fittest" column + .min_by_key(|col| col.len()); + text.clear(); + in_field = false; + } + _ => text.push(ch), + } + } + + if !in_field && !text.is_empty() { + finish_field!(); + } + + let new_inner: HashMap<_, _> = fields + .into_iter() + .map(|(field, query)| (field, query.as_str().into())) + .collect(); + + mem::replace(&mut self.inner, new_inner) + } +} + +#[cfg(test)] +mod test { + use helix_core::hashmap; + + use super::*; + + #[test] + fn parse_query_test() { + let mut query = PickerQuery::new( + [ + "primary".into(), + "field1".into(), + "field2".into(), + "another".into(), + "anode".into(), + ] + .into_iter(), + 0, + ); + + // Basic field splitting + query.parse("hello world"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello world".into(), + ) + ); + query.parse("hello %field1 world %field2 !"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "field1".into() => "world".into(), + "field2".into() => "!".into(), + ) + ); + query.parse("%field1 abc %field2 def xyz"); + assert_eq!( + query, + hashmap!( + "field1".into() => "abc".into(), + "field2".into() => "def xyz".into(), + ) + ); + + // Trailing space is trimmed + query.parse("hello "); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + ) + ); + + // Unknown fields are trimmed. + query.parse("hello %foo"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + ) + ); + + // Multiple words in a field + query.parse("hello %field1 a b c"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "field1".into() => "a b c".into(), + ) + ); + + // Escaping + query.parse(r#"hello\ world"#); + assert_eq!( + query, + hashmap!( + "primary".into() => r#"hello\ world"#.into(), + ) + ); + query.parse(r#"hello \%field1 world"#); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello %field1 world".into(), + ) + ); + query.parse(r#"%field1 hello\ world"#); + assert_eq!( + query, + hashmap!( + "field1".into() => r#"hello\ world"#.into(), + ) + ); + query.parse(r#"hello %field1 a\"b"#); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "field1".into() => r#"a\"b"#.into(), + ) + ); + query.parse(r#"%field1 hello\ world"#); + assert_eq!( + query, + hashmap!( + "field1".into() => r#"hello\ world"#.into(), + ) + ); + query.parse(r#"\bfoo\b"#); + assert_eq!( + query, + hashmap!( + "primary".into() => r#"\bfoo\b"#.into(), + ) + ); + query.parse(r#"\\n"#); + assert_eq!( + query, + hashmap!( + "primary".into() => r#"\\n"#.into(), + ) + ); + + // Only the prefix of a field is required. + query.parse("hello %anot abc"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "another".into() => "abc".into(), + ) + ); + // The shortest matching the prefix is selected. + query.parse("hello %ano abc"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "anode".into() => "abc".into() + ) + ); + // Multiple uses of a column are concatenated with space separators. + query.parse("hello %field1 xyz %fie abc"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "field1".into() => "xyz abc".into() + ) + ); + query.parse("hello %fie abc"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello".into(), + "field1".into() => "abc".into() + ) + ); + // The primary column can be explicitly qualified. + query.parse("hello %fie abc %prim world"); + assert_eq!( + query, + hashmap!( + "primary".into() => "hello world".into(), + "field1".into() => "abc".into() + ) + ); + } +} From 385b398808b38dc7837e7e4516a9137c5b9feed0 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 3 Sep 2023 14:47:17 -0500 Subject: [PATCH 106/200] Add column configurations for existing pickers This removes the menu::Item implementations for picker item types and adds `Vec>` configurations. --- helix-term/src/commands.rs | 429 ++++++++++++++++--------------- helix-term/src/commands/dap.rs | 54 ++-- helix-term/src/commands/lsp.rs | 274 +++++++++++--------- helix-term/src/commands/typed.rs | 7 +- helix-term/src/ui/menu.rs | 14 +- helix-term/src/ui/mod.rs | 10 +- 6 files changed, 398 insertions(+), 390 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5600a1e49..f4d2c634c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,10 +10,7 @@ }; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; -use tui::{ - text::Span, - widgets::{Cell, Row}, -}; +use tui::text::Span; pub use typed::*; use helix_core::{ @@ -61,8 +58,7 @@ compositor::{self, Component, Compositor}, filter_picker_entry, job::Callback, - keymap::ReverseKeymap, - ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent}, + ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Jobs}; @@ -2246,32 +2242,15 @@ struct FileResult { path: PathBuf, /// 0 indexed lines line_num: usize, + line_content: String, } impl FileResult { - fn new(path: &Path, line_num: usize) -> Self { + fn new(path: &Path, line_num: usize, line_content: String) -> Self { Self { path: path.to_path_buf(), line_num, - } - } - } - - impl ui::menu::Item for FileResult { - type Data = Option; - - fn format(&self, current_path: &Self::Data) -> Row { - let relative_path = helix_stdx::path::get_relative_path(&self.path) - .to_string_lossy() - .into_owned(); - if current_path - .as_ref() - .map(|p| p == &self.path) - .unwrap_or(false) - { - format!("{} (*)", relative_path).into() - } else { - relative_path.into() + line_content, } } } @@ -2317,8 +2296,28 @@ fn format(&self, current_path: &Self::Data) -> Row { return; } - // TODO - let columns = vec![]; + let columns = vec![ + PickerColumn::new( + "path", + |item: &FileResult, current_path: &Option| { + let relative_path = helix_stdx::path::get_relative_path(&item.path) + .to_string_lossy() + .into_owned(); + if current_path + .as_ref() + .map(|p| p == &item.path) + .unwrap_or(false) + { + format!("{} (*)", relative_path).into() + } else { + relative_path.into() + } + }, + ), + PickerColumn::new("contents", |item: &FileResult, _| { + item.line_content.as_str().into() + }), + ]; let (picker, injector) = Picker::stream(columns, current_path); let dedup_symlinks = file_picker_config.deduplicate_links; @@ -2345,77 +2344,79 @@ fn format(&self, current_path: &Self::Data) -> Row { .max_depth(file_picker_config.max_depth) .filter_entry(move |entry| { filter_picker_entry(entry, &absolute_root, dedup_symlinks) - }); - - walk_builder - .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")); - walk_builder.add_custom_ignore_filename(".helix/ignore"); - - walk_builder.build_parallel().run(|| { - let mut searcher = searcher.clone(); - let matcher = matcher.clone(); - let injector = injector_.clone(); - let documents = &documents; - Box::new(move |entry: Result| -> WalkState { - let entry = match entry { - Ok(entry) => entry, - Err(_) => return WalkState::Continue, - }; - - match entry.file_type() { - Some(entry) if entry.is_file() => {} - // skip everything else - _ => return WalkState::Continue, - }; - - let mut stop = false; - let sink = sinks::UTF8(|line_num, _| { - stop = injector - .push(FileResult::new(entry.path(), line_num as usize - 1)) - .is_err(); - - Ok(!stop) - }); - let doc = documents.iter().find(|&(doc_path, _)| { - doc_path - .as_ref() - .map_or(false, |doc_path| doc_path == entry.path()) - }); - - let result = if let Some((_, doc)) = doc { - // there is already a buffer for this file - // search the buffer instead of the file because it's faster - // and captures new edits without requiring a save - if searcher.multi_line_with_matcher(&matcher) { - // in this case a continous buffer is required - // convert the rope to a string - let text = doc.to_string(); - searcher.search_slice(&matcher, text.as_bytes(), sink) - } else { - searcher.search_reader( - &matcher, - RopeReader::new(doc.slice(..)), - sink, - ) - } - } else { - searcher.search_path(&matcher, entry.path(), sink) - }; - - if let Err(err) = result { - log::error!( - "Global search error: {}, {}", - entry.path().display(), - err - ); - } - if stop { - WalkState::Quit - } else { - WalkState::Continue - } }) - }); + .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) + .add_custom_ignore_filename(".helix/ignore") + .build_parallel() + .run(|| { + let mut searcher = searcher.clone(); + let matcher = matcher.clone(); + let injector = injector_.clone(); + let documents = &documents; + Box::new(move |entry: Result| -> WalkState { + let entry = match entry { + Ok(entry) => entry, + Err(_) => return WalkState::Continue, + }; + + match entry.file_type() { + Some(entry) if entry.is_file() => {} + // skip everything else + _ => return WalkState::Continue, + }; + + let mut stop = false; + let sink = sinks::UTF8(|line_num, line_content| { + stop = injector + .push(FileResult::new( + entry.path(), + line_num as usize - 1, + line_content.to_string(), + )) + .is_err(); + + Ok(!stop) + }); + let doc = documents.iter().find(|&(doc_path, _)| { + doc_path + .as_ref() + .map_or(false, |doc_path| doc_path == entry.path()) + }); + + let result = if let Some((_, doc)) = doc { + // there is already a buffer for this file + // search the buffer instead of the file because it's faster + // and captures new edits without requiring a save + if searcher.multi_line_with_matcher(&matcher) { + // in this case a continous buffer is required + // convert the rope to a string + let text = doc.to_string(); + searcher.search_slice(&matcher, text.as_bytes(), sink) + } else { + searcher.search_reader( + &matcher, + RopeReader::new(doc.slice(..)), + sink, + ) + } + } else { + searcher.search_path(&matcher, entry.path(), sink) + }; + + if let Err(err) = result { + log::error!( + "Global search error: {}, {}", + entry.path().display(), + err + ); + } + if stop { + WalkState::Quit + } else { + WalkState::Continue + } + }) + }); }); cx.jobs.callback(async move { @@ -2424,7 +2425,7 @@ fn format(&self, current_path: &Self::Data) -> Row { picker, 0, injector, - move |cx, FileResult { path, line_num }, action| { + move |cx, FileResult { path, line_num, .. }, action| { let doc = match cx.editor.open(path, action) { Ok(id) => doc_mut!(cx.editor, &id), Err(e) => { @@ -2456,7 +2457,7 @@ fn format(&self, current_path: &Self::Data) -> Row { }, ) .with_preview( - |_editor, FileResult { path, line_num }| { + |_editor, FileResult { path, line_num, .. }| { Some((path.clone().into(), Some((*line_num, *line_num)))) }, ); @@ -2897,31 +2898,6 @@ struct BufferMeta { focused_at: std::time::Instant, } - impl ui::menu::Item for BufferMeta { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - let path = self - .path - .as_deref() - .map(helix_stdx::path::get_relative_path); - let path = match path.as_deref().and_then(Path::to_str) { - Some(path) => path, - None => SCRATCH_BUFFER_NAME, - }; - - let mut flags = String::new(); - if self.is_modified { - flags.push('+'); - } - if self.is_current { - flags.push('*'); - } - - Row::new([self.id.to_string(), flags, path.to_string()]) - } - } - let new_meta = |doc: &Document| BufferMeta { id: doc.id(), path: doc.path().cloned(), @@ -2940,8 +2916,31 @@ fn format(&self, _data: &Self::Data) -> Row { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let columns = vec![]; - let picker = Picker::new(columns, 0, items, (), |cx, meta, action| { + let columns = vec![ + PickerColumn::new("id", |meta: &BufferMeta, _| meta.id.to_string().into()), + PickerColumn::new("flags", |meta: &BufferMeta, _| { + let mut flags = String::new(); + if meta.is_modified { + flags.push('+'); + } + if meta.is_current { + flags.push('*'); + } + flags.into() + }), + PickerColumn::new("path", |meta: &BufferMeta, _| { + let path = meta + .path + .as_deref() + .map(helix_stdx::path::get_relative_path); + path.as_deref() + .and_then(Path::to_str) + .unwrap_or(SCRATCH_BUFFER_NAME) + .to_string() + .into() + }), + ]; + let picker = Picker::new(columns, 2, items, (), |cx, meta, action| { cx.editor.switch(meta.id, action); }) .with_preview(|editor, meta| { @@ -2965,33 +2964,6 @@ struct JumpMeta { is_current: bool, } - impl ui::menu::Item for JumpMeta { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - let path = self - .path - .as_deref() - .map(helix_stdx::path::get_relative_path); - let path = match path.as_deref().and_then(Path::to_str) { - Some(path) => path, - None => SCRATCH_BUFFER_NAME, - }; - - let mut flags = Vec::new(); - if self.is_current { - flags.push("*"); - } - - let flag = if flags.is_empty() { - "".into() - } else { - format!(" ({})", flags.join("")) - }; - format!("{} {}{} {}", self.id, path, flag, self.text).into() - } - } - for (view, _) in cx.editor.tree.views_mut() { for doc_id in view.jumps.iter().map(|e| e.0).collect::>().iter() { let doc = doc_mut!(cx.editor, doc_id); @@ -3018,10 +2990,37 @@ fn format(&self, _data: &Self::Data) -> Row { } }; - let columns = vec![]; + let columns = vec![ + ui::PickerColumn::new("id", |item: &JumpMeta, _| item.id.to_string().into()), + ui::PickerColumn::new("path", |item: &JumpMeta, _| { + let path = item + .path + .as_deref() + .map(helix_stdx::path::get_relative_path); + path.as_deref() + .and_then(Path::to_str) + .unwrap_or(SCRATCH_BUFFER_NAME) + .to_string() + .into() + }), + ui::PickerColumn::new("flags", |item: &JumpMeta, _| { + let mut flags = Vec::new(); + if item.is_current { + flags.push("*"); + } + + if flags.is_empty() { + "".into() + } else { + format!(" ({})", flags.join("")).into() + } + }), + ui::PickerColumn::new("contents", |item: &JumpMeta, _| item.text.as_str().into()), + ]; + let picker = Picker::new( columns, - 0, + 1, // path cx.editor .tree .views() @@ -3061,33 +3060,6 @@ pub struct FileChangeData { style_renamed: Style, } - impl Item for FileChange { - type Data = FileChangeData; - - fn format(&self, data: &Self::Data) -> Row { - let process_path = |path: &PathBuf| { - path.strip_prefix(&data.cwd) - .unwrap_or(path) - .display() - .to_string() - }; - - let (sign, style, content) = match self { - Self::Untracked { path } => ("[+]", data.style_untracked, process_path(path)), - Self::Modified { path } => ("[~]", data.style_modified, process_path(path)), - Self::Conflict { path } => ("[x]", data.style_conflict, process_path(path)), - Self::Deleted { path } => ("[-]", data.style_deleted, process_path(path)), - Self::Renamed { from_path, to_path } => ( - "[>]", - data.style_renamed, - format!("{} -> {}", process_path(from_path), process_path(to_path)), - ), - }; - - Row::new([Cell::from(Span::styled(sign, style)), Cell::from(content)]) - } - } - let cwd = helix_stdx::env::current_working_dir(); if !cwd.exists() { cx.editor @@ -3101,7 +3073,40 @@ fn format(&self, data: &Self::Data) -> Row { let deleted = cx.editor.theme.get("diff.minus"); let renamed = cx.editor.theme.get("diff.delta.moved"); + let columns = vec![ + PickerColumn::new("change", |change: &FileChange, data: &FileChangeData| { + match change { + FileChange::Untracked { .. } => Span::styled("+ untracked", data.style_untracked), + FileChange::Modified { .. } => Span::styled("~ modified", data.style_modified), + FileChange::Conflict { .. } => Span::styled("x conflict", data.style_conflict), + FileChange::Deleted { .. } => Span::styled("- deleted", data.style_deleted), + FileChange::Renamed { .. } => Span::styled("> renamed", data.style_renamed), + } + .into() + }), + PickerColumn::new("path", |change: &FileChange, data: &FileChangeData| { + let display_path = |path: &PathBuf| { + path.strip_prefix(&data.cwd) + .unwrap_or(path) + .display() + .to_string() + }; + match change { + FileChange::Untracked { path } => display_path(path), + FileChange::Modified { path } => display_path(path), + FileChange::Conflict { path } => display_path(path), + FileChange::Deleted { path } => display_path(path), + FileChange::Renamed { from_path, to_path } => { + format!("{} -> {}", display_path(from_path), display_path(to_path)) + } + } + .into() + }), + ]; + let picker = Picker::new( + columns, + 1, // path Vec::new(), FileChangeData { cwd: cwd.clone(), @@ -3139,35 +3144,6 @@ fn format(&self, data: &Self::Data) -> Row { cx.push_layer(Box::new(overlaid(picker))); } -impl ui::menu::Item for MappableCommand { - type Data = ReverseKeymap; - - fn format(&self, keymap: &Self::Data) -> Row { - let fmt_binding = |bindings: &Vec>| -> String { - bindings.iter().fold(String::new(), |mut acc, bind| { - if !acc.is_empty() { - acc.push(' '); - } - for key in bind { - acc.push_str(&key.key_sequence_format()); - } - acc - }) - }; - - match self { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { - Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [:{}]", doc, name).into(), - }, - MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [{}]", doc, name).into(), - }, - } - } -} - pub fn command_palette(cx: &mut Context) { let register = cx.register; let count = cx.count; @@ -3187,7 +3163,34 @@ pub fn command_palette(cx: &mut Context) { } })); - let columns = vec![]; + let columns = vec![ + ui::PickerColumn::new("name", |item, _| match item { + MappableCommand::Typable { name, .. } => format!(":{name}").into(), + MappableCommand::Static { name, .. } => (*name).into(), + }), + ui::PickerColumn::new( + "bindings", + |item: &MappableCommand, keymap: &crate::keymap::ReverseKeymap| { + keymap + .get(item.name()) + .map(|bindings| { + bindings.iter().fold(String::new(), |mut acc, bind| { + if !acc.is_empty() { + acc.push(' '); + } + for key in bind { + acc.push_str(&key.key_sequence_format()); + } + acc + }) + }) + .unwrap_or_default() + .into() + }, + ), + ui::PickerColumn::new("doc", |item: &MappableCommand, _| item.doc().into()), + ]; + let picker = Picker::new(columns, 0, commands, keymap, move |cx, command, _action| { let mut ctx = Context { register, diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index da2b60dae..14cb83d29 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -12,7 +12,7 @@ use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; -use tui::{text::Spans, widgets::Row}; +use tui::text::Spans; use std::collections::HashMap; use std::future::Future; @@ -22,38 +22,6 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id}; -impl ui::menu::Item for StackFrame { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - self.name.as_str().into() // TODO: include thread_states in the label - } -} - -impl ui::menu::Item for DebugTemplate { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - self.name.as_str().into() - } -} - -impl ui::menu::Item for Thread { - type Data = ThreadStates; - - fn format(&self, thread_states: &Self::Data) -> Row { - format!( - "{} ({})", - self.name, - thread_states - .get(&self.id) - .map(|state| state.as_str()) - .unwrap_or("unknown") - ) - .into() - } -} - fn thread_picker( cx: &mut Context, callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static, @@ -73,7 +41,16 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let columns = vec![]; + let columns = vec![ + ui::PickerColumn::new("name", |item: &Thread, _| item.name.as_str().into()), + ui::PickerColumn::new("state", |item: &Thread, thread_states: &ThreadStates| { + thread_states + .get(&item.id) + .map(|state| state.as_str()) + .unwrap_or("unknown") + .into() + }), + ]; let picker = Picker::new( columns, 0, @@ -273,7 +250,10 @@ pub fn dap_launch(cx: &mut Context) { let templates = config.templates.clone(); - let columns = vec![]; + let columns = vec![ui::PickerColumn::new( + "template", + |item: &DebugTemplate, _| item.name.as_str().into(), + )]; cx.push_layer(Box::new(overlaid(Picker::new( columns, @@ -745,7 +725,9 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let columns = vec![]; + let columns = vec![ui::PickerColumn::new("frame", |item: &StackFrame, _| { + item.name.as_str().into() // TODO: include thread_states in the label + })]; let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| { let debugger = debugger!(cx.editor); // TODO: this should be simpler to find diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index bf9747a4c..2292569bf 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -9,10 +9,7 @@ Client, LanguageServerId, OffsetEncoding, }; use tokio_stream::StreamExt; -use tui::{ - text::{Span, Spans}, - widgets::Row, -}; +use tui::{text::Span, widgets::Row}; use super::{align_view, push_jump, Align, Context, Editor}; @@ -62,69 +59,11 @@ macro_rules! language_server_with_feature { }}; } -impl ui::menu::Item for lsp::Location { - /// Current working directory. - type Data = PathBuf; - - fn format(&self, cwdir: &Self::Data) -> Row { - // The preallocation here will overallocate a few characters since it will account for the - // URL's scheme, which is not used most of the time since that scheme will be "file://". - // Those extra chars will be used to avoid allocating when writing the line number (in the - // common case where it has 5 digits or less, which should be enough for a cast majority - // of usages). - let mut res = String::with_capacity(self.uri.as_str().len()); - - if self.uri.scheme() == "file" { - // With the preallocation above and UTF-8 paths already, this closure will do one (1) - // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`. - let mut write_path_to_res = || -> Option<()> { - let path = self.uri.to_file_path().ok()?; - res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy()); - Some(()) - }; - write_path_to_res(); - } else { - // Never allocates since we declared the string with this capacity already. - res.push_str(self.uri.as_str()); - } - - // Most commonly, this will not allocate, especially on Unix systems where the root prefix - // is a simple `/` and not `C:\` (with whatever drive letter) - write!(&mut res, ":{}", self.range.start.line + 1) - .expect("Will only failed if allocating fail"); - res.into() - } -} - struct SymbolInformationItem { symbol: lsp::SymbolInformation, offset_encoding: OffsetEncoding, } -impl ui::menu::Item for SymbolInformationItem { - /// Path to currently focussed document - type Data = Option; - - fn format(&self, current_doc_path: &Self::Data) -> Row { - if current_doc_path.as_ref() == Some(&self.symbol.location.uri) { - self.symbol.name.as_str().into() - } else { - match self.symbol.location.uri.to_file_path() { - Ok(path) => { - let get_relative_path = path::get_relative_path(path.as_path()); - format!( - "{} ({})", - &self.symbol.name, - get_relative_path.to_string_lossy() - ) - .into() - } - Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(), - } - } - } -} - struct DiagnosticStyles { hint: Style, info: Style, @@ -138,48 +77,6 @@ struct PickerDiagnostic { offset_encoding: OffsetEncoding, } -impl ui::menu::Item for PickerDiagnostic { - type Data = (DiagnosticStyles, DiagnosticsFormat); - - fn format(&self, (styles, format): &Self::Data) -> Row { - let mut style = self - .diag - .severity - .map(|s| match s { - DiagnosticSeverity::HINT => styles.hint, - DiagnosticSeverity::INFORMATION => styles.info, - DiagnosticSeverity::WARNING => styles.warning, - DiagnosticSeverity::ERROR => styles.error, - _ => Style::default(), - }) - .unwrap_or_default(); - - // remove background as it is distracting in the picker list - style.bg = None; - - let code = match self.diag.code.as_ref() { - Some(NumberOrString::Number(n)) => format!(" ({n})"), - Some(NumberOrString::String(s)) => format!(" ({s})"), - None => String::new(), - }; - - let path = match format { - DiagnosticsFormat::HideSourcePath => String::new(), - DiagnosticsFormat::ShowSourcePath => { - let path = path::get_truncated_path(&self.path); - format!("{}: ", path.to_string_lossy()) - } - }; - - Spans::from(vec![ - Span::raw(path), - Span::styled(&self.diag.message, style), - Span::styled(code, style), - ]) - .into() - } -} - fn location_to_file_location(location: &lsp::Location) -> FileLocation { let path = location.uri.to_file_path().unwrap(); let line = Some(( @@ -241,16 +138,82 @@ fn jump_to_position( } } -type SymbolPicker = Picker>; +fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str { + match kind { + lsp::SymbolKind::FILE => "file", + lsp::SymbolKind::MODULE => "module", + lsp::SymbolKind::NAMESPACE => "namespace", + lsp::SymbolKind::PACKAGE => "package", + lsp::SymbolKind::CLASS => "class", + lsp::SymbolKind::METHOD => "method", + lsp::SymbolKind::PROPERTY => "property", + lsp::SymbolKind::FIELD => "field", + lsp::SymbolKind::CONSTRUCTOR => "construct", + lsp::SymbolKind::ENUM => "enum", + lsp::SymbolKind::INTERFACE => "interface", + lsp::SymbolKind::FUNCTION => "function", + lsp::SymbolKind::VARIABLE => "variable", + lsp::SymbolKind::CONSTANT => "constant", + lsp::SymbolKind::STRING => "string", + lsp::SymbolKind::NUMBER => "number", + lsp::SymbolKind::BOOLEAN => "boolean", + lsp::SymbolKind::ARRAY => "array", + lsp::SymbolKind::OBJECT => "object", + lsp::SymbolKind::KEY => "key", + lsp::SymbolKind::NULL => "null", + lsp::SymbolKind::ENUM_MEMBER => "enummem", + lsp::SymbolKind::STRUCT => "struct", + lsp::SymbolKind::EVENT => "event", + lsp::SymbolKind::OPERATOR => "operator", + lsp::SymbolKind::TYPE_PARAMETER => "typeparam", + _ => { + log::warn!("Unknown symbol kind: {:?}", kind); + "" + } + } +} + +type SymbolPicker = Picker; + +fn sym_picker(symbols: Vec, workspace: bool) -> SymbolPicker { + let symbol_kind_column = ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| { + display_symbol_kind(item.symbol.kind).into() + }); + + let columns = if workspace { + vec![ + symbol_kind_column, + ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| { + item.symbol.name.as_str().into() + }) + .without_filtering(), + ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| { + match item.symbol.location.uri.to_file_path() { + Ok(path) => path::get_relative_path(path.as_path()) + .to_string_lossy() + .to_string() + .into(), + Err(_) => item.symbol.location.uri.to_string().into(), + } + }), + ] + } else { + vec![ + symbol_kind_column, + // Some symbols in the document symbol picker may have a URI that isn't + // the current file. It should be rare though, so we concatenate that + // URI in with the symbol name in this picker. + ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| { + item.symbol.name.as_str().into() + }), + ] + }; -fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { - // TODO: drop current_path comparison and instead use workspace: bool flag? - let columns = vec![]; Picker::new( columns, - 0, + 1, // name column symbols, - current_path, + (), move |cx, item, action| { jump_to_location( cx.editor, @@ -270,7 +233,7 @@ enum DiagnosticsFormat { HideSourcePath, } -type DiagnosticsPicker = Picker; +type DiagnosticsPicker = Picker; fn diag_picker( cx: &Context, @@ -302,12 +265,50 @@ fn diag_picker( error: cx.editor.theme.get("error"), }; - let columns = vec![]; + let mut columns = vec![ + ui::PickerColumn::new( + "severity", + |item: &PickerDiagnostic, styles: &DiagnosticStyles| { + match item.diag.severity { + Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint), + Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info), + Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning), + Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error), + _ => Span::raw(""), + } + .into() + }, + ), + ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| { + match item.diag.code.as_ref() { + Some(NumberOrString::Number(n)) => n.to_string().into(), + Some(NumberOrString::String(s)) => s.as_str().into(), + None => "".into(), + } + }), + ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| { + item.diag.message.as_str().into() + }), + ]; + let mut primary_column = 2; // message + + if format == DiagnosticsFormat::ShowSourcePath { + columns.insert( + // between message code and message + 2, + ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| { + let path = path::get_truncated_path(&item.path); + path.to_string_lossy().to_string().into() + }), + ); + primary_column += 1; + } + Picker::new( columns, - 0, + primary_column, flat_diag, - (styles, format), + styles, move |cx, PickerDiagnostic { path, @@ -389,7 +390,6 @@ fn nested_to_flat( } }) .collect(); - let current_url = doc.url(); if futures.is_empty() { cx.editor @@ -404,7 +404,7 @@ fn nested_to_flat( symbols.append(&mut lsp_items); } let call = move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = sym_picker(symbols, current_url); + let picker = sym_picker(symbols, false); compositor.push(Box::new(overlaid(picker))) }; @@ -466,13 +466,12 @@ pub fn workspace_symbol_picker(cx: &mut Context) { .boxed() }; - let current_url = doc.url(); let initial_symbols = get_symbols("".to_owned(), cx.editor); cx.jobs.callback(async move { let symbols = initial_symbols.await?; let call = move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = sym_picker(symbols, current_url); + let picker = sym_picker(symbols, true); let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols)); compositor.push(Box::new(overlaid(dyn_picker))) }; @@ -753,13 +752,6 @@ pub fn code_action(cx: &mut Context) { }); } -impl ui::menu::Item for lsp::Command { - type Data = (); - fn format(&self, _data: &Self::Data) -> Row { - self.title.as_str().into() - } -} - pub fn execute_lsp_command( editor: &mut Editor, language_server_id: LanguageServerId, @@ -829,7 +821,37 @@ fn goto_impl( } [] => unreachable!("`locations` should be non-empty for `goto_impl`"), _locations => { - let columns = vec![]; + let columns = vec![ui::PickerColumn::new( + "location", + |item: &lsp::Location, cwdir: &std::path::PathBuf| { + // The preallocation here will overallocate a few characters since it will account for the + // URL's scheme, which is not used most of the time since that scheme will be "file://". + // Those extra chars will be used to avoid allocating when writing the line number (in the + // common case where it has 5 digits or less, which should be enough for a cast majority + // of usages). + let mut res = String::with_capacity(item.uri.as_str().len()); + + if item.uri.scheme() == "file" { + // With the preallocation above and UTF-8 paths already, this closure will do one (1) + // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`. + if let Ok(path) = item.uri.to_file_path() { + res.push_str( + &path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(), + ); + } + } else { + // Never allocates since we declared the string with this capacity already. + res.push_str(item.uri.as_str()); + } + + // Most commonly, this will not allocate, especially on Unix systems where the root prefix + // is a simple `/` and not `C:\` (with whatever drive letter) + write!(&mut res, ":{}", item.range.start.line + 1) + .expect("Will only failed if allocating fail"); + res.into() + }, + )]; + let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) }) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 232e5846b..f65f96574 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1404,7 +1404,12 @@ fn lsp_workspace_command( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let columns = vec![]; + let columns = vec![ui::PickerColumn::new( + "title", + |(_ls_id, command): &(_, helix_lsp::lsp::Command), _| { + command.title.as_str().into() + }, + )]; let picker = ui::Picker::new( columns, 0, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index c5006f958..1520924c0 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, cmp::Reverse, path::PathBuf}; +use std::{borrow::Cow, cmp::Reverse}; use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, @@ -31,18 +31,6 @@ fn filter_text(&self, data: &Self::Data) -> Cow { } } -impl Item for PathBuf { - /// Root prefix to strip. - type Data = PathBuf; - - fn format(&self, root_path: &Self::Data) -> Row { - self.strip_prefix(root_path) - .unwrap_or(self) - .to_string_lossy() - .into() - } -} - pub type MenuCallback = Box, MenuEvent)>; pub struct Menu { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 01b718d45..4f6b031d4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -219,7 +219,15 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi }); log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - let columns = vec![]; + let columns = vec![PickerColumn::new( + "path", + |item: &PathBuf, root: &PathBuf| { + item.strip_prefix(root) + .unwrap_or(item) + .to_string_lossy() + .into() + }, + )]; let picker = Picker::new( columns, 0, From 53ac833efbcd6b968fd7c88318525a8d491fea14 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 16 Feb 2024 11:26:00 -0500 Subject: [PATCH 107/200] Replace picker shutdown bool with version number This works nicely for dynamic pickers: we stop any running jobs like global search that are pushing to the injector by incrementing the version number when we start a new request. The boolean only allowed us to shut the picker down once, but with a usize a picker can have multiple "sessions" / "life-cycles" where it receives new options from an injector. --- helix-term/src/ui/picker.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 5e7bda21b..4f94c6172 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -34,7 +34,7 @@ io::Read, path::{Path, PathBuf}, sync::{ - atomic::{self, AtomicBool}, + atomic::{self, AtomicUsize}, Arc, }, }; @@ -154,7 +154,8 @@ pub struct Injector { dst: nucleo::Injector, columns: Arc<[Column]>, editor_data: Arc, - shutown: Arc, + version: usize, + picker_version: Arc, } impl Clone for Injector { @@ -163,7 +164,8 @@ fn clone(&self) -> Self { dst: self.dst.clone(), columns: self.columns.clone(), editor_data: self.editor_data.clone(), - shutown: self.shutown.clone(), + version: self.version, + picker_version: self.picker_version.clone(), } } } @@ -172,7 +174,7 @@ fn clone(&self) -> Self { impl Injector { pub fn push(&self, item: T) -> Result<(), InjectorShutdown> { - if self.shutown.load(atomic::Ordering::Relaxed) { + if self.version != self.picker_version.load(atomic::Ordering::Relaxed) { return Err(InjectorShutdown); } @@ -220,7 +222,7 @@ pub struct Picker { columns: Arc<[Column]>, primary_column: usize, editor_data: Arc, - shutdown: Arc, + version: Arc, matcher: Nucleo, /// Current height of the completions box @@ -261,7 +263,8 @@ pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injecto dst: matcher.injector(), columns: columns.into(), editor_data: Arc::new(editor_data), - shutown: Arc::new(AtomicBool::new(false)), + version: 0, + picker_version: Arc::new(AtomicUsize::new(0)), }; (matcher, streamer) } @@ -290,7 +293,7 @@ pub fn new( columns.into(), primary_column, Arc::new(editor_data), - Arc::new(AtomicBool::new(false)), + Arc::new(AtomicUsize::new(0)), callback_fn, ) } @@ -306,7 +309,7 @@ pub fn with_stream( injector.columns, primary_column, injector.editor_data, - injector.shutown, + injector.picker_version, callback_fn, ) } @@ -316,7 +319,7 @@ fn with( columns: Arc<[Column]>, default_column: usize, editor_data: Arc, - shutdown: Arc, + version: Arc, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { assert!(!columns.is_empty()); @@ -340,7 +343,7 @@ fn with( primary_column: default_column, matcher, editor_data, - shutdown, + version, cursor: 0, prompt, query, @@ -361,7 +364,8 @@ pub fn injector(&self) -> Injector { dst: self.matcher.injector(), columns: self.columns.clone(), editor_data: self.editor_data.clone(), - shutown: self.shutdown.clone(), + version: self.version.load(atomic::Ordering::Relaxed), + picker_version: self.version.clone(), } } @@ -916,7 +920,7 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { // be restarting the stream somehow once the picker gets // reopened instead (like for an FS crawl) that would also remove the // need for the special case above but that is pretty tricky - picker.shutdown.store(true, atomic::Ordering::Relaxed); + picker.version.fetch_add(1, atomic::Ordering::Relaxed); Box::new(|compositor: &mut Compositor, _ctx| { // remove the layer compositor.last_picker = compositor.pop(); @@ -1002,7 +1006,7 @@ fn id(&self) -> Option<&'static str> { impl Drop for Picker { fn drop(&mut self) { // ensure we cancel any ongoing background threads streaming into the picker - self.shutdown.store(true, atomic::Ordering::Relaxed) + self.version.fetch_add(1, atomic::Ordering::Relaxed); } } From 2c9f5b3efb72f4ab62fe29ddaa1c0871bca9b01d Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 16 Feb 2024 12:08:59 -0500 Subject: [PATCH 108/200] Implement Error for InjectorShutdown --- Cargo.lock | 1 + helix-term/Cargo.toml | 1 + helix-term/src/ui/picker.rs | 3 +++ 3 files changed, 5 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index c614cadf1..57bdb235d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,6 +1467,7 @@ dependencies = [ "smallvec", "tempfile", "termini", + "thiserror", "tokio", "tokio-stream", "toml", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index fb850f7c0..c50165e9a 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -56,6 +56,7 @@ ignore = "0.4" pulldown-cmark = { version = "0.11", default-features = false } # file type detection content_inspector = "0.2.4" +thiserror = "1.0" # opening URLs open = "5.2.0" diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 4f94c6172..7e053c4ad 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -18,6 +18,7 @@ use helix_event::AsyncHook; use nucleo::pattern::CaseMatching; use nucleo::{Config, Nucleo, Utf32String}; +use thiserror::Error; use tokio::sync::mpsc::Sender; use tui::{ buffer::Buffer as Surface, @@ -170,6 +171,8 @@ fn clone(&self) -> Self { } } +#[derive(Error, Debug)] +#[error("picker has been shut down")] pub struct InjectorShutdown; impl Injector { From 11f809c1779db9a02315b58466cc4e1bdf494202 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 21 Feb 2024 09:37:45 -0500 Subject: [PATCH 109/200] Bump nucleo to v0.4.1 We will use this in the child commit to improve the picker's running indicator. Nucleo 0.4.0 includes an `active_injectors` member that we can use to detect if anything can push to the picker. When that count drops to zero we can remove the running indicator. Nucleo 0.4.1 contains a fix for crashes with interactive global search on a large directory. --- Cargo.lock | 15 ++++----------- Cargo.toml | 2 +- helix-core/src/fuzzy.rs | 10 ++++++++-- helix-term/src/ui/menu.rs | 10 ++++++++-- helix-term/src/ui/picker.rs | 12 ++++++++---- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57bdb235d..d8e65a8f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,12 +209,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" -[[package]] -name = "cov-mark" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3" - [[package]] name = "crc32fast" version = "1.3.2" @@ -1785,9 +1779,9 @@ dependencies = [ [[package]] name = "nucleo" -version = "0.2.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3" +checksum = "6350a138d8860658523a7593cbf6813999d17a099371d14f70c5c905b37593e9" dependencies = [ "nucleo-matcher", "parking_lot", @@ -1796,11 +1790,10 @@ dependencies = [ [[package]] name = "nucleo-matcher" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" dependencies = [ - "cov-mark", "memchr", "unicode-segmentation", ] diff --git a/Cargo.toml b/Cargo.toml index 9be265fc5..d9541b648 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ package.helix-term.opt-level = 2 [workspace.dependencies] tree-sitter = { version = "0.22" } -nucleo = "0.2.0" +nucleo = "0.4.1" slotmap = "1.0.7" thiserror = "1.0" diff --git a/helix-core/src/fuzzy.rs b/helix-core/src/fuzzy.rs index 549c6b0e5..da46518f9 100644 --- a/helix-core/src/fuzzy.rs +++ b/helix-core/src/fuzzy.rs @@ -1,6 +1,6 @@ use std::ops::DerefMut; -use nucleo::pattern::{Atom, AtomKind, CaseMatching}; +use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization}; use nucleo::Config; use parking_lot::Mutex; @@ -38,6 +38,12 @@ pub fn fuzzy_match>( if path { matcher.config.set_match_paths(); } - let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false); + let pattern = Atom::new( + pattern, + CaseMatching::Smart, + Normalization::Smart, + AtomKind::Fuzzy, + false, + ); pattern.match_list(items, &mut matcher) } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 1520924c0..c120d0b25 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -5,7 +5,7 @@ ctrl, key, shift, }; use helix_core::fuzzy::MATCHER; -use nucleo::pattern::{Atom, AtomKind, CaseMatching}; +use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization}; use nucleo::{Config, Utf32Str}; use tui::{buffer::Buffer as Surface, widgets::Table}; @@ -80,7 +80,13 @@ pub fn new( pub fn score(&mut self, pattern: &str, incremental: bool) { let mut matcher = MATCHER.lock(); matcher.config = Config::DEFAULT; - let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false); + let pattern = Atom::new( + pattern, + CaseMatching::Ignore, + Normalization::Smart, + AtomKind::Fuzzy, + false, + ); let mut buf = Vec::new(); if incremental { self.matches.retain_mut(|(index, score)| { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 7e053c4ad..fc1eba6e7 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -16,7 +16,7 @@ }; use futures_util::future::BoxFuture; use helix_event::AsyncHook; -use nucleo::pattern::CaseMatching; +use nucleo::pattern::{CaseMatching, Normalization}; use nucleo::{Config, Nucleo, Utf32String}; use thiserror::Error; use tokio::sync::mpsc::Sender; @@ -506,9 +506,13 @@ fn handle_prompt_change(&mut self) { continue; } let is_append = pattern.starts_with(old_pattern); - self.matcher - .pattern - .reparse(i, pattern, CaseMatching::Smart, is_append); + self.matcher.pattern.reparse( + i, + pattern, + CaseMatching::Smart, + Normalization::Smart, + is_append, + ); } } From 9e31ba547574ca77b1cf63a62237a51eeea2e222 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 16 Feb 2024 11:19:00 -0500 Subject: [PATCH 110/200] Consolidate DynamicPicker into Picker DynamicPicker is a thin wrapper over Picker that holds some additional state, similar to the old FilePicker type. Like with FilePicker, we want to fold the two types together, having Picker optionally hold that extra state. The DynamicPicker is a little more complicated than FilePicker was though - it holds a query callback and current query string in state and provides some debounce for queries using the IdleTimeout event. We can move all of that state and debounce logic into an AsyncHook implementation, introduced here as `DynamicQueryHandler`. The hook receives updates to the primary query and debounces those events so that once a query has been idle for a short time (275ms) we re-run the query. A standard Picker created through `new` for example can be promoted into a Dynamic picker by chaining the new `with_dynamic_query` function, very similar to FilePicker's replacement `with_preview`. The workspace symbol picker has been migrated to the new way of writing dynamic pickers as an example. The child commit will promote global search into a dynamic Picker as well. --- helix-term/src/commands/lsp.rs | 69 ++++++++++++----- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 112 +++++++-------------------- helix-term/src/ui/picker/handlers.rs | 99 +++++++++++++++++++---- 4 files changed, 163 insertions(+), 119 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 2292569bf..3dfe46ad7 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -26,7 +26,7 @@ use crate::{ compositor::{self, Compositor}, job::Callback, - ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent}, + ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent}, }; use std::{ @@ -413,6 +413,8 @@ fn nested_to_flat( } pub fn workspace_symbol_picker(cx: &mut Context) { + use crate::ui::picker::Injector; + let doc = doc!(cx.editor); if doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) @@ -424,19 +426,21 @@ pub fn workspace_symbol_picker(cx: &mut Context) { return; } - let get_symbols = move |pattern: String, editor: &mut Editor| { + let get_symbols = |pattern: &str, editor: &mut Editor, _data, injector: &Injector<_, _>| { let doc = doc!(editor); let mut seen_language_servers = HashSet::new(); let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) .map(|language_server| { - let request = language_server.workspace_symbols(pattern.clone()).unwrap(); + let request = language_server + .workspace_symbols(pattern.to_string()) + .unwrap(); let offset_encoding = language_server.offset_encoding(); async move { let json = request.await?; - let response = + let response: Vec<_> = serde_json::from_value::>>(json)? .unwrap_or_default() .into_iter() @@ -455,29 +459,56 @@ pub fn workspace_symbol_picker(cx: &mut Context) { editor.set_error("No configured language server supports workspace symbols"); } + let injector = injector.clone(); async move { - let mut symbols = Vec::new(); // TODO if one symbol request errors, all other requests are discarded (even if they're valid) - while let Some(mut lsp_items) = futures.try_next().await? { - symbols.append(&mut lsp_items); + while let Some(lsp_items) = futures.try_next().await? { + for item in lsp_items { + injector.push(item)?; + } } - anyhow::Ok(symbols) + Ok(()) } .boxed() }; + let columns = vec![ + ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| { + display_symbol_kind(item.symbol.kind).into() + }), + ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| { + item.symbol.name.as_str().into() + }) + .without_filtering(), + ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| { + match item.symbol.location.uri.to_file_path() { + Ok(path) => path::get_relative_path(path.as_path()) + .to_string_lossy() + .to_string() + .into(), + Err(_) => item.symbol.location.uri.to_string().into(), + } + }), + ]; - let initial_symbols = get_symbols("".to_owned(), cx.editor); + let picker = Picker::new( + columns, + 1, // name column + vec![], + (), + move |cx, item, action| { + jump_to_location( + cx.editor, + &item.symbol.location, + item.offset_encoding, + action, + ); + }, + ) + .with_preview(|_editor, item| Some(location_to_file_location(&item.symbol.location))) + .with_dynamic_query(get_symbols, None) + .truncate_start(false); - cx.jobs.callback(async move { - let symbols = initial_symbols.await?; - let call = move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = sym_picker(symbols, true); - let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols)); - compositor.push(Box::new(overlaid(dyn_picker))) - }; - - Ok(Callback::EditorCompositor(Box::new(call))) - }); + cx.push_layer(Box::new(overlaid(picker))); } pub fn diagnostics_picker(cx: &mut Context) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 4f6b031d4..93ac2e651 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -21,7 +21,7 @@ use helix_stdx::rope; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{Column as PickerColumn, DynamicPicker, FileLocation, Picker}; +pub use picker::{Column as PickerColumn, FileLocation, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index fc1eba6e7..bfec9ddb4 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -4,9 +4,7 @@ use crate::{ alt, compositor::{self, Component, Compositor, Context, Event, EventResult}, - ctrl, - job::Callback, - key, shift, + ctrl, key, shift, ui::{ self, document::{render_document, LineDecoration, LinePos, TextRenderer}, @@ -53,9 +51,7 @@ Document, DocumentId, Editor, }; -use super::overlay::Overlay; - -use self::handlers::PreviewHighlightHandler; +use self::handlers::{DynamicQueryHandler, PreviewHighlightHandler}; pub const ID: &str = "picker"; @@ -221,6 +217,11 @@ fn format_text<'a>(&self, item: &'a T, data: &'a D) -> Cow<'a, str> { } } +/// Returns a new list of options to replace the contents of the picker +/// when called with the current picker query, +type DynQueryCallback = + fn(&str, &mut Editor, Arc, &Injector) -> BoxFuture<'static, anyhow::Result<()>>; + pub struct Picker { columns: Arc<[Column]>, primary_column: usize, @@ -250,6 +251,7 @@ pub struct Picker { file_fn: Option>, /// An event handler for syntax highlighting the currently previewed file. preview_highlight_handler: Sender>, + dynamic_query_handler: Option>>, } impl Picker { @@ -359,6 +361,7 @@ fn with( read_buffer: Vec::with_capacity(1024), file_fn: None, preview_highlight_handler: PreviewHighlightHandler::::default().spawn(), + dynamic_query_handler: None, } } @@ -394,12 +397,15 @@ pub fn with_line(mut self, line: String, editor: &Editor) -> Self { self } - pub fn set_options(&mut self, new_options: Vec) { - self.matcher.restart(false); - let injector = self.matcher.injector(); - for item in new_options { - inject_nucleo_item(&injector, &self.columns, item, &self.editor_data); - } + pub fn with_dynamic_query( + mut self, + callback: DynQueryCallback, + debounce_ms: Option, + ) -> Self { + let handler = DynamicQueryHandler::new(callback, debounce_ms).spawn(); + helix_event::send_blocking(&handler, self.primary_query()); + self.dynamic_query_handler = Some(handler); + self } /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) @@ -514,6 +520,11 @@ fn handle_prompt_change(&mut self) { is_append, ); } + // If this is a dynamic picker, notify the query hook that the primary + // query might have been updated. + if let Some(handler) = &self.dynamic_query_handler { + helix_event::send_blocking(handler, self.primary_query()); + } } fn current_file(&self, editor: &Editor) -> Option { @@ -621,7 +632,11 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) let count = format!( "{}{}/{}", - if status.running { "(running) " } else { "" }, + if status.running || self.matcher.active_injectors() > 0 { + "(running) " + } else { + "" + }, snapshot.matched_item_count(), snapshot.item_count(), ); @@ -1018,74 +1033,3 @@ fn drop(&mut self) { } type PickerCallback = Box; - -/// Returns a new list of options to replace the contents of the picker -/// when called with the current picker query, -pub type DynQueryCallback = - Box BoxFuture<'static, anyhow::Result>>>; - -/// A picker that updates its contents via a callback whenever the -/// query string changes. Useful for live grep, workspace symbols, etc. -pub struct DynamicPicker { - file_picker: Picker, - query_callback: DynQueryCallback, - query: String, -} - -impl DynamicPicker { - pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { - Self { - file_picker, - query_callback, - query: String::new(), - } - } -} - -impl Component for DynamicPicker { - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - self.file_picker.render(area, surface, cx); - } - - fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - let event_result = self.file_picker.handle_event(event, cx); - let Some(current_query) = self.file_picker.primary_query() else { - return event_result; - }; - - if !matches!(event, Event::IdleTimeout) || self.query == *current_query { - return event_result; - } - - self.query = current_query.to_string(); - - let new_options = (self.query_callback)(current_query.to_owned(), cx.editor); - - cx.jobs.callback(async move { - let new_options = new_options.await?; - let callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { - // Wrapping of pickers in overlay is done outside the picker code, - // so this is fragile and will break if wrapped in some other widget. - let picker = match compositor.find_id::>(ID) { - Some(overlay) => &mut overlay.content.file_picker, - None => return, - }; - picker.set_options(new_options); - })); - anyhow::Ok(callback) - }); - EventResult::Consumed(None) - } - - fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { - self.file_picker.cursor(area, ctx) - } - - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - self.file_picker.required_size(viewport) - } - - fn id(&self) -> Option<&'static str> { - Some(ID) - } -} diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs index f01c982a7..e426adda7 100644 --- a/helix-term/src/ui/picker/handlers.rs +++ b/helix-term/src/ui/picker/handlers.rs @@ -1,11 +1,15 @@ -use std::{path::Path, sync::Arc, time::Duration}; +use std::{ + path::Path, + sync::{atomic, Arc}, + time::Duration, +}; use helix_event::AsyncHook; use tokio::time::Instant; use crate::{job, ui::overlay::Overlay}; -use super::{CachedPreview, DynamicPicker, Picker}; +use super::{CachedPreview, DynQueryCallback, Picker}; pub(super) struct PreviewHighlightHandler { trigger: Option>, @@ -50,12 +54,11 @@ fn finish_debounce(&mut self) { }; job::dispatch_blocking(move |editor, compositor| { - let picker = match compositor.find::>>() { - Some(Overlay { content, .. }) => content, - None => match compositor.find::>>() { - Some(Overlay { content, .. }) => &mut content.file_picker, - None => return, - }, + let Some(Overlay { + content: picker, .. + }) = compositor.find::>>() + else { + return; }; let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) @@ -87,13 +90,10 @@ fn finish_debounce(&mut self) { }; job::dispatch_blocking(move |editor, compositor| { - let picker = match compositor.find::>>() { - Some(Overlay { content, .. }) => Some(content), - None => compositor - .find::>>() - .map(|overlay| &mut overlay.content.file_picker), - }; - let Some(picker) = picker else { + let Some(Overlay { + content: picker, .. + }) = compositor.find::>>() + else { log::info!("picker closed before syntax highlighting finished"); return; }; @@ -114,3 +114,72 @@ fn finish_debounce(&mut self) { }); } } + +pub(super) struct DynamicQueryHandler { + callback: Arc>, + // Duration used as a debounce. + // Defaults to 100ms if not provided via `Picker::with_dynamic_query`. Callers may want to set + // this higher if the dynamic query is expensive - for example global search. + debounce: Duration, + last_query: Arc, + query: Option>, +} + +impl DynamicQueryHandler { + pub(super) fn new(callback: DynQueryCallback, duration_ms: Option) -> Self { + Self { + callback: Arc::new(callback), + debounce: Duration::from_millis(duration_ms.unwrap_or(100)), + last_query: "".into(), + query: None, + } + } +} + +impl AsyncHook for DynamicQueryHandler { + type Event = Arc; + + fn handle_event(&mut self, query: Self::Event, _timeout: Option) -> Option { + if query == self.last_query { + // If the search query reverts to the last one we requested, no need to + // make a new request. + self.query = None; + None + } else { + self.query = Some(query); + Some(Instant::now() + self.debounce) + } + } + + fn finish_debounce(&mut self) { + let Some(query) = self.query.take() else { + return; + }; + self.last_query = query.clone(); + let callback = self.callback.clone(); + + job::dispatch_blocking(move |editor, compositor| { + let Some(Overlay { + content: picker, .. + }) = compositor.find::>>() + else { + return; + }; + // Increment the version number to cancel any ongoing requests. + picker.version.fetch_add(1, atomic::Ordering::Relaxed); + picker.matcher.restart(false); + let injector = picker.injector(); + let get_options = (callback)(&query, editor, picker.editor_data.clone(), &injector); + tokio::spawn(async move { + if let Err(err) = get_options.await { + log::info!("Dynamic request failed: {err}"); + } + // The picker's shows its running indicator when there are any active + // injectors. When we're done injecting new options, drop the injector + // and request a redraw to remove the running indicator. + drop(injector); + helix_event::request_redraw(); + }); + }) + } +} From 5622db6798f665cdb6c208970763a31902c3e37e Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 1 Apr 2024 11:45:49 -0400 Subject: [PATCH 111/200] Remove sym_picker helper fun The parent commit split out the workspace symbol picker to an inline definition so the `workspace` parameter is never passed as `true`. We should consolidate this picker definition into the symbol_picker function. --- helix-term/src/commands/lsp.rs | 86 ++++++++++++---------------------- 1 file changed, 31 insertions(+), 55 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 3dfe46ad7..23bcb977f 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -173,60 +173,6 @@ fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str { } } -type SymbolPicker = Picker; - -fn sym_picker(symbols: Vec, workspace: bool) -> SymbolPicker { - let symbol_kind_column = ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| { - display_symbol_kind(item.symbol.kind).into() - }); - - let columns = if workspace { - vec![ - symbol_kind_column, - ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| { - item.symbol.name.as_str().into() - }) - .without_filtering(), - ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| { - match item.symbol.location.uri.to_file_path() { - Ok(path) => path::get_relative_path(path.as_path()) - .to_string_lossy() - .to_string() - .into(), - Err(_) => item.symbol.location.uri.to_string().into(), - } - }), - ] - } else { - vec![ - symbol_kind_column, - // Some symbols in the document symbol picker may have a URI that isn't - // the current file. It should be rare though, so we concatenate that - // URI in with the symbol name in this picker. - ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| { - item.symbol.name.as_str().into() - }), - ] - }; - - Picker::new( - columns, - 1, // name column - symbols, - (), - move |cx, item, action| { - jump_to_location( - cx.editor, - &item.symbol.location, - item.offset_encoding, - action, - ); - }, - ) - .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location))) - .truncate_start(false) -} - #[derive(Copy, Clone, PartialEq)] enum DiagnosticsFormat { ShowSourcePath, @@ -404,7 +350,37 @@ fn nested_to_flat( symbols.append(&mut lsp_items); } let call = move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = sym_picker(symbols, false); + let columns = vec![ + ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| { + display_symbol_kind(item.symbol.kind).into() + }), + // Some symbols in the document symbol picker may have a URI that isn't + // the current file. It should be rare though, so we concatenate that + // URI in with the symbol name in this picker. + ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| { + item.symbol.name.as_str().into() + }), + ]; + + let picker = Picker::new( + columns, + 1, // name column + symbols, + (), + move |cx, item, action| { + jump_to_location( + cx.editor, + &item.symbol.location, + item.offset_encoding, + action, + ); + }, + ) + .with_preview(move |_editor, item| { + Some(location_to_file_location(&item.symbol.location)) + }) + .truncate_start(false); + compositor.push(Box::new(overlaid(picker))) }; From 1d023b05ac7a5d5176ee7cb02042d045bdc3a095 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 17 Feb 2024 10:37:21 -0500 Subject: [PATCH 112/200] Refactor global_search as a dynamic Picker --- helix-term/src/commands.rs | 387 ++++++++++++++++++------------------- 1 file changed, 185 insertions(+), 202 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f4d2c634c..b7ffeeb77 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,6 +3,7 @@ pub(crate) mod typed; pub use dap::*; +use futures_util::FutureExt; use helix_event::status; use helix_stdx::{ path::expand_tilde, @@ -2255,222 +2256,204 @@ fn new(path: &Path, line_num: usize, line_content: String) -> Self { } } + struct GlobalSearchConfig { + smart_case: bool, + file_picker_config: helix_view::editor::FilePickerConfig, + } + let config = cx.editor.config(); - let smart_case = config.search.smart_case; - let file_picker_config = config.file_picker.clone(); + let config = GlobalSearchConfig { + smart_case: config.search.smart_case, + file_picker_config: config.file_picker.clone(), + }; - let reg = cx.register.unwrap_or('/'); - let completions = search_completions(cx, Some(reg)); - ui::raw_regex_prompt( - cx, - "global-search:".into(), - Some(reg), - move |_editor: &Editor, input: &str| { - completions - .iter() - .filter(|comp| comp.starts_with(input)) - .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) - .collect() - }, - move |cx, _, input, event| { - if event != PromptEvent::Validate { - return; + let columns = vec![ + PickerColumn::new("path", |item: &FileResult, _| { + let path = helix_stdx::path::get_relative_path(&item.path); + format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into() + }), + PickerColumn::new("contents", |item: &FileResult, _| { + item.line_content.as_str().into() + }) + .without_filtering(), + ]; + + let get_files = |query: &str, + editor: &mut Editor, + config: std::sync::Arc, + injector: &ui::picker::Injector<_, _>| { + if query.is_empty() { + return async { Ok(()) }.boxed(); + } + + let search_root = helix_stdx::env::current_working_dir(); + if !search_root.exists() { + return async { Err(anyhow::anyhow!("Current working directory does not exist")) } + .boxed(); + } + + let documents: Vec<_> = editor + .documents() + .map(|doc| (doc.path().cloned(), doc.text().to_owned())) + .collect(); + + let matcher = match RegexMatcherBuilder::new() + .case_smart(config.smart_case) + .build(query) + { + Ok(matcher) => { + // Clear any "Failed to compile regex" errors out of the statusline. + editor.clear_status(); + matcher } - cx.editor.registers.last_search_register = reg; + Err(err) => { + log::info!("Failed to compile search pattern in global search: {}", err); + return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed(); + } + }; - let current_path = doc_mut!(cx.editor).path().cloned(); - let documents: Vec<_> = cx - .editor - .documents() - .map(|doc| (doc.path().cloned(), doc.text().to_owned())) - .collect(); + let dedup_symlinks = config.file_picker_config.deduplicate_links; + let absolute_root = search_root + .canonicalize() + .unwrap_or_else(|_| search_root.clone()); - if let Ok(matcher) = RegexMatcherBuilder::new() - .case_smart(smart_case) - .build(input) - { - let search_root = helix_stdx::env::current_working_dir(); - if !search_root.exists() { + let injector = injector.clone(); + async move { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + WalkBuilder::new(search_root) + .hidden(config.file_picker_config.hidden) + .parents(config.file_picker_config.parents) + .ignore(config.file_picker_config.ignore) + .follow_links(config.file_picker_config.follow_symlinks) + .git_ignore(config.file_picker_config.git_ignore) + .git_global(config.file_picker_config.git_global) + .git_exclude(config.file_picker_config.git_exclude) + .max_depth(config.file_picker_config.max_depth) + .filter_entry(move |entry| { + filter_picker_entry(entry, &absolute_root, dedup_symlinks) + }) + .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) + .add_custom_ignore_filename(".helix/ignore") + .build_parallel() + .run(|| { + let mut searcher = searcher.clone(); + let matcher = matcher.clone(); + let injector = injector.clone(); + let documents = &documents; + Box::new(move |entry: Result| -> WalkState { + let entry = match entry { + Ok(entry) => entry, + Err(_) => return WalkState::Continue, + }; + + match entry.file_type() { + Some(entry) if entry.is_file() => {} + // skip everything else + _ => return WalkState::Continue, + }; + + let mut stop = false; + let sink = sinks::UTF8(|line_num, line_content| { + stop = injector + .push(FileResult::new( + entry.path(), + line_num as usize - 1, + line_content.to_string(), + )) + .is_err(); + + Ok(!stop) + }); + let doc = documents.iter().find(|&(doc_path, _)| { + doc_path + .as_ref() + .map_or(false, |doc_path| doc_path == entry.path()) + }); + + let result = if let Some((_, doc)) = doc { + // there is already a buffer for this file + // search the buffer instead of the file because it's faster + // and captures new edits without requiring a save + if searcher.multi_line_with_matcher(&matcher) { + // in this case a continous buffer is required + // convert the rope to a string + let text = doc.to_string(); + searcher.search_slice(&matcher, text.as_bytes(), sink) + } else { + searcher.search_reader( + &matcher, + RopeReader::new(doc.slice(..)), + sink, + ) + } + } else { + searcher.search_path(&matcher, entry.path(), sink) + }; + + if let Err(err) = result { + log::error!("Global search error: {}, {}", entry.path().display(), err); + } + if stop { + WalkState::Quit + } else { + WalkState::Continue + } + }) + }); + Ok(()) + } + .boxed() + }; + + let mut picker = Picker::new( + columns, + 1, // contents + vec![], + config, + move |cx, FileResult { path, line_num, .. }, action| { + let doc = match cx.editor.open(path, action) { + Ok(id) => doc_mut!(cx.editor, &id), + Err(e) => { cx.editor - .set_error("Current working directory does not exist"); + .set_error(format!("Failed to open file '{}': {}", path.display(), e)); return; } + }; - let columns = vec![ - PickerColumn::new( - "path", - |item: &FileResult, current_path: &Option| { - let relative_path = helix_stdx::path::get_relative_path(&item.path) - .to_string_lossy() - .into_owned(); - if current_path - .as_ref() - .map(|p| p == &item.path) - .unwrap_or(false) - { - format!("{} (*)", relative_path).into() - } else { - relative_path.into() - } - }, - ), - PickerColumn::new("contents", |item: &FileResult, _| { - item.line_content.as_str().into() - }), - ]; - let (picker, injector) = Picker::stream(columns, current_path); - - let dedup_symlinks = file_picker_config.deduplicate_links; - let absolute_root = search_root - .canonicalize() - .unwrap_or_else(|_| search_root.clone()); - let injector_ = injector.clone(); - - std::thread::spawn(move || { - let searcher = SearcherBuilder::new() - .binary_detection(BinaryDetection::quit(b'\x00')) - .build(); - - let mut walk_builder = WalkBuilder::new(search_root); - - walk_builder - .hidden(file_picker_config.hidden) - .parents(file_picker_config.parents) - .ignore(file_picker_config.ignore) - .follow_links(file_picker_config.follow_symlinks) - .git_ignore(file_picker_config.git_ignore) - .git_global(file_picker_config.git_global) - .git_exclude(file_picker_config.git_exclude) - .max_depth(file_picker_config.max_depth) - .filter_entry(move |entry| { - filter_picker_entry(entry, &absolute_root, dedup_symlinks) - }) - .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) - .add_custom_ignore_filename(".helix/ignore") - .build_parallel() - .run(|| { - let mut searcher = searcher.clone(); - let matcher = matcher.clone(); - let injector = injector_.clone(); - let documents = &documents; - Box::new(move |entry: Result| -> WalkState { - let entry = match entry { - Ok(entry) => entry, - Err(_) => return WalkState::Continue, - }; - - match entry.file_type() { - Some(entry) if entry.is_file() => {} - // skip everything else - _ => return WalkState::Continue, - }; - - let mut stop = false; - let sink = sinks::UTF8(|line_num, line_content| { - stop = injector - .push(FileResult::new( - entry.path(), - line_num as usize - 1, - line_content.to_string(), - )) - .is_err(); - - Ok(!stop) - }); - let doc = documents.iter().find(|&(doc_path, _)| { - doc_path - .as_ref() - .map_or(false, |doc_path| doc_path == entry.path()) - }); - - let result = if let Some((_, doc)) = doc { - // there is already a buffer for this file - // search the buffer instead of the file because it's faster - // and captures new edits without requiring a save - if searcher.multi_line_with_matcher(&matcher) { - // in this case a continous buffer is required - // convert the rope to a string - let text = doc.to_string(); - searcher.search_slice(&matcher, text.as_bytes(), sink) - } else { - searcher.search_reader( - &matcher, - RopeReader::new(doc.slice(..)), - sink, - ) - } - } else { - searcher.search_path(&matcher, entry.path(), sink) - }; - - if let Err(err) = result { - log::error!( - "Global search error: {}, {}", - entry.path().display(), - err - ); - } - if stop { - WalkState::Quit - } else { - WalkState::Continue - } - }) - }); - }); - - cx.jobs.callback(async move { - let call = move |_: &mut Editor, compositor: &mut Compositor| { - let picker = Picker::with_stream( - picker, - 0, - injector, - move |cx, FileResult { path, line_num, .. }, action| { - let doc = match cx.editor.open(path, action) { - Ok(id) => doc_mut!(cx.editor, &id), - Err(e) => { - cx.editor.set_error(format!( - "Failed to open file '{}': {}", - path.display(), - e - )); - return; - } - }; - - let line_num = *line_num; - let view = view_mut!(cx.editor); - let text = doc.text(); - if line_num >= text.len_lines() { - cx.editor.set_error( + let line_num = *line_num; + let view = view_mut!(cx.editor); + let text = doc.text(); + if line_num >= text.len_lines() { + cx.editor.set_error( "The line you jumped to does not exist anymore because the file has changed.", ); - return; - } - let start = text.line_to_char(line_num); - let end = text.line_to_char((line_num + 1).min(text.len_lines())); + return; + } + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); - doc.set_selection(view.id, Selection::single(start, end)); - if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); - } - }, - ) - .with_preview( - |_editor, FileResult { path, line_num, .. }| { - Some((path.clone().into(), Some((*line_num, *line_num)))) - }, - ); - compositor.push(Box::new(overlaid(picker))) - }; - Ok(Callback::EditorCompositor(Box::new(call))) - }) - } else { - // Otherwise do nothing - // log::warn!("Global Search Invalid Pattern") + doc.set_selection(view.id, Selection::single(start, end)); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); } }, - ); + ) + .with_preview(|_editor, FileResult { path, line_num, .. }| { + Some((path.clone().into(), Some((*line_num, *line_num)))) + }) + .with_dynamic_query(get_files, Some(275)); + + if let Some((reg, line)) = cx + .register + .and_then(|reg| Some((reg, cx.editor.registers.first(reg, cx.editor)?))) + { + picker = picker.with_line(line.into_owned(), cx.editor); + cx.editor.registers.last_search_register = reg; + } + + cx.push_layer(Box::new(overlaid(picker))); } enum Extend { From 7b1131adf62f649daeb197b07327f5729d20a2b7 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 6 Mar 2024 11:04:37 -0500 Subject: [PATCH 113/200] global_search: Suggest latest '/' register value --- helix-term/src/commands.rs | 14 +++++--------- helix-term/src/ui/picker.rs | 22 ++++++++++++++++------ helix-term/src/ui/prompt.rs | 21 +++++++++++++++------ 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b7ffeeb77..1df25ba52 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2407,7 +2407,10 @@ struct GlobalSearchConfig { .boxed() }; - let mut picker = Picker::new( + let reg = cx.register.unwrap_or('/'); + cx.editor.registers.last_search_register = reg; + + let picker = Picker::new( columns, 1, // contents vec![], @@ -2443,16 +2446,9 @@ struct GlobalSearchConfig { .with_preview(|_editor, FileResult { path, line_num, .. }| { Some((path.clone().into(), Some((*line_num, *line_num)))) }) + .with_history_register(Some(reg)) .with_dynamic_query(get_files, Some(275)); - if let Some((reg, line)) = cx - .register - .and_then(|reg| Some((reg, cx.editor.registers.first(reg, cx.editor)?))) - { - picker = picker.with_line(line.into_owned(), cx.editor); - cx.editor.registers.last_search_register = reg; - } - cx.push_layer(Box::new(overlaid(picker))); } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index bfec9ddb4..76a1805a0 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -391,9 +391,8 @@ pub fn with_preview( self } - pub fn with_line(mut self, line: String, editor: &Editor) -> Self { - self.prompt.set_line(line, editor); - self.handle_prompt_change(); + pub fn with_history_register(mut self, history_register: Option) -> Self { + self.prompt.with_history_register(history_register); self } @@ -977,10 +976,21 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { } } key!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(ctx, option, Action::Replace); + // If the prompt has a history completion and is empty, use enter to accept + // that completion + if let Some(completion) = self + .prompt + .first_history_completion(ctx.editor) + .filter(|_| self.prompt.line().is_empty()) + { + self.prompt.set_line(completion.to_string(), ctx.editor); + self.handle_prompt_change(); + } else { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::Replace); + } + return close_fn(self); } - return close_fn(self); } ctrl!('s') => { if let Some(option) = self.selection() { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 19183470c..8e8896183 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -117,6 +117,19 @@ pub fn line(&self) -> &String { &self.line } + pub fn with_history_register(&mut self, history_register: Option) -> &mut Self { + self.history_register = history_register; + self + } + + pub(crate) fn first_history_completion<'a>( + &'a self, + editor: &'a Editor, + ) -> Option> { + self.history_register + .and_then(|reg| editor.registers.first(reg, editor)) + } + pub fn recalculate_completion(&mut self, editor: &Editor) { self.exit_selection(); self.completion = (self.completion_fn)(editor, &self.line); @@ -480,10 +493,7 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line); if self.line.is_empty() { // Show the most recently entered value as a suggestion. - if let Some(suggestion) = self - .history_register - .and_then(|reg| cx.editor.registers.first(reg, cx.editor)) - { + if let Some(suggestion) = self.first_history_completion(cx.editor) { surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color); } } else if let Some((language, loader)) = self.language.as_ref() { @@ -578,8 +588,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { self.recalculate_completion(cx.editor); } else { let last_item = self - .history_register - .and_then(|reg| cx.editor.registers.first(reg, cx.editor)) + .first_history_completion(cx.editor) .map(|entry| entry.to_string()) .unwrap_or_else(|| String::from("")); From 6492f17e7da54081d9fb3bd2bf6828d0ef7f1a87 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 24 Mar 2024 11:52:57 -0400 Subject: [PATCH 114/200] Add a hidden column for the global search line contents We could expand on this in the future to have different preview modes that you can toggle between with C-t. Currently that binding just hides the preview but it could switch between different preview modes and in one mode hide the path and just show the line contents. --- helix-term/src/commands.rs | 17 ++++------------- helix-term/src/ui/picker.rs | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1df25ba52..b23c52787 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2243,15 +2243,13 @@ struct FileResult { path: PathBuf, /// 0 indexed lines line_num: usize, - line_content: String, } impl FileResult { - fn new(path: &Path, line_num: usize, line_content: String) -> Self { + fn new(path: &Path, line_num: usize) -> Self { Self { path: path.to_path_buf(), line_num, - line_content, } } } @@ -2272,10 +2270,7 @@ struct GlobalSearchConfig { let path = helix_stdx::path::get_relative_path(&item.path); format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into() }), - PickerColumn::new("contents", |item: &FileResult, _| { - item.line_content.as_str().into() - }) - .without_filtering(), + PickerColumn::hidden("contents"), ]; let get_files = |query: &str, @@ -2355,13 +2350,9 @@ struct GlobalSearchConfig { }; let mut stop = false; - let sink = sinks::UTF8(|line_num, line_content| { + let sink = sinks::UTF8(|line_num, _line_content| { stop = injector - .push(FileResult::new( - entry.path(), - line_num as usize - 1, - line_content.to_string(), - )) + .push(FileResult::new(entry.path(), line_num as usize - 1)) .is_err(); Ok(!stop) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 76a1805a0..6394fb87f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -191,6 +191,7 @@ pub struct Column { /// `DynamicPicker` uses this so that the dynamic column (for example regex in /// global search) is not used for filtering twice. filter: bool, + hidden: bool, } impl Column { @@ -199,6 +200,19 @@ pub fn new(name: impl Into>, format: ColumnFormatFn) -> Self { name: name.into(), format, filter: true, + hidden: false, + } + } + + /// A column which does not display any contents + pub fn hidden(name: impl Into>) -> Self { + let format = |_: &T, _: &D| unreachable!(); + + Self { + name: name.into(), + format, + filter: false, + hidden: true, } } @@ -677,6 +691,10 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) let mut matcher_index = 0; Row::new(self.columns.iter().map(|column| { + if column.hidden { + return Cell::default(); + } + let Some(Constraint::Length(max_width)) = widths.next() else { unreachable!(); }; @@ -757,7 +775,11 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) let header_style = cx.editor.theme.get("ui.picker.header"); table = table.header(Row::new(self.columns.iter().map(|column| { - Cell::from(Span::styled(Cow::from(&*column.name), header_style)) + if column.hidden { + Cell::default() + } else { + Cell::from(Span::styled(Cow::from(&*column.name), header_style)) + } }))); } From 6ccbfe9bdfedb95f2e3bb8e80bfdb11b5d01797e Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 26 Mar 2024 15:13:51 -0400 Subject: [PATCH 115/200] Request a UI redraw on Drop of an Injector This fixes the changed files picker when used against a clean worktree for example. Without it the running indicator does not disappear. It also simplifies the dynamic query handler's implementation so that it doesn't need to request a redraw explicitly. Co-authored-by: Pascal Kuthe --- helix-event/src/lib.rs | 4 +++- helix-event/src/redraw.rs | 9 +++++++++ helix-term/src/ui/picker.rs | 9 +++++++++ helix-term/src/ui/picker/handlers.rs | 7 ++----- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/helix-event/src/lib.rs b/helix-event/src/lib.rs index 894de5e8d..de018a79d 100644 --- a/helix-event/src/lib.rs +++ b/helix-event/src/lib.rs @@ -34,7 +34,9 @@ use anyhow::Result; pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx}; pub use debounce::{send_blocking, AsyncHook}; -pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard}; +pub use redraw::{ + lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop, +}; pub use registry::Event; mod cancel; diff --git a/helix-event/src/redraw.rs b/helix-event/src/redraw.rs index 8fadb8aea..d1a188995 100644 --- a/helix-event/src/redraw.rs +++ b/helix-event/src/redraw.rs @@ -51,3 +51,12 @@ pub fn start_frame() { pub fn lock_frame() -> RenderLockGuard { RENDER_LOCK.read() } + +/// A zero sized type that requests a redraw via [request_redraw] when the type [Drop]s. +pub struct RequestRedrawOnDrop; + +impl Drop for RequestRedrawOnDrop { + fn drop(&mut self) { + request_redraw(); + } +} diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6394fb87f..1c47552d0 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -153,6 +153,12 @@ pub struct Injector { editor_data: Arc, version: usize, picker_version: Arc, + /// A marker that requests a redraw when the injector drops. + /// This marker causes the "running" indicator to disappear when a background job + /// providing items is finished and drops. This could be wrapped in an [Arc] to ensure + /// that the redraw is only requested when all Injectors drop for a Picker (which removes + /// the "running" indicator) but the redraw handle is debounced so this is unnecessary. + _redraw: helix_event::RequestRedrawOnDrop, } impl Clone for Injector { @@ -163,6 +169,7 @@ fn clone(&self) -> Self { editor_data: self.editor_data.clone(), version: self.version, picker_version: self.picker_version.clone(), + _redraw: helix_event::RequestRedrawOnDrop, } } } @@ -284,6 +291,7 @@ pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injecto editor_data: Arc::new(editor_data), version: 0, picker_version: Arc::new(AtomicUsize::new(0)), + _redraw: helix_event::RequestRedrawOnDrop, }; (matcher, streamer) } @@ -386,6 +394,7 @@ pub fn injector(&self) -> Injector { editor_data: self.editor_data.clone(), version: self.version.load(atomic::Ordering::Relaxed), picker_version: self.version.clone(), + _redraw: helix_event::RequestRedrawOnDrop, } } diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs index e426adda7..4896ccbc6 100644 --- a/helix-term/src/ui/picker/handlers.rs +++ b/helix-term/src/ui/picker/handlers.rs @@ -174,11 +174,8 @@ fn finish_debounce(&mut self) { if let Err(err) = get_options.await { log::info!("Dynamic request failed: {err}"); } - // The picker's shows its running indicator when there are any active - // injectors. When we're done injecting new options, drop the injector - // and request a redraw to remove the running indicator. - drop(injector); - helix_event::request_redraw(); + // NOTE: the Drop implementation of Injector will request a redraw when the + // injector falls out of scope here, clearing the "running" indicator. }); }) } From 408282097f4df04444b87283c5a64e09b8509122 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 2 Apr 2024 02:34:20 +0200 Subject: [PATCH 116/200] avoid collecting columns to a temporary vec --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- helix-term/src/ui/picker.rs | 13 ++++--------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8e65a8f3..89c8553c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1779,9 +1779,9 @@ dependencies = [ [[package]] name = "nucleo" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6350a138d8860658523a7593cbf6813999d17a099371d14f70c5c905b37593e9" +checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4" dependencies = [ "nucleo-matcher", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index d9541b648..e7f784428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ package.helix-term.opt-level = 2 [workspace.dependencies] tree-sitter = { version = "0.22" } -nucleo = "0.4.1" +nucleo = "0.5.0" slotmap = "1.0.7" thiserror = "1.0" diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 1c47552d0..69a87f25b 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -15,7 +15,7 @@ use futures_util::future::BoxFuture; use helix_event::AsyncHook; use nucleo::pattern::{CaseMatching, Normalization}; -use nucleo::{Config, Nucleo, Utf32String}; +use nucleo::{Config, Nucleo}; use thiserror::Error; use tokio::sync::mpsc::Sender; use tui::{ @@ -135,14 +135,9 @@ fn inject_nucleo_item( item: T, editor_data: &D, ) { - let column_texts: Vec = columns - .iter() - .filter(|column| column.filter) - .map(|column| column.format_text(&item, editor_data).into()) - .collect(); - injector.push(item, |dst| { - for (i, text) in column_texts.into_iter().enumerate() { - dst[i] = text; + injector.push(item, |item, dst| { + for (column, text) in columns.iter().filter(|column| column.filter).zip(dst) { + *text = column.format_text(item, editor_data).into() } }); } From f4a433f855880667eb7888557c2e8a4b6d2c6dd3 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 5 Apr 2024 12:20:35 -0400 Subject: [PATCH 117/200] Convert LSP URIs into custom URIs This introduces a custom URI type in core meant to be extended later if we want to support other schemes. For now it's just a wrapper over a PathBuf. We use this new URI type to firewall `lsp::Url`. This was previously done in 8141a4a but using a custom URI type is more flexible and will improve the way Pickers handle paths for previews in the child commit(s). Co-authored-by: soqb --- Cargo.lock | 1 + helix-core/Cargo.toml | 1 + helix-core/src/lib.rs | 3 + helix-core/src/uri.rs | 122 +++++++++++++++++++++++++++++++++ helix-term/src/application.rs | 26 +++---- helix-term/src/commands/lsp.rs | 63 ++++++++++------- helix-view/src/document.rs | 4 ++ helix-view/src/editor.rs | 12 ++-- helix-view/src/handlers/lsp.rs | 67 ++++++++++++------ 9 files changed, 235 insertions(+), 64 deletions(-) create mode 100644 helix-core/src/uri.rs diff --git a/Cargo.lock b/Cargo.lock index 89c8553c0..f1cd1632f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,6 +1331,7 @@ dependencies = [ "unicode-general-category", "unicode-segmentation", "unicode-width", + "url", ] [[package]] diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index c6b87e682..392b4a4ca 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -34,6 +34,7 @@ bitflags = "2.6" ahash = "0.8.11" hashbrown = { version = "0.14.5", features = ["raw"] } dunce = "1.0" +url = "2.5.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 1abd90d10..681d3456d 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -27,6 +27,7 @@ pub mod text_annotations; pub mod textobject; mod transaction; +pub mod uri; pub mod wrap; pub mod unicode { @@ -66,3 +67,5 @@ pub mod unicode { pub use line_ending::{LineEnding, NATIVE_LINE_ENDING}; pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction}; + +pub use uri::Uri; diff --git a/helix-core/src/uri.rs b/helix-core/src/uri.rs new file mode 100644 index 000000000..4e03c58b1 --- /dev/null +++ b/helix-core/src/uri.rs @@ -0,0 +1,122 @@ +use std::path::{Path, PathBuf}; + +/// A generic pointer to a file location. +/// +/// Currently this type only supports paths to local files. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum Uri { + File(PathBuf), +} + +impl Uri { + // This clippy allow mirrors url::Url::from_file_path + #[allow(clippy::result_unit_err)] + pub fn to_url(&self) -> Result { + match self { + Uri::File(path) => url::Url::from_file_path(path), + } + } + + pub fn as_path(&self) -> Option<&Path> { + match self { + Self::File(path) => Some(path), + } + } + + pub fn as_path_buf(self) -> Option { + match self { + Self::File(path) => Some(path), + } + } +} + +impl From for Uri { + fn from(path: PathBuf) -> Self { + Self::File(path) + } +} + +impl TryFrom for PathBuf { + type Error = (); + + fn try_from(uri: Uri) -> Result { + match uri { + Uri::File(path) => Ok(path), + } + } +} + +#[derive(Debug)] +pub struct UrlConversionError { + source: url::Url, + kind: UrlConversionErrorKind, +} + +#[derive(Debug)] +pub enum UrlConversionErrorKind { + UnsupportedScheme, + UnableToConvert, +} + +impl std::fmt::Display for UrlConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + UrlConversionErrorKind::UnsupportedScheme => { + write!(f, "unsupported scheme in URL: {}", self.source.scheme()) + } + UrlConversionErrorKind::UnableToConvert => { + write!(f, "unable to convert URL to file path: {}", self.source) + } + } + } +} + +impl std::error::Error for UrlConversionError {} + +fn convert_url_to_uri(url: &url::Url) -> Result { + if url.scheme() == "file" { + url.to_file_path() + .map(|path| Uri::File(helix_stdx::path::normalize(path))) + .map_err(|_| UrlConversionErrorKind::UnableToConvert) + } else { + Err(UrlConversionErrorKind::UnsupportedScheme) + } +} + +impl TryFrom for Uri { + type Error = UrlConversionError; + + fn try_from(url: url::Url) -> Result { + convert_url_to_uri(&url).map_err(|kind| Self::Error { source: url, kind }) + } +} + +impl TryFrom<&url::Url> for Uri { + type Error = UrlConversionError; + + fn try_from(url: &url::Url) -> Result { + convert_url_to_uri(url).map_err(|kind| Self::Error { + source: url.clone(), + kind, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use url::Url; + + #[test] + fn unknown_scheme() { + let url = Url::parse("csharp:/metadata/foo/bar/Baz.cs").unwrap(); + assert!(matches!( + Uri::try_from(url), + Err(UrlConversionError { + kind: UrlConversionErrorKind::UnsupportedScheme, + .. + }) + )); + } +} diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 9adc764cc..9695703b7 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -735,10 +735,10 @@ macro_rules! language_server { } } Notification::PublishDiagnostics(mut params) => { - let path = match params.uri.to_file_path() { - Ok(path) => helix_stdx::path::normalize(path), - Err(_) => { - log::error!("Unsupported file URI: {}", params.uri); + let uri = match helix_core::Uri::try_from(params.uri) { + Ok(uri) => uri, + Err(err) => { + log::error!("{err}"); return; } }; @@ -749,11 +749,11 @@ macro_rules! language_server { } // have to inline the function because of borrow checking... let doc = self.editor.documents.values_mut() - .find(|doc| doc.path().map(|p| p == &path).unwrap_or(false)) + .find(|doc| doc.uri().is_some_and(|u| u == uri)) .filter(|doc| { if let Some(version) = params.version { if version != doc.version() { - log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version()); + log::info!("Version ({version}) is out of date for {uri:?} (expected ({}), dropping PublishDiagnostic notification", doc.version()); return false; } } @@ -765,7 +765,7 @@ macro_rules! language_server { let lang_conf = doc.language.clone(); if let Some(lang_conf) = &lang_conf { - if let Some(old_diagnostics) = self.editor.diagnostics.get(&path) { + if let Some(old_diagnostics) = self.editor.diagnostics.get(&uri) { if !lang_conf.persistent_diagnostic_sources.is_empty() { // Sort diagnostics first by severity and then by line numbers. // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order @@ -798,7 +798,7 @@ macro_rules! language_server { // Insert the original lsp::Diagnostics here because we may have no open document // for diagnosic message and so we can't calculate the exact position. // When using them later in the diagnostics picker, we calculate them on-demand. - let diagnostics = match self.editor.diagnostics.entry(path) { + let diagnostics = match self.editor.diagnostics.entry(uri) { Entry::Occupied(o) => { let current_diagnostics = o.into_mut(); // there may entries of other language servers, which is why we can't overwrite the whole entry @@ -1132,20 +1132,22 @@ fn handle_show_document( .. } = params; - let path = match uri.to_file_path() { - Ok(path) => path, + let uri = match helix_core::Uri::try_from(uri) { + Ok(uri) => uri, Err(err) => { - log::error!("unsupported file URI: {}: {:?}", uri, err); + log::error!("{err}"); return lsp::ShowDocumentResult { success: false }; } }; + // If `Uri` gets another variant other than `Path` this may not be valid. + let path = uri.as_path().expect("URIs are valid paths"); let action = match take_focus { Some(true) => helix_view::editor::Action::Replace, _ => helix_view::editor::Action::VerticalSplit, }; - let doc_id = match self.editor.open(&path, action) { + let doc_id = match self.editor.open(path, action) { Ok(id) => id, Err(err) => { log::error!("failed to open path: {:?}: {:?}", uri, err); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 23bcb977f..fae0833e0 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -13,7 +13,9 @@ use super::{align_view, push_jump, Align, Context, Editor}; -use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection}; +use helix_core::{ + syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri, +}; use helix_stdx::path; use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId}, @@ -34,7 +36,7 @@ collections::{BTreeMap, HashSet}, fmt::Write, future::Future, - path::{Path, PathBuf}, + path::Path, }; /// Gets the first language server that is attached to a document which supports a specific feature. @@ -72,7 +74,7 @@ struct DiagnosticStyles { } struct PickerDiagnostic { - path: PathBuf, + uri: Uri, diag: lsp::Diagnostic, offset_encoding: OffsetEncoding, } @@ -183,20 +185,20 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap>, + diagnostics: BTreeMap>, format: DiagnosticsFormat, ) -> DiagnosticsPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs let mut flat_diag = Vec::new(); - for (path, diags) in diagnostics { + for (uri, diags) in diagnostics { flat_diag.reserve(diags.len()); for (diag, ls) in diags { if let Some(ls) = cx.editor.language_server_by_id(ls) { flat_diag.push(PickerDiagnostic { - path: path.clone(), + uri: uri.clone(), diag, offset_encoding: ls.offset_encoding(), }); @@ -243,8 +245,14 @@ fn diag_picker( // between message code and message 2, ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| { - let path = path::get_truncated_path(&item.path); - path.to_string_lossy().to_string().into() + if let Some(path) = item.uri.as_path() { + path::get_truncated_path(path) + .to_string_lossy() + .to_string() + .into() + } else { + Default::default() + } }), ); primary_column += 1; @@ -257,17 +265,20 @@ fn diag_picker( styles, move |cx, PickerDiagnostic { - path, + uri, diag, offset_encoding, }, action| { + let Some(path) = uri.as_path() else { + return; + }; jump_to_position(cx.editor, path, diag.range, *offset_encoding, action) }, ) - .with_preview(move |_editor, PickerDiagnostic { path, diag, .. }| { + .with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| { let line = Some((diag.range.start.line as usize, diag.range.end.line as usize)); - Some((path.clone().into(), line)) + Some((uri.clone().as_path_buf()?.into(), line)) }) .truncate_start(false) } @@ -456,12 +467,17 @@ pub fn workspace_symbol_picker(cx: &mut Context) { }) .without_filtering(), ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| { - match item.symbol.location.uri.to_file_path() { - Ok(path) => path::get_relative_path(path.as_path()) - .to_string_lossy() - .to_string() - .into(), - Err(_) => item.symbol.location.uri.to_string().into(), + if let Ok(uri) = Uri::try_from(&item.symbol.location.uri) { + if let Some(path) = uri.as_path() { + path::get_relative_path(path) + .to_string_lossy() + .to_string() + .into() + } else { + item.symbol.location.uri.to_string().into() + } + } else { + item.symbol.location.uri.to_string().into() } }), ]; @@ -489,16 +505,11 @@ pub fn workspace_symbol_picker(cx: &mut Context) { pub fn diagnostics_picker(cx: &mut Context) { let doc = doc!(cx.editor); - if let Some(current_path) = doc.path() { - let diagnostics = cx - .editor - .diagnostics - .get(current_path) - .cloned() - .unwrap_or_default(); + if let Some(uri) = doc.uri() { + let diagnostics = cx.editor.diagnostics.get(&uri).cloned().unwrap_or_default(); let picker = diag_picker( cx, - [(current_path.clone(), diagnostics)].into(), + [(uri, diagnostics)].into(), DiagnosticsFormat::HideSourcePath, ); cx.push_layer(Box::new(overlaid(picker))); @@ -842,6 +853,8 @@ fn goto_impl( // With the preallocation above and UTF-8 paths already, this closure will do one (1) // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`. if let Ok(path) = item.uri.to_file_path() { + // We don't convert to a `helix_core::Uri` here because we've already checked the scheme. + // This path won't be normalized but it's only used for display. res.push_str( &path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(), ); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ccf2fa8c3..3314a243f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1741,6 +1741,10 @@ pub fn url(&self) -> Option { Url::from_file_path(self.path()?).ok() } + pub fn uri(&self) -> Option { + Some(self.path()?.clone().into()) + } + #[inline] pub fn text(&self) -> &Rope { &self.text diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3eeb4830e..ae942cf08 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -44,7 +44,7 @@ use helix_core::{ auto_pairs::AutoPairs, syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap}, - Change, LineEnding, Position, Range, Selection, NATIVE_LINE_ENDING, + Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING, }; use helix_dap as dap; use helix_lsp::lsp; @@ -1022,7 +1022,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, pub language_servers: helix_lsp::Registry, - pub diagnostics: BTreeMap>, + pub diagnostics: BTreeMap>, pub diff_providers: DiffProviderRegistry, pub debugger: Option, @@ -1929,7 +1929,7 @@ pub fn document_by_path_mut>(&mut self, path: P) -> Option<&mut D /// Returns all supported diagnostics for the document pub fn doc_diagnostics<'a>( language_servers: &'a helix_lsp::Registry, - diagnostics: &'a BTreeMap>, + diagnostics: &'a BTreeMap>, document: &Document, ) -> impl Iterator + 'a { Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true) @@ -1939,15 +1939,15 @@ pub fn doc_diagnostics<'a>( /// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from pub fn doc_diagnostics_with_filter<'a>( language_servers: &'a helix_lsp::Registry, - diagnostics: &'a BTreeMap>, + diagnostics: &'a BTreeMap>, document: &Document, filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a, ) -> impl Iterator + 'a { let text = document.text().clone(); let language_config = document.language.clone(); document - .path() - .and_then(|path| diagnostics.get(path)) + .uri() + .and_then(|uri| diagnostics.get(&uri)) .map(|diags| { diags.iter().filter_map(move |(diagnostic, lsp_id)| { let ls = language_servers.get_by_id(*lsp_id)?; diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index beb106b2b..d817a4235 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -1,6 +1,7 @@ use crate::editor::Action; use crate::Editor; use crate::{DocumentId, ViewId}; +use helix_core::Uri; use helix_lsp::util::generate_transaction_from_edits; use helix_lsp::{lsp, OffsetEncoding}; @@ -54,18 +55,30 @@ pub struct ApplyEditError { pub enum ApplyEditErrorKind { DocumentChanged, FileNotFound, - UnknownURISchema, + InvalidUrl(helix_core::uri::UrlConversionError), IoError(std::io::Error), // TODO: check edits before applying and propagate failure // InvalidEdit, } +impl From for ApplyEditErrorKind { + fn from(err: std::io::Error) -> Self { + ApplyEditErrorKind::IoError(err) + } +} + +impl From for ApplyEditErrorKind { + fn from(err: helix_core::uri::UrlConversionError) -> Self { + ApplyEditErrorKind::InvalidUrl(err) + } +} + impl ToString for ApplyEditErrorKind { fn to_string(&self) -> String { match self { ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(), ApplyEditErrorKind::FileNotFound => "file not found".to_string(), - ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(), + ApplyEditErrorKind::InvalidUrl(err) => err.to_string(), ApplyEditErrorKind::IoError(err) => err.to_string(), } } @@ -74,25 +87,28 @@ fn to_string(&self) -> String { impl Editor { fn apply_text_edits( &mut self, - uri: &helix_lsp::Url, + url: &helix_lsp::Url, version: Option, text_edits: Vec, offset_encoding: OffsetEncoding, ) -> Result<(), ApplyEditErrorKind> { - let path = match uri.to_file_path() { - Ok(path) => path, - Err(_) => { - let err = format!("unable to convert URI to filepath: {}", uri); - log::error!("{}", err); - self.set_error(err); - return Err(ApplyEditErrorKind::UnknownURISchema); + let uri = match Uri::try_from(url) { + Ok(uri) => uri, + Err(err) => { + log::error!("{err}"); + return Err(err.into()); } }; + let path = uri.as_path().expect("URIs are valid paths"); - let doc_id = match self.open(&path, Action::Load) { + let doc_id = match self.open(path, Action::Load) { Ok(doc_id) => doc_id, Err(err) => { - let err = format!("failed to open document: {}: {}", uri, err); + let err = format!( + "failed to open document: {}: {}", + path.to_string_lossy(), + err + ); log::error!("{}", err); self.set_error(err); return Err(ApplyEditErrorKind::FileNotFound); @@ -158,9 +174,9 @@ pub fn apply_workspace_edit( for (i, operation) in operations.iter().enumerate() { match operation { lsp::DocumentChangeOperation::Op(op) => { - self.apply_document_resource_op(op).map_err(|io| { + self.apply_document_resource_op(op).map_err(|err| { ApplyEditError { - kind: ApplyEditErrorKind::IoError(io), + kind: err, failed_change_idx: i, } })?; @@ -214,12 +230,18 @@ pub fn apply_workspace_edit( Ok(()) } - fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Result<()> { + fn apply_document_resource_op( + &mut self, + op: &lsp::ResourceOp, + ) -> Result<(), ApplyEditErrorKind> { use lsp::ResourceOp; use std::fs; + // NOTE: If `Uri` gets another variant than `Path`, the below `expect`s + // may no longer be valid. match op { ResourceOp::Create(op) => { - let path = op.uri.to_file_path().unwrap(); + let uri = Uri::try_from(&op.uri)?; + let path = uri.as_path_buf().expect("URIs are valid paths"); let ignore_if_exists = op.options.as_ref().map_or(false, |options| { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) }); @@ -236,7 +258,8 @@ fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Resul } } ResourceOp::Delete(op) => { - let path = op.uri.to_file_path().unwrap(); + let uri = Uri::try_from(&op.uri)?; + let path = uri.as_path_buf().expect("URIs are valid paths"); if path.is_dir() { let recursive = op .options @@ -251,17 +274,19 @@ fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Resul } self.language_servers.file_event_handler.file_changed(path); } else if path.is_file() { - fs::remove_file(&path)?; + fs::remove_file(path)?; } } ResourceOp::Rename(op) => { - let from = op.old_uri.to_file_path().unwrap(); - let to = op.new_uri.to_file_path().unwrap(); + let from_uri = Uri::try_from(&op.old_uri)?; + let from = from_uri.as_path().expect("URIs are valid paths"); + let to_uri = Uri::try_from(&op.new_uri)?; + let to = to_uri.as_path().expect("URIs are valid paths"); let ignore_if_exists = op.options.as_ref().map_or(false, |options| { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) }); if !ignore_if_exists || !to.exists() { - self.move_path(&from, &to)?; + self.move_path(from, to)?; } } } From 3906f6605fd1ba1ed72ada9f4bbd8b88facc51ce Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 5 Apr 2024 14:49:02 -0400 Subject: [PATCH 118/200] Avoid allocations in Picker file preview callback The `FileLocation` and `PathOrId` types can borrow paths rather than requiring them to be owned. This takes a refactor of the preview functions and preview internals within `Picker`. With this change we avoid an unnecessary `PathBuf` clone per render for any picker with a file preview function (i.e. most pickers). This refactor is not fully complete. The `PathOrId` is _sometimes_ an owned `PathBuf`. This is for pragmatic reasons rather than technical ones. We need a further refactor to introduce more core types like `Location` in order to eliminate the Cow and only use `&Path`s within `PathOrId`. This is left for future work as it will be a larger refactor almost entirely fitting into the LSP commands module and helix-core - i.e. mostly unrelated to refactoring the `Picker` code itself. Co-authored-by: Pascal Kuthe --- helix-term/src/commands.rs | 4 +- helix-term/src/commands/dap.rs | 6 +-- helix-term/src/commands/lsp.rs | 90 ++++++++++++++++++++++++---------- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 84 ++++++++++++++++--------------- 5 files changed, 112 insertions(+), 74 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b23c52787..7e59bbdd6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2435,7 +2435,7 @@ struct GlobalSearchConfig { }, ) .with_preview(|_editor, FileResult { path, line_num, .. }| { - Some((path.clone().into(), Some((*line_num, *line_num)))) + Some((path.as_path().into(), Some((*line_num, *line_num)))) }) .with_history_register(Some(reg)) .with_dynamic_query(get_files, Some(275)); @@ -3098,7 +3098,7 @@ pub struct FileChangeData { } }, ) - .with_preview(|_editor, meta| Some((meta.path().to_path_buf().into(), None))); + .with_preview(|_editor, meta| Some((meta.path().into(), None))); let injector = picker.injector(); cx.editor diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 14cb83d29..39c79a66c 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -61,7 +61,7 @@ fn thread_picker( .with_preview(move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; let frame = frames.first()?; - let path = frame.source.as_ref()?.path.clone()?; + let path = frame.source.as_ref()?.path.as_ref()?.as_path(); let pos = Some(( frame.line.saturating_sub(1), frame.end_line.unwrap_or(frame.line).saturating_sub(1), @@ -747,10 +747,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { frame .source .as_ref() - .and_then(|source| source.path.clone()) + .and_then(|source| source.path.as_ref()) .map(|path| { ( - path.into(), + path.as_path().into(), Some(( frame.line.saturating_sub(1), frame.end_line.unwrap_or(frame.line).saturating_sub(1), diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index fae0833e0..059fb814d 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -64,6 +64,7 @@ macro_rules! language_server_with_feature { struct SymbolInformationItem { symbol: lsp::SymbolInformation, offset_encoding: OffsetEncoding, + uri: Uri, } struct DiagnosticStyles { @@ -79,13 +80,10 @@ struct PickerDiagnostic { offset_encoding: OffsetEncoding, } -fn location_to_file_location(location: &lsp::Location) -> FileLocation { - let path = location.uri.to_file_path().unwrap(); - let line = Some(( - location.range.start.line as usize, - location.range.end.line as usize, - )); - (path.into(), line) +fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option> { + let path = uri.as_path()?; + let line = Some((range.start.line as usize, range.end.line as usize)); + Some((path.into(), line)) } fn jump_to_location( @@ -278,7 +276,7 @@ fn diag_picker( ) .with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| { let line = Some((diag.range.start.line as usize, diag.range.end.line as usize)); - Some((uri.clone().as_path_buf()?.into(), line)) + Some((uri.as_path()?.into(), line)) }) .truncate_start(false) } @@ -287,6 +285,7 @@ pub fn symbol_picker(cx: &mut Context) { fn nested_to_flat( list: &mut Vec, file: &lsp::TextDocumentIdentifier, + uri: &Uri, symbol: lsp::DocumentSymbol, offset_encoding: OffsetEncoding, ) { @@ -301,9 +300,10 @@ fn nested_to_flat( container_name: None, }, offset_encoding, + uri: uri.clone(), }); for child in symbol.children.into_iter().flatten() { - nested_to_flat(list, file, child, offset_encoding); + nested_to_flat(list, file, uri, child, offset_encoding); } } let doc = doc!(cx.editor); @@ -317,6 +317,9 @@ fn nested_to_flat( let request = language_server.document_symbols(doc.identifier()).unwrap(); let offset_encoding = language_server.offset_encoding(); let doc_id = doc.identifier(); + let doc_uri = doc + .uri() + .expect("docs with active language servers must be backed by paths"); async move { let json = request.await?; @@ -331,6 +334,7 @@ fn nested_to_flat( lsp::DocumentSymbolResponse::Flat(symbols) => symbols .into_iter() .map(|symbol| SymbolInformationItem { + uri: doc_uri.clone(), symbol, offset_encoding, }) @@ -338,7 +342,13 @@ fn nested_to_flat( lsp::DocumentSymbolResponse::Nested(symbols) => { let mut flat_symbols = Vec::new(); for symbol in symbols { - nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) + nested_to_flat( + &mut flat_symbols, + &doc_id, + &doc_uri, + symbol, + offset_encoding, + ) } flat_symbols } @@ -388,7 +398,7 @@ fn nested_to_flat( }, ) .with_preview(move |_editor, item| { - Some(location_to_file_location(&item.symbol.location)) + uri_to_file_location(&item.uri, &item.symbol.location.range) }) .truncate_start(false); @@ -431,9 +441,19 @@ pub fn workspace_symbol_picker(cx: &mut Context) { serde_json::from_value::>>(json)? .unwrap_or_default() .into_iter() - .map(|symbol| SymbolInformationItem { - symbol, - offset_encoding, + .filter_map(|symbol| { + let uri = match Uri::try_from(&symbol.location.uri) { + Ok(uri) => uri, + Err(err) => { + log::warn!("discarding symbol with invalid URI: {err}"); + return None; + } + }; + Some(SymbolInformationItem { + symbol, + uri, + offset_encoding, + }) }) .collect(); @@ -467,15 +487,11 @@ pub fn workspace_symbol_picker(cx: &mut Context) { }) .without_filtering(), ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| { - if let Ok(uri) = Uri::try_from(&item.symbol.location.uri) { - if let Some(path) = uri.as_path() { - path::get_relative_path(path) - .to_string_lossy() - .to_string() - .into() - } else { - item.symbol.location.uri.to_string().into() - } + if let Some(path) = item.uri.as_path() { + path::get_relative_path(path) + .to_string_lossy() + .to_string() + .into() } else { item.symbol.location.uri.to_string().into() } @@ -496,7 +512,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { ); }, ) - .with_preview(|_editor, item| Some(location_to_file_location(&item.symbol.location))) + .with_preview(|_editor, item| uri_to_file_location(&item.uri, &item.symbol.location.range)) .with_dynamic_query(get_symbols, None) .truncate_start(false); @@ -875,7 +891,31 @@ fn goto_impl( let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) }) - .with_preview(move |_editor, location| Some(location_to_file_location(location))); + .with_preview(move |_editor, location| { + use crate::ui::picker::PathOrId; + + let lines = Some(( + location.range.start.line as usize, + location.range.end.line as usize, + )); + + // TODO: we should avoid allocating by doing the Uri conversion ahead of time. + // + // To do this, introduce a `Location` type in `helix-core` that reuses the core + // `Uri` type instead of the LSP `Url` type and replaces the LSP `Range` type. + // Refactor the callers of `goto_impl` to pass iterators that translate the + // LSP location type to the custom one in core, or have them collect and pass + // `Vec`s. Replace the `uri_to_file_location` function with + // `location_to_file_location` that takes only `&helix_core::Location` as + // parameters. + // + // By doing this we can also eliminate the duplicated URI info in the + // `SymbolInformationItem` type and introduce a custom Symbol type in `helix-core` + // which will be reused in the future for tree-sitter based symbol pickers. + let path = Uri::try_from(&location.uri).ok()?.as_path_buf()?; + #[allow(deprecated)] + Some((PathOrId::from_path_buf(path), lines)) + }); compositor.push(Box::new(overlaid(picker))); } } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 93ac2e651..14d92b575 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -244,7 +244,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } }, ) - .with_preview(|_editor, path| Some((path.clone().into(), None))); + .with_preview(|_editor, path| Some((path.as_path().into(), None))); let injector = picker.injector(); let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 69a87f25b..4f5095300 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -60,37 +60,41 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; #[derive(PartialEq, Eq, Hash)] -pub enum PathOrId { +pub enum PathOrId<'a> { Id(DocumentId), - Path(Arc), + // See [PathOrId::from_path_buf]: this will eventually become `Path(&Path)`. + Path(Cow<'a, Path>), } -impl PathOrId { - fn get_canonicalized(self) -> Self { - use PathOrId::*; - match self { - Path(path) => Path(helix_stdx::path::canonicalize(&path).into()), - Id(id) => Id(id), - } +impl<'a> PathOrId<'a> { + /// Creates a [PathOrId] from a PathBuf + /// + /// # Deprecated + /// The owned version of PathOrId will be removed in a future refactor + /// and replaced with `&'a Path`. See the caller of this function for + /// more details on its removal. + #[deprecated] + pub fn from_path_buf(path_buf: PathBuf) -> Self { + Self::Path(Cow::Owned(path_buf)) } } -impl From for PathOrId { - fn from(v: PathBuf) -> Self { - Self::Path(v.as_path().into()) +impl<'a> From<&'a Path> for PathOrId<'a> { + fn from(path: &'a Path) -> Self { + Self::Path(Cow::Borrowed(path)) } } -impl From for PathOrId { +impl<'a> From for PathOrId<'a> { fn from(v: DocumentId) -> Self { Self::Id(v) } } -type FileCallback = Box Option>; +type FileCallback = Box Fn(&'a Editor, &'a T) -> Option>>; /// File path and range of lines (used to align and highlight lines) -pub type FileLocation = (PathOrId, Option<(usize, usize)>); +pub type FileLocation<'a> = (PathOrId<'a>, Option<(usize, usize)>); pub enum CachedPreview { Document(Box), @@ -400,7 +404,7 @@ pub fn truncate_start(mut self, truncate_start: bool) -> Self { pub fn with_preview( mut self, - preview_fn: impl Fn(&Editor, &T) -> Option + 'static, + preview_fn: impl for<'a> Fn(&'a Editor, &'a T) -> Option> + 'static, ) -> Self { self.file_fn = Some(Box::new(preview_fn)); // assumption: if we have a preview we are matching paths... If this is ever @@ -544,40 +548,35 @@ fn handle_prompt_change(&mut self) { } } - fn current_file(&self, editor: &Editor) -> Option { - self.selection() - .and_then(|current| (self.file_fn.as_ref()?)(editor, current)) - .map(|(path_or_id, line)| (path_or_id.get_canonicalized(), line)) - } - - /// Get (cached) preview for a given path. If a document corresponding + /// Get (cached) preview for the currently selected item. If a document corresponding /// to the path is already open in the editor, it is used instead. fn get_preview<'picker, 'editor>( &'picker mut self, - path_or_id: PathOrId, editor: &'editor Editor, - ) -> Preview<'picker, 'editor> { + ) -> Option<(Preview<'picker, 'editor>, Option<(usize, usize)>)> { + let current = self.selection()?; + let (path_or_id, range) = (self.file_fn.as_ref()?)(editor, current)?; + match path_or_id { PathOrId::Path(path) => { - if let Some(doc) = editor.document_by_path(&path) { - return Preview::EditorDocument(doc); + let path = path.as_ref(); + if let Some(doc) = editor.document_by_path(path) { + return Some((Preview::EditorDocument(doc), range)); } - if self.preview_cache.contains_key(&path) { - let preview = &self.preview_cache[&path]; - match preview { - // If the document isn't highlighted yet, attempt to highlight it. - CachedPreview::Document(doc) if doc.language_config().is_none() => { - helix_event::send_blocking( - &self.preview_highlight_handler, - path.clone(), - ); - } - _ => (), + if self.preview_cache.contains_key(path) { + // NOTE: we use `HashMap::get_key_value` here instead of indexing so we can + // retrieve the `Arc` key. The `path` in scope here is a `&Path` and + // we can cheaply clone the key for the preview highlight handler. + let (path, preview) = self.preview_cache.get_key_value(path).unwrap(); + if matches!(preview, CachedPreview::Document(doc) if doc.language_config().is_none()) + { + helix_event::send_blocking(&self.preview_highlight_handler, path.clone()); } - return Preview::Cached(preview); + return Some((Preview::Cached(preview), range)); } + let path: Arc = path.into(); let data = std::fs::File::open(&path).and_then(|file| { let metadata = file.metadata()?; // Read up to 1kb to detect the content type @@ -607,11 +606,11 @@ fn get_preview<'picker, 'editor>( ) .unwrap_or(CachedPreview::NotFound); self.preview_cache.insert(path.clone(), preview); - Preview::Cached(&self.preview_cache[&path]) + Some((Preview::Cached(&self.preview_cache[&path]), range)) } PathOrId::Id(id) => { let doc = editor.documents.get(&id).unwrap(); - Preview::EditorDocument(doc) + Some((Preview::EditorDocument(doc), range)) } } } @@ -816,8 +815,7 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context let inner = inner.inner(margin); BLOCK.render(area, surface); - if let Some((path, range)) = self.current_file(cx.editor) { - let preview = self.get_preview(path, cx.editor); + if let Some((preview, range)) = self.get_preview(cx.editor) { let doc = match preview.document() { Some(doc) if range.map_or(true, |(start, end)| { From 8555248b012ddfc4578c5d82f9c7b31495b03ca0 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 23 Apr 2024 14:34:43 -0400 Subject: [PATCH 119/200] Accept 'IntoIterator' for Picker::new options `Picker::new` loops through the input options to inject each of them, so there's no need to collect into an intermediary Vec. This removes some unnecessary collections. Also, pickers that start with no initial options can now pass an empty slice instead of an empty Vec. Co-authored-by: Luis Useche --- helix-term/src/commands.rs | 37 ++++++++++++++++------------------ helix-term/src/commands/lsp.rs | 2 +- helix-term/src/ui/mod.rs | 26 +++++++++--------------- helix-term/src/ui/picker.rs | 2 +- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7e59bbdd6..847695946 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2404,7 +2404,7 @@ struct GlobalSearchConfig { let picker = Picker::new( columns, 1, // contents - vec![], + [], config, move |cx, FileResult { path, line_num, .. }, action| { let doc = match cx.editor.open(path, action) { @@ -2991,16 +2991,12 @@ struct JumpMeta { let picker = Picker::new( columns, 1, // path - cx.editor - .tree - .views() - .flat_map(|(view, _)| { - view.jumps - .iter() - .rev() - .map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone())) - }) - .collect(), + cx.editor.tree.views().flat_map(|(view, _)| { + view.jumps + .iter() + .rev() + .map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone())) + }), (), |cx, meta, action| { cx.editor.switch(meta.id, action); @@ -3077,7 +3073,7 @@ pub struct FileChangeData { let picker = Picker::new( columns, 1, // path - Vec::new(), + [], FileChangeData { cwd: cwd.clone(), style_untracked: added, @@ -3124,14 +3120,15 @@ pub fn command_palette(cx: &mut Context) { [&cx.editor.mode] .reverse_map(); - let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); - commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { - MappableCommand::Typable { - name: cmd.name.to_owned(), - doc: cmd.doc.to_owned(), - args: Vec::new(), - } - })); + let commands = MappableCommand::STATIC_COMMAND_LIST.iter().cloned().chain( + typed::TYPABLE_COMMAND_LIST + .iter() + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + args: Vec::new(), + doc: cmd.doc.to_owned(), + }), + ); let columns = vec![ ui::PickerColumn::new("name", |item, _| match item { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 059fb814d..601c58ebe 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -501,7 +501,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let picker = Picker::new( columns, 1, // name column - vec![], + [], (), move |cx, item, action| { jump_to_location( diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 14d92b575..e1ecf0a66 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -228,22 +228,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi .into() }, )]; - let picker = Picker::new( - columns, - 0, - Vec::new(), - root, - move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }, - ) + let picker = Picker::new(columns, 0, [], root, move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }) .with_preview(|_editor, path| Some((path.as_path().into(), None))); let injector = picker.injector(); let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 4f5095300..ebc06cc02 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -298,7 +298,7 @@ pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injecto pub fn new( columns: Vec>, primary_column: usize, - options: Vec, + options: impl IntoIterator, editor_data: D, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { From 009bbdaf8dc076178cc888cfaa1513bf1baa35f5 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 23 Apr 2024 20:25:56 -0400 Subject: [PATCH 120/200] Picker: Reset the cursor on prompt change --- helix-term/src/ui/picker.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ebc06cc02..c65e839cb 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -512,6 +512,8 @@ fn handle_prompt_change(&mut self) { if self.query == old_query { return; } + // If the query has meaningfully changed, reset the cursor to the top of the results. + self.cursor = 0; // Have nucleo reparse each changed column. for (i, column) in self .columns From a7777b3c118d7cd08868cb688e281b290898a31e Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 24 Apr 2024 09:22:22 -0400 Subject: [PATCH 121/200] Accept 'IntoIterator>' for picker columns This allows us to replace any `vec![..]`s of columns where all columns are static with static slices `[..]`. --- helix-term/src/commands.rs | 10 +++++----- helix-term/src/commands/dap.rs | 6 +++--- helix-term/src/commands/lsp.rs | 6 +++--- helix-term/src/commands/typed.rs | 2 +- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 26 ++++++++++++++++++-------- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 847695946..097c3493c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2265,7 +2265,7 @@ struct GlobalSearchConfig { file_picker_config: config.file_picker.clone(), }; - let columns = vec![ + let columns = [ PickerColumn::new("path", |item: &FileResult, _| { let path = helix_stdx::path::get_relative_path(&item.path); format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into() @@ -2886,7 +2886,7 @@ struct BufferMeta { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let columns = vec![ + let columns = [ PickerColumn::new("id", |meta: &BufferMeta, _| meta.id.to_string().into()), PickerColumn::new("flags", |meta: &BufferMeta, _| { let mut flags = String::new(); @@ -2960,7 +2960,7 @@ struct JumpMeta { } }; - let columns = vec![ + let columns = [ ui::PickerColumn::new("id", |item: &JumpMeta, _| item.id.to_string().into()), ui::PickerColumn::new("path", |item: &JumpMeta, _| { let path = item @@ -3039,7 +3039,7 @@ pub struct FileChangeData { let deleted = cx.editor.theme.get("diff.minus"); let renamed = cx.editor.theme.get("diff.delta.moved"); - let columns = vec![ + let columns = [ PickerColumn::new("change", |change: &FileChange, data: &FileChangeData| { match change { FileChange::Untracked { .. } => Span::styled("+ untracked", data.style_untracked), @@ -3130,7 +3130,7 @@ pub fn command_palette(cx: &mut Context) { }), ); - let columns = vec![ + let columns = [ ui::PickerColumn::new("name", |item, _| match item { MappableCommand::Typable { name, .. } => format!(":{name}").into(), MappableCommand::Static { name, .. } => (*name).into(), diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 39c79a66c..0b754bc21 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -41,7 +41,7 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let columns = vec![ + let columns = [ ui::PickerColumn::new("name", |item: &Thread, _| item.name.as_str().into()), ui::PickerColumn::new("state", |item: &Thread, thread_states: &ThreadStates| { thread_states @@ -250,7 +250,7 @@ pub fn dap_launch(cx: &mut Context) { let templates = config.templates.clone(); - let columns = vec![ui::PickerColumn::new( + let columns = [ui::PickerColumn::new( "template", |item: &DebugTemplate, _| item.name.as_str().into(), )]; @@ -725,7 +725,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let columns = vec![ui::PickerColumn::new("frame", |item: &StackFrame, _| { + let columns = [ui::PickerColumn::new("frame", |item: &StackFrame, _| { item.name.as_str().into() // TODO: include thread_states in the label })]; let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 601c58ebe..103d1df24 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -371,7 +371,7 @@ fn nested_to_flat( symbols.append(&mut lsp_items); } let call = move |_editor: &mut Editor, compositor: &mut Compositor| { - let columns = vec![ + let columns = [ ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| { display_symbol_kind(item.symbol.kind).into() }), @@ -478,7 +478,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { } .boxed() }; - let columns = vec![ + let columns = [ ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| { display_symbol_kind(item.symbol.kind).into() }), @@ -855,7 +855,7 @@ fn goto_impl( } [] => unreachable!("`locations` should be non-empty for `goto_impl`"), _locations => { - let columns = vec![ui::PickerColumn::new( + let columns = [ui::PickerColumn::new( "location", |item: &lsp::Location, cwdir: &std::path::PathBuf| { // The preallocation here will overallocate a few characters since it will account for the diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f65f96574..1b828b3f4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1404,7 +1404,7 @@ fn lsp_workspace_command( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let columns = vec![ui::PickerColumn::new( + let columns = [ui::PickerColumn::new( "title", |(_ls_id, command): &(_, helix_lsp::lsp::Command), _| { command.title.as_str().into() diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e1ecf0a66..fae64062d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -219,7 +219,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi }); log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - let columns = vec![PickerColumn::new( + let columns = [PickerColumn::new( "path", |item: &PathBuf, root: &PathBuf| { item.strip_prefix(root) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c65e839cb..869431654 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -275,7 +275,11 @@ pub struct Picker { } impl Picker { - pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injector) { + pub fn stream( + columns: impl IntoIterator>, + editor_data: D, + ) -> (Nucleo, Injector) { + let columns: Arc<[_]> = columns.into_iter().collect(); let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32; assert!(matcher_columns > 0); let matcher = Nucleo::new( @@ -286,7 +290,7 @@ pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injecto ); let streamer = Injector { dst: matcher.injector(), - columns: columns.into(), + columns, editor_data: Arc::new(editor_data), version: 0, picker_version: Arc::new(AtomicUsize::new(0)), @@ -295,13 +299,19 @@ pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injecto (matcher, streamer) } - pub fn new( - columns: Vec>, + pub fn new( + columns: C, primary_column: usize, - options: impl IntoIterator, + options: O, editor_data: D, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, - ) -> Self { + callback_fn: F, + ) -> Self + where + C: IntoIterator>, + O: IntoIterator, + F: Fn(&mut Context, &T, Action) + 'static, + { + let columns: Arc<[_]> = columns.into_iter().collect(); let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32; assert!(matcher_columns > 0); let matcher = Nucleo::new( @@ -316,7 +326,7 @@ pub fn new( } Self::with( matcher, - columns.into(), + columns, primary_column, Arc::new(editor_data), Arc::new(AtomicUsize::new(0)), From 6345b7840961222b30f91316f953b7cfa51a19e2 Mon Sep 17 00:00:00 2001 From: FlorianNAdam <64063379+FlorianNAdam@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:48:37 +0200 Subject: [PATCH 122/200] Keep editor from switching to normal mode when loading a Document (#11176) --- helix-view/src/editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3eeb4830e..50b6c9318 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1544,7 +1544,9 @@ pub fn switch(&mut self, id: DocumentId, action: Action) { return; } - self.enter_normal_mode(); + if !matches!(action, Action::Load) { + self.enter_normal_mode(); + } match action { Action::Replace => { From 9de5f5cefab30f5c6d0b8ebbc0a1c5310a67e7ea Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Apr 2024 16:13:48 -0400 Subject: [PATCH 123/200] Picker: Highlight the currently active column We can track the ranges in the input text that correspond to each column and use this information during rendering to apply a new theme key that makes the "active column" stand out. This makes it easier to tell at a glance which column you're entering. --- book/src/themes.md | 1 + helix-term/src/ui/picker.rs | 10 +++- helix-term/src/ui/picker/query.rs | 94 +++++++++++++++++++++++++++++-- helix-term/src/ui/prompt.rs | 6 ++ 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index a59df2fd7..b8e271374 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -298,6 +298,7 @@ #### Interface | `ui.popup` | Documentation popups (e.g. Space + k) | | `ui.popup.info` | Prompt for multiple key options | | `ui.picker.header` | Column names in pickers with multiple columns | +| `ui.picker.header.active` | The column name in pickers with multiple columns where the cursor is entering into. | | `ui.window` | Borderlines separating splits | | `ui.help` | Description box for commands | | `ui.text` | Default text style, command prompts, popup text, etc. | diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 869431654..079012396 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -787,13 +787,21 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) // -- Header if self.columns.len() > 1 { + let active_column = self.query.active_column(self.prompt.position()); let header_style = cx.editor.theme.get("ui.picker.header"); table = table.header(Row::new(self.columns.iter().map(|column| { if column.hidden { Cell::default() } else { - Cell::from(Span::styled(Cow::from(&*column.name), header_style)) + let style = if active_column.is_some_and(|name| Arc::ptr_eq(name, &column.name)) + { + cx.editor.theme.get("ui.picker.header.active") + } else { + header_style + }; + + Cell::from(Span::styled(Cow::from(&*column.name), style)) } }))); } diff --git a/helix-term/src/ui/picker/query.rs b/helix-term/src/ui/picker/query.rs index 89ade95ff..e433a11fa 100644 --- a/helix-term/src/ui/picker/query.rs +++ b/helix-term/src/ui/picker/query.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, mem, sync::Arc}; +use std::{collections::HashMap, mem, ops::Range, sync::Arc}; #[derive(Debug)] pub(super) struct PickerQuery { @@ -11,6 +11,10 @@ pub(super) struct PickerQuery { /// The mapping between column names and input in the query /// for those columns. inner: HashMap, Arc>, + /// The byte ranges of the input text which are used as input for each column. + /// This is calculated at parsing time for use in [Self::active_column]. + /// This Vec is naturally sorted in ascending order and ranges do not overlap. + column_ranges: Vec<(Range, Option>)>, } impl PartialEq, Arc>> for PickerQuery { @@ -26,10 +30,12 @@ pub(super) fn new>>( ) -> Self { let column_names: Box<[_]> = column_names.collect(); let inner = HashMap::with_capacity(column_names.len()); + let column_ranges = vec![(0..usize::MAX, Some(column_names[primary_column].clone()))]; Self { column_names, primary_column, inner, + column_ranges, } } @@ -44,6 +50,9 @@ pub(super) fn parse(&mut self, input: &str) -> HashMap, Arc> { let mut in_field = false; let mut field = None; let mut text = String::new(); + self.column_ranges.clear(); + self.column_ranges + .push((0..usize::MAX, Some(primary_field.clone()))); macro_rules! finish_field { () => { @@ -59,7 +68,7 @@ macro_rules! finish_field { }; } - for ch in input.chars() { + for (idx, ch) in input.char_indices() { match ch { // Backslash escaping _ if escaped => { @@ -77,9 +86,19 @@ macro_rules! finish_field { if !text.is_empty() { finish_field!(); } + let (range, _field) = self + .column_ranges + .last_mut() + .expect("column_ranges is non-empty"); + range.end = idx; in_field = true; } ' ' if in_field => { + text.clear(); + in_field = false; + } + _ if in_field => { + text.push(ch); // Go over all columns and their indices, find all that starts with field key, // select a column that fits key the most. field = self @@ -88,8 +107,17 @@ macro_rules! finish_field { .filter(|col| col.starts_with(&text)) // select "fittest" column .min_by_key(|col| col.len()); - text.clear(); - in_field = false; + + // Update the column range for this column. + if let Some((_range, current_field)) = self + .column_ranges + .last_mut() + .filter(|(range, _)| range.end == usize::MAX) + { + *current_field = field.cloned(); + } else { + self.column_ranges.push((idx..usize::MAX, field.cloned())); + } } _ => text.push(ch), } @@ -106,6 +134,23 @@ macro_rules! finish_field { mem::replace(&mut self.inner, new_inner) } + + /// Finds the column which the cursor is 'within' in the last parse. + /// + /// The cursor is considered to be within a column when it is placed within any + /// of a column's text. See the `active_column_test` unit test below for examples. + /// + /// `cursor` is a byte index that represents the location of the prompt's cursor. + pub fn active_column(&self, cursor: usize) -> Option<&Arc> { + let point = self + .column_ranges + .partition_point(|(range, _field)| cursor > range.end); + + self.column_ranges + .get(point) + .filter(|(range, _field)| cursor >= range.start && cursor <= range.end) + .and_then(|(_range, field)| field.as_ref()) + } } #[cfg(test)] @@ -279,4 +324,45 @@ fn parse_query_test() { ) ); } + + #[test] + fn active_column_test() { + fn active_column<'a>(query: &'a mut PickerQuery, input: &str) -> Option<&'a str> { + let cursor = input.find('|').expect("cursor must be indicated with '|'"); + let input = input.replace('|', ""); + query.parse(&input); + query.active_column(cursor).map(AsRef::as_ref) + } + + let mut query = PickerQuery::new( + ["primary".into(), "foo".into(), "bar".into()].into_iter(), + 0, + ); + + assert_eq!(active_column(&mut query, "|"), Some("primary")); + assert_eq!(active_column(&mut query, "hello| world"), Some("primary")); + assert_eq!(active_column(&mut query, "|%foo hello"), Some("primary")); + assert_eq!(active_column(&mut query, "%foo|"), Some("foo")); + assert_eq!(active_column(&mut query, "%|"), None); + assert_eq!(active_column(&mut query, "%baz|"), None); + assert_eq!(active_column(&mut query, "%quiz%|"), None); + assert_eq!(active_column(&mut query, "%foo hello| world"), Some("foo")); + assert_eq!(active_column(&mut query, "%foo hello world|"), Some("foo")); + assert_eq!(active_column(&mut query, "%foo| hello world"), Some("foo")); + assert_eq!(active_column(&mut query, "%|foo hello world"), Some("foo")); + assert_eq!(active_column(&mut query, "%f|oo hello world"), Some("foo")); + assert_eq!(active_column(&mut query, "hello %f|oo world"), Some("foo")); + assert_eq!( + active_column(&mut query, "hello %f|oo world %bar !"), + Some("foo") + ); + assert_eq!( + active_column(&mut query, "hello %foo wo|rld %bar !"), + Some("foo") + ); + assert_eq!( + active_column(&mut query, "hello %foo world %bar !|"), + Some("bar") + ); + } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 8e8896183..3518ddf77 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -92,6 +92,12 @@ pub fn new( } } + /// Gets the byte index in the input representing the current cursor location. + #[inline] + pub(crate) fn position(&self) -> usize { + self.cursor + } + pub fn with_line(mut self, line: String, editor: &Editor) -> Self { self.set_line(line, editor); self From 22dfad605a2cd429b0cb739fa3db7d0912fc9554 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:35:40 +0100 Subject: [PATCH 124/200] implement Add/Sub for position being able to add/subtract positions is very handy when writing rendering code --- helix-core/src/position.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index ee764bc64..dba112121 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -1,4 +1,8 @@ -use std::{borrow::Cow, cmp::Ordering}; +use std::{ + borrow::Cow, + cmp::Ordering, + ops::{Add, AddAssign, Sub, SubAssign}, +}; use crate::{ chars::char_is_line_ending, @@ -16,6 +20,38 @@ pub struct Position { pub col: usize, } +impl AddAssign for Position { + fn add_assign(&mut self, rhs: Self) { + self.row += rhs.row; + self.col += rhs.col; + } +} + +impl SubAssign for Position { + fn sub_assign(&mut self, rhs: Self) { + self.row -= rhs.row; + self.col -= rhs.col; + } +} + +impl Sub for Position { + type Output = Position; + + fn sub(mut self, rhs: Self) -> Self::Output { + self -= rhs; + self + } +} + +impl Add for Position { + type Output = Position; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + impl Position { pub const fn new(row: usize, col: usize) -> Self { Self { row, col } From 2023445a08e7b67ccd0a772b5deb4c6a236a7843 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:05 +0100 Subject: [PATCH 125/200] ensure highlight scopes are skipped properly --- helix-term/src/ui/document.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index bcbaa3519..17695287c 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -68,10 +68,7 @@ fn next(&mut self) -> Option<(Style, usize)> { HighlightEvent::HighlightEnd => { self.active_highlights.pop(); } - HighlightEvent::Source { start, mut end } => { - if start == end { - continue; - } + HighlightEvent::Source { mut end, .. } => { let style = self .active_highlights .iter() @@ -300,12 +297,12 @@ pub fn render_text<'t>( } // acquire the correct grapheme style - if char_pos >= syntax_style_span.1 { + while char_pos >= syntax_style_span.1 { syntax_style_span = syntax_styles .next() .unwrap_or((Style::default(), usize::MAX)); } - if char_pos >= overlay_style_span.1 { + while char_pos >= overlay_style_span.1 { overlay_style_span = overlay_styles .next() .unwrap_or((Style::default(), usize::MAX)); From e15626a00a5019e9fbc33e2f798ec7629faad4e9 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:06 +0100 Subject: [PATCH 126/200] track char_idx in DocFormatter --- helix-core/src/doc_formatter.rs | 199 ++++++++++++++++----------- helix-core/src/doc_formatter/test.rs | 18 +-- helix-core/src/position.rs | 50 ++++--- helix-term/src/ui/document.rs | 100 ++++++++------ 4 files changed, 209 insertions(+), 158 deletions(-) diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index cbe2da3b6..f934b38a1 100644 --- a/helix-core/src/doc_formatter.rs +++ b/helix-core/src/doc_formatter.rs @@ -37,52 +37,91 @@ pub enum GraphemeSource { }, } -#[derive(Debug, Clone)] -pub struct FormattedGrapheme<'a> { - pub grapheme: Grapheme<'a>, - pub source: GraphemeSource, +impl GraphemeSource { + /// Returns whether this grapheme is virtual inline text + pub fn is_virtual(self) -> bool { + matches!(self, GraphemeSource::VirtualText { .. }) + } + + pub fn doc_chars(self) -> usize { + match self { + GraphemeSource::Document { codepoints } => codepoints as usize, + GraphemeSource::VirtualText { .. } => 0, + } + } } -impl<'a> FormattedGrapheme<'a> { - pub fn new( +#[derive(Debug, Clone)] +pub struct FormattedGrapheme<'a> { + pub raw: Grapheme<'a>, + pub source: GraphemeSource, + pub visual_pos: Position, + /// Document line at the start of the grapheme + pub line_idx: usize, + /// Document char position at the start of the grapheme + pub char_idx: usize, +} + +impl FormattedGrapheme<'_> { + pub fn is_virtual(&self) -> bool { + self.source.is_virtual() + } + + pub fn doc_chars(&self) -> usize { + self.source.doc_chars() + } + + pub fn is_whitespace(&self) -> bool { + self.raw.is_whitespace() + } + + pub fn width(&self) -> usize { + self.raw.width() + } + + pub fn is_word_boundary(&self) -> bool { + self.raw.is_word_boundary() + } +} + +#[derive(Debug, Clone)] +struct GraphemeWithSource<'a> { + grapheme: Grapheme<'a>, + source: GraphemeSource, +} + +impl<'a> GraphemeWithSource<'a> { + fn new( g: GraphemeStr<'a>, visual_x: usize, tab_width: u16, source: GraphemeSource, - ) -> FormattedGrapheme<'a> { - FormattedGrapheme { + ) -> GraphemeWithSource<'a> { + GraphemeWithSource { grapheme: Grapheme::new(g, visual_x, tab_width), source, } } - /// Returns whether this grapheme is virtual inline text - pub fn is_virtual(&self) -> bool { - matches!(self.source, GraphemeSource::VirtualText { .. }) - } - - pub fn placeholder() -> Self { - FormattedGrapheme { + fn placeholder() -> Self { + GraphemeWithSource { grapheme: Grapheme::Other { g: " ".into() }, source: GraphemeSource::Document { codepoints: 0 }, } } - pub fn doc_chars(&self) -> usize { - match self.source { - GraphemeSource::Document { codepoints } => codepoints as usize, - GraphemeSource::VirtualText { .. } => 0, - } + fn doc_chars(&self) -> usize { + self.source.doc_chars() } - pub fn is_whitespace(&self) -> bool { + fn is_whitespace(&self) -> bool { self.grapheme.is_whitespace() } - pub fn width(&self) -> usize { + fn width(&self) -> usize { self.grapheme.width() } - pub fn is_word_boundary(&self) -> bool { + fn is_word_boundary(&self) -> bool { self.grapheme.is_word_boundary() } } @@ -139,9 +178,9 @@ pub struct DocumentFormatter<'t> { indent_level: Option, /// In case a long word needs to be split a single grapheme might need to be wrapped /// while the rest of the word stays on the same line - peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>, + peeked_grapheme: Option>, /// A first-in first-out (fifo) buffer for the Graphemes of any given word - word_buf: Vec>, + word_buf: Vec>, /// The index of the next grapheme that will be yielded from the `word_buf` word_i: usize, } @@ -157,32 +196,33 @@ pub fn new_at_prev_checkpoint( text_fmt: &'t TextFormat, annotations: &'t TextAnnotations, char_idx: usize, - ) -> (Self, usize) { + ) -> Self { // TODO divide long lines into blocks to avoid bad performance for long lines let block_line_idx = text.char_to_line(char_idx.min(text.len_chars())); let block_char_idx = text.line_to_char(block_line_idx); annotations.reset_pos(block_char_idx); - ( - DocumentFormatter { - text_fmt, - annotations, - visual_pos: Position { row: 0, col: 0 }, - graphemes: RopeGraphemes::new(text.slice(block_char_idx..)), - char_pos: block_char_idx, - exhausted: false, - virtual_lines: 0, - indent_level: None, - peeked_grapheme: None, - word_buf: Vec::with_capacity(64), - word_i: 0, - line_pos: block_line_idx, - inline_anntoation_graphemes: None, - }, - block_char_idx, - ) + + DocumentFormatter { + text_fmt, + annotations, + visual_pos: Position { row: 0, col: 0 }, + graphemes: RopeGraphemes::new(text.slice(block_char_idx..)), + char_pos: block_char_idx, + exhausted: false, + virtual_lines: 0, + indent_level: None, + peeked_grapheme: None, + word_buf: Vec::with_capacity(64), + word_i: 0, + line_pos: block_line_idx, + inline_anntoation_graphemes: None, + } } - fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option)> { + fn next_inline_annotation_grapheme( + &mut self, + char_pos: usize, + ) -> Option<(&'t str, Option)> { loop { if let Some(&mut (ref mut annotation, highlight)) = self.inline_anntoation_graphemes.as_mut() @@ -193,7 +233,7 @@ fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option Option<(&'t str, Option Option> { + fn advance_grapheme(&mut self, col: usize, char_pos: usize) -> Option> { let (grapheme, source) = - if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() { + if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme(char_pos) { (grapheme.into(), GraphemeSource::VirtualText { highlight }) } else if let Some(grapheme) = self.graphemes.next() { self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos); let codepoints = grapheme.len_chars() as u32; - let overlay = self.annotations.overlay_at(self.char_pos); + let overlay = self.annotations.overlay_at(char_pos); let grapheme = match overlay { Some((overlay, _)) => overlay.grapheme.as_str().into(), None => Cow::from(grapheme).into(), }; - self.char_pos += codepoints as usize; (grapheme, GraphemeSource::Document { codepoints }) } else { if self.exhausted { @@ -228,19 +267,19 @@ fn advance_grapheme(&mut self, col: usize) -> Option> { self.exhausted = true; // EOF grapheme is required for rendering // and correct position computations - return Some(FormattedGrapheme { + return Some(GraphemeWithSource { grapheme: Grapheme::Other { g: " ".into() }, source: GraphemeSource::Document { codepoints: 0 }, }); }; - let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source); + let grapheme = GraphemeWithSource::new(grapheme, col, self.text_fmt.tab_width, source); Some(grapheme) } /// Move a word to the next visual line - fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize { + fn wrap_word(&mut self) -> usize { // softwrap this word to the next line let indent_carry_over = if let Some(indent) = self.indent_level { if indent as u16 <= self.text_fmt.max_indent_retain { @@ -255,14 +294,13 @@ fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize { }; self.visual_pos.col = indent_carry_over as usize; - self.virtual_lines -= virtual_lines_before_word; - self.visual_pos.row += 1 + virtual_lines_before_word; + self.visual_pos.row += 1 + take(&mut self.virtual_lines); let mut i = 0; let mut word_width = 0; let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true) .map(|g| { i += 1; - let grapheme = FormattedGrapheme::new( + let grapheme = GraphemeWithSource::new( g.into(), self.visual_pos.col + word_width, self.text_fmt.tab_width, @@ -288,8 +326,7 @@ fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize { fn advance_to_next_word(&mut self) { self.word_buf.clear(); let mut word_width = 0; - let virtual_lines_before_word = self.virtual_lines; - let mut virtual_lines_before_grapheme = self.virtual_lines; + let mut word_chars = 0; loop { // softwrap word if necessary @@ -301,27 +338,24 @@ fn advance_to_next_word(&mut self) { // However if the last grapheme is multiple columns wide it might extend beyond the EOL. // The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize { - self.peeked_grapheme = self.word_buf.pop().map(|grapheme| { - (grapheme, self.virtual_lines - virtual_lines_before_grapheme) - }); - self.virtual_lines = virtual_lines_before_grapheme; + self.peeked_grapheme = self.word_buf.pop(); } return; } - word_width = self.wrap_word(virtual_lines_before_word); + word_width = self.wrap_word(); } - virtual_lines_before_grapheme = self.virtual_lines; - - let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() { - self.virtual_lines += virtual_lines; + let grapheme = if let Some(grapheme) = self.peeked_grapheme.take() { grapheme - } else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) { + } else if let Some(grapheme) = + self.advance_grapheme(self.visual_pos.col + word_width, self.char_pos + word_chars) + { grapheme } else { return; }; + word_chars += grapheme.doc_chars(); // Track indentation if !grapheme.is_whitespace() && self.indent_level.is_none() { @@ -340,19 +374,19 @@ fn advance_to_next_word(&mut self) { } } - /// returns the document line pos of the **next** grapheme that will be yielded - pub fn line_pos(&self) -> usize { - self.line_pos + /// returns the char index at the end of the last yielded grapheme + pub fn next_char_pos(&self) -> usize { + self.char_pos } - /// returns the visual pos of the **next** grapheme that will be yielded - pub fn visual_pos(&self) -> Position { + /// returns the visual position at the end of the last yielded grapheme + pub fn next_visual_pos(&self) -> Position { self.visual_pos } } impl<'t> Iterator for DocumentFormatter<'t> { - type Item = (FormattedGrapheme<'t>, Position); + type Item = FormattedGrapheme<'t>; fn next(&mut self) -> Option { let grapheme = if self.text_fmt.soft_wrap { @@ -362,15 +396,18 @@ fn next(&mut self) -> Option { } let grapheme = replace( self.word_buf.get_mut(self.word_i)?, - FormattedGrapheme::placeholder(), + GraphemeWithSource::placeholder(), ); self.word_i += 1; grapheme } else { - self.advance_grapheme(self.visual_pos.col)? + self.advance_grapheme(self.visual_pos.col, self.char_pos)? }; - let pos = self.visual_pos; + let visual_pos = self.visual_pos; + let char_pos = self.char_pos; + self.char_pos += grapheme.doc_chars(); + let line_idx = self.line_pos; if grapheme.grapheme == Grapheme::Newline { self.visual_pos.row += 1; self.visual_pos.row += take(&mut self.virtual_lines); @@ -379,6 +416,12 @@ fn next(&mut self) -> Option { } else { self.visual_pos.col += grapheme.width(); } - Some((grapheme, pos)) + Some(FormattedGrapheme { + raw: grapheme.grapheme, + source: grapheme.source, + visual_pos, + line_idx, + char_idx: char_pos, + }) } } diff --git a/helix-core/src/doc_formatter/test.rs b/helix-core/src/doc_formatter/test.rs index d2b6ddc74..b214ab944 100644 --- a/helix-core/src/doc_formatter/test.rs +++ b/helix-core/src/doc_formatter/test.rs @@ -23,18 +23,18 @@ fn collect_to_str(&mut self) -> String { let viewport_width = self.text_fmt.viewport_width; let mut line = 0; - for (grapheme, pos) in self { - if pos.row != line { + for grapheme in self { + if grapheme.visual_pos.row != line { line += 1; - assert_eq!(pos.row, line); - write!(res, "\n{}", ".".repeat(pos.col)).unwrap(); + assert_eq!(grapheme.visual_pos.row, line); + write!(res, "\n{}", ".".repeat(grapheme.visual_pos.col)).unwrap(); assert!( - pos.col <= viewport_width as usize, + grapheme.visual_pos.col <= viewport_width as usize, "softwrapped failed {}<={viewport_width}", - pos.col + grapheme.visual_pos.col ); } - write!(res, "{}", grapheme.grapheme).unwrap(); + write!(res, "{}", grapheme.raw).unwrap(); } res @@ -48,7 +48,6 @@ fn softwrap_text(text: &str) -> String { &TextAnnotations::default(), 0, ) - .0 .collect_to_str() } @@ -106,7 +105,6 @@ fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay TextAnnotations::default().add_overlay(overlays, None), char_pos, ) - .0 .collect_to_str() } @@ -143,7 +141,6 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) - TextAnnotations::default().add_inline_annotations(annotations, None), 0, ) - .0 .collect_to_str() } @@ -182,7 +179,6 @@ fn annotation_and_overlay() { .add_overlay(overlay.as_slice(), None), 0, ) - .0 .collect_to_str(), "fooo bar " ); diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index dba112121..0860da12f 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -157,16 +157,14 @@ pub fn visual_offset_from_block( annotations: &TextAnnotations, ) -> (Position, usize) { let mut last_pos = Position::default(); - let (formatter, block_start) = + let mut formatter = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); - let mut char_pos = block_start; + let block_start = formatter.next_char_pos(); - for (grapheme, vpos) in formatter { - last_pos = vpos; - char_pos += grapheme.doc_chars(); - - if char_pos > pos { - return (last_pos, block_start); + while let Some(grapheme) = formatter.next() { + last_pos = grapheme.visual_pos; + if formatter.next_char_pos() > pos { + return (grapheme.visual_pos, block_start); } } @@ -189,22 +187,21 @@ pub fn visual_offset_from_anchor( annotations: &TextAnnotations, max_rows: usize, ) -> Result<(Position, usize), VisualOffsetError> { - let (formatter, block_start) = + let mut formatter = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); - let mut char_pos = block_start; let mut anchor_line = None; let mut found_pos = None; let mut last_pos = Position::default(); + let block_start = formatter.next_char_pos(); if pos < block_start { return Err(VisualOffsetError::PosBeforeAnchorRow); } - for (grapheme, vpos) in formatter { - last_pos = vpos; - char_pos += grapheme.doc_chars(); + while let Some(grapheme) = formatter.next() { + last_pos = grapheme.visual_pos; - if char_pos > pos { + if formatter.next_char_pos() > pos { if let Some(anchor_line) = anchor_line { last_pos.row -= anchor_line; return Ok((last_pos, block_start)); @@ -212,7 +209,7 @@ pub fn visual_offset_from_anchor( found_pos = Some(last_pos); } } - if char_pos > anchor && anchor_line.is_none() { + if formatter.next_char_pos() > anchor && anchor_line.is_none() { if let Some(mut found_pos) = found_pos { return if found_pos.row == last_pos.row { found_pos.row = 0; @@ -226,7 +223,7 @@ pub fn visual_offset_from_anchor( } if let Some(anchor_line) = anchor_line { - if vpos.row >= anchor_line + max_rows { + if grapheme.visual_pos.row >= anchor_line + max_rows { return Err(VisualOffsetError::PosAfterMaxRow); } } @@ -404,34 +401,33 @@ pub fn char_idx_at_visual_block_offset( text_fmt: &TextFormat, annotations: &TextAnnotations, ) -> (usize, usize) { - let (formatter, mut char_idx) = + let mut formatter = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); - let mut last_char_idx = char_idx; + let mut last_char_idx = formatter.next_char_pos(); let mut last_char_idx_on_line = None; let mut last_row = 0; - for (grapheme, grapheme_pos) in formatter { - match grapheme_pos.row.cmp(&row) { + for grapheme in &mut formatter { + match grapheme.visual_pos.row.cmp(&row) { Ordering::Equal => { - if grapheme_pos.col + grapheme.width() > column { + if grapheme.visual_pos.col + grapheme.width() > column { if !grapheme.is_virtual() { - return (char_idx, 0); + return (grapheme.char_idx, 0); } else if let Some(char_idx) = last_char_idx_on_line { return (char_idx, 0); } } else if !grapheme.is_virtual() { - last_char_idx_on_line = Some(char_idx) + last_char_idx_on_line = Some(grapheme.char_idx) } } Ordering::Greater => return (last_char_idx, row - last_row), _ => (), } - last_char_idx = char_idx; - last_row = grapheme_pos.row; - char_idx += grapheme.doc_chars(); + last_char_idx = grapheme.char_idx; + last_row = grapheme.visual_pos.row; } - (char_idx, 0) + (formatter.next_char_pos(), 0) } #[cfg(test)] diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index 17695287c..34282b261 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -179,21 +179,18 @@ pub fn render_text<'t>( line_decorations: &mut [Box], translated_positions: &mut [TranslatedPosition], ) { - let ( - Position { - row: mut row_off, .. - }, - mut char_pos, - ) = visual_offset_from_block( + let mut row_off = visual_offset_from_block( text, offset.anchor, offset.anchor, text_fmt, text_annotations, - ); + ) + .0 + .row; row_off += offset.vertical_offset; - let (mut formatter, mut first_visible_char_idx) = + let mut formatter = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor); let mut syntax_styles = StyleIter { text_style: renderer.text_style, @@ -226,19 +223,19 @@ pub fn render_text<'t>( let mut overlay_style_span = overlay_styles .next() .unwrap_or_else(|| (Style::default(), usize::MAX)); + let mut first_visible_char_idx = formatter.next_char_pos(); loop { // formattter.line_pos returns to line index of the next grapheme // so it must be called before formatter.next - let doc_line = formatter.line_pos(); - let Some((grapheme, mut pos)) = formatter.next() else { - let mut last_pos = formatter.visual_pos(); + let Some(mut grapheme) = formatter.next() else { + let mut last_pos = formatter.next_visual_pos(); if last_pos.row >= row_off { last_pos.col -= 1; last_pos.row -= row_off; // check if any positions translated on the fly (like cursor) are at the EOF translate_positions( - char_pos + 1, + text.len_chars() + 1, first_visible_char_idx, translated_positions, text_fmt, @@ -250,46 +247,56 @@ pub fn render_text<'t>( }; // skip any graphemes on visual lines before the block start - if pos.row < row_off { - if char_pos >= syntax_style_span.1 { - syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() { - syntax_style_span + // if pos.row < row_off { + // if char_pos >= syntax_style_span.1 { + // syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() { + // syntax_style_span + // } else { + // break; + // } + // } + // if char_pos >= overlay_style_span.1 { + // overlay_style_span = if let Some(overlay_style_span) = overlay_styles.next() { + // overlay_style_span + if grapheme.visual_pos.row < row_off { + if grapheme.char_idx >= style_span.1 { + style_span = if let Some(style_span) = styles.next() { + style_span } else { break; - } - } - if char_pos >= overlay_style_span.1 { - overlay_style_span = if let Some(overlay_style_span) = overlay_styles.next() { - overlay_style_span + }; + overlay_span = if let Some(overlay_span) = overlays.next() { + overlay_span } else { break; - } + }; } - char_pos += grapheme.doc_chars(); - first_visible_char_idx = char_pos + 1; + first_visible_char_idx = formatter.next_char_pos(); continue; } - pos.row -= row_off; + grapheme.visual_pos.row -= row_off; // if the end of the viewport is reached stop rendering - if pos.row as u16 >= renderer.viewport.height { + if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 { break; } // apply decorations before rendering a new line - if pos.row as u16 != last_line_pos.visual_line { - if pos.row > 0 { - renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); + if grapheme.visual_pos.row as u16 != last_line_pos.visual_line { + if grapheme.visual_pos.row > 0 { + // draw indent guides for the last line + renderer + .draw_indent_guides(last_line_indent_level, last_line_pos.visual_line as u16); is_in_indent_area = true; for line_decoration in &mut *line_decorations { - line_decoration.render_foreground(renderer, last_line_pos, char_pos); + line_decoration.render_foreground(renderer, last_line_pos, grapheme.char_idx); } } last_line_pos = LinePos { - first_visual_line: doc_line != last_line_pos.doc_line, - doc_line, - visual_line: pos.row as u16, - start_char_idx: char_pos, + first_visual_line: grapheme.line_idx != last_line_pos.doc_line, + doc_line: grapheme.line_idx, + visual_line: grapheme.visual_pos.row as u16, + start_char_idx: grapheme.char_idx, }; for line_decoration in &mut *line_decorations { line_decoration.render_background(renderer, last_line_pos); @@ -297,26 +304,25 @@ pub fn render_text<'t>( } // acquire the correct grapheme style - while char_pos >= syntax_style_span.1 { + while grapheme.char_idx >= syntax_style_span.1 { syntax_style_span = syntax_styles .next() .unwrap_or((Style::default(), usize::MAX)); } - while char_pos >= overlay_style_span.1 { + while grapheme.char_idx >= overlay_style_span.1 { overlay_style_span = overlay_styles .next() .unwrap_or((Style::default(), usize::MAX)); } - char_pos += grapheme.doc_chars(); // check if any positions translated on the fly (like cursor) has been reached translate_positions( - char_pos, + formatter.next_char_pos(), first_visible_char_idx, translated_positions, text_fmt, renderer, - pos, + grapheme.visual_pos, ); let (syntax_style, overlay_style) = @@ -332,27 +338,37 @@ pub fn render_text<'t>( let is_virtual = grapheme.is_virtual(); renderer.draw_grapheme( +<<<<<<< HEAD grapheme.grapheme, GraphemeStyle { syntax_style, overlay_style, }, is_virtual, +||||||| parent of 5e32edd8 (track char_idx in DocFormatter) + grapheme.grapheme, + grapheme_style, + virt, +======= + grapheme.raw, + grapheme_style, + virt, +>>>>>>> 5e32edd8 (track char_idx in DocFormatter) &mut last_line_indent_level, &mut is_in_indent_area, - pos, + grapheme.visual_pos, ); } renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); for line_decoration in &mut *line_decorations { - line_decoration.render_foreground(renderer, last_line_pos, char_pos); + line_decoration.render_foreground(renderer, last_line_pos, formatter.next_char_pos()); } } #[derive(Debug)] pub struct TextRenderer<'a> { - pub surface: &'a mut Surface, + surface: &'a mut Surface, pub text_style: Style, pub whitespace_style: Style, pub indent_guide_char: String, From 4c7cdb8feaf94e5f0ee993680fe9e26c0b9339eb Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:06 +0100 Subject: [PATCH 127/200] Improve line annotation API The line annotation as implemented in #5420 had two shortcomings: * It required the height of virtual text lines to be known ahead time * It checked for line anchors at every grapheme The first problem made the API impractical to use in practice because almost all virtual text needs to be softwrapped. For example inline diagnostics should be softwrapped to avoid cutting off the diagnostic message (as no scrolling is possible). While more complex virtual text like side by side diffs must dynamically calculate the number of empty lines two align two documents (which requires taking account both softwrap and virtual text). To address this, the API has been refactored to use a trait. The second issue caused some performance overhead and unnecessarily complicated the `DocumentFormatter`. It was addressed by only calling the trait mentioned above at line breaks (instead of always). This allows offers additional flexibility to annotations as it offers the flexibility to align lines (needed for side by side diffs). --- helix-core/src/doc_formatter.rs | 48 ++++--- helix-core/src/text_annotations.rs | 221 ++++++++++++++++++++++++----- 2 files changed, 214 insertions(+), 55 deletions(-) diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index f934b38a1..0f9a52b5d 100644 --- a/helix-core/src/doc_formatter.rs +++ b/helix-core/src/doc_formatter.rs @@ -11,7 +11,7 @@ use std::borrow::Cow; use std::fmt::Debug; -use std::mem::{replace, take}; +use std::mem::replace; #[cfg(test)] mod test; @@ -166,9 +166,6 @@ pub struct DocumentFormatter<'t> { line_pos: usize, exhausted: bool, - /// Line breaks to be reserved for virtual text - /// at the next line break - virtual_lines: usize, inline_anntoation_graphemes: Option<(Graphemes<'t>, Option)>, // softwrap specific @@ -209,7 +206,6 @@ pub fn new_at_prev_checkpoint( graphemes: RopeGraphemes::new(text.slice(block_char_idx..)), char_pos: block_char_idx, exhausted: false, - virtual_lines: 0, indent_level: None, peeked_grapheme: None, word_buf: Vec::with_capacity(64), @@ -250,7 +246,6 @@ fn advance_grapheme(&mut self, col: usize, char_pos: usize) -> Option usize { 0 }; + let virtual_lines = + self.annotations + .virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos); self.visual_pos.col = indent_carry_over as usize; - self.visual_pos.row += 1 + take(&mut self.virtual_lines); + self.visual_pos.row += 1 + virtual_lines; let mut i = 0; let mut word_width = 0; let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true) @@ -404,24 +402,32 @@ fn next(&mut self) -> Option { self.advance_grapheme(self.visual_pos.col, self.char_pos)? }; - let visual_pos = self.visual_pos; - let char_pos = self.char_pos; + let grapheme = FormattedGrapheme { + raw: grapheme.grapheme, + source: grapheme.source, + visual_pos: self.visual_pos, + line_idx: self.line_pos, + char_idx: self.char_pos, + }; + self.char_pos += grapheme.doc_chars(); - let line_idx = self.line_pos; - if grapheme.grapheme == Grapheme::Newline { - self.visual_pos.row += 1; - self.visual_pos.row += take(&mut self.virtual_lines); + if !grapheme.is_virtual() { + self.annotations.process_virtual_text_anchors(&grapheme); + } + if grapheme.raw == Grapheme::Newline { + // move to end of newline char + self.visual_pos.col += 1; + let virtual_lines = + self.annotations + .virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos); + self.visual_pos.row += 1 + virtual_lines; self.visual_pos.col = 0; - self.line_pos += 1; + if !grapheme.is_virtual() { + self.line_pos += 1; + } } else { self.visual_pos.col += grapheme.width(); } - Some(FormattedGrapheme { - raw: grapheme.grapheme, - source: grapheme.source, - visual_pos, - line_idx, - char_idx: char_pos, - }) + Some(grapheme) } } diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index 1576914e3..785e38fd7 100644 --- a/helix-core/src/text_annotations.rs +++ b/helix-core/src/text_annotations.rs @@ -1,8 +1,12 @@ use std::cell::Cell; +use std::cmp::Ordering; +use std::fmt::Debug; use std::ops::Range; +use std::ptr::NonNull; +use crate::doc_formatter::FormattedGrapheme; use crate::syntax::Highlight; -use crate::Tendril; +use crate::{Position, Tendril}; /// An inline annotation is continuous text shown /// on the screen before the grapheme that starts at @@ -75,19 +79,98 @@ pub fn new(char_idx: usize, grapheme: impl Into) -> Self { } } -/// Line annotations allow for virtual text between normal -/// text lines. They cause `height` empty lines to be inserted -/// below the document line that contains `anchor_char_idx`. +/// Line annotations allow inserting virtual text lines between normal text +/// lines. These lines can be filled with text in the rendering code as their +/// contents have no effect beyond visual appearance. /// -/// These lines can be filled with text in the rendering code -/// as their contents have no effect beyond visual appearance. +/// The height of virtual text is usually not known ahead of time as virtual +/// text often requires softwrapping. Furthermore the height of some virtual +/// text like side-by-side diffs depends on the height of the text (again +/// influenced by softwrap) and other virtual text. Therefore line annotations +/// are computed on the fly instead of ahead of time like other annotations. /// -/// To insert a line after a document line simply set -/// `anchor_char_idx` to `doc.line_to_char(line_idx)` -#[derive(Debug, Clone)] -pub struct LineAnnotation { - pub anchor_char_idx: usize, - pub height: usize, +/// The core of this trait `insert_virtual_lines` function. It is called at the +/// end of every visual line and allows the `LineAnnotation` to insert empty +/// virtual lines. Apart from that the `LineAnnotation` trait has multiple +/// methods that allow it to track anchors in the document. +/// +/// When a new traversal of a document starts `reset_pos` is called. Afterwards +/// the other functions are called with indices that are larger then the +/// one passed to `reset_pos`. This allows performing a binary search (use +/// `partition_point`) in `reset_pos` once and then to only look at the next +/// anchor during each method call. +/// +/// The `reset_pos`, `skip_conceal` and `process_anchor` functions all return a +/// `char_idx` anchor. This anchor is stored when transversing the document and +/// when the grapheme at the anchor is traversed the `process_anchor` function +/// is called. +/// +/// # Note +/// +/// All functions only receive immutable references to `self`. +/// `LineAnnotation`s that want to store an internal position or +/// state of some kind should use `Cell`. Using interior mutability for +/// caches is preferable as otherwise a lot of lifetimes become invariant +/// which complicates APIs a lot. +pub trait LineAnnotation { + /// Resets the internal position to `char_idx`. This function is called + /// when a new traversal of a document starts. + /// + /// All `char_idx` passed to `insert_virtual_lines` are strictly monotonically increasing + /// with the first `char_idx` greater or equal to the `char_idx` + /// passed to this function. + /// + /// # Returns + /// + /// The `char_idx` of the next anchor this `LineAnnotation` is interested in, + /// replaces the currently registered anchor. Return `usize::MAX` to ignore + fn reset_pos(&mut self, _char_idx: usize) -> usize { + usize::MAX + } + + /// Called when a text is concealed that contains an anchor registered by this `LineAnnotation`. + /// In this case the line decorations **must** ensure that virtual text anchored within that + /// char range is skipped. + /// + /// # Returns + /// + /// The `char_idx` of the next anchor this `LineAnnotation` is interested in, + /// **after the end of conceal_end_char_idx** + /// replaces the currently registered anchor. Return `usize::MAX` to ignore + fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize { + self.reset_pos(conceal_end_char_idx) + } + + /// Process an anchor (horizontal position is provided) and returns the next anchor. + /// + /// # Returns + /// + /// The `char_idx` of the next anchor this `LineAnnotation` is interested in, + /// replaces the currently registered anchor. Return `usize::MAX` to ignore + fn process_anchor(&mut self, _grapheme: &FormattedGrapheme) -> usize { + usize::MAX + } + + /// This function is called at the end of a visual line to insert virtual text + /// + /// # Returns + /// + /// The number of additional virtual lines to reserve + /// + /// # Note + /// + /// The `line_end_visual_pos` parameter indicates the visual vertical distance + /// from the start of block where the traversal starts. This includes the offset + /// from other `LineAnnotations`. This allows inline annotations to consider + /// the height of the text and "align" two different documents (like for side + /// by side diffs). These annotations that want to "align" two documents should + /// therefore be added last so that other virtual text is also considered while aligning + fn insert_virtual_lines( + &mut self, + line_end_char_idx: usize, + line_end_visual_pos: Position, + doc_line: usize, + ) -> Position; } #[derive(Debug)] @@ -143,13 +226,68 @@ fn reset_pos(layers: &[Layer], pos: usize, get_pos: impl Fn(&A) -> u } } +/// Safety: We store LineAnnotation in a NonNull pointer. This is necessary to work +/// around an unfortunate inconsistency in rusts variance system that unnnecesarily +/// makes the lifetime invariant if implemented with safe code. This makes the +/// DocFormatter API very cumbersome/basically impossible to work with. +/// +/// Normally object types `dyn Foo + 'a` are covariant so if we used `Box` below +/// everything would be alright. However we want to use `Cell>` +/// to be able to call the mutable function on `LineAnnotation`. The problem is that +/// some types like `Cell` make all their arguments invariant. This is important for soundness +/// normally for the same reasons that `&'a mut T` is invariant over `T` +/// (see ). However for `&'a mut` (`dyn Foo + 'b`) +/// there is a specical rule in the language to make `'b` covariant (otherwise trait objects would be +/// super annoying to use). See for +/// why this is sound. Sadly that rule doesn't apply to `Cell` +/// (or other invariant types like `UnsafeCell` or `*mut (dyn Foo + 'a)`). +/// +/// We sidestep the problem by using `NonNull` which is covariant. In the +/// special case of trait objects this is sound (easily checked by adding a +/// `PhantomData<&'a mut Foo + 'a)>` field). We don't need an explicit `Cell` +/// type here because we never hand out any refereces to the trait objects. That +/// means any reference to the pointer can create a valid multable reference +/// that is covariant over `'a` (or in other words it's a raw pointer, as long as +/// we don't hand out references we are free to do whatever we want). +struct RawBox(NonNull); + +impl RawBox { + /// Safety: Only a single mutable reference + /// created by this function may exist at a given time. + #[allow(clippy::mut_from_ref)] + unsafe fn get(&self) -> &mut T { + &mut *self.0.as_ptr() + } +} +impl From> for RawBox { + fn from(box_: Box) -> Self { + // obviously safe because Box::into_raw never returns null + unsafe { Self(NonNull::new_unchecked(Box::into_raw(box_))) } + } +} + +impl Drop for RawBox { + fn drop(&mut self) { + unsafe { drop(Box::from_raw(self.0.as_ptr())) } + } +} + /// Annotations that change that is displayed when the document is render. /// Also commonly called virtual text. -#[derive(Default, Debug, Clone)] +#[derive(Default)] pub struct TextAnnotations<'a> { inline_annotations: Vec>>, overlays: Vec>>, - line_annotations: Vec>, + line_annotations: Vec<(Cell, RawBox)>, +} + +impl Debug for TextAnnotations<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TextAnnotations") + .field("inline_annotations", &self.inline_annotations) + .field("overlays", &self.overlays) + .finish_non_exhaustive() + } } impl<'a> TextAnnotations<'a> { @@ -157,9 +295,9 @@ impl<'a> TextAnnotations<'a> { pub fn reset_pos(&self, char_idx: usize) { reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx); reset_pos(&self.overlays, char_idx, |annot| annot.char_idx); - reset_pos(&self.line_annotations, char_idx, |annot| { - annot.anchor_char_idx - }); + for (next_anchor, layer) in &self.line_annotations { + next_anchor.set(unsafe { layer.get().reset_pos(char_idx) }); + } } pub fn collect_overlay_highlights( @@ -219,8 +357,9 @@ pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option /// /// The line annotations **must be sorted** by their `char_idx`. /// Multiple line annotations with the same `char_idx` **are not allowed**. - pub fn add_line_annotation(&mut self, layer: &'a [LineAnnotation]) -> &mut Self { - self.line_annotations.push((layer, ()).into()); + pub fn add_line_annotation(&mut self, layer: Box) -> &mut Self { + self.line_annotations + .push((Cell::new(usize::MAX), layer.into())); self } @@ -250,21 +389,35 @@ pub(crate) fn overlay_at(&self, char_idx: usize) -> Option<(&Overlay, Option usize { - self.line_annotations - .iter() - .map(|layer| { - let mut lines = 0; - while let Some(annot) = layer.annotations.get(layer.current_index.get()) { - if annot.anchor_char_idx == char_idx { - layer.current_index.set(layer.current_index.get() + 1); - lines += annot.height - } else { - break; + pub(crate) fn process_virtual_text_anchors(&self, grapheme: &FormattedGrapheme) { + for (next_anchor, layer) in &self.line_annotations { + loop { + match next_anchor.get().cmp(&grapheme.char_idx) { + Ordering::Less => next_anchor + .set(unsafe { layer.get().skip_concealed_anchors(grapheme.char_idx) }), + Ordering::Equal => { + next_anchor.set(unsafe { layer.get().process_anchor(grapheme) }) } - } - lines - }) - .sum() + Ordering::Greater => break, + }; + } + } + } + + pub(crate) fn virtual_lines_at( + &self, + char_idx: usize, + line_end_visual_pos: Position, + doc_line: usize, + ) -> usize { + let mut virt_off = Position::new(0, 0); + for (_, layer) in &self.line_annotations { + virt_off += unsafe { + layer + .get() + .insert_virtual_lines(char_idx, line_end_visual_pos + virt_off, doc_line) + }; + } + virt_off.row } } From 9a93240d27ac2cdd589526293d6901b2342c8576 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:06 +0100 Subject: [PATCH 128/200] correctly wrap at text-width --- helix-core/src/doc_formatter.rs | 86 +++++++++++++++++++++------- helix-core/src/doc_formatter/test.rs | 20 +++++++ helix-view/src/document.rs | 16 +++--- 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index 0f9a52b5d..65e1532f0 100644 --- a/helix-core/src/doc_formatter.rs +++ b/helix-core/src/doc_formatter.rs @@ -10,6 +10,7 @@ //! called a "block" and the caller must advance it as needed. use std::borrow::Cow; +use std::cmp::Ordering; use std::fmt::Debug; use std::mem::replace; @@ -43,6 +44,11 @@ pub fn is_virtual(self) -> bool { matches!(self, GraphemeSource::VirtualText { .. }) } + pub fn is_eof(self) -> bool { + // all doc chars except the EOF char have non-zero codepoints + matches!(self, GraphemeSource::Document { codepoints: 0 }) + } + pub fn doc_chars(self) -> usize { match self { GraphemeSource::Document { codepoints } => codepoints as usize, @@ -117,6 +123,14 @@ fn is_whitespace(&self) -> bool { self.grapheme.is_whitespace() } + fn is_newline(&self) -> bool { + matches!(self.grapheme, Grapheme::Newline) + } + + fn is_eof(&self) -> bool { + self.source.is_eof() + } + fn width(&self) -> usize { self.grapheme.width() } @@ -135,6 +149,7 @@ pub struct TextFormat { pub wrap_indicator: Box, pub wrap_indicator_highlight: Option, pub viewport_width: u16, + pub soft_wrap_at_text_width: bool, } // test implementation is basically only used for testing or when softwrap is always disabled @@ -148,6 +163,7 @@ fn default() -> Self { wrap_indicator: Box::from(" "), viewport_width: 17, wrap_indicator_highlight: None, + soft_wrap_at_text_width: false, } } } @@ -318,39 +334,68 @@ fn wrap_word(&mut self) -> usize { .change_position(visual_x, self.text_fmt.tab_width); word_width += grapheme.width(); } + if let Some(grapheme) = &mut self.peeked_grapheme { + let visual_x = self.visual_pos.col + word_width; + grapheme + .grapheme + .change_position(visual_x, self.text_fmt.tab_width); + } word_width } + fn peek_grapheme(&mut self, col: usize, char_pos: usize) -> Option<&GraphemeWithSource<'t>> { + if self.peeked_grapheme.is_none() { + self.peeked_grapheme = self.advance_grapheme(col, char_pos); + } + self.peeked_grapheme.as_ref() + } + + fn next_grapheme(&mut self, col: usize, char_pos: usize) -> Option> { + self.peek_grapheme(col, char_pos); + self.peeked_grapheme.take() + } + fn advance_to_next_word(&mut self) { self.word_buf.clear(); let mut word_width = 0; let mut word_chars = 0; + if self.exhausted { + return; + } + loop { - // softwrap word if necessary - if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize { - // wrapping this word would move too much text to the next line - // split the word at the line end instead - if word_width > self.text_fmt.max_wrap as usize { - // Usually we stop accomulating graphemes as soon as softwrapping becomes necessary. - // However if the last grapheme is multiple columns wide it might extend beyond the EOL. - // The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line - if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize { - self.peeked_grapheme = self.word_buf.pop(); - } + let mut col = self.visual_pos.col + word_width; + let char_pos = self.char_pos + word_chars; + match col.cmp(&(self.text_fmt.viewport_width as usize)) { + // The EOF char and newline chars are always selectable in helix. That means + // that wrapping happens "too-early" if a word fits a line perfectly. This + // is intentional so that all selectable graphemes are always visisble (and + // therefore the cursor never dissapears). However if the user manually set a + // lower softwrap width then this is undesirable. Just increasing the viewport- + // width by one doesn't work because if a line is wrapped multiple times then + // some words may extend past the specified width. + // + // So we special case a word that ends exactly at line bounds and is followed + // by a newline/eof character here. + Ordering::Equal + if self.text_fmt.soft_wrap_at_text_width + && self.peek_grapheme(col, char_pos).map_or(false, |grapheme| { + grapheme.is_newline() || grapheme.is_eof() + }) => {} + Ordering::Equal if word_width > self.text_fmt.max_wrap as usize => return, + Ordering::Greater if word_width > self.text_fmt.max_wrap as usize => { + self.peeked_grapheme = self.word_buf.pop(); return; } - - word_width = self.wrap_word(); + Ordering::Equal | Ordering::Greater => { + word_width = self.wrap_word(); + col = self.visual_pos.col + word_width; + } + Ordering::Less => (), } - let grapheme = if let Some(grapheme) = self.peeked_grapheme.take() { - grapheme - } else if let Some(grapheme) = - self.advance_grapheme(self.visual_pos.col + word_width, self.char_pos + word_chars) - { - grapheme - } else { + let Some(grapheme) = self.next_grapheme(col, char_pos) else { return; }; word_chars += grapheme.doc_chars(); @@ -376,7 +421,6 @@ fn advance_to_next_word(&mut self) { pub fn next_char_pos(&self) -> usize { self.char_pos } - /// returns the visual position at the end of the last yielded grapheme pub fn next_visual_pos(&self) -> Position { self.visual_pos diff --git a/helix-core/src/doc_formatter/test.rs b/helix-core/src/doc_formatter/test.rs index b214ab944..415ce8f6a 100644 --- a/helix-core/src/doc_formatter/test.rs +++ b/helix-core/src/doc_formatter/test.rs @@ -12,6 +12,7 @@ fn new_test(softwrap: bool) -> Self { wrap_indicator_highlight: None, // use a prime number to allow lining up too often with repeat viewport_width: 17, + soft_wrap_at_text_width: false, } } } @@ -21,6 +22,7 @@ fn collect_to_str(&mut self) -> String { use std::fmt::Write; let mut res = String::new(); let viewport_width = self.text_fmt.viewport_width; + let soft_wrap_at_text_width = self.text_fmt.soft_wrap_at_text_width; let mut line = 0; for grapheme in self { @@ -28,6 +30,8 @@ fn collect_to_str(&mut self) -> String { line += 1; assert_eq!(grapheme.visual_pos.row, line); write!(res, "\n{}", ".".repeat(grapheme.visual_pos.col)).unwrap(); + } + if !soft_wrap_at_text_width { assert!( grapheme.visual_pos.col <= viewport_width as usize, "softwrapped failed {}<={viewport_width}", @@ -98,6 +102,22 @@ fn long_word_softwrap() { ); } +fn softwrap_text_at_text_width(text: &str) -> String { + let mut text_fmt = TextFormat::new_test(true); + text_fmt.soft_wrap_at_text_width = true; + let annotations = TextAnnotations::default(); + let mut formatter = + DocumentFormatter::new_at_prev_checkpoint(text.into(), &text_fmt, &annotations, 0); + formatter.collect_to_str() +} +#[test] +fn long_word_softwrap_text_width() { + assert_eq!( + softwrap_text_at_text_width("xxxxxxxx1xxxx2xxx\nxxxxxxxx1xxxx2xxx"), + "xxxxxxxx1xxxx2xxx \nxxxxxxxx1xxxx2xxx " + ); +} + fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String { DocumentFormatter::new_at_prev_checkpoint( text.into(), diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3314a243f..412f79fa2 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1953,7 +1953,7 @@ pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> Tex .language_config() .and_then(|config| config.text_width) .unwrap_or(config.text_width); - let soft_wrap_at_text_width = self + let mut soft_wrap_at_text_width = self .language_config() .and_then(|config| { config @@ -1964,12 +1964,13 @@ pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> Tex .or(config.soft_wrap.wrap_at_text_width) .unwrap_or(false); if soft_wrap_at_text_width { - // We increase max_line_len by 1 because softwrap considers the newline character - // as part of the line length while the "typical" expectation is that this is not the case. - // In particular other commands like :reflow do not count the line terminator. - // This is technically inconsistent for the last line as that line never has a line terminator - // but having the last visual line exceed the width by 1 seems like a rare edge case. - viewport_width = viewport_width.min(text_width as u16 + 1) + // if the viewport is smaller than the specified + // width then this setting has no effcet + if text_width >= viewport_width as usize { + soft_wrap_at_text_width = false; + } else { + viewport_width = text_width as u16; + } } let config = self.config.load(); let editor_soft_wrap = &config.soft_wrap; @@ -2006,6 +2007,7 @@ pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> Tex wrap_indicator_highlight: theme .and_then(|theme| theme.find_scope_index("ui.virtual.wrap")) .map(Highlight), + soft_wrap_at_text_width, } } From 2c0506aa9609033e3e61426d9658e3179107bb86 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:07 +0100 Subject: [PATCH 129/200] streamline text decoration API This commit brings the text decoration API inline with the LineAnnotation API (so they are consistent) resulting in a single streamlined API instead of multiple ADHOK callbacks. --- helix-term/src/application.rs | 2 +- helix-term/src/ui/document.rs | 325 +++++++++++--------------- helix-term/src/ui/editor.rs | 74 +++--- helix-term/src/ui/mod.rs | 1 + helix-term/src/ui/picker.rs | 14 +- helix-term/src/ui/text_decorations.rs | 175 ++++++++++++++ helix-view/src/editor.rs | 39 +++- 7 files changed, 379 insertions(+), 251 deletions(-) create mode 100644 helix-term/src/ui/text_decorations.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 9695703b7..3d10862d1 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -290,7 +290,7 @@ async fn render(&mut self) { self.compositor.render(area, surface, &mut cx); let (pos, kind) = self.compositor.cursor(area, &self.editor); // reset cursor cache - self.editor.cursor_cache.set(None); + self.editor.cursor_cache.reset(); let pos = pos.map(|pos| (pos.col as u16, pos.row as u16)); self.terminal.draw(pos, kind).unwrap(); diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index 34282b261..2da4d4b31 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -12,26 +12,10 @@ use helix_view::graphics::Rect; use helix_view::theme::Style; use helix_view::view::ViewPosition; -use helix_view::Document; -use helix_view::Theme; +use helix_view::{Document, Theme}; use tui::buffer::Buffer as Surface; -pub trait LineDecoration { - fn render_background(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {} - fn render_foreground( - &mut self, - _renderer: &mut TextRenderer, - _pos: LinePos, - _end_char_idx: usize, - ) { - } -} - -impl LineDecoration for F { - fn render_background(&mut self, renderer: &mut TextRenderer, pos: LinePos) { - self(renderer, pos) - } -} +use crate::ui::text_decorations::DecorationManager; #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum StyleIterKind { @@ -95,15 +79,8 @@ pub struct LinePos { pub doc_line: usize, /// Vertical offset from the top of the inner view area pub visual_line: u16, - /// The first char index of this visual line. - /// Note that if the visual line is entirely filled by - /// a very long inline virtual text then this index will point - /// at the next (non-virtual) char after this visual line - pub start_char_idx: usize, } -pub type TranslatedPosition<'a> = (usize, Box); - #[allow(clippy::too_many_arguments)] pub fn render_document( surface: &mut Surface, @@ -114,84 +91,46 @@ pub fn render_document( syntax_highlight_iter: impl Iterator, overlay_highlight_iter: impl Iterator, theme: &Theme, - line_decoration: &mut [Box], - translated_positions: &mut [TranslatedPosition], + decorations: DecorationManager, ) { - let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport); + let mut renderer = TextRenderer::new( + surface, + doc, + theme, + Position::new(offset.vertical_offset, offset.horizontal_offset), + viewport, + ); render_text( &mut renderer, doc.text().slice(..), - offset, + offset.anchor, &doc.text_format(viewport.width, Some(theme)), doc_annotations, syntax_highlight_iter, overlay_highlight_iter, theme, - line_decoration, - translated_positions, + decorations, ) } -fn translate_positions( - char_pos: usize, - first_visible_char_idx: usize, - translated_positions: &mut [TranslatedPosition], - text_fmt: &TextFormat, - renderer: &mut TextRenderer, - pos: Position, -) { - // check if any positions translated on the fly (like cursor) has been reached - for (char_idx, callback) in &mut *translated_positions { - if *char_idx < char_pos && *char_idx >= first_visible_char_idx { - // by replacing the char_index with usize::MAX large number we ensure - // that the same position is only translated once - // text will never reach usize::MAX as rust memory allocations are limited - // to isize::MAX - *char_idx = usize::MAX; - - if text_fmt.soft_wrap { - callback(renderer, pos) - } else if pos.col >= renderer.col_offset - && pos.col - renderer.col_offset < renderer.viewport.width as usize - { - callback( - renderer, - Position { - row: pos.row, - col: pos.col - renderer.col_offset, - }, - ) - } - } - } -} - #[allow(clippy::too_many_arguments)] pub fn render_text<'t>( renderer: &mut TextRenderer, - text: RopeSlice<'t>, - offset: ViewPosition, + text: RopeSlice<'_>, + anchor: usize, text_fmt: &TextFormat, text_annotations: &TextAnnotations, syntax_highlight_iter: impl Iterator, overlay_highlight_iter: impl Iterator, theme: &Theme, - line_decorations: &mut [Box], - translated_positions: &mut [TranslatedPosition], + mut decorations: DecorationManager, ) { - let mut row_off = visual_offset_from_block( - text, - offset.anchor, - offset.anchor, - text_fmt, - text_annotations, - ) - .0 - .row; - row_off += offset.vertical_offset; + let row_off = visual_offset_from_block(text, anchor, anchor, text_fmt, text_annotations) + .0 + .row; let mut formatter = - DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor); + DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, anchor); let mut syntax_styles = StyleIter { text_style: renderer.text_style, active_highlights: Vec::with_capacity(64), @@ -213,8 +152,8 @@ pub fn render_text<'t>( first_visual_line: false, doc_line: usize::MAX, visual_line: u16::MAX, - start_char_idx: usize::MAX, }; + let mut last_line_end = 0; let mut is_in_indent_area = true; let mut last_line_indent_level = 0; let mut syntax_style_span = syntax_styles @@ -223,58 +162,22 @@ pub fn render_text<'t>( let mut overlay_style_span = overlay_styles .next() .unwrap_or_else(|| (Style::default(), usize::MAX)); - let mut first_visible_char_idx = formatter.next_char_pos(); + let mut reached_view_top = false; loop { - // formattter.line_pos returns to line index of the next grapheme - // so it must be called before formatter.next let Some(mut grapheme) = formatter.next() else { - let mut last_pos = formatter.next_visual_pos(); - if last_pos.row >= row_off { - last_pos.col -= 1; - last_pos.row -= row_off; - // check if any positions translated on the fly (like cursor) are at the EOF - translate_positions( - text.len_chars() + 1, - first_visible_char_idx, - translated_positions, - text_fmt, - renderer, - last_pos, - ); - } break; }; // skip any graphemes on visual lines before the block start - // if pos.row < row_off { - // if char_pos >= syntax_style_span.1 { - // syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() { - // syntax_style_span - // } else { - // break; - // } - // } - // if char_pos >= overlay_style_span.1 { - // overlay_style_span = if let Some(overlay_style_span) = overlay_styles.next() { - // overlay_style_span if grapheme.visual_pos.row < row_off { - if grapheme.char_idx >= style_span.1 { - style_span = if let Some(style_span) = styles.next() { - style_span - } else { - break; - }; - overlay_span = if let Some(overlay_span) = overlays.next() { - overlay_span - } else { - break; - }; - } - first_visible_char_idx = formatter.next_char_pos(); continue; } grapheme.visual_pos.row -= row_off; + if !reached_view_top { + decorations.prepare_for_rendering(grapheme.char_idx); + reached_view_top = true; + } // if the end of the viewport is reached stop rendering if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 { @@ -283,87 +186,67 @@ pub fn render_text<'t>( // apply decorations before rendering a new line if grapheme.visual_pos.row as u16 != last_line_pos.visual_line { - if grapheme.visual_pos.row > 0 { + // we initiate doc_line with usize::MAX because no file + // can reach that size (memory allocations are limited to isize::MAX) + // initially there is no "previous" line (so doc_line is set to usize::MAX) + // in that case we don't need to draw indent guides/virtual text + if last_line_pos.doc_line != usize::MAX { // draw indent guides for the last line - renderer - .draw_indent_guides(last_line_indent_level, last_line_pos.visual_line as u16); + renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); is_in_indent_area = true; - for line_decoration in &mut *line_decorations { - line_decoration.render_foreground(renderer, last_line_pos, grapheme.char_idx); - } + decorations.render_virtual_lines(renderer, last_line_pos, last_line_end) } last_line_pos = LinePos { first_visual_line: grapheme.line_idx != last_line_pos.doc_line, doc_line: grapheme.line_idx, visual_line: grapheme.visual_pos.row as u16, - start_char_idx: grapheme.char_idx, }; - for line_decoration in &mut *line_decorations { - line_decoration.render_background(renderer, last_line_pos); - } + decorations.decorate_line(renderer, last_line_pos); } // acquire the correct grapheme style - while grapheme.char_idx >= syntax_style_span.1 { + while grapheme.char_idx >= syntax_style_span.1 { syntax_style_span = syntax_styles .next() .unwrap_or((Style::default(), usize::MAX)); } - while grapheme.char_idx >= overlay_style_span.1 { + while grapheme.char_idx >= overlay_style_span.1 { overlay_style_span = overlay_styles .next() .unwrap_or((Style::default(), usize::MAX)); } - // check if any positions translated on the fly (like cursor) has been reached - translate_positions( - formatter.next_char_pos(), - first_visible_char_idx, - translated_positions, - text_fmt, - renderer, - grapheme.visual_pos, - ); - - let (syntax_style, overlay_style) = - if let GraphemeSource::VirtualText { highlight } = grapheme.source { - let mut style = renderer.text_style; - if let Some(highlight) = highlight { - style = style.patch(theme.highlight(highlight.0)) - } - (style, Style::default()) - } else { - (syntax_style_span.0, overlay_style_span.0) - }; - - let is_virtual = grapheme.is_virtual(); - renderer.draw_grapheme( -<<<<<<< HEAD - grapheme.grapheme, + let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source { + let mut style = renderer.text_style; + if let Some(highlight) = highlight { + style = style.patch(theme.highlight(highlight.0)); + } GraphemeStyle { - syntax_style, - overlay_style, - }, - is_virtual, -||||||| parent of 5e32edd8 (track char_idx in DocFormatter) - grapheme.grapheme, - grapheme_style, - virt, -======= + syntax_style: style, + overlay_style: Style::default(), + } + } else { + GraphemeStyle { + syntax_style: syntax_style_span.0, + overlay_style: overlay_style_span.0, + } + }; + decorations.decorate_grapheme(renderer, &grapheme); + + let virt = grapheme.is_virtual(); + let grapheme_width = renderer.draw_grapheme( grapheme.raw, grapheme_style, virt, ->>>>>>> 5e32edd8 (track char_idx in DocFormatter) &mut last_line_indent_level, &mut is_in_indent_area, grapheme.visual_pos, ); + last_line_end = grapheme.visual_pos.col + grapheme_width; } renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); - for line_decoration in &mut *line_decorations { - line_decoration.render_foreground(renderer, last_line_pos, formatter.next_char_pos()); - } + decorations.render_virtual_lines(renderer, last_line_pos, last_line_end) } #[derive(Debug)] @@ -382,8 +265,8 @@ pub struct TextRenderer<'a> { pub indent_width: u16, pub starting_indent: usize, pub draw_indent_guides: bool, - pub col_offset: usize, pub viewport: Rect, + pub offset: Position, } pub struct GraphemeStyle { @@ -396,7 +279,7 @@ pub fn new( surface: &'a mut Surface, doc: &Document, theme: &Theme, - col_offset: usize, + offset: Position, viewport: Rect, ) -> TextRenderer<'a> { let editor_config = doc.config.load(); @@ -451,8 +334,8 @@ pub fn new( virtual_tab, whitespace_style: theme.get("ui.virtual.whitespace"), indent_width, - starting_indent: col_offset / indent_width as usize - + (col_offset % indent_width as usize != 0) as usize + starting_indent: offset.col / indent_width as usize + + (offset.col % indent_width as usize != 0) as usize + editor_config.indent_guides.skip_levels as usize, indent_guide_style: text_style.patch( theme @@ -462,7 +345,7 @@ pub fn new( text_style, draw_indent_guides: editor_config.indent_guides.render, viewport, - col_offset, + offset, } } @@ -474,9 +357,13 @@ pub fn draw_grapheme( is_virtual: bool, last_indent_level: &mut usize, is_in_indent_area: &mut bool, - position: Position, - ) { - let cut_off_start = self.col_offset.saturating_sub(position.col); + mut position: Position, + ) -> usize { + if position.row < self.offset.row { + return 0; + } + position.row -= self.offset.row; + let cut_off_start = self.offset.col.saturating_sub(position.col); let is_whitespace = grapheme.is_whitespace(); // TODO is it correct to apply the whitespace style to all unicode white spaces? @@ -508,12 +395,11 @@ pub fn draw_grapheme( Grapheme::Newline => &self.newline, }; - let in_bounds = self.col_offset <= position.col - && position.col < self.viewport.width as usize + self.col_offset; + let in_bounds = self.column_in_bounds(position.col + width - 1); if in_bounds { self.surface.set_string( - self.viewport.x + (position.col - self.col_offset) as u16, + self.viewport.x + (position.col - self.offset.col) as u16, self.viewport.y + position.row as u16, grapheme, style, @@ -533,26 +419,33 @@ pub fn draw_grapheme( *last_indent_level = position.col; *is_in_indent_area = false; } + + width + } + + pub fn column_in_bounds(&self, colum: usize) -> bool { + self.offset.col <= colum && colum < self.viewport.width as usize + self.offset.col } /// Overlay indentation guides ontop of a rendered line /// The indentation level is computed in `draw_lines`. /// Therefore this function must always be called afterwards. - pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) { - if !self.draw_indent_guides { + pub fn draw_indent_guides(&mut self, indent_level: usize, mut row: u16) { + if !self.draw_indent_guides || self.offset.row > row as usize { return; } + row -= self.offset.row as u16; // Don't draw indent guides outside of view let end_indent = min( indent_level, // Add indent_width - 1 to round up, since the first visible // indent might be a bit after offset.col - self.col_offset + self.viewport.width as usize + (self.indent_width as usize - 1), + self.offset.col + self.viewport.width as usize + (self.indent_width as usize - 1), ) / self.indent_width as usize; for i in self.starting_indent..end_indent { - let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.col_offset) + let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.offset.col) as u16; let y = self.viewport.y + row; debug_assert!(self.surface.in_bounds(x, y)); @@ -560,4 +453,62 @@ pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) { .set_string(x, y, &self.indent_guide_char, self.indent_guide_style); } } + + pub fn set_string(&mut self, x: u16, y: u16, string: impl AsRef, style: Style) { + if (y as usize) < self.offset.row { + return; + } + self.surface + .set_string(x, y + self.viewport.y, string, style) + } + + pub fn set_stringn( + &mut self, + x: u16, + y: u16, + string: impl AsRef, + width: usize, + style: Style, + ) { + if (y as usize) < self.offset.row { + return; + } + self.surface + .set_stringn(x, y + self.viewport.y, string, width, style); + } + + /// Sets the style of an area **within the text viewport* this accounts + /// both for the renderers vertical offset and its viewport + pub fn set_style(&mut self, mut area: Rect, style: Style) { + area = area.clip_top(self.offset.row as u16); + area.y += self.viewport.y; + self.surface.set_style(area, style); + } + + /// Sets the style of an area **within the text viewport* this accounts + /// both for the renderers vertical offset and its viewport + #[allow(clippy::too_many_arguments)] + pub fn set_string_truncated( + &mut self, + x: u16, + y: u16, + string: &str, + width: usize, + style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style + ellipsis: bool, + truncate_start: bool, + ) -> (u16, u16) { + if (y as usize) < self.offset.row { + return (x, y); + } + self.surface.set_string_truncated( + x, + y + self.viewport.y, + string, + width, + style, + ellipsis, + truncate_start, + ) + } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a071bfaa8..49386b835 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -5,8 +5,10 @@ key, keymap::{KeymapResult, Keymaps}, ui::{ - document::{render_document, LinePos, TextRenderer, TranslatedPosition}, - Completion, ProgressSpinners, + document::{render_document, LinePos, TextRenderer}, + statusline, + text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics}, + Completion, CompletionItem, ProgressSpinners, }, }; @@ -31,9 +33,6 @@ use tui::{buffer::Buffer as Surface, text::Span}; -use super::document::LineDecoration; -use super::{completion::CompletionItem, statusline}; - pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option, @@ -94,11 +93,10 @@ pub fn render_view( let config = editor.config(); let text_annotations = view.text_annotations(doc, Some(theme)); - let mut line_decorations: Vec> = Vec::new(); - let mut translated_positions: Vec = Vec::new(); + let mut decorations = DecorationManager::default(); if is_focused && config.cursorline { - line_decorations.push(Self::cursorline_decorator(doc, view, theme)) + decorations.add_decoration(Self::cursorline(doc, view, theme)); } if is_focused && config.cursorcolumn { @@ -113,13 +111,10 @@ pub fn render_view( if pos.doc_line != dap_line { return; } - renderer.surface.set_style( - Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1), - style, - ); + renderer.set_style(Rect::new(inner.x, pos.visual_line, inner.width, 1), style); }; - line_decorations.push(Box::new(line_decoration)); + decorations.add_decoration(line_decoration); } let syntax_highlights = @@ -176,22 +171,20 @@ pub fn render_view( view.area, theme, is_focused & self.terminal_focused, - &mut line_decorations, + &mut decorations, ); } + let primary_cursor = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); if is_focused { - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - // set the cursor_cache to out of view in case the position is not found - editor.cursor_cache.set(Some(None)); - let update_cursor_cache = - |_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos))); - translated_positions.push((cursor, Box::new(update_cursor_cache))); + decorations.add_decoration(text_decorations::Cursor { + cache: &editor.cursor_cache, + primary_cursor, + }); } - render_document( surface, inner, @@ -201,8 +194,7 @@ pub fn render_view( syntax_highlights, overlay_highlights, theme, - &mut line_decorations, - &mut translated_positions, + decorations, ); Self::render_rulers(editor, doc, view, inner, surface, theme); @@ -637,7 +629,7 @@ pub fn render_gutter<'d>( viewport: Rect, theme: &Theme, is_focused: bool, - line_decorations: &mut Vec>, + decoration_manager: &mut DecorationManager<'d>, ) { let text = doc.text().slice(..); let cursors: Rc<[_]> = doc @@ -663,7 +655,7 @@ pub fn render_gutter<'d>( // TODO handle softwrap in gutters let selected = cursors.contains(&pos.doc_line); let x = viewport.x + offset; - let y = viewport.y + pos.visual_line; + let y = pos.visual_line; let gutter_style = match (selected, pos.first_visual_line) { (false, true) => gutter_style, @@ -675,11 +667,9 @@ pub fn render_gutter<'d>( if let Some(style) = gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) { - renderer - .surface - .set_stringn(x, y, &text, width, gutter_style.patch(style)); + renderer.set_stringn(x, y, &text, width, gutter_style.patch(style)); } else { - renderer.surface.set_style( + renderer.set_style( Rect { x, y, @@ -691,7 +681,7 @@ pub fn render_gutter<'d>( } text.clear(); }; - line_decorations.push(Box::new(gutter_decoration)); + decoration_manager.add_decoration(gutter_decoration); offset += width as u16; } @@ -761,11 +751,7 @@ pub fn render_diagnostics( } /// Apply the highlighting on the lines where a cursor is active - pub fn cursorline_decorator( - doc: &Document, - view: &View, - theme: &Theme, - ) -> Box { + pub fn cursorline(doc: &Document, view: &View, theme: &Theme) -> impl Decoration { let text = doc.text().slice(..); // TODO only highlight the visual line that contains the cursor instead of the full visual line let primary_line = doc.selection(view.id).primary().cursor_line(text); @@ -786,16 +772,14 @@ pub fn cursorline_decorator( let secondary_style = theme.get("ui.cursorline.secondary"); let viewport = view.area; - let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { - let area = Rect::new(viewport.x, viewport.y + pos.visual_line, viewport.width, 1); + move |renderer: &mut TextRenderer, pos: LinePos| { + let area = Rect::new(viewport.x, pos.visual_line, viewport.width, 1); if primary_line == pos.doc_line { - renderer.surface.set_style(area, primary_style); + renderer.set_style(area, primary_style); } else if secondary_lines.binary_search(&pos.doc_line).is_ok() { - renderer.surface.set_style(area, secondary_style); + renderer.set_style(area, secondary_style); } - }; - - Box::new(line_decoration) + } } /// Apply the highlighting on the columns where a cursor is active diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index fae64062d..10f4104ab 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -12,6 +12,7 @@ mod spinner; mod statusline; mod text; +mod text_decorations; use crate::compositor::Compositor; use crate::filter_picker_entry; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 079012396..70be421ae 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -7,8 +7,9 @@ ctrl, key, shift, ui::{ self, - document::{render_document, LineDecoration, LinePos, TextRenderer}, + document::{render_document, LinePos, TextRenderer}, picker::query::PickerQuery, + text_decorations::DecorationManager, EditorView, }, }; @@ -895,7 +896,7 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context } overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans)); } - let mut decorations: Vec> = Vec::new(); + let mut decorations = DecorationManager::default(); if let Some((start, end)) = range { let style = cx @@ -907,14 +908,14 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context if (start..=end).contains(&pos.doc_line) { let area = Rect::new( renderer.viewport.x, - renderer.viewport.y + pos.visual_line, + pos.visual_line, renderer.viewport.width, 1, ); - renderer.surface.set_style(area, style) + renderer.set_style(area, style) } }; - decorations.push(Box::new(draw_highlight)) + decorations.add_decoration(draw_highlight); } render_document( @@ -927,8 +928,7 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context syntax_highlights, overlay_highlights, &cx.editor.theme, - &mut decorations, - &mut [], + decorations, ); } } diff --git a/helix-term/src/ui/text_decorations.rs b/helix-term/src/ui/text_decorations.rs new file mode 100644 index 000000000..630af5817 --- /dev/null +++ b/helix-term/src/ui/text_decorations.rs @@ -0,0 +1,175 @@ +use std::cmp::Ordering; + +use helix_core::doc_formatter::FormattedGrapheme; +use helix_core::Position; +use helix_view::editor::CursorCache; + +use crate::ui::document::{LinePos, TextRenderer}; + +pub use diagnostics::InlineDiagnostics; + +mod diagnostics; + +/// Decorations are the primary mechanism for extending the text rendering. +/// +/// Any on-screen element which is anchored to the rendered text in some form +/// should be implemented using this trait. Translating char positions to +/// on-screen positions can be expensive and should not be done manually in the +/// ui loop. Instead such translations are automatically performed on the fly +/// while the text is being rendered. The results are provided to this trait by +/// the rendering infrastructure. +/// +/// To reserve space for virtual text lines (which is then filled by this trait) emit appropriate +/// [`LineAnnotation`](helix_core::text_annotations::LineAnnotation)s in [`helix_view::View::text_annotations`] +pub trait Decoration { + /// Called **before** a **visual** line is rendered. A visual line does not + /// necessarily correspond to a single line in a document as soft wrapping can + /// spread a single document line across multiple visual lines. + /// + /// This function is called before text is rendered as any decorations should + /// never overlap the document text. That means that setting the forground color + /// here is (essentially) useless as the text color is overwritten by the + /// rendered text. This _of course_ doesn't apply when rendering inside virtual lines + /// below the line reserved by `LineAnnotation`s as no text will be rendered here. + fn decorate_line(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {} + + /// Called **after** a **visual** line is rendered. A visual line does not + /// necessarily correspond to a single line in a document as soft wrapping can + /// spread a single document line across multiple visual lines. + /// + /// This function is called after text is rendered so that decorations can collect + /// horizontal positions on the line (see [`Decoration::decorate_grapheme`]) first and + /// use those positions` while rendering + /// virtual text. + /// That means that setting the forground color + /// here is (essentially) useless as the text color is overwritten by the + /// rendered text. This -ofcourse- doesn't apply when rendering inside virtual lines + /// below the line reserved by `LineAnnotation`s. e as no text will be rendered here. + /// **Note**: To avoid overlapping decorations in the virtual lines, each decoration + /// must return the number of virtual text lines it has taken up. Each `Decoration` recieves + /// an offset `virt_off` based on these return values where it can render virtual text: + /// + /// That means that a `render_line` implementation that returns `X` can render virtual text + /// in the following area: + /// ``` no-compile + /// let start = inner.y + pos.virtual_line + virt_off; + /// start .. start + X + /// ```` + fn render_virt_lines( + &mut self, + _renderer: &mut TextRenderer, + _pos: LinePos, + _virt_off: Position, + ) -> Position { + Position::new(0, 0) + } + + fn reset_pos(&mut self, _pos: usize) -> usize { + usize::MAX + } + + fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize { + self.reset_pos(conceal_end_char_idx) + } + + /// This function is called **before** the grapheme at `char_idx` is rendered. + /// + /// # Returns + /// + /// The char idx of the next grapheme that this function should be called for + fn decorate_grapheme( + &mut self, + _renderer: &mut TextRenderer, + _grapheme: &FormattedGrapheme, + ) -> usize { + usize::MAX + } +} + +impl Decoration for F { + fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) { + self(renderer, pos); + } +} + +#[derive(Default)] +pub struct DecorationManager<'a> { + decorations: Vec<(Box, usize)>, +} + +impl<'a> DecorationManager<'a> { + pub fn add_decoration(&mut self, decoration: impl Decoration + 'a) { + self.decorations.push((Box::new(decoration), 0)); + } + + pub fn prepare_for_rendering(&mut self, first_visible_char: usize) { + for (decoration, next_position) in &mut self.decorations { + *next_position = decoration.reset_pos(first_visible_char) + } + } + + pub fn decorate_grapheme(&mut self, renderer: &mut TextRenderer, grapheme: &FormattedGrapheme) { + for (decoration, hook_char_idx) in &mut self.decorations { + loop { + match (*hook_char_idx).cmp(&grapheme.char_idx) { + // this grapheme has been concealed or we are at the first grapheme + Ordering::Less => { + *hook_char_idx = decoration.skip_concealed_anchor(grapheme.char_idx) + } + Ordering::Equal => { + *hook_char_idx = decoration.decorate_grapheme(renderer, grapheme) + } + Ordering::Greater => break, + } + } + } + } + + pub fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) { + for (decoration, _) in &mut self.decorations { + decoration.decorate_line(renderer, pos); + } + } + + pub fn render_virtual_lines( + &mut self, + renderer: &mut TextRenderer, + pos: LinePos, + line_width: usize, + ) { + let mut virt_off = Position::new(1, line_width); // start at 1 so the line is never overwritten + for (decoration, _) in &mut self.decorations { + virt_off += decoration.render_virt_lines(renderer, pos, virt_off); + } + } +} + +/// Cursor rendering is done externally so all the cursor decoration +/// does is save the position of primary cursor +pub struct Cursor<'a> { + pub cache: &'a CursorCache, + pub primary_cursor: usize, +} +impl Decoration for Cursor<'_> { + fn reset_pos(&mut self, pos: usize) -> usize { + if pos <= self.primary_cursor { + self.primary_cursor + } else { + usize::MAX + } + } + + fn decorate_grapheme( + &mut self, + renderer: &mut TextRenderer, + grapheme: &FormattedGrapheme, + ) -> usize { + if renderer.column_in_bounds(grapheme.visual_pos.col) + && renderer.offset.row < grapheme.visual_pos.row + { + let position = grapheme.visual_pos - renderer.offset; + self.cache.set(Some(position)); + } + usize::MAX + } +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 290590950..fb8438be0 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1070,10 +1070,10 @@ pub struct Editor { /// This cache is only a performance optimization to /// avoid calculating the cursor position multiple /// times during rendering and should not be set by other functions. - pub cursor_cache: Cell>>, pub handlers: Handlers, pub mouse_down_range: Option, + pub cursor_cache: CursorCache, } pub type Motion = Box; @@ -1188,9 +1188,9 @@ pub fn new( exit_code: 0, config_events: unbounded_channel(), needs_redraw: false, - cursor_cache: Cell::new(None), handlers, mouse_down_range: None, + cursor_cache: CursorCache::default(), } } @@ -1985,15 +1985,7 @@ pub fn doc_diagnostics_with_filter<'a>( pub fn cursor(&self) -> (Option, CursorKind) { let config = self.config(); let (view, doc) = current_ref!(self); - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let pos = self - .cursor_cache - .get() - .unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor)); - if let Some(mut pos) = pos { + if let Some(mut pos) = self.cursor_cache.get(view, doc) { let inner = view.inner_area(doc); pos.col += inner.x as usize; pos.row += inner.y as usize; @@ -2188,3 +2180,28 @@ fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: us doc.apply(&transaction, view.id); } } + +#[derive(Default)] +pub struct CursorCache(Cell>>); + +impl CursorCache { + pub fn get(&self, view: &View, doc: &Document) -> Option { + if let Some(pos) = self.0.get() { + return pos; + } + + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + let res = view.screen_coords_at_pos(doc, text, cursor); + self.set(res); + res + } + + pub fn set(&self, cursor_pos: Option) { + self.0.set(Some(cursor_pos)) + } + + pub fn reset(&self) { + self.0.set(None) + } +} From 839f4d758df7793965a3c1c5c878d3b6a24a9919 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:07 +0100 Subject: [PATCH 130/200] fix typo in doc_formatter.rs --- helix-core/src/doc_formatter.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index 65e1532f0..3cfc15708 100644 --- a/helix-core/src/doc_formatter.rs +++ b/helix-core/src/doc_formatter.rs @@ -182,7 +182,7 @@ pub struct DocumentFormatter<'t> { line_pos: usize, exhausted: bool, - inline_anntoation_graphemes: Option<(Graphemes<'t>, Option)>, + inline_annotation_graphemes: Option<(Graphemes<'t>, Option)>, // softwrap specific /// The indentation of the current line @@ -227,7 +227,7 @@ pub fn new_at_prev_checkpoint( word_buf: Vec::with_capacity(64), word_i: 0, line_pos: block_line_idx, - inline_anntoation_graphemes: None, + inline_annotation_graphemes: None, } } @@ -237,7 +237,7 @@ fn next_inline_annotation_grapheme( ) -> Option<(&'t str, Option)> { loop { if let Some(&mut (ref mut annotation, highlight)) = - self.inline_anntoation_graphemes.as_mut() + self.inline_annotation_graphemes.as_mut() { if let Some(grapheme) = annotation.next() { return Some((grapheme, highlight)); @@ -247,7 +247,7 @@ fn next_inline_annotation_grapheme( if let Some((annotation, highlight)) = self.annotations.next_inline_annotation_at(char_pos) { - self.inline_anntoation_graphemes = Some(( + self.inline_annotation_graphemes = Some(( UnicodeSegmentation::graphemes(&*annotation.text, true), highlight, )) From 39b3d81abf4cc3aede4b739e9aaf6a83899cb22c Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 29 Jan 2024 17:07:16 +0100 Subject: [PATCH 131/200] stable sort diagnostics to avoid flickering --- helix-term/src/application.rs | 7 +++---- helix-view/src/document.rs | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3d10862d1..4c305f6ac 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -771,7 +771,7 @@ macro_rules! language_server { // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order params .diagnostics - .sort_unstable_by_key(|d| (d.severity, d.range.start)); + .sort_by_key(|d| (d.severity, d.range.start)); } for source in &lang_conf.persistent_diagnostic_sources { let new_diagnostics = params @@ -812,9 +812,8 @@ macro_rules! language_server { // Sort diagnostics first by severity and then by line numbers. // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order - diagnostics.sort_unstable_by_key(|(d, server_id)| { - (d.severity, d.range.start, *server_id) - }); + diagnostics + .sort_by_key(|(d, server_id)| (d.severity, d.range.start, *server_id)); if let Some(doc) = doc { let diagnostic_of_language_server_and_not_in_unchanged_sources = diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 412f79fa2..f3ace89e5 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -99,7 +99,6 @@ fn serialize(&self, serializer: S) -> Result serializer.collect_str(self) } } - /// A snapshot of the text of a document that we want to write out to disk #[derive(Debug, Clone)] pub struct DocumentSavedEvent { @@ -1321,7 +1320,7 @@ fn apply_impl( true }); - self.diagnostics.sort_unstable_by_key(|diagnostic| { + self.diagnostics.sort_by_key(|diagnostic| { (diagnostic.range, diagnostic.severity, diagnostic.provider) }); @@ -1911,9 +1910,8 @@ pub fn replace_diagnostics( }); } self.diagnostics.extend(diagnostics); - self.diagnostics.sort_unstable_by_key(|diagnostic| { - (diagnostic.range, diagnostic.severity, diagnostic.provider) - }); + self.diagnostics + .sort_by_key(|diagnostic| (diagnostic.range, diagnostic.severity, diagnostic.provider)); } /// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared From 6d051d7084ab4d75df7bb3e77cc64767c1d9b98e Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 29 Jan 2024 17:11:00 +0100 Subject: [PATCH 132/200] render diagnostic inline --- book/src/editor.md | 40 +++ helix-core/src/diagnostic.rs | 16 +- helix-core/src/graphemes.rs | 5 + helix-core/src/lib.rs | 4 +- helix-core/src/position.rs | 11 + helix-term/src/commands.rs | 1 + helix-term/src/ui/document.rs | 40 ++- helix-term/src/ui/editor.rs | 13 +- .../src/ui/text_decorations/diagnostics.rs | 305 +++++++++++++++++ helix-view/src/annotations.rs | 1 + helix-view/src/annotations/diagnostics.rs | 309 ++++++++++++++++++ helix-view/src/editor.rs | 6 + helix-view/src/lib.rs | 1 + helix-view/src/view.rs | 63 ++-- 14 files changed, 786 insertions(+), 29 deletions(-) create mode 100644 helix-term/src/ui/text_decorations/diagnostics.rs create mode 100644 helix-view/src/annotations.rs create mode 100644 helix-view/src/annotations/diagnostics.rs diff --git a/book/src/editor.md b/book/src/editor.md index ba03e90e5..82d5f8461 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -16,6 +16,7 @@ ## Editor - [`[editor.gutters.spacer]` Section](#editorguttersspacer-section) - [`[editor.soft-wrap]` Section](#editorsoft-wrap-section) - [`[editor.smart-tab]` Section](#editorsmart-tab-section) +- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section) ### `[editor]` Section @@ -50,6 +51,7 @@ ### `[editor]` Section | `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | | `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid` | `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"` +| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable" ### `[editor.statusline]` Section @@ -393,3 +395,41 @@ ### `[editor.smart-tab]` Section tab = "extend_parent_node_end" S-tab = "extend_parent_node_start" ``` + +### `[editor.inline-diagnostics]` Section + +Options for rendering diagnostics inside the text like shown below + +``` +fn main() { + let foo = bar; + └─ no such value in this scope +} +```` + +| Key | Description | Default | +|------------|-------------|---------| +| `cursor-line` | The minimum severity that a diagnostic must have to be shown inline on the line that contains the primary cursor. Set to `disable` to not show any diagnostics inline. This option does not have any effect when in insert-mode and will only take effect 350ms after moving the cursor to a different line. | `"disable"` | +| `other-lines` | The minimum severity that a diagnostic must have to be shown inline on a line that does not contain the cursor-line. Set to `disable` to not show any diagnostics inline. | `"disable"` | +| `prefix-len` | How many horizontal bars `─` are rendered before the diagnostic text. | `1` | +| `max-wrap` | Equivalent of the `editor.soft-wrap.max-wrap` option for diagnostics. | `20` | +| `max-diagnostics` | Maximum number of diagnostics to render inline for a given line | `10` | + +The (first) diagnostic with the highest severity that is not shown inline is rendered at the end of the line (as long as its severity is higher than the `end-of-line-diagnostics` config option): + +``` +fn main() { + let baz = 1; + let foo = bar; a local variable with a similar name exists: baz + └─ no such value in this scope +} +``` + + +The new diagnostic rendering is not yet enabled by default. As soon as end of line or inline diagnostics are enabled the old diagnostics rendering is automatically disabled. The recommended default setting are: + +``` +end-of-line-diagnostics = "hint" +[editor.inline-diagnostics] +cursor-line = "warning" # show warnings and errors on the cursorline inline +``` diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index d41119d38..4e89361d2 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; /// Describes the severity level of a [`Diagnostic`]. -#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum Severity { Hint, Info, @@ -25,6 +26,12 @@ pub struct Range { pub end: usize, } +impl Range { + pub fn contains(self, pos: usize) -> bool { + (self.start..self.end).contains(&pos) + } +} + #[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] pub enum NumberOrString { Number(i32), @@ -71,3 +78,10 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self.0) } } + +impl Diagnostic { + #[inline] + pub fn severity(&self) -> Severity { + self.severity.unwrap_or(Severity::Warning) + } +} diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index a02d6e4dd..91f11e620 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -28,6 +28,11 @@ pub enum Grapheme<'a> { } impl<'a> Grapheme<'a> { + pub fn new_decoration(g: &'static str) -> Grapheme<'a> { + assert_ne!(g, "\t"); + Grapheme::new(g.into(), 0, 0) + } + pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> { match g { g if g == "\t" => Grapheme::Tab { diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 681d3456d..9165560d0 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -53,8 +53,8 @@ pub mod unicode { pub use graphemes::RopeGraphemes; pub use position::{ - char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, - visual_offset_from_block, Position, VisualOffsetError, + char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions, + visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError, }; #[allow(deprecated)] pub use position::{pos_at_visual_coords, visual_coords_at_pos}; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 0860da12f..3719abb0b 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -171,6 +171,17 @@ pub fn visual_offset_from_block( (last_pos, block_start) } +/// Returns the height of the given text when softwrapping +pub fn softwrapped_dimensions(text: RopeSlice, text_fmt: &TextFormat) -> (usize, u16) { + let last_pos = + visual_offset_from_block(text, 0, usize::MAX, text_fmt, &TextAnnotations::default()).0; + if last_pos.row == 0 { + (1, last_pos.col as u16) + } else { + (last_pos.row + 1, text_fmt.viewport_width) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum VisualOffsetError { PosBeforeAnchorRow, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 097c3493c..d205f234c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1711,6 +1711,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor &mut annotations, ) }); + drop(annotations); doc.set_selection(view.id, selection); return; } diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index 2da4d4b31..79145ba04 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -114,7 +114,7 @@ pub fn render_document( } #[allow(clippy::too_many_arguments)] -pub fn render_text<'t>( +pub fn render_text( renderer: &mut TextRenderer, text: RopeSlice<'_>, anchor: usize, @@ -348,6 +348,44 @@ pub fn new( offset, } } + /// Draws a single `grapheme` at the current render position with a specified `style`. + pub fn draw_decoration_grapheme( + &mut self, + grapheme: Grapheme, + mut style: Style, + mut row: u16, + col: u16, + ) -> bool { + if (row as usize) < self.offset.row + || row >= self.viewport.height + || col >= self.viewport.width + { + return false; + } + row -= self.offset.row as u16; + // TODO is it correct to apply the whitspace style to all unicode white spaces? + if grapheme.is_whitespace() { + style = style.patch(self.whitespace_style); + } + + let grapheme = match grapheme { + Grapheme::Tab { width } => { + let grapheme_tab_width = char_to_byte_idx(&self.virtual_tab, width); + &self.virtual_tab[..grapheme_tab_width] + } + Grapheme::Other { ref g } if g == "\u{00A0}" => " ", + Grapheme::Other { ref g } => g, + Grapheme::Newline => " ", + }; + + self.surface.set_string( + self.viewport.x + col, + self.viewport.y + row, + grapheme, + style, + ); + true + } /// Draws a single `grapheme` at the current render position with a specified `style`. pub fn draw_grapheme( diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 49386b835..e934659cd 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -22,6 +22,7 @@ visual_offset_from_block, Change, Position, Range, Selection, Transaction, }; use helix_view::{ + annotations::diagnostics::DiagnosticFilter, document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, @@ -185,6 +186,12 @@ pub fn render_view( primary_cursor, }); } + decorations.add_decoration(InlineDiagnostics::new( + doc, + theme, + primary_cursor, + config.lsp.inline_diagnostics.clone(), + )); render_document( surface, inner, @@ -210,7 +217,11 @@ pub fn render_view( } } - Self::render_diagnostics(doc, view, inner, surface, theme); + if config.inline_diagnostics.disabled() + && config.end_of_line_diagnostics == DiagnosticFilter::Disable + { + Self::render_diagnostics(doc, view, inner, surface, theme); + } let statusline_area = view .area diff --git a/helix-term/src/ui/text_decorations/diagnostics.rs b/helix-term/src/ui/text_decorations/diagnostics.rs new file mode 100644 index 000000000..2d9e83700 --- /dev/null +++ b/helix-term/src/ui/text_decorations/diagnostics.rs @@ -0,0 +1,305 @@ +use std::cmp::Ordering; + +use helix_core::diagnostic::Severity; +use helix_core::doc_formatter::{DocumentFormatter, FormattedGrapheme}; +use helix_core::graphemes::Grapheme; +use helix_core::text_annotations::TextAnnotations; +use helix_core::{Diagnostic, Position}; +use helix_view::annotations::diagnostics::{ + DiagnosticFilter, InlineDiagnosticAccumulator, InlineDiagnosticsConfig, +}; + +use helix_view::theme::Style; +use helix_view::{Document, Theme}; + +use crate::ui::document::{LinePos, TextRenderer}; +use crate::ui::text_decorations::Decoration; + +#[derive(Debug)] +struct Styles { + hint: Style, + info: Style, + warning: Style, + error: Style, +} + +impl Styles { + fn new(theme: &Theme) -> Styles { + Styles { + hint: theme.get("hint"), + info: theme.get("info"), + warning: theme.get("warning"), + error: theme.get("error"), + } + } + + fn severity_style(&self, severity: Severity) -> Style { + match severity { + Severity::Hint => self.hint, + Severity::Info => self.info, + Severity::Warning => self.warning, + Severity::Error => self.error, + } + } +} + +pub struct InlineDiagnostics<'a> { + state: InlineDiagnosticAccumulator<'a>, + eol_diagnostics: DiagnosticFilter, + styles: Styles, +} + +impl<'a> InlineDiagnostics<'a> { + pub fn new( + doc: &'a Document, + theme: &Theme, + cursor: usize, + config: InlineDiagnosticsConfig, + eol_diagnostics: DiagnosticFilter, + ) -> Self { + InlineDiagnostics { + state: InlineDiagnosticAccumulator::new(cursor, doc, config), + styles: Styles::new(theme), + eol_diagnostics, + } + } +} + +const BL_CORNER: &str = "┘"; +const TR_CORNER: &str = "┌"; +const BR_CORNER: &str = "└"; +const STACK: &str = "├"; +const MULTI: &str = "┴"; +const HOR_BAR: &str = "─"; +const VER_BAR: &str = "│"; + +struct Renderer<'a, 'b> { + renderer: &'a mut TextRenderer<'b>, + first_row: u16, + row: u16, + config: &'a InlineDiagnosticsConfig, + styles: &'a Styles, +} + +impl Renderer<'_, '_> { + fn draw_decoration(&mut self, g: &'static str, severity: Severity, col: u16) { + self.draw_decoration_at(g, severity, col, self.row) + } + + fn draw_decoration_at(&mut self, g: &'static str, severity: Severity, col: u16, row: u16) { + self.renderer.draw_decoration_grapheme( + Grapheme::new_decoration(g), + self.styles.severity_style(severity), + row, + col, + ); + } + + fn draw_eol_diagnostic(&mut self, diag: &Diagnostic, row: u16, col: usize) -> u16 { + let style = self.styles.severity_style(diag.severity()); + let width = self.renderer.viewport.width; + if !self.renderer.column_in_bounds(col + 1) { + return 0; + } + let col = (col - self.renderer.offset.col) as u16; + let (new_col, _) = self.renderer.set_string_truncated( + self.renderer.viewport.x + col + 1, + row, + &diag.message, + width.saturating_sub(col + 1) as usize, + |_| style, + true, + false, + ); + new_col - col + } + + fn draw_diagnostic(&mut self, diag: &Diagnostic, col: u16, next_severity: Option) { + let severity = diag.severity(); + let (sym, sym_severity) = if let Some(next_severity) = next_severity { + (STACK, next_severity.max(severity)) + } else { + (BR_CORNER, severity) + }; + self.draw_decoration(sym, sym_severity, col); + for i in 0..self.config.prefix_len { + self.draw_decoration(HOR_BAR, severity, col + i + 1); + } + + let text_col = col + self.config.prefix_len + 1; + let text_fmt = self.config.text_fmt(text_col, self.renderer.viewport.width); + let annotations = TextAnnotations::default(); + let formatter = DocumentFormatter::new_at_prev_checkpoint( + diag.message.as_str().trim().into(), + &text_fmt, + &annotations, + 0, + ); + let mut last_row = 0; + let style = self.styles.severity_style(severity); + for grapheme in formatter { + last_row = grapheme.visual_pos.row; + self.renderer.draw_decoration_grapheme( + grapheme.raw, + style, + self.row + grapheme.visual_pos.row as u16, + text_col + grapheme.visual_pos.col as u16, + ); + } + self.row += 1; + // height is last_row + 1 and extra_rows is height - 1 + let extra_lines = last_row; + if let Some(next_severity) = next_severity { + for _ in 0..extra_lines { + self.draw_decoration(VER_BAR, next_severity, col); + self.row += 1; + } + } else { + self.row += extra_lines as u16; + } + } + + fn draw_multi_diagnostics(&mut self, stack: &mut Vec<(&Diagnostic, u16)>) { + let Some(&(last_diag, last_anchor)) = stack.last() else { + return; + }; + let start = self + .config + .max_diagnostic_start(self.renderer.viewport.width); + + if last_anchor <= start { + return; + } + let mut severity = last_diag.severity(); + let mut last_anchor = last_anchor; + self.draw_decoration(BL_CORNER, severity, last_anchor); + let mut stacked_diagnostics = 1; + for &(diag, anchor) in stack.iter().rev().skip(1) { + let sym = match anchor.cmp(&start) { + Ordering::Less => break, + Ordering::Equal => STACK, + Ordering::Greater => MULTI, + }; + stacked_diagnostics += 1; + severity = severity.max(diag.severity()); + let old_severity = severity; + if anchor == last_anchor && severity == old_severity { + continue; + } + for col in (anchor + 1)..last_anchor { + self.draw_decoration(HOR_BAR, old_severity, col) + } + self.draw_decoration(sym, severity, anchor); + last_anchor = anchor; + } + + // if no diagnostic anchor was found exactly at the start of the + // diagnostic text draw an upwards corner and ensure the last piece + // of the line is not missing + if last_anchor != start { + for col in (start + 1)..last_anchor { + self.draw_decoration(HOR_BAR, severity, col) + } + self.draw_decoration(TR_CORNER, severity, start) + } + self.row += 1; + let stacked_diagnostics = &stack[stack.len() - stacked_diagnostics..]; + + for (i, (diag, _)) in stacked_diagnostics.iter().rev().enumerate() { + let next_severity = stacked_diagnostics[..stacked_diagnostics.len() - i - 1] + .iter() + .map(|(diag, _)| diag.severity()) + .max(); + self.draw_diagnostic(diag, start, next_severity); + } + + stack.truncate(stack.len() - stacked_diagnostics.len()); + } + + fn draw_diagnostics(&mut self, stack: &mut Vec<(&Diagnostic, u16)>) { + let mut stack = stack.drain(..).rev().peekable(); + let mut last_anchor = self.renderer.viewport.width; + while let Some((diag, anchor)) = stack.next() { + if anchor != last_anchor { + for row in self.first_row..self.row { + self.draw_decoration_at(VER_BAR, diag.severity(), anchor, row); + } + } + let next_severity = stack.peek().and_then(|&(diag, next_anchor)| { + (next_anchor == anchor).then_some(diag.severity()) + }); + self.draw_diagnostic(diag, anchor, next_severity); + last_anchor = anchor; + } + } +} + +impl Decoration for InlineDiagnostics<'_> { + fn render_virt_lines( + &mut self, + renderer: &mut TextRenderer, + pos: LinePos, + virt_off: Position, + ) -> Position { + let mut col_off = 0; + let filter = self.state.filter(); + let eol_diagnostic = match self.eol_diagnostics { + DiagnosticFilter::Enable(eol_filter) => { + let eol_diganogistcs = self + .state + .stack + .iter() + .filter(|(diag, _)| eol_filter <= diag.severity()); + match filter { + DiagnosticFilter::Enable(filter) => eol_diganogistcs + .filter(|(diag, _)| filter > diag.severity()) + .max_by_key(|(diagnostic, _)| diagnostic.severity), + DiagnosticFilter::Disable => { + eol_diganogistcs.max_by_key(|(diagnostic, _)| diagnostic.severity) + } + } + } + DiagnosticFilter::Disable => None, + }; + if let Some((eol_diagnostic, _)) = eol_diagnostic { + let mut renderer = Renderer { + renderer, + first_row: pos.visual_line, + row: pos.visual_line, + config: &self.state.config, + styles: &self.styles, + }; + col_off = renderer.draw_eol_diagnostic(eol_diagnostic, pos.visual_line, virt_off.col); + } + + self.state.compute_line_diagnostics(); + let mut renderer = Renderer { + renderer, + first_row: pos.visual_line + virt_off.row as u16, + row: pos.visual_line + virt_off.row as u16, + config: &self.state.config, + styles: &self.styles, + }; + renderer.draw_multi_diagnostics(&mut self.state.stack); + renderer.draw_diagnostics(&mut self.state.stack); + let horizontal_off = renderer.row - renderer.first_row; + Position::new(horizontal_off as usize, col_off as usize) + } + + fn reset_pos(&mut self, pos: usize) -> usize { + self.state.reset_pos(pos) + } + + fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize { + self.state.skip_concealed(conceal_end_char_idx) + } + + fn decorate_grapheme( + &mut self, + renderer: &mut TextRenderer, + grapheme: &FormattedGrapheme, + ) -> usize { + self.state + .proccess_anchor(grapheme, renderer.viewport.width, renderer.offset.col) + } +} diff --git a/helix-view/src/annotations.rs b/helix-view/src/annotations.rs new file mode 100644 index 000000000..4c630487f --- /dev/null +++ b/helix-view/src/annotations.rs @@ -0,0 +1 @@ +pub mod diagnostics; diff --git a/helix-view/src/annotations/diagnostics.rs b/helix-view/src/annotations/diagnostics.rs new file mode 100644 index 000000000..afe0685a5 --- /dev/null +++ b/helix-view/src/annotations/diagnostics.rs @@ -0,0 +1,309 @@ +use helix_core::diagnostic::Severity; +use helix_core::doc_formatter::{FormattedGrapheme, TextFormat}; +use helix_core::text_annotations::LineAnnotation; +use helix_core::{softwrapped_dimensions, Diagnostic, Position}; +use serde::{Deserialize, Serialize}; + +use crate::Document; + +/// Describes the severity level of a [`Diagnostic`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +pub enum DiagnosticFilter { + Disable, + Enable(Severity), +} + +impl<'de> Deserialize<'de> for DiagnosticFilter { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + match &*String::deserialize(deserializer)? { + "disable" => Ok(DiagnosticFilter::Disable), + "hint" => Ok(DiagnosticFilter::Enable(Severity::Hint)), + "info" => Ok(DiagnosticFilter::Enable(Severity::Info)), + "warning" => Ok(DiagnosticFilter::Enable(Severity::Warning)), + "error" => Ok(DiagnosticFilter::Enable(Severity::Error)), + variant => Err(serde::de::Error::unknown_variant( + variant, + &["disable", "hint", "info", "warning", "error"], + )), + } + } +} + +impl Serialize for DiagnosticFilter { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let filter = match self { + DiagnosticFilter::Disable => "disable", + DiagnosticFilter::Enable(Severity::Hint) => "hint", + DiagnosticFilter::Enable(Severity::Info) => "info", + DiagnosticFilter::Enable(Severity::Warning) => "warning", + DiagnosticFilter::Enable(Severity::Error) => "error", + }; + filter.serialize(serializer) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct InlineDiagnosticsConfig { + pub cursor_line: DiagnosticFilter, + pub other_lines: DiagnosticFilter, + pub min_diagnostic_width: u16, + pub prefix_len: u16, + pub max_wrap: u16, + pub max_diagnostics: usize, +} + +impl InlineDiagnosticsConfig { + // last column where to start diagnostics + // every diagnostics that start afterwards will be displayed with a "backwards + // line" to ensure they are still rendered with `min_diagnostic_widht`. If `width` + // it too small to display diagnostics with atleast `min_diagnostic_width` space + // (or inline diagnostics are displed) `None` is returned. In that case inline + // diagnostics should not be shown + pub fn enable(&self, width: u16) -> bool { + let disabled = matches!( + self, + Self { + cursor_line: DiagnosticFilter::Disable, + other_lines: DiagnosticFilter::Disable, + .. + } + ); + !disabled && width >= self.min_diagnostic_width + self.prefix_len + } + + pub fn max_diagnostic_start(&self, width: u16) -> u16 { + width - self.min_diagnostic_width - self.prefix_len + } + + pub fn text_fmt(&self, anchor_col: u16, width: u16) -> TextFormat { + let width = if anchor_col > self.max_diagnostic_start(width) { + self.min_diagnostic_width + } else { + width - anchor_col - self.prefix_len + }; + + TextFormat { + soft_wrap: true, + tab_width: 4, + max_wrap: self.max_wrap.min(width / 4), + max_indent_retain: 0, + wrap_indicator: "".into(), + wrap_indicator_highlight: None, + viewport_width: width, + soft_wrap_at_text_width: true, + } + } +} + +impl Default for InlineDiagnosticsConfig { + fn default() -> Self { + InlineDiagnosticsConfig { + cursor_line: DiagnosticFilter::Disable, + other_lines: DiagnosticFilter::Disable, + min_diagnostic_width: 40, + prefix_len: 1, + max_wrap: 20, + max_diagnostics: 10, + } + } +} + +pub struct InlineDiagnosticAccumulator<'a> { + idx: usize, + doc: &'a Document, + pub stack: Vec<(&'a Diagnostic, u16)>, + pub config: InlineDiagnosticsConfig, + cursor: usize, + cursor_line: bool, +} + +impl<'a> InlineDiagnosticAccumulator<'a> { + pub fn new(cursor: usize, doc: &'a Document, config: InlineDiagnosticsConfig) -> Self { + InlineDiagnosticAccumulator { + idx: 0, + doc, + stack: Vec::new(), + config, + cursor, + cursor_line: false, + } + } + + pub fn reset_pos(&mut self, char_idx: usize) -> usize { + self.idx = 0; + self.clear(); + self.skip_concealed(char_idx) + } + + pub fn skip_concealed(&mut self, conceal_end_char_idx: usize) -> usize { + let diagnostics = &self.doc.diagnostics[self.idx..]; + let idx = diagnostics.partition_point(|diag| diag.range.start < conceal_end_char_idx); + self.idx += idx; + self.next_anchor(conceal_end_char_idx) + } + + pub fn next_anchor(&self, current_char_idx: usize) -> usize { + let next_diag_start = self + .doc + .diagnostics + .get(self.idx) + .map_or(usize::MAX, |diag| diag.range.start); + if (current_char_idx..next_diag_start).contains(&self.cursor) { + self.cursor + } else { + next_diag_start + } + } + + pub fn clear(&mut self) { + self.cursor_line = false; + self.stack.clear(); + } + + fn process_anchor_impl( + &mut self, + grapheme: &FormattedGrapheme, + width: u16, + horizontal_off: usize, + ) -> bool { + // TODO: doing the cursor tracking here works well but is somewhat + // duplicate effort/tedious maybe centralize this somehwere? + // In the DocFormatter? + if grapheme.char_idx == self.cursor { + self.cursor_line = true; + if self + .doc + .diagnostics + .get(self.idx) + .map_or(true, |diag| diag.range.start != grapheme.char_idx) + { + return false; + } + } + + let Some(anchor_col) = grapheme.visual_pos.col.checked_sub(horizontal_off) else { + return true; + }; + if anchor_col >= width as usize { + return true; + } + + for diag in &self.doc.diagnostics[self.idx..] { + if diag.range.start != grapheme.char_idx { + break; + } + self.stack.push((diag, anchor_col as u16)); + self.idx += 1; + } + false + } + + pub fn proccess_anchor( + &mut self, + grapheme: &FormattedGrapheme, + width: u16, + horizontal_off: usize, + ) -> usize { + if self.process_anchor_impl(grapheme, width, horizontal_off) { + self.idx += self.doc.diagnostics[self.idx..] + .iter() + .take_while(|diag| diag.range.start == grapheme.char_idx) + .count(); + } + self.next_anchor(grapheme.char_idx + 1) + } + + pub fn filter(&self) -> DiagnosticFilter { + if self.cursor_line { + self.config.cursor_line + } else { + self.config.other_lines + } + } + + pub fn compute_line_diagnostics(&mut self) { + let filter = if self.cursor_line { + self.cursor_line = false; + self.config.cursor_line + } else { + self.config.other_lines + }; + let DiagnosticFilter::Enable(filter) = filter else { + self.stack.clear(); + return; + }; + self.stack.retain(|(diag, _)| diag.severity() >= filter); + self.stack.truncate(self.config.max_diagnostics) + } + + pub fn has_multi(&self, width: u16) -> bool { + self.stack.last().map_or(false, |&(_, anchor)| { + anchor > self.config.max_diagnostic_start(width) + }) + } +} + +pub(crate) struct InlineDiagnostics<'a> { + state: InlineDiagnosticAccumulator<'a>, + width: u16, + horizontal_off: usize, +} + +impl<'a> InlineDiagnostics<'a> { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new( + doc: &'a Document, + cursor: usize, + width: u16, + horizontal_off: usize, + config: InlineDiagnosticsConfig, + ) -> Box { + Box::new(InlineDiagnostics { + state: InlineDiagnosticAccumulator::new(cursor, doc, config), + width, + horizontal_off, + }) + } +} + +impl LineAnnotation for InlineDiagnostics<'_> { + fn reset_pos(&mut self, char_idx: usize) -> usize { + self.state.reset_pos(char_idx) + } + + fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize { + self.state.skip_concealed(conceal_end_char_idx) + } + + fn process_anchor(&mut self, grapheme: &FormattedGrapheme) -> usize { + self.state + .proccess_anchor(grapheme, self.width, self.horizontal_off) + } + + fn insert_virtual_lines( + &mut self, + _line_end_char_idx: usize, + _line_end_visual_pos: Position, + _doc_line: usize, + ) -> Position { + self.state.compute_line_diagnostics(); + let multi = self.state.has_multi(self.width); + let diagostic_height: usize = self + .state + .stack + .drain(..) + .map(|(diag, anchor)| { + let text_fmt = self.state.config.text_fmt(anchor, self.width); + softwrapped_dimensions(diag.message.as_str().trim().into(), &text_fmt).0 + }) + .sum(); + Position::new(multi as usize + diagostic_height, 0) + } +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index fb8438be0..cead30d7c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,5 +1,6 @@ use crate::{ align_view, + annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig}, document::{ DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, }, @@ -343,6 +344,9 @@ pub struct Config { deserialize_with = "deserialize_alphabet" )] pub jump_label_alphabet: Vec, + /// Display diagnostic below the line they occur. + pub inline_diagnostics: InlineDiagnosticsConfig, + pub end_of_line_diagnostics: DiagnosticFilter, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] @@ -975,6 +979,8 @@ fn default() -> Self { popup_border: PopupBorderConfig::None, indent_heuristic: IndentationHeuristic::default(), jump_label_alphabet: ('a'..='z').collect(), + inline_diagnostics: InlineDiagnosticsConfig::default(), + end_of_line_diagnostics: DiagnosticFilter::Disable, } } } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 14b6e1ce8..5628c830c 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] pub mod macros; +pub mod annotations; pub mod base64; pub mod clipboard; pub mod document; diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 4c0674949..4aea98a70 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -1,5 +1,6 @@ use crate::{ align_view, + annotations::diagnostics::InlineDiagnostics, document::DocumentInlayHints, editor::{GutterConfig, GutterType}, graphics::Rect, @@ -438,37 +439,51 @@ pub fn text_annotations<'a>( text_annotations.add_overlay(labels, style); } - let DocumentInlayHints { + if let Some(DocumentInlayHints { id: _, type_inlay_hints, parameter_inlay_hints, other_inlay_hints, padding_before_inlay_hints, padding_after_inlay_hints, - } = match doc.inlay_hints.get(&self.id) { - Some(doc_inlay_hints) => doc_inlay_hints, - None => return text_annotations, + }) = doc.inlay_hints.get(&self.id) + { + let type_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) + .map(Highlight); + let parameter_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter")) + .map(Highlight); + let other_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) + .map(Highlight); + + // Overlapping annotations are ignored apart from the first so the order here is not random: + // types -> parameters -> others should hopefully be the "correct" order for most use cases, + // with the padding coming before and after as expected. + text_annotations + .add_inline_annotations(padding_before_inlay_hints, None) + .add_inline_annotations(type_inlay_hints, type_style) + .add_inline_annotations(parameter_inlay_hints, parameter_style) + .add_inline_annotations(other_inlay_hints, other_style) + .add_inline_annotations(padding_after_inlay_hints, None); }; - - let type_style = theme - .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) - .map(Highlight); - let parameter_style = theme - .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter")) - .map(Highlight); - let other_style = theme - .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) - .map(Highlight); - - // Overlapping annotations are ignored apart from the first so the order here is not random: - // types -> parameters -> others should hopefully be the "correct" order for most use cases, - // with the padding coming before and after as expected. - text_annotations - .add_inline_annotations(padding_before_inlay_hints, None) - .add_inline_annotations(type_inlay_hints, type_style) - .add_inline_annotations(parameter_inlay_hints, parameter_style) - .add_inline_annotations(other_inlay_hints, other_style) - .add_inline_annotations(padding_after_inlay_hints, None); + let width = self.inner_width(doc); + let config = doc.config.load(); + if config.lsp.inline_diagnostics.enable(width) { + let config = config.lsp.inline_diagnostics.clone(); + let cursor = doc + .selection(self.id) + .primary() + .cursor(doc.text().slice(..)); + text_annotations.add_line_annotation(InlineDiagnostics::new( + doc, + cursor, + width, + self.offset.horizontal_offset, + config, + )); + } text_annotations } From 7e133167ca18e126d52939a717f6f34511068b3c Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:08 +0100 Subject: [PATCH 133/200] remove redudant/incorrect view bound check --- helix-view/src/view.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 4aea98a70..a5654759e 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -393,11 +393,6 @@ pub fn screen_coords_at_pos( text: RopeSlice, pos: usize, ) -> Option { - if pos < self.offset.anchor { - // Line is not visible on screen - return None; - } - let viewport = self.inner_area(doc); let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); From 3abc07a79e6ddd6a146977648031a9322492f445 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:08 +0100 Subject: [PATCH 134/200] use correct position for cursor in doc popup --- helix-term/src/ui/completion.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 372e3e5ef..14397bb5c 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -448,14 +448,12 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // --- // option.documentation - let (view, doc) = current!(cx.editor); - let language = doc.language_name().unwrap_or(""); - let text = doc.text().slice(..); - let cursor_pos = doc.selection(view.id).primary().cursor(text); - let coords = view - .screen_coords_at_pos(doc, text, cursor_pos) - .expect("cursor must be in view"); + let Some(coords) = cx.editor.cursor().0 else { + return; + }; let cursor_pos = coords.row as u16; + let doc = doc!(cx.editor); + let language = doc.language_name().unwrap_or(""); let markdowned = |lang: &str, detail: Option<&str>, doc: Option<&str>| { let md = match (detail, doc) { From a17b008b42698ecf2b9f64fb5676768b1a7f8652 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 19 Nov 2023 22:34:09 +0100 Subject: [PATCH 135/200] ignore empty virtual text layers --- helix-core/src/text_annotations.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index 785e38fd7..ff28a8dd2 100644 --- a/helix-core/src/text_annotations.rs +++ b/helix-core/src/text_annotations.rs @@ -334,7 +334,9 @@ pub fn add_inline_annotations( layer: &'a [InlineAnnotation], highlight: Option, ) -> &mut Self { - self.inline_annotations.push((layer, highlight).into()); + if !layer.is_empty() { + self.inline_annotations.push((layer, highlight).into()); + } self } @@ -349,7 +351,9 @@ pub fn add_inline_annotations( /// If multiple layers contain overlay at the same position /// the overlay from the layer added last will be show. pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option) -> &mut Self { - self.overlays.push((layer, highlight).into()); + if !layer.is_empty() { + self.overlays.push((layer, highlight).into()); + } self } From d8a115641d7e6738473aba158be6efc2ad3493a7 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 2 Apr 2024 14:48:14 +0200 Subject: [PATCH 136/200] fix scrolling/movement for multiline virtual text --- helix-core/src/movement.rs | 10 ++++----- helix-core/src/position.rs | 45 +++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 54eb02fd0..f5c2b2ed5 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -79,19 +79,19 @@ pub fn move_vertically_visual( Direction::Backward => -(count as isize), }; - // TODO how to handle inline annotations that span an entire visual line (very unlikely). - // Compute visual offset relative to block start to avoid trasversing the block twice row_off += visual_pos.row as isize; - let new_pos = char_idx_at_visual_offset( + let (mut new_pos, virtual_rows) = char_idx_at_visual_offset( slice, block_off, row_off, new_col as usize, text_fmt, annotations, - ) - .0; + ); + if dir == Direction::Forward { + new_pos += (virtual_rows != 0) as usize; + } // Special-case to avoid moving to the end of the last non-empty line. if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 { diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 3719abb0b..1b3789110 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -415,7 +415,7 @@ pub fn char_idx_at_visual_block_offset( let mut formatter = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); let mut last_char_idx = formatter.next_char_pos(); - let mut last_char_idx_on_line = None; + let mut found_non_virtual_on_row = false; let mut last_row = 0; for grapheme in &mut formatter { match grapheme.visual_pos.row.cmp(&row) { @@ -423,19 +423,23 @@ pub fn char_idx_at_visual_block_offset( if grapheme.visual_pos.col + grapheme.width() > column { if !grapheme.is_virtual() { return (grapheme.char_idx, 0); - } else if let Some(char_idx) = last_char_idx_on_line { - return (char_idx, 0); + } else if found_non_virtual_on_row { + return (last_char_idx, 0); } } else if !grapheme.is_virtual() { - last_char_idx_on_line = Some(grapheme.char_idx) + found_non_virtual_on_row = true; + last_char_idx = grapheme.char_idx; } } + Ordering::Greater if found_non_virtual_on_row => return (last_char_idx, 0), Ordering::Greater => return (last_char_idx, row - last_row), - _ => (), + Ordering::Less => { + if !grapheme.is_virtual() { + last_row = grapheme.visual_pos.row; + last_char_idx = grapheme.char_idx; + } + } } - - last_char_idx = grapheme.char_idx; - last_row = grapheme.visual_pos.row; } (formatter.next_char_pos(), 0) @@ -444,6 +448,7 @@ pub fn char_idx_at_visual_block_offset( #[cfg(test)] mod test { use super::*; + use crate::text_annotations::InlineAnnotation; use crate::Rope; #[test] @@ -804,6 +809,30 @@ fn test_pos_at_visual_coords() { assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0); } + #[test] + fn test_char_idx_at_visual_row_offset_inline_annotation() { + let text = Rope::from("foo\nbar"); + let slice = text.slice(..); + let mut text_fmt = TextFormat::default(); + let annotations = [InlineAnnotation { + text: "x".repeat(100).into(), + char_idx: 3, + }]; + text_fmt.soft_wrap = true; + + assert_eq!( + char_idx_at_visual_offset( + slice, + 0, + 1, + 0, + &text_fmt, + TextAnnotations::default().add_inline_annotations(&annotations, None) + ), + (2, 1) + ); + } + #[test] fn test_char_idx_at_visual_row_offset() { let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ\nfoo"); From 7283ef881f2855ef94b67eca88799fa07157df9c Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 5 Apr 2024 03:17:06 +0200 Subject: [PATCH 137/200] only show inline diagnostics after a delay --- helix-term/src/application.rs | 7 ++ helix-term/src/commands.rs | 8 ++ helix-term/src/commands/lsp.rs | 5 +- helix-term/src/events.rs | 3 +- helix-term/src/handlers.rs | 2 + helix-term/src/handlers/diagnostics.rs | 24 ++++ helix-term/src/ui/editor.rs | 9 +- helix-view/src/annotations/diagnostics.rs | 24 ++-- helix-view/src/events.rs | 3 +- helix-view/src/handlers.rs | 1 + helix-view/src/handlers/diagnostics.rs | 131 ++++++++++++++++++++++ helix-view/src/view.rs | 19 +++- 12 files changed, 219 insertions(+), 17 deletions(-) create mode 100644 helix-term/src/handlers/diagnostics.rs create mode 100644 helix-view/src/handlers/diagnostics.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 4c305f6ac..b7123e972 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -11,6 +11,7 @@ align_view, document::{DocumentOpenError, DocumentSavedEventResult}, editor::{ConfigEvent, EditorEvent}, + events::DiagnosticsDidChange, graphics::Rect, theme, tree::Layout, @@ -834,6 +835,12 @@ macro_rules! language_server { &unchanged_diag_sources, Some(server_id), ); + + let doc = doc.id(); + helix_event::dispatch(DiagnosticsDidChange { + editor: &mut self.editor, + doc, + }); } } Notification::ShowMessage(params) => { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d205f234c..234902886 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3550,6 +3550,8 @@ fn goto_first_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); + view.diagnostics_handler + .immediately_show_diagnostic(doc, view.id); } fn goto_last_diag(cx: &mut Context) { @@ -3559,6 +3561,8 @@ fn goto_last_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); + view.diagnostics_handler + .immediately_show_diagnostic(doc, view.id); } fn goto_next_diag(cx: &mut Context) { @@ -3581,6 +3585,8 @@ fn goto_next_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); + view.diagnostics_handler + .immediately_show_diagnostic(doc, view.id); }; cx.editor.apply_motion(motion); @@ -3609,6 +3615,8 @@ fn goto_prev_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); + view.diagnostics_handler + .immediately_show_diagnostic(doc, view.id); }; cx.editor.apply_motion(motion) } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 103d1df24..3b9efb431 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -271,7 +271,10 @@ fn diag_picker( let Some(path) = uri.as_path() else { return; }; - jump_to_position(cx.editor, path, diag.range, *offset_encoding, action) + jump_to_position(cx.editor, path, diag.range, *offset_encoding, action); + let (view, doc) = current!(cx.editor); + view.diagnostics_handler + .immediately_show_diagnostic(doc, view.id); }, ) .with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| { diff --git a/helix-term/src/events.rs b/helix-term/src/events.rs index 49b44f775..415213c22 100644 --- a/helix-term/src/events.rs +++ b/helix-term/src/events.rs @@ -1,6 +1,6 @@ use helix_event::{events, register_event}; use helix_view::document::Mode; -use helix_view::events::{DocumentDidChange, SelectionDidChange}; +use helix_view::events::{DiagnosticsDidChange, DocumentDidChange, SelectionDidChange}; use crate::commands; use crate::keymap::MappableCommand; @@ -17,4 +17,5 @@ pub fn register() { register_event::(); register_event::(); register_event::(); + register_event::(); } diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index fc9273137..b27e34e29 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -14,6 +14,7 @@ mod auto_save; pub mod completion; +mod diagnostics; mod signature_help; pub fn setup(config: Arc>) -> Handlers { @@ -32,5 +33,6 @@ pub fn setup(config: Arc>) -> Handlers { completion::register_hooks(&handlers); signature_help::register_hooks(&handlers); auto_save::register_hooks(&handlers); + diagnostics::register_hooks(&handlers); handlers } diff --git a/helix-term/src/handlers/diagnostics.rs b/helix-term/src/handlers/diagnostics.rs new file mode 100644 index 000000000..3e44d416d --- /dev/null +++ b/helix-term/src/handlers/diagnostics.rs @@ -0,0 +1,24 @@ +use helix_event::{register_hook, send_blocking}; +use helix_view::document::Mode; +use helix_view::events::DiagnosticsDidChange; +use helix_view::handlers::diagnostics::DiagnosticEvent; +use helix_view::handlers::Handlers; + +use crate::events::OnModeSwitch; + +pub(super) fn register_hooks(_handlers: &Handlers) { + register_hook!(move |event: &mut DiagnosticsDidChange<'_>| { + if event.editor.mode != Mode::Insert { + for (view, _) in event.editor.tree.views_mut() { + send_blocking(&view.diagnostics_handler.events, DiagnosticEvent::Refresh) + } + } + Ok(()) + }); + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + for (view, _) in event.cx.editor.tree.views_mut() { + view.diagnostics_handler.active = event.new_mode != Mode::Insert; + } + Ok(()) + }); +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index e934659cd..c151a7dd5 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -186,11 +186,18 @@ pub fn render_view( primary_cursor, }); } + let width = view.inner_width(doc); + let config = doc.config.load(); + let enable_cursor_line = view + .diagnostics_handler + .show_cursorline_diagnostics(doc, view.id); + let inline_diagnostic_config = config.inline_diagnostics.prepare(width, enable_cursor_line); decorations.add_decoration(InlineDiagnostics::new( doc, theme, primary_cursor, - config.lsp.inline_diagnostics.clone(), + inline_diagnostic_config, + config.end_of_line_diagnostics, )); render_document( surface, diff --git a/helix-view/src/annotations/diagnostics.rs b/helix-view/src/annotations/diagnostics.rs index afe0685a5..09085d3fe 100644 --- a/helix-view/src/annotations/diagnostics.rs +++ b/helix-view/src/annotations/diagnostics.rs @@ -60,22 +60,26 @@ pub struct InlineDiagnosticsConfig { } impl InlineDiagnosticsConfig { - // last column where to start diagnostics - // every diagnostics that start afterwards will be displayed with a "backwards - // line" to ensure they are still rendered with `min_diagnostic_widht`. If `width` - // it too small to display diagnostics with atleast `min_diagnostic_width` space - // (or inline diagnostics are displed) `None` is returned. In that case inline - // diagnostics should not be shown - pub fn enable(&self, width: u16) -> bool { - let disabled = matches!( + pub fn disabled(&self) -> bool { + matches!( self, Self { cursor_line: DiagnosticFilter::Disable, other_lines: DiagnosticFilter::Disable, .. } - ); - !disabled && width >= self.min_diagnostic_width + self.prefix_len + ) + } + + pub fn prepare(&self, width: u16, enable_cursor_line: bool) -> Self { + let mut config = self.clone(); + if width < self.min_diagnostic_width + self.prefix_len { + config.cursor_line = DiagnosticFilter::Disable; + config.other_lines = DiagnosticFilter::Disable; + } else if !enable_cursor_line { + config.cursor_line = self.cursor_line.min(self.other_lines); + } + config } pub fn max_diagnostic_start(&self, width: u16) -> u16 { diff --git a/helix-view/src/events.rs b/helix-view/src/events.rs index 8b789cc0d..881412495 100644 --- a/helix-view/src/events.rs +++ b/helix-view/src/events.rs @@ -1,9 +1,10 @@ use helix_core::Rope; use helix_event::events; -use crate::{Document, ViewId}; +use crate::{Document, DocumentId, Editor, ViewId}; events! { DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope } SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId } + DiagnosticsDidChange<'a> { editor: &'a mut Editor, doc: DocumentId } } diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs index e2848f264..93336beb5 100644 --- a/helix-view/src/handlers.rs +++ b/helix-view/src/handlers.rs @@ -5,6 +5,7 @@ use crate::{DocumentId, Editor, ViewId}; pub mod dap; +pub mod diagnostics; pub mod lsp; #[derive(Debug)] diff --git a/helix-view/src/handlers/diagnostics.rs b/helix-view/src/handlers/diagnostics.rs new file mode 100644 index 000000000..2b8ff6325 --- /dev/null +++ b/helix-view/src/handlers/diagnostics.rs @@ -0,0 +1,131 @@ +use std::cell::Cell; +use std::num::NonZeroUsize; +use std::sync::atomic::{self, AtomicUsize}; +use std::sync::Arc; +use std::time::Duration; + +use helix_event::{request_redraw, send_blocking, AsyncHook}; +use tokio::sync::mpsc::Sender; +use tokio::time::Instant; + +use crate::{Document, DocumentId, ViewId}; + +#[derive(Debug)] +pub enum DiagnosticEvent { + CursorLineChanged { generation: usize }, + Refresh, +} + +struct DiagnosticTimeout { + active_generation: Arc, + generation: usize, +} + +const TIMEOUT: Duration = Duration::from_millis(350); + +impl AsyncHook for DiagnosticTimeout { + type Event = DiagnosticEvent; + + fn handle_event( + &mut self, + event: DiagnosticEvent, + timeout: Option, + ) -> Option { + match event { + DiagnosticEvent::CursorLineChanged { generation } => { + if generation > self.generation { + self.generation = generation; + Some(Instant::now() + TIMEOUT) + } else { + timeout + } + } + DiagnosticEvent::Refresh if timeout.is_some() => Some(Instant::now() + TIMEOUT), + DiagnosticEvent::Refresh => None, + } + } + + fn finish_debounce(&mut self) { + if self.active_generation.load(atomic::Ordering::Relaxed) < self.generation { + self.active_generation + .store(self.generation, atomic::Ordering::Relaxed); + request_redraw(); + } + } +} + +pub struct DiagnosticsHandler { + active_generation: Arc, + generation: Cell, + last_doc: Cell, + last_cursor_line: Cell, + pub active: bool, + pub events: Sender, +} + +// make sure we never share handlers across multiple views this is a stop +// gap solution. We just shouldn't be cloneing a view to begin with (we do +// for :hsplit/vsplit) and really this should not be view specific to begin with +// but to fix that larger architecutre changes are needed +impl Clone for DiagnosticsHandler { + fn clone(&self) -> Self { + Self::new() + } +} + +impl DiagnosticsHandler { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let active_generation = Arc::new(AtomicUsize::new(0)); + let events = DiagnosticTimeout { + active_generation: active_generation.clone(), + generation: 0, + } + .spawn(); + Self { + active_generation, + generation: Cell::new(0), + events, + last_doc: Cell::new(DocumentId(NonZeroUsize::new(usize::MAX).unwrap())), + last_cursor_line: Cell::new(usize::MAX), + active: true, + } + } +} + +impl DiagnosticsHandler { + pub fn immediately_show_diagnostic(&self, doc: &Document, view: ViewId) { + self.last_doc.set(doc.id()); + let cursor_line = doc + .selection(view) + .primary() + .cursor_line(doc.text().slice(..)); + self.last_cursor_line.set(cursor_line); + self.active_generation + .store(self.generation.get(), atomic::Ordering::Relaxed); + } + pub fn show_cursorline_diagnostics(&self, doc: &Document, view: ViewId) -> bool { + if !self.active { + return false; + } + let cursor_line = doc + .selection(view) + .primary() + .cursor_line(doc.text().slice(..)); + if self.last_cursor_line.get() == cursor_line && self.last_doc.get() == doc.id() { + let active_generation = self.active_generation.load(atomic::Ordering::Relaxed); + self.generation.get() == active_generation + } else { + self.last_doc.set(doc.id()); + self.last_cursor_line.set(cursor_line); + self.generation.set(self.generation.get() + 1); + send_blocking( + &self.events, + DiagnosticEvent::CursorLineChanged { + generation: self.generation.get(), + }, + ); + false + } + } +} diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index a5654759e..af4fdfe4e 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -4,6 +4,7 @@ document::DocumentInlayHints, editor::{GutterConfig, GutterType}, graphics::Rect, + handlers::diagnostics::DiagnosticsHandler, Align, Document, DocumentId, Theme, ViewId, }; @@ -147,6 +148,14 @@ pub struct View { /// mapping keeps track of the last applied history revision so that only new changes /// are applied. doc_revisions: HashMap, + // HACKS: there should really only be a global diagnostics handler (the + // non-focused views should just not have different handling for the cursor + // line). For that we would need accces to editor everywhere (we want to use + // the positioning code) so this can only happen by refactoring View and + // Document into entity component like structure. That is a huge refactor + // left to future work. For now we treat all views as focused and give them + // each their own handler. + pub diagnostics_handler: DiagnosticsHandler, } impl fmt::Debug for View { @@ -176,6 +185,7 @@ pub fn new(doc: DocumentId, gutters: GutterConfig) -> Self { object_selections: Vec::new(), gutters, doc_revisions: HashMap::new(), + diagnostics_handler: DiagnosticsHandler::new(), } } @@ -463,10 +473,13 @@ pub fn text_annotations<'a>( .add_inline_annotations(other_inlay_hints, other_style) .add_inline_annotations(padding_after_inlay_hints, None); }; - let width = self.inner_width(doc); let config = doc.config.load(); - if config.lsp.inline_diagnostics.enable(width) { - let config = config.lsp.inline_diagnostics.clone(); + let width = self.inner_width(doc); + let enable_cursor_line = self + .diagnostics_handler + .show_cursorline_diagnostics(doc, self.id); + let config = config.inline_diagnostics.prepare(width, enable_cursor_line); + if !config.disabled() { let cursor = doc .selection(self.id) .primary() From 386fa371d708f8e91a83762dfc7a58971681e091 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 5 Apr 2024 03:25:41 +0200 Subject: [PATCH 138/200] gracefully handle lack of tokio runtime --- helix-event/src/debounce.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helix-event/src/debounce.rs b/helix-event/src/debounce.rs index 30b6f671b..5971f72b3 100644 --- a/helix-event/src/debounce.rs +++ b/helix-event/src/debounce.rs @@ -28,7 +28,10 @@ fn spawn(self) -> mpsc::Sender { // so it should only be reached in case of total CPU overload. // However, a bounded channel is much more efficient so it's nice to use here let (tx, rx) = mpsc::channel(128); - tokio::spawn(run(self, rx)); + // only spawn worker if we are inside runtime to avoid having to spawn a runtime for unrelated unit tests + if tokio::runtime::Handle::try_current().is_ok() { + tokio::spawn(run(self, rx)); + } tx } } From b0cf86d31bd85e1f0e38ec86f4dfeeade2cd0918 Mon Sep 17 00:00:00 2001 From: jyn Date: Mon, 15 Jul 2024 15:39:05 -0400 Subject: [PATCH 139/200] add `:edit` and `:e` as aliases for `:open` (#11186) Vim supports these, and i can't think of any reason helix would want to have a different meaning for `:edit` than `:open`. docs: https://vimhelp.org/editing.txt.html#%3Aedit --- helix-term/src/commands/typed.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1b828b3f4..88ee22527 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2537,7 +2537,7 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> }, TypableCommand { name: "open", - aliases: &["o"], + aliases: &["o", "edit", "e"], doc: "Open a file from disk into the current view.", fun: open, signature: CommandSignature::all(completers::filename), From 6b947518c6fba1d405c2bbff03e5815ce6b6d335 Mon Sep 17 00:00:00 2001 From: Masanori Ogino <167209+omasanori@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:27:45 +0900 Subject: [PATCH 140/200] Regenerate documentation (#11196) --- book/src/generated/typable-cmd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index cf1b550dc..100c65953 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -2,7 +2,7 @@ | --- | --- | | `:quit`, `:q` | Close the current view. | | `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. | -| `:open`, `:o` | Open a file from disk into the current view. | +| `:open`, `:o`, `:edit`, `:e` | Open a file from disk into the current view. | | `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. | | `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully, ignoring unsaved changes. | | `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. | From 884b53c76779df3cf25076278e8037d6a6b3ef83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:28:41 +0900 Subject: [PATCH 141/200] build(deps): bump the rust-dependencies group with 3 updates (#11194) Bumps the rust-dependencies group with 3 updates: [thiserror](https://github.com/dtolnay/thiserror), [open](https://github.com/Byron/open-rs) and [cc](https://github.com/rust-lang/cc-rs). Updates `thiserror` from 1.0.61 to 1.0.62 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.62) Updates `open` from 5.2.0 to 5.3.0 - [Release notes](https://github.com/Byron/open-rs/releases) - [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md) - [Commits](https://github.com/Byron/open-rs/compare/v5.2.0...v5.3.0) Updates `cc` from 1.0.106 to 1.1.5 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.0.106...cc-v1.1.5) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: open dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++-------- helix-term/Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1cd1632f..3b43e0210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.106" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" [[package]] name = "cfg-if" @@ -1850,9 +1850,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" dependencies = [ "is-wsl", "libc", @@ -2345,18 +2345,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index c50165e9a..cc90396e4 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -59,7 +59,7 @@ content_inspector = "0.2.4" thiserror = "1.0" # opening URLs -open = "5.2.0" +open = "5.3.0" url = "2.5.2" # config From 850c9f691e771bf8a4a5c1307d23788db7097f7f Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Tue, 16 Jul 2024 05:29:00 +0200 Subject: [PATCH 142/200] Fixed headings (# / ##) to match other docs (#11192) --- book/src/guides/adding_languages.md | 2 +- book/src/guides/indent.md | 2 +- book/src/guides/injection.md | 2 +- book/src/guides/textobject.md | 2 +- book/src/lang-support.md | 2 +- book/src/languages.md | 2 +- book/src/remapping.md | 2 +- book/src/themes.md | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 1a47b79c6..aa4bf0c5c 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -1,4 +1,4 @@ -# Adding new languages to Helix +## Adding new languages to Helix In order to add a new language to Helix, you will need to follow the steps below. diff --git a/book/src/guides/indent.md b/book/src/guides/indent.md index be140384a..1e520731a 100644 --- a/book/src/guides/indent.md +++ b/book/src/guides/indent.md @@ -1,4 +1,4 @@ -# Adding indent queries +## Adding indent queries Helix uses tree-sitter to correctly indent new lines. This requires a tree- sitter grammar and an `indent.scm` query file placed in `runtime/queries/ diff --git a/book/src/guides/injection.md b/book/src/guides/injection.md index 0a1d2c9a2..c0f750941 100644 --- a/book/src/guides/injection.md +++ b/book/src/guides/injection.md @@ -1,4 +1,4 @@ -# Adding Injection Queries +## Adding Injection Queries Writing language injection queries allows one to highlight a specific node as a different language. In addition to the [standard][upstream-docs] language injection options used by tree-sitter, there diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md index a31baef93..4d33ab676 100644 --- a/book/src/guides/textobject.md +++ b/book/src/guides/textobject.md @@ -1,4 +1,4 @@ -# Adding textobject queries +## Adding textobject queries Helix supports textobjects that are language specific, such as functions, classes, etc. These textobjects require an accompanying tree-sitter grammar and a `textobjects.scm` query file diff --git a/book/src/lang-support.md b/book/src/lang-support.md index aede33fa0..3b3261645 100644 --- a/book/src/lang-support.md +++ b/book/src/lang-support.md @@ -1,4 +1,4 @@ -# Language Support +## Language Support The following languages and Language Servers are supported. To use Language Server features, you must first [configure][lsp-config-wiki] the diff --git a/book/src/languages.md b/book/src/languages.md index 33ecbb92f..fe105cced 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -1,4 +1,4 @@ -# Languages +## Languages Language-specific settings and settings for language servers are configured in `languages.toml` files. diff --git a/book/src/remapping.md b/book/src/remapping.md index d762c6add..863c55570 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -1,4 +1,4 @@ -# Key remapping +## Key remapping Helix currently supports one-way key remapping through a simple TOML configuration file. (More powerful solutions such as rebinding via commands will be diff --git a/book/src/themes.md b/book/src/themes.md index b8e271374..f9f8393c3 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -1,4 +1,4 @@ -# Themes +## Themes To use a theme add `theme = ""` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme `. From 535351067c2ac018ee2fef6cc685f49065617bd1 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Mon, 15 Jul 2024 20:29:44 -0700 Subject: [PATCH 143/200] fix(commands): change `pipe`-like output trimming (#11183) --- helix-term/src/commands.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 234902886..7e0bee92b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5745,14 +5745,20 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let output = if let Some(output) = shell_output.as_ref() { output.clone() } else { - let fragment = range.slice(text); - match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { - Ok(result) => { - let result = Tendril::from(result.trim_end()); - if !pipe { - shell_output = Some(result.clone()); + let input = range.slice(text); + match shell_impl(shell, cmd, pipe.then(|| input.into())) { + Ok(mut output) => { + if !input.ends_with("\n") && !output.is_empty() && output.ends_with('\n') { + output.pop(); + if output.ends_with('\r') { + output.pop(); + } } - result + + if !pipe { + shell_output = Some(output.clone()); + } + output } Err(err) => { cx.editor.set_error(err.to_string()); From aac81424cd25a950d85ca152c337ed530b5d0699 Mon Sep 17 00:00:00 2001 From: Leandro Braga <18340809+leandrobbraga@users.noreply.github.com> Date: Tue, 16 Jul 2024 00:30:45 -0300 Subject: [PATCH 144/200] Fix `select_all_children` command (#11195) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f82721a0..491edb204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ # 24.07 (2024-07-14) Commands: - `select_all_siblings` (`A-a`) - select all siblings of each selection ([87c4161](https://github.com/helix-editor/helix/commit/87c4161)) -- `select_all_children` (`A-i`) - select all children of each selection ([fa67c5c](https://github.com/helix-editor/helix/commit/fa67c5c)) +- `select_all_children` (`A-I`) - select all children of each selection ([fa67c5c](https://github.com/helix-editor/helix/commit/fa67c5c)) - `:read` - insert the contents of the given file at each selection ([#10447](https://github.com/helix-editor/helix/pull/10447)) Usability improvements: From 22a051408a467ff8b3e43457961d5497903dc7d0 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 15 Jul 2024 22:31:29 -0500 Subject: [PATCH 145/200] Update release docs (#11182) These haven't been updated in a little while. The original plan was to update the version (in `Cargo.toml`) after a release to the next planned release date but the way we release now is to update the version as a part of the release process (just before tagging). Typically this is all taken care of in the CHANGELOG-updating branch along with the other documentation changes like the appdata file. The workflow now is basically just to merge the changelog/release branch, pull, tag and push. --- docs/releases.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 2be14553a..84f95c97e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -5,19 +5,18 @@ ## Checklist `22.05.1`. In these instructions we'll use `` as a placeholder for the tag being published. -* Merge the changelog PR -* Add new `` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html) +* Merge the PR with the release updates. That branch should: + * Update the version: + * Update the `workspace.package.version` key in `Cargo.toml`. Cargo only accepts + SemVer versions so a CalVer version of `22.07` for example must be formatted + as `22.7.0`. Patch/bugfix releases should increment the SemVer patch number. A + patch release for 22.07 would be `22.7.1`. + * Run `cargo check` and commit the resulting change to `Cargo.lock` + * Add changelog notes to `CHANGELOG.md` + * Add new `` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html) * Tag and push - * `git tag -s -m "" -a && git push` - * Make sure to switch to master and pull first -* Edit the `Cargo.toml` file and change the date in the `version` field to the next planned release - * Due to Cargo having a strict requirement on SemVer with 3 or more version - numbers, a `0` is required in the micro version; however, unless we are - publishing a patch release after a major release, the `.0` is dropped in - the user facing version. - * Releases are planned to happen every two months, so `22.05.0` would change to `22.07.0` - * If we are pushing a patch/bugfix release in the same month as the previous - release, bump the micro version, e.g. `22.07.0` to `22.07.1` + * Switch to master and pull + * `git tag -s -m "" -a && git push` (note the `-s` which signs the tag) * Wait for the Release CI to finish * It will automatically turn the git tag into a GitHub release when it uploads artifacts * Edit the new release From 72e0f6301b20a14dccdc7b5867b8f09cb708d678 Mon Sep 17 00:00:00 2001 From: irkill <42892040+irkill@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:34:41 +0200 Subject: [PATCH 146/200] Fix a typo in the tutor (#11201) --- runtime/tutor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/tutor b/runtime/tutor index 505f88482..5e82cf888 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -1455,7 +1455,7 @@ letters! that is not good grammar. you can fix this. ================================================================= Open a split on the left with :vs hello1 and then a split below - with :vs hello2. + with :hs hello2. Move to the tutor split, then press Ctrl-w t to transpose the vertical split opened from this window: now, hello1 and From bd5e8931499dc6d4294d825697b9996946045502 Mon Sep 17 00:00:00 2001 From: karei Date: Wed, 17 Jul 2024 10:36:01 +0300 Subject: [PATCH 147/200] Bring `kanagawa` colours better in line with neovim version (#11187) - Fixes some colours not matching their counterpart in neovim. - Adds `ui.debug` colours - Fix for separators in inactive statuslines --- runtime/themes/kanagawa.toml | 63 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml index 02b5271ec..2d3836513 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -11,14 +11,13 @@ "ui.selection.primary" = { bg = "waveBlue2" } "ui.background" = { fg = "fujiWhite", bg = "sumiInk1" } -"ui.linenr" = { fg = "sumiInk4" } -"ui.linenr.selected" = { fg = "roninYellow" } +"ui.linenr" = { fg = "sumiInk6" } +"ui.linenr.selected" = { fg = "roninYellow", modifiers = ["bold"] } +"ui.gutter" = { fg = "sumiInk6", bg = "sumiInk4" } -"ui.virtual" = "sumiInk4" +"ui.virtual" = "sumiInk6" "ui.virtual.ruler" = { bg = "sumiInk2" } -"ui.virtual.inlay-hint" = "fujiGray" -"ui.virtual.inlay-hint.parameter" = { fg = "carpYellow", modifiers = ["dim"] } -"ui.virtual.inlay-hint.type" = { fg = "waveAqua2", modifiers = ["dim"] } +"ui.virtual.inlay-hint" = "sumiInk6" "ui.virtual.jump-label" = { fg = "peachRed", modifiers = ["bold"] } "ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" } @@ -26,6 +25,7 @@ "ui.statusline.normal" = { fg = "sumiInk0", bg = "crystalBlue", modifiers = ["bold"] } "ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen", modifiers = ["bold"] } "ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet", modifiers = ["bold"] } +"ui.statusline.separator" = { fg = "", bg = "" } "ui.bufferline" = { fg = "fujiGray", bg = "sumiInk0" } "ui.bufferline.active" = { fg = "oldWhite", bg = "sumiInk0" } @@ -35,7 +35,7 @@ "ui.window" = { fg = "sumiInk0" } "ui.help" = { fg = "fujiWhite", bg = "sumiInk0" } "ui.text" = "fujiWhite" -"ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } +"ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue2", modifiers = ["bold"] } "ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2" } "ui.cursor.primary" = { fg = "waveBlue1", bg = "fujiWhite" } @@ -48,30 +48,37 @@ "ui.cursorline.primary" = { bg = "sumiInk3" } "ui.cursorcolumn.primary" = { bg = "sumiInk3" } +"ui.debug.breakpoint" = "springBlue" +"ui.debug.active" = "winterRed" + "diagnostic.error" = { underline = { color = "samuraiRed", style = "curl" } } "diagnostic.warning" = { underline = { color = "roninYellow", style = "curl" } } -"diagnostic.info" = { underline = { color = "waveAqua1", style = "curl" } } -"diagnostic.hint" = { underline = { color = "dragonBlue", style = "curl" } } +"diagnostic.info" = { underline = { color = "dragonBlue", style = "curl" } } +"diagnostic.hint" = { underline = { color = "waveAqua1", style = "curl" } } "diagnostic.unnecessary" = { modifiers = ["dim"] } "diagnostic.deprecated" = { modifiers = ["crossed_out"] } error = "samuraiRed" warning = "roninYellow" -info = "waveAqua1" -hint = "dragonBlue" +info = "dragonBlue" +hint = "waveAqua1" -## Git gutter -"diff.plus" = "autumnGreen" -"diff.minus" = "autumnRed" -"diff.delta" = "autumnYellow" +## Diff +"diff.plus" = "winterGreen" +"diff.plus.gutter" = "autumnGreen" +"diff.minus" = "winterRed" +"diff.minus.gutter" = "autumnRed" +"diff.delta" = "winterBlue" +"diff.delta.gutter" = "autumnYellow" ## Syntax highlighting "attribute" = "waveRed" "type" = "waveAqua2" +"type.builtin" = "springBlue" "constructor" = "springBlue" "constant" = "surimiOrange" "constant.numeric" = "sakuraPink" -"constant.character.escape" = "springBlue" +"constant.character.escape" = { fg = "boatYellow2", modifiers = ["bold"] } "string" = "springGreen" "string.regexp" = "boatYellow2" "string.special.url" = "springBlue" @@ -79,14 +86,14 @@ hint = "dragonBlue" "comment" = "fujiGray" "variable" = "fujiWhite" "variable.builtin" = "waveRed" -"variable.parameter" = "carpYellow" +"variable.parameter" = "oniViolet2" "variable.other.member" = "carpYellow" "label" = "springBlue" "punctuation" = "springViolet2" -"keyword" = "oniViolet" +"keyword" = { fg = "oniViolet", modifiers = ["italic"] } "keyword.control.return" = "peachRed" "keyword.control.exception" = "peachRed" -"keyword.directive" = "peachRed" +"keyword.directive" = "waveRed" "operator" = "boatYellow2" "function" = "crystalBlue" "function.builtin" = "springBlue" @@ -97,19 +104,13 @@ hint = "dragonBlue" ## Markup modifiers "markup.heading.marker" = "springViolet2" -"markup.heading.1" = { fg = "surimiOrange", modifiers = ["bold"] } -"markup.heading.2" = { fg = "carpYellow", modifiers = ["bold"] } -"markup.heading.3" = { fg = "waveAqua2", modifiers = ["bold"] } -"markup.heading.4" = { fg = "lightBlue", modifiers = ["bold"] } -"markup.heading.5" = { fg = "oniViolet", modifiers = ["bold"] } -"markup.heading.6" = { fg = "springViolet1", modifiers = ["bold"] } -"markup.list.numbered" = "sakuraPink" -"markup.list.unnumbered" = "waveRed" +"markup.heading" = { fg = "crystalBlue", modifiers = ["bold"] } +"markup.list" = "oniViolet" "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.text" = "crystalBlue" -"markup.link.url" = { fg = "springBlue", underline.style = "line" } +"markup.link.text" = { fg = "springBlue" } +"markup.link.url" = { fg = "sakuraPink" } "markup.link.label" = "surimiOrange" "markup.quote" = "springViolet1" "markup.raw" = "springGreen" @@ -122,7 +123,8 @@ sumiInk0 = "#16161D" # dark background, e.g. statuslines, floating windows sumiInk1 = "#1F1F28" # default background sumiInk2 = "#2A2A37" # lighter background, e.g. colorcolumns, folds sumiInk3 = "#363646" # lighter background, e.g. cursorline -sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column +sumiInk4 = "#2A2A37" # darker foreground, e.g. linenumbers, fold column +sumiInk6 = "#54546D" # inlay hints waveBlue1 = "#223249" # popup background, visual selection background waveBlue2 = "#2D4F67" # popup selection background, search background winterGreen = "#2B3328" # diff add background @@ -139,6 +141,7 @@ dragonBlue = "#658594" # diagnostic hint fujiGray = "#727169" # comments springViolet1 = "#938AA9" # light foreground oniViolet = "#957FB8" # statements and keywords +oniViolet2 = "#B8B4D0" # parameters crystalBlue = "#7E9CD8" # functions and titles springViolet2 = "#9CABCA" # brackets and punctuation springBlue = "#7FB4CA" # specials and builtins From c9d829a26dac81a57f18796308d3806d77d0c439 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 17 Jul 2024 08:41:57 -0500 Subject: [PATCH 148/200] global_search: Save search when accepting an option (#11209) The Prompt is set up to push the current line to history when hitting Enter but the Picker doesn't pass the Enter event down to the Prompt (for good reason: we don't want the Prompt's behavior of changing completions when we hit a path separator). We should save the Prompt's line to its configured history register when hitting Enter when there is a selection in the Picker. This currently only applies to `global_search`'s Picker since it's the only Picker to use `Picker::with_history_register`. --- helix-term/src/ui/picker.rs | 1 + helix-term/src/ui/prompt.rs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 70be421ae..e2d15ed21 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1031,6 +1031,7 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { self.handle_prompt_change(); } else { if let Some(option) = self.selection() { + self.prompt.save_line_to_history(ctx.editor); (self.callback_fn)(ctx, option, Action::Replace); } return close_fn(self); diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 3518ddf77..1ba014f5d 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -516,6 +516,16 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color); } } + + /// Saves the current line to the configured history register, if there is one. + pub(crate) fn save_line_to_history(&self, editor: &mut Editor) { + let Some(register) = self.history_register else { + return; + }; + if let Err(err) = editor.registers.push(register, self.line.clone()) { + editor.set_error(err.to_string()); + } + } } impl Component for Prompt { @@ -603,14 +613,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { &last_item } else { if last_item != self.line { - // store in history - if let Some(register) = self.history_register { - if let Err(err) = - cx.editor.registers.push(register, self.line.clone()) - { - cx.editor.set_error(err.to_string()); - } - }; + self.save_line_to_history(cx.editor); } &self.line From b927985cd0f8b68f9a7f177353e96d8aaabf1b06 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 17 Jul 2024 21:08:39 -0500 Subject: [PATCH 149/200] global_search: Save only the primary query to the history register (#11216) Two changes from the parent commit: * Save only the `Picker::primary_query` - so you don't save other parts of the query, for example `%path foo.rs` while in `global_search`. * Move the saving out of the `if let Some(option) = self.selection()` block. So when you hit enter you save to history whether you have a selection or not. If you want to close the picker without saving to the register you can use C-c or Esc instead. --- helix-term/src/ui/picker.rs | 10 +++++++++- helix-term/src/ui/prompt.rs | 23 ++++++++++++----------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e2d15ed21..ffec0fd83 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1031,9 +1031,17 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { self.handle_prompt_change(); } else { if let Some(option) = self.selection() { - self.prompt.save_line_to_history(ctx.editor); (self.callback_fn)(ctx, option, Action::Replace); } + if let Some(history_register) = self.prompt.history_register() { + if let Err(err) = ctx + .editor + .registers + .push(history_register, self.primary_query().to_string()) + { + ctx.editor.set_error(err.to_string()); + } + } return close_fn(self); } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 1ba014f5d..6ba2fcb9e 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -128,6 +128,10 @@ pub fn with_history_register(&mut self, history_register: Option) -> &mut self } + pub(crate) fn history_register(&self) -> Option { + self.history_register + } + pub(crate) fn first_history_completion<'a>( &'a self, editor: &'a Editor, @@ -516,16 +520,6 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color); } } - - /// Saves the current line to the configured history register, if there is one. - pub(crate) fn save_line_to_history(&self, editor: &mut Editor) { - let Some(register) = self.history_register else { - return; - }; - if let Err(err) = editor.registers.push(register, self.line.clone()) { - editor.set_error(err.to_string()); - } - } } impl Component for Prompt { @@ -613,7 +607,14 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { &last_item } else { if last_item != self.line { - self.save_line_to_history(cx.editor); + // store in history + if let Some(register) = self.history_register { + if let Err(err) = + cx.editor.registers.push(register, self.line.clone()) + { + cx.editor.set_error(err.to_string()); + } + }; } &self.line From 748a9cf022eb74e96a3697e4b11b2490b1e58f08 Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Thu, 18 Jul 2024 16:10:10 +0200 Subject: [PATCH 150/200] tree-sitter: Update SHA of parser fro the slint language (#11224) There has been a new release with a few minor tweaks to the parser. The queries are fine still. --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 97ee3e54a..87783b3de 100644 --- a/languages.toml +++ b/languages.toml @@ -2373,7 +2373,7 @@ language-servers = [ "slint-lsp" ] [[grammar]] name = "slint" -source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "0701312b74b87fe20e61aa662ba41c5815b5d428" } +source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "4a0558cc0fcd7a6110815b9bbd7cc12d7ab31e74" } [[language]] name = "task" From dbaa6366834790cda0bd92ea8971fec9ae9b601b Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 19 Jul 2024 03:44:55 -0500 Subject: [PATCH 151/200] Picker: Skip dynamic query debounce for pastes (#11211) Pastes are probably the last edit one means to make before the query should run so it doesn't need to be debounced. This makes global search much snappier for example when accepting the history suggestion from the '/' register or pasting a pattern from the clipboard or a register. --- helix-term/src/ui/picker.rs | 24 +++++++++++++++++------- helix-term/src/ui/picker/handlers.rs | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ffec0fd83..118dafa77 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -52,7 +52,7 @@ Document, DocumentId, Editor, }; -use self::handlers::{DynamicQueryHandler, PreviewHighlightHandler}; +use self::handlers::{DynamicQueryChange, DynamicQueryHandler, PreviewHighlightHandler}; pub const ID: &str = "picker"; @@ -272,7 +272,7 @@ pub struct Picker { file_fn: Option>, /// An event handler for syntax highlighting the currently previewed file. preview_highlight_handler: Sender>, - dynamic_query_handler: Option>>, + dynamic_query_handler: Option>, } impl Picker { @@ -435,7 +435,12 @@ pub fn with_dynamic_query( debounce_ms: Option, ) -> Self { let handler = DynamicQueryHandler::new(callback, debounce_ms).spawn(); - helix_event::send_blocking(&handler, self.primary_query()); + let event = DynamicQueryChange { + query: self.primary_query(), + // Treat the initial query as a paste. + is_paste: true, + }; + helix_event::send_blocking(&handler, event); self.dynamic_query_handler = Some(handler); self } @@ -511,12 +516,12 @@ pub fn toggle_preview(&mut self) { fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { - self.handle_prompt_change(); + self.handle_prompt_change(matches!(event, Event::Paste(_))); } EventResult::Consumed(None) } - fn handle_prompt_change(&mut self) { + fn handle_prompt_change(&mut self, is_paste: bool) { // TODO: better track how the pattern has changed let line = self.prompt.line(); let old_query = self.query.parse(line); @@ -557,7 +562,11 @@ fn handle_prompt_change(&mut self) { // If this is a dynamic picker, notify the query hook that the primary // query might have been updated. if let Some(handler) = &self.dynamic_query_handler { - helix_event::send_blocking(handler, self.primary_query()); + let event = DynamicQueryChange { + query: self.primary_query(), + is_paste, + }; + helix_event::send_blocking(handler, event); } } @@ -1028,7 +1037,8 @@ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { .filter(|_| self.prompt.line().is_empty()) { self.prompt.set_line(completion.to_string(), ctx.editor); - self.handle_prompt_change(); + // Inserting from the history register is a paste. + self.handle_prompt_change(true); } else { if let Some(option) = self.selection() { (self.callback_fn)(ctx, option, Action::Replace); diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs index 4896ccbc6..040fffa88 100644 --- a/helix-term/src/ui/picker/handlers.rs +++ b/helix-term/src/ui/picker/handlers.rs @@ -115,6 +115,11 @@ fn finish_debounce(&mut self) { } } +pub(super) struct DynamicQueryChange { + pub query: Arc, + pub is_paste: bool, +} + pub(super) struct DynamicQueryHandler { callback: Arc>, // Duration used as a debounce. @@ -137,9 +142,10 @@ pub(super) fn new(callback: DynQueryCallback, duration_ms: Option) -> } impl AsyncHook for DynamicQueryHandler { - type Event = Arc; + type Event = DynamicQueryChange; - fn handle_event(&mut self, query: Self::Event, _timeout: Option) -> Option { + fn handle_event(&mut self, change: Self::Event, _timeout: Option) -> Option { + let DynamicQueryChange { query, is_paste } = change; if query == self.last_query { // If the search query reverts to the last one we requested, no need to // make a new request. @@ -147,7 +153,12 @@ fn handle_event(&mut self, query: Self::Event, _timeout: Option) -> Opt None } else { self.query = Some(query); - Some(Instant::now() + self.debounce) + if is_paste { + self.finish_debounce(); + None + } else { + Some(Instant::now() + self.debounce) + } } } From 6c0a7f60eb51e4043a8ae79093500fa0831010d0 Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Mon, 22 Jul 2024 16:47:27 +0200 Subject: [PATCH 152/200] contrib: add nushell completions (#11262) --- contrib/completion/hx.nu | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 contrib/completion/hx.nu diff --git a/contrib/completion/hx.nu b/contrib/completion/hx.nu new file mode 100644 index 000000000..b7bb542c0 --- /dev/null +++ b/contrib/completion/hx.nu @@ -0,0 +1,25 @@ +# Completions for Helix: +# +# NOTE: the `+N` syntax is not supported in Nushell (https://github.com/nushell/nushell/issues/13418) +# so it has not been specified here and will not be proposed in the autocompletion of Nushell. +# The help message won't be overriden though, so it will still be present here + +def health_categories [] { ["all", "clipboard", "languages"] } + +def grammar_categories [] { ["fetch", "build"] } + +# A post-modern text editor. +export extern hx [ + --help(-h), # Prints help information + --tutor, # Loads the tutorial + --health: string@health_categories = "all", # Checks for potential errors in editor setup + --grammar(-g): string@grammar_categories, # Fetches or builds tree-sitter grammars listed in `languages.toml` + --config(-c): glob, # Specifies a file to use for configuration + -v, # Increases logging verbosity each use for up to 3 times + --log: glob, # Specifies a file to use for logging + --version(-V), # Prints version information + --vsplit, # Splits all given files vertically into different windows + --hsplit, # Splits all given files horizontally into different windows + --working-dir(-w): glob, # Specify an initial working directory + ...files: glob, # Sets the input file to use, position can also be specified via file[:row[:col]] +] From 70a9477ec8221f25e7ee978f0fa0217f98c861c6 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 22 Jul 2024 09:55:12 -0500 Subject: [PATCH 153/200] Add `:mv` as an alias for `:move` (#11256) `mv` is the familiar shell command to move or rename a file. Add this to Helix as an alias for `:move`. --- book/src/generated/typable-cmd.md | 2 +- helix-term/src/commands/typed.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 100c65953..f48e1490a 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -85,6 +85,6 @@ | `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | | `:clear-register` | Clear given register. If no argument is provided, clear all registers. | | `:redraw` | Clear and re-render the whole UI | -| `:move` | Move the current buffer and its corresponding file to a different path | +| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path | | `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default | | `:read`, `:r` | Load a file into buffer | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 88ee22527..530d78097 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3122,7 +3122,7 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> }, TypableCommand { name: "move", - aliases: &[], + aliases: &["mv"], doc: "Move the current buffer and its corresponding file to a different path", fun: move_buffer, signature: CommandSignature::positional(&[completers::filename]), From f5231196bc1659846352e4c3c6d9a6a3c60c38bb Mon Sep 17 00:00:00 2001 From: Hamir Mahal Date: Mon, 22 Jul 2024 11:23:16 -0700 Subject: [PATCH 154/200] fix: usage of `node12 which is deprecated` (#11277) * chore: changes from formatting on save * fix: usage of `node12 which is deprecated` --- .github/workflows/build.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ba46ce56..93fcb9816 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: - master merge_group: schedule: - - cron: '00 01 * * *' + - cron: "00 01 * * *" jobs: check: @@ -17,10 +17,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - name: Install stable toolchain - uses: helix-editor/rust-toolchain@v1 - with: - profile: minimal - override: true + uses: dtolnay/rust-toolchain@1.70 - uses: Swatinem/rust-cache@v2 with: @@ -119,4 +116,3 @@ jobs: git diff-files --quiet \ || (echo "Run 'cargo xtask docgen', commit the changes and push again" \ && exit 1) - From d47e085fe06dd04188a17cc32faf22a86a70622d Mon Sep 17 00:00:00 2001 From: karei Date: Tue, 23 Jul 2024 00:54:52 +0300 Subject: [PATCH 155/200] Revert `kanagawa` diff colour change from #11187 (#11270) --- runtime/themes/kanagawa.toml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml index 2d3836513..dabece68d 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -64,12 +64,9 @@ info = "dragonBlue" hint = "waveAqua1" ## Diff -"diff.plus" = "winterGreen" -"diff.plus.gutter" = "autumnGreen" -"diff.minus" = "winterRed" -"diff.minus.gutter" = "autumnRed" -"diff.delta" = "winterBlue" -"diff.delta.gutter" = "autumnYellow" +"diff.plus" = "autumnGreen" +"diff.minus" = "autumnRed" +"diff.delta" = "autumnYellow" ## Syntax highlighting "attribute" = "waveRed" From 86795a9dc7a0f7fdd9a40f5b1e4a12b0c87b8d96 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 22 Jul 2024 16:56:26 -0500 Subject: [PATCH 156/200] Return document display name from the '%' special register (#11275) --- helix-view/src/register.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index 166d2027d..3a2e1b7cc 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -5,7 +5,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType}, - document::SCRATCH_BUFFER_NAME, Editor, }; @@ -61,14 +60,7 @@ pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option { - let doc = doc!(editor); - - let path = doc - .path() - .as_ref() - .map(|p| p.to_string_lossy()) - .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); - + let path = doc!(editor).display_name(); Some(RegisterValues::new(iter::once(path))) } '*' | '+' => Some(read_from_clipboard( From 0d62656c987ec32f44d19ad7ab02c120c6344470 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:05:34 +0200 Subject: [PATCH 157/200] build(deps): bump the rust-dependencies group with 5 updates (#11281) Bumps the rust-dependencies group with 5 updates: | Package | From | To | | --- | --- | --- | | [thiserror](https://github.com/dtolnay/thiserror) | `1.0.62` | `1.0.63` | | [toml](https://github.com/toml-rs/toml) | `0.8.14` | `0.8.15` | | [tokio](https://github.com/tokio-rs/tokio) | `1.38.0` | `1.38.1` | | [cc](https://github.com/rust-lang/cc-rs) | `1.1.5` | `1.1.6` | | [libloading](https://github.com/nagisa/rust_libloading) | `0.8.4` | `0.8.5` | Updates `thiserror` from 1.0.62 to 1.0.63 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.62...1.0.63) Updates `toml` from 0.8.14 to 0.8.15 - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.14...toml-v0.8.15) Updates `tokio` from 1.38.0 to 1.38.1 - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.38.0...tokio-1.38.1) Updates `cc` from 1.1.5 to 1.1.6 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.1.5...cc-v1.1.6) Updates `libloading` from 0.8.4 to 0.8.5 - [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.4...0.8.5) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: libloading dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b43e0210..dfb204c06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" [[package]] name = "cfg-if" @@ -1681,9 +1681,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets 0.52.0", @@ -2345,18 +2345,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -2422,9 +2422,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -2463,9 +2463,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", @@ -2484,9 +2484,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap", "serde", From 1d0a3d49d38e9d882fed573b836729f5b1410076 Mon Sep 17 00:00:00 2001 From: Ingrid Date: Tue, 23 Jul 2024 19:54:00 +0200 Subject: [PATCH 158/200] Consistently maintain view position (#10559) * replicate t-monaghan's changes * remove View.offset in favour of Document.view_data.view_position * improve access patterns for Document.view_data * better borrow checker wrangling with doc_mut!() * reintroduce ensure_cursor_in_view in handle_config_events since we sorted out the borrow checker issues using partial borrows, there's nothing stopping us from going back to the simpler implementation * introduce helper functions on Document .view_offset, set_view_offset * fix rebase breakage --- helix-term/src/application.rs | 6 +-- helix-term/src/commands.rs | 57 +++++++++++++++++----------- helix-term/src/commands/lsp.rs | 3 +- helix-term/src/commands/typed.rs | 2 +- helix-term/src/ui/editor.rs | 21 +++++++---- helix-term/src/ui/mod.rs | 6 +-- helix-view/src/document.rs | 51 ++++++++++++++++++++++--- helix-view/src/editor.rs | 15 ++++---- helix-view/src/lib.rs | 9 +++-- helix-view/src/view.rs | 64 +++++++++++++++++--------------- 10 files changed, 150 insertions(+), 84 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b7123e972..60bc5b7cf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -395,9 +395,9 @@ pub fn handle_config_events(&mut self, config_event: ConfigEvent) { // reset view position in case softwrap was enabled/disabled let scrolloff = self.editor.config().scrolloff; - for (view, _) in self.editor.tree.views_mut() { - let doc = &self.editor.documents[&view.doc]; - view.ensure_cursor_in_view(doc, scrolloff) + for (view, _) in self.editor.tree.views() { + let doc = doc_mut!(self.editor, &view.doc); + view.ensure_cursor_in_view(doc, scrolloff); } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7e0bee92b..2c5d2783b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1032,6 +1032,7 @@ fn goto_window(cx: &mut Context, align: Align) { let count = cx.count() - 1; let config = cx.editor.config(); let (view, doc) = current!(cx.editor); + let view_offset = doc.view_offset(view.id); let height = view.inner_height(); @@ -1044,15 +1045,15 @@ fn goto_window(cx: &mut Context, align: Align) { let last_visual_line = view.last_visual_line(doc); let visual_line = match align { - Align::Top => view.offset.vertical_offset + scrolloff + count, - Align::Center => view.offset.vertical_offset + (last_visual_line / 2), + Align::Top => view_offset.vertical_offset + scrolloff + count, + Align::Center => view_offset.vertical_offset + (last_visual_line / 2), Align::Bottom => { - view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count) + view_offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count) } }; let visual_line = visual_line - .max(view.offset.vertical_offset + scrolloff) - .min(view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff)); + .max(view_offset.vertical_offset + scrolloff) + .min(view_offset.vertical_offset + last_visual_line.saturating_sub(scrolloff)); let pos = view .pos_at_visual_coords(doc, visual_line as u16, 0, false) @@ -1665,6 +1666,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor use Direction::*; let config = cx.editor.config(); let (view, doc) = current!(cx.editor); + let mut view_offset = doc.view_offset(view.id); let range = doc.selection(view.id).primary(); let text = doc.text().slice(..); @@ -1681,15 +1683,19 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor let doc_text = doc.text().slice(..); let viewport = view.inner_area(doc); let text_fmt = doc.text_format(viewport.width, None); - let mut annotations = view.text_annotations(&*doc, None); - (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset( + (view_offset.anchor, view_offset.vertical_offset) = char_idx_at_visual_offset( doc_text, - view.offset.anchor, - view.offset.vertical_offset as isize + offset, + view_offset.anchor, + view_offset.vertical_offset as isize + offset, 0, &text_fmt, - &annotations, + // &annotations, + &view.text_annotations(&*doc, None), ); + doc.set_view_offset(view.id, view_offset); + + let doc_text = doc.text().slice(..); + let mut annotations = view.text_annotations(&*doc, None); if sync_cursor { let movement = match cx.editor.mode { @@ -1716,14 +1722,16 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor return; } + let view_offset = doc.view_offset(view.id); + let mut head; match direction { Forward => { let off; (head, off) = char_idx_at_visual_offset( doc_text, - view.offset.anchor, - (view.offset.vertical_offset + scrolloff) as isize, + view_offset.anchor, + (view_offset.vertical_offset + scrolloff) as isize, 0, &text_fmt, &annotations, @@ -1736,8 +1744,8 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor Backward => { head = char_idx_at_visual_offset( doc_text, - view.offset.anchor, - (view.offset.vertical_offset + height - scrolloff - 1) as isize, + view_offset.anchor, + (view_offset.vertical_offset + height - scrolloff - 1) as isize, 0, &text_fmt, &annotations, @@ -5124,7 +5132,7 @@ fn split(editor: &mut Editor, action: Action) { let (view, doc) = current!(editor); let id = doc.id(); let selection = doc.selection(view.id).clone(); - let offset = view.offset; + let offset = doc.view_offset(view.id); editor.switch(id, action); @@ -5133,7 +5141,7 @@ fn split(editor: &mut Editor, action: Action) { doc.set_selection(view.id, selection); // match the view scroll offset (switch doesn't handle this fully // since the selection is only matched after the split) - view.offset = offset; + doc.set_view_offset(view.id, offset); } fn hsplit(cx: &mut Context) { @@ -5228,14 +5236,21 @@ fn align_view_middle(cx: &mut Context) { return; } let doc_text = doc.text().slice(..); - let annotations = view.text_annotations(doc, None); let pos = doc.selection(view.id).primary().cursor(doc_text); - let pos = - visual_offset_from_block(doc_text, view.offset.anchor, pos, &text_fmt, &annotations).0; + let pos = visual_offset_from_block( + doc_text, + doc.view_offset(view.id).anchor, + pos, + &text_fmt, + &view.text_annotations(doc, None), + ) + .0; - view.offset.horizontal_offset = pos + let mut offset = doc.view_offset(view.id); + offset.horizontal_offset = pos .col .saturating_sub((view.inner_area(doc).width as usize) / 2); + doc.set_view_offset(view.id, offset); } fn scroll_up(cx: &mut Context) { @@ -6117,7 +6132,7 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { // This is not necessarily exact if there is virtual text like soft wrap. // It's ok though because the extra jump labels will not be rendered. - let start = text.line_to_char(text.char_to_line(view.offset.anchor)); + let start = text.line_to_char(text.char_to_line(doc.view_offset(view.id).anchor)); let end = text.line_to_char(view.estimate_last_doc_line(doc) + 1); let primary_selection = doc.selection(view.id).primary(); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 3b9efb431..9194377c4 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1299,7 +1299,8 @@ fn compute_inlay_hints_for_view( // than computing all the hints for the full file (which could be dozens of time // longer than the view is). let view_height = view.inner_height(); - let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars())); + let first_visible_line = + doc_text.char_to_line(doc.view_offset(view_id).anchor.min(doc_text.len_chars())); let first_line = first_visible_line.saturating_sub(view_height); let last_line = first_visible_line .saturating_add(view_height.saturating_mul(2)) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 530d78097..720d32ac8 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1587,7 +1587,7 @@ fn find_highlight_at_cursor( // Query the same range as the one used in syntax highlighting. let range = { // Calculate viewport byte ranges: - let row = text.char_to_line(view.offset.anchor.min(text.len_chars())); + let row = text.char_to_line(doc.view_offset(view.id).anchor.min(text.len_chars())); // Saturating subs to make it inclusive zero indexing. let last_line = text.len_lines().saturating_sub(1); let height = view.inner_area(doc).height; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index c151a7dd5..f7541fe25 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -93,6 +93,8 @@ pub fn render_view( let theme = &editor.theme; let config = editor.config(); + let view_offset = doc.view_offset(view.id); + let text_annotations = view.text_annotations(doc, Some(theme)); let mut decorations = DecorationManager::default(); @@ -119,13 +121,13 @@ pub fn render_view( } let syntax_highlights = - Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); + Self::doc_syntax_highlights(doc, view_offset.anchor, inner.height, theme); let mut overlay_highlights = - Self::empty_highlight_iter(doc, view.offset.anchor, inner.height); + Self::empty_highlight_iter(doc, view_offset.anchor, inner.height); let overlay_syntax_highlights = Self::overlay_syntax_highlights( doc, - view.offset.anchor, + view_offset.anchor, inner.height, &text_annotations, ); @@ -203,7 +205,7 @@ pub fn render_view( surface, inner, doc, - view.offset, + view_offset, &text_annotations, syntax_highlights, overlay_highlights, @@ -259,11 +261,13 @@ pub fn render_rulers( .and_then(|config| config.rulers.as_ref()) .unwrap_or(editor_rulers); + let view_offset = doc.view_offset(view.id); + rulers .iter() // View might be horizontally scrolled, convert from absolute distance // from the 1st column to relative distance from left of viewport - .filter_map(|ruler| ruler.checked_sub(1 + view.offset.horizontal_offset as u16)) + .filter_map(|ruler| ruler.checked_sub(1 + view_offset.horizontal_offset as u16)) .filter(|ruler| ruler < &viewport.width) .map(|ruler| viewport.clip_left(ruler).with_width(1)) .for_each(|area| surface.set_style(area, ruler_theme)) @@ -825,6 +829,7 @@ pub fn highlight_cursorcolumn( let inner_area = view.inner_area(doc); let selection = doc.selection(view.id); + let view_offset = doc.view_offset(view.id); let primary = selection.primary(); let text_format = doc.text_format(viewport.width, None); for range in selection.iter() { @@ -835,11 +840,11 @@ pub fn highlight_cursorcolumn( visual_offset_from_block(text, cursor, cursor, &text_format, text_annotations).0; // if the cursor is horizontally in the view - if col >= view.offset.horizontal_offset - && inner_area.width > (col - view.offset.horizontal_offset) as u16 + if col >= view_offset.horizontal_offset + && inner_area.width > (col - view_offset.horizontal_offset) as u16 { let area = Rect::new( - inner_area.x + (col - view.offset.horizontal_offset) as u16, + inner_area.x + (col - view_offset.horizontal_offset) as u16, view.area.y, 1, view.area.height, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 10f4104ab..6a3e198c1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -83,7 +83,7 @@ pub fn raw_regex_prompt( let (view, doc) = current!(cx.editor); let doc_id = view.doc; let snapshot = doc.selection(view.id).clone(); - let offset_snapshot = view.offset; + let offset_snapshot = doc.view_offset(view.id); let config = cx.editor.config(); let mut prompt = Prompt::new( @@ -95,7 +95,7 @@ pub fn raw_regex_prompt( PromptEvent::Abort => { let (view, doc) = current!(cx.editor); doc.set_selection(view.id, snapshot.clone()); - view.offset = offset_snapshot; + doc.set_view_offset(view.id, offset_snapshot); } PromptEvent::Update | PromptEvent::Validate => { // skip empty input @@ -136,7 +136,7 @@ pub fn raw_regex_prompt( Err(err) => { let (view, doc) = current!(cx.editor); doc.set_selection(view.id, snapshot.clone()); - view.offset = offset_snapshot; + doc.set_view_offset(view.id, offset_snapshot); if event == PromptEvent::Validate { let callback = async move { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f3ace89e5..532f4c344 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -37,9 +37,12 @@ ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction, }; -use crate::editor::Config; -use crate::events::{DocumentDidChange, SelectionDidChange}; -use crate::{DocumentId, Editor, Theme, View, ViewId}; +use crate::{ + editor::Config, + events::{DocumentDidChange, SelectionDidChange}, + view::ViewPosition, + DocumentId, Editor, Theme, View, ViewId, +}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; @@ -130,6 +133,7 @@ pub struct Document { pub(crate) id: DocumentId, text: Rope, selections: HashMap, + view_data: HashMap, /// Inlay hints annotations for the document, by view. /// @@ -265,6 +269,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { .field("selections", &self.selections) .field("inlay_hints_oudated", &self.inlay_hints_oudated) .field("text_annotations", &self.inlay_hints) + .field("view_data", &self.view_data) .field("path", &self.path) .field("encoding", &self.encoding) .field("restore_cursor", &self.restore_cursor) @@ -656,6 +661,7 @@ pub fn from( selections: HashMap::default(), inlay_hints: HashMap::default(), inlay_hints_oudated: false, + view_data: Default::default(), indent_style: DEFAULT_INDENT, line_ending, restore_cursor: false, @@ -1184,12 +1190,14 @@ pub fn reset_selection(&mut self, view_id: ViewId) { self.set_selection(view_id, Selection::single(origin.anchor, origin.head)); } - /// Initializes a new selection for the given view if it does not - /// already have one. + /// Initializes a new selection and view_data for the given view + /// if it does not already have them. pub fn ensure_view_init(&mut self, view_id: ViewId) { if self.selections.get(&view_id).is_none() { self.reset_selection(view_id); } + + self.view_data_mut(view_id); } /// Mark document as recent used for MRU sorting @@ -1235,6 +1243,12 @@ fn apply_impl( .ensure_invariants(self.text.slice(..)); } + for view_data in self.view_data.values_mut() { + view_data.view_position.anchor = transaction + .changes() + .map_pos(view_data.view_position.anchor, Assoc::Before); + } + // if specified, the current selection should instead be replaced by transaction.selection if let Some(selection) = transaction.selection() { self.selections.insert( @@ -1759,6 +1773,28 @@ pub fn selections(&self) -> &HashMap { &self.selections } + fn view_data(&self, view_id: ViewId) -> &ViewData { + self.view_data + .get(&view_id) + .expect("This should only be called after ensure_view_init") + } + + fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData { + self.view_data.entry(view_id).or_default() + } + + pub(crate) fn get_view_offset(&self, view_id: ViewId) -> Option { + Some(self.view_data.get(&view_id)?.view_position) + } + + pub fn view_offset(&self, view_id: ViewId) -> ViewPosition { + self.view_data(view_id).view_position + } + + pub fn set_view_offset(&mut self, view_id: ViewId, new_offset: ViewPosition) { + self.view_data_mut(view_id).view_position = new_offset; + } + pub fn relative_path(&self) -> Option> { self.path .as_deref() @@ -2034,6 +2070,11 @@ pub fn reset_all_inlay_hints(&mut self) { } } +#[derive(Debug, Default)] +pub struct ViewData { + view_position: ViewPosition, +} + #[derive(Clone, Debug)] pub enum FormatterError { SpawningFailed { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cead30d7c..ba7337f22 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,5 +1,4 @@ use crate::{ - align_view, annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig}, document::{ DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, @@ -11,8 +10,7 @@ register::Registers, theme::{self, Theme}, tree::{self, Tree}, - view::ViewPosition, - Align, Document, DocumentId, View, ViewId, + Document, DocumentId, View, ViewId, }; use dap::StackFrame; use helix_vcs::DiffProviderRegistry; @@ -1530,16 +1528,17 @@ fn _refresh(&mut self) { } fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) { + let scrolloff = self.config().scrolloff; let view = self.tree.get_mut(current_view); - view.doc = doc_id; - view.offset = ViewPosition::default(); + view.doc = doc_id; let doc = doc_mut!(self, &doc_id); + doc.ensure_view_init(view.id); view.sync_changes(doc); doc.mark_as_focused(); - align_view(doc, view, Align::Center); + view.ensure_cursor_in_view(doc, scrolloff) } pub fn switch(&mut self, id: DocumentId, action: Action) { @@ -1899,8 +1898,8 @@ pub fn should_close(&self) -> bool { pub fn ensure_cursor_in_view(&mut self, id: ViewId) { let config = self.config(); - let view = self.tree.get_mut(id); - let doc = &self.documents[&view.doc]; + let view = self.tree.get(id); + let doc = doc_mut!(self, &view.doc); view.ensure_cursor_in_view(doc, config.scrolloff) } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 5628c830c..d54b49ef5 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -47,11 +47,12 @@ pub enum Align { Bottom, } -pub fn align_view(doc: &Document, view: &mut View, align: Align) { +pub fn align_view(doc: &mut Document, view: &View, align: Align) { let doc_text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(doc_text); let viewport = view.inner_area(doc); let last_line_height = viewport.height.saturating_sub(1); + let mut view_offset = doc.view_offset(view.id); let relative = match align { Align::Center => last_line_height / 2, @@ -60,15 +61,15 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) { }; let text_fmt = doc.text_format(viewport.width, None); - let annotations = view.text_annotations(doc, None); - (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset( + (view_offset.anchor, view_offset.vertical_offset) = char_idx_at_visual_offset( doc_text, cursor, -(relative as isize), 0, &text_fmt, - &annotations, + &view.text_annotations(doc, None), ); + doc.set_view_offset(view.id, view_offset); } pub use document::Document; diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index af4fdfe4e..fb83c4b86 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -128,7 +128,6 @@ pub struct ViewPosition { #[derive(Clone)] pub struct View { pub id: ViewId, - pub offset: ViewPosition, pub area: Rect, pub doc: DocumentId, pub jumps: JumpList, @@ -173,11 +172,6 @@ pub fn new(doc: DocumentId, gutters: GutterConfig) -> Self { Self { id: ViewId::default(), doc, - offset: ViewPosition { - anchor: 0, - horizontal_offset: 0, - vertical_offset: 0, - }, area: Rect::default(), // will get calculated upon inserting into tree jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel docs_access_history: Vec::new(), @@ -240,9 +234,10 @@ pub fn offset_coords_to_in_view_center( doc: &Document, scrolloff: usize, ) -> Option { + let view_offset = doc.get_view_offset(self.id)?; let doc_text = doc.text().slice(..); let viewport = self.inner_area(doc); - let vertical_viewport_end = self.offset.vertical_offset + viewport.height as usize; + let vertical_viewport_end = view_offset.vertical_offset + viewport.height as usize; let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); @@ -256,7 +251,7 @@ pub fn offset_coords_to_in_view_center( }; let cursor = doc.selection(self.id).primary().cursor(doc_text); - let mut offset = self.offset; + let mut offset = view_offset; let off = visual_offset_from_anchor( doc_text, offset.anchor, @@ -321,22 +316,22 @@ pub fn offset_coords_to_in_view_center( } // if we are not centering return None if view position is unchanged - if !CENTERING && offset == self.offset { + if !CENTERING && offset == view_offset { return None; } Some(offset) } - pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + pub fn ensure_cursor_in_view(&self, doc: &mut Document, scrolloff: usize) { if let Some(offset) = self.offset_coords_to_in_view_center::(doc, scrolloff) { - self.offset = offset; + doc.set_view_offset(self.id, offset); } } - pub fn ensure_cursor_in_view_center(&mut self, doc: &Document, scrolloff: usize) { + pub fn ensure_cursor_in_view_center(&self, doc: &mut Document, scrolloff: usize) { if let Some(offset) = self.offset_coords_to_in_view_center::(doc, scrolloff) { - self.offset = offset; + doc.set_view_offset(self.id, offset); } else { align_view(doc, self, Align::Center); } @@ -354,7 +349,7 @@ pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool { #[inline] pub fn estimate_last_doc_line(&self, doc: &Document) -> usize { let doc_text = doc.text().slice(..); - let line = doc_text.char_to_line(self.offset.anchor.min(doc_text.len_chars())); + let line = doc_text.char_to_line(doc.view_offset(self.id).anchor.min(doc_text.len_chars())); // Saturating subs to make it inclusive zero indexing. (line + self.inner_height()) .min(doc_text.len_lines()) @@ -368,9 +363,10 @@ pub fn last_visual_line(&self, doc: &Document) -> usize { let viewport = self.inner_area(doc); let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); + let view_offset = doc.view_offset(self.id); // last visual line in view is trivial to compute - let visual_height = self.offset.vertical_offset + viewport.height as usize; + let visual_height = doc.view_offset(self.id).vertical_offset + viewport.height as usize; // fast path when the EOF is not visible on the screen, if self.estimate_last_doc_line(doc) < doc_text.len_lines() - 1 { @@ -380,7 +376,7 @@ pub fn last_visual_line(&self, doc: &Document) -> usize { // translate to document line let pos = visual_offset_from_anchor( doc_text, - self.offset.anchor, + view_offset.anchor, usize::MAX, &text_fmt, &annotations, @@ -388,7 +384,7 @@ pub fn last_visual_line(&self, doc: &Document) -> usize { ); match pos { - Ok((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), + Ok((Position { row, .. }, _)) => row.saturating_sub(view_offset.vertical_offset), Err(PosAfterMaxRow) => visual_height.saturating_sub(1), Err(PosBeforeAnchorRow) => 0, } @@ -403,13 +399,15 @@ pub fn screen_coords_at_pos( text: RopeSlice, pos: usize, ) -> Option { + let view_offset = doc.view_offset(self.id); + let viewport = self.inner_area(doc); let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); let mut pos = visual_offset_from_anchor( text, - self.offset.anchor, + view_offset.anchor, pos, &text_fmt, &annotations, @@ -417,14 +415,14 @@ pub fn screen_coords_at_pos( ) .ok()? .0; - if pos.row < self.offset.vertical_offset { + if pos.row < view_offset.vertical_offset { return None; } - pos.row -= self.offset.vertical_offset; + pos.row -= view_offset.vertical_offset; if pos.row >= viewport.height as usize { return None; } - pos.col = pos.col.saturating_sub(self.offset.horizontal_offset); + pos.col = pos.col.saturating_sub(view_offset.horizontal_offset); Some(pos) } @@ -488,7 +486,7 @@ pub fn text_annotations<'a>( doc, cursor, width, - self.offset.horizontal_offset, + doc.view_offset(self.id).horizontal_offset, config, )); } @@ -535,13 +533,14 @@ pub fn text_pos_at_visual_coords( ignore_virtual_text: bool, ) -> Option { let text = doc.text().slice(..); + let view_offset = doc.view_offset(self.id); - let text_row = row as usize + self.offset.vertical_offset; - let text_col = column as usize + self.offset.horizontal_offset; + let text_row = row as usize + view_offset.vertical_offset; + let text_col = column as usize + view_offset.horizontal_offset; let (char_idx, virt_lines) = char_idx_at_visual_offset( text, - self.offset.anchor, + view_offset.anchor, text_row as isize, text_col, &text_fmt, @@ -689,11 +688,12 @@ fn test_text_pos_at_screen_coords() { let mut view = View::new(DocumentId::default(), GutterConfig::default()); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from( + let mut doc = Document::from( rope, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), ); + doc.ensure_view_init(view.id); assert_eq!( view.text_pos_at_screen_coords( @@ -863,11 +863,12 @@ fn test_text_pos_at_screen_coords_without_line_numbers_gutter() { ); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from( + let mut doc = Document::from( rope, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), ); + doc.ensure_view_init(view.id); assert_eq!( view.text_pos_at_screen_coords( &doc, @@ -892,11 +893,12 @@ fn test_text_pos_at_screen_coords_without_any_gutters() { ); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from( + let mut doc = Document::from( rope, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), ); + doc.ensure_view_init(view.id); assert_eq!( view.text_pos_at_screen_coords( &doc, @@ -915,11 +917,12 @@ fn test_text_pos_at_screen_coords_cjk() { let mut view = View::new(DocumentId::default(), GutterConfig::default()); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("Hi! こんにちは皆さん"); - let doc = Document::from( + let mut doc = Document::from( rope, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), ); + doc.ensure_view_init(view.id); assert_eq!( view.text_pos_at_screen_coords( @@ -998,11 +1001,12 @@ fn test_text_pos_at_screen_coords_graphemes() { let mut view = View::new(DocumentId::default(), GutterConfig::default()); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("Hèl̀l̀ò world!"); - let doc = Document::from( + let mut doc = Document::from( rope, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), ); + doc.ensure_view_init(view.id); assert_eq!( view.text_pos_at_screen_coords( From 4c1835504b9bf963db209db28a05820e0cdc745c Mon Sep 17 00:00:00 2001 From: JR Date: Tue, 23 Jul 2024 21:49:22 +0200 Subject: [PATCH 159/200] Add tutor entry about 2-character label jump (#11273) * Add tutor entry about 2-character label jump * Move gw tutor to chapter 9 * Do not explicitely say which labels are shown following gw --- runtime/tutor | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/runtime/tutor b/runtime/tutor index 5e82cf888..f5162c5c7 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -988,6 +988,28 @@ lines. +================================================================= += 9.4 JUMP WITH TWO-CHARACTER LABELS = +================================================================= + + Type gw to enable the 2-character labels. The start of each word + will be replaced by 2 highlighted characters. Type any sequence + of 2 highlighted characters to jump to the corresponding label, + or use ESC to drop the labels. + + The 2-character labels allow to quickly jump to any location + in the viewable selection. + + 1. Move the cursor to the start of the line marked '-->' below. + 2. Press gw to enable the 2-character labels, and then the two + characters that replace the two leters he at the start of + here to jump to the corresponding word. + + --> This is just a simple line of text. + There may be many such lines + But you really want to jump here! + This is fast with the 2-character labels. + ================================================================= = CHAPTER 9 RECAP = ================================================================= @@ -1001,8 +1023,8 @@ lines. * Press Ctrl-i and Ctrl-o to go forward and backward in the jumplist. - - + * Type gw to enable 2-characters labels, and any 2 characters + to jump to the corresponding label, or ESC to drop the labels. From 182b26bebcaa7d36421365473ac5a0eff113e01d Mon Sep 17 00:00:00 2001 From: Rich Robinson Date: Wed, 24 Jul 2024 15:14:46 +0100 Subject: [PATCH 160/200] Fix typos in 2-character label jump Tutor entry (#11298) --- runtime/tutor | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/tutor b/runtime/tutor index f5162c5c7..431d24f8f 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -997,12 +997,12 @@ lines. of 2 highlighted characters to jump to the corresponding label, or use ESC to drop the labels. - The 2-character labels allow to quickly jump to any location + The 2-character labels allow you to quickly jump to any location in the viewable selection. 1. Move the cursor to the start of the line marked '-->' below. 2. Press gw to enable the 2-character labels, and then the two - characters that replace the two leters he at the start of + characters that replace the two letters he at the start of here to jump to the corresponding word. --> This is just a simple line of text. @@ -1023,8 +1023,8 @@ lines. * Press Ctrl-i and Ctrl-o to go forward and backward in the jumplist. - * Type gw to enable 2-characters labels, and any 2 characters - to jump to the corresponding label, or ESC to drop the labels. + * Type gw to enable 2-character labels, and any 2 characters to + jump to the corresponding label, or ESC to drop the labels. From 9d21b8fa85e8ff8bacc073875fc27eb007484f06 Mon Sep 17 00:00:00 2001 From: 1adept <69433209+1adept@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:34:34 +0200 Subject: [PATCH 161/200] just module extension (#11286) Co-authored-by: adept --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 87783b3de..5103cee3c 100644 --- a/languages.toml +++ b/languages.toml @@ -3073,7 +3073,7 @@ source = { git = "https://github.com/lefp/tree-sitter-opencl", rev = "8e1d24a570 [[language]] name = "just" scope = "source.just" -file-types = [{ glob = "justfile" }, { glob = "Justfile" }, { glob = ".justfile" }, { glob = ".Justfile" }] +file-types = ["just", { glob = "justfile" }, { glob = "Justfile" }, { glob = ".justfile" }, { glob = ".Justfile" }] injection-regex = "just" comment-token = "#" indent = { tab-width = 4, unit = " " } From 7c5e5f4e412f65e123fe1e1fbc802229d1a303ae Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Wed, 24 Jul 2024 08:34:20 -0700 Subject: [PATCH 162/200] fix(lsp): `find_completion_range` off-by-one (#11266) --- helix-lsp/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index ec89e1f82..4a27802d3 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -284,10 +284,8 @@ fn find_completion_range(text: RopeSlice, replace_mode: bool, cursor: usize) -> if replace_mode { end += text .chars_at(cursor) - .skip(1) .take_while(|ch| chars::char_is_word(*ch)) - .count() - + 1; + .count(); } (start, end) } From 5d3f05cbe148fce050cab40aa9e718dd8917258c Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Wed, 24 Jul 2024 12:25:00 -0400 Subject: [PATCH 163/200] Document use of filter columns in pickers (#11218) * Document use of filter columns in pickers. Filtering on columns was implemented in #9647. The only documentation I could find on this feature was the PR itself, and the video demo used a different syntax. * Note that column filters are space-separated. * Note that picker filters can be abbreviated. * Specify correct picker in docs. * Clarify picker filter prefix shortenting. Co-authored-by: Michael Davis * Move picker docs to their own section. * Update book/src/pickers.md Co-authored-by: Michael Davis * Improve docs on picker registers, keybinds, and syntax. * Clarify wording around picker queries. Co-authored-by: Michael Davis --------- Co-authored-by: Ryan Roden-Corrent Co-authored-by: Michael Davis --- book/src/SUMMARY.md | 1 + book/src/keymap.md | 2 ++ book/src/pickers.md | 11 +++++++++++ 3 files changed, 14 insertions(+) create mode 100644 book/src/pickers.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 027b885a8..e6be2ebc0 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -10,6 +10,7 @@ # Summary - [Surround](./surround.md) - [Textobjects](./textobjects.md) - [Syntax aware motions](./syntax-aware-motions.md) + - [Pickers](./pickers.md) - [Keymap](./keymap.md) - [Commands](./commands.md) - [Language support](./lang-support.md) diff --git a/book/src/keymap.md b/book/src/keymap.md index 402b83ef7..0e60f2826 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -436,6 +436,8 @@ ## Select / extend mode ## Picker Keys to use within picker. Remapping currently not supported. +See the documentation page on [pickers](./pickers.md) for more info. +[Prompt](#prompt) keybinds also work in pickers, except where they conflict with picker keybinds. | Key | Description | | ----- | ------------- | diff --git a/book/src/pickers.md b/book/src/pickers.md new file mode 100644 index 000000000..993d71466 --- /dev/null +++ b/book/src/pickers.md @@ -0,0 +1,11 @@ +## Using pickers + +Helix has a variety of pickers, which are interactive windows used to select various kinds of items. These include a file picker, global search picker, and more. Most pickers are accessed via keybindings in [space mode](./keymap.md#space-mode). Pickers have their own [keymap](./keymap.md#picker) for navigation. + +### Filtering Picker Results + +Most pickers perform fuzzy matching using [fzf syntax](https://github.com/junegunn/fzf?tab=readme-ov-file#search-syntax). Two exceptions are the global search picker, which uses regex, and the workspace symbol picker, which passes search terms to the LSP. Note that OR operations (`|`) are not currently supported. + +If a picker shows multiple columns, you may apply the filter to a specific column by prefixing the column name with `%`. Column names can be shortened to any prefix, so `%p`, `%pa` or `%pat` all mean the same as `%path`. For example, a query of `helix %p .toml$ !lang` in the global search picker searches for the term "helix" within files with paths ending in ".toml" but not including "lang". + +You can insert the contents of a [register](./registers.md) using `Ctrl-r` followed by a register name. For example, one could insert the currently selected text using `Ctrl-r`-`.`, or the directory of the current file using `Ctrl-r`-`%` followed by `Ctrl-w` to remove the last path section. The global search picker will use the contents of the [search register](./registers.md#default-registers) if you press `Enter` without typing a filter. For example, pressing `*`-`Space-/`-`Enter` will start a global search for the currently selected text. From ef4a4ff3c558afff0917633876f11b586899934d Mon Sep 17 00:00:00 2001 From: Luca Saccarola <96259932+saccarosium@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:25:25 +0200 Subject: [PATCH 164/200] Make bash completion behave normally (#11246) --- contrib/completion/hx.bash | 42 +++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/contrib/completion/hx.bash b/contrib/completion/hx.bash index 62ca029bf..1b102017b 100644 --- a/contrib/completion/hx.bash +++ b/contrib/completion/hx.bash @@ -2,23 +2,31 @@ # Bash completion script for Helix editor _hx() { - # $1 command name - # $2 word being completed - # $3 word preceding + local cur prev languages + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD - 1]}" - case "$3" in - -g | --grammar) - COMPREPLY="$(compgen -W 'fetch build' -- $2)" - ;; - --health) - local languages=$(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g') - COMPREPLY="$(compgen -W """$languages""" -- $2)" - ;; - *) - COMPREPLY="$(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2""")" - ;; - esac + case "$prev" in + -g | --grammar) + COMPREPLY=($(compgen -W 'fetch build' -- "$cur")) + return 0 + ;; + --health) + languages=$(hx --health | tail -n '+7' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g') + COMPREPLY=($(compgen -W """$languages""" -- "$cur")) + return 0 + ;; + esac - local IFS=$'\n' - COMPREPLY=($COMPREPLY) + case "$2" in + -*) + COMPREPLY=($(compgen -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2""")) + return 0 + ;; + *) + COMPREPLY=($(compgen -fd -- """$2""")) + return 0 + ;; + esac } && complete -o filenames -F _hx hx From f34dca797cf696a19466ab2b3f7e1ca6ffa2bcb7 Mon Sep 17 00:00:00 2001 From: karei Date: Thu, 25 Jul 2024 17:12:55 +0300 Subject: [PATCH 165/200] Add support for `jjdescription` files (#11271) --- book/src/generated/lang-support.md | 1 + languages.toml | 13 +++++++++++++ runtime/queries/jjdescription/highlights.scm | 8 ++++++++ 3 files changed, 22 insertions(+) create mode 100644 runtime/queries/jjdescription/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 5afde0972..686dec5e2 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -96,6 +96,7 @@ | java | ✓ | ✓ | ✓ | `jdtls` | | javascript | ✓ | ✓ | ✓ | `typescript-language-server` | | jinja | ✓ | | | | +| jjdescription | ✓ | | | | | jsdoc | ✓ | | | | | json | ✓ | ✓ | ✓ | `vscode-json-language-server` | | json5 | ✓ | | | | diff --git a/languages.toml b/languages.toml index 5103cee3c..55bb7a4fb 100644 --- a/languages.toml +++ b/languages.toml @@ -3203,6 +3203,19 @@ grammar = "jinja2" name = "jinja2" source = { git = "https://github.com/varpeti/tree-sitter-jinja2", rev = "a533cd3c33aea6acb0f9bf9a56f35dcfe6a8eb53" } +[[language]] +name = "jjdescription" +scope = "jj.description" +file-types = [{ glob = "*.jjdescription" }] +comment-token = "JJ:" +indent = { tab-width = 2, unit = " " } +rulers = [51, 73] +text-width = 72 + +[[grammar]] +name = "jjdescription" +source = { git = "https://github.com/kareigu/tree-sitter-jjdescription", rev = "2ddec6cad07b366aee276a608e1daa2c29d3caf2" } + [[grammar]] name = "wren" source = { git = "https://git.sr.ht/~jummit/tree-sitter-wren", rev = "6748694be32f11e7ec6b5faeb1b48ca6156d4e06" } diff --git a/runtime/queries/jjdescription/highlights.scm b/runtime/queries/jjdescription/highlights.scm new file mode 100644 index 000000000..651f1e7f2 --- /dev/null +++ b/runtime/queries/jjdescription/highlights.scm @@ -0,0 +1,8 @@ +(text) @string +(filepath) @string.special.path + +(change type: "A" @diff.plus) +(change type: "D" @diff.minus) +(change type: "M" @diff.delta) + +(comment) @comment From d48b6904f25fbeac5c1626315df42ca8073791b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:41:53 +0200 Subject: [PATCH 166/200] build(deps): bump gix-attributes from 0.22.2 to 0.22.3 (#11318) Bumps [gix-attributes](https://github.com/Byron/gitoxide) from 0.22.2 to 0.22.3. - [Release notes](https://github.com/Byron/gitoxide/releases) - [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md) - [Commits](https://github.com/Byron/gitoxide/compare/gix-attributes-v0.22.2...gix-attributes-v0.22.3) --- updated-dependencies: - dependency-name: gix-attributes dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfb204c06..263ca7d7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,9 +605,9 @@ dependencies = [ [[package]] name = "gix-attributes" -version = "0.22.2" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eefb48f42eac136a4a0023f49a54ec31be1c7a9589ed762c45dcb9b953f7ecc8" +checksum = "e37ce99c7e81288c28b703641b6d5d119aacc45c1a6b247156e6249afa486257" dependencies = [ "bstr", "gix-glob", @@ -819,9 +819,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.16.2" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682bdc43cb3c00dbedfcc366de2a849b582efd8d886215dbad2ea662ec156bb5" +checksum = "fa7df15afa265cc8abe92813cd354d522f1ac06b29ec6dfa163ad320575cb447" dependencies = [ "bitflags 2.6.0", "bstr", @@ -986,9 +986,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.7" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23623cf0f475691a6d943f898c4d0b89f5c1a2a64d0f92bce0e0322ee6528783" +checksum = "8d23d5bbda31344d8abc8de7c075b3cf26e5873feba7c4a15d916bce67382bd9" dependencies = [ "bstr", "gix-trace", @@ -1686,7 +1686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.0", + "windows-targets 0.48.0", ] [[package]] From 5e945c327fa0d0f54fee0a1e9d9b3d55480363bd Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 25 Jul 2024 17:22:35 -0400 Subject: [PATCH 167/200] languages: add mdx to markdown filetypes (#11122) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 55bb7a4fb..4a57a7e58 100644 --- a/languages.toml +++ b/languages.toml @@ -1533,7 +1533,7 @@ source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "568dd name = "markdown" scope = "source.md" injection-regex = "md|markdown" -file-types = ["md", "markdown", "mkd", "mkdn", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }] +file-types = ["md", "markdown", "mdx", "mkd", "mkdn", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }] roots = [".marksman.toml"] language-servers = [ "marksman", "markdown-oxide" ] indent = { tab-width = 2, unit = " " } From 9b65448f53cccf95724839ee08aab2cee1249f3a Mon Sep 17 00:00:00 2001 From: Damir Vandic Date: Thu, 25 Jul 2024 23:23:08 +0200 Subject: [PATCH 168/200] Fix example query in pickers.md (#11322) Co-authored-by: Pascal Kuthe --- book/src/pickers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/pickers.md b/book/src/pickers.md index 993d71466..4149e560b 100644 --- a/book/src/pickers.md +++ b/book/src/pickers.md @@ -6,6 +6,6 @@ ### Filtering Picker Results Most pickers perform fuzzy matching using [fzf syntax](https://github.com/junegunn/fzf?tab=readme-ov-file#search-syntax). Two exceptions are the global search picker, which uses regex, and the workspace symbol picker, which passes search terms to the LSP. Note that OR operations (`|`) are not currently supported. -If a picker shows multiple columns, you may apply the filter to a specific column by prefixing the column name with `%`. Column names can be shortened to any prefix, so `%p`, `%pa` or `%pat` all mean the same as `%path`. For example, a query of `helix %p .toml$ !lang` in the global search picker searches for the term "helix" within files with paths ending in ".toml" but not including "lang". +If a picker shows multiple columns, you may apply the filter to a specific column by prefixing the column name with `%`. Column names can be shortened to any prefix, so `%p`, `%pa` or `%pat` all mean the same as `%path`. For example, a query of `helix %p .toml !lang` in the global search picker searches for the term "helix" within files with paths ending in ".toml" but not including "lang". You can insert the contents of a [register](./registers.md) using `Ctrl-r` followed by a register name. For example, one could insert the currently selected text using `Ctrl-r`-`.`, or the directory of the current file using `Ctrl-r`-`%` followed by `Ctrl-w` to remove the last path section. The global search picker will use the contents of the [search register](./registers.md#default-registers) if you press `Enter` without typing a filter. For example, pressing `*`-`Space-/`-`Enter` will start a global search for the currently selected text. From a1e20a342641241d06c2553a76ee518aca0e35bb Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 26 Jul 2024 02:32:02 -0500 Subject: [PATCH 169/200] Reorganize Document::apply_impl (#11304) These changes are ported from . It's a cleanup of `Document::apply_impl` that uses some early returns to reduce nesting and some reordering of the steps. The early returns bail out of `apply_impl` early if the transaction fails to apply or if the changes are empty (in which case we emit the SelectionDidChange event). It's a somewhat cosmetic refactor that makes the function easier to reason about but it also makes it harder to introduce bugs by mapping positions through empty changesets for example. Co-authored-by: Pascal Kuthe --- helix-view/src/document.rs | 291 +++++++++++++++++++------------------ 1 file changed, 150 insertions(+), 141 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 532f4c344..489555112 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1222,34 +1222,12 @@ fn apply_impl( use helix_core::Assoc; let old_doc = self.text().clone(); + let changes = transaction.changes(); + if !changes.apply(&mut self.text) { + return false; + } - let success = transaction.changes().apply(&mut self.text); - - if success { - if emit_lsp_notification { - helix_event::dispatch(DocumentDidChange { - doc: self, - view: view_id, - old_text: &old_doc, - }); - } - - for selection in self.selections.values_mut() { - *selection = selection - .clone() - // Map through changes - .map(transaction.changes()) - // Ensure all selections across all views still adhere to invariants. - .ensure_invariants(self.text.slice(..)); - } - - for view_data in self.view_data.values_mut() { - view_data.view_position.anchor = transaction - .changes() - .map_pos(view_data.view_position.anchor, Assoc::Before); - } - - // if specified, the current selection should instead be replaced by transaction.selection + if changes.is_empty() { if let Some(selection) = transaction.selection() { self.selections.insert( view_id, @@ -1260,129 +1238,160 @@ fn apply_impl( view: view_id, }); } - - self.modified_since_accessed = true; + return true; } - if !transaction.changes().is_empty() { - self.version += 1; - // start computing the diff in parallel - if let Some(diff_handle) = &self.diff_handle { - diff_handle.update_document(self.text.clone(), false); - } + self.modified_since_accessed = true; + self.version += 1; - // generate revert to savepoint - if !self.savepoints.is_empty() { - let revert = transaction.invert(&old_doc); - self.savepoints - .retain_mut(|save_point| match save_point.upgrade() { - Some(savepoint) => { - let mut revert_to_savepoint = savepoint.revert.lock(); - *revert_to_savepoint = - revert.clone().compose(mem::take(&mut revert_to_savepoint)); - true - } - None => false, - }) - } + for selection in self.selections.values_mut() { + *selection = selection + .clone() + // Map through changes + .map(transaction.changes()) + // Ensure all selections across all views still adhere to invariants. + .ensure_invariants(self.text.slice(..)); + } - // update tree-sitter syntax tree - if let Some(syntax) = &mut self.syntax { - // TODO: no unwrap - let res = syntax.update( - old_doc.slice(..), - self.text.slice(..), - transaction.changes(), - ); - if res.is_err() { - log::error!("TS parser failed, disabling TS for the current buffer: {res:?}"); - self.syntax = None; - } - } + for view_data in self.view_data.values_mut() { + view_data.view_position.anchor = transaction + .changes() + .map_pos(view_data.view_position.anchor, Assoc::Before); + } - let changes = transaction.changes(); - - // map diagnostics over changes too - changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| { - let assoc = if diagnostic.starts_at_word { - Assoc::BeforeWord - } else { - Assoc::After - }; - (&mut diagnostic.range.start, assoc) - })); - changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| { - if diagnostic.zero_width { - // for zero width diagnostics treat the diagnostic as a point - // rather than a range - return None; - } - let assoc = if diagnostic.ends_at_word { - Assoc::AfterWord - } else { - Assoc::Before - }; - Some((&mut diagnostic.range.end, assoc)) - })); - self.diagnostics.retain_mut(|diagnostic| { - if diagnostic.zero_width { - diagnostic.range.end = diagnostic.range.start - } else if diagnostic.range.start >= diagnostic.range.end { - return false; - } - diagnostic.line = self.text.char_to_line(diagnostic.range.start); - true - }); - - self.diagnostics.sort_by_key(|diagnostic| { - (diagnostic.range, diagnostic.severity, diagnostic.provider) - }); - - // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place - let apply_inlay_hint_changes = |annotations: &mut Vec| { - changes.update_positions( - annotations - .iter_mut() - .map(|annotation| (&mut annotation.char_idx, Assoc::After)), - ); - }; - - self.inlay_hints_oudated = true; - for text_annotation in self.inlay_hints.values_mut() { - let DocumentInlayHints { - id: _, - type_inlay_hints, - parameter_inlay_hints, - other_inlay_hints, - padding_before_inlay_hints, - padding_after_inlay_hints, - } = text_annotation; - - apply_inlay_hint_changes(padding_before_inlay_hints); - apply_inlay_hint_changes(type_inlay_hints); - apply_inlay_hint_changes(parameter_inlay_hints); - apply_inlay_hint_changes(other_inlay_hints); - apply_inlay_hint_changes(padding_after_inlay_hints); - } - - if emit_lsp_notification { - // TODO: move to hook - // emit lsp notification - for language_server in self.language_servers() { - let notify = language_server.text_document_did_change( - self.versioned_identifier(), - &old_doc, - self.text(), - changes, - ); - - if let Some(notify) = notify { - tokio::spawn(notify); + // generate revert to savepoint + if !self.savepoints.is_empty() { + let revert = transaction.invert(&old_doc); + self.savepoints + .retain_mut(|save_point| match save_point.upgrade() { + Some(savepoint) => { + let mut revert_to_savepoint = savepoint.revert.lock(); + *revert_to_savepoint = + revert.clone().compose(mem::take(&mut revert_to_savepoint)); + true } + None => false, + }) + } + + // update tree-sitter syntax tree + if let Some(syntax) = &mut self.syntax { + // TODO: no unwrap + let res = syntax.update( + old_doc.slice(..), + self.text.slice(..), + transaction.changes(), + ); + if res.is_err() { + log::error!("TS parser failed, disabling TS for the current buffer: {res:?}"); + self.syntax = None; + } + } + + // TODO: all of that should likely just be hooks + // start computing the diff in parallel + if let Some(diff_handle) = &self.diff_handle { + diff_handle.update_document(self.text.clone(), false); + } + + // map diagnostics over changes too + changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| { + let assoc = if diagnostic.starts_at_word { + Assoc::BeforeWord + } else { + Assoc::After + }; + (&mut diagnostic.range.start, assoc) + })); + changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| { + if diagnostic.zero_width { + // for zero width diagnostics treat the diagnostic as a point + // rather than a range + return None; + } + let assoc = if diagnostic.ends_at_word { + Assoc::AfterWord + } else { + Assoc::Before + }; + Some((&mut diagnostic.range.end, assoc)) + })); + self.diagnostics.retain_mut(|diagnostic| { + if diagnostic.zero_width { + diagnostic.range.end = diagnostic.range.start + } else if diagnostic.range.start >= diagnostic.range.end { + return false; + } + diagnostic.line = self.text.char_to_line(diagnostic.range.start); + true + }); + + self.diagnostics + .sort_by_key(|diagnostic| (diagnostic.range, diagnostic.severity, diagnostic.provider)); + + // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place + let apply_inlay_hint_changes = |annotations: &mut Vec| { + changes.update_positions( + annotations + .iter_mut() + .map(|annotation| (&mut annotation.char_idx, Assoc::After)), + ); + }; + + self.inlay_hints_oudated = true; + for text_annotation in self.inlay_hints.values_mut() { + let DocumentInlayHints { + id: _, + type_inlay_hints, + parameter_inlay_hints, + other_inlay_hints, + padding_before_inlay_hints, + padding_after_inlay_hints, + } = text_annotation; + + apply_inlay_hint_changes(padding_before_inlay_hints); + apply_inlay_hint_changes(type_inlay_hints); + apply_inlay_hint_changes(parameter_inlay_hints); + apply_inlay_hint_changes(other_inlay_hints); + apply_inlay_hint_changes(padding_after_inlay_hints); + } + + helix_event::dispatch(DocumentDidChange { + doc: self, + view: view_id, + old_text: &old_doc, + }); + + // if specified, the current selection should instead be replaced by transaction.selection + if let Some(selection) = transaction.selection() { + self.selections.insert( + view_id, + selection.clone().ensure_invariants(self.text.slice(..)), + ); + helix_event::dispatch(SelectionDidChange { + doc: self, + view: view_id, + }); + } + + if emit_lsp_notification { + // TODO: move to hook + // emit lsp notification + for language_server in self.language_servers() { + let notify = language_server.text_document_did_change( + self.versioned_identifier(), + &old_doc, + self.text(), + changes, + ); + + if let Some(notify) = notify { + tokio::spawn(notify); } } } - success + + true } fn apply_inner( From 229784ccc7833a52bc88c7a8b60ecb1d56303593 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 26 Jul 2024 17:20:33 +0200 Subject: [PATCH 170/200] Improve scrolloff behavior (#11323) * Allow perfect centering of cursor * Fix horizontal scrolloff * Fix copypasta in comment --- helix-view/src/view.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index fb83c4b86..a229f01ea 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -241,13 +241,23 @@ pub fn offset_coords_to_in_view_center( let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); - // - 1 so we have at least one gap in the middle. - // a height of 6 with padding of 3 on each side will keep shifting the view back and forth - // as we type - let scrolloff = if CENTERING { - 0 + let (scrolloff_top, scrolloff_bottom) = if CENTERING { + (0, 0) } else { - scrolloff.min(viewport.height.saturating_sub(1) as usize / 2) + ( + // - 1 from the top so we have at least one gap in the middle. + scrolloff.min(viewport.height.saturating_sub(1) as usize / 2), + scrolloff.min(viewport.height as usize / 2), + ) + }; + let (scrolloff_left, scrolloff_right) = if CENTERING { + (0, 0) + } else { + ( + // - 1 from the left so we have at least one gap in the middle. + scrolloff.min(viewport.width.saturating_sub(1) as usize / 2), + scrolloff.min(viewport.width as usize / 2), + ) }; let cursor = doc.selection(self.id).primary().cursor(doc_text); @@ -262,14 +272,14 @@ pub fn offset_coords_to_in_view_center( ); let (new_anchor, at_top) = match off { - Ok((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { + Ok((visual_pos, _)) if visual_pos.row < scrolloff_top + offset.vertical_offset => { if CENTERING { // cursor out of view return None; } (true, true) } - Ok((visual_pos, _)) if visual_pos.row + scrolloff >= vertical_viewport_end => { + Ok((visual_pos, _)) if visual_pos.row + scrolloff_bottom >= vertical_viewport_end => { (true, false) } Ok((_, _)) => (false, false), @@ -280,9 +290,9 @@ pub fn offset_coords_to_in_view_center( if new_anchor { let v_off = if at_top { - scrolloff as isize + scrolloff_top as isize } else { - viewport.height as isize - scrolloff as isize - 1 + viewport.height as isize - scrolloff_bottom as isize - 1 }; (offset.anchor, offset.vertical_offset) = char_idx_at_visual_offset(doc_text, cursor, -v_off, 0, &text_fmt, &annotations); @@ -306,12 +316,12 @@ pub fn offset_coords_to_in_view_center( .col; let last_col = offset.horizontal_offset + viewport.width.saturating_sub(1) as usize; - if col > last_col.saturating_sub(scrolloff) { + if col > last_col.saturating_sub(scrolloff_right) { // scroll right - offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff)) - } else if col < offset.horizontal_offset + scrolloff { + offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff_right)) + } else if col < offset.horizontal_offset + scrolloff_left { // scroll left - offset.horizontal_offset = col.saturating_sub(scrolloff) + offset.horizontal_offset = col.saturating_sub(scrolloff_left) }; } From 0813147b97b8d7538704e167bf2463d4588ba7ee Mon Sep 17 00:00:00 2001 From: Nikolay Minaev Date: Sat, 27 Jul 2024 19:47:23 +0400 Subject: [PATCH 171/200] Use fs' mtime to avoid saving problem on out-of-synced network fs (#11142) In the case of network file systems, if the server time is ahead of the local system time, then helix could annoy with messages that the file has already been modified by another application. --- helix-term/src/application.rs | 2 +- helix-view/src/document.rs | 36 ++++++++++++++++++++++++++++++----- helix-view/src/editor.rs | 2 +- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 60bc5b7cf..7da96b08a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -575,7 +575,7 @@ pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult doc_save_event.revision ); - doc.set_last_saved_revision(doc_save_event.revision); + doc.set_last_saved_revision(doc_save_event.revision, doc_save_event.save_time); let lines = doc_save_event.text.len_lines(); let bytes = doc_save_event.text.len_bytes(); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 489555112..346f04ed8 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -106,6 +106,7 @@ fn serialize(&self, serializer: S) -> Result #[derive(Debug, Clone)] pub struct DocumentSavedEvent { pub revision: usize, + pub save_time: SystemTime, pub doc_id: DocumentId, pub path: PathBuf, pub text: Rope, @@ -965,6 +966,11 @@ impl Future> + 'static + Send } .await; + let save_time = match fs::metadata(&write_path).await { + Ok(metadata) => metadata.modified().map_or(SystemTime::now(), |mtime| mtime), + Err(_) => SystemTime::now(), + }; + if let Some(backup) = backup { if write_result.is_err() { // restore backup @@ -987,6 +993,7 @@ impl Future> + 'static + Send let event = DocumentSavedEvent { revision: current_rev, + save_time, doc_id, path, text: text.clone(), @@ -1045,6 +1052,26 @@ pub fn detect_indent_and_line_ending(&mut self) { } } + pub fn pickup_last_saved_time(&mut self) { + self.last_saved_time = match self.path.as_mut().unwrap().metadata() { + Ok(metadata) => match metadata.modified() { + Ok(mtime) => mtime, + Err(_) => { + log::error!( + "Use a system time instead of fs' mtime not supported on this platform" + ); + SystemTime::now() + } + }, + Err(e) => { + log::error!( + "Use a system time instead of fs' mtime: failed to file's metadata: {e}" + ); + SystemTime::now() + } + }; + } + // Detect if the file is readonly and change the readonly field if necessary (unix only) pub fn detect_readonly(&mut self) { // Allows setting the flag for files the user cannot modify, like root files @@ -1082,9 +1109,7 @@ pub fn reload( self.apply(&transaction, view.id); self.append_changes_to_history(view); self.reset_modified(); - - self.last_saved_time = SystemTime::now(); - + self.pickup_last_saved_time(); self.detect_indent_and_line_ending(); match provider_registry.get_diff_base(&path) { @@ -1123,6 +1148,7 @@ pub fn set_path(&mut self, path: Option<&Path>) { self.path = path; self.detect_readonly(); + self.pickup_last_saved_time(); } /// Set the programming language for the file and load associated data (e.g. highlighting) @@ -1603,7 +1629,7 @@ pub fn reset_modified(&mut self) { } /// Set the document's latest saved revision to the given one. - pub fn set_last_saved_revision(&mut self, rev: usize) { + pub fn set_last_saved_revision(&mut self, rev: usize, save_time: SystemTime) { log::debug!( "doc {} revision updated {} -> {}", self.id, @@ -1611,7 +1637,7 @@ pub fn set_last_saved_revision(&mut self, rev: usize) { rev ); self.last_saved_revision = rev; - self.last_saved_time = SystemTime::now(); + self.last_saved_time = save_time; } /// Get the document's latest saved revision. diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ba7337f22..4249db1d5 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2083,7 +2083,7 @@ pub async fn flush_writes(&mut self) -> anyhow::Result<()> { }; let doc = doc_mut!(self, &save_event.doc_id); - doc.set_last_saved_revision(save_event.revision); + doc.set_last_saved_revision(save_event.revision, save_event.save_time); } } From e5372b04a15701d1f2d6f06ceb52be6befe7d27f Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sat, 27 Jul 2024 13:21:52 -0400 Subject: [PATCH 172/200] Fix writing hardlinks (#11340) * don't use backup files with hardlinks * check if the inodes remain the same in the test * move funcs to faccess and use AsRawHandle * use a copy as a backup for hardlinks * delete backup after copy --- Cargo.lock | 1 + helix-stdx/src/faccess.rs | 27 +++++++++++++-- helix-term/Cargo.toml | 1 + helix-term/tests/test/commands/write.rs | 34 +++++++++++++++++++ helix-view/src/document.rs | 45 ++++++++++++++++++++----- 5 files changed, 96 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 263ca7d7e..c54e481e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1455,6 +1455,7 @@ dependencies = [ "once_cell", "open", "pulldown-cmark", + "same-file", "serde", "serde_json", "signal-hook", diff --git a/helix-stdx/src/faccess.rs b/helix-stdx/src/faccess.rs index 0270c1f8a..c69571b68 100644 --- a/helix-stdx/src/faccess.rs +++ b/helix-stdx/src/faccess.rs @@ -74,6 +74,11 @@ pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> { Ok(()) } + + pub fn hardlink_count(p: &Path) -> std::io::Result { + let metadata = p.metadata()?; + Ok(metadata.nlink()) + } } // Licensed under MIT from faccess except for `chown`, `copy_metadata` and `is_acl_inherited` @@ -94,8 +99,8 @@ mod imp { SID_IDENTIFIER_AUTHORITY, TOKEN_DUPLICATE, TOKEN_QUERY, }; use windows_sys::Win32::Storage::FileSystem::{ - FILE_ACCESS_RIGHTS, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, - FILE_GENERIC_WRITE, + GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_ACCESS_RIGHTS, + FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, }; use windows_sys::Win32::System::Threading::{GetCurrentThread, OpenThreadToken}; @@ -103,7 +108,7 @@ mod imp { use std::ffi::c_void; - use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt}; + use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt, io::AsRawHandle}; struct SecurityDescriptor { sd: PSECURITY_DESCRIPTOR, @@ -411,6 +416,18 @@ pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> { Ok(()) } + + pub fn hardlink_count(p: &Path) -> std::io::Result { + let file = std::fs::File::open(p)?; + let handle = file.as_raw_handle() as isize; + let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() }; + + if unsafe { GetFileInformationByHandle(handle, &mut info) } == 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(info.nNumberOfLinks as u64) + } + } } // Licensed under MIT from faccess except for `copy_metadata` @@ -457,3 +474,7 @@ pub fn readonly(p: &Path) -> bool { pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> { imp::copy_metadata(from, to) } + +pub fn hardlink_count(p: &Path) -> io::Result { + imp::hardlink_count(p) +} diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index cc90396e4..4887b0a27 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -86,3 +86,4 @@ helix-loader = { path = "../helix-loader" } smallvec = "1.13" indoc = "2.0.5" tempfile = "3.10.1" +same-file = "1.0.1" diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index 4db98a046..01824f171 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -649,6 +649,40 @@ async fn test_symlink_write_relative() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_hardlink_write() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + + let mut file = tempfile::NamedTempFile::new_in(&dir)?; + let hardlink_path = dir.path().join("linked"); + std::fs::hard_link(file.path(), &hardlink_path)?; + + let mut app = helpers::AppBuilder::new() + .with_file(&hardlink_path, None) + .build()?; + + test_key_sequence( + &mut app, + Some("ithe gostak distims the doshes:w"), + None, + false, + ) + .await?; + + reload_file(&mut file).unwrap(); + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + + assert_eq!( + LineFeedHandling::Native.apply("the gostak distims the doshes"), + file_content + ); + assert!(helix_stdx::faccess::hardlink_count(&hardlink_path)? > 1); + assert!(same_file::is_same_file(file.path(), &hardlink_path)?); + + Ok(()) +} + async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 346f04ed8..885f33e8a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -934,6 +934,9 @@ impl Future> + 'static + Send "Path is read only" )); } + + // Assume it is a hardlink to prevent data loss if the metadata cant be read (e.g. on certain Windows configurations) + let is_hardlink = helix_stdx::faccess::hardlink_count(&write_path).unwrap_or(2) > 1; let backup = if path.exists() { let path_ = write_path.clone(); // hacks: we use tempfile to handle the complex task of creating @@ -942,14 +945,22 @@ impl Future> + 'static + Send // since the path doesn't exist yet, we just want // the path tokio::task::spawn_blocking(move || -> Option { - tempfile::Builder::new() - .prefix(path_.file_name()?) - .suffix(".bck") - .make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup)) - .ok()? - .into_temp_path() - .keep() - .ok() + let mut builder = tempfile::Builder::new(); + builder.prefix(path_.file_name()?).suffix(".bck"); + + let backup_path = if is_hardlink { + builder + .make_in(path_.parent()?, |backup| std::fs::copy(&path_, backup)) + .ok()? + .into_temp_path() + } else { + builder + .make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup)) + .ok()? + .into_temp_path() + }; + + backup_path.keep().ok() }) .await .ok() @@ -972,7 +983,23 @@ impl Future> + 'static + Send }; if let Some(backup) = backup { - if write_result.is_err() { + if is_hardlink { + let mut delete = true; + if write_result.is_err() { + // Restore backup + let _ = tokio::fs::copy(&backup, &write_path).await.map_err(|e| { + delete = false; + log::error!("Failed to restore backup on write failure: {e}") + }); + } + + if delete { + // Delete backup + let _ = tokio::fs::remove_file(backup) + .await + .map_err(|e| log::error!("Failed to remove backup file on write: {e}")); + } + } else if write_result.is_err() { // restore backup let _ = tokio::fs::rename(&backup, &write_path) .await From 2824e692a7f8cf49491d8a749195c6a3c142a1ea Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sat, 27 Jul 2024 18:32:22 +0200 Subject: [PATCH 173/200] lock unicode width Cargo automatically pumbs the patch version when installed with `cargo install` without the locked flag which creates weird rendering artifacts --- helix-core/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 392b4a4ca..98f7022d8 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -23,7 +23,11 @@ ropey = { version = "1.6.1", default-features = false, features = ["simd"] } smallvec = "1.13" smartstring = "1.0.1" unicode-segmentation = "1.11" -unicode-width = "0.1" +# unicode-width is changing width definitions +# that both break our logic and disagree with common +# width definitions in terminals, we need to replace it. +# For now lets lock the version to avoid +unicode-width = "=0.1.12" unicode-general-category = "0.6" slotmap.workspace = true tree-sitter.workspace = true From 30fb63cc3df96a0c79820dd2980a570cce11c8c3 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sat, 27 Jul 2024 18:59:36 +0200 Subject: [PATCH 174/200] Update helix-core/Cargo.toml Co-authored-by: Michael Davis --- helix-core/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 98f7022d8..6608b69c8 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -26,7 +26,8 @@ unicode-segmentation = "1.11" # unicode-width is changing width definitions # that both break our logic and disagree with common # width definitions in terminals, we need to replace it. -# For now lets lock the version to avoid +# For now lets lock the version to avoid rendering glitches +# when installing without `--locked` unicode-width = "=0.1.12" unicode-general-category = "0.6" slotmap.workspace = true From face6a3268290a2b388a7d72ae520c2197a596bb Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 27 Jul 2024 22:34:41 -0400 Subject: [PATCH 175/200] Disable hard link integration test on Android Non-rooted Android typically doesn't have permission to use hard links at all, so this test fails on Android. --- helix-term/tests/test/commands/write.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index 01824f171..aba101e9f 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -650,6 +650,7 @@ async fn test_symlink_write_relative() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread")] +#[cfg(not(target_os = "android"))] async fn test_hardlink_write() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; From 59429e18d638103691f5d241f9c81d3ba2da3ec9 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Jul 2024 15:04:16 -0400 Subject: [PATCH 176/200] Lower log level for message about removing clients from the registry Servers stopped with `:lsp-stop` will show this message when the server exits. If the client isn't in the registry there isn't any work to do to remove it so this branch is benign. --- helix-lsp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 4a27802d3..6127de52e 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -675,7 +675,7 @@ pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc> { pub fn remove_by_id(&mut self, id: LanguageServerId) { let Some(client) = self.inner.remove(id) else { - log::error!("client was already removed"); + log::debug!("client was already removed"); return; }; self.file_event_handler.remove_client(id); From ae72a1dc42b64c322452cb1116f154b9ff174f58 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Jul 2024 15:54:50 -0400 Subject: [PATCH 177/200] Tombstone LSP clients stopped with :lsp-stop We use the empty vec in `inner_by_name` as a tombstone value. When the vec is empty `get` should not automatically restart the server. --- helix-lsp/src/lib.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 6127de52e..8e423e1c3 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -735,6 +735,11 @@ pub fn restart( .iter() .filter_map(|LanguageServerFeatures { name, .. }| { 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()); @@ -763,8 +768,13 @@ pub fn restart( } pub fn stop(&mut self, name: &str) { - if let Some(clients) = self.inner_by_name.remove(name) { - for client in clients { + 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(..) { self.file_event_handler.remove_client(client.id()); self.inner.remove(client.id()); tokio::spawn(async move { @@ -784,6 +794,14 @@ pub fn get<'a>( 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)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) }) { From 6eae8461978a5fd84387d7bb7b4021e2fa48fa31 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Sun, 28 Jul 2024 06:54:10 -0700 Subject: [PATCH 178/200] feat(languages): update `just` grammar and queries (#11306) * feat(languages): update `just` grammar and queries Bump the * refactor(syntax): inject shebang by id not name --------- Co-authored-by: Trevor Gross --- helix-core/src/syntax.rs | 7 +- languages.toml | 2 +- runtime/queries/just/folds.scm | 14 ++- runtime/queries/just/highlights.scm | 160 +++++++++++++++++++++++---- runtime/queries/just/indents.scm | 12 +- runtime/queries/just/injections.scm | 92 +++++++++++++-- runtime/queries/just/locals.scm | 50 +++++++-- runtime/queries/just/textobjects.scm | 54 ++------- 8 files changed, 296 insertions(+), 95 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 93f618c09..ab6bcc1fb 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1025,9 +1025,10 @@ pub fn language_configuration_for_injection_string( match capture { InjectionLanguageMarker::Name(string) => self.language_config_for_name(string), InjectionLanguageMarker::Filename(file) => self.language_config_for_file_name(file), - InjectionLanguageMarker::Shebang(shebang) => { - self.language_config_for_language_id(shebang) - } + InjectionLanguageMarker::Shebang(shebang) => self + .language_config_ids_by_shebang + .get(shebang) + .and_then(|&id| self.language_configs.get(id).cloned()), } } diff --git a/languages.toml b/languages.toml index 4a57a7e58..d51a18216 100644 --- a/languages.toml +++ b/languages.toml @@ -3082,7 +3082,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "just" -source = { git = "https://github.com/IndianBoy42/tree-sitter-just", rev = "8af0aab79854aaf25b620a52c39485849922f766" } +source = { git = "https://github.com/IndianBoy42/tree-sitter-just", rev = "379fbe36d1e441bc9414ea050ad0c85c9d6935ea" } [[language]] name = "gn" diff --git a/runtime/queries/just/folds.scm b/runtime/queries/just/folds.scm index 6fc68fbd4..77079fd4f 100644 --- a/runtime/queries/just/folds.scm +++ b/runtime/queries/just/folds.scm @@ -1,4 +1,10 @@ -(body) @fold -(recipe) @fold -(interpolation) @fold -(item (_) @fold) +; From + +; Define collapse points + +([ + (recipe) + (string) + (external_command) +] @fold + (#trim! @fold)) diff --git a/runtime/queries/just/highlights.scm b/runtime/queries/just/highlights.scm index 1026f654f..d5e5cc191 100644 --- a/runtime/queries/just/highlights.scm +++ b/runtime/queries/just/highlights.scm @@ -1,33 +1,149 @@ -(assignment (NAME) @variable) -(alias (NAME) @variable) -(value (NAME) @variable) -(parameter (NAME) @variable) -(setting (NAME) @keyword) -(setting "shell" @keyword) +; From -(call (NAME) @function) -(dependency (NAME) @function) -(depcall (NAME) @function) -(recipeheader (NAME) @function) +; This file specifies how matched syntax patterns should be highlighted -(depcall (expression) @variable.parameter) -(parameter) @variable.parameter -(variadic_parameters) @variable.parameter +[ + "export" + "import" +] @keyword.control.import -["if" "else"] @keyword.control.conditional +"mod" @keyword.directive -(string) @string +[ + "alias" + "set" + "shell" +] @keyword -(boolean ["true" "false"]) @constant.builtin.boolean +[ + "if" + "else" +] @keyword.control.conditional -(comment) @comment +; Variables -; (interpolation) @string +(value + (identifier) @variable) -(shebang interpreter:(TEXT) @keyword ) @comment +(alias + left: (identifier) @variable) -["export" "alias" "set"] @keyword +(assignment + left: (identifier) @variable) -["@" "==" "!=" "+" ":="] @operator +; Functions -[ "(" ")" "[" "]" "{{" "}}" "{" "}"] @punctuation.bracket +(recipe_header + name: (identifier) @function) + +(dependency + name: (identifier) @function) + +(dependency_expression + name: (identifier) @function) + +(function_call + name: (identifier) @function) + +; Parameters + +(parameter + name: (identifier) @variable.parameter) + +; Namespaces + +(module + name: (identifier) @namespace) + +; Operators + +[ + ":=" + "?" + "==" + "!=" + "=~" + "@" + "=" + "$" + "*" + "+" + "&&" + "@-" + "-@" + "-" + "/" + ":" +] @operator + +; Punctuation + +"," @punctuation.delimiter + +[ + "{" + "}" + "[" + "]" + "(" + ")" + "{{" + "}}" +] @punctuation.bracket + +[ "`" "```" ] @punctuation.special + +; Literals + +(boolean) @constant.builtin.boolean + +[ + (string) + (external_command) +] @string + +(escape_sequence) @constant.character.escape + +; Comments + +(comment) @comment.line + +(shebang) @keyword.directive + +; highlight known settings (filtering does not always work) +(setting + left: (identifier) @keyword + (#any-of? @keyword + "allow-duplicate-recipes" + "dotenv-filename" + "dotenv-load" + "dotenv-path" + "export" + "fallback" + "ignore-comments" + "positional-arguments" + "shell" + "tempdi" + "windows-powershell" + "windows-shell")) + +; highlight known attributes (filtering does not always work) +(attribute + (identifier) @attribute + (#any-of? @attribute + "private" + "allow-duplicate-recipes" + "dotenv-filename" + "dotenv-load" + "dotenv-path" + "export" + "fallback" + "ignore-comments" + "positional-arguments" + "shell" + "tempdi" + "windows-powershell" + "windows-shell")) + +; Numbers are part of the syntax tree, even if disallowed +(numeric_error) @error diff --git a/runtime/queries/just/indents.scm b/runtime/queries/just/indents.scm index 3db597466..7cfca3d7e 100644 --- a/runtime/queries/just/indents.scm +++ b/runtime/queries/just/indents.scm @@ -1,3 +1,11 @@ +; From +; +; This query specifies how to auto-indent logical blocks. +; +; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html + [ - (recipe_body) -] @indent + (recipe) + (string) + (external_command) +] @indent @extend diff --git a/runtime/queries/just/injections.scm b/runtime/queries/just/injections.scm index cae1035ae..54393059b 100644 --- a/runtime/queries/just/injections.scm +++ b/runtime/queries/just/injections.scm @@ -1,16 +1,84 @@ +; From +; +; Specify nested languages that live within a `justfile` + +; ================ Always applicable ================ + ((comment) @injection.content - (#set! injection.language "comment")) + (#set! injection.language "comment")) -(shebang_recipe - (shebang - interpreter:(TEXT) @injection.language) - (shebang_body) @injection.content -) +; Highlight the RHS of `=~` as regex +((regex_literal + (_) @injection.content) + (#set! injection.language "regex")) -(source_file - (item (setting lang:(NAME) @injection.language)) - (item (recipe (body (recipe_body) @injection.content))) -) +; ================ Global defaults ================ -; ((interpolation (expression) @injection.content) -; (#set! injection.language "just")) +; Default everything to be bash +(recipe_body + !shebang + (#set! injection.language "bash") + (#set! injection.include-children)) @injection.content + +(external_command + (command_body) @injection.content + (#set! injection.language "bash")) + +; ================ Global language specified ================ +; Global language is set with something like one of the following: +; +; set shell := ["bash", "-c", ...] +; set shell := ["pwsh.exe"] +; +; We can extract the first item of the array, but we can't extract the language +; name from the string with something like regex. So instead we special case +; two things: powershell, which is likely to come with a `.exe` attachment that +; we need to strip, and everything else which hopefully has no extension. We +; separate this with a `#match?`. +; +; Unfortunately, there also isn't a way to allow arbitrary nesting or +; alternatively set "global" capture variables. So we can set this for item- +; level external commands, but not for e.g. external commands within an +; expression without getting _really_ annoying. Should at least look fine since +; they default to bash. Limitations... +; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. + +(source_file + (setting "shell" ":=" "[" (string) @_langstr + (#match? @_langstr ".*(powershell|pwsh|cmd).*") + (#set! injection.language "powershell")) + [ + (recipe + (recipe_body + !shebang + (#set! injection.include-children)) @injection.content) + + (assignment + (expression + (value + (external_command + (command_body) @injection.content)))) + ]) + +(source_file + (setting "shell" ":=" "[" (string) @injection.language + (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) + [ + (recipe + (recipe_body + !shebang + (#set! injection.include-children)) @injection.content) + + (assignment + (expression + (value + (external_command + (command_body) @injection.content)))) + ]) + +; ================ Recipe language specified - Helix only ================ + +; Set highlighting for recipes that specify a language using builtin shebang matching +(recipe_body + (shebang) @injection.shebang + (#set! injection.include-children)) @injection.content diff --git a/runtime/queries/just/locals.scm b/runtime/queries/just/locals.scm index 18e162a97..827148a17 100644 --- a/runtime/queries/just/locals.scm +++ b/runtime/queries/just/locals.scm @@ -1,10 +1,42 @@ -(assignment (NAME) @local.definition) -(alias left:(NAME) @local.definition) -(alias right:(NAME) @local.reference) -(value (NAME) @local.reference) -(parameter (NAME) @local.definition) +; From +; +; This file tells us about the scope of variables so e.g. local +; variables override global functions with the same name -(call (NAME) @local.reference) -(dependency (NAME) @local.reference) -(depcall (NAME) @local.reference) -(recipeheader (NAME) @local.definition) +; Scope + +(recipe) @local.scope + +; Definitions + +(alias + left: (identifier) @local.definition) + +(assignment + left: (identifier) @local.definition) + +(module + name: (identifier) @local.definition) + +(parameter + name: (identifier) @local.definition) + +(recipe_header + name: (identifier) @local.definition) + +; References + +(alias + right: (identifier) @local.reference) + +(function_call + name: (identifier) @local.reference) + +(dependency + name: (identifier) @local.reference) + +(dependency_expression + name: (identifier) @local.reference) + +(value + (identifier) @local.reference) diff --git a/runtime/queries/just/textobjects.scm b/runtime/queries/just/textobjects.scm index 4be379587..bb604178e 100644 --- a/runtime/queries/just/textobjects.scm +++ b/runtime/queries/just/textobjects.scm @@ -1,48 +1,18 @@ -(body) @function.inside -(recipe) @function.around -(expression - if:(expression) @function.inside -) -(expression - else:(expression) @function.inside -) -(interpolation (expression) @function.inside) @function.around -(settinglist (stringlist) @function.inside) @function.around +; From +; +; Specify how to navigate around logical blocks in code -(call (NAME) @class.inside) @class.around -(dependency (NAME) @class.inside) @class.around -(depcall (NAME) @class.inside) +(recipe + (recipe_body) @function.inside) @function.around -(dependency) @parameter.around -(depcall) @parameter.inside -(depcall (expression) @parameter.inside) +(parameters + ((_) @parameter.inside . ","? @parameter.around)) @parameter.around -(stringlist - (string) @parameter.inside - . ","? @_end - ; Commented out since we don't support `#make-range!` at the moment - ; (#make-range! "parameter.around" @parameter.inside @_end) -) -(parameters - [(parameter) - (variadic_parameters)] @parameter.inside - . " "? @_end - ; Commented out since we don't support `#make-range!` at the moment - ; (#make-range! "parameter.around" @parameter.inside @_end) -) +(dependency_expression + (_) @parameter.inside) @parameter.around -(expression - (condition) @function.inside -) @function.around -(expression - if:(expression) @function.inside -) -(expression - else:(expression) @function.inside -) - -(item [(alias) (assignment) (export) (setting)]) @class.around -(recipeheader) @class.around -(line) @class.around +(function_call + arguments: (sequence + (expression) @parameter.inside) @parameter.around) @function.around (comment) @comment.around From 2900bc03cf098bdd29959b6a0778d043d9a9549e Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 27 Jul 2024 12:13:51 -0400 Subject: [PATCH 179/200] Vendor the `lsp-types` crate --- helix-lsp-types/Cargo.lock | 176 ++ helix-lsp-types/Cargo.toml | 28 + helix-lsp-types/LICENSE | 22 + helix-lsp-types/README.md | 3 + helix-lsp-types/src/call_hierarchy.rs | 127 + helix-lsp-types/src/code_action.rs | 395 +++ helix-lsp-types/src/code_lens.rs | 66 + helix-lsp-types/src/color.rs | 122 + helix-lsp-types/src/completion.rs | 616 ++++ helix-lsp-types/src/document_diagnostic.rs | 269 ++ helix-lsp-types/src/document_highlight.rs | 51 + helix-lsp-types/src/document_link.rs | 67 + helix-lsp-types/src/document_symbols.rs | 134 + helix-lsp-types/src/error_codes.rs | 54 + helix-lsp-types/src/file_operations.rs | 213 ++ helix-lsp-types/src/folding_range.rs | 145 + helix-lsp-types/src/formatting.rs | 153 + helix-lsp-types/src/hover.rs | 86 + helix-lsp-types/src/inlay_hint.rs | 281 ++ helix-lsp-types/src/inline_completion.rs | 162 ++ helix-lsp-types/src/inline_value.rs | 217 ++ helix-lsp-types/src/lib.rs | 2880 +++++++++++++++++++ helix-lsp-types/src/linked_editing.rs | 61 + helix-lsp-types/src/lsif.rs | 336 +++ helix-lsp-types/src/moniker.rs | 92 + helix-lsp-types/src/notification.rs | 361 +++ helix-lsp-types/src/progress.rs | 134 + helix-lsp-types/src/references.rs | 30 + helix-lsp-types/src/rename.rs | 88 + helix-lsp-types/src/request.rs | 1068 +++++++ helix-lsp-types/src/selection_range.rs | 86 + helix-lsp-types/src/semantic_tokens.rs | 734 +++++ helix-lsp-types/src/signature_help.rs | 207 ++ helix-lsp-types/src/trace.rs | 77 + helix-lsp-types/src/type_hierarchy.rs | 90 + helix-lsp-types/src/window.rs | 173 ++ helix-lsp-types/src/workspace_diagnostic.rs | 149 + helix-lsp-types/src/workspace_folders.rs | 49 + helix-lsp-types/src/workspace_symbols.rs | 105 + 39 files changed, 10107 insertions(+) create mode 100644 helix-lsp-types/Cargo.lock create mode 100644 helix-lsp-types/Cargo.toml create mode 100644 helix-lsp-types/LICENSE create mode 100644 helix-lsp-types/README.md create mode 100644 helix-lsp-types/src/call_hierarchy.rs create mode 100644 helix-lsp-types/src/code_action.rs create mode 100644 helix-lsp-types/src/code_lens.rs create mode 100644 helix-lsp-types/src/color.rs create mode 100644 helix-lsp-types/src/completion.rs create mode 100644 helix-lsp-types/src/document_diagnostic.rs create mode 100644 helix-lsp-types/src/document_highlight.rs create mode 100644 helix-lsp-types/src/document_link.rs create mode 100644 helix-lsp-types/src/document_symbols.rs create mode 100644 helix-lsp-types/src/error_codes.rs create mode 100644 helix-lsp-types/src/file_operations.rs create mode 100644 helix-lsp-types/src/folding_range.rs create mode 100644 helix-lsp-types/src/formatting.rs create mode 100644 helix-lsp-types/src/hover.rs create mode 100644 helix-lsp-types/src/inlay_hint.rs create mode 100644 helix-lsp-types/src/inline_completion.rs create mode 100644 helix-lsp-types/src/inline_value.rs create mode 100644 helix-lsp-types/src/lib.rs create mode 100644 helix-lsp-types/src/linked_editing.rs create mode 100644 helix-lsp-types/src/lsif.rs create mode 100644 helix-lsp-types/src/moniker.rs create mode 100644 helix-lsp-types/src/notification.rs create mode 100644 helix-lsp-types/src/progress.rs create mode 100644 helix-lsp-types/src/references.rs create mode 100644 helix-lsp-types/src/rename.rs create mode 100644 helix-lsp-types/src/request.rs create mode 100644 helix-lsp-types/src/selection_range.rs create mode 100644 helix-lsp-types/src/semantic_tokens.rs create mode 100644 helix-lsp-types/src/signature_help.rs create mode 100644 helix-lsp-types/src/trace.rs create mode 100644 helix-lsp-types/src/type_hierarchy.rs create mode 100644 helix-lsp-types/src/window.rs create mode 100644 helix-lsp-types/src/workspace_diagnostic.rs create mode 100644 helix-lsp-types/src/workspace_folders.rs create mode 100644 helix-lsp-types/src/workspace_symbols.rs diff --git a/helix-lsp-types/Cargo.lock b/helix-lsp-types/Cargo.lock new file mode 100644 index 000000000..11ac87521 --- /dev/null +++ b/helix-lsp-types/Cargo.lock @@ -0,0 +1,176 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "lsp-types" +version = "0.95.1" +dependencies = [ + "bitflags", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] diff --git a/helix-lsp-types/Cargo.toml b/helix-lsp-types/Cargo.toml new file mode 100644 index 000000000..ad7a3ca05 --- /dev/null +++ b/helix-lsp-types/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "lsp-types" +version = "0.95.1" +authors = ["Markus Westerlind ", "Bruno Medeiros "] +edition = "2018" +description = "Types for interaction with a language server, using VSCode's Language Server Protocol" + +repository = "https://github.com/gluon-lang/lsp-types" +documentation = "https://docs.rs/lsp-types" + +readme = "README.md" + +keywords = ["language", "server", "lsp", "vscode", "lsif"] + +license = "MIT" + +[dependencies] +bitflags = "1.0.1" +serde = { version = "1.0.34", features = ["derive"] } +serde_json = "1.0.50" +serde_repr = "0.1" +url = {version = "2.0.0", features = ["serde"]} + +[features] +default = [] +# Enables proposed LSP extensions. +# NOTE: No semver compatibility is guaranteed for types enabled by this feature. +proposed = [] diff --git a/helix-lsp-types/LICENSE b/helix-lsp-types/LICENSE new file mode 100644 index 000000000..32781d976 --- /dev/null +++ b/helix-lsp-types/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Markus Westerlind + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/helix-lsp-types/README.md b/helix-lsp-types/README.md new file mode 100644 index 000000000..01803be17 --- /dev/null +++ b/helix-lsp-types/README.md @@ -0,0 +1,3 @@ +# Helix's `lsp-types` + +This is a fork of the [`lsp-types`](https://crates.io/crates/lsp-types) crate ([`gluon-lang/lsp-types`](https://github.com/gluon-lang/lsp-types)) taken at version v0.95.1 (commit [3e6daee](https://github.com/gluon-lang/lsp-types/commit/3e6daee771d14db4094a554b8d03e29c310dfcbe)). This fork focuses usability improvements that make the types easier to work with for the Helix codebase. For example the URL type - the `uri` crate at this version of `lsp-types` - will be replaced with a wrapper around a string. diff --git a/helix-lsp-types/src/call_hierarchy.rs b/helix-lsp-types/src/call_hierarchy.rs new file mode 100644 index 000000000..dea78803f --- /dev/null +++ b/helix-lsp-types/src/call_hierarchy.rs @@ -0,0 +1,127 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use url::Url; + +use crate::{ + DynamicRegistrationClientCapabilities, PartialResultParams, Range, SymbolKind, SymbolTag, + TextDocumentPositionParams, WorkDoneProgressOptions, WorkDoneProgressParams, +}; + +pub type CallHierarchyClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize, Copy)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Copy)] +#[serde(untagged)] +pub enum CallHierarchyServerCapability { + Simple(bool), + Options(CallHierarchyOptions), +} + +impl From for CallHierarchyServerCapability { + fn from(from: CallHierarchyOptions) -> Self { + Self::Options(from) + } +} + +impl From for CallHierarchyServerCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyPrepareParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyItem { + /// The name of this item. + pub name: String, + + /// The kind of this item. + pub kind: SymbolKind, + + /// Tags for this item. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + + /// More detail for this item, e.g. the signature of a function. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + + /// The resource identifier of this item. + pub uri: Url, + + /// The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. + pub range: Range, + + /// The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. + /// Must be contained by the [`range`](#CallHierarchyItem.range). + pub selection_range: Range, + + /// A data entry field that is preserved between a call hierarchy prepare and incoming calls or outgoing calls requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyIncomingCallsParams { + pub item: CallHierarchyItem, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// Represents an incoming call, e.g. a caller of a method or constructor. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyIncomingCall { + /// The item that makes the call. + pub from: CallHierarchyItem, + + /// The range at which at which the calls appears. This is relative to the caller + /// denoted by [`this.from`](#CallHierarchyIncomingCall.from). + pub from_ranges: Vec, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyOutgoingCallsParams { + pub item: CallHierarchyItem, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CallHierarchyOutgoingCall { + /// The item that is called. + pub to: CallHierarchyItem, + + /// The range at which this item is called. This is the range relative to the caller, e.g the item + /// passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) + /// and not [`this.to`](#CallHierarchyOutgoingCall.to). + pub from_ranges: Vec, +} diff --git a/helix-lsp-types/src/code_action.rs b/helix-lsp-types/src/code_action.rs new file mode 100644 index 000000000..6cc39e0d0 --- /dev/null +++ b/helix-lsp-types/src/code_action.rs @@ -0,0 +1,395 @@ +use crate::{ + Command, Diagnostic, PartialResultParams, Range, TextDocumentIdentifier, + WorkDoneProgressOptions, WorkDoneProgressParams, WorkspaceEdit, +}; +use serde::{Deserialize, Serialize}; + +use serde_json::Value; + +use std::borrow::Cow; +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum CodeActionProviderCapability { + Simple(bool), + Options(CodeActionOptions), +} + +impl From for CodeActionProviderCapability { + fn from(from: CodeActionOptions) -> Self { + Self::Options(from) + } +} + +impl From for CodeActionProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionClientCapabilities { + /// + /// This capability supports dynamic registration. + /// + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The client support code action literals as a valid + /// response of the `textDocument/codeAction` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub code_action_literal_support: Option, + + /// Whether code action supports the `isPreferred` property. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub is_preferred_support: Option, + + /// Whether code action supports the `disabled` property. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_support: Option, + + /// Whether code action supports the `data` property which is + /// preserved between a `textDocument/codeAction` and a + /// `codeAction/resolve` request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub data_support: Option, + + /// Whether the client supports resolving additional code action + /// properties via a separate `codeAction/resolve` request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_support: Option, + + /// Whether the client honors the change annotations in + /// text edits and resource operations returned via the + /// `CodeAction#edit` property by for example presenting + /// the workspace edit in the user interface and asking + /// for confirmation. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub honors_change_annotations: Option, +} + +/// Whether the client supports resolving additional code action +/// properties via a separate `codeAction/resolve` request. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionCapabilityResolveSupport { + /// The properties that a client can resolve lazily. + pub properties: Vec, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionLiteralSupport { + /// The code action kind is support with the following value set. + pub code_action_kind: CodeActionKindLiteralSupport, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionKindLiteralSupport { + /// The code action kind values the client supports. When this + /// property exists the client also guarantees that it will + /// handle values outside its set gracefully and falls back + /// to a default value when unknown. + pub value_set: Vec, +} + +/// Params for the CodeActionRequest +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionParams { + /// The document in which the command was invoked. + pub text_document: TextDocumentIdentifier, + + /// The range for which the command was invoked. + pub range: Range, + + /// Context carrying additional information. + pub context: CodeActionContext, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// response for CodeActionRequest +pub type CodeActionResponse = Vec; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum CodeActionOrCommand { + Command(Command), + CodeAction(CodeAction), +} + +impl From for CodeActionOrCommand { + fn from(command: Command) -> Self { + CodeActionOrCommand::Command(command) + } +} + +impl From for CodeActionOrCommand { + fn from(action: CodeAction) -> Self { + CodeActionOrCommand::CodeAction(action) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Clone, Deserialize, Serialize)] +pub struct CodeActionKind(Cow<'static, str>); + +impl CodeActionKind { + /// Empty kind. + pub const EMPTY: CodeActionKind = CodeActionKind::new(""); + + /// Base kind for quickfix actions: 'quickfix' + pub const QUICKFIX: CodeActionKind = CodeActionKind::new("quickfix"); + + /// Base kind for refactoring actions: 'refactor' + pub const REFACTOR: CodeActionKind = CodeActionKind::new("refactor"); + + /// Base kind for refactoring extraction actions: 'refactor.extract' + /// + /// Example extract actions: + /// + /// - Extract method + /// - Extract function + /// - Extract variable + /// - Extract interface from class + /// - ... + pub const REFACTOR_EXTRACT: CodeActionKind = CodeActionKind::new("refactor.extract"); + + /// Base kind for refactoring inline actions: 'refactor.inline' + /// + /// Example inline actions: + /// + /// - Inline function + /// - Inline variable + /// - Inline constant + /// - ... + pub const REFACTOR_INLINE: CodeActionKind = CodeActionKind::new("refactor.inline"); + + /// Base kind for refactoring rewrite actions: 'refactor.rewrite' + /// + /// Example rewrite actions: + /// + /// - Convert JavaScript function to class + /// - Add or remove parameter + /// - Encapsulate field + /// - Make method static + /// - Move method to base class + /// - ... + pub const REFACTOR_REWRITE: CodeActionKind = CodeActionKind::new("refactor.rewrite"); + + /// Base kind for source actions: `source` + /// + /// Source code actions apply to the entire file. + pub const SOURCE: CodeActionKind = CodeActionKind::new("source"); + + /// Base kind for an organize imports source action: `source.organizeImports` + pub const SOURCE_ORGANIZE_IMPORTS: CodeActionKind = + CodeActionKind::new("source.organizeImports"); + + /// Base kind for a 'fix all' source action: `source.fixAll`. + /// + /// 'Fix all' actions automatically fix errors that have a clear fix that + /// do not require user input. They should not suppress errors or perform + /// unsafe fixes such as generating new types or classes. + /// + /// @since 3.17.0 + pub const SOURCE_FIX_ALL: CodeActionKind = CodeActionKind::new("source.fixAll"); + + pub const fn new(tag: &'static str) -> Self { + CodeActionKind(Cow::Borrowed(tag)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for CodeActionKind { + fn from(from: String) -> Self { + CodeActionKind(Cow::from(from)) + } +} + +impl From<&'static str> for CodeActionKind { + fn from(from: &'static str) -> Self { + CodeActionKind::new(from) + } +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeAction { + /// A short, human-readable, title for this code action. + pub title: String, + + /// The kind of the code action. + /// Used to filter code actions. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + + /// The diagnostics that this code action resolves. + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostics: Option>, + + /// The workspace edit this code action performs. + #[serde(skip_serializing_if = "Option::is_none")] + pub edit: Option, + + /// A command this code action executes. If a code action + /// provides an edit and a command, first the edit is + /// executed and then the command. + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + + /// Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted + /// by keybindings. + /// A quick fix should be marked preferred if it properly addresses the underlying error. + /// A refactoring should be marked preferred if it is the most reasonable choice of actions to take. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub is_preferred: Option, + + /// Marks that the code action cannot currently be applied. + /// + /// Clients should follow the following guidelines regarding disabled code actions: + /// + /// - Disabled code actions are not shown in automatic + /// [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) + /// code action menu. + /// + /// - Disabled actions are shown as faded out in the code action menu when the user request + /// a more specific type of code action, such as refactorings. + /// + /// - If the user has a keybinding that auto applies a code action and only a disabled code + /// actions are returned, the client should show the user an error message with `reason` + /// in the editor. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled: Option, + + /// A data entry field that is preserved on a code action between + /// a `textDocument/codeAction` and a `codeAction/resolve` request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionDisabled { + /// Human readable description of why the code action is currently disabled. + /// + /// This is displayed in the code actions UI. + pub reason: String, +} + +/// The reason why code actions were requested. +/// +/// @since 3.17.0 +#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] +#[serde(transparent)] +pub struct CodeActionTriggerKind(i32); +lsp_enum! { +impl CodeActionTriggerKind { + /// Code actions were explicitly requested by the user or by an extension. + pub const INVOKED: CodeActionTriggerKind = CodeActionTriggerKind(1); + + /// Code actions were requested automatically. + /// + /// This typically happens when current selection in a file changes, but can + /// also be triggered when file content changes. + pub const AUTOMATIC: CodeActionTriggerKind = CodeActionTriggerKind(2); +} +} + +/// Contains additional diagnostic information about the context in which +/// a code action is run. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionContext { + /// An array of diagnostics. + pub diagnostics: Vec, + + /// Requested kind of actions to return. + /// + /// Actions not of this kind are filtered out by the client before being shown. So servers + /// can omit computing them. + #[serde(skip_serializing_if = "Option::is_none")] + pub only: Option>, + + /// The reason why code actions were requested. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_kind: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionOptions { + /// CodeActionKinds that this server may return. + /// + /// The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server + /// may list out every specific kind they provide. + #[serde(skip_serializing_if = "Option::is_none")] + pub code_action_kinds: Option>, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, + + /// The server provides support to resolve additional + /// information for a code action. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_provider: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::test_serialization; + + #[test] + fn test_code_action_response() { + test_serialization( + &vec![ + CodeActionOrCommand::Command(Command { + title: "title".to_string(), + command: "command".to_string(), + arguments: None, + }), + CodeActionOrCommand::CodeAction(CodeAction { + title: "title".to_string(), + kind: Some(CodeActionKind::QUICKFIX), + command: None, + diagnostics: None, + edit: None, + is_preferred: None, + ..CodeAction::default() + }), + ], + r#"[{"title":"title","command":"command"},{"title":"title","kind":"quickfix"}]"#, + ) + } +} diff --git a/helix-lsp-types/src/code_lens.rs b/helix-lsp-types/src/code_lens.rs new file mode 100644 index 000000000..c4c9c904e --- /dev/null +++ b/helix-lsp-types/src/code_lens.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + Command, DynamicRegistrationClientCapabilities, PartialResultParams, Range, + TextDocumentIdentifier, WorkDoneProgressParams, +}; + +pub type CodeLensClientCapabilities = DynamicRegistrationClientCapabilities; + +/// Code Lens options. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Copy)] +#[serde(rename_all = "camelCase")] +pub struct CodeLensOptions { + /// Code lens has a resolve provider as well. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_provider: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeLensParams { + /// The document to request code lens for. + pub text_document: TextDocumentIdentifier, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// A code lens represents a command that should be shown along with +/// source text, like the number of references, a way to run tests, etc. +/// +/// A code lens is _unresolved_ when no command is associated to it. For performance +/// reasons the creation of a code lens and resolving should be done in two stages. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeLens { + /// The range in which this code lens is valid. Should only span a single line. + pub range: Range, + + /// The command this code lens represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + + /// A data entry field that is preserved on a code lens item between + /// a code lens and a code lens resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeLensWorkspaceClientCapabilities { + /// Whether the client implementation supports a refresh request sent from the + /// server to the client. + /// + /// Note that this event is global and will force the client to refresh all + /// code lenses currently shown. It should be used with absolute care and is + /// useful for situation where a server for example detect a project wide + /// change that requires such a calculation. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_support: Option, +} diff --git a/helix-lsp-types/src/color.rs b/helix-lsp-types/src/color.rs new file mode 100644 index 000000000..584f979ee --- /dev/null +++ b/helix-lsp-types/src/color.rs @@ -0,0 +1,122 @@ +use crate::{ + DocumentSelector, DynamicRegistrationClientCapabilities, PartialResultParams, Range, + TextDocumentIdentifier, TextEdit, WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; + +pub type DocumentColorClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ColorProviderOptions {} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StaticTextDocumentColorProviderOptions { + /// A document selector to identify the scope of the registration. If set to null + /// the document selector provided on the client side will be used. + pub document_selector: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ColorProviderCapability { + Simple(bool), + ColorProvider(ColorProviderOptions), + Options(StaticTextDocumentColorProviderOptions), +} + +impl From for ColorProviderCapability { + fn from(from: ColorProviderOptions) -> Self { + Self::ColorProvider(from) + } +} + +impl From for ColorProviderCapability { + fn from(from: StaticTextDocumentColorProviderOptions) -> Self { + Self::Options(from) + } +} + +impl From for ColorProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentColorParams { + /// The text document + pub text_document: TextDocumentIdentifier, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ColorInformation { + /// The range in the document where this color appears. + pub range: Range, + /// The actual color value for this color range. + pub color: Color, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Copy)] +#[serde(rename_all = "camelCase")] +pub struct Color { + /// The red component of this color in the range [0-1]. + pub red: f32, + /// The green component of this color in the range [0-1]. + pub green: f32, + /// The blue component of this color in the range [0-1]. + pub blue: f32, + /// The alpha component of this color in the range [0-1]. + pub alpha: f32, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ColorPresentationParams { + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The color information to request presentations for. + pub color: Color, + + /// The range where the color would be inserted. Serves as a context. + pub range: Range, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ColorPresentation { + /// The label of this color presentation. It will be shown on the color + /// picker header. By default this is also the text that is inserted when selecting + /// this color presentation. + pub label: String, + + /// An [edit](#TextEdit) which is applied to a document when selecting + /// this presentation for the color. When `falsy` the [label](#ColorPresentation.label) + /// is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub text_edit: Option, + + /// An optional array of additional [text edits](#TextEdit) that are applied when + /// selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_text_edits: Option>, +} diff --git a/helix-lsp-types/src/completion.rs b/helix-lsp-types/src/completion.rs new file mode 100644 index 000000000..bdf60fb51 --- /dev/null +++ b/helix-lsp-types/src/completion.rs @@ -0,0 +1,616 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + Command, Documentation, MarkupKind, PartialResultParams, TagSupport, + TextDocumentPositionParams, TextDocumentRegistrationOptions, TextEdit, WorkDoneProgressOptions, + WorkDoneProgressParams, +}; + +use crate::Range; +use serde_json::Value; +use std::fmt::Debug; + +/// Defines how to interpret the insert text in a completion item +#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InsertTextFormat(i32); +lsp_enum! { +impl InsertTextFormat { + pub const PLAIN_TEXT: InsertTextFormat = InsertTextFormat(1); + pub const SNIPPET: InsertTextFormat = InsertTextFormat(2); +} +} + +/// The kind of a completion entry. +#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CompletionItemKind(i32); +lsp_enum! { +impl CompletionItemKind { + pub const TEXT: CompletionItemKind = CompletionItemKind(1); + pub const METHOD: CompletionItemKind = CompletionItemKind(2); + pub const FUNCTION: CompletionItemKind = CompletionItemKind(3); + pub const CONSTRUCTOR: CompletionItemKind = CompletionItemKind(4); + pub const FIELD: CompletionItemKind = CompletionItemKind(5); + pub const VARIABLE: CompletionItemKind = CompletionItemKind(6); + pub const CLASS: CompletionItemKind = CompletionItemKind(7); + pub const INTERFACE: CompletionItemKind = CompletionItemKind(8); + pub const MODULE: CompletionItemKind = CompletionItemKind(9); + pub const PROPERTY: CompletionItemKind = CompletionItemKind(10); + pub const UNIT: CompletionItemKind = CompletionItemKind(11); + pub const VALUE: CompletionItemKind = CompletionItemKind(12); + pub const ENUM: CompletionItemKind = CompletionItemKind(13); + pub const KEYWORD: CompletionItemKind = CompletionItemKind(14); + pub const SNIPPET: CompletionItemKind = CompletionItemKind(15); + pub const COLOR: CompletionItemKind = CompletionItemKind(16); + pub const FILE: CompletionItemKind = CompletionItemKind(17); + pub const REFERENCE: CompletionItemKind = CompletionItemKind(18); + pub const FOLDER: CompletionItemKind = CompletionItemKind(19); + pub const ENUM_MEMBER: CompletionItemKind = CompletionItemKind(20); + pub const CONSTANT: CompletionItemKind = CompletionItemKind(21); + pub const STRUCT: CompletionItemKind = CompletionItemKind(22); + pub const EVENT: CompletionItemKind = CompletionItemKind(23); + pub const OPERATOR: CompletionItemKind = CompletionItemKind(24); + pub const TYPE_PARAMETER: CompletionItemKind = CompletionItemKind(25); +} +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionItemCapability { + /// Client supports snippets as insert text. + /// + /// A snippet can define tab stops and placeholders with `$1`, `$2` + /// and `${3:foo}`. `$0` defines the final tab stop, it defaults to + /// the end of the snippet. Placeholders with equal identifiers are linked, + /// that is typing in one will update others too. + #[serde(skip_serializing_if = "Option::is_none")] + pub snippet_support: Option, + + /// Client supports commit characters on a completion item. + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_characters_support: Option, + + /// Client supports the follow content formats for the documentation + /// property. The order describes the preferred format of the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub documentation_format: Option>, + + /// Client supports the deprecated property on a completion item. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated_support: Option, + + /// Client supports the preselect property on a completion item. + #[serde(skip_serializing_if = "Option::is_none")] + pub preselect_support: Option, + + /// Client supports the tag property on a completion item. Clients supporting + /// tags have to handle unknown tags gracefully. Clients especially need to + /// preserve unknown tags when sending a completion item back to the server in + /// a resolve call. + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "TagSupport::deserialize_compat" + )] + pub tag_support: Option>, + + /// Client support insert replace edit to control different behavior if a + /// completion item is inserted in the text or should replace text. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_replace_support: Option, + + /// Indicates which properties a client can resolve lazily on a completion + /// item. Before version 3.16.0 only the predefined properties `documentation` + /// and `details` could be resolved lazily. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_support: Option, + + /// The client supports the `insertTextMode` property on + /// a completion item to override the whitespace handling mode + /// as defined by the client. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_text_mode_support: Option, + + /// The client has support for completion item label + /// details (see also `CompletionItemLabelDetails`). + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub label_details_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionItemCapabilityResolveSupport { + /// The properties that a client can resolve lazily. + pub properties: Vec, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InsertTextModeSupport { + pub value_set: Vec, +} + +/// How whitespace and indentation is handled during completion +/// item insertion. +/// +/// @since 3.16.0 +#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InsertTextMode(i32); +lsp_enum! { +impl InsertTextMode { + /// The insertion or replace strings is taken as it is. If the + /// value is multi line the lines below the cursor will be + /// inserted using the indentation defined in the string value. + /// The client will not apply any kind of adjustments to the + /// string. + pub const AS_IS: InsertTextMode = InsertTextMode(1); + + /// The editor adjusts leading whitespace of new lines so that + /// they match the indentation up to the cursor of the line for + /// which the item is accepted. + /// + /// Consider a line like this: <2tabs><3tabs>foo. Accepting a + /// multi line completion item is indented using 2 tabs all + /// following lines inserted will be indented using 2 tabs as well. + pub const ADJUST_INDENTATION: InsertTextMode = InsertTextMode(2); +} +} + +#[derive(Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct CompletionItemTag(i32); +lsp_enum! { +impl CompletionItemTag { + pub const DEPRECATED: CompletionItemTag = CompletionItemTag(1); +} +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionItemKindCapability { + /// The completion item kind values the client supports. When this + /// property exists the client also guarantees that it will + /// handle values outside its set gracefully and falls back + /// to a default value when unknown. + /// + /// If this property is not present the client only supports + /// the completion items kinds from `Text` to `Reference` as defined in + /// the initial version of the protocol. + #[serde(skip_serializing_if = "Option::is_none")] + pub value_set: Option>, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionListCapability { + /// The client supports the following itemDefaults on + /// a completion list. + /// + /// The value lists the supported property names of the + /// `CompletionList.itemDefaults` object. If omitted + /// no properties are supported. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub item_defaults: Option>, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionClientCapabilities { + /// Whether completion supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The client supports the following `CompletionItem` specific + /// capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_item: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_item_kind: Option, + + /// The client supports to send additional context information for a + /// `textDocument/completion` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_support: Option, + + /// The client's default when the completion item doesn't provide a + /// `insertTextMode` property. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_text_mode: Option, + + /// The client supports the following `CompletionList` specific + /// capabilities. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_list: Option, +} + +/// A special text edit to provide an insert and a replace operation. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InsertReplaceEdit { + /// The string to be inserted. + pub new_text: String, + + /// The range if the insert is requested + pub insert: Range, + + /// The range if the replace is requested. + pub replace: Range, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum CompletionTextEdit { + Edit(TextEdit), + InsertAndReplace(InsertReplaceEdit), +} + +impl From for CompletionTextEdit { + fn from(edit: TextEdit) -> Self { + CompletionTextEdit::Edit(edit) + } +} + +impl From for CompletionTextEdit { + fn from(edit: InsertReplaceEdit) -> Self { + CompletionTextEdit::InsertAndReplace(edit) + } +} + +/// Completion options. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionOptions { + /// The server provides support to resolve additional information for a completion item. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_provider: Option, + + /// Most tools trigger completion request automatically without explicitly + /// requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they + /// do so when the user starts to type an identifier. For example if the user + /// types `c` in a JavaScript file code complete will automatically pop up + /// present `console` besides others as a completion item. Characters that + /// make up identifiers don't need to be listed here. + /// + /// If code complete should automatically be trigger on characters not being + /// valid inside an identifier (for example `.` in JavaScript) list them in + /// `triggerCharacters`. + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_characters: Option>, + + /// The list of all possible characters that commit a completion. This field + /// can be used if clients don't support individual commit characters per + /// completion item. See client capability + /// `completion.completionItem.commitCharactersSupport`. + /// + /// If a server provides both `allCommitCharacters` and commit characters on + /// an individual completion item the ones on the completion item win. + /// + /// @since 3.2.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub all_commit_characters: Option>, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, + + /// The server supports the following `CompletionItem` specific + /// capabilities. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_item: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionOptionsCompletionItem { + /// The server has support for completion item label + /// details (see also `CompletionItemLabelDetails`) when receiving + /// a completion item in a resolve call. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub label_details_support: Option, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct CompletionRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub completion_options: CompletionOptions, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CompletionResponse { + Array(Vec), + List(CompletionList), +} + +impl From> for CompletionResponse { + fn from(items: Vec) -> Self { + CompletionResponse::Array(items) + } +} + +impl From for CompletionResponse { + fn from(list: CompletionList) -> Self { + CompletionResponse::List(list) + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionParams { + // This field was "mixed-in" from TextDocumentPositionParams + #[serde(flatten)] + pub text_document_position: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, + + // CompletionParams properties: + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionContext { + /// How the completion was triggered. + pub trigger_kind: CompletionTriggerKind, + + /// The trigger character (a single character) that has trigger code complete. + /// Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_character: Option, +} + +/// How a completion was triggered. +#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] +#[serde(transparent)] +pub struct CompletionTriggerKind(i32); +lsp_enum! { +impl CompletionTriggerKind { + pub const INVOKED: CompletionTriggerKind = CompletionTriggerKind(1); + pub const TRIGGER_CHARACTER: CompletionTriggerKind = CompletionTriggerKind(2); + pub const TRIGGER_FOR_INCOMPLETE_COMPLETIONS: CompletionTriggerKind = CompletionTriggerKind(3); +} +} + +/// Represents a collection of [completion items](#CompletionItem) to be presented +/// in the editor. +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionList { + /// This list it not complete. Further typing should result in recomputing + /// this list. + pub is_incomplete: bool, + + /// The completion items. + pub items: Vec, +} + +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CompletionItem { + /// The label of this completion item. By default + /// also the text that is inserted when selecting + /// this completion. + pub label: String, + + /// Additional details for the label + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub label_details: Option, + + /// The kind of this completion item. Based of the kind + /// an icon is chosen by the editor. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + + /// A human-readable string with additional information + /// about this item, like type or symbol information. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + + /// A human-readable string that represents a doc-comment. + #[serde(skip_serializing_if = "Option::is_none")] + pub documentation: Option, + + /// Indicates if this item is deprecated. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + + /// Select this item when showing. + #[serde(skip_serializing_if = "Option::is_none")] + pub preselect: Option, + + /// A string that should be used when comparing this item + /// with other items. When `falsy` the label is used + /// as the sort text for this item. + #[serde(skip_serializing_if = "Option::is_none")] + pub sort_text: Option, + + /// A string that should be used when filtering a set of + /// completion items. When `falsy` the label is used as the + /// filter text for this item. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter_text: Option, + + /// A string that should be inserted into a document when selecting + /// this completion. When `falsy` the label is used as the insert text + /// for this item. + /// + /// The `insertText` is subject to interpretation by the client side. + /// Some tools might not take the string literally. For example + /// VS Code when code complete is requested in this example + /// `con` and a completion item with an `insertText` of + /// `console` is provided it will only insert `sole`. Therefore it is + /// recommended to use `textEdit` instead since it avoids additional client + /// side interpretation. + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_text: Option, + + /// The format of the insert text. The format applies to both the `insertText` property + /// and the `newText` property of a provided `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_text_format: Option, + + /// How whitespace and indentation is handled during completion + /// item insertion. If not provided the client's default value depends on + /// the `textDocument.completion.insertTextMode` client capability. + /// + /// @since 3.16.0 + /// @since 3.17.0 - support for `textDocument.completion.insertTextMode` + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_text_mode: Option, + + /// An edit which is applied to a document when selecting + /// this completion. When an edit is provided the value of + /// insertText is ignored. + /// + /// Most editors support two different operation when accepting a completion item. One is to insert a + + /// completion text and the other is to replace an existing text with a completion text. Since this can + /// usually not predetermined by a server it can report both ranges. Clients need to signal support for + /// `InsertReplaceEdits` via the `textDocument.completion.insertReplaceSupport` client capability + /// property. + /// + /// *Note 1:* The text edit's range as well as both ranges from a insert replace edit must be a + /// [single line] and they must contain the position at which completion has been requested. + /// *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range must be a prefix of + /// the edit's replace range, that means it must be contained and starting at the same position. + /// + /// @since 3.16.0 additional type `InsertReplaceEdit` + #[serde(skip_serializing_if = "Option::is_none")] + pub text_edit: Option, + + /// An optional array of additional text edits that are applied when + /// selecting this completion. Edits must not overlap with the main edit + /// nor with themselves. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_text_edits: Option>, + + /// An optional command that is executed *after* inserting this completion. *Note* that + /// additional modifications to the current document should be described with the + /// additionalTextEdits-property. + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + + /// An optional set of characters that when pressed while this completion is + /// active will accept it first and then type that character. *Note* that all + /// commit characters should have `length=1` and that superfluous characters + /// will be ignored. + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_characters: Option>, + + /// An data entry field that is preserved on a completion item between + /// a completion and a completion resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + + /// Tags for this completion item. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, +} + +impl CompletionItem { + /// Create a CompletionItem with the minimum possible info (label and detail). + pub fn new_simple(label: String, detail: String) -> CompletionItem { + CompletionItem { + label, + detail: Some(detail), + ..Self::default() + } + } +} + +/// Additional details for a completion item label. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CompletionItemLabelDetails { + /// An optional string which is rendered less prominently directly after + /// {@link CompletionItemLabel.label label}, without any spacing. Should be + /// used for function signatures or type annotations. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + + /// An optional string which is rendered less prominently after + /// {@link CompletionItemLabel.detail}. Should be used for fully qualified + /// names or file path. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::test_deserialization; + + #[test] + fn test_tag_support_deserialization() { + let mut empty = CompletionItemCapability::default(); + empty.tag_support = None; + + test_deserialization(r#"{}"#, &empty); + test_deserialization(r#"{"tagSupport": false}"#, &empty); + + let mut t = CompletionItemCapability::default(); + t.tag_support = Some(TagSupport { value_set: vec![] }); + test_deserialization(r#"{"tagSupport": true}"#, &t); + + let mut t = CompletionItemCapability::default(); + t.tag_support = Some(TagSupport { + value_set: vec![CompletionItemTag::DEPRECATED], + }); + test_deserialization(r#"{"tagSupport": {"valueSet": [1]}}"#, &t); + } + + #[test] + fn test_debug_enum() { + assert_eq!(format!("{:?}", CompletionItemKind::TEXT), "Text"); + assert_eq!( + format!("{:?}", CompletionItemKind::TYPE_PARAMETER), + "TypeParameter" + ); + } + + #[test] + fn test_try_from_enum() { + use std::convert::TryInto; + assert_eq!("Text".try_into(), Ok(CompletionItemKind::TEXT)); + assert_eq!( + "TypeParameter".try_into(), + Ok(CompletionItemKind::TYPE_PARAMETER) + ); + } +} diff --git a/helix-lsp-types/src/document_diagnostic.rs b/helix-lsp-types/src/document_diagnostic.rs new file mode 100644 index 000000000..a2b5c41fa --- /dev/null +++ b/helix-lsp-types/src/document_diagnostic.rs @@ -0,0 +1,269 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{ + Diagnostic, PartialResultParams, StaticRegistrationOptions, TextDocumentIdentifier, + TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams, +}; + +/// Client capabilities specific to diagnostic pull requests. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticClientCapabilities { + /// Whether implementation supports dynamic registration. + /// + /// If this is set to `true` the client supports the new `(TextDocumentRegistrationOptions & + /// StaticRegistrationOptions)` return value for the corresponding server capability as well. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Whether the clients supports related documents for document diagnostic pulls. + #[serde(skip_serializing_if = "Option::is_none")] + pub related_document_support: Option, +} + +/// Diagnostic options. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticOptions { + /// An optional identifier under which the diagnostics are + /// managed by the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + + /// Whether the language has inter file dependencies, meaning that editing code in one file can + /// result in a different diagnostic set in another file. Inter file dependencies are common + /// for most programming languages and typically uncommon for linters. + pub inter_file_dependencies: bool, + + /// The server provides support for workspace diagnostics as well. + pub workspace_diagnostics: bool, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +/// Diagnostic registration options. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub diagnostic_options: DiagnosticOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum DiagnosticServerCapabilities { + Options(DiagnosticOptions), + RegistrationOptions(DiagnosticRegistrationOptions), +} + +/// Parameters of the document diagnostic request. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentDiagnosticParams { + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The additional identifier provided during registration. + pub identifier: Option, + + /// The result ID of a previous response if provided. + pub previous_result_id: Option, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// A diagnostic report with a full set of problems. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FullDocumentDiagnosticReport { + /// An optional result ID. If provided it will be sent on the next diagnostic request for the + /// same document. + #[serde(skip_serializing_if = "Option::is_none")] + pub result_id: Option, + + /// The actual items. + pub items: Vec, +} + +/// A diagnostic report indicating that the last returned report is still accurate. +/// +/// A server can only return `unchanged` if result ids are provided. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct UnchangedDocumentDiagnosticReport { + /// A result ID which will be sent on the next diagnostic request for the same document. + pub result_id: String, +} + +/// The document diagnostic report kinds. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum DocumentDiagnosticReportKind { + /// A diagnostic report with a full set of problems. + Full(FullDocumentDiagnosticReport), + /// A report indicating that the last returned report is still accurate. + Unchanged(UnchangedDocumentDiagnosticReport), +} + +impl From for DocumentDiagnosticReportKind { + fn from(from: FullDocumentDiagnosticReport) -> Self { + DocumentDiagnosticReportKind::Full(from) + } +} + +impl From for DocumentDiagnosticReportKind { + fn from(from: UnchangedDocumentDiagnosticReport) -> Self { + DocumentDiagnosticReportKind::Unchanged(from) + } +} + +/// A full diagnostic report with a set of related documents. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RelatedFullDocumentDiagnosticReport { + /// Diagnostics of related documents. + /// + /// This information is useful in programming languages where code in a file A can generate + /// diagnostics in a file B which A depends on. An example of such a language is C/C++ where + /// macro definitions in a file `a.cpp` result in errors in a header file `b.hpp`. + /// + /// @since 3.17.0 + #[serde(with = "crate::url_map")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub related_documents: Option>, + // relatedDocuments?: { [uri: string]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; }; + #[serde(flatten)] + pub full_document_diagnostic_report: FullDocumentDiagnosticReport, +} + +/// An unchanged diagnostic report with a set of related documents. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RelatedUnchangedDocumentDiagnosticReport { + /// Diagnostics of related documents. + /// + /// This information is useful in programming languages where code in a file A can generate + /// diagnostics in a file B which A depends on. An example of such a language is C/C++ where + /// macro definitions in a file `a.cpp` result in errors in a header file `b.hpp`. + /// + /// @since 3.17.0 + #[serde(with = "crate::url_map")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub related_documents: Option>, + // relatedDocuments?: { [uri: string]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; }; + #[serde(flatten)] + pub unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport, +} + +/// The result of a document diagnostic pull request. +/// +/// A report can either be a full report containing all diagnostics for the requested document or +/// an unchanged report indicating that nothing has changed in terms of diagnostics in comparison +/// to the last pull request. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum DocumentDiagnosticReport { + /// A diagnostic report with a full set of problems. + Full(RelatedFullDocumentDiagnosticReport), + /// A report indicating that the last returned report is still accurate. + Unchanged(RelatedUnchangedDocumentDiagnosticReport), +} + +impl From for DocumentDiagnosticReport { + fn from(from: RelatedFullDocumentDiagnosticReport) -> Self { + DocumentDiagnosticReport::Full(from) + } +} + +impl From for DocumentDiagnosticReport { + fn from(from: RelatedUnchangedDocumentDiagnosticReport) -> Self { + DocumentDiagnosticReport::Unchanged(from) + } +} + +/// A partial result for a document diagnostic report. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DocumentDiagnosticReportPartialResult { + #[serde(with = "crate::url_map")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub related_documents: Option>, + // relatedDocuments?: { [uri: string]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; }; +} + +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum DocumentDiagnosticReportResult { + Report(DocumentDiagnosticReport), + Partial(DocumentDiagnosticReportPartialResult), +} + +impl From for DocumentDiagnosticReportResult { + fn from(from: DocumentDiagnosticReport) -> Self { + DocumentDiagnosticReportResult::Report(from) + } +} + +impl From for DocumentDiagnosticReportResult { + fn from(from: DocumentDiagnosticReportPartialResult) -> Self { + DocumentDiagnosticReportResult::Partial(from) + } +} + +/// Cancellation data returned from a diagnostic request. +/// +/// If no data is provided, it defaults to `{ retrigger_request: true }`. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticServerCancellationData { + pub retrigger_request: bool, +} + +impl Default for DiagnosticServerCancellationData { + fn default() -> Self { + DiagnosticServerCancellationData { + retrigger_request: true, + } + } +} diff --git a/helix-lsp-types/src/document_highlight.rs b/helix-lsp-types/src/document_highlight.rs new file mode 100644 index 000000000..f2954e6d9 --- /dev/null +++ b/helix-lsp-types/src/document_highlight.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + DynamicRegistrationClientCapabilities, PartialResultParams, Range, TextDocumentPositionParams, + WorkDoneProgressParams, +}; + +pub type DocumentHighlightClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentHighlightParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// A document highlight is a range inside a text document which deserves +/// special attention. Usually a document highlight is visualized by changing +/// the background color of its range. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct DocumentHighlight { + /// The range this highlight applies to. + pub range: Range, + + /// The highlight kind, default is DocumentHighlightKind.Text. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, +} + +/// A document highlight kind. +#[derive(Eq, PartialEq, Copy, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct DocumentHighlightKind(i32); +lsp_enum! { +impl DocumentHighlightKind { + /// A textual occurrence. + pub const TEXT: DocumentHighlightKind = DocumentHighlightKind(1); + + /// Read-access of a symbol, like reading a variable. + pub const READ: DocumentHighlightKind = DocumentHighlightKind(2); + + /// Write-access of a symbol, like writing to a variable. + pub const WRITE: DocumentHighlightKind = DocumentHighlightKind(3); +} +} diff --git a/helix-lsp-types/src/document_link.rs b/helix-lsp-types/src/document_link.rs new file mode 100644 index 000000000..1400dd96b --- /dev/null +++ b/helix-lsp-types/src/document_link.rs @@ -0,0 +1,67 @@ +use crate::{ + PartialResultParams, Range, TextDocumentIdentifier, WorkDoneProgressOptions, + WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use url::Url; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentLinkClientCapabilities { + /// Whether document link supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Whether the client support the `tooltip` property on `DocumentLink`. + #[serde(skip_serializing_if = "Option::is_none")] + pub tooltip_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentLinkOptions { + /// Document links have a resolve provider as well. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_provider: Option, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentLinkParams { + /// The document to provide document links for. + pub text_document: TextDocumentIdentifier, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// A document link is a range in a text document that links to an internal or external resource, like another +/// text document or a web site. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct DocumentLink { + /// The range this link applies to. + pub range: Range, + /// The uri this link points to. + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, + + /// The tooltip text when you hover over this link. + /// + /// If a tooltip is provided, is will be displayed in a string that includes instructions on how to + /// trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, + /// user settings, and localization. + #[serde(skip_serializing_if = "Option::is_none")] + pub tooltip: Option, + + /// A data entry field that is preserved on a document link between a DocumentLinkRequest + /// and a DocumentLinkResolveRequest. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} diff --git a/helix-lsp-types/src/document_symbols.rs b/helix-lsp-types/src/document_symbols.rs new file mode 100644 index 000000000..3f482e166 --- /dev/null +++ b/helix-lsp-types/src/document_symbols.rs @@ -0,0 +1,134 @@ +use crate::{ + Location, PartialResultParams, Range, SymbolKind, SymbolKindCapability, TextDocumentIdentifier, + WorkDoneProgressParams, +}; + +use crate::{SymbolTag, TagSupport}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentSymbolClientCapabilities { + /// This capability supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Specific capabilities for the `SymbolKind`. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_kind: Option, + + /// The client support hierarchical document symbols. + #[serde(skip_serializing_if = "Option::is_none")] + pub hierarchical_document_symbol_support: Option, + + /// The client supports tags on `SymbolInformation`. Tags are supported on + /// `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true. + /// Clients supporting tags have to handle unknown tags gracefully. + /// + /// @since 3.16.0 + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "TagSupport::deserialize_compat" + )] + pub tag_support: Option>, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DocumentSymbolResponse { + Flat(Vec), + Nested(Vec), +} + +impl From> for DocumentSymbolResponse { + fn from(info: Vec) -> Self { + DocumentSymbolResponse::Flat(info) + } +} + +impl From> for DocumentSymbolResponse { + fn from(symbols: Vec) -> Self { + DocumentSymbolResponse::Nested(symbols) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentSymbolParams { + /// The text document. + pub text_document: TextDocumentIdentifier, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// Represents programming constructs like variables, classes, interfaces etc. +/// that appear in a document. Document symbols can be hierarchical and they have two ranges: +/// one that encloses its definition and one that points to its most interesting range, +/// e.g. the range of an identifier. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentSymbol { + /// The name of this symbol. + pub name: String, + /// More detail for this symbol, e.g the signature of a function. If not provided the + /// name is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The kind of this symbol. + pub kind: SymbolKind, + /// Tags for this completion item. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + /// Indicates if this symbol is deprecated. + #[serde(skip_serializing_if = "Option::is_none")] + #[deprecated(note = "Use tags instead")] + pub deprecated: Option, + /// The range enclosing this symbol not including leading/trailing whitespace but everything else + /// like comments. This information is typically used to determine if the the clients cursor is + /// inside the symbol to reveal in the symbol in the UI. + pub range: Range, + /// The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. + /// Must be contained by the the `range`. + pub selection_range: Range, + /// Children of this symbol, e.g. properties of a class. + #[serde(skip_serializing_if = "Option::is_none")] + pub children: Option>, +} + +/// Represents information about programming constructs like variables, classes, +/// interfaces etc. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SymbolInformation { + /// The name of this symbol. + pub name: String, + + /// The kind of this symbol. + pub kind: SymbolKind, + + /// Tags for this completion item. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + + /// Indicates if this symbol is deprecated. + #[serde(skip_serializing_if = "Option::is_none")] + #[deprecated(note = "Use tags instead")] + pub deprecated: Option, + + /// The location of this symbol. + pub location: Location, + + /// The name of the symbol containing this symbol. + #[serde(skip_serializing_if = "Option::is_none")] + pub container_name: Option, +} diff --git a/helix-lsp-types/src/error_codes.rs b/helix-lsp-types/src/error_codes.rs new file mode 100644 index 000000000..f09fae168 --- /dev/null +++ b/helix-lsp-types/src/error_codes.rs @@ -0,0 +1,54 @@ +//! In this module we only define constants for lsp specific error codes. +//! There are other error codes that are defined in the +//! [JSON RPC specification](https://www.jsonrpc.org/specification#error_object). + +/// Defined in the LSP specification but in the range reserved for JSON-RPC error codes, +/// namely the -32099 to -32000 "Reserved for implementation-defined server-errors." range. +/// The code has, nonetheless, been left in this range for backwards compatibility reasons. +pub const SERVER_NOT_INITIALIZED: i64 = -32002; + +/// Defined in the LSP specification but in the range reserved for JSON-RPC error codes, +/// namely the -32099 to -32000 "Reserved for implementation-defined server-errors." range. +/// The code has, nonetheless, left in this range for backwards compatibility reasons. +pub const UNKNOWN_ERROR_CODE: i64 = -32001; + +/// This is the start range of LSP reserved error codes. +/// It doesn't denote a real error code. +/// +/// @since 3.16.0 +pub const LSP_RESERVED_ERROR_RANGE_START: i64 = -32899; + +/// A request failed but it was syntactically correct, e.g the +/// method name was known and the parameters were valid. The error +/// message should contain human readable information about why +/// the request failed. +/// +/// @since 3.17.0 +pub const REQUEST_FAILED: i64 = -32803; + +/// The server cancelled the request. This error code should +/// only be used for requests that explicitly support being +/// server cancellable. +/// +/// @since 3.17.0 +pub const SERVER_CANCELLED: i64 = -32802; + +/// The server detected that the content of a document got +/// modified outside normal conditions. A server should +/// NOT send this error code if it detects a content change +/// in it unprocessed messages. The result even computed +/// on an older state might still be useful for the client. +/// +/// If a client decides that a result is not of any use anymore +/// the client should cancel the request. +pub const CONTENT_MODIFIED: i64 = -32801; + +/// The client has canceled a request and a server as detected +/// the cancel. +pub const REQUEST_CANCELLED: i64 = -32800; + +/// This is the end range of LSP reserved error codes. +/// It doesn't denote a real error code. +/// +/// @since 3.16.0 +pub const LSP_RESERVED_ERROR_RANGE_END: i64 = -32800; diff --git a/helix-lsp-types/src/file_operations.rs b/helix-lsp-types/src/file_operations.rs new file mode 100644 index 000000000..52572f931 --- /dev/null +++ b/helix-lsp-types/src/file_operations.rs @@ -0,0 +1,213 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFileOperationsClientCapabilities { + /// Whether the client supports dynamic registration for file + /// requests/notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The client has support for sending didCreateFiles notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_create: Option, + + /// The server is interested in receiving willCreateFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_create: Option, + + /// The server is interested in receiving didRenameFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_rename: Option, + + /// The server is interested in receiving willRenameFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_rename: Option, + + /// The server is interested in receiving didDeleteFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_delete: Option, + + /// The server is interested in receiving willDeleteFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_delete: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFileOperationsServerCapabilities { + /// The server is interested in receiving didCreateFiles + /// notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_create: Option, + + /// The server is interested in receiving willCreateFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_create: Option, + + /// The server is interested in receiving didRenameFiles + /// notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_rename: Option, + + /// The server is interested in receiving willRenameFiles requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_rename: Option, + + /// The server is interested in receiving didDeleteFiles file + /// notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_delete: Option, + + /// The server is interested in receiving willDeleteFiles file + /// requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_delete: Option, +} + +/// The options to register for file operations. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileOperationRegistrationOptions { + /// The actual filters. + pub filters: Vec, +} + +/// A filter to describe in which file operation requests or notifications +/// the server is interested in. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileOperationFilter { + /// A Uri like `file` or `untitled`. + pub scheme: Option, + + /// The actual file operation pattern. + pub pattern: FileOperationPattern, +} + +/// A pattern kind describing if a glob pattern matches a file a folder or +/// both. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FileOperationPatternKind { + /// The pattern matches a file only. + File, + + /// The pattern matches a folder only. + Folder, +} + +/// Matching options for the file operation pattern. +/// +/// @since 3.16.0 +/// +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileOperationPatternOptions { + /// The pattern should be matched ignoring casing. + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_case: Option, +} + +/// A pattern to describe in which file operation requests or notifications +/// the server is interested in. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileOperationPattern { + /// The glob pattern to match. Glob patterns can have the following syntax: + /// - `*` to match one or more characters in a path segment + /// - `?` to match on one character in a path segment + /// - `**` to match any number of path segments, including none + /// - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript + /// and JavaScript files) + /// - `[]` to declare a range of characters to match in a path segment + /// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + /// - `[!...]` to negate a range of characters to match in a path segment + /// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but + /// not `example.0`) + pub glob: String, + + /// Whether to match files or folders with this pattern. + /// + /// Matches both if undefined. + #[serde(skip_serializing_if = "Option::is_none")] + pub matches: Option, + + /// Additional options used during matching. + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, +} + +/// The parameters sent in notifications/requests for user-initiated creation +/// of files. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateFilesParams { + /// An array of all files/folders created in this operation. + pub files: Vec, +} +/// Represents information on a file/folder create. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileCreate { + /// A file:// URI for the location of the file/folder being created. + pub uri: String, +} + +/// The parameters sent in notifications/requests for user-initiated renames +/// of files. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameFilesParams { + /// An array of all files/folders renamed in this operation. When a folder + /// is renamed, only the folder will be included, and not its children. + pub files: Vec, +} + +/// Represents information on a file/folder rename. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileRename { + /// A file:// URI for the original location of the file/folder being renamed. + pub old_uri: String, + + /// A file:// URI for the new location of the file/folder being renamed. + pub new_uri: String, +} + +/// The parameters sent in notifications/requests for user-initiated deletes +/// of files. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteFilesParams { + /// An array of all files/folders deleted in this operation. + pub files: Vec, +} + +/// Represents information on a file/folder delete. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileDelete { + /// A file:// URI for the location of the file/folder being deleted. + pub uri: String, +} diff --git a/helix-lsp-types/src/folding_range.rs b/helix-lsp-types/src/folding_range.rs new file mode 100644 index 000000000..c9109ebf3 --- /dev/null +++ b/helix-lsp-types/src/folding_range.rs @@ -0,0 +1,145 @@ +use crate::{ + PartialResultParams, StaticTextDocumentColorProviderOptions, TextDocumentIdentifier, + WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FoldingRangeParams { + /// The text document. + pub text_document: TextDocumentIdentifier, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum FoldingRangeProviderCapability { + Simple(bool), + FoldingProvider(FoldingProviderOptions), + Options(StaticTextDocumentColorProviderOptions), +} + +impl From for FoldingRangeProviderCapability { + fn from(from: StaticTextDocumentColorProviderOptions) -> Self { + Self::Options(from) + } +} + +impl From for FoldingRangeProviderCapability { + fn from(from: FoldingProviderOptions) -> Self { + Self::FoldingProvider(from) + } +} + +impl From for FoldingRangeProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct FoldingProviderOptions {} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FoldingRangeKindCapability { + /// The folding range kind values the client supports. When this + /// property exists the client also guarantees that it will + /// handle values outside its set gracefully and falls back + /// to a default value when unknown. + #[serde(skip_serializing_if = "Option::is_none")] + pub value_set: Option>, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FoldingRangeCapability { + /// If set, the client signals that it supports setting collapsedText on + /// folding ranges to display custom labels instead of the default text. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub collapsed_text: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FoldingRangeClientCapabilities { + /// Whether implementation supports dynamic registration for folding range providers. If this is set to `true` + /// the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)` + /// return value for the corresponding server capability as well. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The maximum number of folding ranges that the client prefers to receive per document. The value serves as a + /// hint, servers are free to follow the limit. + #[serde(skip_serializing_if = "Option::is_none")] + pub range_limit: Option, + + /// If set, the client signals that it only supports folding complete lines. If set, client will + /// ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. + #[serde(skip_serializing_if = "Option::is_none")] + pub line_folding_only: Option, + + /// Specific options for the folding range kind. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub folding_range_kind: Option, + + /// Specific options for the folding range. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub folding_range: Option, +} + +/// Enum of known range kinds +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FoldingRangeKind { + /// Folding range for a comment + Comment, + /// Folding range for a imports or includes + Imports, + /// Folding range for a region (e.g. `#region`) + Region, +} + +/// Represents a folding range. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FoldingRange { + /// The zero-based line number from where the folded range starts. + pub start_line: u32, + + /// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + #[serde(skip_serializing_if = "Option::is_none")] + pub start_character: Option, + + /// The zero-based line number where the folded range ends. + pub end_line: u32, + + /// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_character: Option, + + /// Describes the kind of the folding range such as `comment' or 'region'. The kind + /// is used to categorize folding ranges and used by commands like 'Fold all comments'. See + /// [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + + /// The text that the client should show when the specified range is + /// collapsed. If not defined or not supported by the client, a default + /// will be chosen by the client. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub collapsed_text: Option, +} diff --git a/helix-lsp-types/src/formatting.rs b/helix-lsp-types/src/formatting.rs new file mode 100644 index 000000000..4e5593c83 --- /dev/null +++ b/helix-lsp-types/src/formatting.rs @@ -0,0 +1,153 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + DocumentSelector, DynamicRegistrationClientCapabilities, Range, TextDocumentIdentifier, + TextDocumentPositionParams, WorkDoneProgressParams, +}; + +use std::collections::HashMap; + +pub type DocumentFormattingClientCapabilities = DynamicRegistrationClientCapabilities; +pub type DocumentRangeFormattingClientCapabilities = DynamicRegistrationClientCapabilities; +pub type DocumentOnTypeFormattingClientCapabilities = DynamicRegistrationClientCapabilities; + +/// Format document on type options +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentOnTypeFormattingOptions { + /// A character on which formatting should be triggered, like `}`. + pub first_trigger_character: String, + + /// More trigger characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub more_trigger_character: Option>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentFormattingParams { + /// The document to format. + pub text_document: TextDocumentIdentifier, + + /// The format options. + pub options: FormattingOptions, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +/// Value-object describing what options formatting should use. +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FormattingOptions { + /// Size of a tab in spaces. + pub tab_size: u32, + + /// Prefer spaces over tabs. + pub insert_spaces: bool, + + /// Signature for further properties. + #[serde(flatten)] + pub properties: HashMap, + + /// Trim trailing whitespace on a line. + #[serde(skip_serializing_if = "Option::is_none")] + pub trim_trailing_whitespace: Option, + + /// Insert a newline character at the end of the file if one does not exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_final_newline: Option, + + /// Trim all newlines after the final newline at the end of the file. + #[serde(skip_serializing_if = "Option::is_none")] + pub trim_final_newlines: Option, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum FormattingProperty { + Bool(bool), + Number(i32), + String(String), +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentRangeFormattingParams { + /// The document to format. + pub text_document: TextDocumentIdentifier, + + /// The range to format + pub range: Range, + + /// The format options + pub options: FormattingOptions, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentOnTypeFormattingParams { + /// Text Document and Position fields. + #[serde(flatten)] + pub text_document_position: TextDocumentPositionParams, + + /// The character that has been typed. + pub ch: String, + + /// The format options. + pub options: FormattingOptions, +} + +/// Extends TextDocumentRegistrationOptions +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentOnTypeFormattingRegistrationOptions { + /// A document selector to identify the scope of the registration. If set to null + /// the document selector provided on the client side will be used. + pub document_selector: Option, + + /// A character on which formatting should be triggered, like `}`. + pub first_trigger_character: String, + + /// More trigger characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub more_trigger_character: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::test_serialization; + + #[test] + fn formatting_options() { + test_serialization( + &FormattingOptions { + tab_size: 123, + insert_spaces: true, + properties: HashMap::new(), + trim_trailing_whitespace: None, + insert_final_newline: None, + trim_final_newlines: None, + }, + r#"{"tabSize":123,"insertSpaces":true}"#, + ); + + test_serialization( + &FormattingOptions { + tab_size: 123, + insert_spaces: true, + properties: vec![("prop".to_string(), FormattingProperty::Number(1))] + .into_iter() + .collect(), + trim_trailing_whitespace: None, + insert_final_newline: None, + trim_final_newlines: None, + }, + r#"{"tabSize":123,"insertSpaces":true,"prop":1}"#, + ); + } +} diff --git a/helix-lsp-types/src/hover.rs b/helix-lsp-types/src/hover.rs new file mode 100644 index 000000000..01bd2f8d1 --- /dev/null +++ b/helix-lsp-types/src/hover.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + MarkedString, MarkupContent, MarkupKind, Range, TextDocumentPositionParams, + TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams, +}; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HoverClientCapabilities { + /// Whether completion supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Client supports the follow content formats for the content + /// property. The order describes the preferred format of the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub content_format: Option>, +} + +/// Hover options. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HoverOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HoverRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub hover_options: HoverOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum HoverProviderCapability { + Simple(bool), + Options(HoverOptions), +} + +impl From for HoverProviderCapability { + fn from(from: HoverOptions) -> Self { + Self::Options(from) + } +} + +impl From for HoverProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HoverParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +/// The result of a hover request. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct Hover { + /// The hover's content + pub contents: HoverContents, + /// An optional range is a range inside a text document + /// that is used to visualize a hover, e.g. by changing the background color. + #[serde(skip_serializing_if = "Option::is_none")] + pub range: Option, +} + +/// Hover contents could be single entry or multiple entries. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum HoverContents { + Scalar(MarkedString), + Array(Vec), + Markup(MarkupContent), +} diff --git a/helix-lsp-types/src/inlay_hint.rs b/helix-lsp-types/src/inlay_hint.rs new file mode 100644 index 000000000..171f3c9d1 --- /dev/null +++ b/helix-lsp-types/src/inlay_hint.rs @@ -0,0 +1,281 @@ +use crate::{ + Command, LSPAny, Location, MarkupContent, Position, Range, StaticRegistrationOptions, + TextDocumentIdentifier, TextDocumentRegistrationOptions, TextEdit, WorkDoneProgressOptions, + WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum InlayHintServerCapabilities { + Options(InlayHintOptions), + RegistrationOptions(InlayHintRegistrationOptions), +} + +/// Inlay hint client capabilities. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintClientCapabilities { + /// Whether inlay hints support dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Indicates which properties a client can resolve lazily on a inlay + /// hint. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_support: Option, +} + +/// Inlay hint options used during static registration. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, + + /// The server provides support to resolve additional + /// information for an inlay hint item. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_provider: Option, +} + +/// Inlay hint options used during static or dynamic registration. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintRegistrationOptions { + #[serde(flatten)] + pub inlay_hint_options: InlayHintOptions, + + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +/// A parameter literal used in inlay hint requests. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintParams { + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The visible document range for which inlay hints should be computed. + pub range: Range, +} + +/// Inlay hint information. +/// +/// @since 3.17.0 +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHint { + /// The position of this hint. + pub position: Position, + + /// The label of this hint. A human readable string or an array of + /// InlayHintLabelPart label parts. + /// + /// *Note* that neither the string nor the label part can be empty. + pub label: InlayHintLabel, + + /// The kind of this hint. Can be omitted in which case the client + /// should fall back to a reasonable default. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + + /// Optional text edits that are performed when accepting this inlay hint. + /// + /// *Note* that edits are expected to change the document so that the inlay + /// hint (or its nearest variant) is now part of the document and the inlay + /// hint itself is now obsolete. + /// + /// Depending on the client capability `inlayHint.resolveSupport` clients + /// might resolve this property late using the resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub text_edits: Option>, + + /// The tooltip text when you hover over this item. + /// + /// Depending on the client capability `inlayHint.resolveSupport` clients + /// might resolve this property late using the resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub tooltip: Option, + + /// Render padding before the hint. + /// + /// Note: Padding should use the editor's background color, not the + /// background color of the hint itself. That means padding can be used + /// to visually align/separate an inlay hint. + #[serde(skip_serializing_if = "Option::is_none")] + pub padding_left: Option, + + /// Render padding after the hint. + /// + /// Note: Padding should use the editor's background color, not the + /// background color of the hint itself. That means padding can be used + /// to visually align/separate an inlay hint. + #[serde(skip_serializing_if = "Option::is_none")] + pub padding_right: Option, + + /// A data entry field that is preserved on a inlay hint between + /// a `textDocument/inlayHint` and a `inlayHint/resolve` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InlayHintLabel { + String(String), + LabelParts(Vec), +} + +impl From for InlayHintLabel { + #[inline] + fn from(from: String) -> Self { + Self::String(from) + } +} + +impl From> for InlayHintLabel { + #[inline] + fn from(from: Vec) -> Self { + Self::LabelParts(from) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InlayHintTooltip { + String(String), + MarkupContent(MarkupContent), +} + +impl From for InlayHintTooltip { + #[inline] + fn from(from: String) -> Self { + Self::String(from) + } +} + +impl From for InlayHintTooltip { + #[inline] + fn from(from: MarkupContent) -> Self { + Self::MarkupContent(from) + } +} + +/// An inlay hint label part allows for interactive and composite labels +/// of inlay hints. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintLabelPart { + /// The value of this label part. + pub value: String, + + /// The tooltip text when you hover over this label part. Depending on + /// the client capability `inlayHint.resolveSupport` clients might resolve + /// this property late using the resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub tooltip: Option, + + /// An optional source code location that represents this + /// label part. + /// + /// The editor will use this location for the hover and for code navigation + /// features: This part will become a clickable link that resolves to the + /// definition of the symbol at the given location (not necessarily the + /// location itself), it shows the hover that shows at the given location, + /// and it shows a context menu with further code navigation commands. + /// + /// Depending on the client capability `inlayHint.resolveSupport` clients + /// might resolve this property late using the resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + + /// An optional command for this label part. + /// + /// Depending on the client capability `inlayHint.resolveSupport` clients + /// might resolve this property late using the resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InlayHintLabelPartTooltip { + String(String), + MarkupContent(MarkupContent), +} + +impl From for InlayHintLabelPartTooltip { + #[inline] + fn from(from: String) -> Self { + Self::String(from) + } +} + +impl From for InlayHintLabelPartTooltip { + #[inline] + fn from(from: MarkupContent) -> Self { + Self::MarkupContent(from) + } +} + +/// Inlay hint kinds. +/// +/// @since 3.17.0 +#[derive(Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InlayHintKind(i32); +lsp_enum! { +impl InlayHintKind { + /// An inlay hint that for a type annotation. + pub const TYPE: InlayHintKind = InlayHintKind(1); + + /// An inlay hint that is for a parameter. + pub const PARAMETER: InlayHintKind = InlayHintKind(2); +} +} + +/// Inlay hint client capabilities. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintResolveClientCapabilities { + /// The properties that a client can resolve lazily. + pub properties: Vec, +} + +/// Client workspace capabilities specific to inlay hints. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintWorkspaceClientCapabilities { + /// Whether the client implementation supports a refresh request sent from + /// the server to the client. + /// + /// Note that this event is global and will force the client to refresh all + /// inlay hints currently shown. It should be used with absolute care and + /// is useful for situation where a server for example detects a project wide + /// change that requires such a calculation. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_support: Option, +} + +// TODO(sno2): add tests once stabilized diff --git a/helix-lsp-types/src/inline_completion.rs b/helix-lsp-types/src/inline_completion.rs new file mode 100644 index 000000000..8289858ad --- /dev/null +++ b/helix-lsp-types/src/inline_completion.rs @@ -0,0 +1,162 @@ +use crate::{ + Command, InsertTextFormat, Range, StaticRegistrationOptions, TextDocumentPositionParams, + TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; + +/// Client capabilities specific to inline completions. +/// +/// @since 3.18.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineCompletionClientCapabilities { + /// Whether implementation supports dynamic registration for inline completion providers. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, +} + +/// Inline completion options used during static registration. +/// +/// @since 3.18.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct InlineCompletionOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +/// Inline completion options used during static or dynamic registration. +/// +// @since 3.18.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct InlineCompletionRegistrationOptions { + #[serde(flatten)] + pub inline_completion_options: InlineCompletionOptions, + + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +/// A parameter literal used in inline completion requests. +/// +/// @since 3.18.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineCompletionParams { + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub text_document_position: TextDocumentPositionParams, + + /// Additional information about the context in which inline completions were requested. + pub context: InlineCompletionContext, +} + +/// Describes how an [`InlineCompletionItemProvider`] was triggered. +/// +/// @since 3.18.0 +#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] +pub struct InlineCompletionTriggerKind(i32); +lsp_enum! { +impl InlineCompletionTriggerKind { + /// Completion was triggered explicitly by a user gesture. + /// Return multiple completion items to enable cycling through them. + pub const Invoked: InlineCompletionTriggerKind = InlineCompletionTriggerKind(1); + + /// Completion was triggered automatically while editing. + /// It is sufficient to return a single completion item in this case. + pub const Automatic: InlineCompletionTriggerKind = InlineCompletionTriggerKind(2); +} +} + +/// Describes the currently selected completion item. +/// +/// @since 3.18.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct SelectedCompletionInfo { + /// The range that will be replaced if this completion item is accepted. + pub range: Range, + /// The text the range will be replaced with if this completion is + /// accepted. + pub text: String, +} + +/// Provides information about the context in which an inline completion was +/// requested. +/// +/// @since 3.18.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineCompletionContext { + /// Describes how the inline completion was triggered. + pub trigger_kind: InlineCompletionTriggerKind, + /// Provides information about the currently selected item in the + /// autocomplete widget if it is visible. + /// + /// If set, provided inline completions must extend the text of the + /// selected item and use the same range, otherwise they are not shown as + /// preview. + /// As an example, if the document text is `console.` and the selected item + /// is `.log` replacing the `.` in the document, the inline completion must + /// also replace `.` and start with `.log`, for example `.log()`. + /// + /// Inline completion providers are requested again whenever the selected + /// item changes. + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_completion_info: Option, +} + +/// InlineCompletion response can be multiple completion items, or a list of completion items +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum InlineCompletionResponse { + Array(Vec), + List(InlineCompletionList), +} + +/// Represents a collection of [`InlineCompletionItem`] to be presented in the editor. +/// +/// @since 3.18.0 +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct InlineCompletionList { + /// The inline completion items + pub items: Vec, +} + +/// An inline completion item represents a text snippet that is proposed inline +/// to complete text that is being typed. +/// +/// @since 3.18.0 +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineCompletionItem { + /// The text to replace the range with. Must be set. + /// Is used both for the preview and the accept operation. + pub insert_text: String, + /// A text that is used to decide if this inline completion should be + /// shown. When `falsy` the [`InlineCompletionItem::insertText`] is + /// used. + /// + /// An inline completion is shown if the text to replace is a prefix of the + /// filter text. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter_text: Option, + /// The range to replace. + /// Must begin and end on the same line. + /// + /// Prefer replacements over insertions to provide a better experience when + /// the user deletes typed text. + #[serde(skip_serializing_if = "Option::is_none")] + pub range: Option, + /// An optional command that is executed *after* inserting this + /// completion. + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + /// The format of the insert text. The format applies to the `insertText`. + /// If omitted defaults to `InsertTextFormat.PlainText`. + #[serde(skip_serializing_if = "Option::is_none")] + pub insert_text_format: Option, +} diff --git a/helix-lsp-types/src/inline_value.rs b/helix-lsp-types/src/inline_value.rs new file mode 100644 index 000000000..dd29fbbf9 --- /dev/null +++ b/helix-lsp-types/src/inline_value.rs @@ -0,0 +1,217 @@ +use crate::{ + DynamicRegistrationClientCapabilities, Range, StaticRegistrationOptions, + TextDocumentIdentifier, TextDocumentRegistrationOptions, WorkDoneProgressOptions, + WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; + +pub type InlineValueClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InlineValueServerCapabilities { + Options(InlineValueOptions), + RegistrationOptions(InlineValueRegistrationOptions), +} + +/// Inline value options used during static registration. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct InlineValueOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +/// Inline value options used during static or dynamic registration. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct InlineValueRegistrationOptions { + #[serde(flatten)] + pub inline_value_options: InlineValueOptions, + + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +/// A parameter literal used in inline value requests. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineValueParams { + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The document range for which inline values should be computed. + pub range: Range, + + /// Additional information about the context in which inline values were + /// requested. + pub context: InlineValueContext, +} + +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineValueContext { + /// The stack frame (as a DAP Id) where the execution has stopped. + pub frame_id: i32, + + /// The document range where execution has stopped. + /// Typically the end position of the range denotes the line where the + /// inline values are shown. + pub stopped_location: Range, +} + +/// Provide inline value as text. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct InlineValueText { + /// The document range for which the inline value applies. + pub range: Range, + + /// The text of the inline value. + pub text: String, +} + +/// Provide inline value through a variable lookup. +/// +/// If only a range is specified, the variable name will be extracted from +/// the underlying document. +/// +/// An optional variable name can be used to override the extracted name. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineValueVariableLookup { + /// The document range for which the inline value applies. + /// The range is used to extract the variable name from the underlying + /// document. + pub range: Range, + + /// If specified the name of the variable to look up. + #[serde(skip_serializing_if = "Option::is_none")] + pub variable_name: Option, + + /// How to perform the lookup. + pub case_sensitive_lookup: bool, +} + +/// Provide an inline value through an expression evaluation. +/// +/// If only a range is specified, the expression will be extracted from the +/// underlying document. +/// +/// An optional expression can be used to override the extracted expression. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineValueEvaluatableExpression { + /// The document range for which the inline value applies. + /// The range is used to extract the evaluatable expression from the + /// underlying document. + pub range: Range, + + /// If specified the expression overrides the extracted expression. + #[serde(skip_serializing_if = "Option::is_none")] + pub expression: Option, +} + +/// Inline value information can be provided by different means: +/// - directly as a text value (class InlineValueText). +/// - as a name to use for a variable lookup (class InlineValueVariableLookup) +/// - as an evaluatable expression (class InlineValueEvaluatableExpression) +/// The InlineValue types combines all inline value types into one type. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InlineValue { + Text(InlineValueText), + VariableLookup(InlineValueVariableLookup), + EvaluatableExpression(InlineValueEvaluatableExpression), +} + +impl From for InlineValue { + #[inline] + fn from(from: InlineValueText) -> Self { + Self::Text(from) + } +} + +impl From for InlineValue { + #[inline] + fn from(from: InlineValueVariableLookup) -> Self { + Self::VariableLookup(from) + } +} + +impl From for InlineValue { + #[inline] + fn from(from: InlineValueEvaluatableExpression) -> Self { + Self::EvaluatableExpression(from) + } +} + +/// Client workspace capabilities specific to inline values. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +/// +/// @since 3.17.0 +#[serde(rename_all = "camelCase")] +pub struct InlineValueWorkspaceClientCapabilities { + /// Whether the client implementation supports a refresh request sent from + /// the server to the client. + /// + /// Note that this event is global and will force the client to refresh all + /// inline values currently shown. It should be used with absolute care and + /// is useful for situation where a server for example detect a project wide + /// change that requires such a calculation. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_support: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::test_serialization; + use crate::Position; + + #[test] + fn inline_values() { + test_serialization( + &InlineValueText { + range: Range::new(Position::new(0, 0), Position::new(0, 4)), + text: "one".to_owned(), + }, + r#"{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":4}},"text":"one"}"#, + ); + + test_serialization( + &InlineValue::VariableLookup(InlineValueVariableLookup { + range: Range::new(Position::new(1, 0), Position::new(1, 4)), + variable_name: None, + case_sensitive_lookup: false, + }), + r#"{"range":{"start":{"line":1,"character":0},"end":{"line":1,"character":4}},"caseSensitiveLookup":false}"#, + ); + + test_serialization( + &InlineValue::EvaluatableExpression(InlineValueEvaluatableExpression { + range: Range::new(Position::new(2, 0), Position::new(2, 4)), + expression: None, + }), + r#"{"range":{"start":{"line":2,"character":0},"end":{"line":2,"character":4}}}"#, + ); + } +} diff --git a/helix-lsp-types/src/lib.rs b/helix-lsp-types/src/lib.rs new file mode 100644 index 000000000..e38fae205 --- /dev/null +++ b/helix-lsp-types/src/lib.rs @@ -0,0 +1,2880 @@ +/*! + +Language Server Protocol types for Rust. + +Based on: + +This library uses the URL crate for parsing URIs. Note that there is +some confusion on the meaning of URLs vs URIs: +. According to that +information, on the classical sense of "URLs", "URLs" are a subset of +URIs, But on the modern/new meaning of URLs, they are the same as +URIs. The important take-away aspect is that the URL crate should be +able to parse any URI, such as `urn:isbn:0451450523`. + + +*/ +#![allow(non_upper_case_globals)] +#![forbid(unsafe_code)] +#[macro_use] +extern crate bitflags; + +use std::{collections::HashMap, fmt::Debug}; + +use serde::{de, de::Error as Error_, Deserialize, Serialize}; +use serde_json::Value; +pub use url::Url; + +// Large enough to contain any enumeration name defined in this crate +type PascalCaseBuf = [u8; 32]; +const fn fmt_pascal_case_const(name: &str) -> (PascalCaseBuf, usize) { + let mut buf = [0; 32]; + let mut buf_i = 0; + let mut name_i = 0; + let name = name.as_bytes(); + while name_i < name.len() { + let first = name[name_i]; + name_i += 1; + + buf[buf_i] = first; + buf_i += 1; + + while name_i < name.len() { + let rest = name[name_i]; + name_i += 1; + if rest == b'_' { + break; + } + + buf[buf_i] = rest.to_ascii_lowercase(); + buf_i += 1; + } + } + (buf, buf_i) +} + +fn fmt_pascal_case(f: &mut std::fmt::Formatter<'_>, name: &str) -> std::fmt::Result { + for word in name.split('_') { + let mut chars = word.chars(); + let first = chars.next().unwrap(); + write!(f, "{}", first)?; + for rest in chars { + write!(f, "{}", rest.to_lowercase())?; + } + } + Ok(()) +} + +macro_rules! lsp_enum { + (impl $typ: ident { $( $(#[$attr:meta])* pub const $name: ident : $enum_type: ty = $value: expr; )* }) => { + impl $typ { + $( + $(#[$attr])* + pub const $name: $enum_type = $value; + )* + } + + impl std::fmt::Debug for $typ { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + $( + Self::$name => crate::fmt_pascal_case(f, stringify!($name)), + )* + _ => write!(f, "{}({})", stringify!($typ), self.0), + } + } + } + + impl std::convert::TryFrom<&str> for $typ { + type Error = &'static str; + fn try_from(value: &str) -> Result { + match () { + $( + _ if { + const X: (crate::PascalCaseBuf, usize) = crate::fmt_pascal_case_const(stringify!($name)); + let (buf, len) = X; + &buf[..len] == value.as_bytes() + } => Ok(Self::$name), + )* + _ => Err("unknown enum variant"), + } + } + } + + } +} + +pub mod error_codes; +pub mod notification; +pub mod request; + +mod call_hierarchy; +pub use call_hierarchy::*; + +mod code_action; +pub use code_action::*; + +mod code_lens; +pub use code_lens::*; + +mod color; +pub use color::*; + +mod completion; +pub use completion::*; + +mod document_diagnostic; +pub use document_diagnostic::*; + +mod document_highlight; +pub use document_highlight::*; + +mod document_link; +pub use document_link::*; + +mod document_symbols; +pub use document_symbols::*; + +mod file_operations; +pub use file_operations::*; + +mod folding_range; +pub use folding_range::*; + +mod formatting; +pub use formatting::*; + +mod hover; +pub use hover::*; + +mod inlay_hint; +pub use inlay_hint::*; + +mod inline_value; +pub use inline_value::*; + +#[cfg(feature = "proposed")] +mod inline_completion; +#[cfg(feature = "proposed")] +pub use inline_completion::*; + +mod moniker; +pub use moniker::*; + +mod progress; +pub use progress::*; + +mod references; +pub use references::*; + +mod rename; +pub use rename::*; + +pub mod selection_range; +pub use selection_range::*; + +mod semantic_tokens; +pub use semantic_tokens::*; + +mod signature_help; +pub use signature_help::*; + +mod type_hierarchy; +pub use type_hierarchy::*; + +mod linked_editing; +pub use linked_editing::*; + +mod window; +pub use window::*; + +mod workspace_diagnostic; +pub use workspace_diagnostic::*; + +mod workspace_folders; +pub use workspace_folders::*; + +mod workspace_symbols; +pub use workspace_symbols::*; + +pub mod lsif; + +mod trace; +pub use trace::*; + +/* ----------------- Auxiliary types ----------------- */ + +#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum NumberOrString { + Number(i32), + String(String), +} + +/* ----------------- Cancel support ----------------- */ + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct CancelParams { + /// The request id to cancel. + pub id: NumberOrString, +} + +/* ----------------- Basic JSON Structures ----------------- */ + +/// The LSP any type +/// +/// @since 3.17.0 +pub type LSPAny = serde_json::Value; + +/// LSP object definition. +/// +/// @since 3.17.0 +pub type LSPObject = serde_json::Map; + +/// LSP arrays. +/// +/// @since 3.17.0 +pub type LSPArray = Vec; + +/// Position in a text document expressed as zero-based line and character offset. +/// A position is between two characters like an 'insert' cursor in a editor. +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Deserialize, Serialize, Hash)] +pub struct Position { + /// Line position in a document (zero-based). + pub line: u32, + /// Character offset on a line in a document (zero-based). The meaning of this + /// offset is determined by the negotiated `PositionEncodingKind`. + /// + /// If the character value is greater than the line length it defaults back + /// to the line length. + pub character: u32, +} + +impl Position { + pub fn new(line: u32, character: u32) -> Position { + Position { line, character } + } +} + +/// A range in a text document expressed as (zero-based) start and end positions. +/// A range is comparable to a selection in an editor. Therefore the end position is exclusive. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize, Hash)] +pub struct Range { + /// The range's start position. + pub start: Position, + /// The range's end position. + pub end: Position, +} + +impl Range { + pub fn new(start: Position, end: Position) -> Range { + Range { start, end } + } +} + +/// Represents a location inside a resource, such as a line inside a text file. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Hash)] +pub struct Location { + pub uri: Url, + pub range: Range, +} + +impl Location { + pub fn new(uri: Url, range: Range) -> Location { + Location { uri, range } + } +} + +/// Represents a link between a source and a target location. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LocationLink { + /// Span of the origin of this link. + /// + /// Used as the underlined span for mouse interaction. Defaults to the word range at + /// the mouse position. + #[serde(skip_serializing_if = "Option::is_none")] + pub origin_selection_range: Option, + + /// The target resource identifier of this link. + pub target_uri: Url, + + /// The full target range of this link. + pub target_range: Range, + + /// The span of this link. + pub target_selection_range: Range, +} + +/// A type indicating how positions are encoded, +/// specifically what column offsets mean. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Clone, Deserialize, Serialize)] +pub struct PositionEncodingKind(std::borrow::Cow<'static, str>); + +impl PositionEncodingKind { + /// Character offsets count UTF-8 code units. + pub const UTF8: PositionEncodingKind = PositionEncodingKind::new("utf-8"); + + /// Character offsets count UTF-16 code units. + /// + /// This is the default and must always be supported + /// by servers + pub const UTF16: PositionEncodingKind = PositionEncodingKind::new("utf-16"); + + /// Character offsets count UTF-32 code units. + /// + /// Implementation note: these are the same as Unicode code points, + /// so this `PositionEncodingKind` may also be used for an + /// encoding-agnostic representation of character offsets. + pub const UTF32: PositionEncodingKind = PositionEncodingKind::new("utf-32"); + + pub const fn new(tag: &'static str) -> Self { + PositionEncodingKind(std::borrow::Cow::Borrowed(tag)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for PositionEncodingKind { + fn from(from: String) -> Self { + PositionEncodingKind(std::borrow::Cow::from(from)) + } +} + +impl From<&'static str> for PositionEncodingKind { + fn from(from: &'static str) -> Self { + PositionEncodingKind::new(from) + } +} + +/// Represents a diagnostic, such as a compiler error or warning. +/// Diagnostic objects are only valid in the scope of a resource. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Diagnostic { + /// The range at which the message applies. + pub range: Range, + + /// The diagnostic's severity. Can be omitted. If omitted it is up to the + /// client to interpret diagnostics as error, warning, info or hint. + #[serde(skip_serializing_if = "Option::is_none")] + pub severity: Option, + + /// The diagnostic's code. Can be omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + + /// An optional property to describe the error code. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub code_description: Option, + + /// A human-readable string describing the source of this + /// diagnostic, e.g. 'typescript' or 'super lint'. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + + /// The diagnostic's message. + pub message: String, + + /// An array of related diagnostic information, e.g. when symbol-names within + /// a scope collide all definitions can be marked via this property. + #[serde(skip_serializing_if = "Option::is_none")] + pub related_information: Option>, + + /// Additional metadata about the diagnostic. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + + /// A data entry field that is preserved between a `textDocument/publishDiagnostics` + /// notification and `textDocument/codeAction` request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeDescription { + pub href: Url, +} + +impl Diagnostic { + pub fn new( + range: Range, + severity: Option, + code: Option, + source: Option, + message: String, + related_information: Option>, + tags: Option>, + ) -> Diagnostic { + Diagnostic { + range, + severity, + code, + source, + message, + related_information, + tags, + ..Diagnostic::default() + } + } + + pub fn new_simple(range: Range, message: String) -> Diagnostic { + Self::new(range, None, None, None, message, None, None) + } + + pub fn new_with_code_number( + range: Range, + severity: DiagnosticSeverity, + code_number: i32, + source: Option, + message: String, + ) -> Diagnostic { + let code = Some(NumberOrString::Number(code_number)); + Self::new(range, Some(severity), code, source, message, None, None) + } +} + +/// The protocol currently supports the following diagnostic severities: +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Deserialize, Serialize)] +#[serde(transparent)] +pub struct DiagnosticSeverity(i32); +lsp_enum! { +impl DiagnosticSeverity { + /// Reports an error. + pub const ERROR: DiagnosticSeverity = DiagnosticSeverity(1); + /// Reports a warning. + pub const WARNING: DiagnosticSeverity = DiagnosticSeverity(2); + /// Reports an information. + pub const INFORMATION: DiagnosticSeverity = DiagnosticSeverity(3); + /// Reports a hint. + pub const HINT: DiagnosticSeverity = DiagnosticSeverity(4); +} +} + +/// Represents a related message and source code location for a diagnostic. This +/// should be used to point to code locations that cause or related to a +/// diagnostics, e.g when duplicating a symbol in a scope. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct DiagnosticRelatedInformation { + /// The location of this related diagnostic information. + pub location: Location, + + /// The message of this related diagnostic information. + pub message: String, +} + +/// The diagnostic tags. +#[derive(Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct DiagnosticTag(i32); +lsp_enum! { +impl DiagnosticTag { + /// Unused or unnecessary code. + /// Clients are allowed to render diagnostics with this tag faded out instead of having + /// an error squiggle. + pub const UNNECESSARY: DiagnosticTag = DiagnosticTag(1); + + /// Deprecated or obsolete code. + /// Clients are allowed to rendered diagnostics with this tag strike through. + pub const DEPRECATED: DiagnosticTag = DiagnosticTag(2); +} +} + +/// Represents a reference to a command. Provides a title which will be used to represent a command in the UI. +/// Commands are identified by a string identifier. The recommended way to handle commands is to implement +/// their execution on the server side if the client and server provides the corresponding capabilities. +/// Alternatively the tool extension code could handle the command. +/// The protocol currently doesn’t specify a set of well-known commands. +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct Command { + /// Title of the command, like `save`. + pub title: String, + /// The identifier of the actual command handler. + pub command: String, + /// Arguments that the command handler should be + /// invoked with. + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option>, +} + +impl Command { + pub fn new(title: String, command: String, arguments: Option>) -> Command { + Command { + title, + command, + arguments, + } + } +} + +/// A textual edit applicable to a text document. +/// +/// If n `TextEdit`s are applied to a text document all text edits describe changes to the initial document version. +/// Execution wise text edits should applied from the bottom to the top of the text document. Overlapping text edits +/// are not supported. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextEdit { + /// The range of the text document to be manipulated. To insert + /// text into a document create a range where start === end. + pub range: Range, + /// The string to be inserted. For delete operations use an + /// empty string. + pub new_text: String, +} + +impl TextEdit { + pub fn new(range: Range, new_text: String) -> TextEdit { + TextEdit { range, new_text } + } +} + +/// An identifier referring to a change annotation managed by a workspace +/// edit. +/// +/// @since 3.16.0 +pub type ChangeAnnotationIdentifier = String; + +/// A special text edit with an additional change annotation. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AnnotatedTextEdit { + #[serde(flatten)] + pub text_edit: TextEdit, + + /// The actual annotation + pub annotation_id: ChangeAnnotationIdentifier, +} + +/// Describes textual changes on a single text document. The text document is referred to as a +/// `OptionalVersionedTextDocumentIdentifier` to allow clients to check the text document version before an +/// edit is applied. A `TextDocumentEdit` describes all changes on a version Si and after they are +/// applied move the document to version Si+1. So the creator of a `TextDocumentEdit` doesn't need to +/// sort the array or do any kind of ordering. However the edits must be non overlapping. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentEdit { + /// The text document to change. + pub text_document: OptionalVersionedTextDocumentIdentifier, + + /// The edits to be applied. + /// + /// @since 3.16.0 - support for AnnotatedTextEdit. This is guarded by the + /// client capability `workspace.workspaceEdit.changeAnnotationSupport` + pub edits: Vec>, +} + +/// Additional information that describes document changes. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeAnnotation { + /// A human-readable string describing the actual change. The string + /// is rendered prominent in the user interface. + pub label: String, + + /// A flag which indicates that user confirmation is needed + /// before applying the change. + #[serde(skip_serializing_if = "Option::is_none")] + pub needs_confirmation: Option, + + /// A human-readable string which is rendered less prominent in + /// the user interface. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeAnnotationWorkspaceEditClientCapabilities { + /// Whether the client groups edits with equal labels into tree nodes, + /// for instance all edits labelled with "Changes in Strings" would + /// be a tree node. + #[serde(skip_serializing_if = "Option::is_none")] + pub groups_on_label: Option, +} + +/// Options to create a file. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateFileOptions { + /// Overwrite existing file. Overwrite wins over `ignoreIfExists` + #[serde(skip_serializing_if = "Option::is_none")] + pub overwrite: Option, + /// Ignore if exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_if_exists: Option, +} + +/// Create file operation +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateFile { + /// The resource to create. + pub uri: Url, + /// Additional options + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, + + /// An optional annotation identifier describing the operation. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub annotation_id: Option, +} + +/// Rename file options +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameFileOptions { + /// Overwrite target if existing. Overwrite wins over `ignoreIfExists` + #[serde(skip_serializing_if = "Option::is_none")] + pub overwrite: Option, + /// Ignores if target exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_if_exists: Option, +} + +/// Rename file operation +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameFile { + /// The old (existing) location. + pub old_uri: Url, + /// The new location. + pub new_uri: Url, + /// Rename options. + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, + + /// An optional annotation identifier describing the operation. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub annotation_id: Option, +} + +/// Delete file options +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteFileOptions { + /// Delete the content recursively if a folder is denoted. + #[serde(skip_serializing_if = "Option::is_none")] + pub recursive: Option, + /// Ignore the operation if the file doesn't exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_if_not_exists: Option, + + /// An optional annotation identifier describing the operation. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub annotation_id: Option, +} + +/// Delete file operation +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteFile { + /// The file to delete. + pub uri: Url, + /// Delete options. + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, +} + +/// A workspace edit represents changes to many resources managed in the workspace. +/// The edit should either provide `changes` or `documentChanges`. +/// If the client can handle versioned document edits and if `documentChanges` are present, +/// the latter are preferred over `changes`. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceEdit { + /// Holds changes to existing resources. + #[serde(with = "url_map")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub changes: Option>>, // changes?: { [uri: string]: TextEdit[]; }; + + /// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes + /// are either an array of `TextDocumentEdit`s to express changes to n different text documents + /// where each text document edit addresses a specific version of a text document. Or it can contain + /// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. + /// + /// Whether a client supports versioned document edits is expressed via + /// `workspace.workspaceEdit.documentChanges` client capability. + /// + /// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then + /// only plain `TextEdit`s using the `changes` property are supported. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_changes: Option, + + /// A map of change annotations that can be referenced in + /// `AnnotatedTextEdit`s or create, rename and delete file / folder + /// operations. + /// + /// Whether clients honor this property depends on the client capability + /// `workspace.changeAnnotationSupport`. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub change_annotations: Option>, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum DocumentChanges { + Edits(Vec), + Operations(Vec), +} + +// TODO: Once https://github.com/serde-rs/serde/issues/912 is solved +// we can remove ResourceOp and switch to the following implementation +// of DocumentChangeOperation: +// +// #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +// #[serde(tag = "kind", rename_all="lowercase" )] +// pub enum DocumentChangeOperation { +// Create(CreateFile), +// Rename(RenameFile), +// Delete(DeleteFile), +// +// #[serde(other)] +// Edit(TextDocumentEdit), +// } + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged, rename_all = "lowercase")] +pub enum DocumentChangeOperation { + Op(ResourceOp), + Edit(TextDocumentEdit), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum ResourceOp { + Create(CreateFile), + Rename(RenameFile), + Delete(DeleteFile), +} + +pub type DidChangeConfigurationClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurationParams { + pub items: Vec, +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurationItem { + /// The scope to get the configuration section for. + #[serde(skip_serializing_if = "Option::is_none")] + pub scope_uri: Option, + + ///The configuration section asked for. + #[serde(skip_serializing_if = "Option::is_none")] + pub section: Option, +} + +mod url_map { + use std::fmt; + use std::marker::PhantomData; + + use super::*; + + pub fn deserialize<'de, D, V>(deserializer: D) -> Result>, D::Error> + where + D: serde::Deserializer<'de>, + V: de::DeserializeOwned, + { + struct UrlMapVisitor { + _marker: PhantomData, + } + + impl Default for UrlMapVisitor { + fn default() -> Self { + UrlMapVisitor { + _marker: PhantomData, + } + } + } + impl<'de, V: de::DeserializeOwned> de::Visitor<'de> for UrlMapVisitor { + type Value = HashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("map") + } + + fn visit_map(self, mut visitor: M) -> Result + where + M: de::MapAccess<'de>, + { + let mut values = HashMap::with_capacity(visitor.size_hint().unwrap_or(0)); + + // While there are entries remaining in the input, add them + // into our map. + while let Some((key, value)) = visitor.next_entry::()? { + values.insert(key, value); + } + + Ok(values) + } + } + + struct OptionUrlMapVisitor { + _marker: PhantomData, + } + impl Default for OptionUrlMapVisitor { + fn default() -> Self { + OptionUrlMapVisitor { + _marker: PhantomData, + } + } + } + impl<'de, V: de::DeserializeOwned> de::Visitor<'de> for OptionUrlMapVisitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("option") + } + + #[inline] + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + #[inline] + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + #[inline] + fn visit_some(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer + .deserialize_map(UrlMapVisitor::::default()) + .map(Some) + } + } + + // Instantiate our Visitor and ask the Deserializer to drive + // it over the input data, resulting in an instance of MyMap. + deserializer.deserialize_option(OptionUrlMapVisitor::default()) + } + + pub fn serialize( + changes: &Option>, + serializer: S, + ) -> Result + where + S: serde::Serializer, + V: serde::Serialize, + { + use serde::ser::SerializeMap; + + match *changes { + Some(ref changes) => { + let mut map = serializer.serialize_map(Some(changes.len()))?; + for (k, v) in changes { + map.serialize_entry(k.as_str(), v)?; + } + map.end() + } + None => serializer.serialize_none(), + } + } +} + +impl WorkspaceEdit { + pub fn new(changes: HashMap>) -> WorkspaceEdit { + WorkspaceEdit { + changes: Some(changes), + document_changes: None, + ..Default::default() + } + } +} + +/// Text documents are identified using a URI. On the protocol level, URIs are passed as strings. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct TextDocumentIdentifier { + // !!!!!! Note: + // In the spec VersionedTextDocumentIdentifier extends TextDocumentIdentifier + // This modelled by "mixing-in" TextDocumentIdentifier in VersionedTextDocumentIdentifier, + // so any changes to this type must be effected in the sub-type as well. + /// The text document's URI. + pub uri: Url, +} + +impl TextDocumentIdentifier { + pub fn new(uri: Url) -> TextDocumentIdentifier { + TextDocumentIdentifier { uri } + } +} + +/// An item to transfer a text document from the client to the server. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentItem { + /// The text document's URI. + pub uri: Url, + + /// The text document's language identifier. + pub language_id: String, + + /// The version number of this document (it will strictly increase after each + /// change, including undo/redo). + pub version: i32, + + /// The content of the opened text document. + pub text: String, +} + +impl TextDocumentItem { + pub fn new(uri: Url, language_id: String, version: i32, text: String) -> TextDocumentItem { + TextDocumentItem { + uri, + language_id, + version, + text, + } + } +} + +/// An identifier to denote a specific version of a text document. This information usually flows from the client to the server. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct VersionedTextDocumentIdentifier { + // This field was "mixed-in" from TextDocumentIdentifier + /// The text document's URI. + pub uri: Url, + + /// The version number of this document. + /// + /// The version number of a document will increase after each change, + /// including undo/redo. The number doesn't need to be consecutive. + pub version: i32, +} + +impl VersionedTextDocumentIdentifier { + pub fn new(uri: Url, version: i32) -> VersionedTextDocumentIdentifier { + VersionedTextDocumentIdentifier { uri, version } + } +} + +/// An identifier which optionally denotes a specific version of a text document. This information usually flows from the server to the client +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct OptionalVersionedTextDocumentIdentifier { + // This field was "mixed-in" from TextDocumentIdentifier + /// The text document's URI. + pub uri: Url, + + /// The version number of this document. If an optional versioned text document + /// identifier is sent from the server to the client and the file is not + /// open in the editor (the server has not received an open notification + /// before) the server can send `null` to indicate that the version is + /// known and the content on disk is the master (as specified with document + /// content ownership). + /// + /// The version number of a document will increase after each change, + /// including undo/redo. The number doesn't need to be consecutive. + pub version: Option, +} + +impl OptionalVersionedTextDocumentIdentifier { + pub fn new(uri: Url, version: i32) -> OptionalVersionedTextDocumentIdentifier { + OptionalVersionedTextDocumentIdentifier { + uri, + version: Some(version), + } + } +} + +/// A parameter literal used in requests to pass a text document and a position inside that document. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentPositionParams { + // !!!!!! Note: + // In the spec ReferenceParams extends TextDocumentPositionParams + // This modelled by "mixing-in" TextDocumentPositionParams in ReferenceParams, + // so any changes to this type must be effected in sub-type as well. + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The position inside the text document. + pub position: Position, +} + +impl TextDocumentPositionParams { + pub fn new( + text_document: TextDocumentIdentifier, + position: Position, + ) -> TextDocumentPositionParams { + TextDocumentPositionParams { + text_document, + position, + } + } +} + +/// A document filter denotes a document through properties like language, schema or pattern. +/// Examples are a filter that applies to TypeScript files on disk or a filter the applies to JSON +/// files with name package.json: +/// +/// { language: 'typescript', scheme: 'file' } +/// { language: 'json', pattern: '**/package.json' } +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct DocumentFilter { + /// A language id, like `typescript`. + #[serde(skip_serializing_if = "Option::is_none")] + pub language: Option, + + /// A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme: Option, + + /// A glob pattern, like `*.{ts,js}`. + #[serde(skip_serializing_if = "Option::is_none")] + pub pattern: Option, +} + +/// A document selector is the combination of one or many document filters. +pub type DocumentSelector = Vec; + +// ========================= Actual Protocol ========================= + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct InitializeParams { + /// The process Id of the parent process that started + /// the server. Is null if the process has not been started by another process. + /// If the parent process is not alive then the server should exit (see exit notification) its process. + pub process_id: Option, + + /// The rootPath of the workspace. Is null + /// if no folder is open. + #[serde(skip_serializing_if = "Option::is_none")] + #[deprecated(note = "Use `root_uri` instead when possible")] + pub root_path: Option, + + /// The rootUri of the workspace. Is null if no + /// folder is open. If both `rootPath` and `rootUri` are set + /// `rootUri` wins. + #[serde(default)] + #[deprecated(note = "Use `workspace_folders` instead when possible")] + pub root_uri: Option, + + /// User provided initialization options. + #[serde(skip_serializing_if = "Option::is_none")] + pub initialization_options: Option, + + /// The capabilities provided by the client (editor or tool) + pub capabilities: ClientCapabilities, + + /// The initial trace setting. If omitted trace is disabled ('off'). + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub trace: Option, + + /// The workspace folders configured in the client when the server starts. + /// This property is only available if the client supports workspace folders. + /// It can be `null` if the client supports workspace folders but none are + /// configured. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_folders: Option>, + + /// Information about the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_info: Option, + + /// The locale the client is currently showing the user interface + /// in. This must not necessarily be the locale of the operating + /// system. + /// + /// Uses IETF language tags as the value's syntax + /// (See ) + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + + /// The LSP server may report about initialization progress to the client + /// by using the following work done token if it was passed by the client. + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct ClientInfo { + /// The name of the client as defined by the client. + pub name: String, + /// The client's version as defined by the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, PartialEq, Clone, Copy, Deserialize, Serialize)] +pub struct InitializedParams {} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct GenericRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub options: GenericOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct GenericOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct GenericParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DynamicRegistrationClientCapabilities { + /// This capability supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GotoCapability { + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The client supports additional metadata in the form of definition links. + #[serde(skip_serializing_if = "Option::is_none")] + pub link_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceEditClientCapabilities { + /// The client supports versioned document changes in `WorkspaceEdit`s + #[serde(skip_serializing_if = "Option::is_none")] + pub document_changes: Option, + + /// The resource operations the client supports. Clients should at least + /// support 'create', 'rename' and 'delete' files and folders. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_operations: Option>, + + /// The failure handling strategy of a client if applying the workspace edit fails. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure_handling: Option, + + /// Whether the client normalizes line endings to the client specific + /// setting. + /// If set to `true` the client will normalize line ending characters + /// in a workspace edit to the client specific new line character(s). + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub normalizes_line_endings: Option, + + /// Whether the client in general supports change annotations on text edits, + /// create file, rename file and delete file changes. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub change_annotation_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Copy, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ResourceOperationKind { + Create, + Rename, + Delete, +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub enum FailureHandlingKind { + Abort, + Transactional, + TextOnlyTransactional, + Undo, +} + +/// A symbol kind. +#[derive(Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SymbolKind(i32); +lsp_enum! { +impl SymbolKind { + pub const FILE: SymbolKind = SymbolKind(1); + pub const MODULE: SymbolKind = SymbolKind(2); + pub const NAMESPACE: SymbolKind = SymbolKind(3); + pub const PACKAGE: SymbolKind = SymbolKind(4); + pub const CLASS: SymbolKind = SymbolKind(5); + pub const METHOD: SymbolKind = SymbolKind(6); + pub const PROPERTY: SymbolKind = SymbolKind(7); + pub const FIELD: SymbolKind = SymbolKind(8); + pub const CONSTRUCTOR: SymbolKind = SymbolKind(9); + pub const ENUM: SymbolKind = SymbolKind(10); + pub const INTERFACE: SymbolKind = SymbolKind(11); + pub const FUNCTION: SymbolKind = SymbolKind(12); + pub const VARIABLE: SymbolKind = SymbolKind(13); + pub const CONSTANT: SymbolKind = SymbolKind(14); + pub const STRING: SymbolKind = SymbolKind(15); + pub const NUMBER: SymbolKind = SymbolKind(16); + pub const BOOLEAN: SymbolKind = SymbolKind(17); + pub const ARRAY: SymbolKind = SymbolKind(18); + pub const OBJECT: SymbolKind = SymbolKind(19); + pub const KEY: SymbolKind = SymbolKind(20); + pub const NULL: SymbolKind = SymbolKind(21); + pub const ENUM_MEMBER: SymbolKind = SymbolKind(22); + pub const STRUCT: SymbolKind = SymbolKind(23); + pub const EVENT: SymbolKind = SymbolKind(24); + pub const OPERATOR: SymbolKind = SymbolKind(25); + pub const TYPE_PARAMETER: SymbolKind = SymbolKind(26); +} +} + +/// Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SymbolKindCapability { + /// The symbol kind values the client supports. When this + /// property exists the client also guarantees that it will + /// handle values outside its set gracefully and falls back + /// to a default value when unknown. + /// + /// If this property is not present the client only supports + /// the symbol kinds from `File` to `Array` as defined in + /// the initial version of the protocol. + pub value_set: Option>, +} + +/// Workspace specific client capabilities. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceClientCapabilities { + /// The client supports applying batch edits to the workspace by supporting + /// the request 'workspace/applyEdit' + #[serde(skip_serializing_if = "Option::is_none")] + pub apply_edit: Option, + + /// Capabilities specific to `WorkspaceEdit`s + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_edit: Option, + + /// Capabilities specific to the `workspace/didChangeConfiguration` notification. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_change_configuration: Option, + + /// Capabilities specific to the `workspace/didChangeWatchedFiles` notification. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_change_watched_files: Option, + + /// Capabilities specific to the `workspace/symbol` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol: Option, + + /// Capabilities specific to the `workspace/executeCommand` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub execute_command: Option, + + /// The client has support for workspace folders. + /// + /// @since 3.6.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_folders: Option, + + /// The client supports `workspace/configuration` requests. + /// + /// @since 3.6.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub configuration: Option, + + /// Capabilities specific to the semantic token requests scoped to the workspace. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_tokens: Option, + + /// Capabilities specific to the code lens requests scoped to the workspace. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub code_lens: Option, + + /// The client has support for file requests/notifications. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub file_operations: Option, + + /// Client workspace capabilities specific to inline values. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub inline_value: Option, + + /// Client workspace capabilities specific to inlay hints. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub inlay_hint: Option, + + /// Client workspace capabilities specific to diagnostics. + /// since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostic: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentSyncClientCapabilities { + /// Whether text document synchronization supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The client supports sending will save notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_save: Option, + + /// The client supports sending a will save request and + /// waits for a response providing text edits which will + /// be applied to the document before it is saved. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_save_wait_until: Option, + + /// The client supports did save notifications. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_save: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PublishDiagnosticsClientCapabilities { + /// Whether the clients accepts diagnostics with related information. + #[serde(skip_serializing_if = "Option::is_none")] + pub related_information: Option, + + /// Client supports the tag property to provide meta data about a diagnostic. + /// Clients supporting tags have to handle unknown tags gracefully. + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "TagSupport::deserialize_compat" + )] + pub tag_support: Option>, + + /// Whether the client interprets the version property of the + /// `textDocument/publishDiagnostics` notification's parameter. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub version_support: Option, + + /// Client supports a codeDescription property + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub code_description_support: Option, + + /// Whether code action supports the `data` property which is + /// preserved between a `textDocument/publishDiagnostics` and + /// `textDocument/codeAction` request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub data_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TagSupport { + /// The tags supported by the client. + pub value_set: Vec, +} + +impl TagSupport { + /// Support for deserializing a boolean tag Support, in case it's present. + /// + /// This is currently the case for vscode 1.41.1 + fn deserialize_compat<'de, S>(serializer: S) -> Result>, S::Error> + where + S: serde::Deserializer<'de>, + T: serde::Deserialize<'de>, + { + Ok( + match Option::::deserialize(serializer).map_err(serde::de::Error::custom)? { + Some(Value::Bool(false)) => None, + Some(Value::Bool(true)) => Some(TagSupport { value_set: vec![] }), + Some(other) => { + Some(TagSupport::::deserialize(other).map_err(serde::de::Error::custom)?) + } + None => None, + }, + ) + } +} + +/// Text document specific client capabilities. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentClientCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] + pub synchronization: Option, + /// Capabilities specific to the `textDocument/completion` + #[serde(skip_serializing_if = "Option::is_none")] + pub completion: Option, + + /// Capabilities specific to the `textDocument/hover` + #[serde(skip_serializing_if = "Option::is_none")] + pub hover: Option, + + /// Capabilities specific to the `textDocument/signatureHelp` + #[serde(skip_serializing_if = "Option::is_none")] + pub signature_help: Option, + + /// Capabilities specific to the `textDocument/references` + #[serde(skip_serializing_if = "Option::is_none")] + pub references: Option, + + /// Capabilities specific to the `textDocument/documentHighlight` + #[serde(skip_serializing_if = "Option::is_none")] + pub document_highlight: Option, + + /// Capabilities specific to the `textDocument/documentSymbol` + #[serde(skip_serializing_if = "Option::is_none")] + pub document_symbol: Option, + /// Capabilities specific to the `textDocument/formatting` + #[serde(skip_serializing_if = "Option::is_none")] + pub formatting: Option, + + /// Capabilities specific to the `textDocument/rangeFormatting` + #[serde(skip_serializing_if = "Option::is_none")] + pub range_formatting: Option, + + /// Capabilities specific to the `textDocument/onTypeFormatting` + #[serde(skip_serializing_if = "Option::is_none")] + pub on_type_formatting: Option, + + /// Capabilities specific to the `textDocument/declaration` + #[serde(skip_serializing_if = "Option::is_none")] + pub declaration: Option, + + /// Capabilities specific to the `textDocument/definition` + #[serde(skip_serializing_if = "Option::is_none")] + pub definition: Option, + + /// Capabilities specific to the `textDocument/typeDefinition` + #[serde(skip_serializing_if = "Option::is_none")] + pub type_definition: Option, + + /// Capabilities specific to the `textDocument/implementation` + #[serde(skip_serializing_if = "Option::is_none")] + pub implementation: Option, + + /// Capabilities specific to the `textDocument/codeAction` + #[serde(skip_serializing_if = "Option::is_none")] + pub code_action: Option, + + /// Capabilities specific to the `textDocument/codeLens` + #[serde(skip_serializing_if = "Option::is_none")] + pub code_lens: Option, + + /// Capabilities specific to the `textDocument/documentLink` + #[serde(skip_serializing_if = "Option::is_none")] + pub document_link: Option, + + /// Capabilities specific to the `textDocument/documentColor` and the + /// `textDocument/colorPresentation` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub color_provider: Option, + + /// Capabilities specific to the `textDocument/rename` + #[serde(skip_serializing_if = "Option::is_none")] + pub rename: Option, + + /// Capabilities specific to `textDocument/publishDiagnostics`. + #[serde(skip_serializing_if = "Option::is_none")] + pub publish_diagnostics: Option, + + /// Capabilities specific to `textDocument/foldingRange` requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub folding_range: Option, + + /// Capabilities specific to the `textDocument/selectionRange` request. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub selection_range: Option, + + /// Capabilities specific to `textDocument/linkedEditingRange` requests. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub linked_editing_range: Option, + + /// Capabilities specific to the various call hierarchy requests. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub call_hierarchy: Option, + + /// Capabilities specific to the `textDocument/semanticTokens/*` requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_tokens: Option, + + /// Capabilities specific to the `textDocument/moniker` request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub moniker: Option, + + /// Capabilities specific to the various type hierarchy requests. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub type_hierarchy: Option, + + /// Capabilities specific to the `textDocument/inlineValue` request. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub inline_value: Option, + + /// Capabilities specific to the `textDocument/inlayHint` request. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub inlay_hint: Option, + + /// Capabilities specific to the diagnostic pull model. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostic: Option, + + /// Capabilities specific to the `textDocument/inlineCompletion` request. + /// + /// @since 3.18.0 + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "proposed")] + pub inline_completion: Option, +} + +/// Where ClientCapabilities are currently empty: +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientCapabilities { + /// Workspace specific client capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace: Option, + + /// Text document specific client capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub text_document: Option, + + /// Window specific client capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub window: Option, + + /// General client capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub general: Option, + + /// Unofficial UT8-offsets extension. + /// + /// See https://clangd.llvm.org/extensions.html#utf-8-offsets. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "proposed")] + pub offset_encoding: Option>, + + /// Experimental client capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub experimental: Option, +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GeneralClientCapabilities { + /// Client capabilities specific to regular expressions. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub regular_expressions: Option, + + /// Client capabilities specific to the client's markdown parser. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub markdown: Option, + + /// Client capability that signals how the client handles stale requests (e.g. a request for + /// which the client will not process the response anymore since the information is outdated). + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub stale_request_support: Option, + + /// The position encodings supported by the client. Client and server + /// have to agree on the same position encoding to ensure that offsets + /// (e.g. character position in a line) are interpreted the same on both + /// side. + /// + /// To keep the protocol backwards compatible the following applies: if + /// the value 'utf-16' is missing from the array of position encodings + /// servers can assume that the client supports UTF-16. UTF-16 is + /// therefore a mandatory encoding. + /// + /// If omitted it defaults to ['utf-16']. + /// + /// Implementation considerations: since the conversion from one encoding + /// into another requires the content of the file / line the conversion + /// is best done where the file is read which is usually on the server + /// side. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub position_encodings: Option>, +} + +/// Client capability that signals how the client +/// handles stale requests (e.g. a request +/// for which the client will not process the response +/// anymore since the information is outdated). +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StaleRequestSupportClientCapabilities { + /// The client will actively cancel the request. + pub cancel: bool, + + /// The list of requests for which the client + /// will retry the request if it receives a + /// response with error code `ContentModified`` + pub retry_on_content_modified: Vec, +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RegularExpressionsClientCapabilities { + /// The engine's name. + pub engine: String, + + /// The engine's version + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MarkdownClientCapabilities { + /// The name of the parser. + pub parser: String, + + /// The version of the parser. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + + /// A list of HTML tags that the client allows / supports in + /// Markdown. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_tags: Option>, +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InitializeResult { + /// The capabilities the language server provides. + pub capabilities: ServerCapabilities, + + /// Information about the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub server_info: Option, + + /// Unofficial UT8-offsets extension. + /// + /// See https://clangd.llvm.org/extensions.html#utf-8-offsets. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "proposed")] + pub offset_encoding: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct ServerInfo { + /// The name of the server as defined by the server. + pub name: String, + /// The servers's version as defined by the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct InitializeError { + /// Indicates whether the client execute the following retry logic: + /// + /// - (1) show the message provided by the ResponseError to the user + /// - (2) user selects retry or cancel + /// - (3) if user selected retry the initialize method is sent again. + pub retry: bool, +} + +// The server can signal the following capabilities: + +/// Defines how the host (editor) should sync document changes to the language server. +#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] +#[serde(transparent)] +pub struct TextDocumentSyncKind(i32); +lsp_enum! { +impl TextDocumentSyncKind { + /// Documents should not be synced at all. + pub const NONE: TextDocumentSyncKind = TextDocumentSyncKind(0); + + /// Documents are synced by always sending the full content of the document. + pub const FULL: TextDocumentSyncKind = TextDocumentSyncKind(1); + + /// Documents are synced by sending the full content on open. After that only + /// incremental updates to the document are sent. + pub const INCREMENTAL: TextDocumentSyncKind = TextDocumentSyncKind(2); +} +} + +pub type ExecuteCommandClientCapabilities = DynamicRegistrationClientCapabilities; + +/// Execute command options. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct ExecuteCommandOptions { + /// The commands to be executed on the server + pub commands: Vec, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +/// Save options. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveOptions { + /// The client is supposed to include the content on save. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_text: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum TextDocumentSyncSaveOptions { + Supported(bool), + SaveOptions(SaveOptions), +} + +impl From for TextDocumentSyncSaveOptions { + fn from(from: SaveOptions) -> Self { + Self::SaveOptions(from) + } +} + +impl From for TextDocumentSyncSaveOptions { + fn from(from: bool) -> Self { + Self::Supported(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentSyncOptions { + /// Open and close notifications are sent to the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub open_close: Option, + + /// Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full + /// and TextDocumentSyncKindIncremental. + #[serde(skip_serializing_if = "Option::is_none")] + pub change: Option, + + /// Will save notifications are sent to the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_save: Option, + + /// Will save wait until requests are sent to the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub will_save_wait_until: Option, + + /// Save notifications are sent to the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub save: Option, +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum OneOf { + Left(A), + Right(B), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum TextDocumentSyncCapability { + Kind(TextDocumentSyncKind), + Options(TextDocumentSyncOptions), +} + +impl From for TextDocumentSyncCapability { + fn from(from: TextDocumentSyncOptions) -> Self { + Self::Options(from) + } +} + +impl From for TextDocumentSyncCapability { + fn from(from: TextDocumentSyncKind) -> Self { + Self::Kind(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ImplementationProviderCapability { + Simple(bool), + Options(StaticTextDocumentRegistrationOptions), +} + +impl From for ImplementationProviderCapability { + fn from(from: StaticTextDocumentRegistrationOptions) -> Self { + Self::Options(from) + } +} + +impl From for ImplementationProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum TypeDefinitionProviderCapability { + Simple(bool), + Options(StaticTextDocumentRegistrationOptions), +} + +impl From for TypeDefinitionProviderCapability { + fn from(from: StaticTextDocumentRegistrationOptions) -> Self { + Self::Options(from) + } +} + +impl From for TypeDefinitionProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerCapabilities { + /// The position encoding the server picked from the encodings offered + /// by the client via the client capability `general.positionEncodings`. + /// + /// If the client didn't provide any position encodings the only valid + /// value that a server can return is 'utf-16'. + /// + /// If omitted it defaults to 'utf-16'. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub position_encoding: Option, + + /// Defines how text documents are synced. + #[serde(skip_serializing_if = "Option::is_none")] + pub text_document_sync: Option, + + /// Capabilities specific to `textDocument/selectionRange` requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection_range_provider: Option, + + /// The server provides hover support. + #[serde(skip_serializing_if = "Option::is_none")] + pub hover_provider: Option, + + /// The server provides completion support. + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_provider: Option, + + /// The server provides signature help support. + #[serde(skip_serializing_if = "Option::is_none")] + pub signature_help_provider: Option, + + /// The server provides goto definition support. + #[serde(skip_serializing_if = "Option::is_none")] + pub definition_provider: Option>, + + /// The server provides goto type definition support. + #[serde(skip_serializing_if = "Option::is_none")] + pub type_definition_provider: Option, + + /// The server provides goto implementation support. + #[serde(skip_serializing_if = "Option::is_none")] + pub implementation_provider: Option, + + /// The server provides find references support. + #[serde(skip_serializing_if = "Option::is_none")] + pub references_provider: Option>, + + /// The server provides document highlight support. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_highlight_provider: Option>, + + /// The server provides document symbol support. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_symbol_provider: Option>, + + /// The server provides workspace symbol support. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_symbol_provider: Option>, + + /// The server provides code actions. + #[serde(skip_serializing_if = "Option::is_none")] + pub code_action_provider: Option, + + /// The server provides code lens. + #[serde(skip_serializing_if = "Option::is_none")] + pub code_lens_provider: Option, + + /// The server provides document formatting. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_formatting_provider: Option>, + + /// The server provides document range formatting. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_range_formatting_provider: Option>, + + /// The server provides document formatting on typing. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_on_type_formatting_provider: Option, + + /// The server provides rename support. + #[serde(skip_serializing_if = "Option::is_none")] + pub rename_provider: Option>, + + /// The server provides document link support. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_link_provider: Option, + + /// The server provides color provider support. + #[serde(skip_serializing_if = "Option::is_none")] + pub color_provider: Option, + + /// The server provides folding provider support. + #[serde(skip_serializing_if = "Option::is_none")] + pub folding_range_provider: Option, + + /// The server provides go to declaration support. + #[serde(skip_serializing_if = "Option::is_none")] + pub declaration_provider: Option, + + /// The server provides execute command support. + #[serde(skip_serializing_if = "Option::is_none")] + pub execute_command_provider: Option, + + /// Workspace specific server capabilities + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace: Option, + + /// Call hierarchy provider capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub call_hierarchy_provider: Option, + + /// Semantic tokens server capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_tokens_provider: Option, + + /// Whether server provides moniker support. + #[serde(skip_serializing_if = "Option::is_none")] + pub moniker_provider: Option>, + + /// The server provides linked editing range support. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub linked_editing_range_provider: Option, + + /// The server provides inline values. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub inline_value_provider: Option>, + + /// The server provides inlay hints. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub inlay_hint_provider: Option>, + + /// The server has support for pull model diagnostics. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostic_provider: Option, + + /// The server provides inline completions. + /// + /// @since 3.18.0 + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "proposed")] + pub inline_completion_provider: Option>, + + /// Experimental server capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub experimental: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceServerCapabilities { + /// The server supports workspace folder. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_folders: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub file_operations: Option, +} + +/// General parameters to to register for a capability. +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Registration { + /// The id used to register the request. The id can be used to deregister + /// the request again. + pub id: String, + + /// The method / capability to register for. + pub method: String, + + /// Options necessary for the registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub register_options: Option, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct RegistrationParams { + pub registrations: Vec, +} + +/// Since most of the registration options require to specify a document selector there is a base +/// interface that can be used. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentRegistrationOptions { + /// A document selector to identify the scope of the registration. If set to null + /// the document selector provided on the client side will be used. + pub document_selector: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum DeclarationCapability { + Simple(bool), + RegistrationOptions(DeclarationRegistrationOptions), + Options(DeclarationOptions), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeclarationRegistrationOptions { + #[serde(flatten)] + pub declaration_options: DeclarationOptions, + + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeclarationOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StaticRegistrationOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, Copy)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub work_done_progress: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentFormattingOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentRangeFormattingOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DefinitionOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentSymbolOptions { + /// A human-readable string that is shown when multiple outlines trees are + /// shown for the same document. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReferencesOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentHighlightOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceSymbolOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, + + /// The server provides support to resolve additional + /// information for a workspace symbol. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_provider: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StaticTextDocumentRegistrationOptions { + /// A document selector to identify the scope of the registration. If set to null + /// the document selector provided on the client side will be used. + pub document_selector: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, +} + +/// General parameters to unregister a capability. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct Unregistration { + /// The id used to unregister the request or notification. Usually an id + /// provided during the register request. + pub id: String, + + /// The method / capability to unregister for. + pub method: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct UnregistrationParams { + pub unregisterations: Vec, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct DidChangeConfigurationParams { + /// The actual changed settings + pub settings: Value, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidOpenTextDocumentParams { + /// The document that was opened. + pub text_document: TextDocumentItem, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidChangeTextDocumentParams { + /// The document that did change. The version number points + /// to the version after all provided content changes have + /// been applied. + pub text_document: VersionedTextDocumentIdentifier, + /// The actual content changes. + pub content_changes: Vec, +} + +/// An event describing a change to a text document. If range and rangeLength are omitted +/// the new text is considered to be the full content of the document. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentContentChangeEvent { + /// The range of the document that changed. + #[serde(skip_serializing_if = "Option::is_none")] + pub range: Option, + + /// The length of the range that got replaced. + /// + /// Deprecated: Use range instead + #[serde(skip_serializing_if = "Option::is_none")] + pub range_length: Option, + + /// The new text of the document. + pub text: String, +} + +/// Describe options to be used when registering for text document change events. +/// +/// Extends TextDocumentRegistrationOptions +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentChangeRegistrationOptions { + /// A document selector to identify the scope of the registration. If set to null + /// the document selector provided on the client side will be used. + pub document_selector: Option, + + /// How documents are synced to the server. See TextDocumentSyncKind.Full + /// and TextDocumentSyncKindIncremental. + pub sync_kind: i32, +} + +/// The parameters send in a will save text document notification. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WillSaveTextDocumentParams { + /// The document that will be saved. + pub text_document: TextDocumentIdentifier, + + /// The 'TextDocumentSaveReason'. + pub reason: TextDocumentSaveReason, +} + +/// Represents reasons why a text document is saved. +#[derive(Copy, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct TextDocumentSaveReason(i32); +lsp_enum! { +impl TextDocumentSaveReason { + /// Manually triggered, e.g. by the user pressing save, by starting debugging, + /// or by an API call. + pub const MANUAL: TextDocumentSaveReason = TextDocumentSaveReason(1); + + /// Automatic after a delay. + pub const AFTER_DELAY: TextDocumentSaveReason = TextDocumentSaveReason(2); + + /// When the editor lost focus. + pub const FOCUS_OUT: TextDocumentSaveReason = TextDocumentSaveReason(3); +} +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidCloseTextDocumentParams { + /// The document that was closed. + pub text_document: TextDocumentIdentifier, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidSaveTextDocumentParams { + /// The document that was saved. + pub text_document: TextDocumentIdentifier, + + /// Optional the content when saved. Depends on the includeText value + /// when the save notification was requested. + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentSaveRegistrationOptions { + /// The client is supposed to include the content on save. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_text: Option, + + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidChangeWatchedFilesClientCapabilities { + /// Did change watched files notification supports dynamic registration. + /// Please note that the current protocol doesn't support static + /// configuration for file changes from the server side. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Whether the client has support for relative patterns + /// or not. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub relative_pattern_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct DidChangeWatchedFilesParams { + /// The actual file events. + pub changes: Vec, +} + +/// The file event type. +#[derive(Eq, PartialEq, Hash, Copy, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct FileChangeType(i32); +lsp_enum! { +impl FileChangeType { + /// The file got created. + pub const CREATED: FileChangeType = FileChangeType(1); + + /// The file got changed. + pub const CHANGED: FileChangeType = FileChangeType(2); + + /// The file got deleted. + pub const DELETED: FileChangeType = FileChangeType(3); +} +} + +/// An event describing a file change. +#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] +pub struct FileEvent { + /// The file's URI. + pub uri: Url, + + /// The change type. + #[serde(rename = "type")] + pub typ: FileChangeType, +} + +impl FileEvent { + pub fn new(uri: Url, typ: FileChangeType) -> FileEvent { + FileEvent { uri, typ } + } +} + +/// Describe options to be used when registered for text document change events. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)] +pub struct DidChangeWatchedFilesRegistrationOptions { + /// The watchers to register. + pub watchers: Vec, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileSystemWatcher { + /// The glob pattern to watch. See {@link GlobPattern glob pattern} + /// for more detail. + /// + /// @since 3.17.0 support for relative patterns. + pub glob_pattern: GlobPattern, + + /// The kind of events of interest. If omitted it defaults to WatchKind.Create | + /// WatchKind.Change | WatchKind.Delete which is 7. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, +} + +/// The glob pattern. Either a string pattern or a relative pattern. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum GlobPattern { + String(Pattern), + Relative(RelativePattern), +} + +impl From for GlobPattern { + #[inline] + fn from(from: Pattern) -> Self { + Self::String(from) + } +} + +impl From for GlobPattern { + #[inline] + fn from(from: RelativePattern) -> Self { + Self::Relative(from) + } +} + +/// A relative pattern is a helper to construct glob patterns that are matched +/// relatively to a base URI. The common value for a `baseUri` is a workspace +/// folder root, but it can be another absolute URI as well. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RelativePattern { + /// A workspace folder or a base URI to which this pattern will be matched + /// against relatively. + pub base_uri: OneOf, + + /// The actual glob pattern. + pub pattern: Pattern, +} + +/// The glob pattern to watch relative to the base path. Glob patterns can have +/// the following syntax: +/// - `*` to match one or more characters in a path segment +/// - `?` to match on one character in a path segment +/// - `**` to match any number of path segments, including none +/// - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript +/// and JavaScript files) +/// - `[]` to declare a range of characters to match in a path segment +/// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +/// - `[!...]` to negate a range of characters to match in a path segment +/// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, +/// but not `example.0`) +/// +/// @since 3.17.0 +pub type Pattern = String; + +bitflags! { +pub struct WatchKind: u8 { + /// Interested in create events. + const Create = 1; + /// Interested in change events + const Change = 2; + /// Interested in delete events + const Delete = 4; +} +} + +impl<'de> serde::Deserialize<'de> for WatchKind { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let i = u8::deserialize(deserializer)?; + WatchKind::from_bits(i).ok_or_else(|| { + D::Error::invalid_value(de::Unexpected::Unsigned(u64::from(i)), &"Unknown flag") + }) + } +} + +impl serde::Serialize for WatchKind { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(self.bits()) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct PublishDiagnosticsParams { + /// The URI for which diagnostic information is reported. + pub uri: Url, + + /// An array of diagnostic information items. + pub diagnostics: Vec, + + /// Optional the version number of the document the diagnostics are published for. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +impl PublishDiagnosticsParams { + pub fn new( + uri: Url, + diagnostics: Vec, + version: Option, + ) -> PublishDiagnosticsParams { + PublishDiagnosticsParams { + uri, + diagnostics, + version, + } + } +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum Documentation { + String(String), + MarkupContent(MarkupContent), +} + +/// MarkedString can be used to render human readable text. It is either a +/// markdown string or a code-block that provides a language and a code snippet. +/// The language identifier is semantically equal to the optional language +/// identifier in fenced code blocks in GitHub issues. +/// +/// The pair of a language and a value is an equivalent to markdown: +/// +/// ```${language} +/// ${value} +/// ``` +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum MarkedString { + String(String), + LanguageString(LanguageString), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct LanguageString { + pub language: String, + pub value: String, +} + +impl MarkedString { + pub fn from_markdown(markdown: String) -> MarkedString { + MarkedString::String(markdown) + } + + pub fn from_language_code(language: String, code_block: String) -> MarkedString { + MarkedString::LanguageString(LanguageString { + language, + value: code_block, + }) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GotoDefinitionParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// GotoDefinition response can be single location, or multiple Locations or a link. +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum GotoDefinitionResponse { + Scalar(Location), + Array(Vec), + Link(Vec), +} + +impl From for GotoDefinitionResponse { + fn from(location: Location) -> Self { + GotoDefinitionResponse::Scalar(location) + } +} + +impl From> for GotoDefinitionResponse { + fn from(locations: Vec) -> Self { + GotoDefinitionResponse::Array(locations) + } +} + +impl From> for GotoDefinitionResponse { + fn from(locations: Vec) -> Self { + GotoDefinitionResponse::Link(locations) + } +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct ExecuteCommandParams { + /// The identifier of the actual command handler. + pub command: String, + /// Arguments that the command should be invoked with. + #[serde(default)] + pub arguments: Vec, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +/// Execute command registration options. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct ExecuteCommandRegistrationOptions { + /// The commands to be executed on the server + pub commands: Vec, + + #[serde(flatten)] + pub execute_command_options: ExecuteCommandOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplyWorkspaceEditParams { + /// An optional label of the workspace edit. This label is + /// presented in the user interface for example on an undo + /// stack to undo the workspace edit. + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + + /// The edits to apply. + pub edit: WorkspaceEdit, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplyWorkspaceEditResponse { + /// Indicates whether the edit was applied or not. + pub applied: bool, + + /// An optional textual description for why the edit was not applied. + /// This may be used may be used by the server for diagnostic + /// logging or to provide a suitable error for a request that + /// triggered the edit + #[serde(skip_serializing_if = "Option::is_none")] + pub failure_reason: Option, + + /// Depending on the client's failure handling strategy `failedChange` might + /// contain the index of the change that failed. This property is only available + /// if the client signals a `failureHandlingStrategy` in its client capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub failed_change: Option, +} + +/// Describes the content type that a client supports in various +/// result literals like `Hover`, `ParameterInfo` or `CompletionItem`. +/// +/// Please note that `MarkupKinds` must not start with a `$`. This kinds +/// are reserved for internal usage. +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum MarkupKind { + /// Plain text is supported as a content format + PlainText, + /// Markdown is supported as a content format + Markdown, +} + +/// A `MarkupContent` literal represents a string value which content can be represented in different formats. +/// Currently `plaintext` and `markdown` are supported formats. A `MarkupContent` is usually used in +/// documentation properties of result literals like `CompletionItem` or `SignatureInformation`. +/// If the format is `markdown` the content should follow the [GitHub Flavored Markdown Specification](https://github.github.com/gfm/). +/// +/// Here is an example how such a string can be constructed using JavaScript / TypeScript: +/// +/// ```ignore +/// let markdown: MarkupContent = { +/// kind: MarkupKind::Markdown, +/// value: [ +/// "# Header", +/// "Some text", +/// "```typescript", +/// "someCode();", +/// "```" +/// ] +/// .join("\n"), +/// }; +/// ``` +/// +/// Please *Note* that clients might sanitize the return markdown. A client could decide to +/// remove HTML from the markdown to avoid script execution. +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +pub struct MarkupContent { + pub kind: MarkupKind, + pub value: String, +} + +/// A parameter literal used to pass a partial result token. +#[derive(Debug, Eq, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PartialResultParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub partial_result_token: Option, +} + +/// Symbol tags are extra annotations that tweak the rendering of a symbol. +/// +/// @since 3.16.0 +#[derive(Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct SymbolTag(i32); +lsp_enum! { +impl SymbolTag { + /// Render a symbol as obsolete, usually using a strike-out. + pub const DEPRECATED: SymbolTag = SymbolTag(1); +} +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + use super::*; + + pub(crate) fn test_serialization(ms: &SER, expected: &str) + where + SER: Serialize + for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug, + { + let json_str = serde_json::to_string(ms).unwrap(); + assert_eq!(&json_str, expected); + let deserialized: SER = serde_json::from_str(&json_str).unwrap(); + assert_eq!(&deserialized, ms); + } + + pub(crate) fn test_deserialization(json: &str, expected: &T) + where + T: for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug, + { + let value = serde_json::from_str::(json).unwrap(); + assert_eq!(&value, expected); + } + + #[test] + fn one_of() { + test_serialization(&OneOf::::Left(true), r#"true"#); + test_serialization(&OneOf::::Left("abcd".into()), r#""abcd""#); + test_serialization( + &OneOf::::Right(WorkDoneProgressOptions { + work_done_progress: Some(false), + }), + r#"{"workDoneProgress":false}"#, + ); + } + + #[test] + fn number_or_string() { + test_serialization(&NumberOrString::Number(123), r#"123"#); + + test_serialization(&NumberOrString::String("abcd".into()), r#""abcd""#); + } + + #[test] + fn marked_string() { + test_serialization(&MarkedString::from_markdown("xxx".into()), r#""xxx""#); + + test_serialization( + &MarkedString::from_language_code("lang".into(), "code".into()), + r#"{"language":"lang","value":"code"}"#, + ); + } + + #[test] + fn language_string() { + test_serialization( + &LanguageString { + language: "LL".into(), + value: "VV".into(), + }, + r#"{"language":"LL","value":"VV"}"#, + ); + } + + #[test] + fn workspace_edit() { + test_serialization( + &WorkspaceEdit { + changes: Some(vec![].into_iter().collect()), + document_changes: None, + ..Default::default() + }, + r#"{"changes":{}}"#, + ); + + test_serialization( + &WorkspaceEdit { + changes: None, + document_changes: None, + ..Default::default() + }, + r#"{}"#, + ); + + test_serialization( + &WorkspaceEdit { + changes: Some( + vec![(Url::parse("file://test").unwrap(), vec![])] + .into_iter() + .collect(), + ), + document_changes: None, + ..Default::default() + }, + r#"{"changes":{"file://test/":[]}}"#, + ); + } + + #[test] + fn root_uri_can_be_missing() { + serde_json::from_str::(r#"{ "capabilities": {} }"#).unwrap(); + } + + #[test] + fn test_watch_kind() { + test_serialization(&WatchKind::Create, "1"); + test_serialization(&(WatchKind::Create | WatchKind::Change), "3"); + test_serialization( + &(WatchKind::Create | WatchKind::Change | WatchKind::Delete), + "7", + ); + } + + #[test] + fn test_resource_operation_kind() { + test_serialization( + &vec![ + ResourceOperationKind::Create, + ResourceOperationKind::Rename, + ResourceOperationKind::Delete, + ], + r#"["create","rename","delete"]"#, + ); + } +} diff --git a/helix-lsp-types/src/linked_editing.rs b/helix-lsp-types/src/linked_editing.rs new file mode 100644 index 000000000..b23fb141f --- /dev/null +++ b/helix-lsp-types/src/linked_editing.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + DynamicRegistrationClientCapabilities, Range, StaticRegistrationOptions, + TextDocumentPositionParams, TextDocumentRegistrationOptions, WorkDoneProgressOptions, + WorkDoneProgressParams, +}; + +pub type LinkedEditingRangeClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkedEditingRangeOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkedEditingRangeRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub linked_editing_range_options: LinkedEditingRangeOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum LinkedEditingRangeServerCapabilities { + Simple(bool), + Options(LinkedEditingRangeOptions), + RegistrationOptions(LinkedEditingRangeRegistrationOptions), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkedEditingRangeParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkedEditingRanges { + /// A list of ranges that can be renamed together. The ranges must have + /// identical length and contain identical text content. The ranges cannot overlap. + pub ranges: Vec, + + /// An optional word pattern (regular expression) that describes valid contents for + /// the given ranges. If no pattern is provided, the client configuration's word + /// pattern will be used. + #[serde(skip_serializing_if = "Option::is_none")] + pub word_pattern: Option, +} diff --git a/helix-lsp-types/src/lsif.rs b/helix-lsp-types/src/lsif.rs new file mode 100644 index 000000000..be6df6e58 --- /dev/null +++ b/helix-lsp-types/src/lsif.rs @@ -0,0 +1,336 @@ +//! Types of Language Server Index Format (LSIF). LSIF is a standard format +//! for language servers or other programming tools to dump their knowledge +//! about a workspace. +//! +//! Based on + +use crate::{Range, Url}; +use serde::{Deserialize, Serialize}; + +pub type Id = crate::NumberOrString; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum LocationOrRangeId { + Location(crate::Location), + RangeId(Id), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Entry { + pub id: Id, + #[serde(flatten)] + pub data: Element, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum Element { + Vertex(Vertex), + Edge(Edge), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct ToolInfo { + pub name: String, + #[serde(default = "Default::default")] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy)] +pub enum Encoding { + /// Currently only 'utf-16' is supported due to the limitations in LSP. + #[serde(rename = "utf-16")] + Utf16, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct RangeBasedDocumentSymbol { + pub id: Id, + #[serde(default = "Default::default")] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub children: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum DocumentSymbolOrRangeBasedVec { + DocumentSymbol(Vec), + RangeBased(Vec), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DefinitionTag { + /// The text covered by the range + text: String, + /// The symbol kind. + kind: crate::SymbolKind, + /// Indicates if this symbol is deprecated. + #[serde(default)] + #[serde(skip_serializing_if = "std::ops::Not::not")] + deprecated: bool, + /// The full range of the definition not including leading/trailing whitespace but everything else, e.g comments and code. + /// The range must be included in fullRange. + full_range: Range, + /// Optional detail information for the definition. + #[serde(skip_serializing_if = "Option::is_none")] + detail: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeclarationTag { + /// The text covered by the range + text: String, + /// The symbol kind. + kind: crate::SymbolKind, + /// Indicates if this symbol is deprecated. + #[serde(default)] + deprecated: bool, + /// The full range of the definition not including leading/trailing whitespace but everything else, e.g comments and code. + /// The range must be included in fullRange. + full_range: Range, + /// Optional detail information for the definition. + #[serde(skip_serializing_if = "Option::is_none")] + detail: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReferenceTag { + text: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UnknownTag { + text: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum RangeTag { + Definition(DefinitionTag), + Declaration(DeclarationTag), + Reference(ReferenceTag), + Unknown(UnknownTag), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "label")] +pub enum Vertex { + MetaData(MetaData), + /// + Project(Project), + Document(Document), + /// + Range { + #[serde(flatten)] + range: Range, + #[serde(skip_serializing_if = "Option::is_none")] + tag: Option, + }, + /// + ResultSet(ResultSet), + Moniker(crate::Moniker), + PackageInformation(PackageInformation), + + #[serde(rename = "$event")] + Event(Event), + + DefinitionResult, + DeclarationResult, + TypeDefinitionResult, + ReferenceResult, + ImplementationResult, + FoldingRangeResult { + result: Vec, + }, + HoverResult { + result: crate::Hover, + }, + DocumentSymbolResult { + result: DocumentSymbolOrRangeBasedVec, + }, + DocumentLinkResult { + result: Vec, + }, + DiagnosticResult { + result: Vec, + }, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EventKind { + Begin, + End, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EventScope { + Document, + Project, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Event { + pub kind: EventKind, + pub scope: EventScope, + pub data: Id, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "label")] +pub enum Edge { + Contains(EdgeDataMultiIn), + Moniker(EdgeData), + NextMoniker(EdgeData), + Next(EdgeData), + PackageInformation(EdgeData), + Item(Item), + + // Methods + #[serde(rename = "textDocument/definition")] + Definition(EdgeData), + #[serde(rename = "textDocument/declaration")] + Declaration(EdgeData), + #[serde(rename = "textDocument/hover")] + Hover(EdgeData), + #[serde(rename = "textDocument/references")] + References(EdgeData), + #[serde(rename = "textDocument/implementation")] + Implementation(EdgeData), + #[serde(rename = "textDocument/typeDefinition")] + TypeDefinition(EdgeData), + #[serde(rename = "textDocument/foldingRange")] + FoldingRange(EdgeData), + #[serde(rename = "textDocument/documentLink")] + DocumentLink(EdgeData), + #[serde(rename = "textDocument/documentSymbol")] + DocumentSymbol(EdgeData), + #[serde(rename = "textDocument/diagnostic")] + Diagnostic(EdgeData), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EdgeData { + pub in_v: Id, + pub out_v: Id, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EdgeDataMultiIn { + pub in_vs: Vec, + pub out_v: Id, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DefinitionResultType { + Scalar(LocationOrRangeId), + Array(LocationOrRangeId), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ItemKind { + Declarations, + Definitions, + References, + ReferenceResults, + ImplementationResults, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Item { + pub document: Id, + #[serde(skip_serializing_if = "Option::is_none")] + pub property: Option, + #[serde(flatten)] + pub edge_data: EdgeDataMultiIn, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Document { + pub uri: Url, + pub language_id: String, +} + +/// +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResultSet { + #[serde(skip_serializing_if = "Option::is_none")] + pub key: Option, +} + +/// +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Project { + #[serde(skip_serializing_if = "Option::is_none")] + pub resource: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + pub kind: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MetaData { + /// The version of the LSIF format using semver notation. See . Please note + /// the version numbers starting with 0 don't adhere to semver and adopters have to assume + /// that each new version is breaking. + pub version: String, + + /// The project root (in form of an URI) used to compute this dump. + pub project_root: Url, + + /// The string encoding used to compute line and character values in + /// positions and ranges. + pub position_encoding: Encoding, + + /// Information about the tool that created the dump + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_info: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Repository { + pub r#type: String, + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_id: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PackageInformation { + pub name: String, + pub manager: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} diff --git a/helix-lsp-types/src/moniker.rs b/helix-lsp-types/src/moniker.rs new file mode 100644 index 000000000..74bf89553 --- /dev/null +++ b/helix-lsp-types/src/moniker.rs @@ -0,0 +1,92 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + DynamicRegistrationClientCapabilities, PartialResultParams, TextDocumentPositionParams, + TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams, +}; + +pub type MonikerClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum MonikerServerCapabilities { + Options(MonikerOptions), + RegistrationOptions(MonikerRegistrationOptions), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct MonikerOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MonikerRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub moniker_options: MonikerOptions, +} + +/// Moniker uniqueness level to define scope of the moniker. +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub enum UniquenessLevel { + /// The moniker is only unique inside a document + Document, + /// The moniker is unique inside a project for which a dump got created + Project, + /// The moniker is unique inside the group to which a project belongs + Group, + /// The moniker is unique inside the moniker scheme. + Scheme, + /// The moniker is globally unique + Global, +} + +/// The moniker kind. +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub enum MonikerKind { + /// The moniker represent a symbol that is imported into a project + Import, + /// The moniker represent a symbol that is exported into a project + Export, + /// The moniker represents a symbol that is local to a project (e.g. a local + /// variable of a function, a class not visible outside the project, ...) + Local, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MonikerParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// Moniker definition to match LSIF 0.5 moniker definition. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Moniker { + /// The scheme of the moniker. For example tsc or .Net + pub scheme: String, + + /// The identifier of the moniker. The value is opaque in LSIF however + /// schema owners are allowed to define the structure if they want. + pub identifier: String, + + /// The scope in which the moniker is unique + pub unique: UniquenessLevel, + + /// The moniker kind if known. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, +} diff --git a/helix-lsp-types/src/notification.rs b/helix-lsp-types/src/notification.rs new file mode 100644 index 000000000..aef011d36 --- /dev/null +++ b/helix-lsp-types/src/notification.rs @@ -0,0 +1,361 @@ +use super::*; + +use serde::{de::DeserializeOwned, Serialize}; + +pub trait Notification { + type Params: DeserializeOwned + Serialize + Send + Sync + 'static; + const METHOD: &'static str; +} + +#[macro_export] +macro_rules! lsp_notification { + ("$/cancelRequest") => { + $crate::notification::Cancel + }; + ("$/setTrace") => { + $crate::notification::SetTrace + }; + ("$/logTrace") => { + $crate::notification::LogTrace + }; + ("initialized") => { + $crate::notification::Initialized + }; + ("exit") => { + $crate::notification::Exit + }; + + ("window/showMessage") => { + $crate::notification::ShowMessage + }; + ("window/logMessage") => { + $crate::notification::LogMessage + }; + ("window/workDoneProgress/cancel") => { + $crate::notification::WorkDoneProgressCancel + }; + + ("telemetry/event") => { + $crate::notification::TelemetryEvent + }; + + ("textDocument/didOpen") => { + $crate::notification::DidOpenTextDocument + }; + ("textDocument/didChange") => { + $crate::notification::DidChangeTextDocument + }; + ("textDocument/willSave") => { + $crate::notification::WillSaveTextDocument + }; + ("textDocument/didSave") => { + $crate::notification::DidSaveTextDocument + }; + ("textDocument/didClose") => { + $crate::notification::DidCloseTextDocument + }; + ("textDocument/publishDiagnostics") => { + $crate::notification::PublishDiagnostics + }; + + ("workspace/didChangeConfiguration") => { + $crate::notification::DidChangeConfiguration + }; + ("workspace/didChangeWatchedFiles") => { + $crate::notification::DidChangeWatchedFiles + }; + ("workspace/didChangeWorkspaceFolders") => { + $crate::notification::DidChangeWorkspaceFolders + }; + ("$/progress") => { + $crate::notification::Progress + }; + ("workspace/didCreateFiles") => { + $crate::notification::DidCreateFiles + }; + ("workspace/didRenameFiles") => { + $crate::notification::DidRenameFiles + }; + ("workspace/didDeleteFiles") => { + $crate::notification::DidDeleteFiles + }; +} + +/// The base protocol now offers support for request cancellation. To cancel a request, +/// a notification message with the following properties is sent: +/// +/// A request that got canceled still needs to return from the server and send a response back. +/// It can not be left open / hanging. This is in line with the JSON RPC protocol that requires +/// that every request sends a response back. In addition it allows for returning partial results on cancel. +#[derive(Debug)] +pub enum Cancel {} + +impl Notification for Cancel { + type Params = CancelParams; + const METHOD: &'static str = "$/cancelRequest"; +} + +/// A notification that should be used by the client to modify the trace +/// setting of the server. +#[derive(Debug)] +pub enum SetTrace {} + +impl Notification for SetTrace { + type Params = SetTraceParams; + const METHOD: &'static str = "$/setTrace"; +} + +/// A notification to log the trace of the server’s execution. +/// The amount and content of these notifications depends on the current trace configuration. +/// +/// `LogTrace` should be used for systematic trace reporting. For single debugging messages, +/// the server should send `LogMessage` notifications. +#[derive(Debug)] +pub enum LogTrace {} + +impl Notification for LogTrace { + type Params = LogTraceParams; + const METHOD: &'static str = "$/logTrace"; +} + +/// The initialized notification is sent from the client to the server after the client received +/// the result of the initialize request but before the client is sending any other request or +/// notification to the server. The server can use the initialized notification for example to +/// dynamically register capabilities. +#[derive(Debug)] +pub enum Initialized {} + +impl Notification for Initialized { + type Params = InitializedParams; + const METHOD: &'static str = "initialized"; +} + +/// A notification to ask the server to exit its process. +/// The server should exit with success code 0 if the shutdown request has been received before; +/// otherwise with error code 1. +#[derive(Debug)] +pub enum Exit {} + +impl Notification for Exit { + type Params = (); + const METHOD: &'static str = "exit"; +} + +/// The show message notification is sent from a server to a client to ask the client to display a particular message +/// in the user interface. +#[derive(Debug)] +pub enum ShowMessage {} + +impl Notification for ShowMessage { + type Params = ShowMessageParams; + const METHOD: &'static str = "window/showMessage"; +} + +/// The log message notification is sent from the server to the client to ask the client to log a particular message. +#[derive(Debug)] +pub enum LogMessage {} + +impl Notification for LogMessage { + type Params = LogMessageParams; + const METHOD: &'static str = "window/logMessage"; +} + +/// The telemetry notification is sent from the server to the client to ask the client to log a telemetry event. +/// The protocol doesn't specify the payload since no interpretation of the data happens in the protocol. Most clients even don't handle +/// the event directly but forward them to the extensions owning the corresponding server issuing the event. +#[derive(Debug)] +pub enum TelemetryEvent {} + +impl Notification for TelemetryEvent { + type Params = OneOf; + const METHOD: &'static str = "telemetry/event"; +} + +/// A notification sent from the client to the server to signal the change of configuration settings. +#[derive(Debug)] +pub enum DidChangeConfiguration {} + +impl Notification for DidChangeConfiguration { + type Params = DidChangeConfigurationParams; + const METHOD: &'static str = "workspace/didChangeConfiguration"; +} + +/// The document open notification is sent from the client to the server to signal newly opened text documents. +/// The document's truth is now managed by the client and the server must not try to read the document's truth +/// using the document's uri. +#[derive(Debug)] +pub enum DidOpenTextDocument {} + +impl Notification for DidOpenTextDocument { + type Params = DidOpenTextDocumentParams; + const METHOD: &'static str = "textDocument/didOpen"; +} + +/// The document change notification is sent from the client to the server to signal changes to a text document. +/// In 2.0 the shape of the params has changed to include proper version numbers and language ids. +#[derive(Debug)] +pub enum DidChangeTextDocument {} + +impl Notification for DidChangeTextDocument { + type Params = DidChangeTextDocumentParams; + const METHOD: &'static str = "textDocument/didChange"; +} + +/// The document will save notification is sent from the client to the server before the document +/// is actually saved. +#[derive(Debug)] +pub enum WillSaveTextDocument {} + +impl Notification for WillSaveTextDocument { + type Params = WillSaveTextDocumentParams; + const METHOD: &'static str = "textDocument/willSave"; +} + +/// The document close notification is sent from the client to the server when the document got closed in the client. +/// The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri +/// the truth now exists on disk). +#[derive(Debug)] +pub enum DidCloseTextDocument {} + +impl Notification for DidCloseTextDocument { + type Params = DidCloseTextDocumentParams; + const METHOD: &'static str = "textDocument/didClose"; +} + +/// The document save notification is sent from the client to the server when the document was saved in the client. +#[derive(Debug)] +pub enum DidSaveTextDocument {} + +impl Notification for DidSaveTextDocument { + type Params = DidSaveTextDocumentParams; + const METHOD: &'static str = "textDocument/didSave"; +} + +/// The watched files notification is sent from the client to the server when the client detects changes to files and folders +/// watched by the language client (note although the name suggest that only file events are sent it is about file system events which include folders as well). +/// It is recommended that servers register for these file system events using the registration mechanism. +/// In former implementations clients pushed file events without the server actively asking for it. +#[derive(Debug)] +pub enum DidChangeWatchedFiles {} + +impl Notification for DidChangeWatchedFiles { + type Params = DidChangeWatchedFilesParams; + const METHOD: &'static str = "workspace/didChangeWatchedFiles"; +} + +/// The workspace/didChangeWorkspaceFolders notification is sent from the client to the server to inform the server +/// about workspace folder configuration changes +#[derive(Debug)] +pub enum DidChangeWorkspaceFolders {} + +impl Notification for DidChangeWorkspaceFolders { + type Params = DidChangeWorkspaceFoldersParams; + const METHOD: &'static str = "workspace/didChangeWorkspaceFolders"; +} + +/// Diagnostics notification are sent from the server to the client to signal results of validation runs. +#[derive(Debug)] +pub enum PublishDiagnostics {} + +impl Notification for PublishDiagnostics { + type Params = PublishDiagnosticsParams; + const METHOD: &'static str = "textDocument/publishDiagnostics"; +} + +/// The progress notification is sent from the server to the client to ask +/// the client to indicate progress. +#[derive(Debug)] +pub enum Progress {} + +impl Notification for Progress { + type Params = ProgressParams; + const METHOD: &'static str = "$/progress"; +} + +/// The `window/workDoneProgress/cancel` notification is sent from the client +/// to the server to cancel a progress initiated on the server side using the `window/workDoneProgress/create`. +#[derive(Debug)] +pub enum WorkDoneProgressCancel {} + +impl Notification for WorkDoneProgressCancel { + type Params = WorkDoneProgressCancelParams; + const METHOD: &'static str = "window/workDoneProgress/cancel"; +} + +/// The did create files notification is sent from the client to the server when files were created from within the client. +#[derive(Debug)] +pub enum DidCreateFiles {} + +impl Notification for DidCreateFiles { + type Params = CreateFilesParams; + const METHOD: &'static str = "workspace/didCreateFiles"; +} + +/// The did rename files notification is sent from the client to the server when files were renamed from within the client. +#[derive(Debug)] +pub enum DidRenameFiles {} + +impl Notification for DidRenameFiles { + type Params = RenameFilesParams; + const METHOD: &'static str = "workspace/didRenameFiles"; +} + +/// The did delete files notification is sent from the client to the server when files were deleted from within the client. +#[derive(Debug)] +pub enum DidDeleteFiles {} + +impl Notification for DidDeleteFiles { + type Params = DeleteFilesParams; + const METHOD: &'static str = "workspace/didDeleteFiles"; +} + +#[cfg(test)] +mod test { + use super::*; + + fn fake_call() + where + N: Notification, + N::Params: serde::Serialize, + { + } + + macro_rules! check_macro { + ($name:tt) => { + // check whether the macro name matches the method + assert_eq!(::METHOD, $name); + // test whether type checking passes for each component + fake_call::(); + }; + } + + #[test] + fn check_macro_definitions() { + check_macro!("$/cancelRequest"); + check_macro!("$/progress"); + check_macro!("$/logTrace"); + check_macro!("$/setTrace"); + check_macro!("initialized"); + check_macro!("exit"); + check_macro!("window/showMessage"); + check_macro!("window/logMessage"); + check_macro!("window/workDoneProgress/cancel"); + check_macro!("telemetry/event"); + check_macro!("textDocument/didOpen"); + check_macro!("textDocument/didChange"); + check_macro!("textDocument/willSave"); + check_macro!("textDocument/didSave"); + check_macro!("textDocument/didClose"); + check_macro!("textDocument/publishDiagnostics"); + check_macro!("workspace/didChangeConfiguration"); + check_macro!("workspace/didChangeWatchedFiles"); + check_macro!("workspace/didChangeWorkspaceFolders"); + check_macro!("workspace/didCreateFiles"); + check_macro!("workspace/didRenameFiles"); + check_macro!("workspace/didDeleteFiles"); + } + + #[test] + #[cfg(feature = "proposed")] + fn check_proposed_macro_definitions() {} +} diff --git a/helix-lsp-types/src/progress.rs b/helix-lsp-types/src/progress.rs new file mode 100644 index 000000000..41fa74f9f --- /dev/null +++ b/helix-lsp-types/src/progress.rs @@ -0,0 +1,134 @@ +use serde::{Deserialize, Serialize}; + +use crate::NumberOrString; + +pub type ProgressToken = NumberOrString; + +/// The progress notification is sent from the server to the client to ask +/// the client to indicate progress. +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProgressParams { + /// The progress token provided by the client. + pub token: ProgressToken, + + /// The progress data. + pub value: ProgressParamsValue, +} + +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum ProgressParamsValue { + WorkDone(WorkDoneProgress), +} + +/// The `window/workDoneProgress/create` request is sent +/// from the server to the client to ask the client to create a work done progress. +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressCreateParams { + /// The token to be used to report progress. + pub token: ProgressToken, +} + +/// The `window/workDoneProgress/cancel` notification is sent from the client +/// to the server to cancel a progress initiated on the server side using the `window/workDoneProgress/create`. +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressCancelParams { + /// The token to be used to report progress. + pub token: ProgressToken, +} + +/// Options to signal work done progress support in server capabilities. +#[derive(Debug, Eq, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub work_done_progress: Option, +} + +/// An optional token that a server can use to report work done progress +#[derive(Debug, Eq, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub work_done_token: Option, +} + +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressBegin { + /// Mandatory title of the progress operation. Used to briefly inform + /// about the kind of operation being performed. + /// Examples: "Indexing" or "Linking dependencies". + pub title: String, + + /// Controls if a cancel button should show to allow the user to cancel the + /// long running operation. Clients that don't support cancellation are allowed + /// to ignore the setting. + #[serde(skip_serializing_if = "Option::is_none")] + pub cancellable: Option, + + /// Optional, more detailed associated progress message. Contains + /// complementary information to the `title`. + /// + /// Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". + /// If unset, the previous progress message (if any) is still valid. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Optional progress percentage to display (value 100 is considered 100%). + /// If not provided infinite progress is assumed and clients are allowed + /// to ignore the `percentage` value in subsequent in report notifications. + /// + /// The value should be steadily rising. Clients are free to ignore values + /// that are not following this rule. The value range is [0, 100] + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, +} + +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressReport { + /// Controls if a cancel button should show to allow the user to cancel the + /// long running operation. Clients that don't support cancellation are allowed + /// to ignore the setting. + #[serde(skip_serializing_if = "Option::is_none")] + pub cancellable: Option, + + /// Optional, more detailed associated progress message. Contains + /// complementary information to the `title`. + /// Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". + /// If unset, the previous progress message (if any) is still valid. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Optional progress percentage to display (value 100 is considered 100%). + /// If not provided infinite progress is assumed and clients are allowed + /// to ignore the `percentage` value in subsequent in report notifications. + /// + /// The value should be steadily rising. Clients are free to ignore values + /// that are not following this rule. The value range is [0, 100] + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, +} + +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkDoneProgressEnd { + /// Optional, more detailed associated progress message. Contains + /// complementary information to the `title`. + /// Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". + /// If unset, the previous progress message (if any) is still valid. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum WorkDoneProgress { + Begin(WorkDoneProgressBegin), + Report(WorkDoneProgressReport), + End(WorkDoneProgressEnd), +} diff --git a/helix-lsp-types/src/references.rs b/helix-lsp-types/src/references.rs new file mode 100644 index 000000000..cb590d58e --- /dev/null +++ b/helix-lsp-types/src/references.rs @@ -0,0 +1,30 @@ +use crate::{ + DynamicRegistrationClientCapabilities, PartialResultParams, TextDocumentPositionParams, + WorkDoneProgressParams, +}; +use serde::{Deserialize, Serialize}; + +pub type ReferenceClientCapabilities = DynamicRegistrationClientCapabilities; +#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReferenceContext { + /// Include the declaration of the current symbol. + pub include_declaration: bool, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReferenceParams { + // Text Document and Position fields + #[serde(flatten)] + pub text_document_position: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, + + // ReferenceParams properties: + pub context: ReferenceContext, +} diff --git a/helix-lsp-types/src/rename.rs b/helix-lsp-types/src/rename.rs new file mode 100644 index 000000000..bb57e9afd --- /dev/null +++ b/helix-lsp-types/src/rename.rs @@ -0,0 +1,88 @@ +use crate::{Range, TextDocumentPositionParams, WorkDoneProgressOptions, WorkDoneProgressParams}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameParams { + /// Text Document and Position fields + #[serde(flatten)] + pub text_document_position: TextDocumentPositionParams, + + /// The new name of the symbol. If the given name is not valid the + /// request must return a [ResponseError](#ResponseError) with an + /// appropriate message set. + pub new_name: String, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameOptions { + /// Renames should be checked and tested before being executed. + #[serde(skip_serializing_if = "Option::is_none")] + pub prepare_provider: Option, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameClientCapabilities { + /// Whether rename supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Client supports testing for validity of rename operations before execution. + /// + /// @since 3.12.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub prepare_support: Option, + + /// Client supports the default behavior result. + /// + /// The value indicates the default behavior used by the + /// client. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub prepare_support_default_behavior: Option, + + /// Whether the client honors the change annotations in + /// text edits and resource operations returned via the + /// rename request's workspace edit by for example presenting + /// the workspace edit in the user interface and asking + /// for confirmation. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub honors_change_annotations: Option, +} + +#[derive(Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PrepareSupportDefaultBehavior(i32); +lsp_enum! { +impl PrepareSupportDefaultBehavior { + /// The client's default behavior is to select the identifier + /// according the to language's syntax rule + pub const IDENTIFIER: PrepareSupportDefaultBehavior = PrepareSupportDefaultBehavior(1); +} +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +#[serde(rename_all = "camelCase")] +pub enum PrepareRenameResponse { + Range(Range), + RangeWithPlaceholder { + range: Range, + placeholder: String, + }, + #[serde(rename_all = "camelCase")] + DefaultBehavior { + default_behavior: bool, + }, +} diff --git a/helix-lsp-types/src/request.rs b/helix-lsp-types/src/request.rs new file mode 100644 index 000000000..7502546be --- /dev/null +++ b/helix-lsp-types/src/request.rs @@ -0,0 +1,1068 @@ +use super::*; + +use serde::{de::DeserializeOwned, Serialize}; + +pub trait Request { + type Params: DeserializeOwned + Serialize + Send + Sync + 'static; + type Result: DeserializeOwned + Serialize + Send + Sync + 'static; + const METHOD: &'static str; +} + +#[macro_export] +macro_rules! lsp_request { + ("initialize") => { + $crate::request::Initialize + }; + ("shutdown") => { + $crate::request::Shutdown + }; + + ("window/showMessageRequest") => { + $crate::request::ShowMessageRequest + }; + + ("client/registerCapability") => { + $crate::request::RegisterCapability + }; + ("client/unregisterCapability") => { + $crate::request::UnregisterCapability + }; + + ("workspace/symbol") => { + $crate::request::WorkspaceSymbolRequest + }; + ("workspaceSymbol/resolve") => { + $crate::request::WorkspaceSymbolResolve + }; + ("workspace/executeCommand") => { + $crate::request::ExecuteCommand + }; + + ("textDocument/willSaveWaitUntil") => { + $crate::request::WillSaveWaitUntil + }; + + ("textDocument/completion") => { + $crate::request::Completion + }; + ("completionItem/resolve") => { + $crate::request::ResolveCompletionItem + }; + ("textDocument/hover") => { + $crate::request::HoverRequest + }; + ("textDocument/signatureHelp") => { + $crate::request::SignatureHelpRequest + }; + ("textDocument/declaration") => { + $crate::request::GotoDeclaration + }; + ("textDocument/definition") => { + $crate::request::GotoDefinition + }; + ("textDocument/references") => { + $crate::request::References + }; + ("textDocument/documentHighlight") => { + $crate::request::DocumentHighlightRequest + }; + ("textDocument/documentSymbol") => { + $crate::request::DocumentSymbolRequest + }; + ("textDocument/codeAction") => { + $crate::request::CodeActionRequest + }; + ("textDocument/codeLens") => { + $crate::request::CodeLensRequest + }; + ("codeLens/resolve") => { + $crate::request::CodeLensResolve + }; + ("textDocument/documentLink") => { + $crate::request::DocumentLinkRequest + }; + ("documentLink/resolve") => { + $crate::request::DocumentLinkResolve + }; + ("workspace/applyEdit") => { + $crate::request::ApplyWorkspaceEdit + }; + ("textDocument/rangeFormatting") => { + $crate::request::RangeFormatting + }; + ("textDocument/onTypeFormatting") => { + $crate::request::OnTypeFormatting + }; + ("textDocument/formatting") => { + $crate::request::Formatting + }; + ("textDocument/rename") => { + $crate::request::Rename + }; + ("textDocument/documentColor") => { + $crate::request::DocumentColor + }; + ("textDocument/colorPresentation") => { + $crate::request::ColorPresentationRequest + }; + ("textDocument/foldingRange") => { + $crate::request::FoldingRangeRequest + }; + ("textDocument/prepareRename") => { + $crate::request::PrepareRenameRequest + }; + ("textDocument/implementation") => { + $crate::request::GotoImplementation + }; + ("textDocument/typeDefinition") => { + $crate::request::GotoTypeDefinition + }; + ("textDocument/selectionRange") => { + $crate::request::SelectionRangeRequest + }; + ("workspace/workspaceFolders") => { + $crate::request::WorkspaceFoldersRequest + }; + ("workspace/configuration") => { + $crate::request::WorkspaceConfiguration + }; + ("window/workDoneProgress/create") => { + $crate::request::WorkDoneProgressCreate + }; + ("callHierarchy/incomingCalls") => { + $crate::request::CallHierarchyIncomingCalls + }; + ("callHierarchy/outgoingCalls") => { + $crate::request::CallHierarchyOutgoingCalls + }; + ("textDocument/moniker") => { + $crate::request::MonikerRequest + }; + ("textDocument/linkedEditingRange") => { + $crate::request::LinkedEditingRange + }; + ("textDocument/prepareCallHierarchy") => { + $crate::request::CallHierarchyPrepare + }; + ("textDocument/prepareTypeHierarchy") => { + $crate::request::TypeHierarchyPrepare + }; + ("textDocument/semanticTokens/full") => { + $crate::request::SemanticTokensFullRequest + }; + ("textDocument/semanticTokens/full/delta") => { + $crate::request::SemanticTokensFullDeltaRequest + }; + ("textDocument/semanticTokens/range") => { + $crate::request::SemanticTokensRangeRequest + }; + ("textDocument/inlayHint") => { + $crate::request::InlayHintRequest + }; + ("textDocument/inlineValue") => { + $crate::request::InlineValueRequest + }; + ("textDocument/diagnostic") => { + $crate::request::DocumentDiagnosticRequest + }; + ("workspace/diagnostic") => { + $crate::request::WorkspaceDiagnosticRequest + }; + ("workspace/diagnostic/refresh") => { + $crate::request::WorkspaceDiagnosticRefresh + }; + ("typeHierarchy/supertypes") => { + $crate::request::TypeHierarchySupertypes + }; + ("typeHierarchy/subtypes") => { + $crate::request::TypeHierarchySubtypes + }; + ("workspace/willCreateFiles") => { + $crate::request::WillCreateFiles + }; + ("workspace/willRenameFiles") => { + $crate::request::WillRenameFiles + }; + ("workspace/willDeleteFiles") => { + $crate::request::WillDeleteFiles + }; + ("workspace/semanticTokens/refresh") => { + $crate::request::SemanticTokensRefresh + }; + ("workspace/codeLens/refresh") => { + $crate::request::CodeLensRefresh + }; + ("workspace/inlayHint/refresh") => { + $crate::request::InlayHintRefreshRequest + }; + ("workspace/inlineValue/refresh") => { + $crate::request::InlineValueRefreshRequest + }; + ("codeAction/resolve") => { + $crate::request::CodeActionResolveRequest + }; + ("inlayHint/resolve") => { + $crate::request::InlayHintResolveRequest + }; + ("window/showDocument") => { + $crate::request::ShowDocument + }; +} + +/// The initialize request is sent as the first request from the client to the server. +/// If the server receives request or notification before the `initialize` request it should act as follows: +/// +/// * for a request the respond should be errored with `code: -32001`. The message can be picked by the server. +/// * notifications should be dropped. +#[derive(Debug)] +pub enum Initialize {} + +impl Request for Initialize { + type Params = InitializeParams; + type Result = InitializeResult; + const METHOD: &'static str = "initialize"; +} + +/// The shutdown request is sent from the client to the server. It asks the server to shut down, +/// but to not exit (otherwise the response might not be delivered correctly to the client). +/// There is a separate exit notification that asks the server to exit. +#[derive(Debug)] +pub enum Shutdown {} + +impl Request for Shutdown { + type Params = (); + type Result = (); + const METHOD: &'static str = "shutdown"; +} + +/// The show message request is sent from a server to a client to ask the client to display a particular message +/// in the user interface. In addition to the show message notification the request allows to pass actions and to +/// wait for an answer from the client. +#[derive(Debug)] +pub enum ShowMessageRequest {} + +impl Request for ShowMessageRequest { + type Params = ShowMessageRequestParams; + type Result = Option; + const METHOD: &'static str = "window/showMessageRequest"; +} + +/// The client/registerCapability request is sent from the server to the client to register for a new capability +/// on the client side. Not all clients need to support dynamic capability registration. A client opts in via the +/// ClientCapabilities.GenericCapability property. +#[derive(Debug)] +pub enum RegisterCapability {} + +impl Request for RegisterCapability { + type Params = RegistrationParams; + type Result = (); + const METHOD: &'static str = "client/registerCapability"; +} + +/// The client/unregisterCapability request is sent from the server to the client to unregister a +/// previously register capability. +#[derive(Debug)] +pub enum UnregisterCapability {} + +impl Request for UnregisterCapability { + type Params = UnregistrationParams; + type Result = (); + const METHOD: &'static str = "client/unregisterCapability"; +} + +/// The Completion request is sent from the client to the server to compute completion items at a given cursor position. +/// Completion items are presented in the IntelliSense user interface. If computing full completion items is expensive, +/// servers can additionally provide a handler for the completion item resolve request ('completionItem/resolve'). +/// This request is sent when a completion item is selected in the user interface. A typical use case is for example: +/// the 'textDocument/completion' request doesn’t fill in the documentation property for returned completion items +/// since it is expensive to compute. When the item is selected in the user interface then a ‘completionItem/resolve’ +/// request is sent with the selected completion item as a param. The returned completion item should have the +/// documentation property filled in. The request can delay the computation of the detail and documentation properties. +/// However, properties that are needed for the initial sorting and filtering, like sortText, filterText, insertText, +/// and textEdit must be provided in the textDocument/completion request and must not be changed during resolve. +#[derive(Debug)] +pub enum Completion {} + +impl Request for Completion { + type Params = CompletionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/completion"; +} + +/// The request is sent from the client to the server to resolve additional information for a given completion item. +#[derive(Debug)] +pub enum ResolveCompletionItem {} + +impl Request for ResolveCompletionItem { + type Params = CompletionItem; + type Result = CompletionItem; + const METHOD: &'static str = "completionItem/resolve"; +} + +/// The hover request is sent from the client to the server to request hover information at a given text +/// document position. +#[derive(Debug)] +pub enum HoverRequest {} + +impl Request for HoverRequest { + type Params = HoverParams; + type Result = Option; + const METHOD: &'static str = "textDocument/hover"; +} + +/// The signature help request is sent from the client to the server to request signature information at +/// a given cursor position. +#[derive(Debug)] +pub enum SignatureHelpRequest {} + +impl Request for SignatureHelpRequest { + type Params = SignatureHelpParams; + type Result = Option; + const METHOD: &'static str = "textDocument/signatureHelp"; +} + +#[derive(Debug)] +pub enum GotoDeclaration {} +pub type GotoDeclarationParams = GotoDefinitionParams; +pub type GotoDeclarationResponse = GotoDefinitionResponse; + +/// The goto declaration request is sent from the client to the server to resolve the declaration location of +/// a symbol at a given text document position. +impl Request for GotoDeclaration { + type Params = GotoDeclarationParams; + type Result = Option; + const METHOD: &'static str = "textDocument/declaration"; +} + +/// The goto definition request is sent from the client to the server to resolve the definition location of +/// a symbol at a given text document position. +#[derive(Debug)] +pub enum GotoDefinition {} + +impl Request for GotoDefinition { + type Params = GotoDefinitionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/definition"; +} + +/// The references request is sent from the client to the server to resolve project-wide references for the +/// symbol denoted by the given text document position. +#[derive(Debug)] +pub enum References {} + +impl Request for References { + type Params = ReferenceParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/references"; +} + +/// The goto type definition request is sent from the client to the +/// server to resolve the type definition location of a symbol at a +/// given text document position. +#[derive(Debug)] +pub enum GotoTypeDefinition {} + +pub type GotoTypeDefinitionParams = GotoDefinitionParams; +pub type GotoTypeDefinitionResponse = GotoDefinitionResponse; + +impl Request for GotoTypeDefinition { + type Params = GotoTypeDefinitionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/typeDefinition"; +} + +/// The goto implementation request is sent from the client to the +/// server to resolve the implementation location of a symbol at a +/// given text document position. +#[derive(Debug)] +pub enum GotoImplementation {} + +pub type GotoImplementationParams = GotoTypeDefinitionParams; +pub type GotoImplementationResponse = GotoDefinitionResponse; + +impl Request for GotoImplementation { + type Params = GotoImplementationParams; + type Result = Option; + const METHOD: &'static str = "textDocument/implementation"; +} + +/// The document highlight request is sent from the client to the server to resolve a document highlights +/// for a given text document position. +/// For programming languages this usually highlights all references to the symbol scoped to this file. +/// However we kept 'textDocument/documentHighlight' and 'textDocument/references' separate requests since +/// the first one is allowed to be more fuzzy. +/// Symbol matches usually have a DocumentHighlightKind of Read or Write whereas fuzzy or textual matches +/// use Text as the kind. +#[derive(Debug)] +pub enum DocumentHighlightRequest {} + +impl Request for DocumentHighlightRequest { + type Params = DocumentHighlightParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/documentHighlight"; +} + +/// The document symbol request is sent from the client to the server to list all symbols found in a given +/// text document. +#[derive(Debug)] +pub enum DocumentSymbolRequest {} + +impl Request for DocumentSymbolRequest { + type Params = DocumentSymbolParams; + type Result = Option; + const METHOD: &'static str = "textDocument/documentSymbol"; +} + +/// The workspace symbol request is sent from the client to the server to list project-wide symbols +/// matching the query string. +#[derive(Debug)] +pub enum WorkspaceSymbolRequest {} + +impl Request for WorkspaceSymbolRequest { + type Params = WorkspaceSymbolParams; + type Result = Option; + const METHOD: &'static str = "workspace/symbol"; +} + +/// The `workspaceSymbol/resolve` request is sent from the client to the server to resolve +/// additional information for a given workspace symbol. +#[derive(Debug)] +pub enum WorkspaceSymbolResolve {} + +impl Request for WorkspaceSymbolResolve { + type Params = WorkspaceSymbol; + type Result = WorkspaceSymbol; + const METHOD: &'static str = "workspaceSymbol/resolve"; +} + +/// The workspace/executeCommand request is sent from the client to the server to trigger command execution on the server. +/// In most cases the server creates a WorkspaceEdit structure and applies the changes to the workspace using the request +/// workspace/applyEdit which is sent from the server to the client. +#[derive(Debug)] +pub enum ExecuteCommand {} + +impl Request for ExecuteCommand { + type Params = ExecuteCommandParams; + type Result = Option; + const METHOD: &'static str = "workspace/executeCommand"; +} + +/// The document will save request is sent from the client to the server before the document is +/// actually saved. The request can return an array of TextEdits which will be applied to the text +/// document before it is saved. Please note that clients might drop results if computing the text +/// edits took too long or if a server constantly fails on this request. This is done to keep the +/// save fast and reliable. +#[derive(Debug)] +pub enum WillSaveWaitUntil {} + +impl Request for WillSaveWaitUntil { + type Params = WillSaveTextDocumentParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/willSaveWaitUntil"; +} + +/// The workspace/applyEdit request is sent from the server to the client to modify resource on the +/// client side. +#[derive(Debug)] +pub enum ApplyWorkspaceEdit {} + +impl Request for ApplyWorkspaceEdit { + type Params = ApplyWorkspaceEditParams; + type Result = ApplyWorkspaceEditResponse; + const METHOD: &'static str = "workspace/applyEdit"; +} + +/// The workspace/configuration request is sent from the server to the client to fetch configuration settings +/// from the client. The request can fetch several configuration settings in one roundtrip. +/// The order of the returned configuration settings correspond to the order of the passed ConfigurationItems +/// (e.g. the first item in the response is the result for the first configuration item in the params). +/// +/// A ConfigurationItem consists of the configuration section to ask for and an additional scope URI. +/// The configuration section ask for is defined by the server and doesn’t necessarily need to correspond to +/// the configuration store used be the client. So a server might ask for a configuration cpp.formatterOptions +/// but the client stores the configuration in a XML store layout differently. +/// It is up to the client to do the necessary conversion. If a scope URI is provided the client should return +/// the setting scoped to the provided resource. If the client for example uses EditorConfig to manage its +/// settings the configuration should be returned for the passed resource URI. If the client can’t provide a +/// configuration setting for a given scope then null need to be present in the returned array. +#[derive(Debug)] +pub enum WorkspaceConfiguration {} + +impl Request for WorkspaceConfiguration { + type Params = ConfigurationParams; + type Result = Vec; + const METHOD: &'static str = "workspace/configuration"; +} + +/// The code action request is sent from the client to the server to compute commands for a given text document +/// and range. The request is triggered when the user moves the cursor into a problem marker in the editor or +/// presses the lightbulb associated with a marker. +#[derive(Debug)] +pub enum CodeActionRequest {} + +impl Request for CodeActionRequest { + type Params = CodeActionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/codeAction"; +} + +/// The request is sent from the client to the server to resolve additional information for a given code action. +/// This is usually used to compute the `edit` property of a code action to avoid its unnecessary computation +/// during the `textDocument/codeAction` request. +/// +/// @since 3.16.0 +#[derive(Debug)] +pub enum CodeActionResolveRequest {} + +impl Request for CodeActionResolveRequest { + type Params = CodeAction; + type Result = CodeAction; + const METHOD: &'static str = "codeAction/resolve"; +} + +/// The code lens request is sent from the client to the server to compute code lenses for a given text document. +#[derive(Debug)] +pub enum CodeLensRequest {} + +impl Request for CodeLensRequest { + type Params = CodeLensParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/codeLens"; +} + +/// The code lens resolve request is sent from the client to the server to resolve the command for a +/// given code lens item. +#[derive(Debug)] +pub enum CodeLensResolve {} + +impl Request for CodeLensResolve { + type Params = CodeLens; + type Result = CodeLens; + const METHOD: &'static str = "codeLens/resolve"; +} + +/// The document links request is sent from the client to the server to request the location of links in a document. +#[derive(Debug)] +pub enum DocumentLinkRequest {} + +impl Request for DocumentLinkRequest { + type Params = DocumentLinkParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/documentLink"; +} + +/// The document link resolve request is sent from the client to the server to resolve the target of +/// a given document link. +#[derive(Debug)] +pub enum DocumentLinkResolve {} + +impl Request for DocumentLinkResolve { + type Params = DocumentLink; + type Result = DocumentLink; + const METHOD: &'static str = "documentLink/resolve"; +} + +/// The document formatting request is sent from the server to the client to format a whole document. +#[derive(Debug)] +pub enum Formatting {} + +impl Request for Formatting { + type Params = DocumentFormattingParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/formatting"; +} + +/// The document range formatting request is sent from the client to the server to format a given range in a document. +#[derive(Debug)] +pub enum RangeFormatting {} + +impl Request for RangeFormatting { + type Params = DocumentRangeFormattingParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/rangeFormatting"; +} + +/// The document on type formatting request is sent from the client to the server to format parts of +/// the document during typing. +#[derive(Debug)] +pub enum OnTypeFormatting {} + +impl Request for OnTypeFormatting { + type Params = DocumentOnTypeFormattingParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/onTypeFormatting"; +} + +/// The linked editing request is sent from the client to the server to return for a given position in a document +/// the range of the symbol at the position and all ranges that have the same content. +/// Optionally a word pattern can be returned to describe valid contents. A rename to one of the ranges can be applied +/// to all other ranges if the new content is valid. If no result-specific word pattern is provided, the word pattern from +/// the client’s language configuration is used. +#[derive(Debug)] +pub enum LinkedEditingRange {} + +impl Request for LinkedEditingRange { + type Params = LinkedEditingRangeParams; + type Result = Option; + const METHOD: &'static str = "textDocument/linkedEditingRange"; +} + +/// The rename request is sent from the client to the server to perform a workspace-wide rename of a symbol. +#[derive(Debug)] +pub enum Rename {} + +impl Request for Rename { + type Params = RenameParams; + type Result = Option; + const METHOD: &'static str = "textDocument/rename"; +} + +/// The document color request is sent from the client to the server to list all color references found in a given text document. +/// Along with the range, a color value in RGB is returned. +#[derive(Debug)] +pub enum DocumentColor {} + +impl Request for DocumentColor { + type Params = DocumentColorParams; + type Result = Vec; + const METHOD: &'static str = "textDocument/documentColor"; +} + +/// The color presentation request is sent from the client to the server to obtain a list of presentations for a color value +/// at a given location. +#[derive(Debug)] +pub enum ColorPresentationRequest {} + +impl Request for ColorPresentationRequest { + type Params = ColorPresentationParams; + type Result = Vec; + const METHOD: &'static str = "textDocument/colorPresentation"; +} + +/// The folding range request is sent from the client to the server to return all folding ranges found in a given text document. +#[derive(Debug)] +pub enum FoldingRangeRequest {} + +impl Request for FoldingRangeRequest { + type Params = FoldingRangeParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/foldingRange"; +} + +/// The prepare rename request is sent from the client to the server to setup and test the validity of a rename operation +/// at a given location. +#[derive(Debug)] +pub enum PrepareRenameRequest {} + +impl Request for PrepareRenameRequest { + type Params = TextDocumentPositionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/prepareRename"; +} + +#[derive(Debug)] +#[cfg(feature = "proposed")] +pub enum InlineCompletionRequest {} + +#[cfg(feature = "proposed")] +impl Request for InlineCompletionRequest { + type Params = InlineCompletionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/inlineCompletion"; +} + +/// The workspace/workspaceFolders request is sent from the server to the client to fetch the current open list of +/// workspace folders. Returns null in the response if only a single file is open in the tool. +/// Returns an empty array if a workspace is open but no folders are configured. +#[derive(Debug)] +pub enum WorkspaceFoldersRequest {} + +impl Request for WorkspaceFoldersRequest { + type Params = (); + type Result = Option>; + const METHOD: &'static str = "workspace/workspaceFolders"; +} + +/// The `window/workDoneProgress/create` request is sent from the server +/// to the client to ask the client to create a work done progress. +#[derive(Debug)] +pub enum WorkDoneProgressCreate {} + +impl Request for WorkDoneProgressCreate { + type Params = WorkDoneProgressCreateParams; + type Result = (); + const METHOD: &'static str = "window/workDoneProgress/create"; +} + +/// The selection range request is sent from the client to the server to return +/// suggested selection ranges at given positions. A selection range is a range +/// around the cursor position which the user might be interested in selecting. +/// +/// A selection range in the return array is for the position in the provided parameters at the same index. +/// Therefore `positions[i]` must be contained in `result[i].range`. +/// +/// Typically, but not necessary, selection ranges correspond to the nodes of the +/// syntax tree. +pub enum SelectionRangeRequest {} + +impl Request for SelectionRangeRequest { + type Params = SelectionRangeParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/selectionRange"; +} + +pub enum CallHierarchyPrepare {} + +impl Request for CallHierarchyPrepare { + type Params = CallHierarchyPrepareParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/prepareCallHierarchy"; +} + +pub enum CallHierarchyIncomingCalls {} + +impl Request for CallHierarchyIncomingCalls { + type Params = CallHierarchyIncomingCallsParams; + type Result = Option>; + const METHOD: &'static str = "callHierarchy/incomingCalls"; +} + +pub enum CallHierarchyOutgoingCalls {} + +impl Request for CallHierarchyOutgoingCalls { + type Params = CallHierarchyOutgoingCallsParams; + type Result = Option>; + const METHOD: &'static str = "callHierarchy/outgoingCalls"; +} + +pub enum SemanticTokensFullRequest {} + +impl Request for SemanticTokensFullRequest { + type Params = SemanticTokensParams; + type Result = Option; + const METHOD: &'static str = "textDocument/semanticTokens/full"; +} + +pub enum SemanticTokensFullDeltaRequest {} + +impl Request for SemanticTokensFullDeltaRequest { + type Params = SemanticTokensDeltaParams; + type Result = Option; + const METHOD: &'static str = "textDocument/semanticTokens/full/delta"; +} + +pub enum SemanticTokensRangeRequest {} + +impl Request for SemanticTokensRangeRequest { + type Params = SemanticTokensRangeParams; + type Result = Option; + const METHOD: &'static str = "textDocument/semanticTokens/range"; +} + +/// The `workspace/semanticTokens/refresh` request is sent from the server to the client. +/// Servers can use it to ask clients to refresh the editors for which this server provides semantic tokens. +/// As a result the client should ask the server to recompute the semantic tokens for these editors. +/// This is useful if a server detects a project wide configuration change which requires a re-calculation of all semantic tokens. +/// Note that the client still has the freedom to delay the re-calculation of the semantic tokens if for example an editor is currently not visible. +pub enum SemanticTokensRefresh {} + +impl Request for SemanticTokensRefresh { + type Params = (); + type Result = (); + const METHOD: &'static str = "workspace/semanticTokens/refresh"; +} + +/// The workspace/codeLens/refresh request is sent from the server to the client. +/// Servers can use it to ask clients to refresh the code lenses currently shown in editors. +/// As a result the client should ask the server to recompute the code lenses for these editors. +/// This is useful if a server detects a configuration change which requires a re-calculation of all code lenses. +/// Note that the client still has the freedom to delay the re-calculation of the code lenses if for example an editor is currently not visible. +pub enum CodeLensRefresh {} + +impl Request for CodeLensRefresh { + type Params = (); + type Result = (); + const METHOD: &'static str = "workspace/codeLens/refresh"; +} + +/// The will create files request is sent from the client to the server before files are actually created as long as the creation is triggered from within the client. The request can return a WorkspaceEdit which will be applied to workspace before the files are created. Please note that clients might drop results if computing the edit took too long or if a server constantly fails on this request. This is done to keep creates fast and reliable. +pub enum WillCreateFiles {} + +impl Request for WillCreateFiles { + type Params = CreateFilesParams; + type Result = Option; + const METHOD: &'static str = "workspace/willCreateFiles"; +} + +/// The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client. The request can return a WorkspaceEdit which will be applied to workspace before the files are renamed. Please note that clients might drop results if computing the edit took too long or if a server constantly fails on this request. This is done to keep renames fast and reliable. +pub enum WillRenameFiles {} + +impl Request for WillRenameFiles { + type Params = RenameFilesParams; + type Result = Option; + const METHOD: &'static str = "workspace/willRenameFiles"; +} + +/// The will delete files request is sent from the client to the server before files are actually deleted as long as the deletion is triggered from within the client. The request can return a WorkspaceEdit which will be applied to workspace before the files are deleted. Please note that clients might drop results if computing the edit took too long or if a server constantly fails on this request. This is done to keep deletes fast and reliable. +pub enum WillDeleteFiles {} + +impl Request for WillDeleteFiles { + type Params = DeleteFilesParams; + type Result = Option; + const METHOD: &'static str = "workspace/willDeleteFiles"; +} + +/// The show document request is sent from a server to a client to ask the client to display a particular document in the user interface. +pub enum ShowDocument {} + +impl Request for ShowDocument { + type Params = ShowDocumentParams; + type Result = ShowDocumentResult; + const METHOD: &'static str = "window/showDocument"; +} + +pub enum MonikerRequest {} + +impl Request for MonikerRequest { + type Params = MonikerParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/moniker"; +} + +/// The inlay hints request is sent from the client to the server to compute inlay hints for a given +/// [text document, range] tuple that may be rendered in the editor in place with other text. +pub enum InlayHintRequest {} + +impl Request for InlayHintRequest { + type Params = InlayHintParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/inlayHint"; +} + +/// The `inlayHint/resolve` request is sent from the client to the server to resolve additional +/// information for a given inlay hint. This is usually used to compute the tooltip, location or +/// command properties of a inlay hint’s label part to avoid its unnecessary computation during the +/// `textDocument/inlayHint` request. +pub enum InlayHintResolveRequest {} + +impl Request for InlayHintResolveRequest { + type Params = InlayHint; + type Result = InlayHint; + const METHOD: &'static str = "inlayHint/resolve"; +} + +/// The `workspace/inlayHint/refresh` request is sent from the server to the client. Servers can use +/// it to ask clients to refresh the inlay hints currently shown in editors. As a result the client +/// should ask the server to recompute the inlay hints for these editors. This is useful if a server +/// detects a configuration change which requires a re-calculation of all inlay hints. Note that the +/// client still has the freedom to delay the re-calculation of the inlay hints if for example an +/// editor is currently not visible. +pub enum InlayHintRefreshRequest {} + +impl Request for InlayHintRefreshRequest { + type Params = (); + type Result = (); + const METHOD: &'static str = "workspace/inlayHint/refresh"; +} + +/// The inline value request is sent from the client to the server to compute inline values for a +/// given text document that may be rendered in the editor at the end of lines. +pub enum InlineValueRequest {} + +impl Request for InlineValueRequest { + type Params = InlineValueParams; + type Result = Option; + const METHOD: &'static str = "textDocument/inlineValue"; +} + +/// The `workspace/inlineValue/refresh` request is sent from the server to the client. Servers can +/// use it to ask clients to refresh the inline values currently shown in editors. As a result the +/// client should ask the server to recompute the inline values for these editors. This is useful if +/// a server detects a configuration change which requires a re-calculation of all inline values. +/// Note that the client still has the freedom to delay the re-calculation of the inline values if +/// for example an editor is currently not visible. +pub enum InlineValueRefreshRequest {} + +impl Request for InlineValueRefreshRequest { + type Params = (); + type Result = (); + const METHOD: &'static str = "workspace/inlineValue/refresh"; +} + +/// The text document diagnostic request is sent from the client to the server to ask the server to +/// compute the diagnostics for a given document. As with other pull requests the server is asked +/// to compute the diagnostics for the currently synced version of the document. +#[derive(Debug)] +pub enum DocumentDiagnosticRequest {} + +impl Request for DocumentDiagnosticRequest { + type Params = DocumentDiagnosticParams; + type Result = DocumentDiagnosticReportResult; + const METHOD: &'static str = "textDocument/diagnostic"; +} + +/// The workspace diagnostic request is sent from the client to the server to ask the server to +/// compute workspace wide diagnostics which previously where pushed from the server to the client. +/// In contrast to the document diagnostic request the workspace request can be long running and is +/// not bound to a specific workspace or document state. If the client supports streaming for the +/// workspace diagnostic pull it is legal to provide a document diagnostic report multiple times +/// for the same document URI. The last one reported will win over previous reports. +#[derive(Debug)] +pub enum WorkspaceDiagnosticRequest {} + +impl Request for WorkspaceDiagnosticRequest { + type Params = WorkspaceDiagnosticParams; + const METHOD: &'static str = "workspace/diagnostic"; + type Result = WorkspaceDiagnosticReportResult; +} + +/// The `workspace/diagnostic/refresh` request is sent from the server to the client. Servers can +/// use it to ask clients to refresh all needed document and workspace diagnostics. This is useful +/// if a server detects a project wide configuration change which requires a re-calculation of all +/// diagnostics. +#[derive(Debug)] +pub enum WorkspaceDiagnosticRefresh {} + +impl Request for WorkspaceDiagnosticRefresh { + type Params = (); + type Result = (); + const METHOD: &'static str = "workspace/diagnostic/refresh"; +} + +/// The type hierarchy request is sent from the client to the server to return a type hierarchy for +/// the language element of given text document positions. Will return null if the server couldn’t +/// infer a valid type from the position. The type hierarchy requests are executed in two steps: +/// +/// 1. first a type hierarchy item is prepared for the given text document position. +/// 2. for a type hierarchy item the supertype or subtype type hierarchy items are resolved. +pub enum TypeHierarchyPrepare {} + +impl Request for TypeHierarchyPrepare { + type Params = TypeHierarchyPrepareParams; + type Result = Option>; + const METHOD: &'static str = "textDocument/prepareTypeHierarchy"; +} + +/// The `typeHierarchy/supertypes` request is sent from the client to the server to resolve the +/// supertypes for a given type hierarchy item. Will return null if the server couldn’t infer a +/// valid type from item in the params. The request doesn’t define its own client and server +/// capabilities. It is only issued if a server registers for the +/// `textDocument/prepareTypeHierarchy` request. +pub enum TypeHierarchySupertypes {} + +impl Request for TypeHierarchySupertypes { + type Params = TypeHierarchySupertypesParams; + type Result = Option>; + const METHOD: &'static str = "typeHierarchy/supertypes"; +} + +/// The `typeHierarchy/subtypes` request is sent from the client to the server to resolve the +/// subtypes for a given type hierarchy item. Will return null if the server couldn’t infer a valid +/// type from item in the params. The request doesn’t define its own client and server capabilities. +/// It is only issued if a server registers for the textDocument/prepareTypeHierarchy request. +pub enum TypeHierarchySubtypes {} + +impl Request for TypeHierarchySubtypes { + type Params = TypeHierarchySubtypesParams; + type Result = Option>; + const METHOD: &'static str = "typeHierarchy/subtypes"; +} + +#[cfg(test)] +mod test { + use super::*; + + fn fake_call() + where + R: Request, + R::Params: serde::Serialize, + R::Result: serde::de::DeserializeOwned, + { + } + + macro_rules! check_macro { + ($name:tt) => { + // check whether the macro name matches the method + assert_eq!(::METHOD, $name); + // test whether type checking passes for each component + fake_call::(); + }; + } + + #[test] + fn check_macro_definitions() { + check_macro!("initialize"); + check_macro!("shutdown"); + + check_macro!("window/showDocument"); + check_macro!("window/showMessageRequest"); + check_macro!("window/workDoneProgress/create"); + + check_macro!("client/registerCapability"); + check_macro!("client/unregisterCapability"); + + check_macro!("textDocument/willSaveWaitUntil"); + check_macro!("textDocument/completion"); + check_macro!("textDocument/hover"); + check_macro!("textDocument/signatureHelp"); + check_macro!("textDocument/declaration"); + check_macro!("textDocument/definition"); + check_macro!("textDocument/references"); + check_macro!("textDocument/documentHighlight"); + check_macro!("textDocument/documentSymbol"); + check_macro!("textDocument/codeAction"); + check_macro!("textDocument/codeLens"); + check_macro!("textDocument/documentLink"); + check_macro!("textDocument/rangeFormatting"); + check_macro!("textDocument/onTypeFormatting"); + check_macro!("textDocument/formatting"); + check_macro!("textDocument/rename"); + check_macro!("textDocument/documentColor"); + check_macro!("textDocument/colorPresentation"); + check_macro!("textDocument/foldingRange"); + check_macro!("textDocument/prepareRename"); + check_macro!("textDocument/implementation"); + check_macro!("textDocument/selectionRange"); + check_macro!("textDocument/typeDefinition"); + check_macro!("textDocument/moniker"); + check_macro!("textDocument/linkedEditingRange"); + check_macro!("textDocument/prepareCallHierarchy"); + check_macro!("textDocument/prepareTypeHierarchy"); + check_macro!("textDocument/semanticTokens/full"); + check_macro!("textDocument/semanticTokens/full/delta"); + check_macro!("textDocument/semanticTokens/range"); + check_macro!("textDocument/inlayHint"); + check_macro!("textDocument/inlineValue"); + check_macro!("textDocument/diagnostic"); + + check_macro!("workspace/applyEdit"); + check_macro!("workspace/symbol"); + check_macro!("workspace/executeCommand"); + check_macro!("workspace/configuration"); + check_macro!("workspace/diagnostic"); + check_macro!("workspace/diagnostic/refresh"); + check_macro!("workspace/willCreateFiles"); + check_macro!("workspace/willRenameFiles"); + check_macro!("workspace/willDeleteFiles"); + check_macro!("workspace/workspaceFolders"); + check_macro!("workspace/semanticTokens/refresh"); + check_macro!("workspace/codeLens/refresh"); + check_macro!("workspace/inlayHint/refresh"); + check_macro!("workspace/inlineValue/refresh"); + + check_macro!("callHierarchy/incomingCalls"); + check_macro!("callHierarchy/outgoingCalls"); + check_macro!("codeAction/resolve"); + check_macro!("codeLens/resolve"); + check_macro!("completionItem/resolve"); + check_macro!("documentLink/resolve"); + check_macro!("inlayHint/resolve"); + check_macro!("typeHierarchy/subtypes"); + check_macro!("typeHierarchy/supertypes"); + check_macro!("workspaceSymbol/resolve"); + } + + #[test] + #[cfg(feature = "proposed")] + fn check_proposed_macro_definitions() {} +} diff --git a/helix-lsp-types/src/selection_range.rs b/helix-lsp-types/src/selection_range.rs new file mode 100644 index 000000000..048df6f99 --- /dev/null +++ b/helix-lsp-types/src/selection_range.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + PartialResultParams, Position, Range, StaticTextDocumentRegistrationOptions, + TextDocumentIdentifier, WorkDoneProgressOptions, WorkDoneProgressParams, +}; +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SelectionRangeClientCapabilities { + /// Whether implementation supports dynamic registration for selection range + /// providers. If this is set to `true` the client supports the new + /// `SelectionRangeRegistrationOptions` return value for the corresponding + /// server capability as well. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct SelectionRangeOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct SelectionRangeRegistrationOptions { + #[serde(flatten)] + pub selection_range_options: SelectionRangeOptions, + + #[serde(flatten)] + pub registration_options: StaticTextDocumentRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum SelectionRangeProviderCapability { + Simple(bool), + Options(SelectionRangeOptions), + RegistrationOptions(SelectionRangeRegistrationOptions), +} + +impl From for SelectionRangeProviderCapability { + fn from(from: SelectionRangeRegistrationOptions) -> Self { + Self::RegistrationOptions(from) + } +} + +impl From for SelectionRangeProviderCapability { + fn from(from: SelectionRangeOptions) -> Self { + Self::Options(from) + } +} + +impl From for SelectionRangeProviderCapability { + fn from(from: bool) -> Self { + Self::Simple(from) + } +} + +/// A parameter literal used in selection range requests. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SelectionRangeParams { + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The positions inside the text document. + pub positions: Vec, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// Represents a selection range. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SelectionRange { + /// Range of the selection. + pub range: Range, + + /// The parent selection range containing this range. + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option>, +} diff --git a/helix-lsp-types/src/semantic_tokens.rs b/helix-lsp-types/src/semantic_tokens.rs new file mode 100644 index 000000000..842558bac --- /dev/null +++ b/helix-lsp-types/src/semantic_tokens.rs @@ -0,0 +1,734 @@ +use std::borrow::Cow; + +use serde::ser::SerializeSeq; +use serde::{Deserialize, Serialize}; + +use crate::{ + PartialResultParams, Range, StaticRegistrationOptions, TextDocumentIdentifier, + TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams, +}; +/// A set of predefined token types. This set is not fixed +/// and clients can specify additional token types via the +/// corresponding client capabilities. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Clone, Deserialize, Serialize)] +pub struct SemanticTokenType(Cow<'static, str>); + +impl SemanticTokenType { + pub const NAMESPACE: SemanticTokenType = SemanticTokenType::new("namespace"); + pub const TYPE: SemanticTokenType = SemanticTokenType::new("type"); + pub const CLASS: SemanticTokenType = SemanticTokenType::new("class"); + pub const ENUM: SemanticTokenType = SemanticTokenType::new("enum"); + pub const INTERFACE: SemanticTokenType = SemanticTokenType::new("interface"); + pub const STRUCT: SemanticTokenType = SemanticTokenType::new("struct"); + pub const TYPE_PARAMETER: SemanticTokenType = SemanticTokenType::new("typeParameter"); + pub const PARAMETER: SemanticTokenType = SemanticTokenType::new("parameter"); + pub const VARIABLE: SemanticTokenType = SemanticTokenType::new("variable"); + pub const PROPERTY: SemanticTokenType = SemanticTokenType::new("property"); + pub const ENUM_MEMBER: SemanticTokenType = SemanticTokenType::new("enumMember"); + pub const EVENT: SemanticTokenType = SemanticTokenType::new("event"); + pub const FUNCTION: SemanticTokenType = SemanticTokenType::new("function"); + pub const METHOD: SemanticTokenType = SemanticTokenType::new("method"); + pub const MACRO: SemanticTokenType = SemanticTokenType::new("macro"); + pub const KEYWORD: SemanticTokenType = SemanticTokenType::new("keyword"); + pub const MODIFIER: SemanticTokenType = SemanticTokenType::new("modifier"); + pub const COMMENT: SemanticTokenType = SemanticTokenType::new("comment"); + pub const STRING: SemanticTokenType = SemanticTokenType::new("string"); + pub const NUMBER: SemanticTokenType = SemanticTokenType::new("number"); + pub const REGEXP: SemanticTokenType = SemanticTokenType::new("regexp"); + pub const OPERATOR: SemanticTokenType = SemanticTokenType::new("operator"); + + /// @since 3.17.0 + pub const DECORATOR: SemanticTokenType = SemanticTokenType::new("decorator"); + + pub const fn new(tag: &'static str) -> Self { + SemanticTokenType(Cow::Borrowed(tag)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for SemanticTokenType { + fn from(from: String) -> Self { + SemanticTokenType(Cow::from(from)) + } +} + +impl From<&'static str> for SemanticTokenType { + fn from(from: &'static str) -> Self { + SemanticTokenType::new(from) + } +} + +/// A set of predefined token modifiers. This set is not fixed +/// and clients can specify additional token types via the +/// corresponding client capabilities. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Clone, Deserialize, Serialize)] +pub struct SemanticTokenModifier(Cow<'static, str>); + +impl SemanticTokenModifier { + pub const DECLARATION: SemanticTokenModifier = SemanticTokenModifier::new("declaration"); + pub const DEFINITION: SemanticTokenModifier = SemanticTokenModifier::new("definition"); + pub const READONLY: SemanticTokenModifier = SemanticTokenModifier::new("readonly"); + pub const STATIC: SemanticTokenModifier = SemanticTokenModifier::new("static"); + pub const DEPRECATED: SemanticTokenModifier = SemanticTokenModifier::new("deprecated"); + pub const ABSTRACT: SemanticTokenModifier = SemanticTokenModifier::new("abstract"); + pub const ASYNC: SemanticTokenModifier = SemanticTokenModifier::new("async"); + pub const MODIFICATION: SemanticTokenModifier = SemanticTokenModifier::new("modification"); + pub const DOCUMENTATION: SemanticTokenModifier = SemanticTokenModifier::new("documentation"); + pub const DEFAULT_LIBRARY: SemanticTokenModifier = SemanticTokenModifier::new("defaultLibrary"); + + pub const fn new(tag: &'static str) -> Self { + SemanticTokenModifier(Cow::Borrowed(tag)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for SemanticTokenModifier { + fn from(from: String) -> Self { + SemanticTokenModifier(Cow::from(from)) + } +} + +impl From<&'static str> for SemanticTokenModifier { + fn from(from: &'static str) -> Self { + SemanticTokenModifier::new(from) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Clone, Deserialize, Serialize)] +pub struct TokenFormat(Cow<'static, str>); + +impl TokenFormat { + pub const RELATIVE: TokenFormat = TokenFormat::new("relative"); + + pub const fn new(tag: &'static str) -> Self { + TokenFormat(Cow::Borrowed(tag)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for TokenFormat { + fn from(from: String) -> Self { + TokenFormat(Cow::from(from)) + } +} + +impl From<&'static str> for TokenFormat { + fn from(from: &'static str) -> Self { + TokenFormat::new(from) + } +} + +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensLegend { + /// The token types a server uses. + pub token_types: Vec, + + /// The token modifiers a server uses. + pub token_modifiers: Vec, +} + +/// The actual tokens. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)] +pub struct SemanticToken { + pub delta_line: u32, + pub delta_start: u32, + pub length: u32, + pub token_type: u32, + pub token_modifiers_bitset: u32, +} + +impl SemanticToken { + fn deserialize_tokens<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let data = Vec::::deserialize(deserializer)?; + let chunks = data.chunks_exact(5); + + if !chunks.remainder().is_empty() { + return Result::Err(serde::de::Error::custom("Length is not divisible by 5")); + } + + Result::Ok( + chunks + .map(|chunk| SemanticToken { + delta_line: chunk[0], + delta_start: chunk[1], + length: chunk[2], + token_type: chunk[3], + token_modifiers_bitset: chunk[4], + }) + .collect(), + ) + } + + fn serialize_tokens(tokens: &[SemanticToken], serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(tokens.len() * 5))?; + for token in tokens.iter() { + seq.serialize_element(&token.delta_line)?; + seq.serialize_element(&token.delta_start)?; + seq.serialize_element(&token.length)?; + seq.serialize_element(&token.token_type)?; + seq.serialize_element(&token.token_modifiers_bitset)?; + } + seq.end() + } + + fn deserialize_tokens_opt<'de, D>( + deserializer: D, + ) -> Result>, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(transparent)] + struct Wrapper { + #[serde(deserialize_with = "SemanticToken::deserialize_tokens")] + tokens: Vec, + } + + Ok(Option::::deserialize(deserializer)?.map(|wrapper| wrapper.tokens)) + } + + fn serialize_tokens_opt( + data: &Option>, + serializer: S, + ) -> Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + #[serde(transparent)] + struct Wrapper { + #[serde(serialize_with = "SemanticToken::serialize_tokens")] + tokens: Vec, + } + + let opt = data.as_ref().map(|t| Wrapper { tokens: t.to_vec() }); + + opt.serialize(serializer) + } +} + +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokens { + /// An optional result id. If provided and clients support delta updating + /// the client will include the result id in the next semantic token request. + /// A server can then instead of computing all semantic tokens again simply + /// send a delta. + #[serde(skip_serializing_if = "Option::is_none")] + pub result_id: Option, + + /// The actual tokens. For a detailed description about how the data is + /// structured please see + /// + #[serde( + deserialize_with = "SemanticToken::deserialize_tokens", + serialize_with = "SemanticToken::serialize_tokens" + )] + pub data: Vec, +} + +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensPartialResult { + #[serde( + deserialize_with = "SemanticToken::deserialize_tokens", + serialize_with = "SemanticToken::serialize_tokens" + )] + pub data: Vec, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum SemanticTokensResult { + Tokens(SemanticTokens), + Partial(SemanticTokensPartialResult), +} + +impl From for SemanticTokensResult { + fn from(from: SemanticTokens) -> Self { + SemanticTokensResult::Tokens(from) + } +} + +impl From for SemanticTokensResult { + fn from(from: SemanticTokensPartialResult) -> Self { + SemanticTokensResult::Partial(from) + } +} + +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensEdit { + pub start: u32, + pub delete_count: u32, + + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "SemanticToken::deserialize_tokens_opt", + serialize_with = "SemanticToken::serialize_tokens_opt" + )] + pub data: Option>, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum SemanticTokensFullDeltaResult { + Tokens(SemanticTokens), + TokensDelta(SemanticTokensDelta), + PartialTokensDelta { edits: Vec }, +} + +impl From for SemanticTokensFullDeltaResult { + fn from(from: SemanticTokens) -> Self { + SemanticTokensFullDeltaResult::Tokens(from) + } +} + +impl From for SemanticTokensFullDeltaResult { + fn from(from: SemanticTokensDelta) -> Self { + SemanticTokensFullDeltaResult::TokensDelta(from) + } +} + +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensDelta { + #[serde(skip_serializing_if = "Option::is_none")] + pub result_id: Option, + /// For a detailed description how these edits are structured please see + /// + pub edits: Vec, +} + +/// Capabilities specific to the `textDocument/semanticTokens/*` requests. +/// +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensClientCapabilities { + /// Whether implementation supports dynamic registration. If this is set to `true` + /// the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + /// return value for the corresponding server capability as well. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Which requests the client supports and might send to the server + /// depending on the server's capability. Please note that clients might not + /// show semantic tokens or degrade some of the user experience if a range + /// or full request is advertised by the client but not provided by the + /// server. If for example the client capability `requests.full` and + /// `request.range` are both set to true but the server only provides a + /// range provider the client might not render a minimap correctly or might + /// even decide to not show any semantic tokens at all. + pub requests: SemanticTokensClientCapabilitiesRequests, + + /// The token types that the client supports. + pub token_types: Vec, + + /// The token modifiers that the client supports. + pub token_modifiers: Vec, + + /// The token formats the clients supports. + pub formats: Vec, + + /// Whether the client supports tokens that can overlap each other. + #[serde(skip_serializing_if = "Option::is_none")] + pub overlapping_token_support: Option, + + /// Whether the client supports tokens that can span multiple lines. + #[serde(skip_serializing_if = "Option::is_none")] + pub multiline_token_support: Option, + + /// Whether the client allows the server to actively cancel a + /// semantic token request, e.g. supports returning + /// ErrorCodes.ServerCancelled. If a server does the client + /// needs to retrigger the request. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub server_cancel_support: Option, + + /// Whether the client uses semantic tokens to augment existing + /// syntax tokens. If set to `true` client side created syntax + /// tokens and semantic tokens are both used for colorization. If + /// set to `false` the client only uses the returned semantic tokens + /// for colorization. + /// + /// If the value is `undefined` then the client behavior is not + /// specified. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub augments_syntax_tokens: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensClientCapabilitiesRequests { + /// The client will send the `textDocument/semanticTokens/range` request if the server provides a corresponding handler. + #[serde(skip_serializing_if = "Option::is_none")] + pub range: Option, + + /// The client will send the `textDocument/semanticTokens/full` request if the server provides a corresponding handler. + #[serde(skip_serializing_if = "Option::is_none")] + pub full: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum SemanticTokensFullOptions { + Bool(bool), + Delta { + /// The client will send the `textDocument/semanticTokens/full/delta` request if the server provides a corresponding handler. + /// The server supports deltas for full documents. + #[serde(skip_serializing_if = "Option::is_none")] + delta: Option, + }, +} + +/// @since 3.16.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, + + /// The legend used by the server + pub legend: SemanticTokensLegend, + + /// Server supports providing semantic tokens for a specific range + /// of a document. + #[serde(skip_serializing_if = "Option::is_none")] + pub range: Option, + + /// Server supports providing semantic tokens for a full document. + #[serde(skip_serializing_if = "Option::is_none")] + pub full: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub semantic_tokens_options: SemanticTokensOptions, + + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum SemanticTokensServerCapabilities { + SemanticTokensOptions(SemanticTokensOptions), + SemanticTokensRegistrationOptions(SemanticTokensRegistrationOptions), +} + +impl From for SemanticTokensServerCapabilities { + fn from(from: SemanticTokensOptions) -> Self { + SemanticTokensServerCapabilities::SemanticTokensOptions(from) + } +} + +impl From for SemanticTokensServerCapabilities { + fn from(from: SemanticTokensRegistrationOptions) -> Self { + SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(from) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensWorkspaceClientCapabilities { + /// Whether the client implementation supports a refresh request sent from + /// the server to the client. + /// + /// Note that this event is global and will force the client to refresh all + /// semantic tokens currently shown. It should be used with absolute care + /// and is useful for situation where a server for example detect a project + /// wide change that requires such a calculation. + pub refresh_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensParams { + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, + + /// The text document. + pub text_document: TextDocumentIdentifier, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensDeltaParams { + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, + + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The result id of a previous response. The result Id can either point to a full response + /// or a delta response depending on what was received last. + pub previous_result_id: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SemanticTokensRangeParams { + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, + + /// The text document. + pub text_document: TextDocumentIdentifier, + + /// The range the semantic tokens are requested for. + pub range: Range, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum SemanticTokensRangeResult { + Tokens(SemanticTokens), + Partial(SemanticTokensPartialResult), +} + +impl From for SemanticTokensRangeResult { + fn from(tokens: SemanticTokens) -> Self { + SemanticTokensRangeResult::Tokens(tokens) + } +} + +impl From for SemanticTokensRangeResult { + fn from(partial: SemanticTokensPartialResult) -> Self { + SemanticTokensRangeResult::Partial(partial) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{test_deserialization, test_serialization}; + + #[test] + fn test_semantic_tokens_support_serialization() { + test_serialization( + &SemanticTokens { + result_id: None, + data: vec![], + }, + r#"{"data":[]}"#, + ); + + test_serialization( + &SemanticTokens { + result_id: None, + data: vec![SemanticToken { + delta_line: 2, + delta_start: 5, + length: 3, + token_type: 0, + token_modifiers_bitset: 3, + }], + }, + r#"{"data":[2,5,3,0,3]}"#, + ); + + test_serialization( + &SemanticTokens { + result_id: None, + data: vec![ + SemanticToken { + delta_line: 2, + delta_start: 5, + length: 3, + token_type: 0, + token_modifiers_bitset: 3, + }, + SemanticToken { + delta_line: 0, + delta_start: 5, + length: 4, + token_type: 1, + token_modifiers_bitset: 0, + }, + ], + }, + r#"{"data":[2,5,3,0,3,0,5,4,1,0]}"#, + ); + } + + #[test] + fn test_semantic_tokens_support_deserialization() { + test_deserialization( + r#"{"data":[]}"#, + &SemanticTokens { + result_id: None, + data: vec![], + }, + ); + + test_deserialization( + r#"{"data":[2,5,3,0,3]}"#, + &SemanticTokens { + result_id: None, + data: vec![SemanticToken { + delta_line: 2, + delta_start: 5, + length: 3, + token_type: 0, + token_modifiers_bitset: 3, + }], + }, + ); + + test_deserialization( + r#"{"data":[2,5,3,0,3,0,5,4,1,0]}"#, + &SemanticTokens { + result_id: None, + data: vec![ + SemanticToken { + delta_line: 2, + delta_start: 5, + length: 3, + token_type: 0, + token_modifiers_bitset: 3, + }, + SemanticToken { + delta_line: 0, + delta_start: 5, + length: 4, + token_type: 1, + token_modifiers_bitset: 0, + }, + ], + }, + ); + } + + #[test] + #[should_panic] + fn test_semantic_tokens_support_deserialization_err() { + test_deserialization( + r#"{"data":[1]}"#, + &SemanticTokens { + result_id: None, + data: vec![], + }, + ); + } + + #[test] + fn test_semantic_tokens_edit_support_deserialization() { + test_deserialization( + r#"{"start":0,"deleteCount":1,"data":[2,5,3,0,3,0,5,4,1,0]}"#, + &SemanticTokensEdit { + start: 0, + delete_count: 1, + data: Some(vec![ + SemanticToken { + delta_line: 2, + delta_start: 5, + length: 3, + token_type: 0, + token_modifiers_bitset: 3, + }, + SemanticToken { + delta_line: 0, + delta_start: 5, + length: 4, + token_type: 1, + token_modifiers_bitset: 0, + }, + ]), + }, + ); + + test_deserialization( + r#"{"start":0,"deleteCount":1}"#, + &SemanticTokensEdit { + start: 0, + delete_count: 1, + data: None, + }, + ); + } + + #[test] + fn test_semantic_tokens_edit_support_serialization() { + test_serialization( + &SemanticTokensEdit { + start: 0, + delete_count: 1, + data: Some(vec![ + SemanticToken { + delta_line: 2, + delta_start: 5, + length: 3, + token_type: 0, + token_modifiers_bitset: 3, + }, + SemanticToken { + delta_line: 0, + delta_start: 5, + length: 4, + token_type: 1, + token_modifiers_bitset: 0, + }, + ]), + }, + r#"{"start":0,"deleteCount":1,"data":[2,5,3,0,3,0,5,4,1,0]}"#, + ); + + test_serialization( + &SemanticTokensEdit { + start: 0, + delete_count: 1, + data: None, + }, + r#"{"start":0,"deleteCount":1}"#, + ); + } +} diff --git a/helix-lsp-types/src/signature_help.rs b/helix-lsp-types/src/signature_help.rs new file mode 100644 index 000000000..2466d1088 --- /dev/null +++ b/helix-lsp-types/src/signature_help.rs @@ -0,0 +1,207 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + Documentation, MarkupKind, TextDocumentPositionParams, TextDocumentRegistrationOptions, + WorkDoneProgressOptions, WorkDoneProgressParams, +}; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureInformationSettings { + /// Client supports the follow content formats for the documentation + /// property. The order describes the preferred format of the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub documentation_format: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_information: Option, + + /// The client support the `activeParameter` property on `SignatureInformation` + /// literal. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub active_parameter_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ParameterInformationSettings { + /// The client supports processing label offsets instead of a + /// simple label string. + /// + /// @since 3.14.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub label_offset_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpClientCapabilities { + /// Whether completion supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// The client supports the following `SignatureInformation` + /// specific properties. + #[serde(skip_serializing_if = "Option::is_none")] + pub signature_information: Option, + + /// The client supports to send additional context information for a + /// `textDocument/signatureHelp` request. A client that opts into + /// contextSupport will also support the `retriggerCharacters` on + /// `SignatureHelpOptions`. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub context_support: Option, +} + +/// Signature help options. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpOptions { + /// The characters that trigger signature help automatically. + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_characters: Option>, + + /// List of characters that re-trigger signature help. + /// These trigger characters are only active when signature help is already showing. All trigger characters + /// are also counted as re-trigger characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub retrigger_characters: Option>, + + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +/// Signature help options. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct SignatureHelpRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, +} + +/// Signature help options. +#[derive(Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct SignatureHelpTriggerKind(i32); +lsp_enum! { +impl SignatureHelpTriggerKind { + /// Signature help was invoked manually by the user or by a command. + pub const INVOKED: SignatureHelpTriggerKind = SignatureHelpTriggerKind(1); + /// Signature help was triggered by a trigger character. + pub const TRIGGER_CHARACTER: SignatureHelpTriggerKind = SignatureHelpTriggerKind(2); + /// Signature help was triggered by the cursor moving or by the document content changing. + pub const CONTENT_CHANGE: SignatureHelpTriggerKind = SignatureHelpTriggerKind(3); +} +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpParams { + /// The signature help context. This is only available if the client specifies + /// to send this using the client capability `textDocument.signatureHelp.contextSupport === true` + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpContext { + /// Action that caused signature help to be triggered. + pub trigger_kind: SignatureHelpTriggerKind, + + /// Character that caused signature help to be triggered. + /// This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_character: Option, + + /// `true` if signature help was already showing when it was triggered. + /// Retriggers occur when the signature help is already active and can be caused by actions such as + /// typing a trigger character, a cursor move, or document content changes. + pub is_retrigger: bool, + + /// The currently active `SignatureHelp`. + /// The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on + /// the user navigating through available signatures. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_signature_help: Option, +} + +/// Signature help represents the signature of something +/// callable. There can be multiple signature but only one +/// active and only one active parameter. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelp { + /// One or more signatures. + pub signatures: Vec, + + /// The active signature. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_signature: Option, + + /// The active parameter of the active signature. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_parameter: Option, +} + +/// Represents the signature of something callable. A signature +/// can have a label, like a function-name, a doc-comment, and +/// a set of parameters. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureInformation { + /// The label of this signature. Will be shown in + /// the UI. + pub label: String, + + /// The human-readable doc-comment of this signature. Will be shown + /// in the UI but can be omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub documentation: Option, + + /// The parameters of this signature. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option>, + + /// The index of the active parameter. + /// + /// If provided, this is used in place of `SignatureHelp.activeParameter`. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub active_parameter: Option, +} + +/// Represents a parameter of a callable-signature. A parameter can +/// have a label and a doc-comment. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ParameterInformation { + /// The label of this parameter information. + /// + /// Either a string or an inclusive start and exclusive end offsets within its containing + /// signature label. (see SignatureInformation.label). *Note*: A label of type string must be + /// a substring of its containing signature label. + pub label: ParameterLabel, + + /// The human-readable doc-comment of this parameter. Will be shown + /// in the UI but can be omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub documentation: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ParameterLabel { + Simple(String), + LabelOffsets([u32; 2]), +} diff --git a/helix-lsp-types/src/trace.rs b/helix-lsp-types/src/trace.rs new file mode 100644 index 000000000..7abcc5673 --- /dev/null +++ b/helix-lsp-types/src/trace.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct SetTraceParams { + /// The new value that should be assigned to the trace setting. + pub value: TraceValue, +} + +/// A TraceValue represents the level of verbosity with which the server systematically +/// reports its execution trace using `LogTrace` notifications. +/// +/// The initial trace value is set by the client at initialization and can be modified +/// later using the `SetTrace` notification. +#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub enum TraceValue { + /// The server should not send any `$/logTrace` notification + #[default] + Off, + /// The server should not add the 'verbose' field in the `LogTraceParams` + Messages, + Verbose, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogTraceParams { + /// The message to be logged. + pub message: String, + /// Additional information that can be computed if the `trace` configuration + /// is set to `'verbose'` + #[serde(skip_serializing_if = "Option::is_none")] + pub verbose: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::test_serialization; + + #[test] + fn test_set_trace_params() { + test_serialization( + &SetTraceParams { + value: TraceValue::Off, + }, + r#"{"value":"off"}"#, + ); + } + + #[test] + fn test_log_trace_params() { + test_serialization( + &LogTraceParams { + message: "message".into(), + verbose: None, + }, + r#"{"message":"message"}"#, + ); + + test_serialization( + &LogTraceParams { + message: "message".into(), + verbose: Some("verbose".into()), + }, + r#"{"message":"message","verbose":"verbose"}"#, + ); + } + + #[test] + fn test_trace_value() { + test_serialization( + &vec![TraceValue::Off, TraceValue::Messages, TraceValue::Verbose], + r#"["off","messages","verbose"]"#, + ); + } +} diff --git a/helix-lsp-types/src/type_hierarchy.rs b/helix-lsp-types/src/type_hierarchy.rs new file mode 100644 index 000000000..568a03e25 --- /dev/null +++ b/helix-lsp-types/src/type_hierarchy.rs @@ -0,0 +1,90 @@ +use crate::{ + DynamicRegistrationClientCapabilities, LSPAny, PartialResultParams, Range, + StaticRegistrationOptions, SymbolKind, SymbolTag, TextDocumentPositionParams, + TextDocumentRegistrationOptions, Url, WorkDoneProgressOptions, WorkDoneProgressParams, +}; + +use serde::{Deserialize, Serialize}; + +pub type TypeHierarchyClientCapabilities = DynamicRegistrationClientCapabilities; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct TypeHierarchyOptions { + #[serde(flatten)] + pub work_done_progress_options: WorkDoneProgressOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct TypeHierarchyRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + #[serde(flatten)] + pub type_hierarchy_options: TypeHierarchyOptions, + #[serde(flatten)] + pub static_registration_options: StaticRegistrationOptions, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct TypeHierarchyPrepareParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct TypeHierarchySupertypesParams { + pub item: TypeHierarchyItem, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct TypeHierarchySubtypesParams { + pub item: TypeHierarchyItem, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TypeHierarchyItem { + /// The name of this item. + pub name: String, + + /// The kind of this item. + pub kind: SymbolKind, + + /// Tags for this item. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, + + /// More detail for this item, e.g. the signature of a function. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + + /// The resource identifier of this item. + pub uri: Url, + + /// The range enclosing this symbol not including leading/trailing whitespace + /// but everything else, e.g. comments and code. + pub range: Range, + + /// The range that should be selected and revealed when this symbol is being + /// picked, e.g. the name of a function. Must be contained by the + /// [`range`](#TypeHierarchyItem.range). + pub selection_range: Range, + + /// A data entry field that is preserved between a type hierarchy prepare and + /// supertypes or subtypes requests. It could also be used to identify the + /// type hierarchy in the server, helping improve the performance on + /// resolving supertypes and subtypes. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} diff --git a/helix-lsp-types/src/window.rs b/helix-lsp-types/src/window.rs new file mode 100644 index 000000000..ac45e6083 --- /dev/null +++ b/helix-lsp-types/src/window.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use serde_json::Value; + +use url::Url; + +use crate::Range; + +#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] +#[serde(transparent)] +pub struct MessageType(i32); +lsp_enum! { +impl MessageType { + /// An error message. + pub const ERROR: MessageType = MessageType(1); + /// A warning message. + pub const WARNING: MessageType = MessageType(2); + /// An information message; + pub const INFO: MessageType = MessageType(3); + /// A log message. + pub const LOG: MessageType = MessageType(4); +} +} + +/// Window specific client capabilities. +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowClientCapabilities { + /// Whether client supports handling progress notifications. If set + /// servers are allowed to report in `workDoneProgress` property in the + /// request specific server capabilities. + /// + /// @since 3.15.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub work_done_progress: Option, + + /// Capabilities specific to the showMessage request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub show_message: Option, + + /// Client capabilities for the show document request. + /// + /// @since 3.16.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub show_document: Option, +} + +/// Show message request client capabilities +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ShowMessageRequestClientCapabilities { + /// Capabilities specific to the `MessageActionItem` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub message_action_item: Option, +} + +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageActionItemCapabilities { + /// Whether the client supports additional attributes which + /// are preserved and send back to the server in the + /// request's response. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties_support: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageActionItem { + /// A short title like 'Retry', 'Open Log' etc. + pub title: String, + + /// Additional attributes that the client preserves and + /// sends back to the server. This depends on the client + /// capability window.messageActionItem.additionalPropertiesSupport + #[serde(flatten)] + pub properties: HashMap, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum MessageActionItemProperty { + String(String), + Boolean(bool), + Integer(i32), + Object(Value), +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct LogMessageParams { + /// The message type. See {@link MessageType} + #[serde(rename = "type")] + pub typ: MessageType, + + /// The actual message + pub message: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct ShowMessageParams { + /// The message type. See {@link MessageType}. + #[serde(rename = "type")] + pub typ: MessageType, + + /// The actual message. + pub message: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct ShowMessageRequestParams { + /// The message type. See {@link MessageType} + #[serde(rename = "type")] + pub typ: MessageType, + + /// The actual message + pub message: String, + + /// The message action items to present. + #[serde(skip_serializing_if = "Option::is_none")] + pub actions: Option>, +} + +/// Client capabilities for the show document request. +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ShowDocumentClientCapabilities { + /// The client has support for the show document request. + pub support: bool, +} + +/// Params to show a document. +/// +/// @since 3.16.0 +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ShowDocumentParams { + /// The document uri to show. + pub uri: Url, + + /// Indicates to show the resource in an external program. + /// To show for example `https://code.visualstudio.com/` + /// in the default WEB browser set `external` to `true`. + #[serde(skip_serializing_if = "Option::is_none")] + pub external: Option, + + /// An optional property to indicate whether the editor + /// showing the document should take focus or not. + /// Clients might ignore this property if an external + /// program in started. + #[serde(skip_serializing_if = "Option::is_none")] + pub take_focus: Option, + + /// An optional selection range if the document is a text + /// document. Clients might ignore the property if an + /// external program is started or the file is not a text + /// file. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection: Option, +} + +/// The result of an show document request. +/// +/// @since 3.16.0 +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ShowDocumentResult { + /// A boolean indicating if the show was successful. + pub success: bool, +} diff --git a/helix-lsp-types/src/workspace_diagnostic.rs b/helix-lsp-types/src/workspace_diagnostic.rs new file mode 100644 index 000000000..e8a7646b0 --- /dev/null +++ b/helix-lsp-types/src/workspace_diagnostic.rs @@ -0,0 +1,149 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{ + FullDocumentDiagnosticReport, PartialResultParams, UnchangedDocumentDiagnosticReport, + WorkDoneProgressParams, +}; + +/// Workspace client capabilities specific to diagnostic pull requests. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticWorkspaceClientCapabilities { + /// Whether the client implementation supports a refresh request sent from + /// the server to the client. + /// + /// Note that this event is global and will force the client to refresh all + /// pulled diagnostics currently shown. It should be used with absolute care + /// and is useful for situation where a server for example detects a project + /// wide change that requires such a calculation. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_support: Option, +} + +/// A previous result ID in a workspace pull request. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct PreviousResultId { + /// The URI for which the client knows a result ID. + pub uri: Url, + + /// The value of the previous result ID. + pub value: String, +} + +/// Parameters of the workspace diagnostic request. +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceDiagnosticParams { + /// The additional identifier provided during registration. + pub identifier: Option, + + /// The currently known diagnostic reports with their + /// previous result ids. + pub previous_result_ids: Vec, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + #[serde(flatten)] + pub partial_result_params: PartialResultParams, +} + +/// A full document diagnostic report for a workspace diagnostic result. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFullDocumentDiagnosticReport { + /// The URI for which diagnostic information is reported. + pub uri: Url, + + /// The version number for which the diagnostics are reported. + /// + /// If the document is not marked as open, `None` can be provided. + pub version: Option, + + #[serde(flatten)] + pub full_document_diagnostic_report: FullDocumentDiagnosticReport, +} + +/// An unchanged document diagnostic report for a workspace diagnostic result. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceUnchangedDocumentDiagnosticReport { + /// The URI for which diagnostic information is reported. + pub uri: Url, + + /// The version number for which the diagnostics are reported. + /// + /// If the document is not marked as open, `None` can be provided. + pub version: Option, + + #[serde(flatten)] + pub unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport, +} + +/// A workspace diagnostic document report. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum WorkspaceDocumentDiagnosticReport { + Full(WorkspaceFullDocumentDiagnosticReport), + Unchanged(WorkspaceUnchangedDocumentDiagnosticReport), +} + +impl From for WorkspaceDocumentDiagnosticReport { + fn from(from: WorkspaceFullDocumentDiagnosticReport) -> Self { + WorkspaceDocumentDiagnosticReport::Full(from) + } +} + +impl From for WorkspaceDocumentDiagnosticReport { + fn from(from: WorkspaceUnchangedDocumentDiagnosticReport) -> Self { + WorkspaceDocumentDiagnosticReport::Unchanged(from) + } +} + +/// A workspace diagnostic report. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +pub struct WorkspaceDiagnosticReport { + pub items: Vec, +} + +/// A partial result for a workspace diagnostic report. +/// +/// @since 3.17.0 +#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)] +pub struct WorkspaceDiagnosticReportPartialResult { + pub items: Vec, +} + +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum WorkspaceDiagnosticReportResult { + Report(WorkspaceDiagnosticReport), + Partial(WorkspaceDiagnosticReportPartialResult), +} + +impl From for WorkspaceDiagnosticReportResult { + fn from(from: WorkspaceDiagnosticReport) -> Self { + WorkspaceDiagnosticReportResult::Report(from) + } +} + +impl From for WorkspaceDiagnosticReportResult { + fn from(from: WorkspaceDiagnosticReportPartialResult) -> Self { + WorkspaceDiagnosticReportResult::Partial(from) + } +} diff --git a/helix-lsp-types/src/workspace_folders.rs b/helix-lsp-types/src/workspace_folders.rs new file mode 100644 index 000000000..aeca89ffe --- /dev/null +++ b/helix-lsp-types/src/workspace_folders.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::OneOf; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFoldersServerCapabilities { + /// The server has support for workspace folders + #[serde(skip_serializing_if = "Option::is_none")] + pub supported: Option, + + /// Whether the server wants to receive workspace folder + /// change notifications. + /// + /// If a string is provided, the string is treated as an ID + /// under which the notification is registered on the client + /// side. The ID can be used to unregister for these events + /// using the `client/unregisterCapability` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub change_notifications: Option>, +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFolder { + /// The associated URI for this workspace folder. + pub uri: Url, + /// The name of the workspace folder. Defaults to the uri's basename. + pub name: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidChangeWorkspaceFoldersParams { + /// The actual workspace folder change event. + pub event: WorkspaceFoldersChangeEvent, +} + +/// The workspace folder change event. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFoldersChangeEvent { + /// The array of added workspace folders + pub added: Vec, + + /// The array of the removed workspace folders + pub removed: Vec, +} diff --git a/helix-lsp-types/src/workspace_symbols.rs b/helix-lsp-types/src/workspace_symbols.rs new file mode 100644 index 000000000..9ba80895b --- /dev/null +++ b/helix-lsp-types/src/workspace_symbols.rs @@ -0,0 +1,105 @@ +use crate::{ + LSPAny, Location, OneOf, PartialResultParams, SymbolInformation, SymbolKind, + SymbolKindCapability, SymbolTag, TagSupport, Url, WorkDoneProgressParams, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceSymbolClientCapabilities { + /// This capability supports dynamic registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_registration: Option, + + /// Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_kind: Option, + + /// The client supports tags on `SymbolInformation`. + /// Clients supporting tags have to handle unknown tags gracefully. + /// + /// @since 3.16.0 + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "TagSupport::deserialize_compat" + )] + pub tag_support: Option>, + + /// The client support partial workspace symbols. The client will send the + /// request `workspaceSymbol/resolve` to the server to resolve additional + /// properties. + /// + /// @since 3.17.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve_support: Option, +} + +/// The parameters of a Workspace Symbol Request. +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct WorkspaceSymbolParams { + #[serde(flatten)] + pub partial_result_params: PartialResultParams, + + #[serde(flatten)] + pub work_done_progress_params: WorkDoneProgressParams, + + /// A non-empty query string + pub query: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct WorkspaceSymbolResolveSupportCapability { + /// The properties that a client can resolve lazily. Usually + /// `location.range` + pub properties: Vec, +} + +/// A special workspace symbol that supports locations without a range +/// +/// @since 3.17.0 +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceSymbol { + /// The name of this symbol. + pub name: String, + + /// The kind of this symbol. + pub kind: SymbolKind, + + /// Tags for this completion item. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + + /// The name of the symbol containing this symbol. This information is for + /// user interface purposes (e.g. to render a qualifier in the user interface + /// if necessary). It can't be used to re-infer a hierarchy for the document + /// symbols. + #[serde(skip_serializing_if = "Option::is_none")] + pub container_name: Option, + + /// The location of this symbol. Whether a server is allowed to + /// return a location without a range depends on the client + /// capability `workspace.symbol.resolveSupport`. + /// + /// See also `SymbolInformation.location`. + pub location: OneOf, + + /// A data entry field that is preserved on a workspace symbol between a + /// workspace symbol request and a workspace symbol resolve request. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct WorkspaceLocation { + pub uri: Url, +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum WorkspaceSymbolResponse { + Flat(Vec), + Nested(Vec), +} From 1ccdc55db92f584bf613b70171902998d2cdd2e1 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 27 Jul 2024 12:14:41 -0400 Subject: [PATCH 180/200] Add helix-lsp-types to workspace --- Cargo.toml | 1 + docs/architecture.md | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7f784428..763992480 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "helix-view", "helix-term", "helix-tui", + "helix-lsp-types", "helix-lsp", "helix-event", "helix-dap", diff --git a/docs/architecture.md b/docs/architecture.md index 5d33cbac0..0dd68ed38 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,13 +1,14 @@ -| Crate | Description | -| ----------- | ----------- | -| helix-core | Core editing primitives, functional. | -| helix-lsp | Language server client | -| helix-dap | Debug Adapter Protocol (DAP) client | -| helix-loader | Functions for building, fetching, and loading external resources | -| helix-view | UI abstractions for use in backends, imperative shell. | -| helix-term | Terminal UI | -| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive | +| Crate | Description | +| ----------- | ----------- | +| helix-core | Core editing primitives, functional. | +| helix-lsp | Language server client | +| helix-lsp-types | Language Server Protocol type definitions | +| helix-dap | Debug Adapter Protocol (DAP) client | +| helix-loader | Functions for building, fetching, and loading external resources | +| helix-view | UI abstractions for use in backends, imperative shell. | +| helix-term | Terminal UI | +| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive | This document contains a high-level overview of Helix internals. From 7793031aa6b42dd1753c65896ceb6bc140fe44d3 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 27 Jul 2024 12:15:37 -0400 Subject: [PATCH 181/200] Rename `lsp-types` crate to `helix-lsp-types` --- Cargo.lock | 11 +++++++++++ helix-lsp-types/Cargo.toml | 10 ++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c54e481e0..38cd0e1cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1406,6 +1406,17 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "helix-lsp-types" +version = "0.95.1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "helix-parsec" version = "24.7.0" diff --git a/helix-lsp-types/Cargo.toml b/helix-lsp-types/Cargo.toml index ad7a3ca05..18cc8b148 100644 --- a/helix-lsp-types/Cargo.toml +++ b/helix-lsp-types/Cargo.toml @@ -1,7 +1,13 @@ [package] -name = "lsp-types" +name = "helix-lsp-types" version = "0.95.1" -authors = ["Markus Westerlind ", "Bruno Medeiros "] +authors = [ + # Original authors + "Markus Westerlind ", + "Bruno Medeiros ", + # Since forking + "Helix contributors" +] edition = "2018" description = "Types for interaction with a language server, using VSCode's Language Server Protocol" From e21e4eb825e631ff426ac2d857cf8bff2233ef1f Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 27 Jul 2024 12:19:57 -0400 Subject: [PATCH 182/200] Replace lsp-types in helix-lsp with helix-lsp-types --- Cargo.lock | 15 +-------------- helix-lsp/Cargo.toml | 2 +- helix-lsp/src/client.rs | 13 ++++++------- helix-lsp/src/lib.rs | 4 ++-- helix-lsp/src/transport.rs | 18 ++++++++++-------- 5 files changed, 20 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38cd0e1cf..faa464bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1393,10 +1393,10 @@ dependencies = [ "globset", "helix-core", "helix-loader", + "helix-lsp-types", "helix-parsec", "helix-stdx", "log", - "lsp-types", "parking_lot", "serde", "serde_json", @@ -1732,19 +1732,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "lsp-types" -version = "0.95.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" -dependencies = [ - "bitflags 1.3.2", - "serde", - "serde_json", - "serde_repr", - "url", -] - [[package]] name = "memchr" version = "2.6.3" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index ab9251ebe..f4e7c794b 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -17,13 +17,13 @@ helix-stdx = { path = "../helix-stdx" } helix-core = { path = "../helix-core" } helix-loader = { path = "../helix-loader" } helix-parsec = { path = "../helix-parsec" } +helix-lsp-types = { path = "../helix-lsp-types" } anyhow = "1.0" futures-executor = "0.3" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } globset = "0.4.14" log = "0.4" -lsp-types = { version = "0.95" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.38", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 643aa9a26..cc1c4ce8f 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -5,15 +5,14 @@ Call, Error, LanguageServerId, OffsetEncoding, Result, }; -use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope}; -use helix_loader::VERSION_AND_GIT_HASH; -use helix_stdx::path; -use lsp::{ - notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport, +use crate::lsp::{ + self, notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport, DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url, WorkspaceFolder, WorkspaceFoldersChangeEvent, }; -use lsp_types as lsp; +use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope}; +use helix_loader::VERSION_AND_GIT_HASH; +use helix_stdx::path; use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; @@ -994,7 +993,7 @@ pub fn text_document_did_save( .. }) => match options.as_ref()? { lsp::TextDocumentSyncSaveOptions::Supported(true) => false, - lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp_types::SaveOptions { + lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { include_text, }) => include_text.unwrap_or(false), lsp::TextDocumentSyncSaveOptions::Supported(false) => return None, diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 8e423e1c3..993a712d9 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -8,9 +8,9 @@ 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::{ @@ -1113,7 +1113,7 @@ macro_rules! test_case { #[test] fn emoji_format_gh_4791() { - use lsp_types::{Position, Range, TextEdit}; + use lsp::{Position, Range, TextEdit}; let edits = vec![ TextEdit { diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index bd671abe1..1bded598d 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,4 +1,8 @@ -use crate::{jsonrpc, Error, LanguageServerId, Result}; +use crate::{ + jsonrpc, + lsp::{self, notification::Notification as _}, + Error, LanguageServerId, Result, +}; use anyhow::Context; use log::{error, info}; use serde::{Deserialize, Serialize}; @@ -289,11 +293,10 @@ async fn recv( } // Hack: inject a terminated notification so we trigger code that needs to happen after exit - use lsp_types::notification::Notification as _; let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification { jsonrpc: None, - method: lsp_types::notification::Exit::METHOD.to_string(), + method: lsp::notification::Exit::METHOD.to_string(), params: jsonrpc::Params::None, })); match transport @@ -338,8 +341,8 @@ async fn send( // Determine if a message is allowed to be sent early fn is_initialize(payload: &Payload) -> bool { - use lsp_types::{ - notification::{Initialized, Notification}, + use lsp::{ + notification::Initialized, request::{Initialize, Request}, }; match payload { @@ -357,7 +360,7 @@ fn is_initialize(payload: &Payload) -> bool { } fn is_shutdown(payload: &Payload) -> bool { - use lsp_types::request::{Request, Shutdown}; + use lsp::request::{Request, Shutdown}; matches!(payload, Payload::Request { value: jsonrpc::MethodCall { method, .. }, .. } if method == Shutdown::METHOD) } @@ -370,12 +373,11 @@ fn is_shutdown(payload: &Payload) -> bool { // server successfully initialized is_pending = false; - use lsp_types::notification::Notification; // Hack: inject an initialized notification so we trigger code that needs to happen after init let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification { jsonrpc: None, - method: lsp_types::notification::Initialized::METHOD.to_string(), + method: lsp::notification::Initialized::METHOD.to_string(), params: jsonrpc::Params::None, })); let language_server_name = &transport.name; From 3963969b89d80f0235b566441dcfec7d3797e276 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 28 Jul 2024 09:41:07 -0400 Subject: [PATCH 183/200] 'cargo fmt' --- helix-lsp-types/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helix-lsp-types/src/lib.rs b/helix-lsp-types/src/lib.rs index e38fae205..68d58704e 100644 --- a/helix-lsp-types/src/lib.rs +++ b/helix-lsp-types/src/lib.rs @@ -238,7 +238,9 @@ pub struct CancelParams { /// Position in a text document expressed as zero-based line and character offset. /// A position is between two characters like an 'insert' cursor in a editor. -#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Deserialize, Serialize, Hash)] +#[derive( + Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Deserialize, Serialize, Hash, +)] pub struct Position { /// Line position in a document (zero-based). pub line: u32, From 981e5cd737515153d4e5320cd835c14dba532b33 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 28 Jul 2024 09:52:17 -0400 Subject: [PATCH 184/200] helix-lsp-types: Resolve clippy lints in tests --- helix-lsp-types/src/completion.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/helix-lsp-types/src/completion.rs b/helix-lsp-types/src/completion.rs index bdf60fb51..f6bf3c15d 100644 --- a/helix-lsp-types/src/completion.rs +++ b/helix-lsp-types/src/completion.rs @@ -578,20 +578,26 @@ mod tests { #[test] fn test_tag_support_deserialization() { - let mut empty = CompletionItemCapability::default(); - empty.tag_support = None; + let empty = CompletionItemCapability { + tag_support: None, + ..CompletionItemCapability::default() + }; test_deserialization(r#"{}"#, &empty); test_deserialization(r#"{"tagSupport": false}"#, &empty); - let mut t = CompletionItemCapability::default(); - t.tag_support = Some(TagSupport { value_set: vec![] }); + let t = CompletionItemCapability { + tag_support: Some(TagSupport { value_set: vec![] }), + ..CompletionItemCapability::default() + }; test_deserialization(r#"{"tagSupport": true}"#, &t); - let mut t = CompletionItemCapability::default(); - t.tag_support = Some(TagSupport { - value_set: vec![CompletionItemTag::DEPRECATED], - }); + let t = CompletionItemCapability { + tag_support: Some(TagSupport { + value_set: vec![CompletionItemTag::DEPRECATED], + }), + ..CompletionItemCapability::default() + }; test_deserialization(r#"{"tagSupport": {"valueSet": [1]}}"#, &t); } From af2ac551baf441b7ce02c9738f54414aabd9544f Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 28 Jul 2024 10:02:29 -0400 Subject: [PATCH 185/200] Resolve unclosed HTML tag doc warning --- helix-lsp-types/src/completion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp-types/src/completion.rs b/helix-lsp-types/src/completion.rs index f6bf3c15d..2555228a7 100644 --- a/helix-lsp-types/src/completion.rs +++ b/helix-lsp-types/src/completion.rs @@ -159,7 +159,7 @@ impl InsertTextMode { /// they match the indentation up to the cursor of the line for /// which the item is accepted. /// - /// Consider a line like this: <2tabs><3tabs>foo. Accepting a + /// Consider a line like this: `<2tabs><3tabs>foo`. Accepting a /// multi line completion item is indented using 2 tabs all /// following lines inserted will be indented using 2 tabs as well. pub const ADJUST_INDENTATION: InsertTextMode = InsertTextMode(2); From 9e55e8a4162a8571adc9f14db5ffd3a363410912 Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sun, 28 Jul 2024 16:52:20 +0200 Subject: [PATCH 186/200] contrib: nushell: also complete available languages with --health (#11346) --- contrib/completion/hx.nu | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contrib/completion/hx.nu b/contrib/completion/hx.nu index b7bb542c0..c93d0b52a 100644 --- a/contrib/completion/hx.nu +++ b/contrib/completion/hx.nu @@ -4,7 +4,11 @@ # so it has not been specified here and will not be proposed in the autocompletion of Nushell. # The help message won't be overriden though, so it will still be present here -def health_categories [] { ["all", "clipboard", "languages"] } +def health_categories [] { + let languages = ^hx --health languages | detect columns | get Language | filter { $in != null } + let completions = [ "all", "clipboard", "languages" ] | append $languages + return $completions +} def grammar_categories [] { ["fetch", "build"] } @@ -12,7 +16,7 @@ def grammar_categories [] { ["fetch", "build"] } export extern hx [ --help(-h), # Prints help information --tutor, # Loads the tutorial - --health: string@health_categories = "all", # Checks for potential errors in editor setup + --health: string@health_categories, # Checks for potential errors in editor setup --grammar(-g): string@grammar_categories, # Fetches or builds tree-sitter grammars listed in `languages.toml` --config(-c): glob, # Specifies a file to use for configuration -v, # Increases logging verbosity each use for up to 3 times From fade4b218cf107fa90fd40614eefee8079f40745 Mon Sep 17 00:00:00 2001 From: Michael Jones Date: Sun, 28 Jul 2024 17:23:46 +0200 Subject: [PATCH 187/200] new theme named ao (#11063) * new theme named ao * Update runtime/themes/ao.toml Co-authored-by: Michael Davis --------- Co-authored-by: Michael Davis --- runtime/themes/ao.toml | 165 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 runtime/themes/ao.toml diff --git a/runtime/themes/ao.toml b/runtime/themes/ao.toml new file mode 100644 index 000000000..7313d9fa5 --- /dev/null +++ b/runtime/themes/ao.toml @@ -0,0 +1,165 @@ +# Theme: Ao +# Author: YardQuit + +# SYNTAX HIGHLIGHTING +"attribute" = { fg = "yellow" } +"type" = { fg = "white" } +"type.builtin" = { fg = "white" } +"type.parameter" = { fg = "white" } +"type.enum" = { fg = "white" } +"type.enum.variant" = { fg = "white" } +"constructor" = { fg = "orange" } +"constant" = { fg = "blue" } +"constant.character.escape" = { fg = "yellow" } +"string" = { fg = "blue" } +"string.regexp" = { fg = "yellow" } +"comment" = { fg = "gray" } +"variable" = { fg = "orange" } +"variable.parameter" = { fg = "yellow" } +"variable.other" = { fg = "green" } +"variable.other.member" = { fg = "green" } +"label" = { fg = "blue" } +"punctuation" = { fg = "white" } +"punctuation.bracket" = { fg = "orange" } +"punctuation.special" = { fg = "yellow" } +"keyword" = { fg = "red" } +"keyword.operator" = { fg = "blue" } +"keyword.directive" = { fg = "white" } +"keyword.function" = { fg = "red" } +"keyword.storage" = { fg = "red" } +"keyword.storage.modifier" = { fg = "green" } +"operator" = { fg = "white" } +"function" = { fg = "purple" } +"function.method" = { fg = "green" } +"function.macro" = { fg = "green" } +"function.special" = { fg = "yellow" } +"tag" = { fg = "green" } +"namespace" = { fg = "white" } +"diff" = { fg = "white" } +"diff.minus" = { fg = "red" } +"diff.delta" = { fg = "brown" } + +# MARKUP, SYNAX HIGHLIGHTING AND INTERFACE HYBRID +"markup.heading" = { fg = "blaze_orange" } +"markup.heading.1" = { fg = "crystal_blue", modifiers = ["bold"] } +"markup.heading.2" = { fg = "sky_blue", modifiers = ["bold"] } +"markup.heading.3" = { fg = "dreamy_blue", modifiers = ["bold"] } +"markup.heading.4" = { fg = "crystal_blue" } +"markup.heading.5" = { fg = "sky_blue" } +"markup.heading.6" = { fg = "dreamy_blue" } +"markup.list" = { fg = "blaze_orange" } +"markup.bold" = { fg = "crystal_blue", modifiers = ["bold"] } +"markup.italic" = { fg = "crystal_blue", modifiers = ["italic"] } +"markup.strikethrough" = { fg = "crystal_blue", modifiers = ["crossed_out"] } +"markup.link" = { fg = "crystal_blue", underline = { color = "light_purple", style = "line" } } +"markup.link.url" = { fg = "slate_purple", underline = { color = "slate_purple", style = "line" } } +"markup.link.label" = { fg = "crystal_blue" } +"markup.link.text" = { fg = "crystal_blue", modifiers = ["bold"] } +"markup.quote" = { fg = "winter_sky", modifiers = ["italic"] } +"markup.raw" = { fg = "winter_sky" } +"markup.raw.block" = { fg = "white" } + +# USER INTERFACE +"ui.background" = { bg = "deep_abyss"} # workspace background +"ui.background.separator" = { fg = "winter_sky" } # picker separator below input line (space + j) +"ui.gutter" = { bg = "deep_abyss" } # gutter +"ui.gutter.selected" = { bg = "pitch_black" } # gutter for the line the cursor is on +"ui.linenr" = { fg = "slate_gray" } # line numbers +"ui.linenr.selected" = { fg = "blaze_orange", modifiers = ["bold"] } # line number for the line the cursor is on +"ui.statusline" = { fg = "winter_sky", bg = "twilight_blue" } # statusline, fucused +"ui.statusline.inactive" = { fg = "slate_gray", bg = "pitch_black" } # statusline, unfocused +"ui.statusline.normal" = { fg = "deep_abyss", bg = "leafy_green", modifiers = ["bold"] } # statusline normal mode (if editor.color-modes is enabled) +"ui.statusline.insert" = { fg = "deep_abyss", bg = "blaze_orange", modifiers = ["bold"] } # statusline insert mode (if editor.color-modes is enabled) +"ui.statusline.select" = { fg = "deep_abyss", bg = "sky_blue", modifiers = ["bold"] } # statusline select mode (if editor.color-modes is enabled) +"ui.statusline.separator" = { fg = "winter_sky" } # separator character is statusline +"ui.bufferline" = { fg = "slate_gray", modifiers = ["bold"] } # bufferline inactive tab +"ui.bufferline.active" = { fg = "winter_sky", bg = "twilight_blue" } # bufferline active tab +"ui.bufferline.background" = { bg = "pitch_black" } # bufferline background +"ui.virtual.ruler" = { bg = "stormy_night" } # ruler columns +"ui.virtual.whitespace" = { fg = "stormy_night" } # whitespace characters +"ui.virtual.indent-guide" = { fg = "stormy_night" } # vertical indent width guides +"ui.virtual.inlay-hint" = { fg = "slate_gray" } # inlay hints of all kinds +"ui.virtual.inlay-hint.parameter" = { fg = "slate_gray" } # inlay hints of kind parameter (lsps are not required to set a kind) +"ui.virtual.inlay-hint.type" = { fg = "slate_gray" } # inlay hints of kind type (lsps are not required to set a kind) +"ui.virtual.wrap" = { fg = "slate_gray" } # soft-wrap indicator +"ui.virtual.jump-label" = { modifiers = ["reversed"] } # virtual jump labels (g + w) +"ui.selection" = { bg = "deep_purple" } # slave selections in the editing area +"ui.selection.primary" = { bg = "light_purple" } # primary selection in the editing area +"ui.cursor" = { modifiers = ["reversed"] } # only if "ui.cursor.primary.normal" isn't set +"ui.cursor.normal" = { fg = "winter_sky", bg = "twilight_blue" } # slave cursor block in normal mode +"ui.cursor.insert" = { bg = "rustic_red" } # slave cursor block in insert mode +"ui.cursor.select" = { bg = "deep_purple" } # slave cursor block in select mode +"ui.cursor.match" = { fg = "deep_abyss", bg = "blaze_orange", modifiers = ["bold"] } # matching bracket etc +"ui.cursor.primary" = { modifiers = ["reversed"] } # cursor with primary selection (has no effect due to "ui.cursor.primary.normal" is set) +"ui.cursor.primary.normal" = { fg = "deep_abyss", bg = "sky_blue" } # cursor block in normal mode +"ui.cursor.primary.insert" = { fg = "deep_abyss", bg = "ruby_glow" } # cursor block in insert mode +"ui.cursor.primary.select" = { fg = "winter_sky", bg = "deep_purple" } # cursor block in select mode (not the selected color) +"ui.cursorline.primary" = { bg = "nightfall_blue" } # line of the primary cursor +"ui.cursorline.secondary" = { bg = "midnight_thunder"} # lines of secondary cursors +"ui.cursorcolumn.primary" = { bg = "nightfall_blue" } # column of the primary cursor +"ui.cursorcolumn.secondary" = { bg = "midnight_thunder" } # columns of secondary cursors + +# USER INTERFACE - MENUS AND POPUP +"ui.popup" = { fg = "winter_sky", bg = "midnight_thunder" } # documentation popups (space + k) +"ui.popup.info" = { fg = "winter_sky", bg = "nightfall_blue" } # prompt for multiple key options, menu border (space, g, z, m, etc) +"ui.window" = { fg = "slate_gray" } # borderlines separating splits +"ui.help" = { fg = "winter_sky", bg = "nightfall_blue" } # description box for commands +"ui.text" = { fg = "white" } # default text style, command prompts, popup text, etc +"ui.text.focus" = { fg = "dreamy_blue" } # the currently selected line in the picker (space j, space f, space s, etc) +"ui.text.inactive" = { fg = "slate_gray" } # same as ui.text but when the text is inactive e.g. suggestions +"ui.text.info" = { fg = "winter_sky", bg = "nightfall_blue" } # the key: command in ui.popup.info boxes (space, g, z, m, etc) +"ui.menu" = { fg = "winter_sky", bg = "midnight_thunder" } # code and command completion menus ":" +"ui.menu.selected" = { fg = "winter_sky", bg = "twilight_blue" } # selected autocomplete item +"ui.menu.scroll" = { fg = "crystal_blue", bg = "moonlight_ocean" } # scrollbar +"ui.highlight" = { underline = { color = "sky_blue", style = "line" } } # highlighted lines in the picker preview + +# USER INTERFACE - DIAGNOSTICS +"warning" = { fg = "lemon_zest" } # diagnostics warning (gutter) +"error" = { fg = "ruby_glow" } # diagnostics error (gutter) +"info" = { fg = "sky_blue" } # diagnostics info (gutter) +"hint" = { fg = "walnut_brown" } # diagnostics hint (gutter) +"diagnostic" = { modifiers = ["reversed"] } # diagnostics fallback style (editing area) +"diagnostic.hint" = { fg = "deep_abyss", bg = "walnut_brown" } # diagnostics hint (editing area) +"diagnostic.info" = { fg = "winter_sky", bg = "twilight_blue" } # diagnostics info (editing area) +"diagnostic.warning" = { fg = "winter_sky", bg = "rustic_amber" } # diagnostics warning (editing area) +"diagnostic.error" = { fg = "winter_sky", bg = "rustic_red" } # diagnostics error (editing area) + +# COLOR NAMES +[palette] +# PALETTE USER INTERFACE +deep_abyss = "#080d15" +stormy_night = "#254862" +nightfall_blue = "#1f2937" +midnight_thunder = "#0d1526" +twilight_blue = "#2c5484" +pitch_black = "#000000" +winter_sky = "#f3f4f6" +slate_gray = "#838a97" +blaze_orange = "#ff9000" +lemon_zest = "#ffba00" +leafy_green = "#81be83" +dreamy_blue = "#6eb0ff" +crystal_blue = "#99c7ff" +sky_blue = "#45b1e8" +moonlight_ocean = "#0c1420" +rustic_red = "#540b0c" +ruby_glow = "#fa7970" +walnut_brown = "#987654" +rustic_amber = "#9d5800" +slate_purple = "#d2a8ff" +light_purple = "#7533bd" +deep_purple = "#4c1785" + +# SYNTAX HIGHLIGHTING +black = "#0d1117" +red = "#fa7970" +green = "#81be83" +yellow = "#ffba00" +orange = "#ff9000" +blue = "#45b1e8" +purple = "#d2a8ff" +brown = "#987654" +gray = "#838a97" +white = "#dadada" + + From f5950196d9ab73f338bb617dd3041d4bbc833e1c Mon Sep 17 00:00:00 2001 From: dnaq Date: Sun, 28 Jul 2024 18:29:26 +0200 Subject: [PATCH 188/200] Fix panic when starting helix tutor (#11352) Closes #11351 Also fixed some minor issues related to log message contents, and removed unnecessary use of `.as_mut()` as per code review comments on the PR. --- helix-view/src/document.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 885f33e8a..d470826e7 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1080,22 +1080,25 @@ pub fn detect_indent_and_line_ending(&mut self) { } pub fn pickup_last_saved_time(&mut self) { - self.last_saved_time = match self.path.as_mut().unwrap().metadata() { - Ok(metadata) => match metadata.modified() { - Ok(mtime) => mtime, - Err(_) => { + self.last_saved_time = match self.path().as_mut() { + Some(path) => match path.metadata() { + Ok(metadata) => match metadata.modified() { + Ok(mtime) => mtime, + Err(_) => { + log::error!( + "Use a system time instead of fs' mtime not supported on this platform" + ); + SystemTime::now() + } + }, + Err(e) => { log::error!( - "Use a system time instead of fs' mtime not supported on this platform" + "Use a system time instead of fs' mtime: failed to file's metadata: {e}" ); SystemTime::now() } }, - Err(e) => { - log::error!( - "Use a system time instead of fs' mtime: failed to file's metadata: {e}" - ); - SystemTime::now() - } + None => SystemTime::now(), }; } From 29439116b8d43c7d8b5d8cdec20801e9d54c011e Mon Sep 17 00:00:00 2001 From: lynn pepin <19627675+lynnpepin@users.noreply.github.com> Date: Sun, 28 Jul 2024 12:40:50 -0400 Subject: [PATCH 189/200] Documented ulimit fix for error during integration tests (#11356) --- docs/CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2be8f77c7..86caff717 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -53,6 +53,10 @@ ## Integration tests [helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL` environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`. +Contributors using MacOS might encounter `Too many open files (os error 24)` +failures while running integration tests. This can be resolved by increasing +the default value (e.g. to `10240` from `256`) by running `ulimit -n 10240`. + ## Minimum Stable Rust Version (MSRV) Policy Helix follows the MSRV of Firefox. From 2a2bc793356dff37fcf9463029bbe7e0d508a5ed Mon Sep 17 00:00:00 2001 From: dnaq Date: Sun, 28 Jul 2024 19:30:07 +0200 Subject: [PATCH 190/200] Remove unnecessary `.as_mut()` call and fix log messages (#11358) These are changes that fell out of commit: d7a3cdea65ef321d53b8dc8417175781b5272049 --- helix-view/src/document.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index d470826e7..088a88dd3 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1080,20 +1080,20 @@ pub fn detect_indent_and_line_ending(&mut self) { } pub fn pickup_last_saved_time(&mut self) { - self.last_saved_time = match self.path().as_mut() { + self.last_saved_time = match self.path() { Some(path) => match path.metadata() { Ok(metadata) => match metadata.modified() { Ok(mtime) => mtime, - Err(_) => { + Err(e) => { log::error!( - "Use a system time instead of fs' mtime not supported on this platform" + "Using system time instead of fs' mtime: not supported on this platform: {e}" ); SystemTime::now() } }, Err(e) => { log::error!( - "Use a system time instead of fs' mtime: failed to file's metadata: {e}" + "Using system time instead of fs' mtime: failed to read file's metadata: {e}" ); SystemTime::now() } From fa13b2bd0dd7af04c05edfc0470340b9345aadb8 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 28 Jul 2024 21:59:21 +0100 Subject: [PATCH 191/200] reduce log noise on file writes (#11361) --- helix-view/src/document.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 088a88dd3..6dcd2b17f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1084,17 +1084,13 @@ pub fn pickup_last_saved_time(&mut self) { Some(path) => match path.metadata() { Ok(metadata) => match metadata.modified() { Ok(mtime) => mtime, - Err(e) => { - log::error!( - "Using system time instead of fs' mtime: not supported on this platform: {e}" - ); + Err(err) => { + log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err); SystemTime::now() } }, - Err(e) => { - log::error!( - "Using system time instead of fs' mtime: failed to read file's metadata: {e}" - ); + Err(err) => { + log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err); SystemTime::now() } }, From 8e041c99df8aff8e894263e738faecae41b4cf9b Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 28 Jul 2024 23:22:28 +0200 Subject: [PATCH 192/200] stable sort lsp edits (#11357) --- helix-lsp/src/lib.rs | 2 +- helix-term/src/commands/lsp.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 8e423e1c3..81e4f6207 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -503,7 +503,7 @@ pub fn generate_transaction_from_edits( ) -> Transaction { // Sort edits by start range, since some LSPs (Omnisharp) send them // in reverse order. - edits.sort_unstable_by_key(|edit| edit.range.start); + edits.sort_by_key(|edit| edit.range.start); // Generate a diff if the edit is a full document replacement. #[allow(clippy::collapsible_if)] diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 9194377c4..4fffbb24d 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1360,7 +1360,7 @@ fn compute_inlay_hints_for_view( // Most language servers will already send them sorted but ensure this is the case to // avoid errors on our end. - hints.sort_unstable_by_key(|inlay_hint| inlay_hint.position); + hints.sort_by_key(|inlay_hint| inlay_hint.position); let mut padding_before_inlay_hints = Vec::new(); let mut type_inlay_hints = Vec::new(); From 08ac37d295d91ff5ccb73ac065e0736d1ae0f664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BA=A6=E8=8A=BD=E7=B3=96?= Date: Mon, 29 Jul 2024 22:52:15 +0800 Subject: [PATCH 193/200] Add theme keys for the picker header area (#11343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: pertty header * 更新 themes.md Co-authored-by: Michael Davis --------- Co-authored-by: Michael Davis --- book/src/themes.md | 7 ++++--- helix-term/src/ui/picker.rs | 28 ++++++++++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index f9f8393c3..1bc2627dd 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -293,12 +293,13 @@ #### Interface | `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) | | `ui.statusline.separator` | Separator character in statusline | | `ui.bufferline` | Style for the buffer line | -| `ui.bufferline.active` | Style for the active buffer in buffer line | +| `ui.bufferline.active` | Style for the active buffer in buffer line | | `ui.bufferline.background` | Style for bufferline background | | `ui.popup` | Documentation popups (e.g. Space + k) | | `ui.popup.info` | Prompt for multiple key options | -| `ui.picker.header` | Column names in pickers with multiple columns | -| `ui.picker.header.active` | The column name in pickers with multiple columns where the cursor is entering into. | +| `ui.picker.header` | Header row area in pickers with multiple columns | +| `ui.picker.header.column` | Column names in pickers with multiple columns | +| `ui.picker.header.column.active` | The column name in pickers with multiple columns where the cursor is entering into. | | `ui.window` | Borderlines separating splits | | `ui.help` | Description box for commands | | `ui.text` | Default text style, command prompts, popup text, etc. | diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 118dafa77..82fe96891 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -799,21 +799,25 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) if self.columns.len() > 1 { let active_column = self.query.active_column(self.prompt.position()); let header_style = cx.editor.theme.get("ui.picker.header"); + let header_column_style = cx.editor.theme.get("ui.picker.header.column"); - table = table.header(Row::new(self.columns.iter().map(|column| { - if column.hidden { - Cell::default() - } else { - let style = if active_column.is_some_and(|name| Arc::ptr_eq(name, &column.name)) - { - cx.editor.theme.get("ui.picker.header.active") + table = table.header( + Row::new(self.columns.iter().map(|column| { + if column.hidden { + Cell::default() } else { - header_style - }; + let style = + if active_column.is_some_and(|name| Arc::ptr_eq(name, &column.name)) { + cx.editor.theme.get("ui.picker.header.column.active") + } else { + header_column_style + }; - Cell::from(Span::styled(Cow::from(&*column.name), style)) - } - }))); + Cell::from(Span::styled(Cow::from(&*column.name), style)) + } + })) + .style(header_style), + ); } use tui::widgets::TableState; From ce809fb9ef4c5af59b5401419a2e8ae6964c8229 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:10:47 -0500 Subject: [PATCH 194/200] build(deps): bump the rust-dependencies group with 6 updates (#11371) --- Cargo.lock | 193 +++++++++++++++++++++++------------------- helix-core/Cargo.toml | 2 +- helix-lsp/Cargo.toml | 2 +- helix-vcs/Cargo.toml | 4 +- 4 files changed, 110 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c54e481e0..945b1ba06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" @@ -272,7 +272,7 @@ dependencies = [ "filedescriptor", "futures-core", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -541,9 +541,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gix" -version = "0.63.0" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984c5018adfa7a4536ade67990b3ebc6e11ab57b3d6cd9968de0947ca99b4b06" +checksum = "d78414d29fcc82329080166077e0f7689f4016551fdb334d787c3d040fe2634f" dependencies = [ "gix-actor", "gix-attributes", @@ -584,16 +584,15 @@ dependencies = [ "gix-validate", "gix-worktree", "once_cell", - "parking_lot", "smallvec", "thiserror", ] [[package]] name = "gix-actor" -version = "0.31.2" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69c59d392c7e6c94385b6fd6089d6df0fe945f32b4357687989f3aee253cd7f" +checksum = "a0e454357e34b833cc3a00b6efbbd3dd4d18b24b9fb0c023876ec2645e8aa3f2" dependencies = [ "bstr", "gix-date", @@ -640,9 +639,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c22e086314095c43ffe5cdc5c0922d5439da4fd726f3b0438c56147c34dc225" +checksum = "0d76867867da891cbe32021ad454e8cae90242f6afb06762e4dd0d357afd1d7b" dependencies = [ "bstr", "gix-path", @@ -652,9 +651,9 @@ dependencies = [ [[package]] name = "gix-commitgraph" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b102311085da4af18823413b5176d7c500fb2272eaf391cfa8635d8bcb12c4" +checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78" dependencies = [ "bstr", "gix-chunk", @@ -666,9 +665,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fafe42957e11d98e354a66b6bd70aeea00faf2f62dd11164188224a507c840" +checksum = "28f53fd03d1bf09ebcc2c8654f08969439c4556e644ca925f27cf033bc43e658" dependencies = [ "bstr", "gix-config-value", @@ -687,9 +686,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd06203b1a9b33a78c88252a625031b094d9e1b647260070c25b09910c0a804" +checksum = "b328997d74dd15dc71b2773b162cb4af9a25c424105e4876e6d0686ab41c383e" dependencies = [ "bitflags 2.6.0", "bstr", @@ -700,9 +699,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99" +checksum = "9eed6931f21491ee0aeb922751bd7ec97b4b2fe8fbfedcb678e2a2dce5f3b8c0" dependencies = [ "bstr", "itoa", @@ -712,9 +711,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.44.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9bd8b2d07b6675a840b56a6c177d322d45fa082672b0dad8f063b25baf0a4" +checksum = "1996d5c8a305b59709467d80617c9fde48d9d75fd1f4179ea970912630886c9d" dependencies = [ "bstr", "gix-command", @@ -732,9 +731,9 @@ dependencies = [ [[package]] name = "gix-dir" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60c99f8c545abd63abe541d20ab6cda347de406c0a3f1c80aadc12d9b0e94974" +checksum = "0c975679aa00dd2d757bfd3ddb232e8a188c0094c3306400575a0813858b1365" dependencies = [ "bstr", "gix-discover", @@ -752,9 +751,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf" +checksum = "67662731cec3cb31ba3ed2463809493f76d8e5d6c6d245de8b0560438c13450e" dependencies = [ "bstr", "dunce", @@ -787,9 +786,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00ce6ea5ac8fca7adbc63c48a1b9e0492c222c386aa15f513405f1003f2f4ab2" +checksum = "e6547738da28275f4dff4e9f3a0f28509f53f94dd6bd822733c91cb306bca61a" dependencies = [ "bstr", "encoding_rs", @@ -808,9 +807,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.11.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f78f7d6dcda7a5809efd73a33b145e3dce7421c460df21f32126f9732736b0c" +checksum = "6adf99c27cdf17b1c4d77680c917e0d94d8783d4e1c73d3be0d1d63107163d7a" dependencies = [ "fastrand", "gix-features", @@ -852,9 +851,9 @@ dependencies = [ [[package]] name = "gix-ignore" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640dbeb4f5829f9fc14d31f654a34a0350e43a24e32d551ad130d99bf01f63f1" +checksum = "5e6afb8f98e314d4e1adc822449389ada863c174b5707cedd327d67b84dba527" dependencies = [ "bstr", "gix-glob", @@ -865,9 +864,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.33.0" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8c5a5f1c58edcbc5692b174cda2703aba82ed17d7176ff4c1752eb48b1b167" +checksum = "9a9a44eb55bd84bb48f8a44980e951968ced21e171b22d115d1cdcef82a7d73f" dependencies = [ "bitflags 2.6.0", "bstr", @@ -915,9 +914,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.42.2" +version = "0.42.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe2dc4a41191c680c942e6ebd630c8107005983c4679214fdb1007dcf5ae1df" +checksum = "25da2f46b4e7c2fa7b413ce4dffb87f69eaf89c2057e386491f4c55cadbfe386" dependencies = [ "bstr", "gix-actor", @@ -934,9 +933,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e92b9790e2c919166865d0825b26cc440a387c175bed1b43a2fa99c0e9d45e98" +checksum = "20d384fe541d93d8a3bb7d5d5ef210780d6df4f50c4e684ccba32665a5e3bc9b" dependencies = [ "arc-swap", "gix-date", @@ -954,9 +953,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.51.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8da51212dbff944713edb2141ed7e002eea326b8992070374ce13a6cb610b3" +checksum = "3e0594491fffe55df94ba1c111a6566b7f56b3f8d2e1efc750e77d572f5f5229" dependencies = [ "clru", "gix-chunk", @@ -965,9 +964,7 @@ dependencies = [ "gix-hashtable", "gix-object", "gix-path", - "gix-tempfile", "memmap2", - "parking_lot", "smallvec", "thiserror", ] @@ -999,9 +996,9 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76cab098dc10ba2d89f634f66bf196dea4d7db4bf10b75c7a9c201c55a2ee19" +checksum = "d307d1b8f84dc8386c4aa20ce0cf09242033840e15469a3ecba92f10cfb5c046" dependencies = [ "bitflags 2.6.0", "bstr", @@ -1025,12 +1022,11 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b36752b448647acd59c9668fdd830b16d07db1e6d9c3b3af105c1605a6e23d9" +checksum = "636e96a0a5562715153fee098c217110c33a6f8218f08f4687ff99afde159bb5" dependencies = [ "gix-actor", - "gix-date", "gix-features", "gix-fs", "gix-hash", @@ -1047,9 +1043,9 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde848865834a54fe4d9b4573f15d0e9a68eaf3d061b42d3ed52b4b8acf880b2" +checksum = "6868f8cd2e62555d1f7c78b784bece43ace40dd2a462daf3b588d5416e603f37" dependencies = [ "bstr", "gix-hash", @@ -1061,25 +1057,23 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e08f8107ed1f93a83bcfbb4c38084c7cb3f6cd849793f1d5eec235f9b13b2b" +checksum = "01b13e43c2118c4b0537ddac7d0821ae0dfa90b7b8dbf20c711e153fb749adce" dependencies = [ "bstr", "gix-date", "gix-hash", - "gix-hashtable", "gix-object", "gix-revwalk", - "gix-trace", "thiserror", ] [[package]] name = "gix-revwalk" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4181db9cfcd6d1d0fd258e91569dbb61f94cb788b441b5294dd7f1167a3e788f" +checksum = "1b030ccaab71af141f537e0225f19b9e74f25fefdba0372246b844491cab43e0" dependencies = [ "gix-commitgraph", "gix-date", @@ -1092,9 +1086,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fddc27984a643b20dd03e97790555804f98cf07404e0e552c0ad8133266a79a1" +checksum = "1547d26fa5693a7f34f05b4a3b59a90890972922172653bcb891ab3f09f436df" dependencies = [ "bitflags 2.6.0", "gix-path", @@ -1104,9 +1098,9 @@ dependencies = [ [[package]] name = "gix-status" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4373d989713809554d136f51bc7da565adf45c91aa4d86ef6a79801621bfc8" +checksum = "83f7b084cb65c3d007ce6bb479755ca13d602ca3cd91c4f08d7e59904de33736" dependencies = [ "bstr", "filetime", @@ -1121,14 +1115,15 @@ dependencies = [ "gix-path", "gix-pathspec", "gix-worktree", + "portable-atomic", "thiserror", ] [[package]] name = "gix-submodule" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921cd49924ac14b6611b22e5fb7bbba74d8780dc7ad26153304b64d1272460ac" +checksum = "0f2e0f69aa00805e39d39ec80472a7e9da20ed5d73318b27925a2cc198e854fd" dependencies = [ "bstr", "gix-config", @@ -1161,9 +1156,9 @@ checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e" [[package]] name = "gix-traverse" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f20cb69b63eb3e4827939f42c05b7756e3488ef49c25c412a876691d568ee2a0" +checksum = "e499a18c511e71cf4a20413b743b9f5bcf64b3d9e81e9c3c6cd399eae55a8840" dependencies = [ "bitflags 2.6.0", "gix-commitgraph", @@ -1178,9 +1173,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.27.3" +version = "0.27.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0db829ebdca6180fbe32be7aed393591df6db4a72dbbc0b8369162390954d1cf" +checksum = "e2eb9b35bba92ea8f0b5ab406fad3cf6b87f7929aa677ff10aa042c6da621156" dependencies = [ "bstr", "gix-features", @@ -1213,9 +1208,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f6b7de83839274022aff92157d7505f23debf739d257984a300a35972ca94e" +checksum = "26f7326ebe0b9172220694ea69d344c536009a9b98fb0f9de092c440f3efe7a6" dependencies = [ "bstr", "gix-attributes", @@ -1546,6 +1541,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "home" version = "0.5.9" @@ -1607,9 +1608,9 @@ dependencies = [ [[package]] name = "imara-diff" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af13c8ceb376860ff0c6a66d83a8cdd4ecd9e464da24621bbffcd02b49619434" +checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01" dependencies = [ "ahash", "hashbrown 0.14.5", @@ -1687,7 +1688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.0", + "windows-targets 0.52.0", ] [[package]] @@ -1779,6 +1780,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "nucleo" version = "0.5.0" @@ -1821,7 +1834,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -1907,6 +1920,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2135,11 +2154,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2157,9 +2177,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -2193,7 +2213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -2423,28 +2443,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.1", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -2464,9 +2483,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" dependencies = [ "serde", "serde_spanned", @@ -2476,18 +2495,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap", "serde", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 6608b69c8..90eb747a9 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -46,7 +46,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" -imara-diff = "0.1.6" +imara-diff = "0.1.7" encoding_rs = "0.8" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index ab9251ebe..6a53913aa 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -26,7 +26,7 @@ log = "0.4" lsp-types = { version = "0.95" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.38", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.15" parking_lot = "0.12.3" arc-swap = "1" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index bc8c5b7ba..53d560c9c 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -19,8 +19,8 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p parking_lot = "0.12" arc-swap = { version = "1.7.1" } -gix = { version = "0.63.0", features = ["attributes", "status"], default-features = false, optional = true } -imara-diff = "0.1.6" +gix = { version = "0.64.0", features = ["attributes", "status"], default-features = false, optional = true } +imara-diff = "0.1.7" anyhow = "1" log = "0.4" From 22c1a4072596df6858baddcad63a281b2e82bd40 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 30 Jul 2024 15:29:31 -0500 Subject: [PATCH 195/200] Fix finding injection layer in tree cursor with nested layers (#11365) The `take_while` should limit the layers to those that can match the input range so we don't always scan the entire `injection_layers`. We can limit `depth == 1` layers to those that start before the search `end`. Deeper layers overlap with shallower layers though so we need to allow those layers as well in the `take_while`. For example ```vue ``` L2 and L3 are a typescript layer and the `/foo/` part is a small regex layer. If you used `A-o` before the regex layer you would select the entire typescript layer. The search in `layer_id_containing_byte_range` would not consider the typescript layer since the regex layer comes earlier in `injection_ranges` and that layer's start is after `end`. The regex layer has a depth of `2` though so the change in this commit allows scanning through that layer. Co-authored-by: Pascal Kuthe --- helix-core/src/syntax/tree_cursor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-core/src/syntax/tree_cursor.rs b/helix-core/src/syntax/tree_cursor.rs index 692d5890a..bec4a1c6c 100644 --- a/helix-core/src/syntax/tree_cursor.rs +++ b/helix-core/src/syntax/tree_cursor.rs @@ -204,7 +204,7 @@ fn layer_id_containing_byte_range(&self, start: usize, end: usize) -> LayerId { self.injection_ranges[start_idx..] .iter() - .take_while(|range| range.start < end) + .take_while(|range| range.start < end || range.depth > 1) .find_map(|range| (range.start <= start).then_some(range.layer_id)) .unwrap_or(self.root) } From a4cfcff28414641bdb7871e4f2c2d879b6720205 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 30 Jul 2024 16:52:36 -0400 Subject: [PATCH 196/200] update language configuration for Tcl (#11236) The primary executable that comes with Tcl is `tclsh`. Not really sure what `tclish` is, as I initially thought it was a typo. However, there seems to be references to it based on a quick search (e.g. [here](https://wiki.tcl-lang.org/page/Tclish) and [here](https://tclish.sourceforge.net/)), so maybe it's a valid executable that I just haven't been aware of. I was hesitant to replace it and instead opted to just add `tclsh`. --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index d51a18216..0d268eecd 100644 --- a/languages.toml +++ b/languages.toml @@ -3490,7 +3490,7 @@ name = "tcl" scope = "source.tcl" injection-regex = "tcl" file-types = [ "tcl" ] -shebangs = [ "tclish", "jimsh", "wish" ] +shebangs = [ "tclsh", "tclish", "jimsh", "wish" ] comment-token = '#' [[grammar]] From b19551b11bc00ecd7386f6d9c420f373bfe3f377 Mon Sep 17 00:00:00 2001 From: Erasin Wang Date: Wed, 31 Jul 2024 04:52:47 +0800 Subject: [PATCH 197/200] Updated Godot support (#11235) - update gdscript highlights - add godot-resource textobjects --- book/src/generated/lang-support.md | 2 +- languages.toml | 6 ++-- runtime/queries/gdscript/highlights.scm | 3 ++ runtime/queries/gdscript/indents.scm | 1 + runtime/queries/gdscript/textobjects.scm | 9 +++++ runtime/queries/godot-resource/injections.scm | 35 ++++++++++++++++++- .../queries/godot-resource/textobjects.scm | 23 ++++++++++++ 7 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 runtime/queries/godot-resource/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 686dec5e2..9c88b7a20 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -69,7 +69,7 @@ | glsl | ✓ | ✓ | ✓ | | | gn | ✓ | | | | | go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` | -| godot-resource | ✓ | | | | +| godot-resource | ✓ | ✓ | | | | gomod | ✓ | | | `gopls` | | gotmpl | ✓ | | | `gopls` | | gowork | ✓ | | | `gopls` | diff --git a/languages.toml b/languages.toml index 0d268eecd..2bd57abb3 100644 --- a/languages.toml +++ b/languages.toml @@ -1995,12 +1995,12 @@ shebangs = [] roots = ["project.godot"] auto-format = true formatter = { command = "gdformat", args = ["-"] } -comment-token = "#" +comment-tokens = ["#", "##"] indent = { tab-width = 4, unit = "\t" } [[grammar]] name = "gdscript" -source = { git = "https://github.com/PrestonKnopp/tree-sitter-gdscript", rev = "a4b57cc3bcbfc24550e858159647e9238e7ad1ac" } +source = { git = "https://github.com/PrestonKnopp/tree-sitter-gdscript", rev = "1f1e782fe2600f50ae57b53876505b8282388d77" } [[language]] name = "godot-resource" @@ -2015,7 +2015,7 @@ indent = { tab-width = 4, unit = "\t" } [[grammar]] name = "godot-resource" -source = { git = "https://github.com/PrestonKnopp/tree-sitter-godot-resource", rev = "b6ef0768711086a86b3297056f9ffb5cc1d77b4a" } +source = { git = "https://github.com/PrestonKnopp/tree-sitter-godot-resource", rev = "2ffb90de47417018651fc3b970e5f6b67214dc9d" } [[language]] name = "nu" diff --git a/runtime/queries/gdscript/highlights.scm b/runtime/queries/gdscript/highlights.scm index 37aa3d62f..fee67dc9c 100644 --- a/runtime/queries/gdscript/highlights.scm +++ b/runtime/queries/gdscript/highlights.scm @@ -14,6 +14,7 @@ (attribute_call (identifier) @function) (base_call (identifier) @function) (call (identifier) @function) +(lambda (name) @function) ; Function definitions @@ -88,6 +89,7 @@ "<<" ">>" ":=" + ":" ] @operator (annotation (identifier) @keyword.storage.modifier) @@ -97,6 +99,7 @@ "else" "elif" "match" + "when" ] @keyword.control.conditional [ diff --git a/runtime/queries/gdscript/indents.scm b/runtime/queries/gdscript/indents.scm index c969eb7cf..3d49aec35 100644 --- a/runtime/queries/gdscript/indents.scm +++ b/runtime/queries/gdscript/indents.scm @@ -6,6 +6,7 @@ (pattern_section) (function_definition) + (lambda) (constructor_definition) (class_definition) (enum_definition) diff --git a/runtime/queries/gdscript/textobjects.scm b/runtime/queries/gdscript/textobjects.scm index 47512bba7..3a2775fe2 100644 --- a/runtime/queries/gdscript/textobjects.scm +++ b/runtime/queries/gdscript/textobjects.scm @@ -5,6 +5,8 @@ (function_definition (body) @function.inside) @function.around +(lambda (body) @function.inside) @function.around + (parameters [ (identifier) @@ -15,5 +17,12 @@ (arguments (_expression) @parameter.inside @parameter.around) +[ + (const_statement) + (variable_statement) + (pair) + (enumerator) +] @entry.around + (comment) @comment.inside (comment)+ @comment.around diff --git a/runtime/queries/godot-resource/injections.scm b/runtime/queries/godot-resource/injections.scm index 7929d63cd..6e199f104 100644 --- a/runtime/queries/godot-resource/injections.scm +++ b/runtime/queries/godot-resource/injections.scm @@ -11,8 +11,41 @@ (property (path) @_is_code (string) @injection.content)) - (#match? @_type "type") + (#eq? @_type "type") (#match? @_is_shader "Shader") (#eq? @_is_code "code") (#set! injection.language "glsl") ) + +((section + (identifier) @_is_resource + (property + (path) @_is_code + (string) @injection.content)) + (#eq? @_is_resource "resource") + (#eq? @_is_code "code") + (#set! injection.language "glsl") +) + +((section + (identifier) @_id + (property + (path) @_is_expression + (string) @injection.content)) + (#eq? @_id "sub_resource") + (#eq? @_is_expression "expression") + (#set! injection.language "glsl") +) + +((section + (attribute + (identifier) @_type + (string) @_is_shader) + (property + (path) @_is_code + (string) @injection.content)) + (#eq? @_type "type") + (#match? @_is_shader "GDScript") + (#eq? @_is_code "script/source") + (#set! injection.language "gdscript") +) diff --git a/runtime/queries/godot-resource/textobjects.scm b/runtime/queries/godot-resource/textobjects.scm new file mode 100644 index 000000000..aeb4e3eb3 --- /dev/null +++ b/runtime/queries/godot-resource/textobjects.scm @@ -0,0 +1,23 @@ +(section + (identifier) + (_) + (property) @class.inside +) @class.around + +(attribute + (identifier) + (_) @parameter.inside) @parameter.around + +(property + (path) + (_) @entry.inside) @entry.around + +(pair + (_) @entry.inside) @entry.around + +(array + (_) @entry.around) + +(comment) @comment.inside + +(comment)+ @comment.around From 86aecc96a147094b84eb88968870663a6fb4cb95 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Wed, 31 Jul 2024 14:39:46 -0700 Subject: [PATCH 198/200] chore: clean up clippy lints (#11377) Using clippy 1.80.0. Also cleans up some that were windows only. --- helix-core/src/auto_pairs.rs | 4 ++-- helix-core/src/indent.rs | 9 +++++---- helix-core/src/syntax.rs | 9 ++++++--- helix-stdx/src/faccess.rs | 6 +++--- helix-term/build.rs | 14 ++++++-------- helix-term/src/application.rs | 4 +++- helix-term/src/commands/lsp.rs | 14 +++++++------- helix-term/src/commands/typed.rs | 2 +- helix-term/src/main.rs | 5 ++--- helix-tui/src/text.rs | 2 +- helix-view/src/document.rs | 2 +- helix-view/src/handlers/dap.rs | 2 +- helix-view/src/handlers/lsp.rs | 14 ++++++++------ 13 files changed, 46 insertions(+), 41 deletions(-) diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 31f9d3649..853290404 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -75,9 +75,9 @@ fn from((open, close): (&char, &char)) -> Self { impl AutoPairs { /// Make a new AutoPairs set with the given pairs and default conditions. - pub fn new<'a, V: 'a, A>(pairs: V) -> Self + pub fn new<'a, V, A>(pairs: V) -> Self where - V: IntoIterator, + V: IntoIterator + 'a, A: Into, { let mut auto_pairs = HashMap::new(); diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index fd2b6c959..ae26c13e0 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -265,7 +265,7 @@ fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option( // Skip matches where not all custom predicates are fulfilled if !query.general_predicates(m.pattern_index).iter().all(|pred| { match pred.operator.as_ref() { - "not-kind-eq?" => match (pred.args.get(0), pred.args.get(1)) { + "not-kind-eq?" => match (pred.args.first(), pred.args.get(1)) { ( Some(QueryPredicateArg::Capture(capture_idx)), Some(QueryPredicateArg::String(kind)), @@ -473,7 +473,7 @@ fn query_indents<'a>( } }, "same-line?" | "not-same-line?" => { - match (pred.args.get(0), pred.args.get(1)) { + match (pred.args.first(), pred.args.get(1)) { ( Some(QueryPredicateArg::Capture(capt1)), Some(QueryPredicateArg::Capture(capt2)) @@ -495,7 +495,7 @@ fn query_indents<'a>( } } } - "one-line?" | "not-one-line?" => match pred.args.get(0) { + "one-line?" | "not-one-line?" => match pred.args.first() { Some(QueryPredicateArg::Capture(capture_idx)) => { let node = m.nodes_for_capture_index(*capture_idx).next(); @@ -786,6 +786,7 @@ fn init_indent_query<'a, 'b>( /// - The line after the node. This is defined by: /// - The scope `tail`. /// - The scope `all` if this node is not the first node on its line. +/// /// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node. /// The indents from different nodes for the same line are then combined. /// The result [Indentation] is simply the sum of the [Indentation] for all lines. diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ab6bcc1fb..7be512f52 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1431,8 +1431,11 @@ pub fn highlight_iter<'a>( // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which // prevents them from being moved. But both of these values are really just // pointers, so it's actually ok to move them. - let cursor_ref = - unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; + let cursor_ref = unsafe { + mem::transmute::<&mut tree_sitter::QueryCursor, &mut tree_sitter::QueryCursor>( + &mut cursor, + ) + }; // if reusing cursors & no range this resets to whole range cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); @@ -1737,7 +1740,7 @@ fn traverse(point: Point, text: &Tendril) -> Point { } use std::sync::atomic::{AtomicUsize, Ordering}; -use std::{iter, mem, ops, str, usize}; +use std::{iter, mem, ops, str}; use tree_sitter::{ Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, QueryMatch, Range, TextProvider, Tree, diff --git a/helix-stdx/src/faccess.rs b/helix-stdx/src/faccess.rs index c69571b68..6be6bdd83 100644 --- a/helix-stdx/src/faccess.rs +++ b/helix-stdx/src/faccess.rs @@ -295,21 +295,21 @@ fn eaccess(p: &Path, mut mode: FILE_ACCESS_RIGHTS) -> io::Result<()> { let mut privileges_length = std::mem::size_of::() as u32; let mut result = 0; - let mut mapping = GENERIC_MAPPING { + let mapping = GENERIC_MAPPING { GenericRead: FILE_GENERIC_READ, GenericWrite: FILE_GENERIC_WRITE, GenericExecute: FILE_GENERIC_EXECUTE, GenericAll: FILE_ALL_ACCESS, }; - unsafe { MapGenericMask(&mut mode, &mut mapping) }; + unsafe { MapGenericMask(&mut mode, &mapping) }; if unsafe { AccessCheck( *sd.descriptor(), *token.as_handle(), mode, - &mut mapping, + &mapping, &mut privileges, &mut privileges_length, &mut granted_access, diff --git a/helix-term/build.rs b/helix-term/build.rs index 6bebf00c6..60a646590 100644 --- a/helix-term/build.rs +++ b/helix-term/build.rs @@ -66,18 +66,16 @@ fn find_rc_exe() -> io::Result { .output(); match find_reg_key { - Err(find_reg_key) => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Failed to run registry query: {}", find_reg_key), - )) - } + Err(find_reg_key) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to run registry query: {}", find_reg_key), + )), Ok(find_reg_key) => { if find_reg_key.status.code().unwrap() != 0 { - return Err(io::Error::new( + Err(io::Error::new( io::ErrorKind::Other, "Can not find Windows SDK", - )); + )) } else { let lines = String::from_utf8(find_reg_key.stdout) .expect("Should be able to parse the output"); diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 7da96b08a..bd6b5a870 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -35,7 +35,9 @@ use std::io::stdout; use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc}; -use anyhow::{Context, Error}; +#[cfg(not(windows))] +use anyhow::Context; +use anyhow::Error; use crossterm::{event::Event as CrosstermEvent, tty::IsTty}; #[cfg(not(windows))] diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 4fffbb24d..93ac2a849 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -34,7 +34,7 @@ use std::{ cmp::Ordering, collections::{BTreeMap, HashSet}, - fmt::Write, + fmt::{Display, Write}, future::Future, path::Path, }; @@ -832,13 +832,13 @@ pub enum ApplyEditErrorKind { // InvalidEdit, } -impl ToString for ApplyEditErrorKind { - fn to_string(&self) -> String { +impl Display for ApplyEditErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(), - ApplyEditErrorKind::FileNotFound => "file not found".to_string(), - ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(), - ApplyEditErrorKind::IoError(err) => err.to_string(), + ApplyEditErrorKind::DocumentChanged => f.write_str("document has changed"), + ApplyEditErrorKind::FileNotFound => f.write_str("file not found"), + ApplyEditErrorKind::UnknownURISchema => f.write_str("URI schema not supported"), + ApplyEditErrorKind::IoError(err) => f.write_str(&format!("{err}")), } } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 720d32ac8..d20bdc17d 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2498,7 +2498,7 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> ensure!(!args.is_empty(), "file name is expected"); ensure!(args.len() == 1, "only the file name is expected"); - let filename = args.get(0).unwrap(); + let filename = args.first().unwrap(); let path = PathBuf::from(filename.to_string()); ensure!( path.exists() && path.is_file(), diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index fbe1a8460..a3a27a076 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -117,10 +117,9 @@ async fn main_impl() -> Result { setup_logging(args.verbosity).context("failed to initialize logging")?; // Before setting the working directory, resolve all the paths in args.files - for (path, _) in args.files.iter_mut() { - *path = helix_stdx::path::canonicalize(&path); + for (path, _) in &mut args.files { + *path = helix_stdx::path::canonicalize(&*path); } - // NOTE: Set the working directory early so the correct configuration is loaded. Be aware that // Application::new() depends on this logic so it must be updated if this changes. if let Some(path) = &args.working_directory { diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 076766dd6..a5e8a68af 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -5,7 +5,7 @@ //! - A single line string where all graphemes have the same style is represented by a [`Span`]. //! - A single line string where each grapheme may have its own style is represented by [`Spans`]. //! - A multiple line string where each grapheme may have its own style is represented by a -//! [`Text`]. +//! [`Text`]. //! //! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`] //! is a [`Spans`]. diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 6dcd2b17f..15aa81dae 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1245,7 +1245,7 @@ pub fn reset_selection(&mut self, view_id: ViewId) { /// Initializes a new selection and view_data for the given view /// if it does not already have them. pub fn ensure_view_init(&mut self, view_id: ViewId) { - if self.selections.get(&view_id).is_none() { + if !self.selections.contains_key(&view_id) { self.reset_selection(view_id); } diff --git a/helix-view/src/handlers/dap.rs b/helix-view/src/handlers/dap.rs index a5fa0c29c..81766dd59 100644 --- a/helix-view/src/handlers/dap.rs +++ b/helix-view/src/handlers/dap.rs @@ -37,7 +37,7 @@ pub async fn select_thread_id(editor: &mut Editor, thread_id: ThreadId, force: b debugger.thread_id = Some(thread_id); fetch_stack_trace(debugger, thread_id).await; - let frame = debugger.stack_frames[&thread_id].get(0).cloned(); + let frame = debugger.stack_frames[&thread_id].first().cloned(); if let Some(frame) = &frame { jump_to_stack_frame(editor, frame); } diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index d817a4235..6aff2e50c 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use crate::editor::Action; use crate::Editor; use crate::{DocumentId, ViewId}; @@ -73,13 +75,13 @@ fn from(err: helix_core::uri::UrlConversionError) -> Self { } } -impl ToString for ApplyEditErrorKind { - fn to_string(&self) -> String { +impl Display for ApplyEditErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(), - ApplyEditErrorKind::FileNotFound => "file not found".to_string(), - ApplyEditErrorKind::InvalidUrl(err) => err.to_string(), - ApplyEditErrorKind::IoError(err) => err.to_string(), + ApplyEditErrorKind::DocumentChanged => f.write_str("document has changed"), + ApplyEditErrorKind::FileNotFound => f.write_str("file not found"), + ApplyEditErrorKind::InvalidUrl(err) => f.write_str(&format!("{err}")), + ApplyEditErrorKind::IoError(err) => f.write_str(&format!("{err}")), } } } From 63953e0b9eed6f05199e936ed16ba5d35b7f5430 Mon Sep 17 00:00:00 2001 From: TheoCorn <68430877+TheoCorn@users.noreply.github.com> Date: Wed, 31 Jul 2024 23:40:00 +0200 Subject: [PATCH 199/200] fix :move panic when starting a new language server (#11387) * fix move panic * change location of is initialized check --- helix-view/src/editor.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 4249db1d5..1708b3b4e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1376,6 +1376,11 @@ pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> } let is_dir = new_path.is_dir(); for ls in self.language_servers.iter_clients() { + // A new language server might have been started in `set_doc_path` and won't + // be initialized yet. Skip the `did_rename` notification for this server. + if !ls.is_initialized() { + continue; + } if let Some(notification) = ls.did_rename(old_path, &new_path, is_dir) { tokio::spawn(notification); }; From cfe80acb6f8893e20dc04ff9a99c4551c3129624 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Wed, 31 Jul 2024 16:29:14 -0700 Subject: [PATCH 200/200] output `stderr` in `:sh` popup if shell commands fail (#11239) * refactor(commands): output `stderr` in `:sh` popup * refactor(commands): switch to `from_utf8_lossy` This way something is always displayed. * refactor: no longer log stderr output on failure --- helix-term/src/commands.rs | 33 +++++++++++++++----------------- helix-term/src/commands/typed.rs | 2 +- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2c5d2783b..e9d3a28ff 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5716,27 +5716,24 @@ async fn shell_impl_async( process.wait_with_output().await? }; - if !output.status.success() { - if !output.stderr.is_empty() { - let err = String::from_utf8_lossy(&output.stderr).to_string(); - log::error!("Shell error: {}", err); - bail!("Shell error: {}", err); - } - match output.status.code() { - Some(exit_code) => bail!("Shell command failed: status {}", exit_code), - None => bail!("Shell command failed"), + let output = if !output.status.success() { + if output.stderr.is_empty() { + match output.status.code() { + Some(exit_code) => bail!("Shell command failed: status {}", exit_code), + None => bail!("Shell command failed"), + } } + String::from_utf8_lossy(&output.stderr) + // Prioritize `stderr` output over `stdout` } else if !output.stderr.is_empty() { - log::debug!( - "Command printed to stderr: {}", - String::from_utf8_lossy(&output.stderr).to_string() - ); - } + let stderr = String::from_utf8_lossy(&output.stderr); + log::debug!("Command printed to stderr: {stderr}"); + stderr + } else { + String::from_utf8_lossy(&output.stdout) + }; - let str = std::str::from_utf8(&output.stdout) - .map_err(|_| anyhow!("Process did not output valid UTF-8"))?; - let tendril = Tendril::from(str); - Ok(tendril) + Ok(Tendril::from(output)) } fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index d20bdc17d..cd40e0532 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2308,7 +2308,7 @@ fn run_shell_command( )); compositor.replace_or_push("shell", popup); } - editor.set_status("Command succeeded"); + editor.set_status("Command run"); }, )); Ok(call)