From f5f08becef0c27f183a7772f6ec6f5442eca2bab Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Mon, 22 Jan 2024 09:06:20 -0500 Subject: [PATCH 01/70] Fix typo in string representation of GotoReference (#9395) --- helix-core/src/syntax.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 83bd09b4d..e543df06e 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -263,7 +263,7 @@ impl Display for LanguageServerFeature { GotoDeclaration => "goto-declaration", GotoDefinition => "goto-definition", GotoTypeDefinition => "goto-type-definition", - GotoReference => "goto-type-definition", + GotoReference => "goto-reference", GotoImplementation => "goto-implementation", SignatureHelp => "signature-help", Hover => "hover", From 52a43bcdfcc258a2871be468a3e31496dd2d80c8 Mon Sep 17 00:00:00 2001 From: woojiq <122799969+woojiq@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:51:12 +0200 Subject: [PATCH 02/70] bash, make, css: highlight and indent queries improvement (#9393) * highlights(bash): rework keywords section * Use more specified scope when possible for keywords like @keyword.repeat. * Add more keywords like "local" or "unsetenv". Limitation: * Bash doesn't allow you to have a local variable outside of a function, so maybe we need to have better queries to not highlight the local in this case. * If we name a function with a keyword (such as unset or local), it will use the highlight scope "keyword" instead of "function". * indents(css, make): add basic queries * Despite the fact that queries look simple, they improve indentation in some edge cases that helix couldn't handle correctly by default. --- book/src/generated/lang-support.md | 4 ++-- runtime/queries/bash/highlights.scm | 34 ++++++++++++++++++++--------- runtime/queries/css/indents.scm | 7 ++++++ runtime/queries/make/indents.scm | 8 +++++++ 4 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 runtime/queries/css/indents.scm create mode 100644 runtime/queries/make/indents.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index ee01c4030..6fedace63 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -21,7 +21,7 @@ | cpon | ✓ | | ✓ | | | cpp | ✓ | ✓ | ✓ | `clangd` | | crystal | ✓ | ✓ | | `crystalline` | -| css | ✓ | | | `vscode-css-language-server` | +| css | ✓ | | ✓ | `vscode-css-language-server` | | cue | ✓ | | | `cuelsp` | | d | ✓ | ✓ | ✓ | `serve-d` | | dart | ✓ | | ✓ | `dart` | @@ -97,7 +97,7 @@ | log | ✓ | | | | | lpf | ✓ | | | | | lua | ✓ | ✓ | ✓ | `lua-language-server` | -| make | ✓ | | | | +| make | ✓ | | ✓ | | | markdoc | ✓ | | | `markdoc-ls` | | markdown | ✓ | | | `marksman` | | markdown.inline | ✓ | | | | diff --git a/runtime/queries/bash/highlights.scm b/runtime/queries/bash/highlights.scm index 3f2df6386..92d61e8b8 100644 --- a/runtime/queries/bash/highlights.scm +++ b/runtime/queries/bash/highlights.scm @@ -10,23 +10,37 @@ (variable_name) @variable.other.member [ + "if" + "then" + "else" + "elif" + "fi" "case" + "in" + "esac" +] @keyword.control.conditional + +[ + "for" "do" "done" - "elif" - "else" - "esac" + "select" + "until" + "while" +] @keyword.control.repeat + +[ + "declare" + "typeset" "export" - "fi" - "for" - "function" - "if" - "in" + "readonly" + "local" "unset" - "while" - "then" + "unsetenv" ] @keyword +"function" @keyword.function + (comment) @comment (function_definition name: (word) @function) diff --git a/runtime/queries/css/indents.scm b/runtime/queries/css/indents.scm new file mode 100644 index 000000000..1dfd977d9 --- /dev/null +++ b/runtime/queries/css/indents.scm @@ -0,0 +1,7 @@ +[ + (block) +] @indent + +[ + "}" +] @outdent diff --git a/runtime/queries/make/indents.scm b/runtime/queries/make/indents.scm new file mode 100644 index 000000000..42b2c60e6 --- /dev/null +++ b/runtime/queries/make/indents.scm @@ -0,0 +1,8 @@ +[ + (define_directive) + (rule) +] @indent + +[ + "endef" +] @outdent From 9ed3dc52e0d56f0f0fd721d7f10da5f935f56ef9 Mon Sep 17 00:00:00 2001 From: Jaakko Paju Date: Mon, 22 Jan 2024 20:51:56 +0200 Subject: [PATCH 03/70] Update Scala tree-sitter grammar (#9348) * Update Scala tree-sitter grammar * Support block comments Modify comment handling in textobjects and highlights to support new TS-scala node type 'block_comment' --- languages.toml | 2 +- runtime/queries/scala/highlights.scm | 2 +- runtime/queries/scala/textobjects.scm | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/languages.toml b/languages.toml index 1638cac49..a785828c1 100644 --- a/languages.toml +++ b/languages.toml @@ -1393,7 +1393,7 @@ language-servers = [ "metals" ] [[grammar]] name = "scala" -source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "23d21310fe4ab4b3273e7a6810e781224a3e7fe1" } +source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "7891815f42dca9ed6aeb464c2edc39d479ab965c" } [[language]] name = "dockerfile" diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm index 67603fdda..40b230ec4 100644 --- a/runtime/queries/scala/highlights.scm +++ b/runtime/queries/scala/highlights.scm @@ -263,7 +263,7 @@ "return" @keyword.control.return -(comment) @comment +[(comment) (block_comment)] @comment ;; `case` is a conditional keyword in case_block diff --git a/runtime/queries/scala/textobjects.scm b/runtime/queries/scala/textobjects.scm index fe0b8c255..6e551c417 100644 --- a/runtime/queries/scala/textobjects.scm +++ b/runtime/queries/scala/textobjects.scm @@ -51,8 +51,8 @@ ; Comment queries -(comment) @comment.inside -(comment) @comment.around ; Does not match consecutive block comments +[(comment) (block_comment)] @comment.inside +[(comment) (block_comment)] @comment.around ; Does not match consecutive block comments ; Test queries From 7d7ace551cd58f0b6d65af7a6dfa8f896d94724a Mon Sep 17 00:00:00 2001 From: Boris Verkhovskiy Date: Mon, 22 Jan 2024 15:19:28 -0800 Subject: [PATCH 04/70] Highlight .bash_history as bash (#9401) --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index a785828c1..4ff529c47 100644 --- a/languages.toml +++ b/languages.toml @@ -796,6 +796,7 @@ file-types = [ "sh", "bash", "zsh", + ".bash_history", ".bash_login", ".bash_logout", ".bash_profile", From 13ed4f6c4748019787d24c2b686d417b71604242 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 1 Dec 2023 00:03:26 +0100 Subject: [PATCH 05/70] Add hook/event system --- .cargo/config.toml | 14 +++ Cargo.lock | 6 + helix-event/Cargo.toml | 17 ++- helix-event/src/cancel.rs | 19 ++++ helix-event/src/debounce.rs | 67 +++++++++++ helix-event/src/hook.rs | 91 +++++++++++++++ helix-event/src/lib.rs | 201 ++++++++++++++++++++++++++++++++- helix-event/src/redraw.rs | 24 ++-- helix-event/src/registry.rs | 131 +++++++++++++++++++++ helix-event/src/runtime.rs | 88 +++++++++++++++ helix-event/src/status.rs | 68 +++++++++++ helix-event/src/test.rs | 90 +++++++++++++++ helix-term/Cargo.toml | 2 +- helix-term/src/application.rs | 24 +++- helix-term/src/commands.rs | 26 +++-- helix-term/src/events.rs | 20 ++++ helix-term/src/handlers.rs | 15 +++ helix-term/src/job.rs | 55 +++++++-- helix-term/src/lib.rs | 4 + helix-term/src/ui/editor.rs | 24 +++- helix-view/src/document.rs | 18 +++ helix-view/src/editor.rs | 2 + helix-view/src/events.rs | 9 ++ helix-view/src/handlers.rs | 12 ++ helix-view/src/handlers/lsp.rs | 39 +++++++ helix-view/src/lib.rs | 8 +- 26 files changed, 1024 insertions(+), 50 deletions(-) create mode 100644 helix-event/src/cancel.rs create mode 100644 helix-event/src/debounce.rs create mode 100644 helix-event/src/hook.rs create mode 100644 helix-event/src/registry.rs create mode 100644 helix-event/src/runtime.rs create mode 100644 helix-event/src/status.rs create mode 100644 helix-event/src/test.rs create mode 100644 helix-term/src/events.rs create mode 100644 helix-term/src/handlers.rs create mode 100644 helix-view/src/events.rs create mode 100644 helix-view/src/handlers.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index b016eca31..af4312dc8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,17 @@ +# we use tokio_unstable to enable runtime::Handle::id so we can separate +# globals from multiple parallel tests. If that function ever does get removed +# its possible to replace (with some additional overhead and effort) +# Annoyingly build.rustflags doesn't work here because it gets overwritten +# if people have their own global target.<..> config (for example to enable mold) +# specifying flags this way is more robust as they get merged +# This still gets overwritten by RUST_FLAGS though, luckily it shouldn't be necessary +# to set those most of the time. If downstream does overwrite this its not a huge +# deal since it will only break tests anyway +[target."cfg(all())"] +rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"] + + [alias] xtask = "run --package xtask --" integration-test = "test --features integration --profile integration --workspace --test integration" + diff --git a/Cargo.lock b/Cargo.lock index 9884dadfb..4969ef46b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1102,6 +1102,12 @@ dependencies = [ name = "helix-event" version = "23.10.0" dependencies = [ + "ahash", + "anyhow", + "futures-executor", + "hashbrown 0.14.3", + "log", + "once_cell", "parking_lot", "tokio", ] diff --git a/helix-event/Cargo.toml b/helix-event/Cargo.toml index c20328246..a5c88e93d 100644 --- a/helix-event/Cargo.toml +++ b/helix-event/Cargo.toml @@ -12,5 +12,18 @@ homepage.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] } -parking_lot = { version = "0.12", features = ["send_guard"] } +ahash = "0.8.3" +hashbrown = "0.14.0" +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 +# as it essentially makes the lock entirely free as long as there is no writes +parking_lot = { version = "0.12", features = ["hardware-lock-elision"] } +once_cell = "1.18" + +anyhow = "1" +log = "0.4" +futures-executor = "0.3.28" + +[features] +integration_test = [] diff --git a/helix-event/src/cancel.rs b/helix-event/src/cancel.rs new file mode 100644 index 000000000..f027be80e --- /dev/null +++ b/helix-event/src/cancel.rs @@ -0,0 +1,19 @@ +use std::future::Future; + +pub use oneshot::channel as cancelation; +use tokio::sync::oneshot; + +pub type CancelTx = oneshot::Sender<()>; +pub type CancelRx = oneshot::Receiver<()>; + +pub async fn cancelable_future(future: impl Future, cancel: CancelRx) -> Option { + tokio::select! { + biased; + _ = cancel => { + None + } + res = future => { + Some(res) + } + } +} diff --git a/helix-event/src/debounce.rs b/helix-event/src/debounce.rs new file mode 100644 index 000000000..30b6f671b --- /dev/null +++ b/helix-event/src/debounce.rs @@ -0,0 +1,67 @@ +//! Utilities for declaring an async (usually debounced) hook + +use std::time::Duration; + +use futures_executor::block_on; +use tokio::sync::mpsc::{self, error::TrySendError, Sender}; +use tokio::time::Instant; + +/// Async hooks provide a convenient framework for implementing (debounced) +/// async event handlers. Most synchronous event hooks will likely need to +/// debounce their events, coordinate multiple different hooks and potentially +/// track some state. `AsyncHooks` facilitate these use cases by running as +/// a background tokio task that waits for events (usually an enum) to be +/// sent through a channel. +pub trait AsyncHook: Sync + Send + 'static + Sized { + type Event: Sync + Send + 'static; + /// Called immediately whenever an event is received, this function can + /// consume the event immediately or debounce it. In case of debouncing, + /// it can either define a new debounce timeout or continue the current one + fn handle_event(&mut self, event: Self::Event, timeout: Option) -> Option; + + /// Called whenever the debounce timeline is reached + fn finish_debounce(&mut self); + + fn spawn(self) -> mpsc::Sender { + // the capacity doesn't matter too much here, unless the cpu is totally overwhelmed + // the cap will never be reached since we always immediately drain the channel + // 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)); + tx + } +} + +async fn run(mut hook: Hook, mut rx: mpsc::Receiver) { + let mut deadline = None; + loop { + let event = match deadline { + Some(deadline_) => { + let res = tokio::time::timeout_at(deadline_, rx.recv()).await; + match res { + Ok(event) => event, + Err(_) => { + hook.finish_debounce(); + deadline = None; + continue; + } + } + } + None => rx.recv().await, + }; + let Some(event) = event else { + break; + }; + deadline = hook.handle_event(event, deadline); + } +} + +pub fn send_blocking(tx: &Sender, data: T) { + // block_on has some overhead and in practice the channel should basically + // never be full anyway so first try sending without blocking + if let Err(TrySendError::Full(data)) = tx.try_send(data) { + // set a timeout so that we just drop a message instead of freezing the editor in the worst case + let _ = block_on(tx.send_timeout(data, Duration::from_millis(10))); + } +} diff --git a/helix-event/src/hook.rs b/helix-event/src/hook.rs new file mode 100644 index 000000000..7fb681483 --- /dev/null +++ b/helix-event/src/hook.rs @@ -0,0 +1,91 @@ +//! rust dynamic dispatch is extremely limited so we have to build our +//! own vtable implementation. Otherwise implementing the event system would not be possible. +//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally +//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a +//! pointer to a global (static) vtable entry which itself contains multiple other pointers +//! (the various functions of the trait, drop, size and align). That makes dynamic +//! dispatch pretty slow (double pointer indirections). However, we only have a single function +//! in the hook trait and don't need a drop implementation (event system is global anyway +//! and never dropped) so we can just store the entire vtable inline. + +use anyhow::Result; +use std::ptr::{self, NonNull}; + +use crate::Event; + +/// Opaque handle type that represents an erased type parameter. +/// +/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but +/// until then we can use this. +/// +/// Care should be taken that we don't use a concrete instance of this. It should only be used +/// through a reference, so we can maintain something else's lifetime. +struct Opaque(()); + +pub(crate) struct ErasedHook { + data: NonNull, + call: unsafe fn(NonNull, NonNull, NonNull), +} + +impl ErasedHook { + pub(crate) fn new_dynamic Result<()> + 'static + Send + Sync>( + hook: H, + ) -> ErasedHook { + unsafe fn call Result<()> + 'static + Send + Sync>( + hook: NonNull, + _event: NonNull, + result: NonNull, + ) { + let hook: NonNull = hook.cast(); + let result: NonNull> = result.cast(); + let hook: &F = hook.as_ref(); + let res = hook(); + ptr::write(result.as_ptr(), res) + } + + unsafe { + ErasedHook { + data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque), + call: call::, + } + } + } + + pub(crate) fn new Result<()>>(hook: F) -> ErasedHook { + unsafe fn call Result<()>>( + hook: NonNull, + event: NonNull, + result: NonNull, + ) { + let hook: NonNull = hook.cast(); + let mut event: NonNull = event.cast(); + let result: NonNull> = result.cast(); + let hook: &F = hook.as_ref(); + let res = hook(event.as_mut()); + ptr::write(result.as_ptr(), res) + } + + unsafe { + ErasedHook { + data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque), + call: call::, + } + } + } + + pub(crate) unsafe fn call(&self, event: &mut E) -> Result<()> { + let mut res = Ok(()); + + unsafe { + (self.call)( + self.data, + NonNull::from(event).cast(), + NonNull::from(&mut res).cast(), + ); + } + res + } +} + +unsafe impl Sync for ErasedHook {} +unsafe impl Send for ErasedHook {} diff --git a/helix-event/src/lib.rs b/helix-event/src/lib.rs index 9c082b93a..894de5e8d 100644 --- a/helix-event/src/lib.rs +++ b/helix-event/src/lib.rs @@ -1,8 +1,203 @@ //! `helix-event` contains systems that allow (often async) communication between -//! different editor components without strongly coupling them. Currently this -//! crate only contains some smaller facilities but the intend is to add more -//! functionality in the future ( like a generic hook system) +//! different editor components without strongly coupling them. Specifically +//! it allows defining synchronous hooks that run when certain editor events +//! occur. +//! +//! The core of the event system are hook callbacks and the [`Event`] trait. A +//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>` +//! that gets called every time an appropriate event is dispatched. The implementation +//! details of the [`Event`] trait are considered private. The [`events`] macro is +//! provided which automatically declares event types. Similarly the `register_hook` +//! macro should be used to (safely) declare event hooks. +//! +//! Hooks run synchronously which can be advantageous since they can modify the +//! current editor state right away (for example to immediately hide the completion +//! popup). However, they can not contain their own state without locking since +//! they only receive immutable references. For handler that want to track state, do +//! expensive background computations or debouncing an [`AsyncHook`] is preferable. +//! Async hooks are based around a channels that receive events specific to +//! that `AsyncHook` (usually an enum). These events can be sent by synchronous +//! hooks. Due to some limitations around tokio channels the [`send_blocking`] +//! function exported in this crate should be used instead of the builtin +//! `blocking_send`. +//! +//! In addition to the core event system, this crate contains some message queues +//! that allow transfer of data back to the main event loop from async hooks and +//! hooks that may not have access to all application data (for example in helix-view). +//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and +//! display status messages ([`status`]). +//! +//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the +//! main loop (including access to the compositor). Ideally that queue will be moved +//! to helix-view in the future if we manage to detach the compositor from its rendering backend. +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 registry::Event; +mod cancel; +mod debounce; +mod hook; mod redraw; +mod registry; +#[doc(hidden)] +pub mod runtime; +pub mod status; + +#[cfg(test)] +mod test; + +pub fn register_event() { + registry::with_mut(|registry| registry.register_event::()) +} + +/// Registers a hook that will be called when an event of type `E` is dispatched. +/// This function should usually not be used directly, use the [`register_hook`] +/// macro instead. +/// +/// +/// # Safety +/// +/// `hook` must be totally generic over all lifetime parameters of `E`. For +/// example if `E` was a known type `Foo<'a, 'b>`, then the correct trait bound +/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)`, but there is no way to +/// express that kind of constraint for a generic type with the Rust type system +/// as of this writing. +pub unsafe fn register_hook_raw( + hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync, +) { + registry::with_mut(|registry| registry.register_hook(hook)) +} + +/// Register a hook solely by event name +pub fn register_dynamic_hook( + hook: impl Fn() -> Result<()> + 'static + Send + Sync, + id: &str, +) -> Result<()> { + registry::with_mut(|reg| reg.register_dynamic_hook(hook, id)) +} + +pub fn dispatch(e: impl Event) { + registry::with(|registry| registry.dispatch(e)); +} + +/// Macro to declare events +/// +/// # Examples +/// +/// ``` no-compile +/// events! { +/// FileWrite(&Path) +/// ViewScrolled{ view: View, new_pos: ViewOffset } +/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangeSet } +/// } +/// +/// fn init() { +/// register_event::(); +/// register_event::(); +/// register_event::(); +/// } +/// +/// fn save(path: &Path, content: &str){ +/// std::fs::write(path, content); +/// dispatch(FileWrite(path)); +/// } +/// ``` +#[macro_export] +macro_rules! events { + ($name: ident<$($lt: lifetime),*> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => { + pub struct $name<$($lt),*> { $(pub $data: $data_ty),* } + unsafe impl<$($lt),*> $crate::Event for $name<$($lt),*> { + const ID: &'static str = stringify!($name); + const LIFETIMES: usize = $crate::events!(@sum $(1, $lt),*); + type Static = $crate::events!(@replace_lt $name, $('static, $lt),*); + } + $crate::events!{ $($rem)* } + }; + ($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => { + pub struct $name { $(pub $data: $data_ty),* } + unsafe impl $crate::Event for $name { + const ID: &'static str = stringify!($name); + const LIFETIMES: usize = 0; + type Static = Self; + } + $crate::events!{ $($rem)* } + }; + () => {}; + (@replace_lt $name: ident, $($lt1: lifetime, $lt2: lifetime),* ) => {$name<$($lt1),*>}; + (@sum $($val: expr, $lt1: lifetime),* ) => {0 $(+ $val)*}; +} + +/// Safely register statically typed event hooks +#[macro_export] +macro_rules! register_hook { + // Safety: this is safe because we fully control the type of the event here and + // ensure all lifetime arguments are fully generic and the correct number of lifetime arguments + // is present + (move |$event:ident: &mut $event_ty: ident<$($lt: lifetime),*>| $body: expr) => { + let val = move |$event: &mut $event_ty<$($lt),*>| $body; + unsafe { + // Lifetimes are a bit of a pain. We want to allow events being + // non-static. Lifetimes don't actually exist at runtime so its + // fine to essentially transmute the lifetimes as long as we can + // prove soundness. The hook must therefore accept any combination + // of lifetimes. In other words fn(&'_ mut Event<'_, '_>) is ok + // but examples like fn(&'_ mut Event<'_, 'static>) or fn<'a>(&'a + // mut Event<'a, 'a>) are not. To make this safe we use a macro to + // forbid the user from specifying lifetimes manually (all lifetimes + // specified are always function generics and passed to the event so + // lifetimes can't be used multiple times and using 'static causes a + // syntax error). + // + // There is one soundness hole tough: Type Aliases allow + // "accidentally" creating these problems. For example: + // + // type Event2 = Event<'static>. + // type Event2<'a> = Event<'a, a>. + // + // These cases can be caught by counting the number of lifetimes + // parameters at the parameter declaration site and then at the hook + // declaration site. By asserting the number of lifetime parameters + // are equal we can catch all bad type aliases under one assumption: + // There are no unused lifetime parameters. Introducing a static + // would reduce the number of arguments of the alias by one in the + // above example Event2 has zero lifetime arguments while the original + // event has one lifetime argument. Similar logic applies to using + // a lifetime argument multiple times. The ASSERT below performs a + // a compile time assertion to ensure exactly this property. + // + // With unused lifetime arguments it is still one way to cause unsound code: + // + // type Event2<'a, 'b> = Event<'a, 'a>; + // + // However, this case will always emit a compiler warning/cause CI + // failures so a user would have to introduce #[allow(unused)] which + // is easily caught in review (and a very theoretical case anyway). + // If we want to be pedantic we can simply compile helix with + // forbid(unused). All of this is just a safety net to prevent + // very theoretical misuse. This won't come up in real code (and is + // easily caught in review). + #[allow(unused)] + const ASSERT: () = { + if <$event_ty as $crate::Event>::LIFETIMES != 0 + $crate::events!(@sum $(1, $lt),*){ + panic!("invalid type alias"); + } + }; + $crate::register_hook_raw::<$crate::events!(@replace_lt $event_ty, $('static, $lt),*)>(val); + } + }; + (move |$event:ident: &mut $event_ty: ident| $body: expr) => { + let val = move |$event: &mut $event_ty| $body; + unsafe { + #[allow(unused)] + const ASSERT: () = { + if <$event_ty as $crate::Event>::LIFETIMES != 0{ + panic!("invalid type alias"); + } + }; + $crate::register_hook_raw::<$event_ty>(val); + } + }; +} diff --git a/helix-event/src/redraw.rs b/helix-event/src/redraw.rs index a99152238..8fadb8aea 100644 --- a/helix-event/src/redraw.rs +++ b/helix-event/src/redraw.rs @@ -5,16 +5,20 @@ use std::future::Future; use parking_lot::{RwLock, RwLockReadGuard}; use tokio::sync::Notify; -/// A `Notify` instance that can be used to (asynchronously) request -/// the editor the render a new frame. -static REDRAW_NOTIFY: Notify = Notify::const_new(); - -/// A `RwLock` that prevents the next frame from being -/// drawn until an exclusive (write) lock can be acquired. -/// This allows asynchsonous tasks to acquire `non-exclusive` -/// locks (read) to prevent the next frame from being drawn -/// until a certain computation has finished. -static RENDER_LOCK: RwLock<()> = RwLock::new(()); +use crate::runtime_local; + +runtime_local! { + /// A `Notify` instance that can be used to (asynchronously) request + /// the editor to render a new frame. + static REDRAW_NOTIFY: Notify = Notify::const_new(); + + /// A `RwLock` that prevents the next frame from being + /// drawn until an exclusive (write) lock can be acquired. + /// This allows asynchronous tasks to acquire `non-exclusive` + /// locks (read) to prevent the next frame from being drawn + /// until a certain computation has finished. + static RENDER_LOCK: RwLock<()> = RwLock::new(()); +} pub type RenderLockGuard = RwLockReadGuard<'static, ()>; diff --git a/helix-event/src/registry.rs b/helix-event/src/registry.rs new file mode 100644 index 000000000..d43c48ac4 --- /dev/null +++ b/helix-event/src/registry.rs @@ -0,0 +1,131 @@ +//! A global registry where events are registered and can be +//! subscribed to by registering hooks. The registry identifies event +//! types using their type name so multiple event with the same type name +//! may not be registered (will cause a panic to ensure soundness) + +use std::any::TypeId; + +use anyhow::{bail, Result}; +use hashbrown::hash_map::Entry; +use hashbrown::HashMap; +use parking_lot::RwLock; + +use crate::hook::ErasedHook; +use crate::runtime_local; + +pub struct Registry { + events: HashMap<&'static str, TypeId, ahash::RandomState>, + handlers: HashMap<&'static str, Vec, ahash::RandomState>, +} + +impl Registry { + pub fn register_event(&mut self) { + let ty = TypeId::of::(); + assert_eq!(ty, TypeId::of::()); + match self.events.entry(E::ID) { + Entry::Occupied(entry) => { + if entry.get() == &ty { + // don't warn during tests to avoid log spam + #[cfg(not(feature = "integration_test"))] + panic!("Event {} was registered multiple times", E::ID); + } else { + panic!("Multiple events with ID {} were registered", E::ID); + } + } + Entry::Vacant(ent) => { + ent.insert(ty); + self.handlers.insert(E::ID, Vec::new()); + } + } + } + + /// # Safety + /// + /// `hook` must be totally generic over all lifetime parameters of `E`. For + /// example if `E` was a known type `Foo<'a, 'b> then the correct trait bound + /// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)` but there is no way to + /// express that kind of constraint for a generic type with the rust type system + /// right now. + pub unsafe fn register_hook( + &mut self, + hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync, + ) { + // ensure event type ids match so we can rely on them always matching + let id = E::ID; + let Some(&event_id) = self.events.get(id) else { + panic!("Tried to register handler for unknown event {id}"); + }; + assert!( + TypeId::of::() == event_id, + "Tried to register invalid hook for event {id}" + ); + let hook = ErasedHook::new(hook); + self.handlers.get_mut(id).unwrap().push(hook); + } + + pub fn register_dynamic_hook( + &mut self, + hook: impl Fn() -> Result<()> + 'static + Send + Sync, + id: &str, + ) -> Result<()> { + // ensure event type ids match so we can rely on them always matching + if self.events.get(id).is_none() { + bail!("Tried to register handler for unknown event {id}"); + }; + let hook = ErasedHook::new_dynamic(hook); + self.handlers.get_mut(id).unwrap().push(hook); + Ok(()) + } + + pub fn dispatch(&self, mut event: E) { + let Some(hooks) = self.handlers.get(E::ID) else { + log::error!("Dispatched unknown event {}", E::ID); + return; + }; + let event_id = self.events[E::ID]; + + assert_eq!( + TypeId::of::(), + event_id, + "Tried to dispatch invalid event {}", + E::ID + ); + + for hook in hooks { + // safety: event type is the same + if let Err(err) = unsafe { hook.call(&mut event) } { + log::error!("{} hook failed: {err:#?}", E::ID); + crate::status::report_blocking(err); + } + } + } +} + +runtime_local! { + static REGISTRY: RwLock = RwLock::new(Registry { + // hardcoded random number is good enough here we don't care about DOS resistance + // and avoids the additional complexity of `Option` + events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)), + handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)), + }); +} + +pub(crate) fn with(f: impl FnOnce(&Registry) -> T) -> T { + f(®ISTRY.read()) +} + +pub(crate) fn with_mut(f: impl FnOnce(&mut Registry) -> T) -> T { + f(&mut REGISTRY.write()) +} + +/// # Safety +/// The number of specified lifetimes and the static type *must* be correct. +/// This is ensured automatically by the [`events`](crate::events) +/// macro. +pub unsafe trait Event: Sized { + /// Globally unique (case sensitive) string that identifies this type. + /// A good candidate is the events type name + const ID: &'static str; + const LIFETIMES: usize; + type Static: Event + 'static; +} diff --git a/helix-event/src/runtime.rs b/helix-event/src/runtime.rs new file mode 100644 index 000000000..8da465ef3 --- /dev/null +++ b/helix-event/src/runtime.rs @@ -0,0 +1,88 @@ +//! The event system makes use of global to decouple different systems. +//! However, this can cause problems for the integration test system because +//! it runs multiple helix applications in parallel. Making the globals +//! thread-local does not work because a applications can/does have multiple +//! runtime threads. Instead this crate implements a similar notion to a thread +//! local but instead of being local to a single thread, the statics are local to +//! a single tokio-runtime. The implementation requires locking so it's not exactly efficient. +//! +//! Therefore this function is only enabled during integration tests and behaves like +//! a normal static otherwise. I would prefer this module to be fully private and to only +//! export the macro but the macro still need to construct these internals so it's marked +//! `doc(hidden)` instead + +use std::ops::Deref; + +#[cfg(not(feature = "integration_test"))] +pub struct RuntimeLocal { + /// inner API used in the macro, not part of public API + #[doc(hidden)] + pub __data: T, +} + +#[cfg(not(feature = "integration_test"))] +impl Deref for RuntimeLocal { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.__data + } +} + +#[cfg(not(feature = "integration_test"))] +#[macro_export] +macro_rules! runtime_local { + ($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => { + $($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal { + __data: $init + };)* + }; +} + +#[cfg(feature = "integration_test")] +pub struct RuntimeLocal { + data: + parking_lot::RwLock>, + init: fn() -> T, +} + +#[cfg(feature = "integration_test")] +impl RuntimeLocal { + /// inner API used in the macro, not part of public API + #[doc(hidden)] + pub const fn __new(init: fn() -> T) -> Self { + Self { + data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher( + ahash::RandomState::with_seeds(423, 9978, 38322, 3280080), + )), + init, + } + } +} + +#[cfg(feature = "integration_test")] +impl Deref for RuntimeLocal { + type Target = T; + fn deref(&self) -> &T { + let id = tokio::runtime::Handle::current().id(); + let guard = self.data.read(); + match guard.get(&id) { + Some(res) => res, + None => { + drop(guard); + let data = Box::leak(Box::new((self.init)())); + let mut guard = self.data.write(); + guard.insert(id, data); + data + } + } + } +} + +#[cfg(feature = "integration_test")] +#[macro_export] +macro_rules! runtime_local { + ($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => { + $($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal::__new(|| $init);)* + }; +} diff --git a/helix-event/src/status.rs b/helix-event/src/status.rs new file mode 100644 index 000000000..fdca67624 --- /dev/null +++ b/helix-event/src/status.rs @@ -0,0 +1,68 @@ +//! A queue of async messages/errors that will be shown in the editor + +use std::borrow::Cow; +use std::time::Duration; + +use crate::{runtime_local, send_blocking}; +use once_cell::sync::OnceCell; +use tokio::sync::mpsc::{Receiver, Sender}; + +/// Describes the severity level of a [`StatusMessage`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +pub enum Severity { + Hint, + Info, + Warning, + Error, +} + +pub struct StatusMessage { + pub severity: Severity, + pub message: Cow<'static, str>, +} + +impl From for StatusMessage { + fn from(err: anyhow::Error) -> Self { + StatusMessage { + severity: Severity::Error, + message: err.to_string().into(), + } + } +} + +impl From<&'static str> for StatusMessage { + fn from(msg: &'static str) -> Self { + StatusMessage { + severity: Severity::Info, + message: msg.into(), + } + } +} + +runtime_local! { + static MESSAGES: OnceCell> = OnceCell::new(); +} + +pub async fn report(msg: impl Into) { + // if the error channel overflows just ignore it + let _ = MESSAGES + .wait() + .send_timeout(msg.into(), Duration::from_millis(10)) + .await; +} + +pub fn report_blocking(msg: impl Into) { + let messages = MESSAGES.wait(); + send_blocking(messages, msg.into()) +} + +/// Must be called once during editor startup exactly once +/// before any of the messages in this module can be used +/// +/// # Panics +/// If called multiple times +pub fn setup() -> Receiver { + let (tx, rx) = tokio::sync::mpsc::channel(128); + let _ = MESSAGES.set(tx); + rx +} diff --git a/helix-event/src/test.rs b/helix-event/src/test.rs new file mode 100644 index 000000000..a1283ada1 --- /dev/null +++ b/helix-event/src/test.rs @@ -0,0 +1,90 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::Mutex; + +use crate::{dispatch, events, register_dynamic_hook, register_event, register_hook}; +#[test] +fn smoke_test() { + events! { + Event1 { content: String } + Event2 { content: usize } + } + register_event::(); + register_event::(); + + // setup hooks + let res1: Arc> = Arc::default(); + let acc = Arc::clone(&res1); + register_hook!(move |event: &mut Event1| { + acc.lock().push_str(&event.content); + Ok(()) + }); + let res2: Arc = Arc::default(); + let acc = Arc::clone(&res2); + register_hook!(move |event: &mut Event2| { + acc.fetch_add(event.content, Ordering::Relaxed); + Ok(()) + }); + + // triggers events + let thread = std::thread::spawn(|| { + for i in 0..1000 { + dispatch(Event2 { content: i }); + } + }); + std::thread::sleep(Duration::from_millis(1)); + dispatch(Event1 { + content: "foo".to_owned(), + }); + dispatch(Event2 { content: 42 }); + dispatch(Event1 { + content: "bar".to_owned(), + }); + dispatch(Event1 { + content: "hello world".to_owned(), + }); + thread.join().unwrap(); + + // check output + assert_eq!(&**res1.lock(), "foobarhello world"); + assert_eq!( + res2.load(Ordering::Relaxed), + 42 + (0..1000usize).sum::() + ); +} + +#[test] +fn dynamic() { + events! { + Event3 {} + Event4 { count: usize } + }; + register_event::(); + register_event::(); + + let count = Arc::new(AtomicUsize::new(0)); + let count1 = count.clone(); + let count2 = count.clone(); + register_dynamic_hook( + move || { + count1.fetch_add(2, Ordering::Relaxed); + Ok(()) + }, + "Event3", + ) + .unwrap(); + register_dynamic_hook( + move || { + count2.fetch_add(3, Ordering::Relaxed); + Ok(()) + }, + "Event4", + ) + .unwrap(); + dispatch(Event3 {}); + dispatch(Event4 { count: 0 }); + dispatch(Event3 {}); + assert_eq!(count.load(Ordering::Relaxed), 7) +} diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 21c35553c..7bdd433ed 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -15,7 +15,7 @@ homepage.workspace = true [features] default = ["git"] unicode-lines = ["helix-core/unicode-lines"] -integration = [] +integration = ["helix-event/integration_test"] git = ["helix-vcs/git"] [[bin]] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 290441b41..8215eeaa5 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,6 +1,10 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; -use helix_core::{pos_at_coords, syntax, Selection}; +use helix_core::{ + chars::char_is_word, + diagnostic::{DiagnosticTag, NumberOrString}, + pos_at_coords, syntax, Selection, +}; use helix_lsp::{ lsp::{self, notification::Notification}, util::lsp_range_to_range, @@ -24,6 +28,7 @@ use crate::{ commands::apply_workspace_edit, compositor::{Compositor, Event}, config::Config, + handlers, job::Jobs, keymap::Keymaps, ui::{self, overlay::overlaid}, @@ -138,6 +143,7 @@ impl Application { let area = terminal.size().expect("couldn't get terminal size"); let mut compositor = Compositor::new(area); let config = Arc::new(ArcSwap::from_pointee(config)); + let handlers = handlers::setup(config.clone()); let mut editor = Editor::new( area, theme_loader.clone(), @@ -145,6 +151,7 @@ impl Application { Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor })), + handlers, ); let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { @@ -321,10 +328,21 @@ impl Application { Some(event) = input_stream.next() => { self.handle_terminal_events(event).await; } - Some(callback) = self.jobs.futures.next() => { - self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); + Some(callback) = self.jobs.callbacks.recv() => { + self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback))); self.render().await; } + Some(msg) = self.jobs.status_messages.recv() => { + let severity = match msg.severity{ + helix_event::status::Severity::Hint => Severity::Hint, + helix_event::status::Severity::Info => Severity::Info, + helix_event::status::Severity::Warning => Severity::Warning, + helix_event::status::Severity::Error => Severity::Error, + }; + // TODO: show multiple status messages at once to avoid clobbering + self.editor.status_msg = Some((msg.message, severity)); + helix_event::request_redraw(); + } Some(callback) = self.jobs.wait_futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render().await; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 53783e4e3..48ceb23ba 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -88,7 +88,7 @@ pub struct Context<'a> { pub count: Option, pub editor: &'a mut Editor, - pub callback: Option, + pub callback: Vec, pub on_next_key_callback: Option, pub jobs: &'a mut Jobs, } @@ -96,16 +96,18 @@ pub struct Context<'a> { impl<'a> Context<'a> { /// Push a new component onto the compositor. pub fn push_layer(&mut self, component: Box) { - self.callback = Some(Box::new(|compositor: &mut Compositor, _| { - compositor.push(component) - })); + self.callback + .push(Box::new(|compositor: &mut Compositor, _| { + compositor.push(component) + })); } /// Call `replace_or_push` on the Compositor pub fn replace_or_push_layer(&mut self, id: &'static str, component: T) { - self.callback = Some(Box::new(move |compositor: &mut Compositor, _| { - compositor.replace_or_push(id, component); - })); + self.callback + .push(Box::new(move |compositor: &mut Compositor, _| { + compositor.replace_or_push(id, component); + })); } #[inline] @@ -2934,7 +2936,7 @@ pub fn command_palette(cx: &mut Context) { let register = cx.register; let count = cx.count; - cx.callback = Some(Box::new( + cx.callback.push(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { let keymap = compositor.find::().unwrap().keymaps.map() [&cx.editor.mode] @@ -2954,7 +2956,7 @@ pub fn command_palette(cx: &mut Context) { register, count, editor: cx.editor, - callback: None, + callback: Vec::new(), on_next_key_callback: None, jobs: cx.jobs, }; @@ -2982,7 +2984,7 @@ pub fn command_palette(cx: &mut Context) { fn last_picker(cx: &mut Context) { // TODO: last picker does not seem to work well with buffer_picker - cx.callback = Some(Box::new(|compositor, cx| { + cx.callback.push(Box::new(|compositor, cx| { if let Some(picker) = compositor.last_picker.take() { compositor.push(picker); } else { @@ -3494,6 +3496,7 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range { } pub mod insert { + use crate::events::PostInsertChar; use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option; pub type PostHook = fn(&mut Context, char); @@ -3627,6 +3630,7 @@ pub mod insert { for hook in &[language_server_completion, signature_help] { hook(cx, c); } + helix_event::dispatch(PostInsertChar { c, cx }); } pub fn smart_tab(cx: &mut Context) { @@ -5820,7 +5824,7 @@ fn replay_macro(cx: &mut Context) { cx.editor.macro_replaying.push(reg); let count = cx.count(); - cx.callback = Some(Box::new(move |compositor, cx| { + cx.callback.push(Box::new(move |compositor, cx| { for _ in 0..count { for &key in keys.iter() { compositor.handle_event(&compositor::Event::Key(key), cx); diff --git a/helix-term/src/events.rs b/helix-term/src/events.rs new file mode 100644 index 000000000..49b44f775 --- /dev/null +++ b/helix-term/src/events.rs @@ -0,0 +1,20 @@ +use helix_event::{events, register_event}; +use helix_view::document::Mode; +use helix_view::events::{DocumentDidChange, SelectionDidChange}; + +use crate::commands; +use crate::keymap::MappableCommand; + +events! { + OnModeSwitch<'a, 'cx> { old_mode: Mode, new_mode: Mode, cx: &'a mut commands::Context<'cx> } + PostInsertChar<'a, 'cx> { c: char, cx: &'a mut commands::Context<'cx> } + PostCommand<'a, 'cx> { command: & 'a MappableCommand, cx: &'a mut commands::Context<'cx> } +} + +pub fn register() { + register_event::(); + register_event::(); + register_event::(); + register_event::(); + register_event::(); +} diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs new file mode 100644 index 000000000..ab2d724ff --- /dev/null +++ b/helix-term/src/handlers.rs @@ -0,0 +1,15 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; + +use crate::config::Config; +use crate::events; + + + } +pub fn setup(config: Arc>) -> Handlers { + events::register(); + let handlers = Handlers { + }; + handlers +} diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 19f2521a5..72ed892dd 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -1,13 +1,37 @@ +use helix_event::status::StatusMessage; +use helix_event::{runtime_local, send_blocking}; use helix_view::Editor; +use once_cell::sync::OnceCell; use crate::compositor::Compositor; use futures_util::future::{BoxFuture, Future, FutureExt}; use futures_util::stream::{FuturesUnordered, StreamExt}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; pub type EditorCompositorCallback = Box; pub type EditorCallback = Box; +runtime_local! { + static JOB_QUEUE: OnceCell> = OnceCell::new(); +} + +pub async fn dispatch_callback(job: Callback) { + let _ = JOB_QUEUE.wait().send(job).await; +} + +pub async fn dispatch(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) { + let _ = JOB_QUEUE + .wait() + .send(Callback::EditorCompositor(Box::new(job))) + .await; +} + +pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) { + let jobs = JOB_QUEUE.wait(); + send_blocking(jobs, Callback::EditorCompositor(Box::new(job))) +} + pub enum Callback { EditorCompositor(EditorCompositorCallback), Editor(EditorCallback), @@ -21,11 +45,11 @@ pub struct Job { pub wait: bool, } -#[derive(Default)] pub struct Jobs { - pub futures: FuturesUnordered, - /// These are the ones that need to complete before we exit. + /// jobs that need to complete before we exit. pub wait_futures: FuturesUnordered, + pub callbacks: Receiver, + pub status_messages: Receiver, } impl Job { @@ -52,8 +76,16 @@ impl Job { } impl Jobs { + #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self::default() + let (tx, rx) = channel(1024); + let _ = JOB_QUEUE.set(tx); + let status_messages = helix_event::status::setup(); + Self { + wait_futures: FuturesUnordered::new(), + callbacks: rx, + status_messages, + } } pub fn spawn> + Send + 'static>(&mut self, f: F) { @@ -85,18 +117,17 @@ impl Jobs { } } - pub async fn next_job(&mut self) -> Option>> { - tokio::select! { - event = self.futures.next() => { event } - event = self.wait_futures.next() => { event } - } - } - pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); } else { - self.futures.push(j.future); + tokio::spawn(async move { + match j.future.await { + Ok(Some(cb)) => dispatch_callback(cb).await, + Ok(None) => (), + Err(err) => helix_event::status::report(err).await, + } + }); } } diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index a1d603298..b1413ed0d 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -6,13 +6,17 @@ pub mod args; pub mod commands; pub mod compositor; pub mod config; +pub mod events; pub mod health; pub mod job; pub mod keymap; pub mod ui; + use std::path::Path; use futures_util::Future; +mod handlers; + use ignore::DirEntry; use url::Url; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 24fcdb014..9f186d148 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -2,6 +2,7 @@ use crate::{ commands::{self, OnKeyCallback}, compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, + events::{OnModeSwitch, PostCommand}, key, keymap::{KeymapResult, Keymaps}, ui::{ @@ -835,11 +836,18 @@ impl EditorView { let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); + helix_event::dispatch(PostCommand { command, cx: cxt }); let current_mode = cxt.editor.mode(); match (last_mode, current_mode) { (Mode::Normal, Mode::Insert) => { // HAXX: if we just entered insert mode from normal, clear key buf // and record the command that got us into this mode. + if current_mode != last_mode { + helix_event::dispatch(OnModeSwitch { + old_mode: last_mode, + new_mode: current_mode, + cx: cxt, + }); // how we entered insert mode is important, and we should track that so // we can repeat the side effect. @@ -1004,7 +1012,7 @@ impl EditorView { } let area = completion.area(size, editor); - editor.last_completion = None; + editor.last_completion = Some(CompleteAction::Triggered); self.last_insert.1.push(InsertEvent::TriggerCompletion); // TODO : propagate required size on resize to completion too @@ -1265,7 +1273,7 @@ impl Component for EditorView { editor: context.editor, count: None, register: None, - callback: None, + callback: Vec::new(), on_next_key_callback: None, jobs: context.jobs, }; @@ -1375,7 +1383,7 @@ impl Component for EditorView { } // appease borrowck - let callback = cx.callback.take(); + let callbacks = take(&mut cx.callback); // if the command consumed the last view, skip the render. // on the next loop cycle the Application will then terminate. @@ -1394,6 +1402,16 @@ impl Component for EditorView { if mode != Mode::Insert { doc.append_changes_to_history(view); } + let callback = if callbacks.is_empty() { + None + } else { + let callback: crate::compositor::Callback = Box::new(move |compositor, cx| { + for callback in callbacks { + callback(compositor, cx) + } + }); + Some(callback) + }; EventResult::Consumed(callback) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 6473c2d18..93b83da4f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -36,6 +36,7 @@ use helix_core::{ }; use crate::editor::Config; +use crate::events::{DocumentDidChange, SelectionDidChange}; use crate::{DocumentId, Editor, Theme, View, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. @@ -1096,6 +1097,10 @@ impl Document { // TODO: use a transaction? self.selections .insert(view_id, selection.ensure_invariants(self.text().slice(..))); + helix_event::dispatch(SelectionDidChange { + doc: self, + view: view_id, + }) } /// Find the origin selection of the text in a document, i.e. where @@ -1149,6 +1154,14 @@ impl Document { 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() @@ -1164,6 +1177,10 @@ impl Document { view_id, selection.clone().ensure_invariants(self.text.slice(..)), ); + helix_event::dispatch(SelectionDidChange { + doc: self, + view: view_id, + }); } self.modified_since_accessed = true; @@ -1276,6 +1293,7 @@ impl Document { } 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( diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 0ab4be8b2..44c706d7d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2,6 +2,7 @@ use crate::{ align_view, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint}, graphics::{CursorKind, Rect}, + handlers::Handlers, info::Info, input::KeyEvent, register::Registers, @@ -960,6 +961,7 @@ pub struct Editor { /// field is set and any old requests are automatically /// canceled as a result pub completion_request_handle: Option>, + pub handlers: Handlers, } pub type Motion = Box; diff --git a/helix-view/src/events.rs b/helix-view/src/events.rs new file mode 100644 index 000000000..8b789cc0d --- /dev/null +++ b/helix-view/src/events.rs @@ -0,0 +1,9 @@ +use helix_core::Rope; +use helix_event::events; + +use crate::{Document, ViewId}; + +events! { + DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope } + SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId } +} diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs new file mode 100644 index 000000000..ae3eb545a --- /dev/null +++ b/helix-view/src/handlers.rs @@ -0,0 +1,12 @@ +use std::sync::Arc; + +use helix_event::send_blocking; +use tokio::sync::mpsc::Sender; + +use crate::handlers::lsp::SignatureHelpInvoked; +use crate::Editor; + +pub mod dap; +pub mod lsp; + +pub struct Handlers {} diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index 8b1378917..958385646 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -1 +1,40 @@ +use crate::{DocumentId, ViewId}; +#[derive(Debug, Clone, Copy)] +pub struct CompletionTrigger { + /// The char position of the primary cursor when the + /// completion was triggered + pub trigger_pos: usize, + pub doc: DocumentId, + pub view: ViewId, + /// Whether the cause of the trigger was an automatic completion (any word + /// char for words longer than minimum word length). + /// This is false for trigger chars send by the LS + pub auto: bool, +} + +pub enum CompletionEvent { + /// Auto completion was triggered by typing a word char + /// or a completion trigger + Trigger(CompletionTrigger), + /// A completion was manually requested (c-x) + Manual, + /// Some text was deleted and the cursor is now at `pos` + DeleteText { pos: usize }, + /// Invalidate the current auto completion trigger + Cancel, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum SignatureHelpInvoked { + Automatic, + Manual, +} + +pub enum SignatureHelpEvent { + Invoked, + Trigger, + ReTrigger, + Cancel, + RequestComplete { open: bool }, +} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 6a68e7d6f..82827b5d5 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -1,17 +1,15 @@ #[macro_use] pub mod macros; +pub mod base64; pub mod clipboard; pub mod document; pub mod editor; pub mod env; +pub mod events; pub mod graphics; pub mod gutter; -pub mod handlers { - pub mod dap; - pub mod lsp; -} -pub mod base64; +pub mod handlers; pub mod info; pub mod input; pub mod keyboard; From 8e592a151fe7adfbf3fb35ae134b7f2a70700f09 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 1 Dec 2023 00:03:27 +0100 Subject: [PATCH 06/70] refactor completion and signature help using hooks --- Cargo.lock | 1 + book/src/configuration.md | 3 +- helix-lsp/src/client.rs | 11 +- helix-stdx/Cargo.toml | 1 + helix-stdx/src/lib.rs | 1 + helix-stdx/src/rope.rs | 26 ++ helix-term/src/application.rs | 6 +- helix-term/src/commands.rs | 259 +----------- helix-term/src/commands/lsp.rs | 156 +------- helix-term/src/handlers.rs | 17 +- helix-term/src/handlers/completion.rs | 465 ++++++++++++++++++++++ helix-term/src/handlers/signature_help.rs | 335 ++++++++++++++++ helix-term/src/ui/completion.rs | 87 ++-- helix-term/src/ui/editor.rs | 59 +-- helix-term/src/ui/menu.rs | 34 +- helix-view/src/document.rs | 14 - helix-view/src/editor.rs | 35 +- helix-view/src/handlers.rs | 37 +- helix-view/src/handlers/lsp.rs | 35 +- 19 files changed, 1022 insertions(+), 560 deletions(-) create mode 100644 helix-stdx/src/rope.rs create mode 100644 helix-term/src/handlers/completion.rs create mode 100644 helix-term/src/handlers/signature_help.rs diff --git a/Cargo.lock b/Cargo.lock index 4969ef46b..96496125a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1165,6 +1165,7 @@ version = "23.10.0" dependencies = [ "dunce", "etcetera", + "ropey", "tempfile", ] diff --git a/book/src/configuration.md b/book/src/configuration.md index 36e2fee2e..a43ede76a 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -51,7 +51,8 @@ Its settings will be merged with the configuration directory `config.toml` and t | `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. Used for autocompletion, set to 0 for instant | `250` | +| `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` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` | diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 1af27c1d4..7eef2bf7a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -9,7 +9,7 @@ use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_stdx::path; use lsp::{ notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport, - DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder, + DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, WorkspaceFolder, WorkspaceFoldersChangeEvent, }; use lsp_types as lsp; @@ -999,6 +999,7 @@ impl Client { text_document: lsp::TextDocumentIdentifier, position: lsp::Position, work_done_token: Option, + context: lsp::CompletionContext, ) -> Option>> { let capabilities = self.capabilities.get().unwrap(); @@ -1010,13 +1011,12 @@ impl Client { text_document, position, }, + context: Some(context), // TODO: support these tokens by async receiving and updating the choice list work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token }, partial_result_params: lsp::PartialResultParams { partial_result_token: None, }, - context: None, - // lsp::CompletionContext { trigger_kind: , trigger_character: Some(), } }; Some(self.call::(params)) @@ -1063,7 +1063,7 @@ impl Client { text_document: lsp::TextDocumentIdentifier, position: lsp::Position, work_done_token: Option, - ) -> Option>> { + ) -> Option>>> { let capabilities = self.capabilities.get().unwrap(); // Return early if the server does not support signature help. @@ -1079,7 +1079,8 @@ impl Client { // lsp::SignatureHelpContext }; - Some(self.call::(params)) + let res = self.call::(params); + Some(async move { Ok(serde_json::from_value(res.await?)?) }) } pub fn text_document_range_inlay_hints( diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index 216a3b407..9b4de9fef 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -14,6 +14,7 @@ homepage.workspace = true [dependencies] dunce = "1.0" etcetera = "0.8" +ropey = { version = "1.6.1", default-features = false } [dev-dependencies] tempfile = "3.9" diff --git a/helix-stdx/src/lib.rs b/helix-stdx/src/lib.rs index ae3c3a981..68fe3ec37 100644 --- a/helix-stdx/src/lib.rs +++ b/helix-stdx/src/lib.rs @@ -1,2 +1,3 @@ pub mod env; pub mod path; +pub mod rope; diff --git a/helix-stdx/src/rope.rs b/helix-stdx/src/rope.rs new file mode 100644 index 000000000..4ee39d4a8 --- /dev/null +++ b/helix-stdx/src/rope.rs @@ -0,0 +1,26 @@ +use ropey::RopeSlice; + +pub trait RopeSliceExt: Sized { + fn ends_with(self, text: &str) -> bool; + fn starts_with(self, text: &str) -> bool; +} + +impl RopeSliceExt for RopeSlice<'_> { + fn ends_with(self, text: &str) -> bool { + let len = self.len_bytes(); + if len < text.len() { + return false; + } + self.get_byte_slice(len - text.len()..) + .map_or(false, |end| end == text) + } + + fn starts_with(self, text: &str) -> bool { + let len = self.len_bytes(); + if len < text.len() { + return false; + } + self.get_byte_slice(..len - text.len()) + .map_or(false, |start| start == text) + } +} diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8215eeaa5..3f3e59c6a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,10 +1,6 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; -use helix_core::{ - chars::char_is_word, - diagnostic::{DiagnosticTag, NumberOrString}, - pos_at_coords, syntax, Selection, -}; +use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection}; use helix_lsp::{ lsp::{self, notification::Notification}, util::lsp_range_to_range, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 48ceb23ba..4df3278b8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5,7 +5,6 @@ pub(crate) mod typed; pub use dap::*; use helix_vcs::Hunk; pub use lsp::*; -use tokio::sync::oneshot; use tui::widgets::Row; pub use typed::*; @@ -33,7 +32,7 @@ use helix_core::{ }; use helix_view::{ document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, - editor::{Action, CompleteAction}, + editor::Action, info::Info, input::KeyEvent, keyboard::KeyCode, @@ -52,14 +51,10 @@ use crate::{ filter_picker_entry, job::Callback, keymap::ReverseKeymap, - ui::{ - self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker, - Popup, Prompt, PromptEvent, - }, + ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Jobs}; -use futures_util::{stream::FuturesUnordered, TryStreamExt}; use std::{ collections::{HashMap, HashSet}, fmt, @@ -2593,7 +2588,6 @@ fn delete_by_selection_insert_mode( ); } doc.apply(&transaction, view.id); - lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); } fn delete_selection(cx: &mut Context) { @@ -2667,10 +2661,6 @@ fn insert_mode(cx: &mut Context) { .transform(|range| Range::new(range.to(), range.from())); doc.set_selection(view.id, selection); - - // [TODO] temporary workaround until we're not using the idle timer to - // trigger auto completions any more - cx.editor.clear_idle_timer(); } // inserts at the end of each selection @@ -3497,9 +3487,9 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range { pub mod insert { use crate::events::PostInsertChar; + use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option; - pub type PostHook = fn(&mut Context, char); /// Exclude the cursor in range. fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range { @@ -3513,88 +3503,6 @@ pub mod insert { } } - // It trigger completion when idle timer reaches deadline - // Only trigger completion if the word under cursor is longer than n characters - pub fn idle_completion(cx: &mut Context) { - let config = cx.editor.config(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let cursor = doc.selection(view.id).primary().cursor(text); - - use helix_core::chars::char_is_word; - let mut iter = text.chars_at(cursor); - iter.reverse(); - for _ in 0..config.completion_trigger_len { - match iter.next() { - Some(c) if char_is_word(c) => {} - _ => return, - } - } - super::completion(cx); - } - - fn language_server_completion(cx: &mut Context, ch: char) { - let config = cx.editor.config(); - if !config.auto_completion { - return; - } - - use helix_lsp::lsp; - // if ch matches completion char, trigger completion - let doc = doc_mut!(cx.editor); - let trigger_completion = doc - .language_servers_with_feature(LanguageServerFeature::Completion) - .any(|ls| { - // TODO: what if trigger is multiple chars long - matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions { - trigger_characters: Some(triggers), - .. - }) if triggers.iter().any(|trigger| trigger.contains(ch))) - }); - - if trigger_completion { - cx.editor.clear_idle_timer(); - super::completion(cx); - } - } - - fn signature_help(cx: &mut Context, ch: char) { - use helix_lsp::lsp; - // if ch matches signature_help char, trigger - let doc = doc_mut!(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; - }; - - let capabilities = language_server.capabilities(); - - if let lsp::ServerCapabilities { - signature_help_provider: - Some(lsp::SignatureHelpOptions { - trigger_characters: Some(triggers), - // TODO: retrigger_characters - .. - }), - .. - } = capabilities - { - // TODO: what if trigger is multiple chars long - let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); - // lsp doesn't tell us when to close the signature help, so we request - // the help information again after common close triggers which should - // return None, which in turn closes the popup. - let close_triggers = &[')', ';', '.']; - - if is_trigger || close_triggers.contains(&ch) { - super::signature_help_impl(cx, SignatureHelpInvoked::Automatic); - } - } - } - // The default insert hook: simply insert the character #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option { @@ -3624,12 +3532,6 @@ pub mod insert { doc.apply(&t, view.id); } - // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) - // this could also generically look at Transaction, but it's a bit annoying to look at - // Operation instead of Change. - for hook in &[language_server_completion, signature_help] { - hook(cx, c); - } helix_event::dispatch(PostInsertChar { c, cx }); } @@ -3855,8 +3757,6 @@ pub mod insert { }); let (view, doc) = current!(cx.editor); doc.apply(&transaction, view.id); - - lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); } pub fn delete_char_forward(cx: &mut Context) { @@ -4510,151 +4410,14 @@ fn remove_primary_selection(cx: &mut Context) { } pub fn completion(cx: &mut Context) { - use helix_lsp::{lsp, util::pos_to_lsp_pos}; - let (view, doc) = current!(cx.editor); + let range = doc.selection(view.id).primary(); + let text = doc.text().slice(..); + let cursor = range.cursor(text); - let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion - { - savepoint.clone() - } else { - doc.savepoint(view) - }; - - let text = savepoint.text.clone(); - let cursor = savepoint.cursor(); - - let mut seen_language_servers = HashSet::new(); - - let mut futures: FuturesUnordered<_> = doc - .language_servers_with_feature(LanguageServerFeature::Completion) - .filter(|ls| seen_language_servers.insert(ls.id())) - .map(|language_server| { - let language_server_id = language_server.id(); - let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(&text, cursor, offset_encoding); - let doc_id = doc.identifier(); - let completion_request = language_server.completion(doc_id, pos, None).unwrap(); - - async move { - let json = completion_request.await?; - let response: Option = serde_json::from_value(json)?; - - let items = match response { - Some(lsp::CompletionResponse::Array(items)) => items, - // TODO: do something with is_incomplete - Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: _is_incomplete, - items, - })) => items, - None => Vec::new(), - } - .into_iter() - .map(|item| CompletionItem { - item, - language_server_id, - resolved: false, - }) - .collect(); - - anyhow::Ok(items) - } - }) - .collect(); - - // setup a channel that allows the request to be canceled - let (tx, rx) = oneshot::channel(); - // set completion_request so that this request can be canceled - // by setting completion_request, the old channel stored there is dropped - // and the associated request is automatically dropped - cx.editor.completion_request_handle = Some(tx); - let future = async move { - let items_future = async move { - let mut items = Vec::new(); - // TODO if one completion request errors, all other completion requests are discarded (even if they're valid) - while let Some(mut lsp_items) = futures.try_next().await? { - items.append(&mut lsp_items); - } - anyhow::Ok(items) - }; - tokio::select! { - biased; - _ = rx => { - Ok(Vec::new()) - } - res = items_future => { - res - } - } - }; - - let trigger_offset = cursor; - - // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply - // completion filtering. For example logger.te| should filter the initial suggestion list with "te". - - use helix_core::chars; - let mut iter = text.chars_at(cursor); - iter.reverse(); - let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); - let start_offset = cursor.saturating_sub(offset); - - let trigger_doc = doc.id(); - let trigger_view = view.id; - - // FIXME: The commands Context can only have a single callback - // which means it gets overwritten when executing keybindings - // with multiple commands or macros. This would mean that completion - // might be incorrectly applied when repeating the insertmode action - // - // TODO: to solve this either make cx.callback a Vec of callbacks or - // alternatively move `last_insert` to `helix_view::Editor` - cx.callback = Some(Box::new( - move |compositor: &mut Compositor, _cx: &mut compositor::Context| { - let ui = compositor.find::().unwrap(); - ui.last_insert.1.push(InsertEvent::RequestCompletion); - }, - )); - - cx.jobs.callback(async move { - let items = future.await?; - let call = move |editor: &mut Editor, compositor: &mut Compositor| { - let (view, doc) = current_ref!(editor); - // check if the completion request is stale. - // - // Completions are completed asynchronously and therefore the user could - //switch document/view or leave insert mode. In all of thoise cases the - // completion should be discarded - if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc { - return; - } - - if items.is_empty() { - // editor.set_error("No completion available"); - return; - } - let size = compositor.size(); - let ui = compositor.find::().unwrap(); - let completion_area = ui.set_completion( - editor, - savepoint, - items, - start_offset, - trigger_offset, - size, - ); - let size = compositor.size(); - let signature_help_area = compositor - .find_id::>(SignatureHelp::ID) - .map(|signature_help| signature_help.area(size, editor)); - // Delete the signature help popup if they intersect. - if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) - { - compositor.remove(SignatureHelp::ID); - } - }; - Ok(Callback::EditorCompositor(Box::new(call))) - }); + cx.editor + .handlers + .trigger_completions(cursor, doc.id(), view.id); } // comments @@ -4833,10 +4596,6 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) { ); doc.set_selection(view.id, selection); - - // [TODO] temporary workaround until we're not using the idle timer to - // trigger auto completions any more - editor.clear_idle_timer(); } }; diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 051cdcd35..de2f0e5ec 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,4 +1,4 @@ -use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt}; +use futures_util::{stream::FuturesUnordered, FutureExt}; use helix_lsp::{ block_on, lsp::{ @@ -8,21 +8,21 @@ use helix_lsp::{ util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, Client, OffsetEncoding, }; -use serde_json::Value; use tokio_stream::StreamExt; use tui::{ text::{Span, Spans}, widgets::Row, }; -use super::{align_view, push_jump, Align, Context, Editor, Open}; +use super::{align_view, push_jump, Align, Context, Editor}; use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection}; use helix_stdx::path; use helix_view::{ - document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, + document::{DocumentInlayHints, DocumentInlayHintsId}, editor::Action, graphics::Margin, + handlers::lsp::SignatureHelpInvoked, theme::Style, Document, View, }; @@ -30,10 +30,7 @@ use helix_view::{ use crate::{ compositor::{self, Compositor}, job::Callback, - ui::{ - self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, - PromptEvent, - }, + ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent}, }; use std::{ @@ -42,7 +39,6 @@ use std::{ fmt::Write, future::Future, path::PathBuf, - sync::Arc, }; /// Gets the first language server that is attached to a document which supports a specific feature. @@ -1132,146 +1128,10 @@ pub fn goto_reference(cx: &mut Context) { ); } -#[derive(PartialEq, Eq, Clone, Copy)] -pub enum SignatureHelpInvoked { - Manual, - Automatic, -} - pub fn signature_help(cx: &mut Context) { - signature_help_impl(cx, SignatureHelpInvoked::Manual) -} - -pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { - let (view, doc) = current!(cx.editor); - - // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it - let future = doc - .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .find_map(|language_server| { - let pos = doc.position(view.id, language_server.offset_encoding()); - language_server.text_document_signature_help(doc.identifier(), pos, None) - }); - - let Some(future) = future else { - // Do not show the message if signature help was invoked - // automatically on backspace, trigger characters, etc. - if invoked == SignatureHelpInvoked::Manual { - cx.editor - .set_error("No configured language server supports signature-help"); - } - return; - }; - signature_help_impl_with_future(cx, future.boxed(), invoked); -} - -pub fn signature_help_impl_with_future( - cx: &mut Context, - future: BoxFuture<'static, helix_lsp::Result>, - invoked: SignatureHelpInvoked, -) { - cx.callback( - future, - move |editor, compositor, response: Option| { - let config = &editor.config(); - - if !(config.lsp.auto_signature_help - || SignatureHelp::visible_popup(compositor).is_some() - || invoked == SignatureHelpInvoked::Manual) - { - return; - } - - // If the signature help invocation is automatic, don't show it outside of Insert Mode: - // it very probably means the server was a little slow to respond and the user has - // already moved on to something else, making a signature help popup will just be an - // annoyance, see https://github.com/helix-editor/helix/issues/3112 - if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert { - return; - } - - let response = match response { - // According to the spec the response should be None if there - // are no signatures, but some servers don't follow this. - Some(s) if !s.signatures.is_empty() => s, - _ => { - compositor.remove(SignatureHelp::ID); - return; - } - }; - let doc = doc!(editor); - let language = doc.language_name().unwrap_or(""); - - let signature = match response - .signatures - .get(response.active_signature.unwrap_or(0) as usize) - { - Some(s) => s, - None => return, - }; - let mut contents = SignatureHelp::new( - signature.label.clone(), - language.to_string(), - Arc::clone(&editor.syn_loader), - ); - - let signature_doc = if config.lsp.display_signature_help_docs { - signature.documentation.as_ref().map(|doc| match doc { - lsp::Documentation::String(s) => s.clone(), - lsp::Documentation::MarkupContent(markup) => markup.value.clone(), - }) - } else { - None - }; - - contents.set_signature_doc(signature_doc); - - let active_param_range = || -> Option<(usize, usize)> { - let param_idx = signature - .active_parameter - .or(response.active_parameter) - .unwrap_or(0) as usize; - let param = signature.parameters.as_ref()?.get(param_idx)?; - match ¶m.label { - lsp::ParameterLabel::Simple(string) => { - let start = signature.label.find(string.as_str())?; - Some((start, start + string.len())) - } - lsp::ParameterLabel::LabelOffsets([start, end]) => { - // LS sends offsets based on utf-16 based string representation - // but highlighting in helix is done using byte offset. - use helix_core::str_utils::char_to_byte_idx; - let from = char_to_byte_idx(&signature.label, *start as usize); - let to = char_to_byte_idx(&signature.label, *end as usize); - Some((from, to)) - } - } - }; - contents.set_active_param_range(active_param_range()); - - let old_popup = compositor.find_id::>(SignatureHelp::ID); - let mut popup = Popup::new(SignatureHelp::ID, contents) - .position(old_popup.and_then(|p| p.get_position())) - .position_bias(Open::Above) - .ignore_escape_key(true); - - // Don't create a popup if it intersects the auto-complete menu. - let size = compositor.size(); - if compositor - .find::() - .unwrap() - .completion - .as_mut() - .map(|completion| completion.area(size, editor)) - .filter(|area| area.intersects(popup.area(size, editor))) - .is_some() - { - return; - } - - compositor.replace_or_push(SignatureHelp::ID, popup); - }, - ); + cx.editor + .handlers + .trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor) } pub fn hover(cx: &mut Context) { diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index ab2d724ff..ef5369f85 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -1,15 +1,30 @@ use std::sync::Arc; use arc_swap::ArcSwap; +use helix_event::AsyncHook; use crate::config::Config; use crate::events; +use crate::handlers::completion::CompletionHandler; +use crate::handlers::signature_help::SignatureHelpHandler; +pub use completion::trigger_auto_completion; +pub use helix_view::handlers::lsp::SignatureHelpInvoked; +pub use helix_view::handlers::Handlers; + +mod completion; +mod signature_help; - } pub fn setup(config: Arc>) -> Handlers { events::register(); + + let completions = CompletionHandler::new(config).spawn(); + let signature_hints = SignatureHelpHandler::new().spawn(); let handlers = Handlers { + completions, + signature_hints, }; + completion::register_hooks(&handlers); + signature_help::register_hooks(&handlers); handlers } diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs new file mode 100644 index 000000000..d71fd24fc --- /dev/null +++ b/helix-term/src/handlers/completion.rs @@ -0,0 +1,465 @@ +use std::collections::HashSet; +use std::sync::Arc; +use std::time::Duration; + +use arc_swap::ArcSwap; +use futures_util::stream::FuturesUnordered; +use helix_core::chars::char_is_word; +use helix_core::syntax::LanguageServerFeature; +use helix_event::{ + cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx, +}; +use helix_lsp::lsp; +use helix_lsp::util::pos_to_lsp_pos; +use helix_stdx::rope::RopeSliceExt; +use helix_view::document::{Mode, SavePoint}; +use helix_view::handlers::lsp::CompletionEvent; +use helix_view::{DocumentId, Editor, ViewId}; +use tokio::sync::mpsc::Sender; +use tokio::time::Instant; +use tokio_stream::StreamExt; + +use crate::commands; +use crate::compositor::Compositor; +use crate::config::Config; +use crate::events::{OnModeSwitch, PostCommand, PostInsertChar}; +use crate::job::{dispatch, dispatch_blocking}; +use crate::keymap::MappableCommand; +use crate::ui::editor::InsertEvent; +use crate::ui::lsp::SignatureHelp; +use crate::ui::{self, CompletionItem, Popup}; + +use super::Handlers; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum TriggerKind { + Auto, + TriggerChar, + Manual, +} + +#[derive(Debug, Clone, Copy)] +struct Trigger { + pos: usize, + view: ViewId, + doc: DocumentId, + kind: TriggerKind, +} + +#[derive(Debug)] +pub(super) struct CompletionHandler { + /// currently active trigger which will cause a + /// completion request after the timeout + trigger: Option, + /// A handle for currently active completion request. + /// This can be used to determine whether the current + /// request is still active (and new triggers should be + /// ignored) and can also be used to abort the current + /// request (by dropping the handle) + request: Option, + config: Arc>, +} + +impl CompletionHandler { + pub fn new(config: Arc>) -> CompletionHandler { + Self { + config, + request: None, + trigger: None, + } + } +} + +impl helix_event::AsyncHook for CompletionHandler { + type Event = CompletionEvent; + + fn handle_event( + &mut self, + event: Self::Event, + _old_timeout: Option, + ) -> Option { + match event { + CompletionEvent::AutoTrigger { + cursor: trigger_pos, + doc, + view, + } => { + // techically it shouldn't be possible to switch views/documents in insert mode + // but people may create weird keymaps/use the mouse so lets be extra careful + if self + .trigger + .as_ref() + .map_or(true, |trigger| trigger.doc != doc || trigger.view != view) + { + self.trigger = Some(Trigger { + pos: trigger_pos, + view, + doc, + kind: TriggerKind::Auto, + }); + } + } + CompletionEvent::TriggerChar { cursor, doc, view } => { + // immediately request completions and drop all auto completion requests + self.request = None; + self.trigger = Some(Trigger { + pos: cursor, + view, + doc, + kind: TriggerKind::TriggerChar, + }); + } + CompletionEvent::ManualTrigger { cursor, doc, view } => { + // immediately request completions and drop all auto completion requests + self.request = None; + self.trigger = Some(Trigger { + pos: cursor, + view, + doc, + kind: TriggerKind::Manual, + }); + // stop debouncing immediately and request the completion + self.finish_debounce(); + return None; + } + CompletionEvent::Cancel => { + self.trigger = None; + self.request = None; + } + CompletionEvent::DeleteText { cursor } => { + // if we deleted the original trigger, abort the completion + if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) { + self.trigger = None; + self.request = None; + } + } + } + self.trigger.map(|trigger| { + // if the current request was closed forget about it + // otherwise immediately restart the completion request + let cancel = self.request.take().map_or(false, |req| !req.is_closed()); + let timeout = if trigger.kind == TriggerKind::Auto && !cancel { + self.config.load().editor.completion_timeout + } else { + // we want almost instant completions for trigger chars + // and restarting completion requests. The small timeout here mainly + // serves to better handle cases where the completion handler + // may fall behind (so multiple events in the channel) and macros + Duration::from_millis(5) + }; + Instant::now() + timeout + }) + } + + fn finish_debounce(&mut self) { + let trigger = self.trigger.take().expect("debounce always has a trigger"); + let (tx, rx) = cancelation(); + self.request = Some(tx); + dispatch_blocking(move |editor, compositor| { + request_completion(trigger, rx, editor, compositor) + }); + } +} + +fn request_completion( + mut trigger: Trigger, + cancel: CancelRx, + editor: &mut Editor, + compositor: &mut Compositor, +) { + let (view, doc) = current!(editor); + + if compositor + .find::() + .unwrap() + .completion + .is_some() + || editor.mode != Mode::Insert + { + return; + } + + let text = doc.text(); + let cursor = doc.selection(view.id).primary().cursor(text.slice(..)); + if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos { + return; + } + // this looks odd... Why are we not using the trigger position from + // the `trigger` here? Won't that mean that the trigger char doesn't get + // send to the LS if we type fast enougn? Yes that is true but it's + // not actually a problem. The LSP will resolve the completion to the identifier + // anyway (in fact sending the later position is necessary to get the right results + // from LSPs that provide incomplete completion list). We rely on trigger offset + // and primary cursor matching for multi-cursor completions so this is definitely + // necessary from our side too. + trigger.pos = cursor; + let trigger_text = text.slice(..cursor); + + let mut seen_language_servers = HashSet::new(); + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .filter(|ls| seen_language_servers.insert(ls.id())) + .map(|ls| { + let language_server_id = ls.id(); + let offset_encoding = ls.offset_encoding(); + let pos = pos_to_lsp_pos(text, cursor, offset_encoding); + let doc_id = doc.identifier(); + let context = if trigger.kind == TriggerKind::Manual { + lsp::CompletionContext { + trigger_kind: lsp::CompletionTriggerKind::INVOKED, + trigger_character: None, + } + } else { + let trigger_char = + ls.capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider + .trigger_characters + .as_deref()? + .iter() + .find(|&trigger| trigger_text.ends_with(trigger)) + }); + lsp::CompletionContext { + trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: trigger_char.cloned(), + } + }; + + let completion_response = ls.completion(doc_id, pos, None, context).unwrap(); + async move { + let json = completion_response.await?; + let response: Option = serde_json::from_value(json)?; + let items = match response { + Some(lsp::CompletionResponse::Array(items)) => items, + // TODO: do something with is_incomplete + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: _is_incomplete, + items, + })) => items, + None => Vec::new(), + } + .into_iter() + .map(|item| CompletionItem { + item, + language_server_id, + resolved: false, + }) + .collect(); + anyhow::Ok(items) + } + }) + .collect(); + + let future = async move { + let mut items = Vec::new(); + while let Some(lsp_items) = futures.next().await { + match lsp_items { + Ok(mut lsp_items) => items.append(&mut lsp_items), + Err(err) => { + log::debug!("completion request failed: {err:?}"); + } + }; + } + items + }; + + let savepoint = doc.savepoint(view); + + let ui = compositor.find::().unwrap(); + ui.last_insert.1.push(InsertEvent::RequestCompletion); + tokio::spawn(async move { + let items = cancelable_future(future, cancel).await.unwrap_or_default(); + if items.is_empty() { + return; + } + dispatch(move |editor, compositor| { + show_completion(editor, compositor, items, trigger, savepoint) + }) + .await + }); +} + +fn show_completion( + editor: &mut Editor, + compositor: &mut Compositor, + items: Vec, + trigger: Trigger, + savepoint: Arc, +) { + let (view, doc) = current_ref!(editor); + // check if the completion request is stale. + // + // Completions are completed asynchronously and therefore the user could + //switch document/view or leave insert mode. In all of thoise cases the + // completion should be discarded + if editor.mode != Mode::Insert || view.id != trigger.view || doc.id() != trigger.doc { + return; + } + + let size = compositor.size(); + let ui = compositor.find::().unwrap(); + if ui.completion.is_some() { + return; + } + + let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size); + let signature_help_area = compositor + .find_id::>(SignatureHelp::ID) + .map(|signature_help| signature_help.area(size, editor)); + // Delete the signature help popup if they intersect. + if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) { + compositor.remove(SignatureHelp::ID); + } +} + +pub fn trigger_auto_completion( + tx: &Sender, + editor: &Editor, + trigger_char_only: bool, +) { + let config = editor.config.load(); + if !config.auto_completion { + return; + } + let (view, doc): (&helix_view::View, &helix_view::Document) = current_ref!(editor); + let mut text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + text = doc.text().slice(..cursor); + + let is_trigger_char = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .any(|ls| { + matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), + .. + }) if triggers.iter().any(|trigger| text.ends_with(trigger))) + }); + if is_trigger_char { + send_blocking( + tx, + CompletionEvent::TriggerChar { + cursor, + doc: doc.id(), + view: view.id, + }, + ); + return; + } + + let is_auto_trigger = !trigger_char_only + && doc + .text() + .chars_at(cursor) + .reversed() + .take(config.completion_trigger_len as usize) + .all(char_is_word); + + if is_auto_trigger { + send_blocking( + tx, + CompletionEvent::AutoTrigger { + cursor, + doc: doc.id(), + view: view.id, + }, + ); + } +} + +fn update_completions(cx: &mut commands::Context, c: Option) { + cx.callback.push(Box::new(move |compositor, cx| { + let editor_view = compositor.find::().unwrap(); + if let Some(completion) = &mut editor_view.completion { + completion.update_filter(c); + if completion.is_empty() { + editor_view.clear_completion(cx.editor); + // clearing completions might mean we want to immediately rerequest them (usually + // this occurs if typing a trigger char) + if c.is_some() { + trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false); + } + } + } + })) +} + +fn clear_completions(cx: &mut commands::Context) { + cx.callback.push(Box::new(|compositor, cx| { + let editor_view = compositor.find::().unwrap(); + editor_view.clear_completion(cx.editor); + })) +} + +fn completion_post_command_hook( + tx: &Sender, + PostCommand { command, cx }: &mut PostCommand<'_, '_>, +) -> anyhow::Result<()> { + if cx.editor.mode == Mode::Insert { + if cx.editor.last_completion.is_some() { + match command { + MappableCommand::Static { + name: "delete_word_forward" | "delete_char_forward" | "completion", + .. + } => (), + MappableCommand::Static { + name: "delete_char_backward", + .. + } => update_completions(cx, None), + _ => clear_completions(cx), + } + } else { + let event = match command { + MappableCommand::Static { + name: "delete_char_backward" | "delete_word_forward" | "delete_char_forward", + .. + } => { + let (view, doc) = current!(cx.editor); + let primary_cursor = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + CompletionEvent::DeleteText { + cursor: primary_cursor, + } + } + // hacks: some commands are handeled elsewhere and we don't want to + // cancel in that case + MappableCommand::Static { + name: "completion" | "insert_mode" | "append_mode", + .. + } => return Ok(()), + _ => CompletionEvent::Cancel, + }; + send_blocking(tx, event); + } + } + Ok(()) +} + +pub(super) fn register_hooks(handlers: &Handlers) { + let tx = handlers.completions.clone(); + register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event)); + + let tx = handlers.completions.clone(); + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + if event.old_mode == Mode::Insert { + send_blocking(&tx, CompletionEvent::Cancel); + clear_completions(event.cx); + } else if event.new_mode == Mode::Insert { + trigger_auto_completion(&tx, event.cx.editor, false) + } + Ok(()) + }); + + let tx = handlers.completions.clone(); + register_hook!(move |event: &mut PostInsertChar<'_, '_>| { + if event.cx.editor.last_completion.is_some() { + update_completions(event.cx, Some(event.c)) + } else { + trigger_auto_completion(&tx, event.cx.editor, false); + } + Ok(()) + }); +} diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs new file mode 100644 index 000000000..3c746548a --- /dev/null +++ b/helix-term/src/handlers/signature_help.rs @@ -0,0 +1,335 @@ +use std::sync::Arc; +use std::time::Duration; + +use helix_core::syntax::LanguageServerFeature; +use helix_event::{ + cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx, +}; +use helix_lsp::lsp; +use helix_stdx::rope::RopeSliceExt; +use helix_view::document::Mode; +use helix_view::events::{DocumentDidChange, SelectionDidChange}; +use helix_view::handlers::lsp::{SignatureHelpEvent, SignatureHelpInvoked}; +use helix_view::Editor; +use tokio::sync::mpsc::Sender; +use tokio::time::Instant; + +use crate::commands::Open; +use crate::compositor::Compositor; +use crate::events::{OnModeSwitch, PostInsertChar}; +use crate::handlers::Handlers; +use crate::ui::lsp::SignatureHelp; +use crate::ui::Popup; +use crate::{job, ui}; + +#[derive(Debug)] +enum State { + Open, + Closed, + Pending { request: CancelTx }, +} + +/// debounce timeout in ms, value taken from VSCode +/// TODO: make this configurable? +const TIMEOUT: u64 = 120; + +#[derive(Debug)] +pub(super) struct SignatureHelpHandler { + trigger: Option, + state: State, +} + +impl SignatureHelpHandler { + pub fn new() -> SignatureHelpHandler { + SignatureHelpHandler { + trigger: None, + state: State::Closed, + } + } +} + +impl helix_event::AsyncHook for SignatureHelpHandler { + type Event = SignatureHelpEvent; + + fn handle_event( + &mut self, + event: Self::Event, + timeout: Option, + ) -> Option { + match event { + SignatureHelpEvent::Invoked => { + self.trigger = Some(SignatureHelpInvoked::Manual); + self.state = State::Closed; + self.finish_debounce(); + return None; + } + SignatureHelpEvent::Trigger => {} + SignatureHelpEvent::ReTrigger => { + // don't retrigger if we aren't open/pending yet + if matches!(self.state, State::Closed) { + return timeout; + } + } + SignatureHelpEvent::Cancel => { + self.state = State::Closed; + return None; + } + SignatureHelpEvent::RequestComplete { open } => { + // don't cancel rerequest that was already triggered + if let State::Pending { request } = &self.state { + if !request.is_closed() { + return timeout; + } + } + self.state = if open { State::Open } else { State::Closed }; + return timeout; + } + } + if self.trigger.is_none() { + self.trigger = Some(SignatureHelpInvoked::Automatic) + } + Some(Instant::now() + Duration::from_millis(TIMEOUT)) + } + + fn finish_debounce(&mut self) { + let invocation = self.trigger.take().unwrap(); + let (tx, rx) = cancelation(); + self.state = State::Pending { request: tx }; + job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx)) + } +} + +pub fn request_signature_help( + editor: &mut Editor, + invoked: SignatureHelpInvoked, + cancel: CancelRx, +) { + let (view, doc) = current!(editor); + + // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it + let future = doc + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .find_map(|language_server| { + let pos = doc.position(view.id, language_server.offset_encoding()); + language_server.text_document_signature_help(doc.identifier(), pos, None) + }); + + let Some(future) = future else { + // 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"); + } + return; + }; + + tokio::spawn(async move { + match cancelable_future(future, cancel).await { + Some(Ok(res)) => { + job::dispatch(move |editor, compositor| { + show_signature_help(editor, compositor, invoked, res) + }) + .await + } + Some(Err(err)) => log::error!("signature help request failed: {err}"), + None => (), + } + }); +} + +pub fn show_signature_help( + editor: &mut Editor, + compositor: &mut Compositor, + invoked: SignatureHelpInvoked, + response: Option, +) { + let config = &editor.config(); + + if !(config.lsp.auto_signature_help + || SignatureHelp::visible_popup(compositor).is_some() + || invoked == SignatureHelpInvoked::Manual) + { + return; + } + + // If the signature help invocation is automatic, don't show it outside of Insert Mode: + // it very probably means the server was a little slow to respond and the user has + // already moved on to something else, making a signature help popup will just be an + // annoyance, see https://github.com/helix-editor/helix/issues/3112 + // For the most part this should not be needed as the request gets canceled automatically now + // but it's technically possible for the mode change to just preempt this callback so better safe than sorry + if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert { + return; + } + + let response = match response { + // According to the spec the response should be None if there + // are no signatures, but some servers don't follow this. + Some(s) if !s.signatures.is_empty() => s, + _ => { + send_blocking( + &editor.handlers.signature_hints, + SignatureHelpEvent::RequestComplete { open: false }, + ); + compositor.remove(SignatureHelp::ID); + return; + } + }; + send_blocking( + &editor.handlers.signature_hints, + SignatureHelpEvent::RequestComplete { open: true }, + ); + + let doc = doc!(editor); + let language = doc.language_name().unwrap_or(""); + + let signature = match response + .signatures + .get(response.active_signature.unwrap_or(0) as usize) + { + Some(s) => s, + None => return, + }; + let mut contents = SignatureHelp::new( + signature.label.clone(), + language.to_string(), + Arc::clone(&editor.syn_loader), + ); + + let signature_doc = if config.lsp.display_signature_help_docs { + signature.documentation.as_ref().map(|doc| match doc { + lsp::Documentation::String(s) => s.clone(), + lsp::Documentation::MarkupContent(markup) => markup.value.clone(), + }) + } else { + None + }; + + contents.set_signature_doc(signature_doc); + + let active_param_range = || -> Option<(usize, usize)> { + let param_idx = signature + .active_parameter + .or(response.active_parameter) + .unwrap_or(0) as usize; + let param = signature.parameters.as_ref()?.get(param_idx)?; + match ¶m.label { + lsp::ParameterLabel::Simple(string) => { + let start = signature.label.find(string.as_str())?; + Some((start, start + string.len())) + } + lsp::ParameterLabel::LabelOffsets([start, end]) => { + // LS sends offsets based on utf-16 based string representation + // but highlighting in helix is done using byte offset. + use helix_core::str_utils::char_to_byte_idx; + let from = char_to_byte_idx(&signature.label, *start as usize); + let to = char_to_byte_idx(&signature.label, *end as usize); + Some((from, to)) + } + } + }; + contents.set_active_param_range(active_param_range()); + + let old_popup = compositor.find_id::>(SignatureHelp::ID); + let mut popup = Popup::new(SignatureHelp::ID, contents) + .position(old_popup.and_then(|p| p.get_position())) + .position_bias(Open::Above) + .ignore_escape_key(true); + + // Don't create a popup if it intersects the auto-complete menu. + let size = compositor.size(); + if compositor + .find::() + .unwrap() + .completion + .as_mut() + .map(|completion| completion.area(size, editor)) + .filter(|area| area.intersects(popup.area(size, editor))) + .is_some() + { + return; + } + + compositor.replace_or_push(SignatureHelp::ID, popup); +} + +fn signature_help_post_insert_char_hook( + tx: &Sender, + PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>, +) -> anyhow::Result<()> { + if !cx.editor.config().lsp.auto_signature_help { + return Ok(()); + } + 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(()); + }; + + let capabilities = language_server.capabilities(); + + if let lsp::ServerCapabilities { + signature_help_provider: + Some(lsp::SignatureHelpOptions { + trigger_characters: Some(triggers), + // TODO: retrigger_characters + .. + }), + .. + } = capabilities + { + let mut text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + text = text.slice(..cursor); + if triggers.iter().any(|trigger| text.ends_with(trigger)) { + send_blocking(tx, SignatureHelpEvent::Trigger) + } + } + Ok(()) +} + +pub(super) fn register_hooks(handlers: &Handlers) { + let tx = handlers.signature_hints.clone(); + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + match (event.old_mode, event.new_mode) { + (Mode::Insert, _) => { + send_blocking(&tx, SignatureHelpEvent::Cancel); + event.cx.callback.push(Box::new(|compositor, _| { + compositor.remove(SignatureHelp::ID); + })); + } + (_, Mode::Insert) => { + if event.cx.editor.config().lsp.auto_signature_help { + send_blocking(&tx, SignatureHelpEvent::Trigger); + } + } + _ => (), + } + Ok(()) + }); + + let tx = handlers.signature_hints.clone(); + register_hook!( + move |event: &mut PostInsertChar<'_, '_>| signature_help_post_insert_char_hook(&tx, event) + ); + + let tx = handlers.signature_hints.clone(); + register_hook!(move |event: &mut DocumentDidChange<'_>| { + if event.doc.config.load().lsp.auto_signature_help { + send_blocking(&tx, SignatureHelpEvent::ReTrigger); + } + Ok(()) + }); + + let tx = handlers.signature_hints.clone(); + register_hook!(move |event: &mut SelectionDidChange<'_>| { + if event.doc.config.load().lsp.auto_signature_help { + send_blocking(&tx, SignatureHelpEvent::ReTrigger); + } + Ok(()) + }); +} diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 7c6a0055e..48d97fbd8 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,8 +1,12 @@ -use crate::compositor::{Component, Context, Event, EventResult}; +use crate::{ + compositor::{Component, Context, Event, EventResult}, + handlers::trigger_auto_completion, +}; use helix_view::{ document::SavePoint, editor::CompleteAction, graphics::Margin, + handlers::lsp::SignatureHelpInvoked, theme::{Modifier, Style}, ViewId, }; @@ -10,7 +14,7 @@ use tui::{buffer::Buffer as Surface, text::Span}; use std::{borrow::Cow, sync::Arc}; -use helix_core::{Change, Transaction}; +use helix_core::{chars, Change, Transaction}; use helix_view::{graphics::Rect, Document, Editor}; use crate::commands; @@ -95,10 +99,9 @@ pub struct CompletionItem { /// Wraps a Menu. pub struct Completion { popup: Popup>, - start_offset: usize, #[allow(dead_code)] trigger_offset: usize, - // TODO: maintain a completioncontext with trigger kind & trigger char + filter: String, } impl Completion { @@ -108,7 +111,6 @@ impl Completion { editor: &Editor, savepoint: Arc, mut items: Vec, - start_offset: usize, trigger_offset: usize, ) -> Self { let preview_completion_insert = editor.config().preview_completion_insert; @@ -246,7 +248,7 @@ impl Completion { // (also without sending the transaction to the LS) *before any further transaction is applied*. // Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction // is applied to). - if editor.last_completion.is_none() { + if matches!(editor.last_completion, Some(CompleteAction::Triggered)) { editor.last_completion = Some(CompleteAction::Selected { savepoint: doc.savepoint(view), }) @@ -324,8 +326,18 @@ impl Completion { doc.apply(&transaction, view.id); } } + // we could have just inserted a trigger char (like a `crate::` completion for rust + // so we want to retrigger immediately when accepting a completion. + trigger_auto_completion(&editor.handlers.completions, editor, true); } }; + + // In case the popup was deleted because of an intersection w/ the auto-complete menu. + if event != PromptEvent::Update { + editor + .handlers + .trigger_signature_help(SignatureHelpInvoked::Automatic, editor); + } }); let margin = if editor.menu_border() { @@ -339,14 +351,30 @@ impl Completion { .ignore_escape_key(true) .margin(margin); + let (view, doc) = current_ref!(editor); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + let offset = text + .chars_at(cursor) + .reversed() + .take_while(|ch| chars::char_is_word(*ch)) + .count(); + let start_offset = cursor.saturating_sub(offset); + + let fragment = doc.text().slice(start_offset..cursor); let mut completion = Self { popup, - start_offset, trigger_offset, + // TODO: expand nucleo api to allow moving straight to a Utf32String here + // and avoid allocation during matching + filter: String::from(fragment), }; // need to recompute immediately in case start_offset != trigger_offset - completion.recompute_filter(editor); + completion + .popup + .contents_mut() + .score(&completion.filter, false); completion } @@ -366,39 +394,22 @@ impl Completion { } } - pub fn recompute_filter(&mut self, editor: &Editor) { + /// Appends (`c: Some(c)`) or removes (`c: None`) a character to/from the filter + /// this should be called whenever the user types or deletes a character in insert mode. + pub fn update_filter(&mut self, c: Option) { // recompute menu based on matches let menu = self.popup.contents_mut(); - let (view, doc) = current_ref!(editor); - - // cx.hooks() - // cx.add_hook(enum type, ||) - // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view - // callback with editor & compositor - // - // trigger_hook sends event into channel, that's consumed in the global loop and - // triggers all registered callbacks - // TODO: hooks should get processed immediately so maybe do it after select!(), before - // looping? - - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - if self.trigger_offset <= cursor { - let fragment = doc.text().slice(self.start_offset..cursor); - let text = Cow::from(fragment); - // TODO: logic is same as ui/picker - menu.score(&text); - } else { - // we backspaced before the start offset, clear the menu - // this will cause the editor to remove the completion popup - menu.clear(); + match c { + Some(c) => self.filter.push(c), + None => { + self.filter.pop(); + if self.filter.is_empty() { + menu.clear(); + return; + } + } } - } - - pub fn update(&mut self, cx: &mut commands::Context) { - self.recompute_filter(cx.editor) + menu.score(&self.filter, c.is_some()); } pub fn is_empty(&self) -> bool { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9f186d148..fef62a292 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,7 +1,6 @@ use crate::{ commands::{self, OnKeyCallback}, compositor::{Component, Context, Event, EventResult}, - job::{self, Callback}, events::{OnModeSwitch, PostCommand}, key, keymap::{KeymapResult, Keymaps}, @@ -34,8 +33,8 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc}; use tui::{buffer::Buffer as Surface, text::Span}; +use super::document::LineDecoration; use super::{completion::CompletionItem, statusline}; -use super::{document::LineDecoration, lsp::SignatureHelp}; pub struct EditorView { pub keymaps: Keymaps, @@ -837,11 +836,8 @@ impl EditorView { let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); helix_event::dispatch(PostCommand { command, cx: cxt }); + let current_mode = cxt.editor.mode(); - match (last_mode, current_mode) { - (Mode::Normal, Mode::Insert) => { - // HAXX: if we just entered insert mode from normal, clear key buf - // and record the command that got us into this mode. if current_mode != last_mode { helix_event::dispatch(OnModeSwitch { old_mode: last_mode, @@ -849,29 +845,16 @@ impl EditorView { cx: cxt, }); + // HAXX: if we just entered insert mode from normal, clear key buf + // and record the command that got us into this mode. + if current_mode == Mode::Insert { // how we entered insert mode is important, and we should track that so // we can repeat the side effect. self.last_insert.0 = command.clone(); self.last_insert.1.clear(); - - commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic); - } - (Mode::Insert, Mode::Normal) => { - // if exiting insert mode, remove completion - self.clear_completion(cxt.editor); - cxt.editor.completion_request_handle = None; - - // TODO: Use an on_mode_change hook to remove signature help - cxt.jobs.callback(async { - let call: job::Callback = - Callback::EditorCompositor(Box::new(|_editor, compositor| { - compositor.remove(SignatureHelp::ID); - })); - Ok(call) - }); } - _ => (), } + last_mode = current_mode; }; @@ -999,12 +982,10 @@ impl EditorView { editor: &mut Editor, savepoint: Arc, items: Vec, - start_offset: usize, trigger_offset: usize, size: Rect, ) -> Option { - let mut completion = - Completion::new(editor, savepoint, items, start_offset, trigger_offset); + let mut completion = Completion::new(editor, savepoint, items, trigger_offset); if completion.is_empty() { // skip if we got no completion results @@ -1025,6 +1006,7 @@ impl EditorView { self.completion = None; if let Some(last_completion) = editor.last_completion.take() { match last_completion { + CompleteAction::Triggered => (), CompleteAction::Applied { trigger_offset, changes, @@ -1038,9 +1020,6 @@ impl EditorView { } } } - - // Clear any savepoints - editor.clear_idle_timer(); // don't retrigger } pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult { @@ -1054,13 +1033,7 @@ impl EditorView { }; } - if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion { - return EventResult::Ignored(None); - } - - crate::commands::insert::idle_completion(cx); - - EventResult::Consumed(None) + EventResult::Ignored(None) } } @@ -1346,12 +1319,6 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn self.clear_completion(cx.editor); - - // In case the popup was deleted because of an intersection w/ the auto-complete menu. - commands::signature_help_impl( - &mut cx, - commands::SignatureHelpInvoked::Automatic, - ); } } } @@ -1362,14 +1329,6 @@ impl Component for EditorView { // record last_insert key self.last_insert.1.push(InsertEvent::Key(key)); - - // lastly we recalculate completion - if let Some(completion) = &mut self.completion { - completion.update(&mut cx); - if completion.is_empty() { - self.clear_completion(cx.editor); - } - } } } mode => self.command_mode(mode, &mut cx, key), diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 0ee64ce9e..64127e3af 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -96,20 +96,34 @@ impl Menu { } } - pub fn score(&mut self, pattern: &str) { - // reuse the matches allocation - self.matches.clear(); + 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 mut buf = Vec::new(); - let matches = self.options.iter().enumerate().filter_map(|(i, option)| { - let text = option.filter_text(&self.editor_data); - pattern - .score(Utf32Str::new(&text, &mut buf), &mut matcher) - .map(|score| (i as u32, score as u32)) - }); - self.matches.extend(matches); + if incremental { + self.matches.retain_mut(|(index, score)| { + let option = &self.options[*index as usize]; + let text = option.filter_text(&self.editor_data); + let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher); + match new_score { + Some(new_score) => { + *score = new_score as u32; + true + } + None => false, + } + }) + } else { + self.matches.clear(); + let matches = self.options.iter().enumerate().filter_map(|(i, option)| { + let text = option.filter_text(&self.editor_data); + pattern + .score(Utf32Str::new(&text, &mut buf), &mut matcher) + .map(|score| (i as u32, score as u32)) + }); + self.matches.extend(matches); + } self.matches .sort_unstable_by_key(|&(i, score)| (Reverse(score), i)); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 93b83da4f..388810b13 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -115,19 +115,6 @@ pub struct SavePoint { /// The view this savepoint is associated with pub view: ViewId, revert: Mutex, - pub text: Rope, -} - -impl SavePoint { - pub fn cursor(&self) -> usize { - // we always create transactions with selections - self.revert - .lock() - .selection() - .unwrap() - .primary() - .cursor(self.text.slice(..)) - } } pub struct Document { @@ -1404,7 +1391,6 @@ impl Document { let savepoint = Arc::new(SavePoint { view: view.id, revert: Mutex::new(revert), - text: self.text.clone(), }); self.savepoints.push(Arc::downgrade(&savepoint)); savepoint diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 44c706d7d..dc10a6044 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -31,10 +31,7 @@ use std::{ }; use tokio::{ - sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - oneshot, - }, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, time::{sleep, Duration, Instant, Sleep}, }; @@ -244,12 +241,19 @@ pub struct Config { /// Set a global text_width pub text_width: usize, /// Time in milliseconds since last keypress before idle timers trigger. - /// Used for autocompletion, set to 0 for instant. Defaults to 250ms. + /// Used for various UI timeouts. Defaults to 250ms. #[serde( serialize_with = "serialize_duration_millis", deserialize_with = "deserialize_duration_millis" )] pub idle_timeout: Duration, + /// Time in milliseconds after typing a word character before auto completions + /// are shown, set to 5 for instant. Defaults to 250ms. + #[serde( + serialize_with = "serialize_duration_millis", + deserialize_with = "deserialize_duration_millis" + )] + pub completion_timeout: Duration, /// Whether to insert the completion suggestion on hover. Defaults to true. pub preview_completion_insert: bool, pub completion_trigger_len: u8, @@ -829,6 +833,7 @@ impl Default for Config { auto_format: true, auto_save: false, idle_timeout: Duration::from_millis(250), + completion_timeout: Duration::from_millis(250), preview_completion_insert: true, completion_trigger_len: 2, auto_info: true, @@ -953,14 +958,6 @@ pub struct Editor { /// avoid calculating the cursor position multiple /// times during rendering and should not be set by other functions. pub cursor_cache: Cell>>, - /// When a new completion request is sent to the server old - /// unfinished request must be dropped. Each completion - /// request is associated with a channel that cancels - /// when the channel is dropped. That channel is stored - /// here. When a new completion request is sent this - /// field is set and any old requests are automatically - /// canceled as a result - pub completion_request_handle: Option>, pub handlers: Handlers, } @@ -989,13 +986,16 @@ enum ThemeAction { #[derive(Debug, Clone)] pub enum CompleteAction { + Triggered, + /// A savepoint of the currently selected completion. The savepoint + /// MUST be restored before sending any event to the LSP + Selected { + savepoint: Arc, + }, Applied { trigger_offset: usize, changes: Vec, }, - /// A savepoint of the currently selected completion. The savepoint - /// MUST be restored before sending any event to the LSP - Selected { savepoint: Arc }, } #[derive(Debug, Copy, Clone)] @@ -1029,6 +1029,7 @@ impl Editor { theme_loader: Arc, syn_loader: Arc, config: Arc>, + handlers: Handlers, ) -> Self { let language_servers = helix_lsp::Registry::new(syn_loader.clone()); let conf = config.load(); @@ -1073,7 +1074,7 @@ impl Editor { config_events: unbounded_channel(), needs_redraw: false, cursor_cache: Cell::new(None), - completion_request_handle: None, + handlers, } } diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs index ae3eb545a..724e7b192 100644 --- a/helix-view/src/handlers.rs +++ b/helix-view/src/handlers.rs @@ -1,12 +1,41 @@ -use std::sync::Arc; - use helix_event::send_blocking; use tokio::sync::mpsc::Sender; use crate::handlers::lsp::SignatureHelpInvoked; -use crate::Editor; +use crate::{DocumentId, Editor, ViewId}; pub mod dap; pub mod lsp; -pub struct Handlers {} +pub struct Handlers { + // only public because most of the actual implementation is in helix-term right now :/ + pub completions: Sender, + pub signature_hints: Sender, +} + +impl Handlers { + /// Manually trigger completion (c-x) + pub fn trigger_completions(&self, trigger_pos: usize, doc: DocumentId, view: ViewId) { + send_blocking( + &self.completions, + lsp::CompletionEvent::ManualTrigger { + cursor: trigger_pos, + doc, + view, + }, + ); + } + + pub fn trigger_signature_help(&self, invocation: SignatureHelpInvoked, editor: &Editor) { + let event = match invocation { + SignatureHelpInvoked::Automatic => { + if !editor.config().lsp.auto_signature_help { + return; + } + lsp::SignatureHelpEvent::Trigger + } + SignatureHelpInvoked::Manual => lsp::SignatureHelpEvent::Invoked, + }; + send_blocking(&self.signature_hints, event) + } +} diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index 958385646..1dae45dd5 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -1,26 +1,27 @@ use crate::{DocumentId, ViewId}; -#[derive(Debug, Clone, Copy)] -pub struct CompletionTrigger { - /// The char position of the primary cursor when the - /// completion was triggered - pub trigger_pos: usize, - pub doc: DocumentId, - pub view: ViewId, - /// Whether the cause of the trigger was an automatic completion (any word - /// char for words longer than minimum word length). - /// This is false for trigger chars send by the LS - pub auto: bool, -} - pub enum CompletionEvent { /// Auto completion was triggered by typing a word char - /// or a completion trigger - Trigger(CompletionTrigger), + AutoTrigger { + cursor: usize, + doc: DocumentId, + view: ViewId, + }, + /// Auto completion was triggered by typing a trigger char + /// specified by the LSP + TriggerChar { + cursor: usize, + doc: DocumentId, + view: ViewId, + }, /// A completion was manually requested (c-x) - Manual, + ManualTrigger { + cursor: usize, + doc: DocumentId, + view: ViewId, + }, /// Some text was deleted and the cursor is now at `pos` - DeleteText { pos: usize }, + DeleteText { cursor: usize }, /// Invalidate the current auto completion trigger Cancel, } From 960cda60abf8eb84a7369079f906a0bc53e8088c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:22:42 +0900 Subject: [PATCH 07/70] build(deps): bump chrono from 0.4.31 to 0.4.32 (#9409) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96496125a..4226e06ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,14 +171,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.48.0", + "windows-targets 0.52.0", ] [[package]] From e85507ccac5ad6771b66d2b85e7de29fa08a5b49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:24:29 +0900 Subject: [PATCH 08/70] build(deps): bump regex from 1.10.2 to 1.10.3 (#9408) 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 4226e06ac..839245f60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1738,9 +1738,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -1750,9 +1750,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", From ae8042bb83e34b7cf085ec8792fcbf529896d998 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:24:38 +0900 Subject: [PATCH 09/70] build(deps): bump bitflags from 2.4.1 to 2.4.2 (#9404) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 839245f60..d9519aa21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bstr" @@ -285,7 +285,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "crossterm_winapi", "filedescriptor", "futures-core", @@ -625,7 +625,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e0be46f4cf1f8f9e88d0e3eb7b29718aff23889563249f379119bd1ab6910e" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "bstr", "gix-path", "libc", @@ -704,7 +704,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae6232f18b262770e343dcdd461c0011c9b9ae27f0c805e115012aa2b902c1b8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "bstr", "gix-features", "gix-path", @@ -907,7 +907,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78f6dce0c6683e2219e8169aac4b1c29e89540a8262fef7056b31d80d969408c" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "gix-path", "libc", "windows 0.52.0", @@ -1053,7 +1053,7 @@ version = "23.10.0" dependencies = [ "ahash", "arc-swap", - "bitflags 2.4.1", + "bitflags 2.4.2", "chrono", "dunce", "encoding_rs", @@ -1216,7 +1216,7 @@ dependencies = [ name = "helix-tui" version = "23.10.0" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cassowary", "crossterm", "helix-core", @@ -1250,7 +1250,7 @@ version = "23.10.0" dependencies = [ "anyhow", "arc-swap", - "bitflags 2.4.1", + "bitflags 2.4.2", "chardetng", "clipboard-win", "crossterm", @@ -1787,7 +1787,7 @@ version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", From 4168864572ac29c64d82031ba9edfbda0a521839 Mon Sep 17 00:00:00 2001 From: Jaakko Paju Date: Tue, 23 Jan 2024 16:19:22 +0200 Subject: [PATCH 10/70] Add bufferline config for onedark & onedarker themes (#9397) --- runtime/themes/onedark.toml | 4 ++++ runtime/themes/onedarker.toml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 1db2aff86..a50b5d354 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -77,6 +77,10 @@ "ui.statusline.insert" = { fg = "light-black", bg = "green" } "ui.statusline.select" = { fg = "light-black", bg = "purple" } +"ui.bufferline" = { fg = "light-gray", bg = "light-black" } +"ui.bufferline.active" = { fg = "light-black", bg = "blue", underline = { color = "light-black", style = "line" } } +"ui.bufferline.background" = { bg = "light-black" } + "ui.text" = { fg = "white" } "ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] } diff --git a/runtime/themes/onedarker.toml b/runtime/themes/onedarker.toml index 88b2871ab..307871445 100644 --- a/runtime/themes/onedarker.toml +++ b/runtime/themes/onedarker.toml @@ -75,6 +75,11 @@ "ui.statusline.normal" = { fg = "light-black", bg = "purple" } "ui.statusline.insert" = { fg = "light-black", bg = "green" } "ui.statusline.select" = { fg = "light-black", bg = "cyan" } + +"ui.bufferline" = { fg = "light-gray", bg = "light-black" } +"ui.bufferline.active" = { fg = "light-black", bg = "blue", underline = { color = "light-black", style = "line" } } +"ui.bufferline.background" = { bg = "light-black" } + "ui.text" = { fg = "white" } "ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] } From cbd8602018efd90506fe684ea58a35dd4b40518f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:19:36 +0900 Subject: [PATCH 11/70] build(deps): bump smallvec from 1.12.0 to 1.13.1 (#9405) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- helix-core/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9519aa21..d3da63613 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1940,9 +1940,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "smartstring" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 42c88f4bc..8c63af8ef 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -20,7 +20,7 @@ helix-stdx = { path = "../helix-stdx" } helix-loader = { path = "../helix-loader" } ropey = { version = "1.6.1", default-features = false, features = ["simd"] } -smallvec = "1.12" +smallvec = "1.13" smartstring = "1.0.1" unicode-segmentation = "1.10" unicode-width = "0.1" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 7bdd433ed..683d6dc53 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -83,6 +83,6 @@ crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] } helix-loader = { path = "../helix-loader" } [dev-dependencies] -smallvec = "1.12" +smallvec = "1.13" indoc = "2.0.4" tempfile = "3.9.0" From 7caae13465dd14ae105c69280bbe40774956eda1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:56:16 +0100 Subject: [PATCH 12/70] build(deps): bump gix from 0.57.1 to 0.58.0 (#9407) Bumps [gix](https://github.com/Byron/gitoxide) from 0.57.1 to 0.58.0. - [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-v0.57.1...gix-v0.58.0) --- updated-dependencies: - dependency-name: gix dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 130 +++++++++++++++++++------------------------ helix-vcs/Cargo.toml | 2 +- 2 files changed, 59 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3da63613..9f62947f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -522,9 +522,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gix" -version = "0.57.1" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd025382892c7b500a9ce1582cd803f9c2ebfe44aff52e9c7f86feee7ced75e" +checksum = "31887c304d9a935f3e5494fb5d6a0106c34e965168ec0db9b457424eedd0c741" dependencies = [ "gix-actor", "gix-commitgraph", @@ -558,14 +558,13 @@ dependencies = [ "parking_lot", "smallvec", "thiserror", - "unicode-normalization", ] [[package]] name = "gix-actor" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da27b5ab4ab5c75ff891dccd48409f8cc53c28a79480f1efdd33184b2dc1d958" +checksum = "0a7bb9fad6125c81372987c06469601d37e1a2d421511adb69971b9083517a8a" dependencies = [ "bstr", "btoi", @@ -586,9 +585,9 @@ dependencies = [ [[package]] name = "gix-commitgraph" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a39c675fd737cb43a2120eddf1aa652c19d76b28d79783a198ac1b398ed9ce6" +checksum = "82dbd7fb959862e3df2583331f0ad032ac93533e8a52f1b0694bc517f5d292bc" dependencies = [ "bstr", "gix-chunk", @@ -600,9 +599,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.33.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367304855b369cadcac4ee5fb5a3a20da9378dd7905106141070b79f85241079" +checksum = "e62bf2073b6ce3921ffa6d8326f645f30eec5fc4a8e8a4bc0fcb721a2f3f69dc" dependencies = [ "bstr", "gix-config-value", @@ -621,9 +620,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e0be46f4cf1f8f9e88d0e3eb7b29718aff23889563249f379119bd1ab6910e" +checksum = "5b8a1e7bfb37a46ed0b8468db37a6d8a0a61d56bdbe4603ae492cb322e5f3958" dependencies = [ "bitflags 2.4.2", "bstr", @@ -646,9 +645,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6a0454f8c42d686f17e7f084057c717c082b7dbb8209729e4e8f26749eb93a" +checksum = "cbdcb5e49c4b9729dd1c361040ae5c3cd7c497b2260b18c954f62db3a63e98cf" dependencies = [ "bstr", "gix-hash", @@ -658,12 +657,13 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d7b2896edc3d899d28a646ccc6df729827a6600e546570b2783466404a42d6" +checksum = "b4669218f3ec0cbbf8f16857b32200890f8ca585f36f5817242e4115fe4551af" dependencies = [ "bstr", "dunce", + "gix-fs", "gix-hash", "gix-path", "gix-ref", @@ -673,14 +673,15 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a80f0fe688d654c2a741751578b11131071026d1934d03c1820d6d767525ce" +checksum = "184f7f7d4e45db0e2a362aeaf12c06c5e84817d0ef91d08e8e90170dad9f0b07" dependencies = [ "crc32fast", "flate2", "gix-hash", "gix-trace", + "gix-utils", "libc", "once_cell", "prodash", @@ -691,18 +692,19 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7555c23a005537434bbfcb8939694e18cad42602961d0de617f8477cc2adecdd" +checksum = "4436e883d5769f9fb18677b8712b49228357815f9e4104174a6fc2d8461a437b" dependencies = [ "gix-features", + "gix-utils", ] [[package]] name = "gix-glob" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6232f18b262770e343dcdd461c0011c9b9ae27f0c805e115012aa2b902c1b8" +checksum = "4965a1d06d0ab84a29d4a67697a97352ab14ae1da821084e5afb1fd6d8191ca0" dependencies = [ "bitflags 2.4.2", "bstr", @@ -733,9 +735,9 @@ dependencies = [ [[package]] name = "gix-lock" -version = "12.0.0" +version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cf112ddee94223c119a8534dad027740dc3aba3365ac5edeef8a7f6660c74db" +checksum = "651e46174dc5e7d18b7b809d31937b6de3681b1debd78618c99162cc30fcf3e1" dependencies = [ "gix-tempfile", "gix-utils", @@ -755,9 +757,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.40.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c89402e8faa41b49fde348665a8f38589e461036475af43b6b70615a6a313a2" +checksum = "693ce9d30741506cb082ef2d8b797415b48e032cce0ab23eff894c19a7e4777b" dependencies = [ "bstr", "btoi", @@ -774,13 +776,14 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.56.1" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ae6da873de41c6c2b73570e82c571b69df5154dcd8f46dfafc6687767c33b1" +checksum = "8ba2fa9e81f2461b78b4d81a807867667326c84cdab48e0aed7b73a593aa1be4" dependencies = [ "arc-swap", "gix-date", "gix-features", + "gix-fs", "gix-hash", "gix-object", "gix-pack", @@ -793,9 +796,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.46.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782b4d42790a14072d5c400deda9851f5765f50fe72bca6dece0da1cd6f05a9a" +checksum = "8da5f3e78c96b76c4e6fe5e8e06b76221e4a0ee9a255aa935ed1fdf68988dfd8" dependencies = [ "clru", "gix-chunk", @@ -813,9 +816,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dd0998ab245f33d40ca2267e58d542fe54185ebd1dc41923346cf28d179fb6" +checksum = "14a6282621aed1becc3f83d64099a564b3b9063f22783d9a87ea502a3e9f2e40" dependencies = [ "bstr", "gix-trace", @@ -837,9 +840,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.40.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d9bd1984638d8f3511a2fcbe84fcedb8a5b5d64df677353620572383f42649" +checksum = "5818958994ad7879fa566f5441ebcc48f0926aa027b28948e6fbf6578894dc31" dependencies = [ "gix-actor", "gix-date", @@ -850,6 +853,7 @@ dependencies = [ "gix-object", "gix-path", "gix-tempfile", + "gix-utils", "gix-validate", "memmap2", "thiserror", @@ -858,9 +862,9 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be219df5092c1735abb2a53eccdf775e945eea6986ee1b6e7a5896dccc0be704" +checksum = "613aa4d93034c5791d13bdc635e530f4ddab1412ddfb4a8215f76213177b61c7" dependencies = [ "bstr", "gix-hash", @@ -872,9 +876,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.25.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa78e1df3633bc937d4db15f8dca2abdb1300ca971c0fabcf9fa97e38cf4cd9f" +checksum = "288f6549d7666db74dc3f169a9a333694fc28ecd2f5aa7b2c979c89eb556751a" dependencies = [ "bstr", "gix-date", @@ -888,9 +892,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702de5fe5c2bbdde80219f3a8b9723eb927466e7ecd187cfd1b45d986408e45f" +checksum = "5b9b4d91dfc5c14fee61a28c65113ded720403b65a0f46169c0460f731a5d03c" dependencies = [ "gix-commitgraph", "gix-date", @@ -903,21 +907,21 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f6dce0c6683e2219e8169aac4b1c29e89540a8262fef7056b31d80d969408c" +checksum = "f8d9bf462feaf05f2121cba7399dbc6c34d88a9cad58fc1e95027791d6a3c6d2" dependencies = [ "bitflags 2.4.2", "gix-path", "libc", - "windows 0.52.0", + "windows-sys 0.52.0", ] [[package]] name = "gix-tempfile" -version = "12.0.0" +version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e76a494bd530e1a1309188ff971825a24f159c76c2db0bf71fa5dfb469a2c915" +checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30" dependencies = [ "gix-fs", "libc", @@ -928,15 +932,15 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e1127ede0475b58f4fe9c0aaa0d9bb0bad2af90bbd93ccd307c8632b863d89" +checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1" [[package]] name = "gix-traverse" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb64213e52e1b726cb04581690c1e98b5910f983b977d5e9f2eb09f1a7fea6d2" +checksum = "bfc30c5b5e4e838683b59e1b0574ce6bc1c35916df9709aaab32bb7751daf08b" dependencies = [ "gix-commitgraph", "gix-date", @@ -950,9 +954,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0f17cceb7552a231d1fec690bc2740c346554e3be6f5d2c41dfa809594dc44" +checksum = "26f1981ecc700f4fd73ae62b9ca2da7c8816c8fd267f0185e3f8c21e967984ac" dependencies = [ "bstr", "gix-features", @@ -964,11 +968,12 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6225e2de30b6e9bca2d9f1cc4731640fcef0fb3cabddceee366e7e85d3e94f" +checksum = "56e839f3d0798b296411263da6bee780a176ef8008a5dfc31287f7eda9266ab8" dependencies = [ "fastrand", + "unicode-normalization", ] [[package]] @@ -1307,7 +1312,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.48.0", + "windows", ] [[package]] @@ -2405,25 +2410,6 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core", - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.0", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index f6dbe076d..051134e4a 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.6.0" } -gix = { version = "0.57.1", default-features = false , optional = true } +gix = { version = "0.58.0", default-features = false , optional = true } imara-diff = "0.1.5" anyhow = "1" From 2058b3732c340f5bc29eee2aca1d8edae15e1314 Mon Sep 17 00:00:00 2001 From: melted-brownie <66597133+melted-brownie@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:27:09 +0000 Subject: [PATCH 13/70] Add text object queries for dart (#9411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add text object queries for dart * Update runtime/queries/dart/textobjects.scm Co-authored-by: Michael Davis * Clean up internal capture name --------- Co-authored-by: Sébastien Blondiau Co-authored-by: Michael Davis --- book/src/generated/lang-support.md | 2 +- runtime/queries/dart/textobjects.scm | 68 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 runtime/queries/dart/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 6fedace63..a78dd7935 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -24,7 +24,7 @@ | css | ✓ | | ✓ | `vscode-css-language-server` | | cue | ✓ | | | `cuelsp` | | d | ✓ | ✓ | ✓ | `serve-d` | -| dart | ✓ | | ✓ | `dart` | +| dart | ✓ | ✓ | ✓ | `dart` | | dbml | ✓ | | | | | devicetree | ✓ | | | | | dhall | ✓ | ✓ | | `dhall-lsp-server` | diff --git a/runtime/queries/dart/textobjects.scm b/runtime/queries/dart/textobjects.scm new file mode 100644 index 000000000..028276156 --- /dev/null +++ b/runtime/queries/dart/textobjects.scm @@ -0,0 +1,68 @@ +(class_definition + body: (_) @class.inside) @class.around + +(mixin_declaration + (class_body) @class.inside) @class.around + +(extension_declaration + (extension_body) @class.inside) @class.around + +(enum_declaration + body: (_) @class.inside) @class.around + +(type_alias) @class.around + +(_ + ( + [ + (getter_signature) + (setter_signature) + (function_signature) + (method_signature) + (constructor_signature) + ] + . + (function_body) @function.inside @function.around + ) @function.around +) + +(declaration + [ + (constant_constructor_signature) + (constructor_signature) + (factory_constructor_signature) + (redirecting_factory_constructor_signature) + (getter_signature) + (setter_signature) + (operator_signature) + (function_signature) + ] +) @function.around + +(lambda_expression + body: (_) @function.inside +) @function.around + +(function_expression + body: (_) @function.inside +) @function.around + +[ + (comment) + (documentation_comment) +] @comment.inside + +(comment)+ @comment.around + +(documentation_comment)+ @comment.around + +(formal_parameter) @parameter.inside + +(formal_parameter_list) @parameter.around + +(expression_statement + ((identifier) @_name (#any-of? @_name "test" "testWidgets")) + . + (selector (argument_part (arguments . (_) . (argument) @test.inside))) +) @test.around + From 299bcce481abe7e13f2023ec6e82c59b43f12f47 Mon Sep 17 00:00:00 2001 From: Idobenhamo <135357971+Idobenhamo@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:27:46 +0200 Subject: [PATCH 14/70] Update Typst Tree-Sitter grammar (#9403) Co-authored-by: Idobenhamo --- languages.toml | 2 +- runtime/queries/typst/highlights.scm | 7 ++++++- runtime/queries/typst/injections.scm | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/languages.toml b/languages.toml index 4ff529c47..83c770479 100644 --- a/languages.toml +++ b/languages.toml @@ -2865,7 +2865,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "typst" -source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "e35aa22395fdde82bbc4b5700c324ce346dfc9e5" } +source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "ecf8596336857adfcd5f7cbb3b2aa11a67badc37" } [[language]] name = "nunjucks" diff --git a/runtime/queries/typst/highlights.scm b/runtime/queries/typst/highlights.scm index b422e05b3..0bbccede0 100644 --- a/runtime/queries/typst/highlights.scm +++ b/runtime/queries/typst/highlights.scm @@ -55,7 +55,12 @@ ; MARKUP (item "-" @markup.list) (term ["/" ":"] @markup.list) -(heading ["=" "==" "===" "====" "====="] @markup.heading.marker) @markup.heading +(heading "=" @markup.heading.marker) @markup.heading.1 +(heading "==" @markup.heading.marker) @markup.heading.2 +(heading "===" @markup.heading.marker) @markup.heading.3 +(heading "====" @markup.heading.marker) @markup.heading.4 +(heading "=====" @markup.heading.marker) @markup.heading.5 +(heading "======" @markup.heading.marker) @markup.heading.6 (url) @tag (emph) @markup.italic (strong) @markup.bold diff --git a/runtime/queries/typst/injections.scm b/runtime/queries/typst/injections.scm index 8039b4cab..06a250979 100644 --- a/runtime/queries/typst/injections.scm +++ b/runtime/queries/typst/injections.scm @@ -3,4 +3,5 @@ (raw_blck lang: (ident) @injection.language - (blob) @injection.content) \ No newline at end of file + (blob) @injection.content) + From b606c052467396042b1dbba10f19fd3c114fd42a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:23:39 +0100 Subject: [PATCH 15/70] build(deps): bump actions/cache from 3 to 4 (#9402) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache 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/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22151c378..3d47c2088 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: shared-key: "build" - name: Cache test tree-sitter grammar - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: runtime/grammars key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} From 6bfe1ddc53f542d62e242fd4aaf6748dda1b0e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 24 Jan 2024 15:31:16 +0900 Subject: [PATCH 16/70] minor: Silence noisy set_error log Outside of debugging tests, it makes no sense to log this. --- helix-view/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index dc10a6044..fbfcb3560 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1147,7 +1147,7 @@ impl Editor { #[inline] pub fn set_error>>(&mut self, error: T) { let error = error.into(); - log::error!("editor error: {}", error); + log::debug!("editor error: {}", error); self.status_msg = Some((error, Severity::Error)); } From 6d724a8f331f4b2a8f1a001e990cf6129dc50b00 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 23 Jan 2024 09:29:07 -0500 Subject: [PATCH 17/70] Re-export `which` from `helix-stdx::env` We use `which::which` in many crates, so `which` was a separate dependency across all of them. We can centralize `which` into the stdx crate so it's easy for all crates to depend on it. I also moved the rest of `helix-view/src/env.rs` into helix-stdx's `env` module since it only contained a thin wrapper around `which` and `std::env`. --- Cargo.lock | 23 ++++++++++------------- helix-dap/Cargo.toml | 2 +- helix-dap/src/client.rs | 2 +- helix-loader/Cargo.toml | 1 - helix-loader/src/grammar.rs | 2 +- helix-lsp/Cargo.toml | 1 - helix-lsp/src/client.rs | 2 +- helix-stdx/Cargo.toml | 1 + helix-stdx/src/env.rs | 10 ++++++++++ helix-term/Cargo.toml | 2 -- helix-term/src/health.rs | 6 +++--- helix-view/Cargo.toml | 1 - helix-view/src/clipboard.rs | 4 ++-- helix-view/src/document.rs | 7 ++++++- helix-view/src/editor.rs | 4 ++-- helix-view/src/env.rs | 8 -------- helix-view/src/lib.rs | 1 - 17 files changed, 38 insertions(+), 39 deletions(-) delete mode 100644 helix-view/src/env.rs diff --git a/Cargo.lock b/Cargo.lock index 9f62947f0..d4ac12e43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,9 +358,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" @@ -1095,12 +1095,12 @@ dependencies = [ "anyhow", "fern", "helix-core", + "helix-stdx", "log", "serde", "serde_json", "thiserror", "tokio", - "which", ] [[package]] @@ -1134,7 +1134,6 @@ dependencies = [ "threadpool", "toml", "tree-sitter", - "which", ] [[package]] @@ -1157,7 +1156,6 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", - "which", ] [[package]] @@ -1172,6 +1170,7 @@ dependencies = [ "etcetera", "ropey", "tempfile", + "which", ] [[package]] @@ -1214,7 +1213,6 @@ dependencies = [ "tokio-stream", "toml", "url", - "which", ] [[package]] @@ -1280,7 +1278,6 @@ dependencies = [ "tokio-stream", "toml", "url", - "which", ] [[package]] @@ -1294,11 +1291,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2359,15 +2356,15 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "which" -version = "5.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" dependencies = [ "either", "home", "once_cell", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index f7acb0032..3521f5890 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -13,6 +13,7 @@ homepage.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +helix-stdx = { path = "../helix-stdx" } helix-core = { path = "../helix-core" } anyhow = "1.0" @@ -21,7 +22,6 @@ 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"] } -which = "5.0.0" [dev-dependencies] fern = "0.6" diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index 55ebab577..579811df7 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -113,7 +113,7 @@ impl Client { id: usize, ) -> Result<(Self, UnboundedReceiver)> { // Resolve path to the binary - let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; + let cmd = helix_stdx::env::which(cmd).map_err(|err| anyhow::anyhow!(err))?; let process = Command::new(cmd) .args(args) diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index 08da7f295..469bedc10 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -24,7 +24,6 @@ etcetera = "0.8" tree-sitter.workspace = true once_cell = "1.19" log = "0.4" -which = "5.0.0" # TODO: these two should be on !wasm32 only diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 66111aebb..537e12828 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -86,7 +86,7 @@ pub fn get_language(name: &str) -> Result { } fn ensure_git_is_available() -> Result<()> { - match which::which("git") { + match helix_stdx::env::which("git") { Ok(_cmd) => Ok(()), Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")), } diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 510be6eec..8e9e3407c 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -29,5 +29,4 @@ serde_json = "1.0" thiserror = "1.0" tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.14" -which = "5.0.0" parking_lot = "0.12.1" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 7eef2bf7a..f8c2912e7 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -183,7 +183,7 @@ impl Client { doc_path: Option<&std::path::PathBuf>, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { // Resolve path to the binary - let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; + let cmd = helix_stdx::env::which(cmd).map_err(|err| anyhow::anyhow!(err))?; let process = Command::new(cmd) .envs(server_environment) diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index 9b4de9fef..e77f8b91f 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -15,6 +15,7 @@ homepage.workspace = true dunce = "1.0" etcetera = "0.8" ropey = { version = "1.6.1", default-features = false } +which = "6.0" [dev-dependencies] tempfile = "3.9" diff --git a/helix-stdx/src/env.rs b/helix-stdx/src/env.rs index 864ba828b..3676727f2 100644 --- a/helix-stdx/src/env.rs +++ b/helix-stdx/src/env.rs @@ -1,3 +1,5 @@ +pub use which::which; + use std::{ path::{Path, PathBuf}, sync::RwLock, @@ -30,6 +32,14 @@ pub fn set_current_working_dir(path: impl AsRef) -> std::io::Result<()> { Ok(()) } +pub fn env_var_is_set(env_var_name: &str) -> bool { + std::env::var_os(env_var_name).is_some() +} + +pub fn binary_exists(binary_name: &str) -> bool { + which::which(binary_name).is_ok() +} + #[cfg(test)] mod tests { use super::{current_working_dir, set_current_working_dir}; diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 683d6dc53..9a7162ac1 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -35,8 +35,6 @@ helix-loader = { path = "../helix-loader" } anyhow = "1" once_cell = "1.19" -which = "5.0.0" - tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } crossterm = { version = "0.27", features = ["event-stream"] } diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 44ae2a2f7..5f2019265 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -182,7 +182,7 @@ pub fn languages_all() -> std::io::Result<()> { .sort_unstable_by_key(|l| l.language_id.clone()); let check_binary = |cmd: Option<&str>| match cmd { - Some(cmd) => match which::which(cmd) { + Some(cmd) => match helix_stdx::env::which(cmd) { Ok(_) => column(&format!("✓ {}", cmd), Color::Green), Err(_) => column(&format!("✘ {}", cmd), Color::Red), }, @@ -322,7 +322,7 @@ fn probe_protocols<'a, I: Iterator + 'a>( writeln!(stdout)?; for cmd in server_cmds { - let (path, icon) = match which::which(cmd) { + let (path, icon) = match helix_stdx::env::which(cmd) { Ok(path) => (path.display().to_string().green(), "✓".green()), Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "✘".red()), }; @@ -344,7 +344,7 @@ fn probe_protocol(protocol_name: &str, server_cmd: Option) -> std::io::R writeln!(stdout, "Configured {}: {}", protocol_name, cmd_name)?; if let Some(cmd) = server_cmd { - let path = match which::which(&cmd) { + let path = match helix_stdx::env::which(&cmd) { Ok(path) => path.display().to_string().green(), Err(_) => format!("'{}' not found in $PATH", cmd).red(), }; diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 0dc18b373..2e6893414 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -46,7 +46,6 @@ serde_json = "1.0" toml = "0.7" log = "~0.4" -which = "5.0.0" parking_lot = "0.12.1" diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index 812c803e9..9ff2fd788 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -73,7 +73,7 @@ pub fn get_clipboard_provider() -> Box { #[cfg(target_os = "macos")] pub fn get_clipboard_provider() -> Box { - use crate::env::{binary_exists, env_var_is_set}; + use helix_stdx::env::{binary_exists, env_var_is_set}; if env_var_is_set("TMUX") && binary_exists("tmux") { command_provider! { @@ -98,7 +98,7 @@ pub fn get_clipboard_provider() -> Box { #[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))] pub fn get_clipboard_provider() -> Box { - use crate::env::{binary_exists, env_var_is_set}; + use helix_stdx::env::{binary_exists, env_var_is_set}; use provider::command::is_exit_success; // TODO: support for user-defined provider, probably when we have plugin support by setting a // variable? diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 388810b13..88653948c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -726,7 +726,12 @@ impl Document { if let Some((fmt_cmd, fmt_args)) = self .language_config() .and_then(|c| c.formatter.as_ref()) - .and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args))) + .and_then(|formatter| { + Some(( + helix_stdx::env::which(&formatter.command).ok()?, + &formatter.args, + )) + }) { use std::process::Stdio; let text = self.text().clone(); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index fbfcb3560..f605cbb5f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -329,7 +329,7 @@ pub struct TerminalConfig { #[cfg(windows)] pub fn get_terminal_provider() -> Option { - use crate::env::binary_exists; + use helix_stdx::env::binary_exists; if binary_exists("wt") { return Some(TerminalConfig { @@ -352,7 +352,7 @@ pub fn get_terminal_provider() -> Option { #[cfg(not(any(windows, target_os = "wasm32")))] pub fn get_terminal_provider() -> Option { - use crate::env::{binary_exists, env_var_is_set}; + use helix_stdx::env::{binary_exists, env_var_is_set}; if env_var_is_set("TMUX") && binary_exists("tmux") { return Some(TerminalConfig { diff --git a/helix-view/src/env.rs b/helix-view/src/env.rs deleted file mode 100644 index c68cc609a..000000000 --- a/helix-view/src/env.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub fn binary_exists(binary_name: &str) -> bool { - which::which(binary_name).is_ok() -} - -#[cfg(not(windows))] -pub fn env_var_is_set(env_var_name: &str) -> bool { - std::env::var_os(env_var_name).is_some() -} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 82827b5d5..14b6e1ce8 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -5,7 +5,6 @@ pub mod base64; pub mod clipboard; pub mod document; pub mod editor; -pub mod env; pub mod events; pub mod graphics; pub mod gutter; From cb25d13028ec1cdf986a3567ea52562ea654a7b8 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 23 Jan 2024 13:36:53 -0500 Subject: [PATCH 18/70] Improve error handling for `which::which` failures Co-authored-by: Pascal Kuthe --- helix-dap/src/client.rs | 2 +- helix-dap/src/lib.rs | 2 ++ helix-loader/src/grammar.rs | 6 ++---- helix-lsp/src/client.rs | 2 +- helix-lsp/src/lib.rs | 2 ++ helix-stdx/src/env.rs | 28 +++++++++++++++++++++++++--- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index 579811df7..18af13ae7 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -113,7 +113,7 @@ impl Client { id: usize, ) -> Result<(Self, UnboundedReceiver)> { // Resolve path to the binary - let cmd = helix_stdx::env::which(cmd).map_err(|err| anyhow::anyhow!(err))?; + let cmd = helix_stdx::env::which(cmd)?; let process = Command::new(cmd) .args(args) diff --git a/helix-dap/src/lib.rs b/helix-dap/src/lib.rs index 21162cb86..d0229249d 100644 --- a/helix-dap/src/lib.rs +++ b/helix-dap/src/lib.rs @@ -19,6 +19,8 @@ pub enum Error { #[error("server closed the stream")] StreamClosed, #[error(transparent)] + ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError), + #[error(transparent)] Other(#[from] anyhow::Error), } pub type Result = core::result::Result; diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 537e12828..7977c6df8 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -86,10 +86,8 @@ pub fn get_language(name: &str) -> Result { } fn ensure_git_is_available() -> Result<()> { - match helix_stdx::env::which("git") { - Ok(_cmd) => Ok(()), - Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")), - } + helix_stdx::env::which("git")?; + Ok(()) } pub fn fetch_grammars() -> Result<()> { diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f8c2912e7..fb32f6eb3 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -183,7 +183,7 @@ impl Client { doc_path: Option<&std::path::PathBuf>, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { // Resolve path to the binary - let cmd = helix_stdx::env::which(cmd).map_err(|err| anyhow::anyhow!(err))?; + let cmd = helix_stdx::env::which(cmd)?; let process = Command::new(cmd) .envs(server_environment) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index c99ec217b..53b2712d0 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -44,6 +44,8 @@ pub enum Error { #[error("Unhandled")] Unhandled, #[error(transparent)] + ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError), + #[error(transparent)] Other(#[from] anyhow::Error), } diff --git a/helix-stdx/src/env.rs b/helix-stdx/src/env.rs index 3676727f2..90a0aee87 100644 --- a/helix-stdx/src/env.rs +++ b/helix-stdx/src/env.rs @@ -1,6 +1,5 @@ -pub use which::which; - use std::{ + ffi::OsStr, path::{Path, PathBuf}, sync::RwLock, }; @@ -36,10 +35,33 @@ pub fn env_var_is_set(env_var_name: &str) -> bool { std::env::var_os(env_var_name).is_some() } -pub fn binary_exists(binary_name: &str) -> bool { +pub fn binary_exists>(binary_name: T) -> bool { which::which(binary_name).is_ok() } +pub fn which>( + binary_name: T, +) -> Result { + which::which(binary_name.as_ref()).map_err(|err| ExecutableNotFoundError { + command: binary_name.as_ref().to_string_lossy().into_owned(), + inner: err, + }) +} + +#[derive(Debug)] +pub struct ExecutableNotFoundError { + command: String, + inner: which::Error, +} + +impl std::fmt::Display for ExecutableNotFoundError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "command '{}' not found: {}", self.command, self.inner) + } +} + +impl std::error::Error for ExecutableNotFoundError {} + #[cfg(test)] mod tests { use super::{current_working_dir, set_current_working_dir}; From 83f09ecbff5160e4350c8099be1ad4c64513f665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 24 Jan 2024 16:00:21 +0900 Subject: [PATCH 19/70] minor: Silence noisy language server not found error in log --- helix-view/src/editor.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index f605cbb5f..eca488e74 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1238,12 +1238,19 @@ impl Editor { .filter_map(|(lang, client)| match client { Ok(client) => Some((lang, client)), Err(err) => { - log::error!( - "Failed to initialize the language servers for `{}` - `{}` {{ {} }}", - language.scope(), - lang, - err - ); + if let helix_lsp::Error::ExecutableNotFound(err) = err { + // Silence by default since some language servers might just not be installed + log::debug!( + "Language server not found for `{}` {} {}", language.scope(), lang, err, + ); + } else { + log::error!( + "Failed to initialize the language servers for `{}` - `{}` {{ {} }}", + language.scope(), + lang, + err + ); + } None } }) From cda8ea991e885a84f03d7ffcf4cf046bfbf77490 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Jan 2024 00:10:58 -0500 Subject: [PATCH 20/70] highlighting: Gate multiple captures behind `#is-not? local` predicates (#9390) --- helix-core/src/syntax.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index e543df06e..a5a85c575 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -2306,6 +2306,7 @@ impl<'a> Iterator for HighlightIter<'a> { // highlighting patterns that are disabled for local variables. if definition_highlight.is_some() || reference_highlight.is_some() { while layer.config.non_local_variable_patterns[match_.pattern_index] { + match_.remove(); if let Some((next_match, next_capture_index)) = captures.peek() { let next_capture = next_match.captures[*next_capture_index]; if next_capture.node == capture.node { From d8b8d2fda6a10f25c9704fc95c069a4c457752e6 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 25 Jan 2024 10:41:12 +0530 Subject: [PATCH 21/70] Fix error message shown for goto references (#9382) --- helix-term/src/commands/lsp.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index de2f0e5ec..c694ba25c 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1007,6 +1007,7 @@ pub fn apply_workspace_edit( Ok(()) } +/// Precondition: `locations` should be non-empty. fn goto_impl( editor: &mut Editor, compositor: &mut Compositor, @@ -1019,9 +1020,7 @@ fn goto_impl( [location] => { jump_to_location(editor, location, offset_encoding, Action::Replace); } - [] => { - editor.set_error("No definition found."); - } + [] => unreachable!("`locations` should be non-empty for `goto_impl`"), _locations => { let picker = Picker::new(locations, cwdir, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) @@ -1063,7 +1062,11 @@ where future, move |editor, compositor, response: Option| { let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); + if items.is_empty() { + editor.set_error("No definition found."); + } else { + goto_impl(editor, compositor, items, offset_encoding); + } }, ); } @@ -1123,7 +1126,11 @@ pub fn goto_reference(cx: &mut Context) { future, move |editor, compositor, response: Option>| { let items = response.unwrap_or_default(); - goto_impl(editor, compositor, items, offset_encoding); + if items.is_empty() { + editor.set_error("No references found."); + } else { + goto_impl(editor, compositor, items, offset_encoding); + } }, ); } From 2661e05b3405c216a2303c20e71830b4bde7ac35 Mon Sep 17 00:00:00 2001 From: blinxen Date: Thu, 25 Jan 2024 06:12:17 +0100 Subject: [PATCH 22/70] Update some grammars to a commit where the license file is included (#9279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Blaž Hrastnik Co-authored-by: Michael Davis --- languages.toml | 8 +- runtime/queries/pod/highlights.scm | 122 +++++++++++++++++--------- runtime/queries/unison/highlights.scm | 2 +- runtime/queries/vhs/highlights.scm | 21 +++-- 4 files changed, 99 insertions(+), 54 deletions(-) diff --git a/languages.toml b/languages.toml index 83c770479..87d4ae235 100644 --- a/languages.toml +++ b/languages.toml @@ -1249,7 +1249,7 @@ file-types = ["pod"] [[grammar]] name = "pod" -source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "d466b84009a63986834498073ec05d58d727d55f" } +source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "39da859947b94abdee43e431368e1ae975c0a424" } [[language]] name = "racket" @@ -2259,7 +2259,7 @@ grammar = "vhs" [[grammar]] name = "vhs" -source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d81f34c011c29ee86dd73b45a8ecc9f2e2bdaf" } +source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "9534865e614c95eb9418e5e73f061c32fa4d9540" } [[language]] name = "kdl" @@ -2666,7 +2666,7 @@ language-servers = [ "cs" ] [[grammar]] name = "smithy" -source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" } +source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "8327eb84d55639ffbe08c9dc82da7fff72a1ad07" } [[language]] name = "vhdl" @@ -2917,7 +2917,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "unison" -source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "98c4e8bc5c9f5989814a720457cf36963cf4043d" } +source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "aaec316774c8b50d367ec7cf26523aac5ef0cfc5" } [[language]] name = "todotxt" diff --git a/runtime/queries/pod/highlights.scm b/runtime/queries/pod/highlights.scm index e8bd4b546..d88d9ffa7 100644 --- a/runtime/queries/pod/highlights.scm +++ b/runtime/queries/pod/highlights.scm @@ -1,61 +1,97 @@ -[(pod_directive) - (head_directive) - (over_directive) - (item_directive) - (back_directive) - (encoding_directive) - (cut_directive)] @tag - -(head_paragraph - (head_directive) @directive - (#eq? @directive "=head1") +; A highlight file for nvim-treesitter to use + +[(pod_command) + (command) + (cut_command)] @keyword + +(command_paragraph + (command) @keyword + (#eq? @keyword "=head1") (content) @markup.heading.1) -(head_paragraph - (head_directive) @directive - (#eq? @directive "=head2") + +(command_paragraph + (command) @keyword + (#eq? @keyword "=head2") (content) @markup.heading.2) -(head_paragraph - (head_directive) @directive - (#eq? @directive "=head3") + +(command_paragraph + (command) @keyword + (#eq? @keyword "=head3") (content) @markup.heading.3) -(head_paragraph - (head_directive) @directive - (#eq? @directive "=head4") + +(command_paragraph + (command) @keyword + (#eq? @keyword "=head4") (content) @markup.heading.4) -(head_paragraph - (head_directive) @directive - (#eq? @directive "=head5") + +(command_paragraph + (command) @keyword + (#eq? @keyword "=head5") (content) @markup.heading.5) -(head_paragraph - (head_directive) @directive - (#eq? @directive "=head6") + +(command_paragraph + (command) @keyword + (#eq? @keyword "=head6") (content) @markup.heading.6) -(over_paragraph (content) @constant.numeric.integer) -(item_paragraph (content) @markup.list) -(encoding_paragraph (content) @string) +(command_paragraph + (command) @keyword + (#match? @keyword "^=over") + (content) @constant.numeric) + +(command_paragraph + (command) @keyword + (#match? @keyword "^=item") + (content) @markup) + +(command_paragraph + (command) @keyword + (#match? @keyword "^=encoding") + (content) @string.special) + +(command_paragraph + (command) @keyword + (#not-match? @keyword "^=(head|over|item|encoding)") + (content) @string) (verbatim_paragraph (content) @markup.raw) -(interior_sequence) @tag +(interior_sequence + (sequence_letter) @constant.character + ["<" ">"] @punctuation.delimiter +) (interior_sequence - (sequence_letter) @letter - (#eq? @letter "B") + (sequence_letter) @character + (#eq? @character "B") (content) @markup.bold) + (interior_sequence - (sequence_letter) @letter - (#eq? @letter "C") - (content) @markup.raw) + (sequence_letter) @character + (#eq? @character "C") + (content) @markup.literal) + (interior_sequence - (sequence_letter) @letter - (#eq? @letter "F") - (content) @markup.italic) + (sequence_letter) @character + (#eq? @character "F") + (content) @markup.underline @string.special) + (interior_sequence - (sequence_letter) @letter - (#eq? @letter "I") - (content) @markup.italic) + (sequence_letter) @character + (#eq? @character "I") + (content) @markup.bold) + (interior_sequence - (sequence_letter) @letter - (#eq? @letter "L") + (sequence_letter) @character + (#eq? @character "L") (content) @markup.link.url) + +(interior_sequence + (sequence_letter) @character + (#eq? @character "X") + (content) @markup.reference) + +(interior_sequence + (sequence_letter) @character + (#eq? @character "E") + (content) @string.special.escape) diff --git a/runtime/queries/unison/highlights.scm b/runtime/queries/unison/highlights.scm index 956dc5824..d58285ed8 100644 --- a/runtime/queries/unison/highlights.scm +++ b/runtime/queries/unison/highlights.scm @@ -63,7 +63,7 @@ ;; Terms (type_signature term_name: (path)? @variable term_name: (wordy_id) @variable) (type_signature (wordy_id) @type) -(type_signature (delayed (wordy_id)) @type) +(type_signature (term_type(delayed(wordy_id))) @type) (term_definition param: (wordy_id) @variable.parameter) diff --git a/runtime/queries/vhs/highlights.scm b/runtime/queries/vhs/highlights.scm index 9a2d05cf4..a7e1af301 100644 --- a/runtime/queries/vhs/highlights.scm +++ b/runtime/queries/vhs/highlights.scm @@ -1,4 +1,4 @@ -[ +[ "Output" "Backspace" "Down" @@ -15,22 +15,31 @@ "Hide" "Show" ] @keyword -[ "FontFamily" +[ "Shell" + "FontFamily" "FontSize" "Framerate" + "PlaybackSpeed" "Height" "LetterSpacing" "TypingSpeed" "LineHeight" "Padding" "Theme" - "Width" ] @type + "LoopOffset" + "Width" + "BorderRadius" + "Margin" + "MarginFill" + "WindowBar" + "WindowBarSize" + "CursorBlink" ] @type [ "@" ] @operator (control) @function.macro (float) @constant.numeric.float (integer) @constant.numeric.integer (comment) @comment -(path) @string.special.path -[(string) (json)] @string -(time) @string.special.symbol \ No newline at end of file +[(path) (string) (json)] @string.special.path +(time) @string.special.symbol +(boolean) @constant.builtin.boolean From 0d09fb4f550eb87a02f5f17dffb0800220f2e937 Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sat, 27 Jan 2024 20:17:37 +0100 Subject: [PATCH 23/70] lang(git-ignore): add `helix/ignore` to git-ignore file types (#9447) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 87d4ae235..785bc9701 100644 --- a/languages.toml +++ b/languages.toml @@ -1484,7 +1484,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev = [[language]] name = "git-ignore" scope = "source.gitignore" -file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS"] +file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS", { suffix = ".config/helix/ignore" }, { suffix = ".helix/ignore" }] injection-regex = "git-ignore" comment-token = "#" grammar = "gitignore" From 1616021a5a5378118678b57bca0ae844af5922ab Mon Sep 17 00:00:00 2001 From: Chirikumbrah <78883260+Chirikumbrah@users.noreply.github.com> Date: Sun, 28 Jan 2024 12:11:05 +0300 Subject: [PATCH 24/70] Make status line modes bold (#9449) --- runtime/themes/dracula.toml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index 1253544f2..1473c1a83 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -60,10 +60,10 @@ "diff.minus" = { fg = "red" } "ui.background" = { fg = "foreground", bg = "background" } "ui.cursor.match" = { fg = "foreground", bg = "grey" } -"ui.cursor" = { fg = "background", bg = "purple", modifiers = ["dim"] } -"ui.cursor.normal" = { fg = "background", bg = "purple", modifiers = ["dim"] } -"ui.cursor.insert" = { fg = "background", bg = "green", modifiers = ["dim"] } -"ui.cursor.select" = { fg = "background", bg = "cyan", modifiers = ["dim"] } +"ui.cursor" = { fg = "background", bg = "purple", modifiers = ["dim"] } +"ui.cursor.normal" = { fg = "background", bg = "purple", modifiers = ["dim"] } +"ui.cursor.insert" = { fg = "background", bg = "green", modifiers = ["dim"] } +"ui.cursor.select" = { fg = "background", bg = "cyan", modifiers = ["dim"] } "ui.cursor.primary.normal" = { fg = "background", bg = "purple" } "ui.cursor.primary.insert" = { fg = "background", bg = "green" } "ui.cursor.primary.select" = { fg = "background", bg = "cyan" } @@ -74,16 +74,16 @@ "ui.linenr" = { fg = "comment" } "ui.linenr.selected" = { fg = "foreground" } "ui.menu" = { fg = "foreground", bg = "current_line" } -"ui.menu.selected" = { fg = "current_line", bg = "purple", modifiers = ["dim"] } +"ui.menu.selected" = { fg = "current_line", bg = "purple", modifiers = ["dim"] } "ui.menu.scroll" = { fg = "foreground", bg = "current_line" } "ui.popup" = { fg = "foreground", bg = "black" } "ui.selection.primary" = { bg = "current_line" } "ui.selection" = { bg = "selection" } "ui.statusline" = { fg = "foreground", bg = "darker" } "ui.statusline.inactive" = { fg = "comment", bg = "darker" } -"ui.statusline.normal" = { fg = "black", bg = "purple" } -"ui.statusline.insert" = { fg = "black", bg = "green" } -"ui.statusline.select" = { fg = "black", bg = "cyan" } +"ui.statusline.normal" = { fg = "black", bg = "purple", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "black", bg = "green", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "black", bg = "cyan", modifiers = ["bold"] } "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "cyan" } "ui.window" = { fg = "foreground" } @@ -133,3 +133,4 @@ green = "#50fa7b" purple = "#BD93F9" cyan = "#8be9fd" pink = "#ff79c6" + From f0be0c6d4a6c253e258b977436144796b8bd086f Mon Sep 17 00:00:00 2001 From: Travis Harmon Date: Sun, 28 Jan 2024 03:11:31 -0600 Subject: [PATCH 25/70] Make status line modes bold for theme onedark (#9435) * Make status line modes bold * Revert change to onedarker --- runtime/themes/onedark.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index a50b5d354..eae11172e 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -73,9 +73,9 @@ "ui.statusline" = { fg = "white", bg = "light-black" } "ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" } -"ui.statusline.normal" = { fg = "light-black", bg = "blue" } -"ui.statusline.insert" = { fg = "light-black", bg = "green" } -"ui.statusline.select" = { fg = "light-black", bg = "purple" } +"ui.statusline.normal" = { fg = "light-black", bg = "blue", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "light-black", bg = "green", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "light-black", bg = "purple", modifiers = ["bold"] } "ui.bufferline" = { fg = "light-gray", bg = "light-black" } "ui.bufferline.active" = { fg = "light-black", bg = "blue", underline = { color = "light-black", style = "line" } } From 9978d421fe7237c74932c34263d91445728d65d1 Mon Sep 17 00:00:00 2001 From: Jaakko Paju Date: Sun, 28 Jan 2024 11:12:07 +0200 Subject: [PATCH 26/70] Include interpolated SQL strings in Scala injection queries (#9428) * Change Scala injection queries to include SQL strings * Include block comments in comment injection * Change #match predicate to #any-of Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com> --------- Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com> --- runtime/queries/scala/injections.scm | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/runtime/queries/scala/injections.scm b/runtime/queries/scala/injections.scm index 321c90add..1ad68557e 100644 --- a/runtime/queries/scala/injections.scm +++ b/runtime/queries/scala/injections.scm @@ -1,2 +1,16 @@ -((comment) @injection.content +([(comment) (block_comment)] @injection.content (#set! injection.language "comment")) + + +; TODO for some reason multiline string (triple quotes) interpolation works only if it contains interpolated value +; Matches these SQL interpolators: +; - Doobie: 'sql', 'fr' +; - Quill: 'sql', 'infix' +; - Slick: 'sql', 'sqlu' +(interpolated_string_expression + interpolator: + ((identifier) @interpolator + (#any-of? @interpolator "fr" "infix" "sql" "sqlu")) + (interpolated_string) @injection.content + (#set! injection.language "sql")) + From 5e0b3cc28b8ae6da1f41b47080a695c99bbc844a Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Jan 2024 14:05:42 -0500 Subject: [PATCH 27/70] Use injection syntax trees for bracket matching Previously we used the root syntax tree for bracket matching. We can use the new functionality in `Syntax` for finding the correct syntax tree for a given byte range though so we use the correct syntax tree within injections. This improves bracket matching behavior within HTML injections like script or style tags for example. --- helix-core/src/match_brackets.rs | 8 +++----- helix-core/src/syntax.rs | 9 ++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index 150679b5c..b8bcc28ca 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -57,10 +57,10 @@ fn find_pair( pos_: usize, traverse_parents: bool, ) -> Option { - let tree = syntax.tree(); let pos = doc.char_to_byte(pos_); - let mut node = tree.root_node().descendant_for_byte_range(pos, pos + 1)?; + let root = syntax.tree_for_byte_range(pos, pos + 1).root_node(); + let mut node = root.descendant_for_byte_range(pos, pos + 1)?; loop { if node.is_named() { @@ -118,9 +118,7 @@ fn find_pair( }; node = parent; } - let node = tree - .root_node() - .named_descendant_for_byte_range(pos, pos + 1)?; + let node = root.named_descendant_for_byte_range(pos, pos + 1)?; if node.child_count() != 0 { return None; } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a5a85c575..a403e7ccf 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1338,7 +1338,7 @@ impl Syntax { result } - pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option> { + pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &Tree { let mut container_id = self.root; for (layer_id, layer) in self.layers.iter() { @@ -1349,8 +1349,11 @@ impl Syntax { } } - self.layers[container_id] - .tree() + self.layers[container_id].tree() + } + + pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option> { + self.tree_for_byte_range(start, end) .root_node() .descendant_for_byte_range(start, end) } From 8b6565c83924e7ef9176ae4611ec368fbf75ab8a Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Jan 2024 14:29:49 -0500 Subject: [PATCH 28/70] Respect injections in :tree-sitter-highlight-name --- helix-term/src/commands/typed.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ee02a7d25..81ffdf875 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1546,10 +1546,7 @@ fn tree_sitter_highlight_name( let text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(text); let byte = text.char_to_byte(cursor); - let node = syntax - .tree() - .root_node() - .descendant_for_byte_range(byte, byte)?; + let node = syntax.descendant_for_byte_range(byte, byte)?; // Query the same range as the one used in syntax highlighting. let range = { // Calculate viewport byte ranges: From 035b8eabdbad02bdcf4f254162623f14767e62de Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 25 Jan 2024 14:33:37 -0500 Subject: [PATCH 29/70] Respect injections in movement::move_parent_node_end --- helix-core/src/movement.rs | 7 +------ helix-core/src/syntax.rs | 6 ++++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 6c4f3f535..54eb02fd0 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -573,16 +573,11 @@ pub fn move_parent_node_end( dir: Direction, movement: Movement, ) -> Selection { - let tree = syntax.tree(); - selection.transform(|range| { let start_from = text.char_to_byte(range.from()); let start_to = text.char_to_byte(range.to()); - let mut node = match tree - .root_node() - .named_descendant_for_byte_range(start_from, start_to) - { + let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) { Some(node) => node, None => { log::debug!( diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a403e7ccf..24de1a338 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1352,6 +1352,12 @@ impl Syntax { self.layers[container_id].tree() } + pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option> { + self.tree_for_byte_range(start, end) + .root_node() + .named_descendant_for_byte_range(start, end) + } + pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option> { self.tree_for_byte_range(start, end) .root_node() From 0328fa4d02ff7fcab221fcc6d9051bdafce63a79 Mon Sep 17 00:00:00 2001 From: Abderrahmane TAHRI JOUTI <302837+atahrijouti@users.noreply.github.com> Date: Sun, 28 Jan 2024 10:14:12 +0100 Subject: [PATCH 30/70] adjust color darkness on ruler & inlay-hints (#9375) --- runtime/themes/cyan_light.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/runtime/themes/cyan_light.toml b/runtime/themes/cyan_light.toml index e18c46a9f..45cb6539d 100644 --- a/runtime/themes/cyan_light.toml +++ b/runtime/themes/cyan_light.toml @@ -95,7 +95,8 @@ "ui.text" = "shade05" "ui.text.focus" = { fg = "shade07", bg = "light_blue" } "ui.virtual" = "shade03" -"ui.virtual.ruler" = { bg = "shade04" } +"ui.virtual.ruler" = { bg = "shade01" } +"ui.virtual.inlay-hint" = { fg = "shade03_darker" } "ui.menu" = { fg = "shade05", bg = "shade01" } "ui.menu.selected" = { fg = "shade07", bg = "light_blue" } @@ -119,6 +120,9 @@ shade05 = "#434b6c" shade06 = "#343a54" shade07 = "#25293c" +shade03_darker = "#9199bb" +shade04_lighter = "#616d9d" + background = "#f2f3f7" foreground = "#25293c" @@ -133,7 +137,6 @@ blue = "#0073E6" dark_blue = "#185b93" darker_blue = "#000080" - purple = "#660E7A" light_purple = "#ED9CFF" @@ -142,7 +145,6 @@ green = "#00733B" light_green = "#5DCE87" green_blue = "#458383" - yellow = "#808000" dark_yellow = "#7A7A43" From ee68fd09ac51b613061d6eb0680d8d42cc21260e Mon Sep 17 00:00:00 2001 From: Jaakko Paju Date: Sun, 28 Jan 2024 11:18:35 +0200 Subject: [PATCH 31/70] highlight(scala): highlight abstract methods in traits and classes (#9340) --- runtime/queries/scala/highlights.scm | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm index 40b230ec4..4f90bfda6 100644 --- a/runtime/queries/scala/highlights.scm +++ b/runtime/queries/scala/highlights.scm @@ -57,13 +57,21 @@ (class_definition body: (template_body - (function_definition - name: (identifier) @function.method))) -(object_definition - body: (template_body - (function_definition - name: (identifier) @function.method))) + [ + (function_definition + name: (identifier) @function.method) + (function_declaration + name: (identifier) @function.method) + ])) (trait_definition + body: (template_body + [ + (function_definition + name: (identifier) @function.method) + (function_declaration + name: (identifier) @function.method) + ])) +(object_definition body: (template_body (function_definition name: (identifier) @function.method))) From fe443910160e145c69f32a028b313d6f596e3fbe Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 28 Jan 2024 04:19:25 -0500 Subject: [PATCH 32/70] Add argument to textobject in gdscript. (#9288) Currently `maa` only selects parameters in a function definition. Allow it to also select arguments inside a function call. --- runtime/queries/gdscript/textobjects.scm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/queries/gdscript/textobjects.scm b/runtime/queries/gdscript/textobjects.scm index 089544682..47512bba7 100644 --- a/runtime/queries/gdscript/textobjects.scm +++ b/runtime/queries/gdscript/textobjects.scm @@ -13,5 +13,7 @@ (typed_default_parameter) ] @parameter.inside @parameter.around) +(arguments (_expression) @parameter.inside @parameter.around) + (comment) @comment.inside (comment)+ @comment.around From eb3c4e9f005d323ea8de828c23b4a7c0903a5702 Mon Sep 17 00:00:00 2001 From: Twinkle Date: Sun, 28 Jan 2024 17:20:51 +0800 Subject: [PATCH 33/70] feat: add hard/soft contrast for gruvbox light mode (#9266) --- runtime/themes/gruvbox_light_hard.toml | 7 +++++++ runtime/themes/gruvbox_light_soft.toml | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 runtime/themes/gruvbox_light_hard.toml create mode 100644 runtime/themes/gruvbox_light_soft.toml diff --git a/runtime/themes/gruvbox_light_hard.toml b/runtime/themes/gruvbox_light_hard.toml new file mode 100644 index 000000000..4a48116ed --- /dev/null +++ b/runtime/themes/gruvbox_light_hard.toml @@ -0,0 +1,7 @@ +# Author : Twinkle +# The theme uses the gruvbox light palette with hard contrast: github.com/morhetz/gruvbox + +inherits = "gruvbox_light" + +[palette] +bg0 = "#f9f5d7" # main background diff --git a/runtime/themes/gruvbox_light_soft.toml b/runtime/themes/gruvbox_light_soft.toml new file mode 100644 index 000000000..a29b23734 --- /dev/null +++ b/runtime/themes/gruvbox_light_soft.toml @@ -0,0 +1,7 @@ +# Author : Twinkle +# The theme uses the gruvbox light palette with soft contrast: github.com/morhetz/gruvbox + +inherits = "gruvbox_light" + +[palette] +bg0 = "#f2e5bc" # main background From 4ab70295352f6e4f86c4d170a04ceb858ee79d5b Mon Sep 17 00:00:00 2001 From: NitinKM <70827815+NewtonChutney@users.noreply.github.com> Date: Sun, 28 Jan 2024 15:01:10 +0530 Subject: [PATCH 34/70] Add icon to Windows executable (#9104) * injecting the icon through a resource file, no extra deps * formatted * scripted rc compilation * formatted and restructured * simplified conditional func call --- contrib/helix-256p.ico | Bin 0 -> 270398 bytes helix-term/build.rs | 146 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 contrib/helix-256p.ico diff --git a/contrib/helix-256p.ico b/contrib/helix-256p.ico new file mode 100644 index 0000000000000000000000000000000000000000..16781cc10b53d50b51f56c7a1d95473eec2a8fb8 GIT binary patch literal 270398 zcmeHw4Qv!wwx)-N7!)BEp$J7N!U#nXiXs$6(FmbYKoN=%LTr{8md7w`CNGa6#$f(2 zZHI1bLm0wj2w}kXFj0&P0H7e0XbpP!ofYSw_OG;pJ3(J1i!W#AKl`uKwDdY{aHSJfUDhBc78 z1_oQoo(13E2S&2+Uibp$f9~q$D_H}s&;YppBtY-;x(xE3`R|IF<3g|oa@W96>*BHC z`)fcm@W>?h$p`TBKX>)>m8^k+X#m{)32+R!4~XZv*-z$w!D^5*W(}Cv0JuIC*ah6o zgZIKuF#pY00bjuya9jiOoZZ&NE5Z587Vw_=?|2PzhO7be8W?I_G6sB~2b=;PTF8C! z0sQ(qP60>bUpjZe9#h+GtlH89Y+_&M)ojV@O~2bz6sEa%N6{z`;(vl1vZb?t@8NZ5^%jM#(UyEA%bWBa|JFQXK%3v z23udJxZEW`3y{m4T`PS8^WWn7HIrOtppnM*sh@u*#(BYg`aBDaHs}2Ew90Y0 zv%vWi!1sWaJkB*O=6?ZA2K-(M=y9L;f3&@Z$NdUeJ(guX&|3Bk_`Vkyb_wr=zhM4b zR?FrR0>5Vf$!&i1^Z#&oTLZuEH>XxtosBhkC@!}Gxau> z4`1uN2Eg~1fTKV@ak+Y2wc{A`-(eH!*m{V@{Ptye_LC170cJT|JkBmh4S?@cfO=pM zu!F01q{;lxvA%P?EF5Tf3cS8xfdAwJcIBE6U+0Vl!1W2hTfij`@m}~6=D#y$63+$v zp9x-PYx|2nKp)P1A$~D`ag7ELk3SlGe--!wusSYR@YC*3=D))h1wPYUp9g^8c(%_^ z+5{EUjk0StOmgKalmq*(yf@5&pg5B+&D?WMB-I0HN^a_*C_ zTIG$J^G_)q=D+X*SCMZ8#s;4Bk0#CC32dF>a*qIBh|6`N9YO1iFEyt{K099x6o~&~ zY=G7jTnIlfHsAa3%Jr~uip#A9`T-|+<&``rc7fx|9X8|w@?W$C?xPGx0bkE0#+d$i z*!P>jMdrP(-=lct$qt);(P!rZ{tG`ah_ck9Eilzjm4dam)N#2^?xo|E=e{WCfdRbx z^i$n?;a*(Cf8hsckFk~T0~5TUzf5Js^Xz$`0q}ZUuHdL$pQN9;nRLlAdoQ3J_Y<3 zmur^~qjvv_q9dJ)ZrCII7k;1<tOqaM{G1OMZd&w8j zm_5dS@&h!N_#`kVyzNlIY-g}V-Y=Kpa=V!Kru`npGB4|ITg>DA1>`+RiX0oF#m-g=*B&404CX~!{L@P z>f_G@J_d>{E;nEPQ*6UoVBtVpS<%e(w9_7~O4|qgCqF=O#x1}S>}6>AdnAnOPlN65 z25tiRa-rC+r&-gF=0L2;afgi}p_As_bak=w>(*U(yi|v`6&;3&0=r}N| zzvK11eX!N`&gp42;8Xq!KX664zXro?PuT4Xz(>G6ARitS+qE%Umg_O^|Tkk0gRJ8m#`@Q<|Pdj5&wlBpgoI17k0k? z%usmI+`+cRpTmY9F!xRQKGN4o;FUq1zvm^or>Hm+vm0-ig z_T0|remlYY<$!DEV5b;0A26(eV&y;XTk3e{kaTI^PO0bQDrul)3HyP}{htUpUjtw1 z$`~xe2G1vIGyoo~A}$my0!>n9=(p0>4cn#fPW?i8F1DF!3+MAJ^}@CE9;69e&)Z> z0onpIHt?7IbyDxY{Jk_-JYxfc;C($XRcj^u%dZ;fsBRh!?$dj5=6|vdXl!7|0qM%` zs-?)kzu|?xdah-pxLmBkxA-T1wR66&^cn#77XcJI!#*I54leBajdbnUMydZ#Kl4+c zfadHr1FvCxkK=OFQ^eO2T5B8J|D5@s!T+Hx5Uy#EzWVjAr0f4{HO2=1tJnR&Rq%c- zcwe-8u=$$QCT|Yhp9_qz56IvHsBJ)F1KSTumw&rM>igHXl=il_<_}R^?p`3L*hrIR z#ix5j1MPvPQQ&?R@PK_l79SAv1Lte^OW%C7O^W<+g%{cav@Y=p;HM+J&yGh}>1;9E zs~evH_i2A_o)3^_96--Ee!2VC66O+1{mskV=m)5e--dSnQq0}sxLju`<+;p{r zK=^_4bstDqk8G80ocXzveFBQh{S43hxr42*7i@0??^{0Kfm#FL{&axW+NU-5xh-Hs z2h=CPd0+l&uk`IdRZ0%T8D9YR-@w|11Lz$1 z0Xfe2gPl_EKmXE>IAdC`jMgoC+KPJSG*|JGuX9|k9mdUR8rA@~KLcn2IEL6nTY!A##rO7@jWecwNLHbb&vChiEtF5RTLYcd zjbmWX^MF$T&mA)12jn>853|M@)4pT7fN6HuC8y6C$W;S)Ur2F8D}hS@j|~{r0gVmp z_)xlX7;(m@|C_qsPkZTn3~*d-u13k%Ij(`WEx(_FSh-yt!Grf`{F{A&abHMr#$Eq& zBdtMp0qc{$cI*5?j>~o2f;q!{G;p)?KR=CURz2e5?p;0@lHdpA_h9S?4EO=^mxwd& z2>$j$d)4n2wro7Y{LjZ2`C7*`0M1VU-U4X-ICJQ{qT2Ydn0`zduFR`vt^e_Doi?FQ+>yYF*J!lX>+R?haQu&+;qtM(H4yE7=Y^5$I}VTZ?6`OHt5w>uy-eITDf|bemM~;(W zKVaGqT!0@~0YAX|^5$*voB(Mc`fc^oc;?rP^wdR0dh4a(t6MOZk3F$W^4jqEVr<~E zUrLt{FH3V{*$<8^Ru7(!UxbNjSc*to@ z+`6`PeDr$g0C@jM&Ha(yooM@S1s}1lt(u=se5XDE^7#(yY0|oyw7w?$0aJe95qtox zrgC55|E)svU*!jApN+16TrbhS z8?a}FIUNi`XL|wO|0@e8ibWjl3OtFizO(w=k3Ijt*^Kso0iXYZ|Kbzv0or$q_TQp? zxVSA~+7DiZ52(O?FTDR}u@F7YeMY;3%h2wBkct1%u4-wh9s64e{<+~ZHa0-}aMFI9 zJeSzC51_Guv%o^k8{jzOH2m=M;alHq9vki1aUwnU#rwY<(#;FBr;P*e|IGRp8XLfV z+_Wz@#~GXU0n~q@IOC(hOMbR>zPD5jM8Bo>j?4m5C_u^&mVx~J&%;7agcnd2ws`epcxUfYIwK+E0P4^aC68XHi@8S`9Xvuz=Y zGyVd2HN35X=Mt+pWR=KJZN# zADrPQyXSjRG=OKnY5tQhp!Weczx?~yoIn?j398!yF+YGcb>)3`X#XA9G{c+@D9*SB zeqb_l$3Gg#mH%>kpr=+E{$`Uj)Tvps-d*nqll=gGe|PE^wDwRR?3tcz><7&0fcAY{ zjqxX%@3eH!M8P%ygo=ftr6h5KEZwXhNFPy9XD)vJn#l^5#Y9f zIUP`(@&0gi!*nlmpqR^UCI6K%!a8aA+E%e@W)VZRN;{jK}`wHxrJJD#hz>Z%8 zjsUd2CbtKS>wxAGpF>-qEF3t*amGdsuE6Kn!+%wupgSnt`b!1I1{{qY7zXe6q7RZ& zj9Xja5bbBY05}8CdvNvx#&tk(#>atK9f3m}XIx-@XUpDh{)>47^83PzzeJzF*0BNF zFZTp^KP%F@IB(m74NnBhfhz#}0CPH^IO9F&6XfKBv)L`nttSS3bvD4LHw#;Ro=(uzS07>(XkhEAY3uyf3^1-X8!QUE{XBx^XOQc@coIu0|U3 zVn1M92lt^b8e4tdS}W$%a@9Z~`JWgY2$-2m`~aN)40z>6%j*S;OMwkf0yY5j?u&hZ zIUSJBmSSum?=hNO?VhhwG~fpQD`NvRmw2;;y~Q$(4O{^4R}8e4xn}MuY z*avn&SGP`;LtzMTsJ&x{+xo+kqVfMN`}Es#wIfrex4TYqEud)oq0 z%2V08pZ{{3fc7Fg2K>ZJ$_4jl01W{3b=eQ3)j`9(@U{cSAj18jx$c@0|Ez|!W-)g_CCie_GEQAJ;s0G2QI@0tc>>7duDw~aDP1T z7H|>Zwm`BD0*9`2RDHnf4qHv|Ea`ca|KtZ~pTg6?{Af?zm@K#H8Zo#(1*ioE*$=4w zpxg(T0%$HA&57goKy||-i~+n^B(!tQZTf)! z!Vg5?2eyy&)Vp$yfL?`%w>46n@j`&+#y>22KM*)np{FN4W}gOp#DC!jF2E1009-Ri zFk9^*u4p2-&vC}sk2IUESZ=Kbe8_+D1GJavXTU2XJv%)!rVJl24cG$=1I5@UVE$W6 z@2si$l>eeFa0fo%0LBI!jS1C|1wnQPi!-B3(%#>{^M zoVD_lBI3Vj3;Y27p8$S(tGC`Wv8QMQP@M68fMVwSIyS)kx02ij(kep!3qLRnAFvnj z%=(n)mAS-oz<=6%$B%P~ng0ejYvn0L%zxnruEGaYU~IrM>r-M4JrQTz>9e)~^WREx z8%Qgc{lCE%!5zPR(p=)Rz`|&6op<7lu@^7+PjSW(pZEdhzX8r#c?#Cxd=qT|8XNNq zm-D#~iZeb6X!Z!SQr~u@1@6xPnt;0=_XEs-I}9X4n&AH&@So<;=ffMnT{{SU)B~Q0 zN9zd4amMq&|5Lz2kNN@Te+I7FA&Pd@OhF%j`oMnUuod@n33_=8@KBs_!vt`j;*2kO z&<8O8?I3xEG^3Z-j>di*hnfFr{U41Dd;!e6-RGV4DZu@yKt154IAi922CmvAigpL* zV{b0%`&#kL^lKV-p`RvThF$ewN4nrY#Tg$3ybx#1{I|mhvZNWkR`mq>03S2|4f#Kc zGY$Zgv#8ZIVsL+KxTa|V;tbE>yPM|@G5-r>HL&LwfNsFA&u^>ms}s6fKGIh=u0U00 zD%BE?UKTQ-@45YF#oNkw5il?_tcGoEtdoA1I+nAiZk91OgE)) z8&7Uuy>1-1z6=P*c`x`cJ}b9L;VtjM4;*rfA7K95XcI<~j`oJg2T+V0_XW)P0BYlq z&NcuAi!%OX02i+~5P4|3*maz&Wtt8Nf#X&kZu? z2WT$kIiPIhy0dY{;nkJX!S{W@NLJnp{)^9wO;UJ6ofO{oVIlnh^WOn(8_fdy@{WNW zF96N}4*|dSgdb>f z+7~eYjgr&B^KSRnPlO%QxWE;_4zBn)O^P$#16Uqs+`hVUEcjjqoC6-^&3)kmR={yWHPvsuB0rvbZxn}DA=YsbBj?<~hYrg_I1cdRU@KK@MLW8i!1 zcprN|Ap8K04Xh7J;i?au@&U|$v!rzR{J-|q()i#^fZDdy#EExBN51_Gubv1Zj_`V%u1I&My@Y-xKV8_#e zeZUA{$1~H4Hl=kD~SA-?#T&4C47pTq{p{?k4lkEx*@dF7f6)`OYO~ z{_7FSqsQO}CWHS0ppX54IX`&w;tr|ne{7S^Z^1h_ulN9YYyfkKbAMmR{P!r=GZh=$ zr@6#Uz+LtO2K_)E&g-d{23kYXl>=L)&UF}f^NJs!IfU}~pvkocc>nKAH1f18inX)A z{Z|26OY0%~0pork`fZ(rK7n-UUG%jpywE48l)@BaY$~P*^9;8gSm|ja=fY>z0LBN$ zgZs3m<|Xz427LgHp`bl*>q@P3{gZ0x%Xcs~Ky3lHw+Cnru^dNWq#e-kJ@`K_s{!!| zzR&=;KLw}-2H6i7^aIo<0ERB?kgok6V*{JqJ~n_p3(XfYvgTNzp$G30Esv4#1zCK2 z6X574QL90c)O>#gGpCz^%)*Qupy{=|aG5V*_ZLY}t!GK!$jMz@f7kD}2&b z#>j;!m?mq!60a^f#GdAG|hOgC0y`R-cU+q92yijwAX$=v19if!x|0BE`JQ&{c zz9#0W;0DJ9aHR%r_wJko{%-=h0UjGLsspqKZeFUDzC9L@E>soZec>9+SxQ+`6z}>2 z2X`~~T}d@p7Yf{`IOF{Qt+~&Bz=#fFV*>;2A?cfgm`l9gsj&gP|67lE0QDMzfkPM3 z-(QM(0XeOILNBF2fmNC{*iFihlbZW^S$Bn4O*%JaDOsT14P&l81w_wCjf>h&iIH8amJN_hj^!d z2G9Hj;Z5&yTi;TP=fw0h0Pa(q@nL}XEHcs`pmWF%AkMhI1#!mvw@MvW#2K&Id^x;v zS9!SVK;CPe>(w(KV-2L!0Ju+ciRS^Q0G>;1!Vk!C#;4wqF73)W&KT>Jtq+AO15?x0 z4w!V`0|xz|7#pBCOVc)tp~pWt|9a)zvd95nEMM|O_6ePibW%!>_UZ*FDm%P1F$`8>1- z00aXPpPV@%Ufi&7DgS(*TW2f$whtmr{5y_%A-;2WZ|4&41}{3CiO{`eAuBcd29fDHajjt^RKr99OF|^78GaZ z;QI`q3AhVr+xkX-$JW%RxEP9$xrI0xZVQ<90~g=}-T-)QkK&}Fqj%lDdfjC3y#|Qn z%=={jKz#zl$5Nau#miFMEc*ddet_b#KLcKgUf;>Zkr{Bhv>a-h>P-s|`Q zS}%jv%@}A4DeYl?UudRZKi zZT17E`~d9(UxT?rPgrj$oT8sJfIj{c;CmU+0z4`>_tm}tet`Dbp#3%mJMg}c-j{Ko zz-XU<<__(N_SCZv@DpJzx|jBfjnv1V1sn&ycNOoGd;qlvXy2{we+%Xf84B!%6#bQoJ1d0YiR(_IR7?1GD8@ z$WsI8<4*+N%YiGN;k}L@p#8WhUcMi3^AtbNejuwK_#8UoIHElHCpf=pgLLipm`jXxbYvgEV*}~N255iB*Su=KT<+X80It6X90Bh8g7<14 z1Rp?S19F`49}s7Jh2xCD^K?GoNVKPp=LzOcB~M)mu1^6%K)>&Jul51-o%{gB8NZ7- zW2~>samKnn;3|9ruPNdw7Mm|DxIP|u6S(Mm-s|`QInMY)%q7Np`#d(F@dFe?{IYqA z;w!wMf%fvs(HP%*6`cP9cu;iQSNj4Q8xV2E*nfw|24p`#^8uE5!Irs<<}?7_KM%e) z0J+EIs`=xG@6;#2eOwGGamKVCAHOH4UuDj+_-r?80QOCBxm7?P**Zf<2k--OobevS z8J~&88K)m3OE-4rm*W*~wr4JzF%7h@mg91lf$w4FybkB}d?EY*#Tj31th$ReNBT01 zllz71diTNnH%2j6p4IX8TAH)_GVm$jOkA!Wm%MrGA>I>wf%VB>9qb4_1OA7A{vz`O z%zsxhJA0wP_h*59fb(&=-sHQwKSXPkVy#l`UOM3Zi@--fvFvBe{LfA`SDgX7eG*s) zbOCCc=I`0{F$kU20%_lS-|pKv2JZ z0CaiW2QdHh;H#CF;$1Y&+5HH($GkV?_h>AX)+$_rXTGCr+kyMjfqlS;NBscv-%1ka zkXGBO%2BZ0r2xOrGvD6nhTb-{m#>?YgX%50WTdYSV*_Ue|Yspox;pNPIQ*C{k<9rgp zUPP6yT)PI`pA1w1UWh4R{#(Li^NGNQUj%vpFO9JkvaFrZ75U3?=C$qo72yB#Km$N~ zZ@Be60rTH^(a0qz&r4>12zDQfEx%=4KA8|Svaw*5nKx%I$QTb1vk#KHS9z(SyqamLJl zhiw8jPIG-808Y27-IyQ6{_*t)mF} zPjhxZ0e;%Hy294DT!*XD_UyoY5og?u?{@SFnE$rdqBesf<9{3OcWJnMt)p?dT8(@E z7r0Mx#`}Rg7W)C_zr*HHB>bnihAqHT4y)I<*8b(FBmVc}UbbWJ(Wh0n$6pJe z0dW6GfYur?>jPF6ptwbyr4RT|ak*Hh)cbL{eloiJx@fg?sTZ z|0yo_G%z1+eP6}pdckgtlo5QUzJSqn$LL++Oe5*?Io4^wqx`>ua#P$w-h1g-r%6sp zPXpleZoo(zfYu`6_p^FRwDFk7_)q(e?ZzHj(`-};CvBkyz-fvfFtQ)PzQ6YDfT6F9^pUjr9=CT&J4f3hHb=1#k%AiyceXgz%Y=h4M1)GIW8%3Q5D1G{HM6wb6BHf zQMe+v{c?+{GF^DH;6JS^av}@=j|}(JbKG?S>d(de??Tzu6>=|~0#t|ZvK;BF#ePKf zhcob>Vu?Jzrm9yf&V~G^xZHifbg$}^%bm~wp8pgla4Ze~@8bBf#M%7KX$`oD{}h+| zDeyADeSD`4!KVCTYmJ}P;XiSIRkV-C{A^Oap=4dc|1iq23~l{!h6>Fmc}4?x{yztv zC&vU(n}6v@Z!P=z0@q}L`QL~9t1!OD?|lnw2XryN>H{pi(^t=PHCmy53xV^OfeKnz-0D0yz5F%cApa>Y_f2r0<8t$_Hdm)G(kGAk&jatV9zfkQt}bkm z7tR6xQ(SH>`uJ0dT-8N!`!uEp-cJUe9l2i5Z9JFPx{ds&y>yNMFS%R=Ts#Xk@Yk-| zQPJ)?9@n=}r*2ZQaByffI}L{k^dBz+X9pU zyqAtcb;a4R2CUXV4*c)NH5<^!pJa7eI6c+?Yrt6znBe~%oVy?3xLjweh;w5N*rovk z{HM6wlbF9Vr?Y%L&*!kMCOBc%fb$y2!v9Xlm!qxEakg)=1FV69X&{OJ6qnls%qUoaIAhiTYrruLfa?{25|_Jr-K!m|IWE^R<#R@?fdXp) zduc2J|2H5WpXclrxGp$*?`lBG_3v>o^OX;>$nRy5H)oNTWR^e5B7cxY9?2pPWtKlK z$t-`AMSd@fyg7?Jlv$4AY0LNDCC%T1B>Cgn8nF~fk|$SobCNu{vLV*w9|>yGhjz5` zX006B(aJ+wc_dk`$xM<$ndPJ%P5O_u*OPXV8!Q;tG^FQ9wmI$?_k? z&u-RDKapHWUtStZAf}z<^y3ALX(vT4G!%&?pU6;>Z=&R!$J&lf#Cc-$;>o`m;sZ=u;^l zRNfqytHp8o?6_D{ytq8^Hc73$MEdc|HQLdW2X*sLT(1$w^N-6l+R;g0qa7W2X_oYZ zT6v<{loDvQgLC5rQ@&`m19?b$LQ?V6W+R_CJ1Kiry~*-qvC1q-o}?X|ov<5{rxY(q z!KEqbYi}@^DsQHAV}B{y!RZO!=oL@RyOOx4Qs*Kpn6j9y7KrbmSmyW$ieu`}NL;R0 zJaqw6wL|G7Za`n2E&WhDf8FwByT62A($`O-c>4NnPGn!ArvQ~$Pl4*6?xEETrXPDUg_IM5^2zdl;+`}b z(vd3#jA=+muF;U5yi}sUwE4%3u30`ID4#3;C(0LV87LakldG#xB^N4*J$sepbmgTI z{e`FiLHVolpL~E0Dpe@|RLh%5Mt_QiuPu2b*BX5>d zQ(qrfkgTClid^H^VkfJw*Z7sx`qlcCh@8AeKPuY(Sfr&%KT!ie5?{4)rOcGFj(tm# z6d{rPH#7FBy*^gxn2bKO@`Q%ODO$NGm5?T1FYYR>JW++QQJ`X^OmA zBTv<^Mx5FSRci;QrWlx7JCLVXKt!8<$_<7x%eC6Uxk(DpYA02mRJ@eyo0GCnPCr?k zBu^<`l02n&N%BZM{TE{2wD%W^%SAnC<#Dn8^F>@lPw-~tT+Q_f?UV`@YUHtNaAi=~ zp++83(!~GqHnduv&<>f6aU(2oA|JyE>bxL*0CzCIGqo{lOvtClwlxnh;_FSR@* zB}0@ zOJ5wL0*ddrL|h-|EFI91i{i;us3TAKl!)&2vyqqbhbjz;?}_{qN1&lpdAxWyPfsqi zL&-GjTwh9p*k43P9uz`+-qVq*#HeD`*^2%W+QEsM_jigsp&fmBDGtZ~G_P!w>w|G2 zzC2d@Pg$om;)nWnv_kxXe0It~)d9$Zs-G!e zAE!PoHRn7|){aI>`Ijag(f&+&f2S4Vm&TYyLS4&)iN19_=VqN+pL-o>j io::Result { + let find_reg_key = process::Command::new("reg") + .arg("query") + .arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots") + .arg("/reg:32") + .arg("/v") + .arg("KitsRoot10") + .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), + )) + } + Ok(find_reg_key) => { + if find_reg_key.status.code().unwrap() != 0 { + return 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"); + let mut lines: Vec<&str> = lines.lines().collect(); + let mut rc_exe_paths: Vec = Vec::new(); + lines.reverse(); + for line in lines { + if line.trim().starts_with("KitsRoot") { + let kit: String = line + .chars() + .skip(line.find("REG_SZ").unwrap() + 6) + .skip_while(|c| c.is_whitespace()) + .collect(); + + let p = PathBuf::from(&kit); + let rc = if cfg!(target_arch = "x86_64") { + p.join(r"bin\x64\rc.exe") + } else { + p.join(r"bin\x86\rc.exe") + }; + + if rc.exists() { + println!("{:?}", rc); + rc_exe_paths.push(rc.to_owned()); + } + + if let Ok(bin) = p.join("bin").read_dir() { + for e in bin.filter_map(|e| e.ok()) { + let p = if cfg!(target_arch = "x86_64") { + e.path().join(r"x64\rc.exe") + } else { + e.path().join(r"x86\rc.exe") + }; + if p.exists() { + println!("{:?}", p); + rc_exe_paths.push(p.to_owned()); + } + } + } + } + } + if rc_exe_paths.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Can not find Windows SDK", + )); + } + + println!("{:?}", rc_exe_paths); + let rc_path = rc_exe_paths.pop().unwrap(); + + let rc_exe = if !rc_path.exists() { + if cfg!(target_arch = "x86_64") { + PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x64\rc.exe") + } else { + PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x86\rc.exe") + } + } else { + rc_path + }; + + println!("Selected RC path: '{}'", rc_exe.display()); + Ok(rc_exe) + } + } + } + } + + fn write_resource_file(rc_path: &Path, icon_path: &str) -> io::Result<()> { + let mut f = std::fs::File::create(rc_path)?; + writeln!(f, "{} ICON \"{}\"", 1, icon_path)?; + + Ok(()) + } } From f5b67d9acb89ea54e7226111e3e4d8c3a008144b Mon Sep 17 00:00:00 2001 From: sogaiu <33044872+sogaiu@users.noreply.github.com> Date: Sun, 28 Jan 2024 19:17:46 +0900 Subject: [PATCH 35/70] Use janet-simple grammar for Janet (#9247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use janet-simple grammar for Janet * Update book * Tweak language name and related * Rename janet-simple to janet in book * Remove spurious language section for janet * Drop quote_lit and qq_lit related highlighting --------- Co-authored-by: sogaiu <983021772@users.noreply.github.com> Co-authored-by: Blaž Hrastnik --- languages.toml | 32 +++++++++---- runtime/queries/janet/highlights.scm | 67 +++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/languages.toml b/languages.toml index 785bc9701..ba3b19066 100644 --- a/languages.toml +++ b/languages.toml @@ -326,6 +326,29 @@ comment-token = "//" language-servers = [ "mint" ] indent = { tab-width = 2, unit = " " } +[[language]] +name = "janet" +scope = "source.janet" +injection-regex = "janet" +file-types = ["cgen", "janet", "jdn"] +shebangs = ["janet"] +roots = ["project.janet"] +comment-token = "#" +indent = { tab-width = 2, unit = " " } +formatter = { command = "janet-format" } +grammar = "janet-simple" + +[language.auto-pairs] +'"' = '"' +'(' = ')' +'[' = ']' +'{' = '}' +"`" = "`" + +[[grammar]] +name = "janet-simple" +source = { git = "https://github.com/sogaiu/tree-sitter-janet-simple", rev = "51271e260346878e1a1aa6c506ce6a797b7c25e2" } + [[language]] name = "json" scope = "source.json" @@ -3008,15 +3031,6 @@ file-types = ["log"] name = "log" source = { git = "https://github.com/Tudyx/tree-sitter-log", rev = "62cfe307e942af3417171243b599cc7deac5eab9" } -[[language]] -name = "janet" -scope = "source.janet" -injection-regex = "janet" -file-types = ["janet"] -comment-token = "#" -indent = { tab-width = 2, unit = " " } -grammar = "clojure" - [[language]] name = "hocon" scope = "source.conf" diff --git a/runtime/queries/janet/highlights.scm b/runtime/queries/janet/highlights.scm index a036368a1..133559439 100644 --- a/runtime/queries/janet/highlights.scm +++ b/runtime/queries/janet/highlights.scm @@ -1 +1,66 @@ -; inherits: clojure +(kwd_lit) @string.special.symbol + +(str_lit) @string + +(long_str_lit) @string + +(buf_lit) @string + +(long_buf_lit) @string + +(num_lit) @constant.numeric + +[(bool_lit) (nil_lit)] @constant.builtin + +(comment) @comment + +((sym_lit) @variable + (#match? @variable "^\\*.+\\*$")) + +(short_fn_lit + . + (sym_lit) @function) + +;; special forms +(par_tup_lit + . + (sym_lit) @function.macro + (#match? @function.macro + "^(break|def|do|fn|if|quasiquote|quote|set|splice|unquote|upscope|var|while)$")) + +;; for macros +;; +;; (each name (all-bindings) +;; (when-let [info (dyn (symbol name))] +;; (when (info :macro) +;; (print name)))) +(par_tup_lit + . + (sym_lit) @function.macro + (#match? @function.macro + "^(%=|\\*=|\\+\\+|\\+=|\\-\\-|\\-=|\\->|\\->>|\\-\\?>|\\-\\?>>|/=|and|as\\->|as\\-macro|as\\?\\->|assert|case|catseq|chr|comment|compif|comptime|compwhen|cond|coro|def\\-|default|defdyn|defer|defmacro|defmacro\\-|defn|defn\\-|delay|doc|each|eachk|eachp|edefer|ev/do\\-thread|ev/gather|ev/spawn|ev/spawn\\-thread|ev/with\\-deadline|ffi/defbind|fiber\\-fn|for|forever|forv|generate|if\\-let|if\\-not|if\\-with|import|juxt|label|let|loop|match|or|prompt|protect|repeat|seq|short\\-fn|tabseq|toggle|tracev|try|unless|use|var\\-|varfn|when|when\\-let|when\\-with|with|with\\-dyns|with\\-syms|with\\-vars)$")) + +;; builtin functions +;; +;; (each name (all-bindings) +;; (when-let [info (dyn (symbol name))] +;; (when (and (nil? (info :macro)) +;; (or (function? (info :value)) +;; (cfunction? (info :value)))) +;; (print name)))) +((sym_lit) @function.builtin + (#match? @function.builtin + "^(%|\\*|\\+|\\-|/|<|<=|=|>|>=|\\.break|\\.breakall|\\.bytecode|\\.clear|\\.clearall|\\.disasm|\\.fiber|\\.fn|\\.frame|\\.locals|\\.next|\\.nextc|\\.ppasm|\\.signal|\\.slot|\\.slots|\\.source|\\.stack|\\.step|abstract\\?|accumulate|accumulate2|all|all\\-bindings|all\\-dynamics|any\\?|apply|array|array/clear|array/concat|array/ensure|array/fill|array/insert|array/new|array/new\\-filled|array/peek|array/pop|array/push|array/remove|array/slice|array/trim|array/weak|array\\?|asm|bad\\-compile|bad\\-parse|band|blshift|bnot|boolean\\?|bor|brshift|brushift|buffer|buffer/bit|buffer/bit\\-clear|buffer/bit\\-set|buffer/bit\\-toggle|buffer/blit|buffer/clear|buffer/fill|buffer/format|buffer/from\\-bytes|buffer/new|buffer/new\\-filled|buffer/popn|buffer/push|buffer/push\\-at|buffer/push\\-byte|buffer/push\\-string|buffer/push\\-word|buffer/slice|buffer/trim|buffer\\?|bxor|bytes\\?|cancel|cfunction\\?|cli\\-main|cmp|comp|compare|compare<|compare<=|compare=|compare>|compare>=|compile|complement|count|curenv|debug|debug/arg\\-stack|debug/break|debug/fbreak|debug/lineage|debug/stack|debug/stacktrace|debug/step|debug/unbreak|debug/unfbreak|debugger|debugger\\-on\\-status|dec|deep\\-not=|deep=|defglobal|describe|dictionary\\?|disasm|distinct|div|doc\\*|doc\\-format|doc\\-of|dofile|drop|drop\\-until|drop\\-while|dyn|eflush|empty\\?|env\\-lookup|eprin|eprinf|eprint|eprintf|error|errorf|ev/acquire\\-lock|ev/acquire\\-rlock|ev/acquire\\-wlock|ev/all\\-tasks|ev/call|ev/cancel|ev/capacity|ev/chan|ev/chan\\-close|ev/chunk|ev/close|ev/count|ev/deadline|ev/full|ev/give|ev/give\\-supervisor|ev/go|ev/lock|ev/read|ev/release\\-lock|ev/release\\-rlock|ev/release\\-wlock|ev/rselect|ev/rwlock|ev/select|ev/sleep|ev/take|ev/thread|ev/thread\\-chan|ev/write|eval|eval\\-string|even\\?|every\\?|extreme|false\\?|ffi/align|ffi/call|ffi/calling\\-conventions|ffi/close|ffi/context|ffi/free|ffi/jitfn|ffi/lookup|ffi/malloc|ffi/native|ffi/pointer\\-buffer|ffi/pointer\\-cfunction|ffi/read|ffi/signature|ffi/size|ffi/struct|ffi/trampoline|ffi/write|fiber/can\\-resume\\?|fiber/current|fiber/getenv|fiber/last\\-value|fiber/maxstack|fiber/new|fiber/root|fiber/setenv|fiber/setmaxstack|fiber/status|fiber\\?|file/close|file/flush|file/lines|file/open|file/read|file/seek|file/tell|file/temp|file/write|filter|find|find\\-index|first|flatten|flatten\\-into|flush|flycheck|freeze|frequencies|from\\-pairs|function\\?|gccollect|gcinterval|gcsetinterval|gensym|get|get\\-in|getline|getproto|group\\-by|has\\-key\\?|has\\-value\\?|hash|idempotent\\?|identity|import\\*|in|inc|index\\-of|indexed\\?|int/s64|int/to\\-bytes|int/to\\-number|int/u64|int\\?|interleave|interpose|invert|juxt\\*|keep|keep\\-syntax|keep\\-syntax!|keys|keyword|keyword/slice|keyword\\?|kvs|last|length|lengthable\\?|load\\-image|macex|macex1|maclintf|make\\-env|make\\-image|map|mapcat|marshal|math/abs|math/acos|math/acosh|math/asin|math/asinh|math/atan|math/atan2|math/atanh|math/cbrt|math/ceil|math/cos|math/cosh|math/erf|math/erfc|math/exp|math/exp2|math/expm1|math/floor|math/gamma|math/gcd|math/hypot|math/lcm|math/log|math/log\\-gamma|math/log10|math/log1p|math/log2|math/next|math/pow|math/random|math/rng|math/rng\\-buffer|math/rng\\-int|math/rng\\-uniform|math/round|math/seedrandom|math/sin|math/sinh|math/sqrt|math/tan|math/tanh|math/trunc|max|max\\-of|mean|memcmp|merge|merge\\-into|merge\\-module|min|min\\-of|mod|module/add\\-paths|module/expand\\-path|module/find|module/value|nan\\?|nat\\?|native|neg\\?|net/accept|net/accept\\-loop|net/address|net/address\\-unpack|net/chunk|net/close|net/connect|net/flush|net/listen|net/localname|net/peername|net/read|net/recv\\-from|net/send\\-to|net/server|net/setsockopt|net/shutdown|net/write|next|nil\\?|not|not=|number\\?|odd\\?|one\\?|os/arch|os/cd|os/chmod|os/clock|os/compiler|os/cpu\\-count|os/cryptorand|os/cwd|os/date|os/dir|os/environ|os/execute|os/exit|os/getenv|os/isatty|os/link|os/lstat|os/mkdir|os/mktime|os/open|os/perm\\-int|os/perm\\-string|os/pipe|os/posix\\-exec|os/posix\\-fork|os/proc\\-close|os/proc\\-kill|os/proc\\-wait|os/readlink|os/realpath|os/rename|os/rm|os/rmdir|os/setenv|os/shell|os/sigaction|os/sleep|os/spawn|os/stat|os/strftime|os/symlink|os/time|os/touch|os/umask|os/which|pairs|parse|parse\\-all|parser/byte|parser/clone|parser/consume|parser/eof|parser/error|parser/flush|parser/has\\-more|parser/insert|parser/new|parser/produce|parser/state|parser/status|parser/where|partial|partition|partition\\-by|peg/compile|peg/find|peg/find\\-all|peg/match|peg/replace|peg/replace\\-all|pos\\?|postwalk|pp|prewalk|prin|prinf|print|printf|product|propagate|put|put\\-in|quit|range|reduce|reduce2|repl|require|resume|return|reverse|reverse!|run\\-context|sandbox|scan\\-number|setdyn|signal|slice|slurp|some|sort|sort\\-by|sorted|sorted\\-by|spit|string|string/ascii\\-lower|string/ascii\\-upper|string/bytes|string/check\\-set|string/find|string/find\\-all|string/format|string/from\\-bytes|string/has\\-prefix\\?|string/has\\-suffix\\?|string/join|string/repeat|string/replace|string/replace\\-all|string/reverse|string/slice|string/split|string/trim|string/triml|string/trimr|string\\?|struct|struct/getproto|struct/proto\\-flatten|struct/to\\-table|struct/with\\-proto|struct\\?|sum|symbol|symbol/slice|symbol\\?|table|table/clear|table/clone|table/getproto|table/new|table/proto\\-flatten|table/rawget|table/setproto|table/to\\-struct|table/weak|table/weak\\-keys|table/weak\\-values|table\\?|take|take\\-until|take\\-while|tarray/buffer|tarray/copy\\-bytes|tarray/length|tarray/new|tarray/properties|tarray/slice|tarray/swap\\-bytes|thread/close|thread/current|thread/exit|thread/new|thread/receive|thread/send|thaw|trace|true\\?|truthy\\?|tuple|tuple/brackets|tuple/setmap|tuple/slice|tuple/sourcemap|tuple/type|tuple\\?|type|unmarshal|untrace|update|update\\-in|values|varglobal|walk|warn\\-compile|xprin|xprinf|xprint|xprintf|yield|zero\\?|zipcoll)$")) + +;; other calls +(par_tup_lit + . + (sym_lit) @function) + +(sym_lit) @variable + +["{" "@{" "}" + "[" "@[" "]" + "(" "@(" ")"] @punctuation.bracket + +["~" "'" "|" ";" ","] @operator From 87a720c3a13ccc7245f5b0befc008db5bd039032 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 28 Jan 2024 17:34:45 +0100 Subject: [PATCH 36/70] make path changes LSP spec conform (#8949) Currently, helix implements operations which change the paths of files incorrectly and inconsistently. This PR ensures that we do the following whenever a buffer is renamed (`:move` and workspace edits) * always send did_open/did_close notifications * send will_rename/did_rename requests correctly * send them to all LSP servers not just those that are active for a buffer * also send these requests for paths that are not yet open in a buffer (if triggered from workspace edit). * only send these if the server registered interests in the path * autodetect language, indent, line ending, .. This PR also centralizes the infrastructure for path setting and therefore `:w ` benefits from similar fixed (but without didRename) --- helix-lsp/src/client.rs | 83 ++++++----- helix-lsp/src/file_operations.rs | 105 ++++++++++++++ helix-lsp/src/lib.rs | 1 + helix-term/src/application.rs | 31 +---- helix-term/src/commands/lsp.rs | 198 +------------------------- helix-term/src/commands/typed.rs | 62 +-------- helix-view/src/document.rs | 3 + helix-view/src/editor.rs | 90 +++++++++++- helix-view/src/handlers/lsp.rs | 229 +++++++++++++++++++++++++++++++ 9 files changed, 483 insertions(+), 319 deletions(-) create mode 100644 helix-lsp/src/file_operations.rs diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index fb32f6eb3..94bad6faf 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1,4 +1,5 @@ use crate::{ + file_operations::FileOperationsInterest, find_lsp_workspace, jsonrpc, transport::{Payload, Transport}, Call, Error, OffsetEncoding, Result, @@ -9,20 +10,20 @@ use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_stdx::path; use lsp::{ notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport, - DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, WorkspaceFolder, - WorkspaceFoldersChangeEvent, + DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url, + WorkspaceFolder, WorkspaceFoldersChangeEvent, }; use lsp_types as lsp; use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; -use std::future::Future; -use std::process::Stdio; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; use std::{collections::HashMap, path::PathBuf}; +use std::{future::Future, sync::OnceLock}; +use std::{path::Path, process::Stdio}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, @@ -51,6 +52,7 @@ pub struct Client { server_tx: UnboundedSender, request_counter: AtomicU64, pub(crate) capabilities: OnceCell, + pub(crate) file_operation_interest: OnceLock, config: Option, root_path: std::path::PathBuf, root_uri: Option, @@ -233,6 +235,7 @@ impl Client { server_tx, request_counter: AtomicU64::new(0), capabilities: OnceCell::new(), + file_operation_interest: OnceLock::new(), config, req_timeout, root_path, @@ -278,6 +281,11 @@ impl Client { .expect("language server not yet initialized!") } + pub(crate) fn file_operations_intests(&self) -> &FileOperationsInterest { + self.file_operation_interest + .get_or_init(|| FileOperationsInterest::new(self.capabilities())) + } + /// Client has to be initialized otherwise this function panics #[inline] pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool { @@ -717,27 +725,27 @@ impl Client { }) } - pub fn prepare_file_rename( + pub fn will_rename( &self, - old_uri: &lsp::Url, - new_uri: &lsp::Url, + old_path: &Path, + new_path: &Path, + is_dir: bool, ) -> Option>> { - let capabilities = self.capabilities.get().unwrap(); - - // Return early if the server does not support willRename feature - match &capabilities.workspace { - Some(workspace) => match &workspace.file_operations { - Some(op) => { - op.will_rename.as_ref()?; - } - _ => return None, - }, - _ => return None, + let capabilities = self.file_operations_intests(); + if !capabilities.will_rename.has_interest(old_path, is_dir) { + return None; } - + let url_from_path = |path| { + let url = if is_dir { + Url::from_directory_path(path) + } else { + Url::from_file_path(path) + }; + Some(url.ok()?.to_string()) + }; let files = vec![lsp::FileRename { - old_uri: old_uri.to_string(), - new_uri: new_uri.to_string(), + old_uri: url_from_path(old_path)?, + new_uri: url_from_path(new_path)?, }]; let request = self.call_with_timeout::( lsp::RenameFilesParams { files }, @@ -751,27 +759,28 @@ impl Client { }) } - pub fn did_file_rename( + pub fn did_rename( &self, - old_uri: &lsp::Url, - new_uri: &lsp::Url, + old_path: &Path, + new_path: &Path, + is_dir: bool, ) -> Option>> { - let capabilities = self.capabilities.get().unwrap(); - - // Return early if the server does not support DidRename feature - match &capabilities.workspace { - Some(workspace) => match &workspace.file_operations { - Some(op) => { - op.did_rename.as_ref()?; - } - _ => return None, - }, - _ => return None, + let capabilities = self.file_operations_intests(); + if !capabilities.did_rename.has_interest(new_path, is_dir) { + return None; } + let url_from_path = |path| { + let url = if is_dir { + Url::from_directory_path(path) + } else { + Url::from_file_path(path) + }; + Some(url.ok()?.to_string()) + }; let files = vec![lsp::FileRename { - old_uri: old_uri.to_string(), - new_uri: new_uri.to_string(), + old_uri: url_from_path(old_path)?, + new_uri: url_from_path(new_path)?, }]; Some(self.notify::(lsp::RenameFilesParams { files })) } diff --git a/helix-lsp/src/file_operations.rs b/helix-lsp/src/file_operations.rs new file mode 100644 index 000000000..98ac32a40 --- /dev/null +++ b/helix-lsp/src/file_operations.rs @@ -0,0 +1,105 @@ +use std::path::Path; + +use globset::{GlobBuilder, GlobSet}; + +use crate::lsp; + +#[derive(Default, Debug)] +pub(crate) struct FileOperationFilter { + dir_globs: GlobSet, + file_globs: GlobSet, +} + +impl FileOperationFilter { + fn new(capability: Option<&lsp::FileOperationRegistrationOptions>) -> FileOperationFilter { + let Some(cap) = capability else { + return FileOperationFilter::default(); + }; + let mut dir_globs = GlobSet::builder(); + let mut file_globs = GlobSet::builder(); + for filter in &cap.filters { + // TODO: support other url schemes + let is_non_file_schema = filter + .scheme + .as_ref() + .is_some_and(|schema| schema != "file"); + if is_non_file_schema { + continue; + } + let ignore_case = filter + .pattern + .options + .as_ref() + .and_then(|opts| opts.ignore_case) + .unwrap_or(false); + let mut glob_builder = GlobBuilder::new(&filter.pattern.glob); + glob_builder.case_insensitive(!ignore_case); + let glob = match glob_builder.build() { + Ok(glob) => glob, + Err(err) => { + log::error!("invalid glob send by LS: {err}"); + continue; + } + }; + match filter.pattern.matches { + Some(lsp::FileOperationPatternKind::File) => { + file_globs.add(glob); + } + Some(lsp::FileOperationPatternKind::Folder) => { + dir_globs.add(glob); + } + None => { + file_globs.add(glob.clone()); + dir_globs.add(glob); + } + }; + } + let file_globs = file_globs.build().unwrap_or_else(|err| { + log::error!("invalid globs send by LS: {err}"); + GlobSet::empty() + }); + let dir_globs = dir_globs.build().unwrap_or_else(|err| { + log::error!("invalid globs send by LS: {err}"); + GlobSet::empty() + }); + FileOperationFilter { + dir_globs, + file_globs, + } + } + + pub(crate) fn has_interest(&self, path: &Path, is_dir: bool) -> bool { + if is_dir { + self.dir_globs.is_match(path) + } else { + self.file_globs.is_match(path) + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct FileOperationsInterest { + // TODO: support other notifications + // did_create: FileOperationFilter, + // will_create: FileOperationFilter, + pub did_rename: FileOperationFilter, + pub will_rename: FileOperationFilter, + // did_delete: FileOperationFilter, + // will_delete: FileOperationFilter, +} + +impl FileOperationsInterest { + pub fn new(capabilities: &lsp::ServerCapabilities) -> FileOperationsInterest { + let capabilities = capabilities + .workspace + .as_ref() + .and_then(|capabilities| capabilities.file_operations.as_ref()); + let Some(capabilities) = capabilities else { + return FileOperationsInterest::default(); + }; + FileOperationsInterest { + did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()), + will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()), + } + } +} diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 53b2712d0..4ce445aee 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -1,5 +1,6 @@ mod client; pub mod file_event; +mod file_operations; pub mod jsonrpc; pub mod snippet; mod transport; diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3f3e59c6a..b5150a13a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -21,7 +21,6 @@ use tui::backend::Backend; use crate::{ args::Args, - commands::apply_workspace_edit, compositor::{Compositor, Event}, config::Config, handlers, @@ -573,26 +572,8 @@ impl Application { let lines = doc_save_event.text.len_lines(); let bytes = doc_save_event.text.len_bytes(); - if doc.path() != Some(&doc_save_event.path) { - doc.set_path(Some(&doc_save_event.path)); - - let loader = self.editor.syn_loader.clone(); - - // borrowing the same doc again to get around the borrow checker - let doc = doc_mut!(self.editor, &doc_save_event.doc_id); - let id = doc.id(); - doc.detect_language(loader); - self.editor.refresh_language_servers(id); - // and again a borrow checker workaround... - let doc = doc_mut!(self.editor, &doc_save_event.doc_id); - let diagnostics = Editor::doc_diagnostics( - &self.editor.language_servers, - &self.editor.diagnostics, - doc, - ); - doc.replace_diagnostics(diagnostics, &[], None); - } - + self.editor + .set_doc_path(doc_save_event.doc_id, &doc_save_event.path); // TODO: fix being overwritten by lsp self.editor.set_status(format!( "'{}' written, {}L {}B", @@ -1011,11 +992,9 @@ impl Application { let language_server = language_server!(); if language_server.is_initialized() { let offset_encoding = language_server.offset_encoding(); - let res = apply_workspace_edit( - &mut self.editor, - offset_encoding, - ¶ms.edit, - ); + let res = self + .editor + .apply_workspace_edit(offset_encoding, ¶ms.edit); Ok(json!(lsp::ApplyWorkspaceEditResponse { applied: res.is_ok(), diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index c694ba25c..a1f7bf17d 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -726,8 +726,7 @@ pub fn code_action(cx: &mut Context) { resolved_code_action.as_ref().unwrap_or(code_action); if let Some(ref workspace_edit) = resolved_code_action.edit { - log::debug!("edit: {:?}", workspace_edit); - let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit); + let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit); } // if code action provides both edit and command first the edit @@ -787,63 +786,6 @@ pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: }); } -pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { - use lsp::ResourceOp; - use std::fs; - match op { - ResourceOp::Create(op) => { - let path = op.uri.to_file_path().unwrap(); - 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 && path.exists() { - Ok(()) - } else { - // Create directory if it does not exist - if let Some(dir) = path.parent() { - if !dir.is_dir() { - fs::create_dir_all(dir)?; - } - } - - fs::write(&path, []) - } - } - ResourceOp::Delete(op) => { - let path = op.uri.to_file_path().unwrap(); - if path.is_dir() { - let recursive = op - .options - .as_ref() - .and_then(|options| options.recursive) - .unwrap_or(false); - - if recursive { - fs::remove_dir_all(&path) - } else { - fs::remove_dir(&path) - } - } else if path.is_file() { - fs::remove_file(&path) - } else { - Ok(()) - } - } - ResourceOp::Rename(op) => { - let from = op.old_uri.to_file_path().unwrap(); - let to = op.new_uri.to_file_path().unwrap(); - 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() { - Ok(()) - } else { - fs::rename(from, &to) - } - } - } -} - #[derive(Debug)] pub struct ApplyEditError { pub kind: ApplyEditErrorKind, @@ -871,142 +813,6 @@ impl ToString for ApplyEditErrorKind { } } -///TODO make this transactional (and set failureMode to transactional) -pub fn apply_workspace_edit( - editor: &mut Editor, - offset_encoding: OffsetEncoding, - workspace_edit: &lsp::WorkspaceEdit, -) -> Result<(), ApplyEditError> { - let mut apply_edits = |uri: &helix_lsp::Url, - version: Option, - text_edits: Vec| - -> 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); - editor.set_error(err); - return Err(ApplyEditErrorKind::UnknownURISchema); - } - }; - - let doc_id = match editor.open(&path, Action::Load) { - Ok(doc_id) => doc_id, - Err(err) => { - let err = format!("failed to open document: {}: {}", uri, err); - log::error!("{}", err); - editor.set_error(err); - return Err(ApplyEditErrorKind::FileNotFound); - } - }; - - let doc = doc!(editor, &doc_id); - if let Some(version) = version { - if version != doc.version() { - let err = format!("outdated workspace edit for {path:?}"); - log::error!("{err}, expected {} but got {version}", doc.version()); - editor.set_error(err); - return Err(ApplyEditErrorKind::DocumentChanged); - } - } - - // Need to determine a view for apply/append_changes_to_history - let view_id = editor.get_synced_view_id(doc_id); - let doc = doc_mut!(editor, &doc_id); - - let transaction = helix_lsp::util::generate_transaction_from_edits( - doc.text(), - text_edits, - offset_encoding, - ); - let view = view_mut!(editor, view_id); - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view); - Ok(()) - }; - - if let Some(ref document_changes) = workspace_edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(document_edits) => { - for (i, document_edit) in document_edits.iter().enumerate() { - let edits = document_edit - .edits - .iter() - .map(|edit| match edit { - lsp::OneOf::Left(text_edit) => text_edit, - lsp::OneOf::Right(annotated_text_edit) => { - &annotated_text_edit.text_edit - } - }) - .cloned() - .collect(); - apply_edits( - &document_edit.text_document.uri, - document_edit.text_document.version, - edits, - ) - .map_err(|kind| ApplyEditError { - kind, - failed_change_idx: i, - })?; - } - } - lsp::DocumentChanges::Operations(operations) => { - log::debug!("document changes - operations: {:?}", operations); - for (i, operation) in operations.iter().enumerate() { - match operation { - lsp::DocumentChangeOperation::Op(op) => { - apply_document_resource_op(op).map_err(|io| ApplyEditError { - kind: ApplyEditErrorKind::IoError(io), - failed_change_idx: i, - })?; - } - - lsp::DocumentChangeOperation::Edit(document_edit) => { - let edits = document_edit - .edits - .iter() - .map(|edit| match edit { - lsp::OneOf::Left(text_edit) => text_edit, - lsp::OneOf::Right(annotated_text_edit) => { - &annotated_text_edit.text_edit - } - }) - .cloned() - .collect(); - apply_edits( - &document_edit.text_document.uri, - document_edit.text_document.version, - edits, - ) - .map_err(|kind| ApplyEditError { - kind, - failed_change_idx: i, - })?; - } - } - } - } - } - - return Ok(()); - } - - if let Some(ref changes) = workspace_edit.changes { - log::debug!("workspace changes: {:?}", changes); - for (i, (uri, text_edits)) in changes.iter().enumerate() { - let text_edits = text_edits.to_vec(); - apply_edits(uri, None, text_edits).map_err(|kind| ApplyEditError { - kind, - failed_change_idx: i, - })?; - } - } - - Ok(()) -} - /// Precondition: `locations` should be non-empty. fn goto_impl( editor: &mut Editor, @@ -1263,7 +1069,7 @@ pub fn rename_symbol(cx: &mut Context) { match block_on(future) { Ok(edits) => { - let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); + let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits); } Err(err) => cx.editor.set_error(err.to_string()), } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 81ffdf875..b7ceeba59 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -8,7 +8,6 @@ use super::*; use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; use helix_core::{encoding, line_ending, shellwords::Shellwords}; -use helix_lsp::{OffsetEncoding, Url}; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::editor::{Action, CloseError, ConfigEvent}; use serde_json::Value; @@ -2404,67 +2403,14 @@ fn move_buffer( ensure!(args.len() == 1, format!(":move takes one argument")); let doc = doc!(cx.editor); - - let new_path = - helix_stdx::path::canonicalize(&PathBuf::from(args.first().unwrap().to_string())); let old_path = doc .path() - .ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))? + .context("Scratch buffer cannot be moved. Use :write instead")? .clone(); - let old_path_as_url = doc.url().unwrap(); - let new_path_as_url = Url::from_file_path(&new_path).unwrap(); - - let edits: Vec<( - helix_lsp::Result, - OffsetEncoding, - String, - )> = doc - .language_servers() - .map(|lsp| { - ( - lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url), - lsp.offset_encoding(), - lsp.name().to_owned(), - ) - }) - .filter(|(f, _, _)| f.is_some()) - .map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name)) - .collect(); - - for (lsp_reply, encoding, name) in edits { - match lsp_reply { - Ok(edit) => { - if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) { - log::error!( - ":move command failed to apply edits from lsp {}: {:?}", - name, - e - ); - }; - } - Err(e) => { - log::error!("LSP {} failed to treat willRename request: {:?}", name, e); - } - }; + let new_path = args.first().unwrap().to_string(); + if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) { + bail!("Could not move file: {err}"); } - - let doc = doc_mut!(cx.editor); - - doc.set_path(Some(new_path.as_path())); - if let Err(e) = std::fs::rename(&old_path, &new_path) { - doc.set_path(Some(old_path.as_path())); - bail!("Could not move file: {}", e); - }; - - doc.language_servers().for_each(|lsp| { - lsp.did_file_rename(&old_path_as_url, &new_path_as_url); - }); - - cx.editor - .language_servers - .file_event_handler - .file_changed(new_path); - Ok(()) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 88653948c..33137c6c9 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1041,6 +1041,9 @@ impl Document { self.encoding } + /// sets the document path without sending events to various + /// observers (like LSP), in most cases `Editor::set_doc_path` + /// should be used instead pub fn set_path(&mut self, path: Option<&Path>) { let path = path.map(helix_stdx::path::canonicalize); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index eca488e74..db0d4030b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -23,7 +23,8 @@ use std::{ borrow::Cow, cell::Cell, collections::{BTreeMap, HashMap}, - io::stdin, + fs, + io::{self, stdin}, num::NonZeroUsize, path::{Path, PathBuf}, pin::Pin, @@ -45,6 +46,7 @@ use helix_core::{ }; use helix_dap as dap; use helix_lsp::lsp; +use helix_stdx::path::canonicalize; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -1215,6 +1217,90 @@ impl Editor { self.launch_language_servers(doc_id) } + /// moves/renames a path, invoking any event handlers (currently only lsp) + /// and calling `set_doc_path` if the file is open in the editor + pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> { + let new_path = canonicalize(new_path); + // sanity check + if old_path == new_path { + return Ok(()); + } + let is_dir = old_path.is_dir(); + let language_servers: Vec<_> = self + .language_servers + .iter_clients() + .filter(|client| client.is_initialized()) + .cloned() + .collect(); + for language_server in language_servers { + let Some(request) = language_server.will_rename(old_path, &new_path, is_dir) else { + continue; + }; + let edit = match helix_lsp::block_on(request) { + Ok(edit) => edit, + Err(err) => { + log::error!("invalid willRename response: {err:?}"); + continue; + } + }; + if let Err(err) = self.apply_workspace_edit(language_server.offset_encoding(), &edit) { + log::error!("failed to apply workspace edit: {err:?}") + } + } + fs::rename(old_path, &new_path)?; + if let Some(doc) = self.document_by_path(old_path) { + self.set_doc_path(doc.id(), &new_path); + } + let is_dir = new_path.is_dir(); + for ls in self.language_servers.iter_clients() { + if let Some(notification) = ls.did_rename(old_path, &new_path, is_dir) { + tokio::spawn(notification); + }; + } + self.language_servers + .file_event_handler + .file_changed(old_path.to_owned()); + self.language_servers + .file_event_handler + .file_changed(new_path); + Ok(()) + } + + pub fn set_doc_path(&mut self, doc_id: DocumentId, path: &Path) { + let doc = doc_mut!(self, &doc_id); + let old_path = doc.path(); + + if let Some(old_path) = old_path { + // sanity check, should not occur but some callers (like an LSP) may + // create bogus calls + if old_path == path { + return; + } + // if we are open in LSPs send did_close notification + for language_server in doc.language_servers() { + tokio::spawn(language_server.text_document_did_close(doc.identifier())); + } + } + // we need to clear the list of language servers here so that + // refresh_doc_language/refresh_language_servers doesn't resend + // text_document_did_close. Since we called `text_document_did_close` + // we have fully unregistered this document from its LS + doc.language_servers.clear(); + doc.set_path(Some(path)); + self.refresh_doc_language(doc_id) + } + + pub fn refresh_doc_language(&mut self, doc_id: DocumentId) { + let loader = self.syn_loader.clone(); + let doc = doc_mut!(self, &doc_id); + doc.detect_language(loader); + doc.detect_indent_and_line_ending(); + self.refresh_language_servers(doc_id); + let doc = doc_mut!(self, &doc_id); + let diagnostics = Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, doc); + doc.replace_diagnostics(diagnostics, &[], None); + } + /// Launch a language server for a given document fn launch_language_servers(&mut self, doc_id: DocumentId) { if !self.config().lsp.enable { @@ -1257,7 +1343,7 @@ impl Editor { .collect::>() }); - if language_servers.is_empty() { + if language_servers.is_empty() && doc.language_servers.is_empty() { return; } diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index 1dae45dd5..beb106b2b 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -1,4 +1,8 @@ +use crate::editor::Action; +use crate::Editor; use crate::{DocumentId, ViewId}; +use helix_lsp::util::generate_transaction_from_edits; +use helix_lsp::{lsp, OffsetEncoding}; pub enum CompletionEvent { /// Auto completion was triggered by typing a word char @@ -39,3 +43,228 @@ pub enum SignatureHelpEvent { Cancel, RequestComplete { open: bool }, } + +#[derive(Debug)] +pub struct ApplyEditError { + pub kind: ApplyEditErrorKind, + pub failed_change_idx: usize, +} + +#[derive(Debug)] +pub enum ApplyEditErrorKind { + DocumentChanged, + FileNotFound, + UnknownURISchema, + IoError(std::io::Error), + // TODO: check edits before applying and propagate failure + // InvalidEdit, +} + +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::IoError(err) => err.to_string(), + } + } +} + +impl Editor { + fn apply_text_edits( + &mut self, + uri: &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 doc_id = match self.open(&path, Action::Load) { + Ok(doc_id) => doc_id, + Err(err) => { + let err = format!("failed to open document: {}: {}", uri, err); + log::error!("{}", err); + self.set_error(err); + return Err(ApplyEditErrorKind::FileNotFound); + } + }; + + let doc = doc_mut!(self, &doc_id); + if let Some(version) = version { + if version != doc.version() { + let err = format!("outdated workspace edit for {path:?}"); + log::error!("{err}, expected {} but got {version}", doc.version()); + self.set_error(err); + return Err(ApplyEditErrorKind::DocumentChanged); + } + } + + // Need to determine a view for apply/append_changes_to_history + let view_id = self.get_synced_view_id(doc_id); + let doc = doc_mut!(self, &doc_id); + + let transaction = generate_transaction_from_edits(doc.text(), text_edits, offset_encoding); + let view = view_mut!(self, view_id); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view); + Ok(()) + } + + // TODO make this transactional (and set failureMode to transactional) + pub fn apply_workspace_edit( + &mut self, + offset_encoding: OffsetEncoding, + workspace_edit: &lsp::WorkspaceEdit, + ) -> Result<(), ApplyEditError> { + if let Some(ref document_changes) = workspace_edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(document_edits) => { + for (i, document_edit) in document_edits.iter().enumerate() { + let edits = document_edit + .edits + .iter() + .map(|edit| match edit { + lsp::OneOf::Left(text_edit) => text_edit, + lsp::OneOf::Right(annotated_text_edit) => { + &annotated_text_edit.text_edit + } + }) + .cloned() + .collect(); + self.apply_text_edits( + &document_edit.text_document.uri, + document_edit.text_document.version, + edits, + offset_encoding, + ) + .map_err(|kind| ApplyEditError { + kind, + failed_change_idx: i, + })?; + } + } + lsp::DocumentChanges::Operations(operations) => { + log::debug!("document changes - operations: {:?}", operations); + for (i, operation) in operations.iter().enumerate() { + match operation { + lsp::DocumentChangeOperation::Op(op) => { + self.apply_document_resource_op(op).map_err(|io| { + ApplyEditError { + kind: ApplyEditErrorKind::IoError(io), + failed_change_idx: i, + } + })?; + } + + lsp::DocumentChangeOperation::Edit(document_edit) => { + let edits = document_edit + .edits + .iter() + .map(|edit| match edit { + lsp::OneOf::Left(text_edit) => text_edit, + lsp::OneOf::Right(annotated_text_edit) => { + &annotated_text_edit.text_edit + } + }) + .cloned() + .collect(); + self.apply_text_edits( + &document_edit.text_document.uri, + document_edit.text_document.version, + edits, + offset_encoding, + ) + .map_err(|kind| { + ApplyEditError { + kind, + failed_change_idx: i, + } + })?; + } + } + } + } + } + + return Ok(()); + } + + if let Some(ref changes) = workspace_edit.changes { + log::debug!("workspace changes: {:?}", changes); + for (i, (uri, text_edits)) in changes.iter().enumerate() { + let text_edits = text_edits.to_vec(); + self.apply_text_edits(uri, None, text_edits, offset_encoding) + .map_err(|kind| ApplyEditError { + kind, + failed_change_idx: i, + })?; + } + } + + Ok(()) + } + + fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Result<()> { + use lsp::ResourceOp; + use std::fs; + match op { + ResourceOp::Create(op) => { + let path = op.uri.to_file_path().unwrap(); + 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 || !path.exists() { + // Create directory if it does not exist + if let Some(dir) = path.parent() { + if !dir.is_dir() { + fs::create_dir_all(dir)?; + } + } + + fs::write(&path, [])?; + self.language_servers.file_event_handler.file_changed(path); + } + } + ResourceOp::Delete(op) => { + let path = op.uri.to_file_path().unwrap(); + if path.is_dir() { + let recursive = op + .options + .as_ref() + .and_then(|options| options.recursive) + .unwrap_or(false); + + if recursive { + fs::remove_dir_all(&path)? + } else { + fs::remove_dir(&path)? + } + self.language_servers.file_event_handler.file_changed(path); + } else if path.is_file() { + 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 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)?; + } + } + } + Ok(()) + } +} From cf4492174d0ee27bd3c73a5fa57fe3a26aa064be Mon Sep 17 00:00:00 2001 From: Waleed Dahshan <58462210+wmstack@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:58:33 +1100 Subject: [PATCH 37/70] Use range positions to determine insert_newline motion (#9448) * use anchor and head positions to determine motion * use range cursor to decide extending or shifting * add condition to cursor moving back on normal --- helix-term/src/commands.rs | 2 +- helix-view/src/editor.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4df3278b8..d44f477b7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3651,7 +3651,7 @@ pub mod insert { (pos, pos, local_offs) }; - let new_range = if doc.restore_cursor { + let new_range = if range.cursor(text) > range.anchor { // when appending, extend the range by local_offs Range::new( range.anchor + global_offs, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index db0d4030b..0fa6d67c9 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1990,10 +1990,12 @@ impl Editor { if doc.restore_cursor { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { - Range::new( - range.from(), - graphemes::prev_grapheme_boundary(text, range.to()), - ) + let mut head = range.to(); + if range.head > range.anchor { + head = graphemes::prev_grapheme_boundary(text, head); + } + + Range::new(range.from(), head) }); doc.set_selection(view.id, selection); From dbac78bb3c06717f1f83984da3035e28f39d49a5 Mon Sep 17 00:00:00 2001 From: Andrew Carter Date: Wed, 31 Jan 2024 17:18:53 -0800 Subject: [PATCH 38/70] Set ui.virtual.ruler background for GitHub themes (#9487) Turning on a ruler does not show a visible ruler line for the GitHub themes. This change renders rulers using the `canvas.subtle` color. This matches the color used for the `cursorline` and creates a visible ruler that fits the theme. --- runtime/themes/github_dark.toml | 1 + runtime/themes/github_light.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/runtime/themes/github_dark.toml b/runtime/themes/github_dark.toml index 4f9aa562b..6b3354848 100644 --- a/runtime/themes/github_dark.toml +++ b/runtime/themes/github_dark.toml @@ -60,6 +60,7 @@ label = "scale.red.3" "ui.text.focus" = { fg = "fg.default" } "ui.text.inactive" = "fg.subtle" "ui.virtual" = { fg = "scale.gray.6" } +"ui.virtual.ruler" = { bg = "canvas.subtle" } "ui.selection" = { bg = "scale.blue.8" } "ui.selection.primary" = { bg = "scale.blue.7" } diff --git a/runtime/themes/github_light.toml b/runtime/themes/github_light.toml index 3e2269698..e6912a987 100644 --- a/runtime/themes/github_light.toml +++ b/runtime/themes/github_light.toml @@ -60,6 +60,7 @@ label = "scale.red.5" "ui.text.focus" = { fg = "fg.default" } "ui.text.inactive" = "fg.subtle" "ui.virtual" = { fg = "scale.gray.2" } +"ui.virtual.ruler" = { bg = "canvas.subtle" } "ui.selection" = { bg = "scale.blue.0" } "ui.selection.primary" = { bg = "scale.blue.1" } From dd596028099ea8bc2aa01f980137b22b5ff7b0b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:33:46 -0700 Subject: [PATCH 39/70] build(deps): bump pulldown-cmark from 0.9.3 to 0.9.6 (#9474) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4ac12e43..76213cf0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1653,11 +1653,11 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" [[package]] name = "pulldown-cmark" -version = "0.9.3" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "memchr", "unicase", ] From 5ec9565ddb352744aa4d5a8d4017033d899db01d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:34:05 -0700 Subject: [PATCH 40/70] build(deps): bump serde from 1.0.195 to 1.0.196 (#9473) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76213cf0a..dae1bece5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,18 +1825,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", From aa4241c973d0f5f69ab034ce54a6d2d372e38c3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:34:24 -0700 Subject: [PATCH 41/70] build(deps): bump chrono from 0.4.32 to 0.4.33 (#9472) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dae1bece5..09a019dc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", From 70cea93bff2ae94c4da5676cb1e157a0d8117e66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:34:49 -0700 Subject: [PATCH 42/70] build(deps): bump serde_json from 1.0.111 to 1.0.113 (#9471) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09a019dc9..a0986dcc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1845,9 +1845,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", From 81ae768a4ea20543bc16246a46552a69204fd7c1 Mon Sep 17 00:00:00 2001 From: Shoyu Vanilla Date: Sun, 4 Feb 2024 03:23:33 +0900 Subject: [PATCH 43/70] Use gix pipeline filter instead of manual crlf implementation (#9503) --- Cargo.lock | 192 +++++++++++++++++++++++++++++++++++++++++++ helix-vcs/Cargo.toml | 2 +- helix-vcs/src/git.rs | 38 ++++----- 3 files changed, 209 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0986dcc7..cb815ee3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + [[package]] name = "flate2" version = "1.0.27" @@ -527,33 +539,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31887c304d9a935f3e5494fb5d6a0106c34e965168ec0db9b457424eedd0c741" dependencies = [ "gix-actor", + "gix-attributes", + "gix-command", "gix-commitgraph", "gix-config", "gix-date", "gix-diff", "gix-discover", "gix-features", + "gix-filter", "gix-fs", "gix-glob", "gix-hash", "gix-hashtable", + "gix-ignore", + "gix-index", "gix-lock", "gix-macros", "gix-object", "gix-odb", "gix-pack", "gix-path", + "gix-pathspec", "gix-ref", "gix-refspec", "gix-revision", "gix-revwalk", "gix-sec", + "gix-submodule", "gix-tempfile", "gix-trace", "gix-traverse", "gix-url", "gix-utils", "gix-validate", + "gix-worktree", "once_cell", "parking_lot", "smallvec", @@ -574,6 +594,32 @@ dependencies = [ "winnow 0.5.28", ] +[[package]] +name = "gix-attributes" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ee3792e504ee1ce206b36dcafa4f328ca313d1e2ac0b41433d68ef4e14260" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b6cd0f246180034ddafac9b00a112f19178135b21eb031b3f79355891f7325" +dependencies = [ + "thiserror", +] + [[package]] name = "gix-chunk" version = "0.4.7" @@ -583,6 +629,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-command" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ffc7db3fb50b7dae6ecd937a3527cb725f444614df2ad8988d81806f13f09" +dependencies = [ + "bstr", + "gix-path", + "gix-trace", + "shell-words", +] + [[package]] name = "gix-commitgraph" version = "0.24.0" @@ -690,6 +748,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gix-filter" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9240862840fb740d209422937195e129e4ed3da49af212383260134bea8f6c1a" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror", +] + [[package]] name = "gix-fs" version = "0.10.0" @@ -733,6 +812,44 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "gix-ignore" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7069aaca4a05784c4cb44e392f0eaf627c6e57e05d3100c0e2386a37a682f0" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7152181ba8f0a3addc5075dd612cea31fc3e252b29c8be8c45f4892bf87426" +dependencies = [ + "bitflags 2.4.2", + "bstr", + "btoi", + "filetime", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror", +] + [[package]] name = "gix-lock" version = "13.0.0" @@ -814,6 +931,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-packetline-blocking" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ef6dd3ea50e26f3bf572e90c034d033c804d340cd1eb386392f184a9ba2f7" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + [[package]] name = "gix-path" version = "0.10.4" @@ -827,6 +956,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-pathspec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cbd49750edb26b0a691e5246fc635fa554d344da825cd20fa9ee0da9c1b761f" +dependencies = [ + "bitflags 2.4.2", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + [[package]] name = "gix-quote" version = "0.4.10" @@ -917,6 +1061,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "gix-submodule" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73182f6c1f5ed1ed94ba16581ac62593d5e29cd1c028b2af618f836283b8f8d4" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + [[package]] name = "gix-tempfile" version = "13.0.0" @@ -986,6 +1145,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-worktree" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca36bb3dc54038c66507dc75c4d8edbee2d6d5cc45227b4eb508ad13dd60a006" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", +] + [[package]] name = "globset" version = "0.4.14" @@ -1408,6 +1585,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kstring" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" +dependencies = [ + "static_assertions", +] + [[package]] name = "libc" version = "0.2.152" @@ -1880,6 +2066,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 051134e4a..6aa50dcf7 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.6.0" } -gix = { version = "0.58.0", default-features = false , optional = true } +gix = { version = "0.58.0", features = ["attributes"], default-features = false, optional = true } imara-diff = "0.1.5" anyhow = "1" diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index e4d45301a..995bade06 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -1,5 +1,7 @@ use anyhow::{bail, Context, Result}; use arc_swap::ArcSwap; +use gix::filter::plumbing::driver::apply::Delay; +use std::io::Read; use std::path::Path; use std::sync::Arc; @@ -76,29 +78,21 @@ impl DiffProvider for Git { let file_oid = find_file_in_commit(&repo, &head, file)?; let file_object = repo.find_object(file_oid)?; - let mut data = file_object.detach().data; - // convert LF to CRLF if configured to avoid showing every line as changed - if repo - .config_snapshot() - .boolean("core.autocrlf") - .unwrap_or(false) - { - let mut normalized_file = Vec::with_capacity(data.len()); - let mut at_cr = false; - for &byte in &data { - if byte == b'\n' { - // if this is a LF instead of a CRLF (last byte was not a CR) - // insert a new CR to generate a CRLF - if !at_cr { - normalized_file.push(b'\r'); - } - } - at_cr = byte == b'\r'; - normalized_file.push(byte) - } - data = normalized_file + let data = file_object.detach().data; + // Get the actual data that git would make out of the git object. + // This will apply the user's git config or attributes like crlf conversions. + if let Some(work_dir) = repo.work_dir() { + let rela_path = file.strip_prefix(work_dir)?; + let rela_path = gix::path::try_into_bstr(rela_path)?; + let (mut pipeline, _) = repo.filter_pipeline(None)?; + let mut worktree_outcome = + pipeline.convert_to_worktree(&data, rela_path.as_ref(), Delay::Forbid)?; + let mut buf = Vec::with_capacity(data.len()); + worktree_outcome.read_to_end(&mut buf)?; + Ok(buf) + } else { + Ok(data) } - Ok(data) } fn get_current_head_name(&self, file: &Path) -> Result>>> { From d54545281956bd92f52aad0ced008ae093df2b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Sat, 3 Feb 2024 23:27:40 +0000 Subject: [PATCH 44/70] feat(queries): regex injection for golang (#9510) --- runtime/queries/go/injections.scm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/runtime/queries/go/injections.scm b/runtime/queries/go/injections.scm index 321c90add..d7b03da35 100644 --- a/runtime/queries/go/injections.scm +++ b/runtime/queries/go/injections.scm @@ -1,2 +1,14 @@ ((comment) @injection.content (#set! injection.language "comment")) + + +(call_expression + (selector_expression) @_function + (#any-of? @_function "regexp.Match" "regexp.MatchReader" "regexp.MatchString" "regexp.Compile" "regexp.CompilePOSIX" "regexp.MustCompile" "regexp.MustCompilePOSIX") + (argument_list + . + [ + (raw_string_literal) + (interpreted_string_literal) + ] @injection.content + (#set! injection.language "regex"))) From 3f380722fbb2fd676ccc0a9dbbea4ddc4871c6ea Mon Sep 17 00:00:00 2001 From: Devyn Cairns Date: Sat, 3 Feb 2024 17:04:51 -0800 Subject: [PATCH 45/70] Update grammars for Nushell to rev 358c4f50 (#9502) --- languages.toml | 2 +- runtime/queries/nu/highlights.scm | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/languages.toml b/languages.toml index ba3b19066..1f26f1680 100644 --- a/languages.toml +++ b/languages.toml @@ -1832,7 +1832,7 @@ language-servers = [ "nu-lsp" ] [[grammar]] name = "nu" -source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "98c11c491e3405c75affa1cf004097692da3dda2" } +source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "358c4f509eb97f0148bbd25ad36acc729819b9c1" } [[language]] name = "vala" diff --git a/runtime/queries/nu/highlights.scm b/runtime/queries/nu/highlights.scm index 746c50251..66a305840 100644 --- a/runtime/queries/nu/highlights.scm +++ b/runtime/queries/nu/highlights.scm @@ -2,7 +2,6 @@ ;;; keywords [ "def" - "def-env" "alias" "export-env" "export" @@ -73,7 +72,6 @@ "tb" "tB" "Tb" "TB" "pb" "pB" "Pb" "PB" "eb" "eB" "Eb" "EB" - "zb" "zB" "Zb" "ZB" "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB" "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB" @@ -81,7 +79,6 @@ "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB" "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB" "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB" - "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB" ] @variable.parameter ) (val_binary From d1054de3ced44903c7bdcf5886d8481eb40a948f Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sun, 4 Feb 2024 02:09:11 +0100 Subject: [PATCH 46/70] feat: Add `Tact` language support (#9512) Re-submitting --- book/src/generated/lang-support.md | 1 + languages.toml | 19 ++ runtime/queries/tact/highlights.scm | 298 +++++++++++++++++++++++++++ runtime/queries/tact/indents.scm | 38 ++++ runtime/queries/tact/injections.scm | 5 + runtime/queries/tact/locals.scm | 35 ++++ runtime/queries/tact/textobjects.scm | 58 ++++++ 7 files changed, 454 insertions(+) create mode 100644 runtime/queries/tact/highlights.scm create mode 100644 runtime/queries/tact/indents.scm create mode 100644 runtime/queries/tact/injections.scm create mode 100644 runtime/queries/tact/locals.scm create mode 100644 runtime/queries/tact/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index a78dd7935..e7d29cc73 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -162,6 +162,7 @@ | swift | ✓ | | | `sourcekit-lsp` | | t32 | ✓ | | | | | tablegen | ✓ | ✓ | ✓ | | +| tact | ✓ | ✓ | ✓ | | | task | ✓ | | | | | templ | ✓ | | | `templ` | | tfvars | ✓ | | ✓ | `terraform-ls` | diff --git a/languages.toml b/languages.toml index 1f26f1680..e601c20d9 100644 --- a/languages.toml +++ b/languages.toml @@ -3042,3 +3042,22 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "hocon" source = { git = "https://github.com/antosha417/tree-sitter-hocon", rev = "c390f10519ae69fdb03b3e5764f5592fb6924bcc" } + +[[language]] +name = "tact" +scope = "source.tact" +injection-regex = "tact" +file-types = ["tact"] +comment-token = "//" +indent = { tab-width = 4, unit = " " } + +[language.auto-pairs] +'"' = '"' +'{' = '}' +'(' = ')' +'<' = '>' + +[[grammar]] +name = "tact" +source = { git = "https://github.com/tact-lang/tree-sitter-tact", rev = "ec57ab29c86d632639726631fb2bb178d23e1c91" } + diff --git a/runtime/queries/tact/highlights.scm b/runtime/queries/tact/highlights.scm new file mode 100644 index 000000000..53bf985b5 --- /dev/null +++ b/runtime/queries/tact/highlights.scm @@ -0,0 +1,298 @@ +; See: https://docs.helix-editor.com/master/themes.html#syntax-highlighting +; ------------------------------------------------------------------------- + +; attribute +; --------- + +[ + "@name" + "@interface" +] @attribute + +; comment.line +; ------------ + +((comment) @comment.line + (#match? @comment.line "^//")) + +; comment.block +; ------------- + +(comment) @comment.block + +; function.builtin +; ---------------- + +((identifier) @function.builtin + (#any-of? @function.builtin + "send" "sender" "require" "now" + "myBalance" "myAddress" "newAddress" + "contractAddress" "contractAddressExt" + "emit" "cell" "ton" + "beginString" "beginComment" "beginTailString" "beginStringFromBuilder" "beginCell" "emptyCell" + "randomInt" "random" + "checkSignature" "checkDataSignature" "sha256" + "min" "max" "abs" "pow" + "throw" "dump" "getConfigParam" + "nativeThrowWhen" "nativeThrowUnless" "nativeReserve" + "nativeRandomize" "nativeRandomizeLt" "nativePrepareRandom" "nativeRandom" "nativeRandomInterval") + (#is-not? local)) + +; function.method +; --------------- + +(method_call_expression + name: (identifier) @function.method) + +; function +; -------- + +(func_identifier) @function + +(native_function + name: (identifier) @function) + +(static_function + name: (identifier) @function) + +(static_call_expression + name: (identifier) @function) + +(init_function + "init" @function.method) + +(receive_function + "receive" @function.method) + +(bounced_function + "bounced" @function.method) + +(external_function + "external" @function.method) + +(function + name: (identifier) @function.method) + +; keyword.control.conditional +; --------------------------- + +[ + "if" "else" +] @keyword.control.conditional + +; keyword.control.repeat +; ---------------------- + +[ + "while" "repeat" "do" "until" +] @keyword.control.repeat + +; keyword.control.import +; ---------------------- + +"import" @keyword.control.import + +; keyword.control.return +; ---------------------- + +"return" @keyword.control.return + +; keyword.operator +; ---------------- + +"initOf" @keyword.operator + +; keyword.directive +; ----------------- + +"primitive" @keyword.directive + +; keyword.function +; ---------------- + +[ + "fun" + "native" +] @keyword.function + +; keyword.storage.type +; -------------------- + +[ + "contract" "trait" "struct" "message" "with" + "const" "let" +] @keyword.storage.type + +; keyword.storage.modifier +; ------------------------ + +[ + "get" "mutates" "extends" "virtual" "override" "inline" "abstract" +] @keyword.storage.modifier + +; keyword +; ------- + +[ + "with" + ; "public" ; -- not used, but declared in grammar.ohm + ; "extend" ; -- not used, but declared in grammar.ohm +] @keyword + +; constant.builtin.boolean +; ------------------------ + +(boolean) @constant.builtin.boolean + +; constant.builtin +; ---------------- + +((identifier) @constant.builtin + (#any-of? @constant.builtin + "SendPayGasSeparately" + "SendIgnoreErrors" + "SendDestroyIfZero" + "SendRemainingValue" + "SendRemainingBalance") + (#is-not? local)) + +(null) @constant.builtin + +; constant.numeric.integer +; ------------------------ + +(integer) @constant.numeric.integer + +; constant +; -------- + +(constant + name: (identifier) @constant) + +; string.special.path +; ------------------- + +(import_statement + library: (string) @string.special.path) + +; string +; ------ + +(string) @string + +; type.builtin +; ------------ + +(tlb_serialization + "as" @keyword + type: (identifier) @type.builtin + (#any-of? @type.builtin + "int8" "int16" "int32" "int64" "int128" "int256" "int257" + "uint8" "uint16" "uint32" "uint64" "uint128" "uint256" + "coins" "remaining" "bytes32" "bytes64")) + +((type_identifier) @type.builtin + (#any-of? @type.builtin + "Address" "Bool" "Builder" "Cell" "Int" "Slice" "String" "StringBuilder")) + +(map_type + "map" @type.builtin + "<" @punctuation.bracket + ">" @punctuation.bracket) + +(bounced_type + "bounced" @type.builtin + "<" @punctuation.bracket + ">" @punctuation.bracket) + +((identifier) @type.builtin + (#eq? @type.builtin "SendParameters") + (#is-not? local)) + +; type +; ---- + +(type_identifier) @type + +; constructor +; ----------- + +(instance_expression + name: (identifier) @constructor) + +(initOf + name: (identifier) @constructor) + +; operator +; -------- + +[ + "-" "-=" + "+" "+=" + "*" "*=" + "/" "/=" + "%" "%=" + "=" "==" + "!" "!=" "!!" + "<" "<=" "<<" + ">" ">=" ">>" + "&" "|" + "&&" "||" +] @operator + +; punctuation.bracket +; ------------------- + +[ + "(" ")" + "{" "}" +] @punctuation.bracket + +; punctuation.delimiter +; --------------------- + +[ + ";" + "," + "." + ":" + "?" +] @punctuation.delimiter + +; variable.other.member +; --------------------- + +(field + name: (identifier) @variable.other.member) + +(contract_body + (constant + name: (identifier) @variable.other.member)) + +(trait_body + (constant + name: (identifier) @variable.other.member)) + +(field_access_expression + name: (identifier) @variable.other.member) + +(lvalue (_) (_) @variable.other.member) + +(instance_argument + name: (identifier) @variable.other.member) + +; variable.parameter +; ------------------ + +(parameter + name: (identifier) @variable.parameter) + +; variable.builtin +; ---------------- + +(self) @variable.builtin + +; variable +; -------- + +(identifier) @variable diff --git a/runtime/queries/tact/indents.scm b/runtime/queries/tact/indents.scm new file mode 100644 index 000000000..62c532b22 --- /dev/null +++ b/runtime/queries/tact/indents.scm @@ -0,0 +1,38 @@ +; indent +; ------ + +[ + ; (..., ...) + (parameter_list) + (argument_list) + + ; {..., ...} + (instance_argument_list) + + ; {...; ...} + (message_body) + (struct_body) + (contract_body) + (trait_body) + (function_body) + (block_statement) + + ; misc. + (binary_expression) + (return_statement) +] @indent + +; outdent +; ------- + +[ + "}" + ")" + ">" +] @outdent + +; indent.always +; outdent.always +; align +; extend +; extend.prevent-once \ No newline at end of file diff --git a/runtime/queries/tact/injections.scm b/runtime/queries/tact/injections.scm new file mode 100644 index 000000000..e61db3a56 --- /dev/null +++ b/runtime/queries/tact/injections.scm @@ -0,0 +1,5 @@ +; See: https://docs.helix-editor.com/guides/injection.html + +((comment) @injection.content + (#set! injection.language "comment") + (#match? @injection.content "^//")) \ No newline at end of file diff --git a/runtime/queries/tact/locals.scm b/runtime/queries/tact/locals.scm new file mode 100644 index 000000000..f1b3e8de5 --- /dev/null +++ b/runtime/queries/tact/locals.scm @@ -0,0 +1,35 @@ +; See: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#local-variables + +; Scopes @local.scope +; ------------------------- + +[ + (static_function) + (init_function) + (bounced_function) + (receive_function) + (external_function) + (function) + (block_statement) +] @local.scope + +; Definitions @local.definition +; ------------------------------ + +(let_statement + name: (identifier) @local.definition) + +(parameter + name: (identifier) @local.definition) + +(constant + name: (identifier) @local.definition) + +; References @local.reference +; ----------------------------- + +(self) @local.reference + +(value_expression (identifier) @local.reference) + +(lvalue (identifier) @local.reference) diff --git a/runtime/queries/tact/textobjects.scm b/runtime/queries/tact/textobjects.scm new file mode 100644 index 000000000..54d07014e --- /dev/null +++ b/runtime/queries/tact/textobjects.scm @@ -0,0 +1,58 @@ +; function.inside & around +; ------------------------ + +(static_function + body: (_) @function.inside) @function.around + +(init_function + body: (_) @function.inside) @function.around + +(bounced_function + body: (_) @function.inside) @function.around + +(receive_function + body: (_) @function.inside) @function.around + +(external_function + body: (_) @function.inside) @function.around + +(function + body: (_) @function.inside) @function.around + +; class.inside & around +; --------------------- + +(struct + body: (_) @class.inside) @class.around + +(message + body: (_) @class.inside) @class.around + +(contract + body: (_) @class.inside) @class.around + +; NOTE: Marked as @definition.interface in tags, as it's semantically correct +(trait + body: (_) @class.inside) @class.around + +; parameter.inside & around +; ------------------------- + +(parameter_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(instance_argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +; comment.inside +; -------------- + +(comment) @comment.inside + +; comment.around +; -------------- + +(comment)+ @comment.around \ No newline at end of file From 75d61d8149d1e55d25f0bc34d95f01d6a3f1f526 Mon Sep 17 00:00:00 2001 From: Jaakko Paju Date: Sun, 4 Feb 2024 03:09:42 +0200 Subject: [PATCH 47/70] Improve tree-sitter queries for Scala (#9475) - Simplify function highlighting - Highlight extension methods - Textobject query (mia/maa) for class/trait constructor parameters/arguments - Textobject query (mif/maf) for Scala 3 braceless lambdas --- runtime/queries/scala/highlights.scm | 27 ++++++--------------------- runtime/queries/scala/textobjects.scm | 10 ++++++++-- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm index 4f90bfda6..e21a3909d 100644 --- a/runtime/queries/scala/highlights.scm +++ b/runtime/queries/scala/highlights.scm @@ -53,28 +53,13 @@ (var_declaration name: (identifier) @variable) -; method definition +; function definitions/declarations -(class_definition - body: (template_body - [ - (function_definition - name: (identifier) @function.method) - (function_declaration - name: (identifier) @function.method) - ])) -(trait_definition - body: (template_body - [ - (function_definition - name: (identifier) @function.method) - (function_declaration - name: (identifier) @function.method) - ])) -(object_definition - body: (template_body - (function_definition - name: (identifier) @function.method))) +(function_declaration + name: (identifier) @function.method) + +(function_definition + name: (identifier) @function.method) ; imports/exports diff --git a/runtime/queries/scala/textobjects.scm b/runtime/queries/scala/textobjects.scm index 6e551c417..21286b3ef 100644 --- a/runtime/queries/scala/textobjects.scm +++ b/runtime/queries/scala/textobjects.scm @@ -1,12 +1,15 @@ ; Function queries (function_definition - body: (_) @function.inside) @function.around + body: (_) @function.inside) @function.around ; Does not include end marker -; Does not match block lambdas or Scala 3 braceless lambdas (lambda_expression (_) @function.inside) @function.around +; Scala 3 braceless lambda +(colon_argument + (_) @function.inside) @function.around + ; Class queries @@ -32,6 +35,9 @@ (parameters ((_) @parameter.inside . ","? @parameter.around) @parameter.around) +(class_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + (parameter_types ((_) @parameter.inside . ","? @parameter.around) @parameter.around) From 6e3ed7f0fa91a3adc01f33e182999f606b48ce6a Mon Sep 17 00:00:00 2001 From: zetashift Date: Sun, 4 Feb 2024 02:10:20 +0100 Subject: [PATCH 48/70] Update Unison tree-sitter grammar for type changes and add indent queries (#9505) * Update Unison tree-sitter grammar for type changes * Add indent queries for Unison * Improve Unison indent queries --- book/src/generated/lang-support.md | 2 +- languages.toml | 2 +- runtime/queries/unison/highlights.scm | 4 +--- runtime/queries/unison/indents.scm | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 runtime/queries/unison/indents.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index e7d29cc73..ddd480536 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -174,7 +174,7 @@ | typescript | ✓ | ✓ | ✓ | `typescript-language-server` | | typst | ✓ | | | `typst-lsp` | | ungrammar | ✓ | | | | -| unison | ✓ | | | | +| unison | ✓ | | ✓ | | | uxntal | ✓ | | | | | v | ✓ | ✓ | ✓ | `v-analyzer` | | vala | ✓ | | | `vala-language-server` | diff --git a/languages.toml b/languages.toml index e601c20d9..11afea0ca 100644 --- a/languages.toml +++ b/languages.toml @@ -2940,7 +2940,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "unison" -source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "aaec316774c8b50d367ec7cf26523aac5ef0cfc5" } +source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "1f505e2447fa876a87aee47ff3d70b9e141c744f" } [[language]] name = "todotxt" diff --git a/runtime/queries/unison/highlights.scm b/runtime/queries/unison/highlights.scm index d58285ed8..711779295 100644 --- a/runtime/queries/unison/highlights.scm +++ b/runtime/queries/unison/highlights.scm @@ -9,8 +9,6 @@ ;; Keywords [ (kw_forall) - (unique_kw) - (structural_kw) (type_kw) (kw_equals) (do) @@ -51,7 +49,7 @@ (blank_pattern) @variable.builtin ;; Types -(record_field name: (wordy_id) @variable.other.member type: (wordy_id) @type) +(record_field name: (wordy_id) @variable.other.member type: (_) @type) (type_constructor (type_name (wordy_id) @constructor)) (ability_declaration type_name: (wordy_id) @type type_arg: (wordy_id) @variable.parameter) (effect (wordy_id) @special) ;; NOTE: an effect is just like a type, but in signature we special case it diff --git a/runtime/queries/unison/indents.scm b/runtime/queries/unison/indents.scm new file mode 100644 index 000000000..6cb15517c --- /dev/null +++ b/runtime/queries/unison/indents.scm @@ -0,0 +1,15 @@ +[ + (term_definition) + (type_declaration) + (pattern) + (tuple_or_parenthesized) + (literal_list) + (tuple_pattern) + (function_application) + (exp_if) + (constructor) + (delay_block) + (type_signature) +] @indent + +[(kw_then) (kw_else) (cases)] @indent.always @extend From 5c567f31e236134c0e27dc689c8ad07ef5f9b5d1 Mon Sep 17 00:00:00 2001 From: Doug Kelkhoff <18220321+dgkf@users.noreply.github.com> Date: Sat, 3 Feb 2024 20:11:20 -0500 Subject: [PATCH 49/70] Adding two themes using only colors from 16-color terminal themes (#9477) * adding 16-color terminal themes * minor consistency update * minor consistency update * rename to be more consistent with other helix theme name conventions * fixing improper theme inherits name --- runtime/themes/term16_dark.toml | 80 ++++++++++++++++++++++++++++++ runtime/themes/term16_light.toml | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 runtime/themes/term16_dark.toml create mode 100644 runtime/themes/term16_light.toml diff --git a/runtime/themes/term16_dark.toml b/runtime/themes/term16_dark.toml new file mode 100644 index 000000000..b34a4b4e2 --- /dev/null +++ b/runtime/themes/term16_dark.toml @@ -0,0 +1,80 @@ +# Author: dgkf + +"ui.background" = { } +"ui.background.separator" = { fg = "red" } +"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] } +"ui.cursor.match" = { fg = "light-yellow", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "light-gray", modifiers = ["reversed"] } +"ui.cursor.secondary" = { fg = "gray", modifiers = ["reversed"] } +"ui.cursorline.primary" = { bg = "black" } +"ui.gutter" = { } +"ui.gutter.selected" = { bg = "black" } +"ui.help" = { fg = "white", bg = "black" } +"ui.linenr" = { fg = "gray", modifiers = ["bold"] } +"ui.linenr.selected" = { fg = "white", modifiers = ["bold"] } +"ui.menu" = { fg = "light-gray", bg = "gray" } +"ui.menu.selected" = { modifiers = ["reversed"] } +"ui.menu.scroll" = { fg = "light-blue" } +"ui.popup" = { bg = "black" } +"ui.selection" = { bg = "gray" } +"ui.statusline" = { fg = "light-gray", bg = "gray" } +"ui.statusline.inactive" = { bg = "black" } +"ui.virtual" = { bg = "black" } +"ui.virtual.indent-guide" = { fg = "gray" } +"ui.virtual.whitespace" = {} +"ui.virtual.wrap" = { fg = "gray" } +"ui.virtual.inlay-hint" = { fg = "light-gray", modifiers = ["dim", "italic"] } +"ui.virtual.inlay-hint.parameter" = { fg = "yellow", modifiers = ["dim", "italic"] } +"ui.virtual.inlay-hint.type" = { fg = "blue", modifiers = ["dim", "italic"] } +"ui.window" = { fg = "gray", modifiers = ["dim"] } + +"comment" = { fg = "light-gray", modifiers = ["italic", "dim"] } + +"attribute" = "light-yellow" +"constant" = { fg = "light-yellow", modifiers = ["bold", "dim"] } +"constant.numeric" = "light-yellow" +"constant.character.escape" = "light-cyan" +"constructor" = "light-blue" +"function" = "light-blue" +"function.macro" = "light-red" +"function.builtin" = { fg = "light-blue", modifiers = ["bold"] } +"tag" = { fg = "light-magenta", modifiers = ["dim"] } +"type" = "blue" +"type.builtin" = { fg = "blue", modifiers = ["bold"] } +"type.enum.variant" = { fg = "light-magenta", modifiers = ["dim"] } +"string" = "light-green" +"special" = "light-red" +"variable" = "white" +"variable.parameter" = { fg = "light-yellow", modifiers = ["italic"] } +"variable.other.member" = "light-green" +"keyword" = "light-magenta" +"keyword.control.exception" = "light-red" +"keyword.directive" = { fg = "light-yellow", modifiers = ["bold"] } +"keyword.operator" = { fg = "light-blue", modifiers = ["bold"] } +"label" = "light-green" +"namespace" = { fg = "blue", modifiers = ["dim"] } + +"markup.heading" = "light-blue" +"markup.list" = "light-red" +"markup.bold" = { fg = "light-cyan", modifiers = ["bold"] } +"markup.italic" = { fg = "light-blue", modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.url" = { fg = "magenta", modifiers = ["dim"] } +"markup.link.text" = "light-magenta" +"markup.quote" = "light-cyan" +"markup.raw" = "light-green" + +"diff.plus" = "light-green" +"diff.delta" = "light-yellow" +"diff.minus" = "light-red" + +"diagnostic.hint" = { underline = { color = "gray", style = "curl" } } +"diagnostic.info" = { underline = { color = "light-cyan", style = "curl" } } +"diagnostic.warning" = { underline = { color = "light-yellow", style = "curl" } } +"diagnostic.error" = { underline = { color = "light-red", style = "curl" } } + +"info" = "light-cyan" +"hint" = { fg = "light-gray", modifiers = ["dim"] } +"debug" = "white" +"warning" = "yellow" +"error" = "light-red" diff --git a/runtime/themes/term16_light.toml b/runtime/themes/term16_light.toml new file mode 100644 index 000000000..a02784b25 --- /dev/null +++ b/runtime/themes/term16_light.toml @@ -0,0 +1,85 @@ +# Author: dgkf +# Modified from base16_terminal, Author: NNB + +inherits = "term16_dark" + +"ui.background.separator" = "light-gray" +"ui.cursor" = { fg = "gray", modifiers = ["reversed"] } +"ui.cursor.match" = { fg = "yellow", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "black", modifiers = ["reversed"] } +"ui.cursor.secondary" = { fg = "gray", modifiers = ["reversed"] } +"ui.cursorline.primary" = { bg = "white" } +"ui.cursorline.secondary" = { bg = "white" } +"ui.cursorcolumn.primary" = { bg = "white" } +"ui.cursorcolumn.secondary" = { bg = "white" } +"ui.gutter" = { } +"ui.gutter.selected" = { bg = "white" } +"ui.linenr" = { fg = "gray", modifiers = ["dim"] } +"ui.linenr.selected" = { fg = "black", modifiers = ["bold"] } +"ui.menu" = { bg = "light-gray" } +"ui.menu.selected" = { fg = "white", bg = "gray", modifiers = ["bold"] } +"ui.menu.scroll" = { fg = "light-blue" } +"ui.help" = { } +"ui.text" = { } +"ui.text.focus" = { } +"ui.popup" = { bg = "white" } +"ui.selection" = { bg = "light-gray" } +"ui.statusline" = { bg = "white" } +"ui.statusline.inactive" = { fg = "gray", modifiers = ["underlined"] } +"ui.statusline.insert" = { fg = "white", bg = "blue" } +"ui.statusline.select" = { fg = "white", bg = "magenta" } +"ui.virtual" = { fg = "light-gray" } +"ui.virtual.indent-guide" = { fg = "light-gray", modifiers = ["dim"] } +"ui.virtual.ruler" = { bg = "white" } +"ui.virtual.wrap" = { fg = "light-gray" } +"ui.window" = { fg = "gray", modifiers = ["dim"] } + +"comment" = { fg = "gray", modifiers = ["italic", "dim"] } + +"attribute" = "yellow" +"constant" = { fg = "yellow", modifiers = ["bold"] } +"constant.numeric" = { fg = "yellow", modifiers = ["bold"] } +"constant.character.escape" = "blue" +"constructor" = "blue" +"function" = "blue" +"function.builtin" = { fg = "blue", modifiers = ["bold"] } +"tag" = { fg = "magenta", modifiers = ["dim"] } +"type" = "blue" +"type.builtin" = { fg = "blue", modifiers = ["bold"] } +"type.enum.variant" = { fg = "magenta", modifiers = ["dim"] } +"string" = "green" +"special" = "red" +"variable" = { fg = "black", modifiers = ["dim"] } +"variable.parameter" = { fg = "red", modifiers = ["italic", "dim"] } +"variable.other.member" = "green" +"keyword" = "magenta" +"keyword.control.exception" = "red" +"keyword.directive" = { fg = "yellow", modifiers = ["bold"] } +"keyword.operator" = { fg = "blue", modifiers = ["bold"] } +"label" = "red" +"namespace" = { fg = "blue", modifiers = ["dim"] } + +"markup.heading" = { fg = "blue", modifiers = ["bold"] } +"markup.list" = "red" +"markup.bold" = { fg = "cyan", modifiers = ["bold"] } +"markup.italic" = { fg = "blue", modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.url" = { fg = "magenta", modifiers = ["dim"] } +"markup.link.text" = { fg = "magenta", modifiers = ["bold"] } +"markup.quote" = "cyan" +"markup.raw" = "blue" + +"diff.plus" = "green" +"diff.delta" = "yellow" +"diff.minus" = "red" + +"diagnostic.hint" = { underline = { color = "cyan", style = "curl" } } +"diagnostic.info" = { underline = { color = "blue", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.error" = { underline = { color = "red", style = "curl" } } + +"hint" = "cyan" +"info" = "blue" +"debug" = "light-yellow" +"warning" = "yellow" +"error" = "red" From 0975d9c5e7e4caf08d1c1204f09ab6d2f718105d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Tue, 6 Feb 2024 00:55:56 +0000 Subject: [PATCH 50/70] feat(languages): golang comments and numeric types (#9525) --- runtime/queries/go/highlights.scm | 32 ++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index fba2df99e..8eed12afb 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -183,9 +183,12 @@ [ (int_literal) +] @constant.numeric.integer + +[ (float_literal) (imaginary_literal) -] @constant.numeric.integer +] @constant.numeric.float [ (true) @@ -197,4 +200,31 @@ (iota) ] @constant.builtin +; Comments + (comment) @comment + +; Doc Comments +(source_file + . + (comment)+ @comment.block.documentation) + +(source_file + (comment)+ @comment.block.documentation + . + (const_declaration)) + +(source_file + (comment)+ @comment.block.documentation + . + (function_declaration)) + +(source_file + (comment)+ @comment.block.documentation + . + (type_declaration)) + +(source_file + (comment)+ @comment.block.documentation + . + (var_declaration)) From 1d87c6a999d389011cf8a5849a6096d981c93e33 Mon Sep 17 00:00:00 2001 From: eh Date: Tue, 6 Feb 2024 01:13:20 -0500 Subject: [PATCH 51/70] Update colors used for zed themes (#9544) Official colors used is now publically available: https://github.com/zed-industries/zed/blob/main/assets/themes/one/one.json Modified the theme to more accurately reflect the actual colors being used. Co-authored-by: e4 --- runtime/themes/zed_onedark.toml | 83 ++++++++++++++++---------------- runtime/themes/zed_onelight.toml | 39 +++++++++------ 2 files changed, 64 insertions(+), 58 deletions(-) diff --git a/runtime/themes/zed_onedark.toml b/runtime/themes/zed_onedark.toml index 5fda576f3..300b3c9c5 100644 --- a/runtime/themes/zed_onedark.toml +++ b/runtime/themes/zed_onedark.toml @@ -4,22 +4,20 @@ "comment" = { fg = "light-gray", modifiers = ["italic"] } "constant" = { fg = "yellow" } "constant.numeric" = { fg = "orange" } -"constant.builtin" = { fg = "orange" } +"constant.builtin" = { fg = "yellow" } "constant.builtin.boolean" = { fg = "yellow" } -"constant.character.escape" = { fg = "orange" } +"constant.character.escape" = { fg = "yellow" } "constructor" = { fg = "blue" } "function" = { fg = "blue" } "function.builtin" = { fg = "blue" } -"function.macro" = { fg = "purple" } +"function.method" = { fg = "blue" } +"function.macro" = { fg = "blue" } "keyword" = { fg = "purple" } -"keyword.control" = { fg = "purple" } -"keyword.control.import" = { fg = "purple" } -"keyword.directive" = { fg = "purple" } "label" = { fg = "ui-text" } "namespace" = { fg = "ui-text" } "operator" = { fg = "ui-text" } -"keyword.operator" = { fg = "purple" } -"special" = { fg = "blue" } +"puncuation" = { fg = "ui-text" } +"special" = { fg = "ui-text" } "string" = { fg = "green" } "type" = { fg = "cyan" } "variable.builtin" = { fg = "orange" } @@ -28,41 +26,43 @@ "markup.heading" = { fg = "red" } "markup.raw.inline" = { fg = "green" } -"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } "markup.italic" = { fg = "purple", modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } "markup.list" = { fg = "red" } "markup.quote" = { fg = "yellow" } -"markup.link.url" = { fg = "cyan", modifiers = ["underlined"]} +"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] } "markup.link.text" = { fg = "purple" } "diff.plus" = "green" -"diff.delta" = "orange" +"diff.delta" = "yellow" "diff.minus" = "red" -"diagnostic.info".underline = { color = "blue", style = "curl" } -"diagnostic.hint".underline = { color = "green", style = "curl" } -"diagnostic.warning".underline = { color = "yellow", style = "curl" } -"diagnostic.error".underline = { color = "red", style = "curl" } +"diagnostic.info".underline = { color = "blue", style = "curl" } +"diagnostic.hint".underline = { color = "green", style = "curl" } +"diagnostic.warning".underline = { color = "yellow", style = "curl" } +"diagnostic.error".underline = { color = "red", style = "curl" } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } "error" = { fg = "red", modifiers = ["bold"] } "ui.background" = { bg = "ui-text-reversed" } +"ui.gutter" = { bg = "gray" } "ui.virtual" = { fg = "faint-gray" } -"ui.virtual.indent-guide" = { fg = "faint-gray" } +"ui.virtual.indent-guidje" = { fg = "faint-gray" } "ui.virtual.whitespace" = { fg = "light-gray" } "ui.virtual.ruler" = { bg = "gray" } -"ui.virtual.inlay-hint" = { fg = "light-gray" } +"ui.virtual.inlay-hint" = { fg = "blue-gray", modifiers = ["bold"] } "ui.cursor" = { fg = "white", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } -"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]} +"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"] } +"ui.cursor.insert" = { fg = "dark-blue" } "ui.selection" = { bg = "faint-gray" } -"ui.selection.primary" = { bg = "gray" } -"ui.cursorline.primary" = { bg = "light-black" } +"ui.selection.primary" = { bg = "#293b5bff" } +"ui.cursorline.primary" = { bg = "black" } "ui.highlight" = { bg = "gray" } "ui.highlight.frameline" = { bg = "#97202a" } @@ -70,14 +70,14 @@ "ui.linenr" = { fg = "linenr" } "ui.linenr.selected" = { fg = "ui-text" } -"ui.statusline" = { fg = "white", bg = "light-black" } -"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" } -"ui.statusline.normal" = { fg = "light-black", bg = "blue" } -"ui.statusline.insert" = { fg = "light-black", bg = "green" } -"ui.statusline.select" = { fg = "light-black", bg = "purple" } +"ui.statusline" = { fg = "white", bg = "gray" } +"ui.statusline.inactive" = { fg = "light-gray", bg = "black" } +"ui.statusline.normal" = { fg = "black", bg = "blue" } +"ui.statusline.insert" = { fg = "black", bg = "green" } +"ui.statusline.select" = { fg = "black", bg = "purple" } "ui.text" = { fg = "ui-text" } -"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] } +"ui.text.focus" = { fg = "white", bg = "black", modifiers = ["bold"] } "ui.help" = { fg = "white", bg = "gray" } "ui.popup" = { bg = "gray" } @@ -89,22 +89,21 @@ "ui.debug" = { fg = "red" } [palette] - -yellow = "#dac18c" -blue = "#7ca8dd" -red = "#bd7476" -purple = "#9d74b9" -green = "#a0b783" -orange = "#b4926e" -cyan = "#7eb2be" -light-black = "#2e323a" -gray = "#363f4c" -light-gray = "#5c606b" +yellow = "#dfc184ff" +orange = "#bf956aff" +blue = "#73ade9ff" +blue-gray = "#5a6f89ff" +red = "#d07277ff" +purple = "#b477cfff" +green = "#a1c181ff" +cyan = "#6eb4bfff" +gray = "#2f343ebf" +light-gray = "#5d636fff" faint-gray = "#3B4048" -linenr = "#4B5263" +linenr = "#5d636fff" -white = "#a8adb7" -black = "#292c33" +white = "#c8ccd4ff" +black = "#282c33ff" # black and white are used for a lot of the UI text -ui-text = "#a8adb7" #white -ui-text-reversed = "#292c33" #black +ui-text = "#c8ccd4ff" #white +ui-text-reversed = "#282c33ff" #black diff --git a/runtime/themes/zed_onelight.toml b/runtime/themes/zed_onelight.toml index 2b54cd5be..f3da11676 100644 --- a/runtime/themes/zed_onelight.toml +++ b/runtime/themes/zed_onelight.toml @@ -16,9 +16,13 @@ inherits = "zed_onedark" "ui.cursor" = { fg = "dark-blue", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "dark-blue", modifiers = ["reversed"] } +"ui.cursor.insert" = { fg = "dark-blue" } +"ui.selection.primary" = { bg = "blue-gray" } "ui.cursorline.primary" = { bg = "faint-gray" } +"ui.virtual.inlay-hint" = { fg = "violet", modifiers = ["bold"] } + "ui.statusline" = { fg = "black", bg = "gray" } "ui.statusline.inactive" = { fg = "white", bg = "light-black" } "ui.statusline.normal" = { fg = "white", bg = "blue" } @@ -33,23 +37,26 @@ inherits = "zed_onedark" [palette] -yellow = "#dac18c" -blue = "#5185b5" -red = "#bd7476" -dark-blue = "#607bdb" -orange = "#ca7667" -purple = "#a160ac" -green = "#739d60" -gold = "#a8763c" -cyan = "#4b80b2" +yellow = "#dabb7e" +red = "#d36151ff" +orange = "#d3604fff" +blue = "#5b79e3ff" +dark-blue = "#4a62db" +purple = "#a449abff" +violet = "#9294beff" +green = "#649f57ff" +gold = "#ad6e25ff" +cyan = "#3882b7ff" light-black = "#2e323a" -gray = "#dcdcdd" +# gray = "#dcdcdd" +gray = "#eaeaed" dark-gray = "#ebebec" -light-gray = "#a6a6aa" +light-gray = "#a2a3a7ff" +blue-gray = "#d9dcea" faint-gray = "#efefef" -linenr = "#4B5263" +linenr = "#b0b1b3" -black = "#404248" -white = "#fafafa" -ui-text = "#404248" -ui-text-reversed = "#fafafa" +black = "#383a41ff" +white = "#fafafaff" +ui-text = "#383a41ff" +ui-text-reversed = "#fafafaff" From a37af2dcbf57b6b800946115b4156fefc199ce7a Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 6 Feb 2024 14:45:57 +0100 Subject: [PATCH 52/70] fix division by zero when prompt completion area is too small (#9524) --- helix-term/src/ui/prompt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 702a6e671..3764bba60 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -393,7 +393,7 @@ impl Prompt { height, ); - if !self.completion.is_empty() { + if completion_area.height > 0 && !self.completion.is_empty() { let area = completion_area; let background = theme.get("ui.menu"); From 28a39e6efc79b9aac53d49ac8325e8e32fb127e2 Mon Sep 17 00:00:00 2001 From: eh Date: Tue, 6 Feb 2024 11:47:13 -0500 Subject: [PATCH 53/70] Fix cursorline Zed OneDark (#9549) Co-authored-by: e4 --- runtime/themes/zed_onedark.toml | 6 +++--- runtime/themes/zed_onelight.toml | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/runtime/themes/zed_onedark.toml b/runtime/themes/zed_onedark.toml index 300b3c9c5..7ac1e73cc 100644 --- a/runtime/themes/zed_onedark.toml +++ b/runtime/themes/zed_onedark.toml @@ -50,7 +50,7 @@ "ui.background" = { bg = "ui-text-reversed" } "ui.gutter" = { bg = "gray" } "ui.virtual" = { fg = "faint-gray" } -"ui.virtual.indent-guidje" = { fg = "faint-gray" } +"ui.virtual.indent-guide" = { fg = "faint-gray" } "ui.virtual.whitespace" = { fg = "light-gray" } "ui.virtual.ruler" = { bg = "gray" } "ui.virtual.inlay-hint" = { fg = "blue-gray", modifiers = ["bold"] } @@ -62,7 +62,7 @@ "ui.selection" = { bg = "faint-gray" } "ui.selection.primary" = { bg = "#293b5bff" } -"ui.cursorline.primary" = { bg = "black" } +"ui.cursorline.primary" = { bg = "gray" } "ui.highlight" = { bg = "gray" } "ui.highlight.frameline" = { bg = "#97202a" } @@ -77,7 +77,7 @@ "ui.statusline.select" = { fg = "black", bg = "purple" } "ui.text" = { fg = "ui-text" } -"ui.text.focus" = { fg = "white", bg = "black", modifiers = ["bold"] } +"ui.text.focus" = { fg = "white", bg = "gray", modifiers = ["bold"] } "ui.help" = { fg = "white", bg = "gray" } "ui.popup" = { bg = "gray" } diff --git a/runtime/themes/zed_onelight.toml b/runtime/themes/zed_onelight.toml index f3da11676..086fce34b 100644 --- a/runtime/themes/zed_onelight.toml +++ b/runtime/themes/zed_onelight.toml @@ -36,7 +36,6 @@ inherits = "zed_onedark" "ui.window" = { fg = "dark-gray" } [palette] - yellow = "#dabb7e" red = "#d36151ff" orange = "#d3604fff" From c64a0e615b3b8e182c02efdb7c67162ec269619f Mon Sep 17 00:00:00 2001 From: Bogdan Agica Date: Tue, 6 Feb 2024 23:50:14 -0800 Subject: [PATCH 54/70] Revert "build(deps): bump cc from 1.0.83 to 1.0.84 (#8809)" (#9548) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb815ee3e..3871768f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.84" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] From 72c508de248e363d8a3481ad9d028320361df1fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:34:49 +0900 Subject: [PATCH 55/70] build(deps): bump libc from 0.2.152 to 0.2.153 (#9541) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- helix-term/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3871768f8..d25bd373c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1596,9 +1596,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 9a7162ac1..295782976 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -72,7 +72,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.152" +libc = "0.2.153" [target.'cfg(target_os = "macos")'.dependencies] crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] } From 630d91168a7aa2a3da72a55266dee663565f6edf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 23:56:26 +0000 Subject: [PATCH 56/70] build(deps): bump pulldown-cmark from 0.9.6 to 0.10.0 Bumps [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) from 0.9.6 to 0.10.0. - [Release notes](https://github.com/raphlinus/pulldown-cmark/releases) - [Commits](https://github.com/raphlinus/pulldown-cmark/compare/v0.9.6...v0.10.0) --- updated-dependencies: - dependency-name: pulldown-cmark dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- helix-term/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d25bd373c..73e548ae5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1839,9 +1839,9 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" [[package]] name = "pulldown-cmark" -version = "0.9.6" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" dependencies = [ "bitflags 2.4.2", "memchr", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 295782976..a0d6754d0 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -52,7 +52,7 @@ log = "0.4" nucleo.workspace = true ignore = "0.4" # markdown doc rendering -pulldown-cmark = { version = "0.9", default-features = false } +pulldown-cmark = { version = "0.10", default-features = false } # file type detection content_inspector = "0.2.4" From bbcc89241fd63b460ea7b1661a550f5b9eacba4c Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 5 Feb 2024 19:32:25 -0500 Subject: [PATCH 57/70] Fix pulldown_cmark breaking changes to tag types * Tags and TagEnd are now separate enums since . * The `Tag::Heading` member has been changed from a tuple variant to a struct variant. --- helix-term/src/ui/markdown.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 4d0c0d4a5..5cf530ad8 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -6,7 +6,7 @@ use tui::{ use std::sync::Arc; -use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag}; +use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd}; use helix_core::{ syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax}, @@ -209,7 +209,7 @@ impl Markdown { list_stack.push(list); } - Event::End(Tag::List(_)) => { + Event::End(TagEnd::List(_)) => { list_stack.pop(); // whenever top-level list closes, empty line @@ -249,7 +249,10 @@ impl Markdown { Event::End(tag) => { tags.pop(); match tag { - Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) | Tag::Item => { + TagEnd::Heading(_) + | TagEnd::Paragraph + | TagEnd::CodeBlock + | TagEnd::Item => { push_line(&mut spans, &mut lines); } _ => (), @@ -257,7 +260,7 @@ impl Markdown { // whenever heading, code block or paragraph closes, empty line match tag { - Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) => { + TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::CodeBlock => { lines.push(Spans::default()); } _ => (), @@ -279,7 +282,7 @@ impl Markdown { lines.extend(tui_text.lines.into_iter()); } else { let style = match tags.last() { - Some(Tag::Heading(level, ..)) => match level { + Some(Tag::Heading { level, .. }) => match level { HeadingLevel::H1 => heading_styles[0], HeadingLevel::H2 => heading_styles[1], HeadingLevel::H3 => heading_styles[2], From a1272bdb17a63361342a318982e46129d558743c Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Wed, 7 Feb 2024 19:36:29 +0100 Subject: [PATCH 58/70] slint: Update treesitter parser and queries (#9551) * slint: Update treesitter parser and queries * slint: Port over suggestions from nvim review --- book/src/generated/lang-support.md | 2 +- languages.toml | 2 +- runtime/queries/rust/injections.scm | 20 +- runtime/queries/slint/highlights.scm | 339 ++++++++++++++++---------- runtime/queries/slint/indents.scm | 17 +- runtime/queries/slint/locals.scm | 9 +- runtime/queries/slint/textobjects.scm | 35 +++ 7 files changed, 278 insertions(+), 146 deletions(-) create mode 100644 runtime/queries/slint/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index ddd480536..279acc4fb 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -148,7 +148,7 @@ | scala | ✓ | ✓ | ✓ | `metals` | | scheme | ✓ | | ✓ | | | scss | ✓ | | | `vscode-css-language-server` | -| slint | ✓ | | ✓ | `slint-lsp` | +| slint | ✓ | ✓ | ✓ | `slint-lsp` | | smali | ✓ | | ✓ | | | smithy | ✓ | | | `cs` | | sml | ✓ | | | | diff --git a/languages.toml b/languages.toml index 11afea0ca..bad221299 100644 --- a/languages.toml +++ b/languages.toml @@ -2139,7 +2139,7 @@ language-servers = [ "slint-lsp" ] [[grammar]] name = "slint" -source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "00c8a2d3645766f68c0d0460086c0a994e5b0d85" } +source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "15618215b79b9db08f824a5c97a12d073dcc1c00" } [[language]] name = "task" diff --git a/runtime/queries/rust/injections.scm b/runtime/queries/rust/injections.scm index ae9e587fd..b05b9d975 100644 --- a/runtime/queries/rust/injections.scm +++ b/runtime/queries/rust/injections.scm @@ -2,11 +2,29 @@ (#set! injection.language "comment")) ((macro_invocation - macro: (identifier) @_html (#eq? @_html "html") + macro: + [ + (scoped_identifier + name: (_) @_macro_name) + (identifier) @_macro_name + ] (token_tree) @injection.content) + (#eq? @_macro_name "html") (#set! injection.language "html") (#set! injection.include-children)) +((macro_invocation + macro: + [ + (scoped_identifier + name: (_) @_macro_name) + (identifier) @_macro_name + ] + (token_tree) @injection.content) + (#eq? @_macro_name "slint") + (#set! injection.language "slint") + (#set! injection.include-children)) + ((macro_invocation (token_tree) @injection.content) (#set! injection.language "rust") diff --git a/runtime/queries/slint/highlights.scm b/runtime/queries/slint/highlights.scm index c0ef3dd88..06d82a413 100644 --- a/runtime/queries/slint/highlights.scm +++ b/runtime/queries/slint/highlights.scm @@ -1,122 +1,109 @@ +(comment) @comment + +; Different types: +(string_value) @string +(bool_value) @constant.builtin.boolean + +; Constants + +(escape_sequence) @constant.character.escape + +(color_value) @constant -(identifier) @variable [ - (type_identifier) - (units) -]@type + (children_identifier) + (easing_kind_identifier) +] @constant.builtin -(array_literal - (identifier) @type) +[ + (int_value) + (physical_length_value) +] @constant.numeric.integer -(function_identifier) @function [ - (image_macro) - (children_macro) - (radial_grad_macro) - (linear_grad_macro) -] @function.macro + (float_value) + (percent_value) + (length_value) + (duration_value) + (angle_value) + (relative_font_size_value) +] @constant.numeric.float -(call_expression - function: (identifier) @function) -(call_expression - function: (field_expression - field: (identifier) @function)) +(purity) @keyword.storage.modifier -(vis) @keyword.control.import +(function_visibility) @keyword.storage.modifier -(transition_statement state: (identifier) @variable.other.member) -(state_expression state: (identifier) @variable.other.member) -(struct_block_definition field: (identifier) @variable.other.member) -(assign_property (identifier) @attribute) +(property_visibility) @keyword.storage.modifier -(comment) @comment +(builtin_type_identifier) @type.builtin -(string_literal) @string -(int_literal) @constant.numeric.integer -(float_literal) @constant.numeric.float +(reference_identifier) @variable.builtin -[ - "in" - "in-out" - "for" -] @keyword.control.repeat +(type + [ + (type_list) + (user_type_identifier) + (anon_struct_block) + ]) @type -[ - "import" - "export" - "from" -] @keyword.control.import +(user_type_identifier) @type -[ - "if" - "else" - "when" -] @keyword.control.conditional +; Functions and callbacks +(argument) @variable.parameter -[ - "struct" - "property" -] @keyword.storage.type +(function_call + name: (_) @function.call) -[ - "global" -] @keyword.storage.modifier +; definitions +(callback + name: (_) @function) +(callback_alias + name: (_) @function) -[ - "root" - "parent" - "duration" - "easing" -] @variable.builtin +(callback_event + name: (simple_identifier) @function.call) +(enum_definition + name: (_) @type.enum) -[ - "callback" - "animate" - "states" - "out" - "transitions" - "component" - "inherits" -] @keyword +(function_definition + name: (_) @function) -[ - "black" - "transparent" - "blue" - "ease" - "ease_in" - "ease-in" - "ease_in_out" - "ease-in-out" - "ease_out" - "ease-out" - "end" - "green" - "red" - "start" - "yellow" - "white" - "gray" - ] @constant.builtin +(struct_definition + name: (_) @type) + +(typed_identifier + type: (_) @type) + +; Operators +(binary_expression + op: (_) @operator) + +(unary_expression + op: (_) @operator) [ - "true" - "false" -] @constant.builtin.boolean + (comparison_operator) + (mult_prec_operator) + (add_prec_operator) + (unary_prec_operator) + (assignment_prec_operator) +] @operator -"@" @keyword +[ + ":=" + "=>" + "->" + "<=>" +] @operator -; ; Punctuation [ - "," - "." ";" - ":" + "." + "," ] @punctuation.delimiter -; ; Brackets [ "(" ")" @@ -126,46 +113,136 @@ "}" ] @punctuation.bracket -(define_property ["<" ">"] @punctuation.bracket) +(property + [ + "<" + ">" + ] @punctuation.bracket) -[ - "angle" - "bool" - "brush" - "color" - "duration" - "easing" - "float" - "image" - "int" - "length" - "percent" - "physical-length" - "physical_length" - "string" -] @type.builtin +; Properties, constants and variables +(component + id: (simple_identifier) @constant) + +(property + name: (simple_identifier) @variable) + +(binding_alias + name: (simple_identifier) @variable) + +(binding + name: (simple_identifier) @variable) + +(struct_block + (simple_identifier) @variable.other.member) + +(anon_struct_block + (simple_identifier) @variable.other.member) + +(property_assignment + property: (simple_identifier) @variable) + +(states_definition + name: (simple_identifier) @variable) + +(callback + name: (simple_identifier) @variable) + +(typed_identifier + name: (_) @variable) + +(simple_indexed_identifier + (simple_identifier) @variable) + +(expression + (simple_identifier) @variable) +; Attributes [ - ":=" - "<=>" - "!" - "-" - "+" - "*" - "/" - "&&" - "||" - ">" - "<" - ">=" - "<=" - "=" - ":" - "+=" - "-=" - "*=" - "/=" - "?" - "=>" ] @operator - -(ternary_expression [":" "?"] @keyword.control.conditional) \ No newline at end of file + (linear_gradient_identifier) + (radial_gradient_identifier) + (radial_gradient_kind) +] @attribute + +(image_call + "@image-url" @attribute) + +(tr + "@tr" @attribute) + +; Keywords +(animate_option_identifier) @keyword + +(export) @keyword.control.import + +(if_statement + "if" @keyword.control.conditional) + +(if_expr + [ + "if" + "else" + ] @keyword.control.conditional) + +(ternary_expression + [ + "?" + ":" + ] @keyword.control.conditional) + +(animate_statement + "animate" @keyword) + +(callback + "callback" @keyword.function) + +(component_definition + [ + "component" + "inherits" + ] @keyword.storage.type) + +(enum_definition + "enum" @keyword.storage.type) + +(for_loop + [ + "for" + "in" + ] @keyword.control.repeat) + +(function_definition + "function" @keyword.function) + +(global_definition + "global" @keyword.storage.type) + +(imperative_block + "return" @keyword.control.return) + +(import_statement + [ + "import" + "from" + ] @keyword.control.import) + +(import_type + "as" @keyword.control.import) + +(property + "property" @keyword.storage.type) + +(states_definition + [ + "states" + "when" + ] @keyword) + +(struct_definition + "struct" @keyword.storage.type) + +(transitions_definition + [ + "transitions" + "in" + "out" + ] @keyword) diff --git a/runtime/queries/slint/indents.scm b/runtime/queries/slint/indents.scm index 4b5ce41b8..189f8a0e5 100644 --- a/runtime/queries/slint/indents.scm +++ b/runtime/queries/slint/indents.scm @@ -1,12 +1,11 @@ [ - (comp_body) - (state_statement) - (transition_statement) - (handler_body) - (consequence_body) - (global_single) + (anon_struct_block) + (assignment_block) + (block) + (enum_block) + (global_block) + (imperative_block) + (struct_block) ] @indent -[ - "}" -] @outdent +"}" @outdent diff --git a/runtime/queries/slint/locals.scm b/runtime/queries/slint/locals.scm index a115f0c69..06601b05d 100644 --- a/runtime/queries/slint/locals.scm +++ b/runtime/queries/slint/locals.scm @@ -1,3 +1,6 @@ -; locals.scm - -(component_item) @local.scope +[ + (component) + (component_definition) + (function_definition) + (imperative_block) +] @local.scope diff --git a/runtime/queries/slint/textobjects.scm b/runtime/queries/slint/textobjects.scm new file mode 100644 index 000000000..7e2f36096 --- /dev/null +++ b/runtime/queries/slint/textobjects.scm @@ -0,0 +1,35 @@ +(function_definition + (imperative_block) @funtion.inside) @function.around + +(callback_event + (imperative_block) @function.inside) @function.around + +(property + (imperative_block) @function.inside) @function.around + +(struct_definition + (struct_block) @class.inside) @class.around + +(enum_definition + (enum_block) @class.inside) @class.around + +(global_definition + (global_block) @class.inside) @class.around + +(component_definition + (block) @class.inside) @class.around + +(component_definition + (block) @class.inside) @class.around + +(comment) @comment.around + +(typed_identifier + name: (_) @parameter.inside) @parameter.around + +(callback + arguments: (_) @parameter.inside) + +(string_value + "\"" . (_) @text.inside . "\"") @text.around + From f8e2d822ba3716aa986acee003f3093268344160 Mon Sep 17 00:00:00 2001 From: ath3 <45574139+ath3@users.noreply.github.com> Date: Fri, 9 Feb 2024 03:24:45 +0100 Subject: [PATCH 59/70] Fix scroll track (#9508) --- helix-term/src/ui/menu.rs | 1 + helix-term/src/ui/popup.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 64127e3af..c0e60b33e 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -427,6 +427,7 @@ impl Component for Menu { cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); } else if !render_borders { // Draw scroll track + cell.set_symbol(half_block); cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); } } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 7a6ffe9dd..b38b8b6e3 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -303,6 +303,7 @@ impl Component for Popup { cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); } else if !render_borders { // Draw scroll track + cell.set_symbol(half_block); cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); } } From d137a08231515b0a1694a3841081331e529ce953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 9 Feb 2024 10:44:46 +0000 Subject: [PATCH 60/70] feat(languages): pkl (#9515) * feat(languages): pkl Add [pkl](https://github.com/apple/pkl) language. Official documentation: https://pkl-lang.org/ * remove branch indent --- book/src/generated/lang-support.md | 1 + languages.toml | 11 ++ runtime/queries/pkl/highlights.scm | 179 +++++++++++++++++++++++++++++ runtime/queries/pkl/indents.scm | 23 ++++ runtime/queries/pkl/injections.scm | 30 +++++ 5 files changed, 244 insertions(+) create mode 100644 runtime/queries/pkl/highlights.scm create mode 100644 runtime/queries/pkl/indents.scm create mode 100644 runtime/queries/pkl/injections.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 279acc4fb..c91b9ae11 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -123,6 +123,7 @@ | pem | ✓ | | | | | perl | ✓ | ✓ | ✓ | `perlnavigator` | | php | ✓ | ✓ | ✓ | `intelephense` | +| pkl | ✓ | | ✓ | | | po | ✓ | ✓ | | | | pod | ✓ | | | | | ponylang | ✓ | ✓ | ✓ | | diff --git a/languages.toml b/languages.toml index bad221299..30b411571 100644 --- a/languages.toml +++ b/languages.toml @@ -3061,3 +3061,14 @@ indent = { tab-width = 4, unit = " " } name = "tact" source = { git = "https://github.com/tact-lang/tree-sitter-tact", rev = "ec57ab29c86d632639726631fb2bb178d23e1c91" } +[[language]] +name = "pkl" +scope = "source.pkl" +injection-regex = "pkl" +file-types = ["pkl", "pcf"] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[[grammar]] +name = "pkl" +source = { git = "https://github.com/apple/tree-sitter-pkl", rev = "c03f04a313b712f8ab00a2d862c10b37318699ae" } diff --git a/runtime/queries/pkl/highlights.scm b/runtime/queries/pkl/highlights.scm new file mode 100644 index 000000000..501c94859 --- /dev/null +++ b/runtime/queries/pkl/highlights.scm @@ -0,0 +1,179 @@ +; Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; https://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +; this definition is imprecise in that +; * any qualified or unqualified call to a method named "Regex" is considered a regex +; * string delimiters are considered part of the regex + +; Operators + +[ + "??" + "@" + "=" + "<" + ">" + "!" + "==" + "!=" + "<=" + ">=" + "&&" + "||" + "+" + "-" + "**" + "*" + "/" + "~/" + "%" + "|>" +] @keyword.operator + +[ + "?" + "|" + "->" +] @operator.type + +[ + "," + ":" + "." + "?." +] @punctuation.delimiter + +[ + "(" + ")" + "]" + "{" + "}" + ; "[" @punctuation.bracket TODO: FIGURE OUT HOW TO REFER TO CUSTOM TOKENS +] @punctuation.bracket + +; Keywords + +[ + "abstract" + "amends" + "as" + "class" + "extends" + "external" + "function" + "hidden" + "import" + "import*" + "in" + "let" + "local" + "module" + "new" + "open" + "out" + "typealias" + "when" +] @keyword + +[ + "if" + "is" + "else" +] @keyword.control.conditional + +[ + "for" +] @keyword.control.repeat + +(importExpr "import" @keyword.control.import) +(importGlobExpr "import*" @keyword.control.import) + +"read" @function.builtin +"read?" @function.builtin +"read*" @function.builtin +"throw" @function.builtin +"trace" @function.builtin + +(moduleExpr "module" @type.builtin) +"nothing" @type.builtin +"unknown" @type.builtin + +(outerExpr) @variable.builtin +"super" @variable.builtin +(thisExpr) @variable.builtin + +[ + (falseLiteral) + (nullLiteral) + (trueLiteral) +] @constant.builtin + +; Literals + +(stringConstant) @string +(slStringLiteral) @string +(mlStringLiteral) @string + +(escapeSequence) @constent.character.escape + +(intLiteral) @constant.numeric.integer +(floatLiteral) @constant.numeric.float + +(interpolationExpr + "\\(" @punctuation.special + ")" @punctuation.special) @embedded + +(interpolationExpr + "\\#(" @punctuation.special + ")" @punctuation.special) @embedded + +(interpolationExpr + "\\##(" @punctuation.special + ")" @punctuation.special) @embedded + +(lineComment) @comment +(blockComment) @comment +(docComment) @comment + +; Identifiers + +(classProperty (identifier) @variable.other.member) +(objectProperty (identifier) @variable.other.member) + +(parameterList (typedIdentifier (identifier) @variable.parameter)) +(objectBodyParameters (typedIdentifier (identifier) @variable.parameter)) + +(identifier) @variable + +; Method definitions + +(classMethod (methodHeader (identifier)) @function.method) +(objectMethod (methodHeader (identifier)) @function.method) + +; Method calls + +(methodCallExpr + (identifier) @function.method) + +; Types + +(clazz (identifier) @type) +(typeAlias (identifier) @type) +((identifier) @type + (match? @type "^[A-Z]")) + +(typeArgumentList + "<" @punctuation.bracket + ">" @punctuation.bracket) diff --git a/runtime/queries/pkl/indents.scm b/runtime/queries/pkl/indents.scm new file mode 100644 index 000000000..d2a9be1ab --- /dev/null +++ b/runtime/queries/pkl/indents.scm @@ -0,0 +1,23 @@ +; Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; https://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +; this definition is imprecise in that +; * any qualified or unqualified call to a method named "Regex" is considered a regex +; * string delimiters are considered part of the regex +[ + (objectBody) + (classBody) + (ifExpr) + (mlStringLiteral) ; This isn't perfect; newlines are too indented but it's better than if omitted. +] @indent diff --git a/runtime/queries/pkl/injections.scm b/runtime/queries/pkl/injections.scm new file mode 100644 index 000000000..15867f35e --- /dev/null +++ b/runtime/queries/pkl/injections.scm @@ -0,0 +1,30 @@ +; Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; https://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +; this definition is imprecise in that +; * any qualified or unqualified call to a method named "Regex" is considered a regex +; * string delimiters are considered part of the regex +( + ((methodCallExpr (identifier) @methodName (argumentList (slStringLiteral) @injection.content)) + (#set! injection.language "regex")) + (eq? @methodName "Regex")) + +((lineComment) @injection.content + (#set! injection.language "comment")) + +((blockComment) @injection.content + (#set! injection.language "comment")) + +((docComment) @injection.content + (#set! injection.language "markdown")) From d570c29ce37ffbb46a9c49708c31dfd81daa27cf Mon Sep 17 00:00:00 2001 From: Android789515 Date: Fri, 9 Feb 2024 14:29:11 +0000 Subject: [PATCH 61/70] expand upon the arch linux install instructions (#9574) Signed-off-by: Android789515 --- book/src/install.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/book/src/install.md b/book/src/install.md index 1f200e2ed..07865e698 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -76,6 +76,15 @@ Releases are available in the `extra` repository: ```sh sudo pacman -S helix ``` + +> 💡 When installed from the `extra` repository, run Helix with `helix` instead of `hx`. +> +> For example: +> ```sh +> helix --health +> ``` +> to check health + Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available in the AUR, which builds the master branch. From 581a1ebf5d327c1128fe6c283578e8f36a4b5fb5 Mon Sep 17 00:00:00 2001 From: Galen Abell Date: Sun, 11 Feb 2024 18:24:20 +0100 Subject: [PATCH 62/70] Add glob file type support (#8006) * Replace FileType::Suffix with FileType::Glob Suffix is rather limited and cannot be used to match files which have semantic meaning based on location + file type (for example, Github Action workflow files). This patch adds support for a Glob FileType to replace Suffix, which encompasses the existing behavior & adds additional file matching functionality. Globs are standard Unix-style path globs, which are matched against the absolute path of the file. If the configured glob for a language is a relative glob (that is, it isn't an absolute path or already starts with a glob pattern), a glob pattern will be prepended to allow matching relative paths from any directory. The order of file type matching is also updated to first match on globs and then on extension. This is necessary as most cases where glob-matching is useful will have already been matched by an extension if glob matching is done last. * Convert file-types suffixes to globs * Use globs for filename matching Trying to match the file-type raw strings against both filename and extension leads to files with the same name as the extension having the incorrect syntax. * Match dockerfiles with suffixes It's common practice to add a suffix to dockerfiles based on their context, e.g. `Dockerfile.dev`, `Dockerfile.prod`, etc. * Make env filetype matching more generic Match on `.env` or any `.env.*` files. * Update docs * Use GlobSet to match all file type globs at once * Update todo.txt glob patterns * Consolidate language Configuration and Loader creation This is a refactor that improves the error handling for creating the `helix_core::syntax::Loader` from the default and user language configuration. * Fix integration tests * Add additional starlark file-type glob --------- Co-authored-by: Michael Davis --- Cargo.lock | 1 + book/src/languages.md | 28 ++-- helix-core/Cargo.toml | 1 + helix-core/src/config.rs | 45 ++++++- helix-core/src/syntax.rs | 150 +++++++++++++-------- helix-core/tests/indent.rs | 2 +- helix-term/src/application.rs | 14 +- helix-term/src/health.rs | 10 +- helix-term/src/main.rs | 10 +- helix-term/tests/test/commands/write.rs | 2 +- helix-term/tests/test/helpers.rs | 18 +-- languages.toml | 168 ++++++++++++------------ 12 files changed, 262 insertions(+), 187 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73e548ae5..a7ef8eb05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1240,6 +1240,7 @@ dependencies = [ "dunce", "encoding_rs", "etcetera", + "globset", "hashbrown 0.14.3", "helix-loader", "helix-stdx", diff --git a/book/src/languages.md b/book/src/languages.md index 944ebf097..7e49a6036 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -78,24 +78,26 @@ from the above section. `file-types` is a list of strings or tables, for example: ```toml -file-types = ["Makefile", "toml", { suffix = ".git/config" }] +file-types = ["toml", { glob = "Makefile" }, { glob = ".git/config" }, { glob = ".github/workflows/*.yaml" } ] ``` When determining a language configuration to use, Helix searches the file-types with the following priorities: -1. Exact match: if the filename of a file is an exact match of a string in a - `file-types` list, that language wins. In the example above, `"Makefile"` - will match against `Makefile` files. -2. Extension: if there are no exact matches, any `file-types` string that - matches the file extension of a given file wins. In the example above, the - `"toml"` matches files like `Cargo.toml` or `languages.toml`. -3. Suffix: if there are still no matches, any values in `suffix` tables - are checked against the full path of the given file. In the example above, - the `{ suffix = ".git/config" }` would match against any `config` files - in `.git` directories. Note: `/` is used as the directory separator but is - replaced at runtime with the appropriate path separator for the operating - system, so this rule would match against `.git\config` files on Windows. +1. Glob: values in `glob` tables are checked against the full path of the given + file. Globs are standard Unix-style path globs (e.g. the kind you use in Shell) + and can be used to match paths for a specific prefix, suffix, directory, etc. + In the above example, the `{ glob = "Makefile" }` config would match files + with the name `Makefile`, the `{ glob = ".git/config" }` config would match + `config` files in `.git` directories, and the `{ glob = ".github/workflows/*.yaml" }` + config would match any `yaml` files in `.github/workflow` directories. Note + that globs should always use the Unix path separator `/` even on Windows systems; + the matcher will automatically take the machine-specific separators into account. + If the glob isn't an absolute path or doesn't already start with a glob prefix, + `*/` will automatically be added to ensure it matches for any subdirectory. +2. Extension: if there are no glob matches, any `file-types` string that matches + the file extension of a given file wins. In the example above, the `"toml"` + config matches files like `Cargo.toml` or `languages.toml`. ## Language Server configuration diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 8c63af8ef..bdc879caa 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -49,6 +49,7 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std" etcetera = "0.8" textwrap = "0.16.0" +globset = "0.4.14" nucleo.workspace = true parking_lot = "0.12" diff --git a/helix-core/src/config.rs b/helix-core/src/config.rs index 2076fc224..27cd4e297 100644 --- a/helix-core/src/config.rs +++ b/helix-core/src/config.rs @@ -1,10 +1,45 @@ -/// Syntax configuration loader based on built-in languages.toml. -pub fn default_syntax_loader() -> crate::syntax::Configuration { +use crate::syntax::{Configuration, Loader, LoaderError}; + +/// Language configuration based on built-in languages.toml. +pub fn default_lang_config() -> Configuration { helix_loader::config::default_lang_config() .try_into() - .expect("Could not serialize built-in languages.toml") + .expect("Could not deserialize built-in languages.toml") } -/// Syntax configuration loader based on user configured languages.toml. -pub fn user_syntax_loader() -> Result { + +/// Language configuration loader based on built-in languages.toml. +pub fn default_lang_loader() -> Loader { + Loader::new(default_lang_config()).expect("Could not compile loader for default config") +} + +#[derive(Debug)] +pub enum LanguageLoaderError { + DeserializeError(toml::de::Error), + LoaderError(LoaderError), +} + +impl std::fmt::Display for LanguageLoaderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DeserializeError(err) => write!(f, "Failed to parse language config: {err}"), + Self::LoaderError(err) => write!(f, "Failed to compile language config: {err}"), + } + } +} + +impl std::error::Error for LanguageLoaderError {} + +/// Language configuration based on user configured languages.toml. +pub fn user_lang_config() -> Result { helix_loader::config::user_lang_config()?.try_into() } + +/// Language configuration loader based on user configured languages.toml. +pub fn user_lang_loader() -> Result { + let config: Configuration = helix_loader::config::user_lang_config() + .map_err(LanguageLoaderError::DeserializeError)? + .try_into() + .map_err(LanguageLoaderError::DeserializeError)?; + + Loader::new(config).map_err(LanguageLoaderError::LoaderError) +} diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 24de1a338..99b5a3d10 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -82,12 +82,6 @@ pub struct Configuration { pub language_server: HashMap, } -impl Default for Configuration { - fn default() -> Self { - crate::config::default_syntax_loader() - } -} - // largely based on tree-sitter/cli/src/loader.rs #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] @@ -164,9 +158,11 @@ pub enum FileType { /// The extension of the file, either the `Path::extension` or the full /// filename if the file does not have an extension. Extension(String), - /// The suffix of a file. This is compared to a given file's absolute - /// path, so it can be used to detect files based on their directories. - Suffix(String), + /// A Unix-style path glob. This is compared to the file's absolute path, so + /// it can be used to detect files based on their directories. If the glob + /// is not an absolute path and does not already start with a glob pattern, + /// a glob pattern will be prepended to it. + Glob(globset::Glob), } impl Serialize for FileType { @@ -178,9 +174,9 @@ impl Serialize for FileType { match self { FileType::Extension(extension) => serializer.serialize_str(extension), - FileType::Suffix(suffix) => { + FileType::Glob(glob) => { let mut map = serializer.serialize_map(Some(1))?; - map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?; + map.serialize_entry("glob", glob.glob())?; map.end() } } @@ -213,9 +209,20 @@ impl<'de> Deserialize<'de> for FileType { M: serde::de::MapAccess<'de>, { match map.next_entry::()? { - Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({ - suffix.replace('/', std::path::MAIN_SEPARATOR_STR) - })), + Some((key, mut glob)) if key == "glob" => { + // If the glob isn't an absolute path or already starts + // with a glob pattern, add a leading glob so we + // properly match relative paths. + if !glob.starts_with('/') && !glob.starts_with("*/") { + glob.insert_str(0, "*/"); + } + + globset::Glob::new(glob.as_str()) + .map(FileType::Glob) + .map_err(|err| { + serde::de::Error::custom(format!("invalid `glob` pattern: {}", err)) + }) + } Some((key, _value)) => Err(serde::de::Error::custom(format!( "unknown key in `file-types` list: {}", key @@ -752,6 +759,47 @@ pub struct SoftWrap { pub wrap_at_text_width: Option, } +#[derive(Debug)] +struct FileTypeGlob { + glob: globset::Glob, + language_id: usize, +} + +impl FileTypeGlob { + fn new(glob: globset::Glob, language_id: usize) -> Self { + Self { glob, language_id } + } +} + +#[derive(Debug)] +struct FileTypeGlobMatcher { + matcher: globset::GlobSet, + file_types: Vec, +} + +impl FileTypeGlobMatcher { + fn new(file_types: Vec) -> Result { + let mut builder = globset::GlobSetBuilder::new(); + for file_type in &file_types { + builder.add(file_type.glob.clone()); + } + + Ok(Self { + matcher: builder.build()?, + file_types, + }) + } + + fn language_id_for_path(&self, path: &Path) -> Option<&usize> { + self.matcher + .matches(path) + .iter() + .filter_map(|idx| self.file_types.get(*idx)) + .max_by_key(|file_type| file_type.glob.glob().len()) + .map(|file_type| &file_type.language_id) + } +} + // Expose loader as Lazy<> global since it's always static? #[derive(Debug)] @@ -759,7 +807,7 @@ pub struct Loader { // highlight_names ? language_configs: Vec>, language_config_ids_by_extension: HashMap, // Vec - language_config_ids_by_suffix: HashMap, + language_config_ids_glob_matcher: FileTypeGlobMatcher, language_config_ids_by_shebang: HashMap, language_server_configs: HashMap, @@ -767,66 +815,57 @@ pub struct Loader { scopes: ArcSwap>, } +pub type LoaderError = globset::Error; + impl Loader { - pub fn new(config: Configuration) -> Self { - let mut loader = Self { - language_configs: Vec::new(), - language_server_configs: config.language_server, - language_config_ids_by_extension: HashMap::new(), - language_config_ids_by_suffix: HashMap::new(), - language_config_ids_by_shebang: HashMap::new(), - scopes: ArcSwap::from_pointee(Vec::new()), - }; + pub fn new(config: Configuration) -> Result { + let mut language_configs = Vec::new(); + let mut language_config_ids_by_extension = HashMap::new(); + let mut language_config_ids_by_shebang = HashMap::new(); + let mut file_type_globs = Vec::new(); for config in config.language { // get the next id - let language_id = loader.language_configs.len(); + let language_id = language_configs.len(); for file_type in &config.file_types { // entry().or_insert(Vec::new).push(language_id); match file_type { - FileType::Extension(extension) => loader - .language_config_ids_by_extension - .insert(extension.clone(), language_id), - FileType::Suffix(suffix) => loader - .language_config_ids_by_suffix - .insert(suffix.clone(), language_id), + FileType::Extension(extension) => { + language_config_ids_by_extension.insert(extension.clone(), language_id); + } + FileType::Glob(glob) => { + file_type_globs.push(FileTypeGlob::new(glob.to_owned(), language_id)); + } }; } for shebang in &config.shebangs { - loader - .language_config_ids_by_shebang - .insert(shebang.clone(), language_id); + language_config_ids_by_shebang.insert(shebang.clone(), language_id); } - loader.language_configs.push(Arc::new(config)); + language_configs.push(Arc::new(config)); } - loader + Ok(Self { + language_configs, + language_config_ids_by_extension, + language_config_ids_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?, + language_config_ids_by_shebang, + language_server_configs: config.language_server, + scopes: ArcSwap::from_pointee(Vec::new()), + }) } pub fn language_config_for_file_name(&self, path: &Path) -> Option> { // Find all the language configurations that match this file name // or a suffix of the file name. - let configuration_id = path - .file_name() - .and_then(|n| n.to_str()) - .and_then(|file_name| self.language_config_ids_by_extension.get(file_name)) + let configuration_id = self + .language_config_ids_glob_matcher + .language_id_for_path(path) .or_else(|| { path.extension() .and_then(|extension| extension.to_str()) .and_then(|extension| self.language_config_ids_by_extension.get(extension)) - }) - .or_else(|| { - self.language_config_ids_by_suffix - .iter() - .find_map(|(file_type, id)| { - if path.to_str()?.ends_with(file_type) { - Some(id) - } else { - None - } - }) }); configuration_id.and_then(|&id| self.language_configs.get(id).cloned()) @@ -2592,7 +2631,8 @@ mod test { let loader = Loader::new(Configuration { language: vec![], language_server: HashMap::new(), - }); + }) + .unwrap(); let language = get_language("rust").unwrap(); let query = Query::new(language, query_str).unwrap(); @@ -2654,7 +2694,8 @@ mod test { let loader = Loader::new(Configuration { language: vec![], language_server: HashMap::new(), - }); + }) + .unwrap(); let language = get_language("rust").unwrap(); let config = HighlightConfiguration::new( @@ -2760,7 +2801,8 @@ mod test { let loader = Loader::new(Configuration { language: vec![], language_server: HashMap::new(), - }); + }) + .unwrap(); let language = get_language(language_name).unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap(); diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index faf845c07..de1434f72 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -186,7 +186,7 @@ fn test_treesitter_indent( lang_scope: &str, ignored_lines: Vec>, ) { - let loader = Loader::new(indent_tests_config()); + let loader = Loader::new(indent_tests_config()).unwrap(); // set runtime path so we can find the queries let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b5150a13a..b844b5f05 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -96,11 +96,7 @@ fn setup_integration_logging() { } impl Application { - pub fn new( - args: Args, - config: Config, - syn_loader_conf: syntax::Configuration, - ) -> Result { + pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result { #[cfg(feature = "integration")] setup_integration_logging(); @@ -126,7 +122,7 @@ impl Application { }) .unwrap_or_else(|| theme_loader.default_theme(true_color)); - let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); + let syn_loader = std::sync::Arc::new(lang_loader); #[cfg(not(feature = "integration"))] let backend = CrosstermBackend::new(stdout(), &config.editor); @@ -394,10 +390,8 @@ impl Application { /// refresh language config after config change fn refresh_language_config(&mut self) -> Result<(), Error> { - let syntax_config = helix_core::config::user_syntax_loader() - .map_err(|err| anyhow::anyhow!("Failed to load language config: {}", err))?; - - self.syn_loader = std::sync::Arc::new(syntax::Loader::new(syntax_config)); + let lang_loader = helix_core::config::user_lang_loader()?; + self.syn_loader = std::sync::Arc::new(lang_loader); self.editor.syn_loader = self.syn_loader.clone(); for document in self.editor.documents.values_mut() { document.detect_language(self.syn_loader.clone()); diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 5f2019265..0bbb5735c 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -2,7 +2,7 @@ use crossterm::{ style::{Color, Print, Stylize}, tty::IsTty, }; -use helix_core::config::{default_syntax_loader, user_syntax_loader}; +use helix_core::config::{default_lang_config, user_lang_config}; use helix_loader::grammar::load_runtime_file; use helix_view::clipboard::get_clipboard_provider; use std::io::Write; @@ -128,7 +128,7 @@ pub fn languages_all() -> std::io::Result<()> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - let mut syn_loader_conf = match user_syntax_loader() { + let mut syn_loader_conf = match user_lang_config() { Ok(conf) => conf, Err(err) => { let stderr = std::io::stderr(); @@ -141,7 +141,7 @@ pub fn languages_all() -> std::io::Result<()> { err )?; writeln!(stderr, "{}", "Using default language config".yellow())?; - default_syntax_loader() + default_lang_config() } }; @@ -234,7 +234,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - let syn_loader_conf = match user_syntax_loader() { + let syn_loader_conf = match user_lang_config() { Ok(conf) => conf, Err(err) => { let stderr = std::io::stderr(); @@ -247,7 +247,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { err )?; writeln!(stderr, "{}", "Using default language config".yellow())?; - default_syntax_loader() + default_lang_config() } }; diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 132ee796f..fbe1a8460 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -145,18 +145,18 @@ FLAGS: } }; - let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { - eprintln!("Bad language config: {}", err); + let lang_loader = helix_core::config::user_lang_loader().unwrap_or_else(|err| { + eprintln!("{}", err); eprintln!("Press to continue with default language config"); use std::io::Read; // This waits for an enter press. let _ = std::io::stdin().read(&mut []); - helix_core::config::default_syntax_loader() + helix_core::config::default_lang_loader() }); // TODO: use the thread local executor to spawn the application task separately from the work pool - let mut app = Application::new(args, config, syn_loader_conf) - .context("unable to create new application")?; + let mut app = + Application::new(args, config, lang_loader).context("unable to create new application")?; let exit_code = app.run(&mut EventStream::new()).await?; diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index adc721c5f..f65352c7e 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -315,7 +315,7 @@ async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> { let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .with_input_text("#[l|]#et foo = 0;\n") - .with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into()))) + .with_lang_loader(helpers::test_syntax_loader(Some(lang_conf.into()))) .build()?; test_key_sequences(&mut app, vec![(Some(":w"), None)], false).await?; diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 112b5e358..a978f386e 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -139,7 +139,7 @@ pub async fn test_key_sequence_with_input_text>( let test_case = test_case.into(); let mut app = match app { Some(app) => app, - None => Application::new(Args::default(), test_config(), test_syntax_conf(None))?, + None => Application::new(Args::default(), test_config(), test_syntax_loader(None))?, }; let (view, doc) = helix_view::current!(app.editor); @@ -162,9 +162,9 @@ pub async fn test_key_sequence_with_input_text>( .await } -/// Generates language configs that merge in overrides, like a user language +/// Generates language config loader that merge in overrides, like a user language /// config. The argument string must be a raw TOML document. -pub fn test_syntax_conf(overrides: Option) -> helix_core::syntax::Configuration { +pub fn test_syntax_loader(overrides: Option) -> helix_core::syntax::Loader { let mut lang = helix_loader::config::default_lang_config(); if let Some(overrides) = overrides { @@ -172,7 +172,7 @@ pub fn test_syntax_conf(overrides: Option) -> helix_core::syntax::Config lang = helix_loader::merge_toml_values(lang, override_toml, 3); } - lang.try_into().unwrap() + helix_core::syntax::Loader::new(lang.try_into().unwrap()).unwrap() } /// Use this for very simple test cases where there is one input @@ -271,7 +271,7 @@ pub fn new_readonly_tempfile() -> anyhow::Result { pub struct AppBuilder { args: Args, config: Config, - syn_conf: helix_core::syntax::Configuration, + syn_loader: helix_core::syntax::Loader, input: Option<(String, Selection)>, } @@ -280,7 +280,7 @@ impl Default for AppBuilder { Self { args: Args::default(), config: test_config(), - syn_conf: test_syntax_conf(None), + syn_loader: test_syntax_loader(None), input: None, } } @@ -314,8 +314,8 @@ impl AppBuilder { self } - pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self { - self.syn_conf = syn_conf; + pub fn with_lang_loader(mut self, syn_loader: helix_core::syntax::Loader) -> Self { + self.syn_loader = syn_loader; self } @@ -328,7 +328,7 @@ impl AppBuilder { bail!("Having the directory {path:?} in args.files[0] is not yet supported for integration tests"); } - let mut app = Application::new(self.args, self.config, self.syn_conf)?; + let mut app = Application::new(self.args, self.config, self.syn_loader)?; if let Some((text, selection)) = self.input { let (view, doc) = helix_view::current!(app.editor); diff --git a/languages.toml b/languages.toml index 30b411571..084d4932c 100644 --- a/languages.toml +++ b/languages.toml @@ -253,7 +253,7 @@ source = { git = "https://github.com/FuelLabs/tree-sitter-sway", rev = "e491a005 name = "toml" scope = "source.toml" injection-regex = "toml" -file-types = ["toml", "poetry.lock", "Cargo.lock"] +file-types = ["toml", { glob = "poetry.lock" }, { glob = "Cargo.lock" }] comment-token = "#" language-servers = [ "taplo" ] indent = { tab-width = 2, unit = " " } @@ -292,7 +292,7 @@ source = { git = "https://github.com/yusdacra/tree-sitter-protobuf", rev = "19c2 name = "elixir" scope = "source.elixir" injection-regex = "(elixir|ex)" -file-types = ["ex", "exs", "mix.lock"] +file-types = ["ex", "exs", { glob = "mix.lock" }] shebangs = ["elixir"] roots = ["mix.exs", "mix.lock"] comment-token = "#" @@ -361,20 +361,20 @@ file-types = [ "geojson", "gltf", "webmanifest", - "flake.lock", - ".babelrc", - ".bowerrc", - ".jscrc", + { glob = "flake.lock" }, + { glob = ".babelrc" }, + { glob = ".bowerrc" }, + { glob = ".jscrc" }, "js.map", "ts.map", "css.map", - ".jslintrc", + { glob = ".jslintrc" }, "jsonld", - ".vuerc", - "composer.lock", - ".watchmanconfig", + { glob = ".vuerc" }, + { glob = "composer.lock" }, + { glob = ".watchmanconfig" }, "avsc", - ".prettierrc" + { glob = ".prettierrc" }, ] language-servers = [ "vscode-json-language-server" ] auto-format = true @@ -439,7 +439,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd name = "cpp" scope = "source.cpp" injection-regex = "cpp" -file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm", "h++", "ii", "inl", { suffix = ".hpp.in" }, { suffix = ".h.in" }] +file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm", "h++", "ii", "inl", { glob = ".hpp.in" }, { glob = ".h.in" }] comment-token = "//" language-servers = [ "clangd" ] indent = { tab-width = 2, unit = " " } @@ -571,7 +571,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "64457ea name = "gomod" scope = "source.gomod" injection-regex = "gomod" -file-types = ["go.mod"] +file-types = [{ glob = "go.mod" }] auto-format = true comment-token = "//" language-servers = [ "gopls" ] @@ -598,7 +598,7 @@ source = { git = "https://github.com/dannylongeuay/tree-sitter-go-template", rev name = "gowork" scope = "source.gowork" injection-regex = "gowork" -file-types = ["go.work"] +file-types = [{ glob = "go.work" }] auto-format = true comment-token = "//" language-servers = [ "gopls" ] @@ -613,7 +613,7 @@ name = "javascript" scope = "source.js" injection-regex = "(js|javascript)" language-id = "javascript" -file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", "jakefile"] +file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = "jakefile" }] shebangs = ["node"] comment-token = "//" language-servers = [ "typescript-language-server" ] @@ -716,7 +716,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53 name = "python" scope = "source.python" injection-regex = "python" -file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstruct", "rpy", "cpy", "ipy", "pyt", "SConscript"] +file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }] shebangs = ["python"] roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"] comment-token = "#" @@ -769,38 +769,38 @@ injection-regex = "ruby" file-types = [ "rb", "rake", - "rakefile", "irb", - "gemfile", "gemspec", - "Rakefile", - "Gemfile", "rabl", "jbuilder", "jb", - "Podfile", "podspec", - "Vagrantfile", - "Brewfile", "rjs", "rbi", - "Guardfile", - "Capfile", - "Cheffile", - "Hobofile", - "Appraisals", - "Rantfile", - "Berksfile", - "Berksfile.lock", - "Thorfile", - "Puppetfile", - "Fastfile", - "Appfile", - "Deliverfile", - "Matchfile", - "Scanfile", - "Snapfile", - "Gymfile" + { glob = "rakefile" }, + { glob = "gemfile" }, + { glob = "Rakefile" }, + { glob = "Gemfile" }, + { glob = "Podfile" }, + { glob = "Vagrantfile" }, + { glob = "Brewfile" }, + { glob = "Guardfile" }, + { glob = "Capfile" }, + { glob = "Cheffile" }, + { glob = "Hobofile" }, + { glob = "Appraisals" }, + { glob = "Rantfile" }, + { glob = "Berksfile" }, + { glob = "Berksfile.lock" }, + { glob = "Thorfile" }, + { glob = "Puppetfile" }, + { glob = "Fastfile" }, + { glob = "Appfile" }, + { glob = "Deliverfile" }, + { glob = "Matchfile" }, + { glob = "Scanfile" }, + { glob = "Snapfile" }, + { glob = "Gymfile" }, ] shebangs = ["ruby"] comment-token = "#" @@ -819,43 +819,43 @@ file-types = [ "sh", "bash", "zsh", - ".bash_history", - ".bash_login", - ".bash_logout", - ".bash_profile", - ".bashrc", - ".profile", - ".zshenv", "zshenv", - ".zlogin", "zlogin", - ".zlogout", "zlogout", - ".zprofile", "zprofile", - ".zshrc", "zshrc", - ".zimrc", - "APKBUILD", - "PKGBUILD", "eclass", "ebuild", "bazelrc", - ".bash_aliases", "Renviron", - ".Renviron", - ".xprofile", - ".xsession", - ".xsessionrc", "zsh-theme", "ksh", "cshrc", "tcshrc", - ".yashrc", - ".yash_profile", - ".hushlogin", "bashrc_Apple_Terminal", - "zshrc_Apple_Terminal" + "zshrc_Apple_Terminal", + { glob = ".bash_history" }, + { glob = ".bash_login" }, + { glob = ".bash_logout" }, + { glob = ".bash_profile" }, + { glob = ".bashrc" }, + { glob = ".profile" }, + { glob = ".zshenv" }, + { glob = ".zlogin" }, + { glob = ".zlogout" }, + { glob = ".zprofile" }, + { glob = ".zshrc" }, + { glob = ".zimrc" }, + { glob = "APKBUILD" }, + { glob = "PKGBUILD" }, + { glob = ".bash_aliases" }, + { glob = ".Renviron" }, + { glob = ".xprofile" }, + { glob = ".xsession" }, + { glob = ".xsessionrc" }, + { glob = ".yashrc" }, + { glob = ".yash_profile" }, + { glob = ".hushlogin" }, ] shebangs = ["sh", "bash", "dash", "zsh"] comment-token = "#" @@ -1216,7 +1216,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-tsq", rev = "48b5 [[language]] name = "cmake" scope = "source.cmake" -file-types = ["cmake", "CMakeLists.txt"] +file-types = ["cmake", { glob = "CMakeLists.txt" }] comment-token = "#" indent = { tab-width = 2, unit = " " } language-servers = [ "cmake-language-server" ] @@ -1229,7 +1229,7 @@ source = { git = "https://github.com/uyha/tree-sitter-cmake", rev = "6e51463ef30 [[language]] name = "make" scope = "source.make" -file-types = ["Makefile", "makefile", "make", "mk", "mak", "GNUmakefile", "OCamlMakefile"] +file-types = [{ glob = "Makefile" }, { glob = "makefile" }, "make", "mk", "mak", {glob = "GNUmakefile" }, { glob = "OCamlMakefile" }] shebangs = ["make", "gmake"] injection-regex = "(make|makefile|Makefile|mk)" comment-token = "#" @@ -1372,7 +1372,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", "PULLREQ_EDITMSG", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook"] +file-types = ["md", "markdown", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }] roots = [".marksman.toml"] language-servers = [ "marksman" ] indent = { tab-width = 2, unit = " " } @@ -1424,7 +1424,7 @@ name = "dockerfile" scope = "source.dockerfile" injection-regex = "docker|dockerfile" roots = ["Dockerfile", "Containerfile"] -file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"] +file-types = [{ glob = "Dockerfile*" }, { glob = "dockerfile*" }, { glob = "Containerfile*" }, { glob = "containerfile*" }] comment-token = "#" indent = { tab-width = 2, unit = " " } language-servers = [ "docker-langserver" ] @@ -1436,7 +1436,7 @@ source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev = [[language]] name = "git-commit" scope = "git.commitmsg" -file-types = ["COMMIT_EDITMSG"] +file-types = [{ glob = "COMMIT_EDITMSG" }] comment-token = "#" indent = { tab-width = 2, unit = " " } rulers = [51, 73] @@ -1461,7 +1461,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-diff", rev = "fd7 [[language]] name = "git-rebase" scope = "source.gitrebase" -file-types = ["git-rebase-todo"] +file-types = [{ glob = "git-rebase-todo" }] injection-regex = "git-rebase" comment-token = "#" indent = { tab-width = 2, unit = "y" } @@ -1474,7 +1474,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-rebase", rev name = "regex" scope = "source.regex" injection-regex = "regex" -file-types = ["regex", ".Rbuildignore"] +file-types = ["regex", { glob = ".Rbuildignore" }] [[grammar]] name = "regex" @@ -1483,7 +1483,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-regex", rev = "e1cf [[language]] name = "git-config" scope = "source.gitconfig" -file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }] +file-types = [{ glob = ".gitmodules" }, { glob = ".gitconfig" }, { glob = ".git/config" }, { glob = ".config/git/config" }] injection-regex = "git-config" comment-token = "#" indent = { tab-width = 4, unit = "\t" } @@ -1495,7 +1495,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-config", rev [[language]] name = "git-attributes" scope = "source.gitattributes" -file-types = [".gitattributes"] +file-types = [{ glob = ".gitattributes" }] injection-regex = "git-attributes" comment-token = "#" grammar = "gitattributes" @@ -1507,7 +1507,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev = [[language]] name = "git-ignore" scope = "source.gitignore" -file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS", { suffix = ".config/helix/ignore" }, { suffix = ".helix/ignore" }] +file-types = [{ glob = ".gitignore" }, { glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = ".prettierignore" }, { glob = ".eslintignore" }, { glob = ".npmignore"}, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }] injection-regex = "git-ignore" comment-token = "#" grammar = "gitignore" @@ -1572,7 +1572,7 @@ source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = " name = "erlang" scope = "source.erlang" injection-regex = "erl(ang)?" -file-types = ["erl", "hrl", "app", "rebar.config", "rebar.lock"] +file-types = ["erl", "hrl", "app", { glob = "rebar.config" }, { glob = "rebar.lock" }] roots = ["rebar.config"] shebangs = ["escript"] comment-token = "%%" @@ -1698,7 +1698,7 @@ source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "322e4cc657 name = "r" scope = "source.r" injection-regex = "(r|R)" -file-types = ["r", "R", ".Rprofile", "Rprofile.site", ".RHistory"] +file-types = ["r", "R", { glob = ".Rprofile" }, { glob = "Rprofile.site" }, { glob = ".RHistory" }] shebangs = ["r", "R"] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -1913,7 +1913,7 @@ source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e4 name = "meson" scope = "source.meson" injection-regex = "meson" -file-types = ["meson.build", "meson_options.txt"] +file-types = [{ glob = "meson.build" }, { glob = "meson_options.txt" }] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -1924,7 +1924,7 @@ source = { git = "https://github.com/staysail/tree-sitter-meson", rev = "32a83e8 [[language]] name = "sshclientconfig" scope = "source.sshclientconfig" -file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }] +file-types = [{ glob = ".ssh/config" }, { glob = "/etc/ssh/ssh_config" }] comment-token = "#" [[grammar]] @@ -2045,7 +2045,7 @@ source = { git = "https://github.com/sogaiu/tree-sitter-clojure", rev = "e57c569 name = "starlark" scope = "source.starlark" injection-regex = "(starlark|bzl|bazel)" -file-types = ["bzl", "bazel", "BUILD", "star"] +file-types = ["bzl", "bazel", "star", { glob = "BUILD" }, { glob = "BUILD.*" }] comment-token = "#" indent = { tab-width = 4, unit = " " } grammar = "python" @@ -2413,7 +2413,7 @@ source = { git = "https://github.com/hh9527/tree-sitter-wit", rev = "c917790ab9a [[language]] name = "env" scope = "source.env" -file-types = [".env", ".env.local", ".env.development", ".env.production", ".env.dist", ".envrc", ".envrc.local", ".envrc.private"] +file-types = [{ glob = ".env" }, { glob = ".env.*" }, { glob = ".envrc" }, { glob = ".envrc.*" }] injection-regex = "env" comment-token = "#" indent = { tab-width = 4, unit = "\t" } @@ -2441,7 +2441,7 @@ file-types = [ "volume", "kube", "network", - ".editorconfig", + { glob = ".editorconfig" }, "properties", "cfg", "directory" @@ -2569,7 +2569,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-pem", rev = "be67a433 [[language]] name = "passwd" scope = "source.passwd" -file-types = ["passwd"] +file-types = [{ glob = "passwd" }] [[grammar]] name = "passwd" @@ -2578,7 +2578,7 @@ source = { git = "https://github.com/ath3/tree-sitter-passwd", rev = "20239395ea [[language]] name = "hosts" scope = "source.hosts" -file-types = ["hosts"] +file-types = [{ glob = "hosts" }] comment-token = "#" [[grammar]] @@ -2786,7 +2786,7 @@ source = { git = "https://github.com/lefp/tree-sitter-opencl", rev = "8e1d24a570 [[language]] name = "just" scope = "source.just" -file-types = ["justfile", "Justfile", ".justfile", ".Justfile"] +file-types = [{ glob = "justfile" }, { glob = "Justfile" }, { glob = ".justfile" }, { glob = ".Justfile" }] injection-regex = "just" comment-token = "#" indent = { tab-width = 4, unit = "\t" } @@ -2945,7 +2945,7 @@ source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "1f505 [[language]] name = "todotxt" scope = "text.todotxt" -file-types = [{ suffix = ".todo.txt" }, "todotxt"] +file-types = [{ glob = "todo.txt" }, { glob = "*.todo.txt" }, "todotxt"] formatter = { command = "sort" } auto-format = true From 786b5c533e32cfe88dbf8742fdbc18829fb46334 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sun, 11 Feb 2024 12:38:09 -0500 Subject: [PATCH 63/70] follow neovim's truecolor detection (#9577) --- Cargo.lock | 1 + helix-term/Cargo.toml | 1 + helix-term/src/lib.rs | 25 +++++++++++++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7ef8eb05..a0f5645dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1387,6 +1387,7 @@ dependencies = [ "signal-hook-tokio", "smallvec", "tempfile", + "termini", "tokio", "tokio-stream", "toml", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index a0d6754d0..f3343b7a4 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -42,6 +42,7 @@ signal-hook = "0.3" tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } arc-swap = { version = "1.6.0" } +termini = "1" # Logging fern = "0.6" diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index b1413ed0d..cdde86ec5 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -22,17 +22,30 @@ use url::Url; pub use keymap::macros::*; -#[cfg(not(windows))] -fn true_color() -> bool { - std::env::var("COLORTERM") - .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) - .unwrap_or(false) -} #[cfg(windows)] fn true_color() -> bool { true } +#[cfg(not(windows))] +fn true_color() -> bool { + if matches!( + std::env::var("COLORTERM").map(|v| matches!(v.as_str(), "truecolor" | "24bit")), + Ok(true) + ) { + return true; + } + + match termini::TermInfo::from_env() { + Ok(t) => { + t.extended_cap("RGB").is_some() + || t.extended_cap("Tc").is_some() + || (t.extended_cap("setrgbf").is_some() && t.extended_cap("setrgbb").is_some()) + } + Err(_) => false, + } +} + /// Function used for filtering dir entries in the various file pickers. fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool { // We always want to ignore the .git directory, otherwise if From 13b9885084ed516276db17b730cb1d818fe95716 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:17:02 +0100 Subject: [PATCH 64/70] build(deps): bump tokio from 1.35.1 to 1.36.0 (#9540) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.35.1 to 1.36.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.35.1...tokio-1.36.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- helix-lsp/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0f5645dd..8827dfdb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2318,9 +2318,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 8e9e3407c..2bdd5cfce 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -27,6 +27,6 @@ lsp-types = { version = "0.95" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.36", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.14" parking_lot = "0.12.1" From 204c3707b070c0933366b7366d48744b1426abaa Mon Sep 17 00:00:00 2001 From: 7ombie <80077603+7ombie@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:17:44 +0000 Subject: [PATCH 65/70] Updated Swift grammar, adding 'any' and 'await' keywords. (#9586) --- languages.toml | 2 +- runtime/queries/swift/highlights.scm | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/languages.toml b/languages.toml index 084d4932c..c2432c278 100644 --- a/languages.toml +++ b/languages.toml @@ -1730,7 +1730,7 @@ language-servers = [ "sourcekit-lsp" ] [[grammar]] name = "swift" -source = { git = "https://github.com/alex-pinkus/tree-sitter-swift", rev = "77c6312c8438f4dbaa0350cec92b3d6dd3d74a66" } +source = { git = "https://github.com/alex-pinkus/tree-sitter-swift", rev = "b1b66955d420d5cf5ff268ae552f0d6e43ff66e1" } [[language]] name = "erb" diff --git a/runtime/queries/swift/highlights.scm b/runtime/queries/swift/highlights.scm index 5560010b0..e7610e38d 100644 --- a/runtime/queries/swift/highlights.scm +++ b/runtime/queries/swift/highlights.scm @@ -1,10 +1,10 @@ -; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/8d2fd80e3322df51e3f70952e60d57f5d4077eb8/queries/highlights.scm +; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/1c586339fb00014b23d6933f2cc32b588a226f3b/queries/highlights.scm (line_string_literal ["\\(" ")"] @punctuation.special) ["." ";" ":" "," ] @punctuation.delimiter -["(" ")" "[" "]" "{" "}"] @punctuation.bracket ; TODO: "\\(" ")" in interpolations should be @punctuation.special +["(" ")" "[" "]" "{" "}"] @punctuation.bracket ; Identifiers (attribute) @variable @@ -26,6 +26,7 @@ (function_declaration "init" @constructor) (throws) @keyword "async" @keyword +"await" @keyword (where_keyword) @keyword (parameter external_name: (simple_identifier) @variable.parameter) (parameter name: (simple_identifier) @variable.parameter) @@ -48,6 +49,7 @@ "convenience" "required" "some" + "any" ] @keyword [ From 7d8ce1a4000939bb1d1e0a67277f7733735607c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:23:24 +0100 Subject: [PATCH 66/70] build(deps): bump tempfile from 3.9.0 to 3.10.0 (#9538) Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.9.0 to 3.10.0. - [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md) - [Commits](https://github.com/Stebalien/tempfile/compare/v3.9.0...v3.10.0) --- updated-dependencies: - dependency-name: tempfile dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 13 ++++++------- helix-loader/Cargo.toml | 2 +- helix-stdx/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- helix-vcs/Cargo.toml | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8827dfdb3..24c689336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,9 +424,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fern" @@ -1973,9 +1973,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -2203,13 +2203,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", "rustix", "windows-sys 0.52.0", ] diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index 469bedc10..6c3617dd1 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -30,7 +30,7 @@ log = "0.4" # cloning/compiling tree-sitter grammars cc = { version = "1" } threadpool = { version = "1.0" } -tempfile = "3.9.0" +tempfile = "3.10.0" dunce = "1.0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index e77f8b91f..540a1b99a 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -18,4 +18,4 @@ ropey = { version = "1.6.1", default-features = false } which = "6.0" [dev-dependencies] -tempfile = "3.9" +tempfile = "3.10" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index f3343b7a4..09f02c67c 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -84,4 +84,4 @@ helix-loader = { path = "../helix-loader" } [dev-dependencies] smallvec = "1.13" indoc = "2.0.4" -tempfile = "3.9.0" +tempfile = "3.10.0" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 6aa50dcf7..32aca4f09 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -29,4 +29,4 @@ log = "0.4" git = ["gix"] [dev-dependencies] -tempfile = "3.9" +tempfile = "3.10" From d9f7aaacaf65272dfce092954ec49a78961e8112 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Sun, 11 Feb 2024 20:28:52 -0500 Subject: [PATCH 67/70] languages: add CEL, SpiceDB schema language (#9296) * languages: add CEL language and grammar * languages: add spicedb schema language * chore: docgen * runtime/queries: refine spicedb & cel highlights Co-authored-by: Michael Davis * languages: update spicedb --------- Co-authored-by: Michael Davis --- book/src/generated/lang-support.md | 2 + languages.toml | 24 ++++++++++ runtime/queries/cel/highlights.scm | 66 ++++++++++++++++++++++++++ runtime/queries/spicedb/highlights.scm | 47 ++++++++++++++++++ runtime/queries/spicedb/injections.scm | 5 ++ runtime/queries/spicedb/tags.scm | 4 ++ 6 files changed, 148 insertions(+) create mode 100644 runtime/queries/cel/highlights.scm create mode 100644 runtime/queries/spicedb/highlights.scm create mode 100644 runtime/queries/spicedb/injections.scm create mode 100644 runtime/queries/spicedb/tags.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index c91b9ae11..9131ff580 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -14,6 +14,7 @@ | cabal | | | | `haskell-language-server-wrapper` | | cairo | ✓ | ✓ | ✓ | `cairo-language-server` | | capnp | ✓ | | ✓ | | +| cel | ✓ | | | | | clojure | ✓ | | | `clojure-lsp` | | cmake | ✓ | ✓ | ✓ | `cmake-language-server` | | comment | ✓ | | | | @@ -154,6 +155,7 @@ | smithy | ✓ | | | `cs` | | sml | ✓ | | | | | solidity | ✓ | | | `solc` | +| spicedb | ✓ | | | | | sql | ✓ | | | | | sshclientconfig | ✓ | | | | | starlark | ✓ | ✓ | | | diff --git a/languages.toml b/languages.toml index c2432c278..6fc811814 100644 --- a/languages.toml +++ b/languages.toml @@ -514,6 +514,30 @@ args = { processId = "{0}" } name = "c-sharp" source = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "5b60f99545fea00a33bbfae5be956f684c4c69e2" } +[[language]] +name = "cel" +scope = "source.cel" +injection-regex = "cel" +file-types = ["cel"] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[[grammar]] +name = "cel" +source = { git = "https://github.com/bufbuild/tree-sitter-cel", rev = "9f2b65da14c216df53933748e489db0f11121464" } + +[[language]] +name = "spicedb" +scope = "source.zed" +injection-regex = "spicedb" +file-types = ["zed"] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[[grammar]] +name = "spicedb" +source = { git = "https://github.com/jzelinskie/tree-sitter-spicedb", rev = "a4e4645651f86d6684c15dfa9931b7841dc52a66" } + [[language]] name = "go" scope = "source.go" diff --git a/runtime/queries/cel/highlights.scm b/runtime/queries/cel/highlights.scm new file mode 100644 index 000000000..ab3bae5a5 --- /dev/null +++ b/runtime/queries/cel/highlights.scm @@ -0,0 +1,66 @@ +; Operators + +[ + "-" + "!" + "*" + "/" + "&&" + "%" + "+" + "<" + "<=" + "==" + ">" + ">=" + "||" +] @operator + +; Keywords + +[ +"in" +] @keyword + +; Function calls + +(call_expression + function: (identifier) @function) + +(member_call_expression + function: (identifier) @function) + +; Identifiers + +(select_expression + operand: (identifier) @type) + +(select_expression + operand: (select_expression + member: (identifier) @type)) + +(identifier) @variable.other.member + +; Literals + +[ + (double_quote_string_literal) + (single_quoted_string_literal) + (triple_double_quote_string_literal) + (triple_single_quoted_string_literal) +] @string + +[ + (int_literal) + (uint_literal) +] @constant.numeric.integer +(float_literal) @constant.numeric.float + +[ + (true) + (false) +] @constant.builtin.boolean + +(null) @constant.builtin + +(comment) @comment diff --git a/runtime/queries/spicedb/highlights.scm b/runtime/queries/spicedb/highlights.scm new file mode 100644 index 000000000..63c939551 --- /dev/null +++ b/runtime/queries/spicedb/highlights.scm @@ -0,0 +1,47 @@ +; highlights.scm + +[ + "definition" + "caveat" + "permission" + "relation" + "nil" +] @keyword + +[ + "," + ":" +] @punctuation.delimiter + +[ + "(" + ")" + "{" + "}" +] @punctuation.bracket + +[ + "|" + "+" + "-" + "&" + "#" + "->" + "=" +] @operator +("with") @keyword.operator + +[ + "nil" + "*" +] @constant.builtin + +(comment) @comment +(type_identifier) @type +(cel_type_identifier) @type +(cel_variable_identifier) @variable.parameter +(field_identifier) @variable.other.member +[ + (func_identifier) + (method_identifier) +] @function.method diff --git a/runtime/queries/spicedb/injections.scm b/runtime/queries/spicedb/injections.scm new file mode 100644 index 000000000..f8cafc9d1 --- /dev/null +++ b/runtime/queries/spicedb/injections.scm @@ -0,0 +1,5 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +((caveat_expr) @injection.content + (#set! injection.language "cel")) diff --git a/runtime/queries/spicedb/tags.scm b/runtime/queries/spicedb/tags.scm new file mode 100644 index 000000000..a8fe932b6 --- /dev/null +++ b/runtime/queries/spicedb/tags.scm @@ -0,0 +1,4 @@ +(object_definition + name: (type_identifier) @name) @definition.type + +(type_identifier) @name @reference.type From ac8d1f62a126781fbac087aa374aad540dd659b1 Mon Sep 17 00:00:00 2001 From: HumanEntity <102693471+HumanEntity@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:29:49 +0100 Subject: [PATCH 68/70] sonokai: Add color modes support and change contrast between ruler and bg (#9376) * Added `editor.color-modes` option support * Less contrast between bg and ruler --- runtime/themes/sonokai.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/themes/sonokai.toml b/runtime/themes/sonokai.toml index c7c9adc06..f586be28d 100644 --- a/runtime/themes/sonokai.toml +++ b/runtime/themes/sonokai.toml @@ -63,6 +63,9 @@ "ui.cursorline.primary" = { bg = "bg1" } "ui.statusline" = { fg = "fg", bg = "bg3" } "ui.statusline.inactive" = { fg = "grey", bg = "bg1" } +"ui.statusline.normal" = { fg = "bg0", bg = "blue", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "bg0", bg = "green", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "bg0", bg = "purple", modifiers = ["bold"] } "ui.popup" = { fg = "grey", bg = "bg2" } "ui.window" = { fg = "grey", bg = "bg0" } "ui.help" = { fg = "fg", bg = "bg1" } @@ -71,7 +74,7 @@ "ui.menu" = { fg = "fg", bg = "bg2" } "ui.menu.selected" = { fg = "bg0", bg = "green" } "ui.virtual.whitespace" = { fg = "grey_dim" } -"ui.virtual.ruler" = { bg = "grey_dim" } +"ui.virtual.ruler" = { bg = "bg3" } "ui.virtual.inlay-hint" = { fg = "grey_dim" } info = { fg = 'green', bg = 'bg2' } From 6a90166d0a3d8fd0e2e96e4ac8e196b2b2989760 Mon Sep 17 00:00:00 2001 From: ontley <67148677+ontley@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:35:25 +0100 Subject: [PATCH 69/70] Add required-root-patterns for situational lsp activation (#8696) * Added required-root-patterns for situational lsp activation using globbing * Replaced filter_map with flatten * updated book to include required-root-patterns option * fixed wrong function name for path * Added globset to helix-core. Moved globset building to config parsing. * Normalize implements AsRef * cargo fmt * Revert "cargo fmt" This reverts commit ca8ce123e8d77d2ae8ed84d5273a9b554101b0db. --- book/src/languages.md | 15 +++++----- helix-core/Cargo.toml | 1 + helix-core/src/syntax.rs | 23 +++++++++++++++ helix-lsp/src/client.rs | 21 ++------------ helix-lsp/src/lib.rs | 63 ++++++++++++++++++++++++++++++---------- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/book/src/languages.md b/book/src/languages.md index 7e49a6036..e3900dca9 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -122,13 +122,14 @@ languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT These are the available options for a language server. -| Key | Description | -| ---- | ----------- | -| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` | -| `args` | A list of arguments to pass to the language server binary | -| `config` | LSP initialization options | -| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | -| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | +| Key | Description | +| ---- | ----------- | +| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` | +| `args` | A list of arguments to pass to the language server binary | +| `config` | LSP initialization options | +| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | +| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | +| `required-root-patterns` | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found. | A `format` sub-table within `config` can be used to pass extra formatting options to [Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting). diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index bdc879caa..e0eb6401c 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -53,6 +53,7 @@ globset = "0.4.14" nucleo.workspace = true parking_lot = "0.12" +globset = "0.4.14" [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 99b5a3d10..5d45deaf4 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -10,6 +10,7 @@ use crate::{ use ahash::RandomState; use arc_swap::{ArcSwap, Guard}; use bitflags::bitflags; +use globset::GlobSet; use hashbrown::raw::RawTable; use slotmap::{DefaultKey as LayerId, HopSlotMap}; @@ -365,6 +366,22 @@ where serializer.end() } +fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let patterns = Vec::::deserialize(deserializer)?; + if patterns.is_empty() { + return Ok(None); + } + let mut builder = globset::GlobSetBuilder::new(); + for pattern in patterns { + let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?; + builder.add(glob); + } + builder.build().map(Some).map_err(serde::de::Error::custom) +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -378,6 +395,12 @@ pub struct LanguageServerConfiguration { pub config: Option, #[serde(default = "default_timeout")] pub timeout: u64, + #[serde( + default, + skip_serializing, + deserialize_with = "deserialize_required_root_patterns" + )] + pub required_root_patterns: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 94bad6faf..0d3a2a56e 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -177,12 +177,11 @@ impl Client { args: &[String], config: Option, server_environment: HashMap, - root_markers: &[String], - manual_roots: &[PathBuf], + root_path: PathBuf, + root_uri: Option, id: usize, name: String, req_timeout: u64, - doc_path: Option<&std::path::PathBuf>, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { // Resolve path to the binary let cmd = helix_stdx::env::which(cmd)?; @@ -206,22 +205,6 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id, name.clone()); - let (workspace, workspace_is_cwd) = find_workspace(); - let workspace = path::normalize(workspace); - let root = find_lsp_workspace( - doc_path - .and_then(|x| x.parent().and_then(|x| x.to_str())) - .unwrap_or("."), - root_markers, - manual_roots, - &workspace, - workspace_is_cwd, - ); - - // `root_uri` and `workspace_folder` can be empty in case there is no workspace - // `root_url` can not, use `workspace` as a fallback - let root_path = root.clone().unwrap_or_else(|| workspace.clone()); - let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); let workspace_folders = root_uri .clone() diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 4ce445aee..05764418f 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -680,7 +680,7 @@ impl Registry { doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result> { + ) -> Result>> { let config = self .syn_loader .language_server_configs() @@ -688,7 +688,7 @@ impl Registry { .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?; let id = self.counter; self.counter += 1; - let NewClient(client, incoming) = start_client( + if let Some(NewClient(client, incoming)) = start_client( id, name, ls_config, @@ -696,9 +696,12 @@ impl Registry { doc_path, root_dirs, enable_snippets, - )?; - self.incoming.push(UnboundedReceiverStream::new(incoming)); - Ok(client) + )? { + self.incoming.push(UnboundedReceiverStream::new(incoming)); + Ok(Some(client)) + } else { + Ok(None) + } } /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, @@ -723,8 +726,8 @@ impl Registry { root_dirs, enable_snippets, ) { - Ok(client) => client, - error => return Some(error), + Ok(client) => client?, + Err(error) => return Some(Err(error)), }; let old_clients = self .inner @@ -764,13 +767,13 @@ impl Registry { root_dirs: &'a [PathBuf], enable_snippets: bool, ) -> impl Iterator>)> + 'a { - language_config.language_servers.iter().map( + language_config.language_servers.iter().filter_map( move |LanguageServerFeatures { name, .. }| { if let Some(clients) = self.inner.get(name) { if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) }) { - return (name.to_owned(), Ok(client.clone())); + return Some((name.to_owned(), Ok(client.clone()))); } } match self.start_client( @@ -781,13 +784,14 @@ impl Registry { enable_snippets, ) { Ok(client) => { + let client = client?; self.inner .entry(name.to_owned()) .or_default() .push(client.clone()); - (name.clone(), Ok(client)) + Some((name.clone(), Ok(client))) } - Err(err) => (name.to_owned(), Err(err)), + Err(err) => Some((name.to_owned(), Err(err))), } }, ) @@ -888,18 +892,45 @@ fn start_client( doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, -) -> Result { +) -> Result> { + let (workspace, workspace_is_cwd) = helix_loader::find_workspace(); + let workspace = path::normalize(workspace); + let root = find_lsp_workspace( + doc_path + .and_then(|x| x.parent().and_then(|x| x.to_str())) + .unwrap_or("."), + &config.roots, + config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), + &workspace, + workspace_is_cwd, + ); + + // `root_uri` and `workspace_folder` can be empty in case there is no workspace + // `root_url` can not, use `workspace` as a fallback + let root_path = root.clone().unwrap_or_else(|| workspace.clone()); + let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); + + if let Some(globset) = &ls_config.required_root_patterns { + if !root_path + .read_dir()? + .flatten() + .map(|entry| entry.file_name()) + .any(|entry| globset.is_match(entry)) + { + return Ok(None); + } + } + let (client, incoming, initialize_notify) = Client::start( &ls_config.command, &ls_config.args, ls_config.config.clone(), ls_config.environment.clone(), - &config.roots, - config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), + root_path, + root_uri, id, name, ls_config.timeout, - doc_path, )?; let client = Arc::new(client); @@ -938,7 +969,7 @@ fn start_client( initialize_notify.notify_one(); }); - Ok(NewClient(client, incoming)) + Ok(Some(NewClient(client, incoming))) } /// Find an LSP workspace of a file using the following mechanism: From ad7b7bc8047d3c0930bbc6201d8f69132b396b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 12 Feb 2024 12:01:17 +0900 Subject: [PATCH 70/70] minor: Fix compilation --- helix-core/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index e0eb6401c..fb68ccc08 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -49,7 +49,6 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std" etcetera = "0.8" textwrap = "0.16.0" -globset = "0.4.14" nucleo.workspace = true parking_lot = "0.12"