Compare commits

...

27 Commits

Author SHA1 Message Date
Michael Davis
88a9c463da
Merge d15fab75ea into dc941d6d24 2024-11-22 19:34:22 +08:00
Philipp Mildenberger
dc941d6d24
Add support for path completion (#2608)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-11-21 21:12:36 -06:00
Lens0021 / Leslie
f305c7299d
Add support for Amber-lang (#12021)
Co-authored-by: Phoenix Himself <pkaras.it@gmail.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-11-21 10:09:42 -06:00
Valentin B.
9e0d2d0a19
chore(solidity): add highlight queries (#12102)
Add highlights for `hex` and `unicode` string prefixes and YUL booleans
2024-11-21 07:58:14 -06:00
Michael Davis
d15fab75ea
Merge branch 'master' into reverse-query-precedence-ordering 2024-11-09 15:40:38 -05:00
Michael Davis
f3a46c8751
Update cue parser and queries
Queries ported from nvim-treesitter
2024-10-03 13:02:49 -04:00
gabydd
400a1930ad grammars: update ron and dockerfile 2024-07-16 00:59:48 -04:00
gabydd
8caba39b05 queries: update zig 2024-07-15 23:30:53 -04:00
Blaž Hrastnik
b13999952f queries: update more grammars 2024-05-22 01:06:08 +09:00
Blaž Hrastnik
b7656e0079 queries: bash, devicetree, yaml 2024-05-21 03:43:57 +09:00
Blaž Hrastnik
1231429028 queries: Update cue 2024-05-20 19:48:25 +09:00
Blaž Hrastnik
29b4a2f042 queries: Update ron 2024-05-20 19:36:05 +09:00
Blaž Hrastnik
67baa536ec queries: Update docker, dot 2024-05-20 19:32:54 +09:00
Blaž Hrastnik
bbdebe9d72 queries: Reverse go queries 2024-05-20 19:12:53 +09:00
Blaž Hrastnik
c6645d4d8f queries: Update janet 2024-05-20 19:12:53 +09:00
Blaž Hrastnik
aad5f6a5e2 queries: Update ld, strace 2024-05-20 19:12:53 +09:00
Blaž Hrastnik
0fbfc0cc8d queries: Update vhs 2024-05-20 19:12:53 +09:00
Blaž Hrastnik
de0618186c queries: Update cel 2024-05-20 19:12:53 +09:00
Blaž Hrastnik
44605c4dce queries: Reorder scheme 2024-05-20 19:12:53 +09:00
Blaž Hrastnik
974303e200 queries: Tweak capnp 2024-05-20 19:12:53 +09:00
Iorvethe
bd9cfbb61d
Fix #match? predicates in julia queries (#10793) 2024-05-19 18:29:53 +09:00
Iorvethe
951b454a33 Update tree-sitter-julia (#10031)
Update julia parser to latest version, along with:
- updating the queries,
- pulling changes from `nvim-treesitter`’s queries (as the maintainters
  of the parser update the queries there),
- reversing the queries’ order to be compatible with upstream.
2024-05-17 20:32:40 +09:00
postsolar
0364f7c879 Reverse Nix highlights queries 2024-05-17 20:29:59 +09:00
postsolar
132d388444 Update Haskell highlight queries in light of reversing precedence ordering 2024-05-17 20:29:59 +09:00
postsolar
96688a5915 Update PureScript highlight queries in light of reversing queries precedence order
This commit adapts PureScript highlight queries to match upcoming changes in Helix which will make queries matching order go from more to general to more specific, the opposite of previous order.
2024-05-17 20:29:59 +09:00
Michael Davis
c75179d921 WIP: Reverse highlight queries 2024-05-17 20:29:57 +09:00
Michael Davis
0b5f0d606a Reverse syntax highlighting query precedence ordering 2024-05-17 20:27:08 +09:00
59 changed files with 2116 additions and 772 deletions

2
Cargo.lock generated
View File

@ -1346,6 +1346,8 @@ dependencies = [
"bitflags", "bitflags",
"dunce", "dunce",
"etcetera", "etcetera",
"once_cell",
"regex-automata",
"regex-cursor", "regex-cursor",
"ropey", "ropey",
"rustix", "rustix",

View File

@ -33,6 +33,7 @@ ### `[editor]` Section
| `cursorcolumn` | Highlight all columns with a cursor | `false` | | `cursorcolumn` | Highlight all columns with a cursor | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | | `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
| `auto-completion` | Enable automatic pop up of auto-completion | `true` | | `auto-completion` | Enable automatic pop up of auto-completion | `true` |
| `path-completion` | Enable filepath completion. Show files and directories if an existing path at the cursor was recognized, either absolute or relative to the current opened document or current working directory (if the buffer is not yet saved). Defaults to true. | `true` |
| `auto-format` | Enable automatic formatting on save | `true` | | `auto-format` | Enable automatic formatting on save | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `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` | | `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |

View File

@ -3,6 +3,7 @@
| ada | ✓ | ✓ | | `ada_language_server` | | ada | ✓ | ✓ | | `ada_language_server` |
| adl | ✓ | ✓ | ✓ | | | adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | | | agda | ✓ | | | |
| amber | ✓ | | | |
| astro | ✓ | | | | | astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` | | awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | ✓ | ✓ | `bash-language-server` | | bash | ✓ | ✓ | ✓ | `bash-language-server` |

View File

@ -69,6 +69,7 @@ ## Language configuration
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section) | `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
| `path-completion` | Overrides the `editor.path-completion` config key for the language. |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save. | `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.

View File

@ -0,0 +1,12 @@
use std::borrow::Cow;
use crate::Transaction;
#[derive(Debug, PartialEq, Clone)]
pub struct CompletionItem {
pub transaction: Transaction,
pub label: Cow<'static, str>,
pub kind: Cow<'static, str>,
/// Containing Markdown
pub documentation: String,
}

View File

@ -3,6 +3,7 @@
pub mod auto_pairs; pub mod auto_pairs;
pub mod chars; pub mod chars;
pub mod comment; pub mod comment;
pub mod completion;
pub mod config; pub mod config;
pub mod diagnostic; pub mod diagnostic;
pub mod diff; pub mod diff;
@ -63,6 +64,7 @@ pub mod unicode {
pub use smallvec::{smallvec, SmallVec}; pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax; pub use syntax::Syntax;
pub use completion::CompletionItem;
pub use diagnostic::Diagnostic; pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING}; pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};

View File

@ -125,6 +125,9 @@ pub struct LanguageConfiguration {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub formatter: Option<FormatterConfiguration>, pub formatter: Option<FormatterConfiguration>,
/// If set, overrides `editor.path-completion`.
pub path_completion: Option<bool>,
#[serde(default)] #[serde(default)]
pub diagnostic_severity: Severity, pub diagnostic_severity: Severity,
@ -2481,15 +2484,17 @@ fn next(&mut self) -> Option<Self::Item> {
} }
} }
// Once a highlighting pattern is found for the current node, skip over // Use the last capture found for the current node, skipping over any
// any later highlighting patterns that also match this node. Captures // highlight patterns that also match this node. Captures
// for a given node are ordered by pattern index, so these subsequent // for a given node are ordered by pattern index, so these subsequent
// captures are guaranteed to be for highlighting, not injections or // captures are guaranteed to be for highlighting, not injections or
// local variables. // local variables.
while let Some((next_match, next_capture_index)) = captures.peek() { while let Some((next_match, next_capture_index)) = captures.peek() {
let next_capture = next_match.captures[*next_capture_index]; let next_capture = next_match.captures[*next_capture_index];
if next_capture.node == capture.node { if next_capture.node == capture.node {
captures.next(); match_.remove();
capture = next_capture;
match_ = captures.next().unwrap().0;
} else { } else {
break; break;
} }

View File

@ -1,15 +1,18 @@
use std::borrow::Borrow;
use std::future::Future; use std::future::Future;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::Arc;
pub use oneshot::channel as cancelation; use tokio::sync::Notify;
use tokio::sync::oneshot;
pub type CancelTx = oneshot::Sender<()>; pub async fn cancelable_future<T>(
pub type CancelRx = oneshot::Receiver<()>; future: impl Future<Output = T>,
cancel: impl Borrow<TaskHandle>,
pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> { ) -> Option<T> {
tokio::select! { tokio::select! {
biased; biased;
_ = cancel => { _ = cancel.borrow().canceled() => {
None None
} }
res = future => { res = future => {
@ -17,3 +20,268 @@ pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: Cance
} }
} }
} }
#[derive(Default, Debug)]
struct Shared {
state: AtomicU64,
// `Notify` has some features that we don't really need here because it
// supports waking single tasks (`notify_one`) and does its own (more
// complicated) state tracking, we could reimplement the waiter linked list
// with modest effort and reduce memory consumption by one word/8 bytes and
// reduce code complexity/number of atomic operations.
//
// I don't think that's worth the complexity (unsafe code).
//
// if we only cared about async code then we could also only use a notify
// (without the generation count), this would be equivalent (or maybe more
// correct if we want to allow cloning the TX) but it would be extremly slow
// to frequently check for cancelation from sync code
notify: Notify,
}
impl Shared {
fn generation(&self) -> u32 {
self.state.load(Relaxed) as u32
}
fn num_running(&self) -> u32 {
(self.state.load(Relaxed) >> 32) as u32
}
/// Increments the generation count and sets `num_running`
/// to the provided value, this operation is not with
/// regard to the generation counter (doesn't use `fetch_add`)
/// so the calling code must ensure it cannot execute concurrently
/// to maintain correctness (but not safety)
fn inc_generation(&self, num_running: u32) -> (u32, u32) {
let state = self.state.load(Relaxed);
let generation = state as u32;
let prev_running = (state >> 32) as u32;
// no need to create a new generation if the refcount is zero (fastpath)
if prev_running == 0 && num_running == 0 {
return (generation, 0);
}
let new_generation = generation.saturating_add(1);
self.state.store(
new_generation as u64 | ((num_running as u64) << 32),
Relaxed,
);
self.notify.notify_waiters();
(new_generation, prev_running)
}
fn inc_running(&self, generation: u32) {
let mut state = self.state.load(Relaxed);
loop {
let current_generation = state as u32;
if current_generation != generation {
break;
}
let off = 1 << 32;
let res = self.state.compare_exchange_weak(
state,
state.saturating_add(off),
Relaxed,
Relaxed,
);
match res {
Ok(_) => break,
Err(new_state) => state = new_state,
}
}
}
fn dec_running(&self, generation: u32) {
let mut state = self.state.load(Relaxed);
loop {
let current_generation = state as u32;
if current_generation != generation {
break;
}
let num_running = (state >> 32) as u32;
// running can't be zero here, that would mean we miscounted somewhere
assert_ne!(num_running, 0);
let off = 1 << 32;
let res = self
.state
.compare_exchange_weak(state, state - off, Relaxed, Relaxed);
match res {
Ok(_) => break,
Err(new_state) => state = new_state,
}
}
}
}
// This intentionally doesn't implement `Clone` and requires a mutable reference
// for cancelation to avoid races (in inc_generation).
/// A task controller allows managing a single subtask enabling the controller
/// to cancel the subtask and to check whether it is still running.
///
/// For efficiency reasons the controller can be reused/restarted,
/// in that case the previous task is automatically canceled.
///
/// If the controller is dropped, the subtasks are automatically canceled.
#[derive(Default, Debug)]
pub struct TaskController {
shared: Arc<Shared>,
}
impl TaskController {
pub fn new() -> Self {
TaskController::default()
}
/// Cancels the active task (handle).
///
/// Returns whether any tasks were still running before the cancelation.
pub fn cancel(&mut self) -> bool {
self.shared.inc_generation(0).1 != 0
}
/// Checks whether there are any task handles
/// that haven't been dropped (or canceled) yet.
pub fn is_running(&self) -> bool {
self.shared.num_running() != 0
}
/// Starts a new task and cancels the previous task (handles).
pub fn restart(&mut self) -> TaskHandle {
TaskHandle {
generation: self.shared.inc_generation(1).0,
shared: self.shared.clone(),
}
}
}
impl Drop for TaskController {
fn drop(&mut self) {
self.cancel();
}
}
/// A handle that is used to link a task with a task controller.
///
/// It can be used to cancel async futures very efficiently but can also be checked for
/// cancelation very quickly (single atomic read) in blocking code.
/// The handle can be cheaply cloned (reference counted).
///
/// The TaskController can check whether a task is "running" by inspecting the
/// refcount of the (current) tasks handles. Therefore, if that information
/// is important, ensure that the handle is not dropped until the task fully
/// completes.
pub struct TaskHandle {
shared: Arc<Shared>,
generation: u32,
}
impl Clone for TaskHandle {
fn clone(&self) -> Self {
self.shared.inc_running(self.generation);
TaskHandle {
shared: self.shared.clone(),
generation: self.generation,
}
}
}
impl Drop for TaskHandle {
fn drop(&mut self) {
self.shared.dec_running(self.generation);
}
}
impl TaskHandle {
/// Waits until [`TaskController::cancel`] is called for the corresponding
/// [`TaskController`]. Immediately returns if `cancel` was already called since
pub async fn canceled(&self) {
let notified = self.shared.notify.notified();
if !self.is_canceled() {
notified.await
}
}
pub fn is_canceled(&self) -> bool {
self.generation != self.shared.generation()
}
}
#[cfg(test)]
mod tests {
use std::future::poll_fn;
use futures_executor::block_on;
use tokio::task::yield_now;
use crate::{cancelable_future, TaskController};
#[test]
fn immediate_cancel() {
let mut controller = TaskController::new();
let handle = controller.restart();
controller.cancel();
assert!(handle.is_canceled());
controller.restart();
assert!(handle.is_canceled());
let res = block_on(cancelable_future(
poll_fn(|_cx| std::task::Poll::Ready(())),
handle,
));
assert!(res.is_none());
}
#[test]
fn running_count() {
let mut controller = TaskController::new();
let handle = controller.restart();
assert!(controller.is_running());
assert!(!handle.is_canceled());
drop(handle);
assert!(!controller.is_running());
assert!(!controller.cancel());
let handle = controller.restart();
assert!(!handle.is_canceled());
assert!(controller.is_running());
let handle2 = handle.clone();
assert!(!handle.is_canceled());
assert!(controller.is_running());
drop(handle2);
assert!(!handle.is_canceled());
assert!(controller.is_running());
assert!(controller.cancel());
assert!(handle.is_canceled());
assert!(!controller.is_running());
}
#[test]
fn no_cancel() {
let mut controller = TaskController::new();
let handle = controller.restart();
assert!(!handle.is_canceled());
let res = block_on(cancelable_future(
poll_fn(|_cx| std::task::Poll::Ready(())),
handle,
));
assert!(res.is_some());
}
#[test]
fn delayed_cancel() {
let mut controller = TaskController::new();
let handle = controller.restart();
let mut hit = false;
let res = block_on(cancelable_future(
async {
controller.cancel();
hit = true;
yield_now().await;
},
handle,
));
assert!(res.is_none());
assert!(hit);
}
}

View File

@ -32,7 +32,7 @@
//! to helix-view in the future if we manage to detach the compositor from its rendering backend. //! to helix-view in the future if we manage to detach the compositor from its rendering backend.
use anyhow::Result; use anyhow::Result;
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx}; pub use cancel::{cancelable_future, TaskController, TaskHandle};
pub use debounce::{send_blocking, AsyncHook}; pub use debounce::{send_blocking, AsyncHook};
pub use redraw::{ pub use redraw::{
lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop, lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop,

View File

@ -18,6 +18,8 @@ ropey = { version = "1.6.1", default-features = false }
which = "7.0" which = "7.0"
regex-cursor = "0.1.4" regex-cursor = "0.1.4"
bitflags = "2.6" bitflags = "2.6"
once_cell = "1.19"
regex-automata = "0.4.8"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] } windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }

View File

@ -1,9 +1,12 @@
use std::{ use std::{
ffi::OsStr, borrow::Cow,
ffi::{OsStr, OsString},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::RwLock, sync::RwLock,
}; };
use once_cell::sync::Lazy;
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None); static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
// Get the current working directory. // Get the current working directory.
@ -59,6 +62,93 @@ pub fn which<T: AsRef<OsStr>>(
}) })
} }
fn find_brace_end(src: &[u8]) -> Option<usize> {
use regex_automata::meta::Regex;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::builder().build("[{}]").unwrap());
let mut depth = 0;
for mat in REGEX.find_iter(src) {
let pos = mat.start();
match src[pos] {
b'{' => depth += 1,
b'}' if depth == 0 => return Some(pos),
b'}' => depth -= 1,
_ => unreachable!(),
}
}
None
}
fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>) -> Cow<OsStr> {
use regex_automata::meta::Regex;
static REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::builder()
.build_many(&[
r"\$\{([^\}:]+):-",
r"\$\{([^\}:]+):=",
r"\$\{([^\}-]+)-",
r"\$\{([^\}=]+)=",
r"\$\{([^\}]+)",
r"\$(\w+)",
])
.unwrap()
});
let bytes = src.as_encoded_bytes();
let mut res = Vec::with_capacity(bytes.len());
let mut pos = 0;
for captures in REGEX.captures_iter(bytes) {
let mat = captures.get_match().unwrap();
let pattern_id = mat.pattern().as_usize();
let mut range = mat.range();
let var = &bytes[captures.get_group(1).unwrap().range()];
let default = if pattern_id != 5 {
let Some(bracket_pos) = find_brace_end(&bytes[range.end..]) else {
break;
};
let default = &bytes[range.end..range.end + bracket_pos];
range.end += bracket_pos + 1;
default
} else {
&[]
};
// safety: this is a codepoint aligned substring of an osstr (always valid)
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
let expansion = resolve(var);
let expansion = match &expansion {
Some(val) => {
if val.is_empty() && pattern_id < 2 {
default
} else {
val.as_encoded_bytes()
}
}
None => default,
};
res.extend_from_slice(&bytes[pos..range.start]);
pos = range.end;
res.extend_from_slice(expansion);
}
if pos == 0 {
src.into()
} else {
res.extend_from_slice(&bytes[pos..]);
// safety: this is a composition of valid osstr (and codepoint aligned slices which are also valid)
unsafe { OsString::from_encoded_bytes_unchecked(res) }.into()
}
}
/// performs substitution of enviorment variables. Supports the following (POSIX) syntax:
///
/// * `$<var>`, `${<var>}`
/// * `${<var>:-<default>}`, `${<var>-<default>}`
/// * `${<var>:=<default>}`, `${<var>=default}`
///
pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<OsStr> {
expand_impl(src.as_ref(), |var| std::env::var_os(var))
}
#[derive(Debug)] #[derive(Debug)]
pub struct ExecutableNotFoundError { pub struct ExecutableNotFoundError {
command: String, command: String,
@ -75,7 +165,9 @@ impl std::error::Error for ExecutableNotFoundError {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{current_working_dir, set_current_working_dir}; use std::ffi::{OsStr, OsString};
use super::{current_working_dir, expand_impl, set_current_working_dir};
#[test] #[test]
fn current_dir_is_set() { fn current_dir_is_set() {
@ -88,4 +180,34 @@ fn current_dir_is_set() {
let cwd = current_working_dir(); let cwd = current_working_dir();
assert_eq!(cwd, new_path); assert_eq!(cwd, new_path);
} }
macro_rules! assert_env_expand {
($env: expr, $lhs: expr, $rhs: expr) => {
assert_eq!(&*expand_impl($lhs.as_ref(), $env), OsStr::new($rhs));
};
}
/// paths that should work on all platforms
#[test]
fn test_env_expand() {
let env = |var: &OsStr| -> Option<OsString> {
match var.to_str().unwrap() {
"FOO" => Some("foo".into()),
"EMPTY" => Some("".into()),
_ => None,
}
};
assert_env_expand!(env, "pass_trough", "pass_trough");
assert_env_expand!(env, "$FOO", "foo");
assert_env_expand!(env, "bar/$FOO/baz", "bar/foo/baz");
assert_env_expand!(env, "bar/${FOO}/baz", "bar/foo/baz");
assert_env_expand!(env, "baz/${BAR:-bar}/foo", "baz/bar/foo");
assert_env_expand!(env, "baz/${BAR:=bar}/foo", "baz/bar/foo");
assert_env_expand!(env, "baz/${BAR-bar}/foo", "baz/bar/foo");
assert_env_expand!(env, "baz/${BAR=bar}/foo", "baz/bar/foo");
assert_env_expand!(env, "baz/${EMPTY:-bar}/foo", "baz/bar/foo");
assert_env_expand!(env, "baz/${EMPTY:=bar}/foo", "baz/bar/foo");
assert_env_expand!(env, "baz/${EMPTY-bar}/foo", "baz//foo");
assert_env_expand!(env, "baz/${EMPTY=bar}/foo", "baz//foo");
}
} }

View File

@ -1,8 +1,12 @@
pub use etcetera::home_dir; pub use etcetera::home_dir;
use once_cell::sync::Lazy;
use regex_cursor::{engines::meta::Regex, Input};
use ropey::RopeSlice;
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::OsString, ffi::OsString,
ops::Range,
path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR}, path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR},
}; };
@ -51,7 +55,7 @@ pub fn expand_tilde<'a, P>(path: P) -> Cow<'a, Path>
/// Normalize a path without resolving symlinks. /// Normalize a path without resolving symlinks.
// Strategy: start from the first component and move up. Cannonicalize previous path, // Strategy: start from the first component and move up. Cannonicalize previous path,
// join component, cannonicalize new path, strip prefix and join to the final result. // join component, canonicalize new path, strip prefix and join to the final result.
pub fn normalize(path: impl AsRef<Path>) -> PathBuf { pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
let mut components = path.as_ref().components().peekable(); let mut components = path.as_ref().components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
@ -201,6 +205,96 @@ pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
ret ret
} }
fn path_component_regex(windows: bool) -> String {
// TODO: support backslash path escape on windows (when using git bash for example)
let space_escape = if windows { r"[\^`]\s" } else { r"[\\]\s" };
// partially baesd on what's allowed in an url but with some care to avoid
// false positivies (like any kind of brackets or quotes)
r"[\w@.\-+#$%?!,;~&]|".to_owned() + space_escape
}
/// Regex for delimited environment captures like `${HOME}`.
fn braced_env_regex(windows: bool) -> String {
r"\$\{(?:".to_owned() + &path_component_regex(windows) + r"|[/:=])+\}"
}
fn compile_path_regex(
prefix: &str,
postfix: &str,
match_single_file: bool,
windows: bool,
) -> Regex {
let first_component = format!(
"(?:{}|(?:{}))",
braced_env_regex(windows),
path_component_regex(windows)
);
// For all components except the first we allow an equals so that `foo=/
// bar/baz` does not include foo. This is primarily intended for url queries
// (where an equals is never in the first component)
let component = format!("(?:{first_component}|=)");
let sep = if windows { r"[/\\]" } else { "/" };
let url_prefix = r"[\w+\-.]+://??";
let path_prefix = if windows {
// single slash handles most windows prefixes (like\\server\...) but `\
// \?\C:\..` (and C:\) needs special handling, since we don't allow : in path
// components (so that colon separated paths and <path>:<line> work)
r"\\\\\?\\\w:|\w:|\\|"
} else {
""
};
let path_start = format!("(?:{first_component}+|~|{path_prefix}{url_prefix})");
let optional = if match_single_file {
format!("|{path_start}")
} else {
String::new()
};
let path_regex = format!(
"{prefix}(?:{path_start}?(?:(?:{sep}{component}+)+{sep}?|{sep}){optional}){postfix}"
);
Regex::new(&path_regex).unwrap()
}
/// If `src` ends with a path then this function returns the part of the slice.
pub fn get_path_suffix(src: RopeSlice<'_>, match_single_file: bool) -> Option<RopeSlice<'_>> {
let regex = if match_single_file {
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "$", true, cfg!(windows)));
&*REGEX
} else {
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "$", false, cfg!(windows)));
&*REGEX
};
regex
.find(Input::new(src))
.map(|mat| src.byte_slice(mat.range()))
}
/// Returns an iterator of the **byte** ranges in src that contain a path.
pub fn find_paths(
src: RopeSlice<'_>,
match_single_file: bool,
) -> impl Iterator<Item = Range<usize>> + '_ {
let regex = if match_single_file {
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "", true, cfg!(windows)));
&*REGEX
} else {
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "", false, cfg!(windows)));
&*REGEX
};
regex.find_iter(Input::new(src)).map(|mat| mat.range())
}
/// Performs substitution of `~` and environment variables, see [`env::expand`](crate::env::expand) and [`expand_tilde`]
pub fn expand<T: AsRef<Path> + ?Sized>(path: &T) -> Cow<'_, Path> {
let path = path.as_ref();
let path = expand_tilde(path);
match crate::env::expand(&*path) {
Cow::Borrowed(_) => path,
Cow::Owned(path) => PathBuf::from(path).into(),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::{
@ -208,7 +302,10 @@ mod tests {
path::{Component, Path}, path::{Component, Path},
}; };
use crate::path; use regex_cursor::Input;
use ropey::RopeSlice;
use crate::path::{self, compile_path_regex};
#[test] #[test]
fn expand_tilde() { fn expand_tilde() {
@ -228,4 +325,127 @@ fn expand_tilde() {
assert_ne!(component_count, 0); assert_ne!(component_count, 0);
} }
} }
macro_rules! assert_match {
($regex: expr, $haystack: expr) => {
let haystack = Input::new(RopeSlice::from($haystack));
assert!(
$regex.is_match(haystack),
"regex should match {}",
$haystack
);
};
}
macro_rules! assert_no_match {
($regex: expr, $haystack: expr) => {
let haystack = Input::new(RopeSlice::from($haystack));
assert!(
!$regex.is_match(haystack),
"regex should not match {}",
$haystack
);
};
}
macro_rules! assert_matches {
($regex: expr, $haystack: expr, [$($matches: expr),*]) => {
let src = $haystack;
let matches: Vec<_> = $regex
.find_iter(Input::new(RopeSlice::from(src)))
.map(|it| &src[it.range()])
.collect();
assert_eq!(matches, vec![$($matches),*]);
};
}
/// Linux-only path
#[test]
fn path_regex_unix() {
// due to ambiguity with the `\` path separator we can't support space escapes `\ ` on windows
let regex = compile_path_regex("^", "$", false, false);
assert_match!(regex, "${FOO}/hello\\ world");
assert_match!(regex, "${FOO}/\\ ");
}
/// Windows-only paths
#[test]
fn path_regex_windows() {
let regex = compile_path_regex("^", "$", false, true);
assert_match!(regex, "${FOO}/hello^ world");
assert_match!(regex, "${FOO}/hello` world");
assert_match!(regex, "${FOO}/^ ");
assert_match!(regex, "${FOO}/` ");
assert_match!(regex, r"foo\bar");
assert_match!(regex, r"foo\bar");
assert_match!(regex, r"..\bar");
assert_match!(regex, r"..\");
assert_match!(regex, r"C:\");
assert_match!(regex, r"\\?\C:\foo");
assert_match!(regex, r"\\server\foo");
}
/// Paths that should work on all platforms
#[test]
fn path_regex() {
for windows in [false, true] {
let regex = compile_path_regex("^", "$", false, windows);
assert_no_match!(regex, "foo");
assert_no_match!(regex, "");
assert_match!(regex, "https://github.com/notifications/query=foo");
assert_match!(regex, "file:///foo/bar");
assert_match!(regex, "foo/bar");
assert_match!(regex, "$HOME/foo");
assert_match!(regex, "${FOO:-bar}/baz");
assert_match!(regex, "foo/bar_");
assert_match!(regex, "/home/bar");
assert_match!(regex, "foo/");
assert_match!(regex, "./");
assert_match!(regex, "../");
assert_match!(regex, "../..");
assert_match!(regex, "./foo");
assert_match!(regex, "./foo.rs");
assert_match!(regex, "/");
assert_match!(regex, "~/");
assert_match!(regex, "~/foo");
assert_match!(regex, "~/foo");
assert_match!(regex, "~/foo/../baz");
assert_match!(regex, "${HOME}/foo");
assert_match!(regex, "$HOME/foo");
assert_match!(regex, "/$FOO");
assert_match!(regex, "/${FOO}");
assert_match!(regex, "/${FOO}/${BAR}");
assert_match!(regex, "/${FOO}/${BAR}/foo");
assert_match!(regex, "/${FOO}/${BAR}");
assert_match!(regex, "${FOO}/hello_$WORLD");
assert_match!(regex, "${FOO}/hello_${WORLD}");
let regex = compile_path_regex("", "", false, windows);
assert_no_match!(regex, "");
assert_matches!(
regex,
r#"${FOO}/hello_${WORLD} ${FOO}/hello_${WORLD} foo("./bar", "/home/foo")""#,
[
"${FOO}/hello_${WORLD}",
"${FOO}/hello_${WORLD}",
"./bar",
"/home/foo"
]
);
assert_matches!(
regex,
r#"--> helix-stdx/src/path.rs:427:13"#,
["helix-stdx/src/path.rs"]
);
assert_matches!(
regex,
r#"PATH=/foo/bar:/bar/baz:${foo:-/foo}/bar:${PATH}"#,
["/foo/bar", "/bar/baz", "${foo:-/foo}/bar"]
);
let regex = compile_path_regex("^", "$", true, windows);
assert_no_match!(regex, "");
assert_match!(regex, "foo");
assert_match!(regex, "foo/");
assert_match!(regex, "$FOO");
assert_match!(regex, "${BAR}");
}
}
} }

View File

@ -6,7 +6,7 @@
use futures_util::FutureExt; use futures_util::FutureExt;
use helix_event::status; use helix_event::status;
use helix_stdx::{ use helix_stdx::{
path::expand_tilde, path::{self, find_paths},
rope::{self, RopeSliceExt}, rope::{self, RopeSliceExt},
}; };
use helix_vcs::{FileChange, Hunk}; use helix_vcs::{FileChange, Hunk};
@ -1272,53 +1272,31 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
.unwrap_or_default(); .unwrap_or_default();
let paths: Vec<_> = if selections.len() == 1 && primary.len() == 1 { let paths: Vec<_> = if selections.len() == 1 && primary.len() == 1 {
// Secial case: if there is only one one-width selection, try to detect the let mut pos = primary.cursor(text.slice(..));
// path under the cursor. pos = text.char_to_byte(pos);
let is_valid_path_char = |c: &char| { let search_start = text
#[cfg(target_os = "windows")] .line_to_byte(text.byte_to_line(pos))
let valid_chars = &[ .max(pos.saturating_sub(1000));
'@', '/', '\\', '.', '-', '_', '+', '#', '$', '%', '{', '}', '[', ']', ':', '!', let search_end = text
'~', '=', .line_to_byte(text.byte_to_line(pos) + 1)
]; .min(pos + 1000);
#[cfg(not(target_os = "windows"))] let search_range = text.slice(search_start..search_end);
let valid_chars = &['@', '/', '.', '-', '_', '+', '#', '$', '%', '~', '=', ':']; // we also allow paths that are next to the cursor (can be ambigous but
// rarely so in practice) so that gf on quoted/braced path works (not sure about this
valid_chars.contains(c) || c.is_alphabetic() || c.is_numeric() // but apparently that is how gf has worked historically in helix)
}; let path = find_paths(search_range, true)
.inspect(|mat| println!("{mat:?} {:?}", pos - search_start))
let cursor_pos = primary.cursor(text.slice(..)); .take_while(|range| search_start + range.start <= pos + 1)
let pre_cursor_pos = cursor_pos.saturating_sub(1); .find(|range| pos <= search_start + range.end)
let post_cursor_pos = cursor_pos + 1; .map(|range| Cow::from(search_range.byte_slice(range)));
let start_pos = if is_valid_path_char(&text.char(cursor_pos)) { log::debug!("goto_file auto-detected path: {path:?}");
cursor_pos let path = path.unwrap_or_else(|| primary.fragment(text.slice(..)));
} else if is_valid_path_char(&text.char(pre_cursor_pos)) { vec![path.into_owned()]
pre_cursor_pos
} else {
post_cursor_pos
};
let prefix_len = text
.chars_at(start_pos)
.reversed()
.take_while(is_valid_path_char)
.count();
let postfix_len = text
.chars_at(start_pos)
.take_while(is_valid_path_char)
.count();
let path: String = text
.slice((start_pos - prefix_len)..(start_pos + postfix_len))
.into();
log::debug!("goto_file auto-detected path: {}", path);
vec![path]
} else { } else {
// Otherwise use each selection, trimmed. // Otherwise use each selection, trimmed.
selections selections
.fragments(text.slice(..)) .fragments(text.slice(..))
.map(|sel| sel.trim().to_string()) .map(|sel| sel.trim().to_owned())
.filter(|sel| !sel.is_empty()) .filter(|sel| !sel.is_empty())
.collect() .collect()
}; };
@ -1329,7 +1307,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
continue; continue;
} }
let path = expand_tilde(Cow::from(PathBuf::from(sel))); let path = path::expand(&sel);
let path = &rel_path.join(path); let path = &rel_path.join(path);
if path.is_dir() { if path.is_dir() {
let picker = ui::file_picker(path.into(), &cx.editor.config()); let picker = ui::file_picker(path.into(), &cx.editor.config());

View File

@ -4,20 +4,20 @@
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use futures_util::stream::FuturesUnordered; use futures_util::stream::FuturesUnordered;
use futures_util::FutureExt;
use helix_core::chars::char_is_word; use helix_core::chars::char_is_word;
use helix_core::syntax::LanguageServerFeature; use helix_core::syntax::LanguageServerFeature;
use helix_event::{ use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp; use helix_lsp::lsp;
use helix_lsp::util::pos_to_lsp_pos; use helix_lsp::util::pos_to_lsp_pos;
use helix_stdx::rope::RopeSliceExt; use helix_stdx::rope::RopeSliceExt;
use helix_view::document::{Mode, SavePoint}; use helix_view::document::{Mode, SavePoint};
use helix_view::handlers::lsp::CompletionEvent; use helix_view::handlers::lsp::CompletionEvent;
use helix_view::{DocumentId, Editor, ViewId}; use helix_view::{DocumentId, Editor, ViewId};
use path::path_completion;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::time::Instant; use tokio::time::Instant;
use tokio_stream::StreamExt; use tokio_stream::StreamExt as _;
use crate::commands; use crate::commands;
use crate::compositor::Compositor; use crate::compositor::Compositor;
@ -27,10 +27,13 @@
use crate::keymap::MappableCommand; use crate::keymap::MappableCommand;
use crate::ui::editor::InsertEvent; use crate::ui::editor::InsertEvent;
use crate::ui::lsp::SignatureHelp; use crate::ui::lsp::SignatureHelp;
use crate::ui::{self, CompletionItem, Popup}; use crate::ui::{self, Popup};
use super::Handlers; use super::Handlers;
pub use item::{CompletionItem, LspCompletionItem};
pub use resolve::ResolveHandler; pub use resolve::ResolveHandler;
mod item;
mod path;
mod resolve; mod resolve;
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@ -53,12 +56,8 @@ pub(super) struct CompletionHandler {
/// currently active trigger which will cause a /// currently active trigger which will cause a
/// completion request after the timeout /// completion request after the timeout
trigger: Option<Trigger>, trigger: Option<Trigger>,
/// A handle for currently active completion request. in_flight: Option<Trigger>,
/// This can be used to determine whether the current task_controller: TaskController,
/// 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<CancelTx>,
config: Arc<ArcSwap<Config>>, config: Arc<ArcSwap<Config>>,
} }
@ -66,8 +65,9 @@ impl CompletionHandler {
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler { pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
Self { Self {
config, config,
request: None, task_controller: TaskController::new(),
trigger: None, trigger: None,
in_flight: None,
} }
} }
} }
@ -80,6 +80,9 @@ fn handle_event(
event: Self::Event, event: Self::Event,
_old_timeout: Option<Instant>, _old_timeout: Option<Instant>,
) -> Option<Instant> { ) -> Option<Instant> {
if self.in_flight.is_some() && !self.task_controller.is_running() {
self.in_flight = None;
}
match event { match event {
CompletionEvent::AutoTrigger { CompletionEvent::AutoTrigger {
cursor: trigger_pos, cursor: trigger_pos,
@ -90,7 +93,7 @@ fn handle_event(
// but people may create weird keymaps/use the mouse so lets be extra careful // but people may create weird keymaps/use the mouse so lets be extra careful
if self if self
.trigger .trigger
.as_ref() .or(self.in_flight)
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view) .map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
{ {
self.trigger = Some(Trigger { self.trigger = Some(Trigger {
@ -103,7 +106,7 @@ fn handle_event(
} }
CompletionEvent::TriggerChar { cursor, doc, view } => { CompletionEvent::TriggerChar { cursor, doc, view } => {
// immediately request completions and drop all auto completion requests // immediately request completions and drop all auto completion requests
self.request = None; self.task_controller.cancel();
self.trigger = Some(Trigger { self.trigger = Some(Trigger {
pos: cursor, pos: cursor,
view, view,
@ -113,7 +116,6 @@ fn handle_event(
} }
CompletionEvent::ManualTrigger { cursor, doc, view } => { CompletionEvent::ManualTrigger { cursor, doc, view } => {
// immediately request completions and drop all auto completion requests // immediately request completions and drop all auto completion requests
self.request = None;
self.trigger = Some(Trigger { self.trigger = Some(Trigger {
pos: cursor, pos: cursor,
view, view,
@ -126,21 +128,21 @@ fn handle_event(
} }
CompletionEvent::Cancel => { CompletionEvent::Cancel => {
self.trigger = None; self.trigger = None;
self.request = None; self.task_controller.cancel();
} }
CompletionEvent::DeleteText { cursor } => { CompletionEvent::DeleteText { cursor } => {
// if we deleted the original trigger, abort the completion // if we deleted the original trigger, abort the completion
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) { if matches!(self.trigger.or(self.in_flight), Some(Trigger{ pos, .. }) if cursor < pos)
{
self.trigger = None; self.trigger = None;
self.request = None; self.task_controller.cancel();
} }
} }
} }
self.trigger.map(|trigger| { self.trigger.map(|trigger| {
// if the current request was closed forget about it // if the current request was closed forget about it
// otherwise immediately restart the completion request // 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 {
let timeout = if trigger.kind == TriggerKind::Auto && !cancel {
self.config.load().editor.completion_timeout self.config.load().editor.completion_timeout
} else { } else {
// we want almost instant completions for trigger chars // we want almost instant completions for trigger chars
@ -155,17 +157,17 @@ fn handle_event(
fn finish_debounce(&mut self) { fn finish_debounce(&mut self) {
let trigger = self.trigger.take().expect("debounce always has a trigger"); let trigger = self.trigger.take().expect("debounce always has a trigger");
let (tx, rx) = cancelation(); self.in_flight = Some(trigger);
self.request = Some(tx); let handle = self.task_controller.restart();
dispatch_blocking(move |editor, compositor| { dispatch_blocking(move |editor, compositor| {
request_completion(trigger, rx, editor, compositor) request_completion(trigger, handle, editor, compositor)
}); });
} }
} }
fn request_completion( fn request_completion(
mut trigger: Trigger, mut trigger: Trigger,
cancel: CancelRx, handle: TaskHandle,
editor: &mut Editor, editor: &mut Editor,
compositor: &mut Compositor, compositor: &mut Compositor,
) { ) {
@ -251,15 +253,19 @@ fn request_completion(
None => Vec::new(), None => Vec::new(),
} }
.into_iter() .into_iter()
.map(|item| CompletionItem { .map(|item| {
CompletionItem::Lsp(LspCompletionItem {
item, item,
provider: language_server_id, provider: language_server_id,
resolved: false, resolved: false,
}) })
})
.collect(); .collect();
anyhow::Ok(items) anyhow::Ok(items)
} }
.boxed()
}) })
.chain(path_completion(cursor, text.clone(), doc, handle.clone()))
.collect(); .collect();
let future = async move { let future = async move {
@ -280,12 +286,13 @@ fn request_completion(
let ui = compositor.find::<ui::EditorView>().unwrap(); let ui = compositor.find::<ui::EditorView>().unwrap();
ui.last_insert.1.push(InsertEvent::RequestCompletion); ui.last_insert.1.push(InsertEvent::RequestCompletion);
tokio::spawn(async move { tokio::spawn(async move {
let items = cancelable_future(future, cancel).await.unwrap_or_default(); let items = cancelable_future(future, &handle).await;
if items.is_empty() { let Some(items) = items.filter(|items| !items.is_empty()) else {
return; return;
} };
dispatch(move |editor, compositor| { dispatch(move |editor, compositor| {
show_completion(editor, compositor, items, trigger, savepoint) show_completion(editor, compositor, items, trigger, savepoint);
drop(handle)
}) })
.await .await
}); });
@ -346,7 +353,17 @@ pub fn trigger_auto_completion(
.. ..
}) if triggers.iter().any(|trigger| text.ends_with(trigger))) }) if triggers.iter().any(|trigger| text.ends_with(trigger)))
}); });
if is_trigger_char {
let cursor_char = text
.get_bytes_at(text.len_bytes())
.and_then(|t| t.reversed().next());
#[cfg(windows)]
let is_path_completion_trigger = matches!(cursor_char, Some(b'/' | b'\\'));
#[cfg(not(windows))]
let is_path_completion_trigger = matches!(cursor_char, Some(b'/'));
if is_trigger_char || (is_path_completion_trigger && doc.path_completion_enabled()) {
send_blocking( send_blocking(
tx, tx,
CompletionEvent::TriggerChar { CompletionEvent::TriggerChar {

View File

@ -0,0 +1,41 @@
use helix_lsp::{lsp, LanguageServerId};
#[derive(Debug, PartialEq, Clone)]
pub struct LspCompletionItem {
pub item: lsp::CompletionItem,
pub provider: LanguageServerId,
pub resolved: bool,
}
#[derive(Debug, PartialEq, Clone)]
pub enum CompletionItem {
Lsp(LspCompletionItem),
Other(helix_core::CompletionItem),
}
impl PartialEq<CompletionItem> for LspCompletionItem {
fn eq(&self, other: &CompletionItem) -> bool {
match other {
CompletionItem::Lsp(other) => self == other,
_ => false,
}
}
}
impl PartialEq<CompletionItem> for helix_core::CompletionItem {
fn eq(&self, other: &CompletionItem) -> bool {
match other {
CompletionItem::Other(other) => self == other,
_ => false,
}
}
}
impl CompletionItem {
pub fn preselect(&self) -> bool {
match self {
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.preselect.unwrap_or(false),
CompletionItem::Other(_) => false,
}
}
}

View File

@ -0,0 +1,189 @@
use std::{
borrow::Cow,
fs,
path::{Path, PathBuf},
str::FromStr as _,
};
use futures_util::{future::BoxFuture, FutureExt as _};
use helix_core as core;
use helix_core::Transaction;
use helix_event::TaskHandle;
use helix_stdx::path::{self, canonicalize, fold_home_dir, get_path_suffix};
use helix_view::Document;
use url::Url;
use super::item::CompletionItem;
pub(crate) fn path_completion(
cursor: usize,
text: core::Rope,
doc: &Document,
handle: TaskHandle,
) -> Option<BoxFuture<'static, anyhow::Result<Vec<CompletionItem>>>> {
if !doc.path_completion_enabled() {
return None;
}
let cur_line = text.char_to_line(cursor);
let start = text.line_to_char(cur_line).max(cursor.saturating_sub(1000));
let line_until_cursor = text.slice(start..cursor);
let (dir_path, typed_file_name) =
get_path_suffix(line_until_cursor, false).and_then(|matched_path| {
let matched_path = Cow::from(matched_path);
let path: Cow<_> = if matched_path.starts_with("file://") {
Url::from_str(&matched_path)
.ok()
.and_then(|url| url.to_file_path().ok())?
.into()
} else {
Path::new(&*matched_path).into()
};
let path = path::expand(&path);
let parent_dir = doc.path().and_then(|dp| dp.parent());
let path = match parent_dir {
Some(parent_dir) if path.is_relative() => parent_dir.join(&path),
_ => path.into_owned(),
};
#[cfg(windows)]
let ends_with_slash = matches!(matched_path.as_bytes().last(), Some(b'/' | b'\\'));
#[cfg(not(windows))]
let ends_with_slash = matches!(matched_path.as_bytes().last(), Some(b'/'));
if ends_with_slash {
Some((PathBuf::from(path.as_path()), None))
} else {
path.parent().map(|parent_path| {
(
PathBuf::from(parent_path),
path.file_name().and_then(|f| f.to_str().map(String::from)),
)
})
}
})?;
if handle.is_canceled() {
return None;
}
let future = tokio::task::spawn_blocking(move || {
let Ok(read_dir) = std::fs::read_dir(&dir_path) else {
return Vec::new();
};
read_dir
.filter_map(Result::ok)
.filter_map(|dir_entry| {
dir_entry
.metadata()
.ok()
.and_then(|md| Some((dir_entry.file_name().into_string().ok()?, md)))
})
.map_while(|(file_name, md)| {
if handle.is_canceled() {
return None;
}
let kind = path_kind(&md);
let documentation = path_documentation(&md, &dir_path.join(&file_name), kind);
let edit_diff = typed_file_name
.as_ref()
.map(|f| f.len())
.unwrap_or_default();
let transaction = Transaction::change(
&text,
std::iter::once((cursor - edit_diff, cursor, Some((&file_name).into()))),
);
Some(CompletionItem::Other(core::CompletionItem {
kind: Cow::Borrowed(kind),
label: file_name.into(),
transaction,
documentation,
}))
})
.collect::<Vec<_>>()
});
Some(async move { Ok(future.await?) }.boxed())
}
#[cfg(unix)]
fn path_documentation(md: &fs::Metadata, full_path: &Path, kind: &str) -> String {
let full_path = fold_home_dir(canonicalize(full_path));
let full_path_name = full_path.to_string_lossy();
use std::os::unix::prelude::PermissionsExt;
let mode = md.permissions().mode();
let perms = [
(libc::S_IRUSR, 'r'),
(libc::S_IWUSR, 'w'),
(libc::S_IXUSR, 'x'),
(libc::S_IRGRP, 'r'),
(libc::S_IWGRP, 'w'),
(libc::S_IXGRP, 'x'),
(libc::S_IROTH, 'r'),
(libc::S_IWOTH, 'w'),
(libc::S_IXOTH, 'x'),
]
.into_iter()
.fold(String::with_capacity(9), |mut acc, (p, s)| {
// This cast is necessary on some platforms such as macos as `mode_t` is u16 there
#[allow(clippy::unnecessary_cast)]
acc.push(if mode & (p as u32) > 0 { s } else { '-' });
acc
});
// TODO it would be great to be able to individually color the documentation,
// but this will likely require a custom doc implementation (i.e. not `lsp::Documentation`)
// and/or different rendering in completion.rs
format!(
"type: `{kind}`\n\
permissions: `[{perms}]`\n\
full path: `{full_path_name}`",
)
}
#[cfg(not(unix))]
fn path_documentation(md: &fs::Metadata, full_path: &Path, kind: &str) -> String {
let full_path = fold_home_dir(canonicalize(full_path));
let full_path_name = full_path.to_string_lossy();
format!("type: `{kind}`\nfull path: `{full_path_name}`",)
}
#[cfg(unix)]
fn path_kind(md: &fs::Metadata) -> &'static str {
if md.is_symlink() {
"link"
} else if md.is_dir() {
"folder"
} else {
use std::os::unix::fs::FileTypeExt;
if md.file_type().is_block_device() {
"block"
} else if md.file_type().is_socket() {
"socket"
} else if md.file_type().is_char_device() {
"char_device"
} else if md.file_type().is_fifo() {
"fifo"
} else {
"file"
}
}
}
#[cfg(not(unix))]
fn path_kind(md: &fs::Metadata) -> &'static str {
if md.is_symlink() {
"link"
} else if md.is_dir() {
"folder"
} else {
"file"
}
}

View File

@ -4,9 +4,10 @@
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::time::{Duration, Instant}; use tokio::time::{Duration, Instant};
use helix_event::{send_blocking, AsyncHook, CancelRx}; use helix_event::{send_blocking, AsyncHook, TaskController, TaskHandle};
use helix_view::Editor; use helix_view::Editor;
use super::LspCompletionItem;
use crate::handlers::completion::CompletionItem; use crate::handlers::completion::CompletionItem;
use crate::job; use crate::job;
@ -22,7 +23,7 @@
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter. /// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
/// > The returned completion item should have the documentation property filled in. /// > The returned completion item should have the documentation property filled in.
pub struct ResolveHandler { pub struct ResolveHandler {
last_request: Option<Arc<CompletionItem>>, last_request: Option<Arc<LspCompletionItem>>,
resolver: Sender<ResolveRequest>, resolver: Sender<ResolveRequest>,
} }
@ -30,15 +31,11 @@ impl ResolveHandler {
pub fn new() -> ResolveHandler { pub fn new() -> ResolveHandler {
ResolveHandler { ResolveHandler {
last_request: None, last_request: None,
resolver: ResolveTimeout { resolver: ResolveTimeout::default().spawn(),
next_request: None,
in_flight: None,
}
.spawn(),
} }
} }
pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) { pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut LspCompletionItem) {
if item.resolved { if item.resolved {
return; return;
} }
@ -93,14 +90,15 @@ pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut Completio
} }
struct ResolveRequest { struct ResolveRequest {
item: Arc<CompletionItem>, item: Arc<LspCompletionItem>,
ls: Arc<helix_lsp::Client>, ls: Arc<helix_lsp::Client>,
} }
#[derive(Default)] #[derive(Default)]
struct ResolveTimeout { struct ResolveTimeout {
next_request: Option<ResolveRequest>, next_request: Option<ResolveRequest>,
in_flight: Option<(helix_event::CancelTx, Arc<CompletionItem>)>, in_flight: Option<Arc<LspCompletionItem>>,
task_controller: TaskController,
} }
impl AsyncHook for ResolveTimeout { impl AsyncHook for ResolveTimeout {
@ -120,7 +118,7 @@ fn handle_event(
} else if self } else if self
.in_flight .in_flight
.as_ref() .as_ref()
.is_some_and(|(_, old_request)| old_request.item == request.item.item) .is_some_and(|old_request| old_request.item == request.item.item)
{ {
self.next_request = None; self.next_request = None;
None None
@ -134,14 +132,14 @@ fn finish_debounce(&mut self) {
let Some(request) = self.next_request.take() else { let Some(request) = self.next_request.take() else {
return; return;
}; };
let (tx, rx) = helix_event::cancelation(); let token = self.task_controller.restart();
self.in_flight = Some((tx, request.item.clone())); self.in_flight = Some(request.item.clone());
tokio::spawn(request.execute(rx)); tokio::spawn(request.execute(token));
} }
} }
impl ResolveRequest { impl ResolveRequest {
async fn execute(self, cancel: CancelRx) { async fn execute(self, cancel: TaskHandle) {
let future = self.ls.resolve_completion_item(&self.item.item); let future = self.ls.resolve_completion_item(&self.item.item);
let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else { let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else {
return; return;
@ -152,8 +150,8 @@ async fn execute(self, cancel: CancelRx) {
.unwrap() .unwrap()
.completion .completion
{ {
let resolved_item = match resolved_item { let resolved_item = CompletionItem::Lsp(match resolved_item {
Ok(item) => CompletionItem { Ok(item) => LspCompletionItem {
item, item,
resolved: true, resolved: true,
..*self.item ..*self.item
@ -166,8 +164,8 @@ async fn execute(self, cancel: CancelRx) {
item.resolved = true; item.resolved = true;
item item
} }
}; });
completion.replace_item(&self.item, resolved_item); completion.replace_item(&*self.item, resolved_item);
}; };
}) })
.await .await

View File

@ -2,9 +2,7 @@
use std::time::Duration; use std::time::Duration;
use helix_core::syntax::LanguageServerFeature; use helix_core::syntax::LanguageServerFeature;
use helix_event::{ use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp::{self, SignatureInformation}; use helix_lsp::lsp::{self, SignatureInformation};
use helix_stdx::rope::RopeSliceExt; use helix_stdx::rope::RopeSliceExt;
use helix_view::document::Mode; use helix_view::document::Mode;
@ -22,11 +20,11 @@
use crate::ui::Popup; use crate::ui::Popup;
use crate::{job, ui}; use crate::{job, ui};
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
enum State { enum State {
Open, Open,
Closed, Closed,
Pending { request: CancelTx }, Pending,
} }
/// debounce timeout in ms, value taken from VSCode /// debounce timeout in ms, value taken from VSCode
@ -37,6 +35,7 @@ enum State {
pub(super) struct SignatureHelpHandler { pub(super) struct SignatureHelpHandler {
trigger: Option<SignatureHelpInvoked>, trigger: Option<SignatureHelpInvoked>,
state: State, state: State,
task_controller: TaskController,
} }
impl SignatureHelpHandler { impl SignatureHelpHandler {
@ -44,6 +43,7 @@ pub fn new() -> SignatureHelpHandler {
SignatureHelpHandler { SignatureHelpHandler {
trigger: None, trigger: None,
state: State::Closed, state: State::Closed,
task_controller: TaskController::new(),
} }
} }
} }
@ -76,12 +76,11 @@ fn handle_event(
} }
SignatureHelpEvent::RequestComplete { open } => { SignatureHelpEvent::RequestComplete { open } => {
// don't cancel rerequest that was already triggered // don't cancel rerequest that was already triggered
if let State::Pending { request } = &self.state { if self.state == State::Pending && self.task_controller.is_running() {
if !request.is_closed() {
return timeout; return timeout;
} }
}
self.state = if open { State::Open } else { State::Closed }; self.state = if open { State::Open } else { State::Closed };
self.task_controller.cancel();
return timeout; return timeout;
} }
@ -94,16 +93,16 @@ fn handle_event(
fn finish_debounce(&mut self) { fn finish_debounce(&mut self) {
let invocation = self.trigger.take().unwrap(); let invocation = self.trigger.take().unwrap();
let (tx, rx) = cancelation(); self.state = State::Pending;
self.state = State::Pending { request: tx }; let handle = self.task_controller.restart();
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx)) job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, handle))
} }
} }
pub fn request_signature_help( pub fn request_signature_help(
editor: &mut Editor, editor: &mut Editor,
invoked: SignatureHelpInvoked, invoked: SignatureHelpInvoked,
cancel: CancelRx, cancel: TaskHandle,
) { ) {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);

View File

@ -1,6 +1,9 @@
use crate::{ use crate::{
compositor::{Component, Context, Event, EventResult}, compositor::{Component, Context, Event, EventResult},
handlers::{completion::ResolveHandler, trigger_auto_completion}, handlers::{
completion::{CompletionItem, LspCompletionItem, ResolveHandler},
trigger_auto_completion,
},
}; };
use helix_view::{ use helix_view::{
document::SavePoint, document::SavePoint,
@ -13,12 +16,12 @@
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
use helix_core::{chars, Change, Transaction}; use helix_core::{self as core, chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor}; use helix_view::{graphics::Rect, Document, Editor};
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding}; use helix_lsp::{lsp, util, OffsetEncoding};
impl menu::Item for CompletionItem { impl menu::Item for CompletionItem {
type Data = (); type Data = ();
@ -28,30 +31,35 @@ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
#[inline] #[inline]
fn filter_text(&self, _data: &Self::Data) -> Cow<str> { fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
self.item match self {
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item
.filter_text .filter_text
.as_ref() .as_ref()
.unwrap_or(&self.item.label) .unwrap_or(&item.label)
.as_str() .as_str()
.into() .into(),
CompletionItem::Other(core::CompletionItem { label, .. }) => label.clone(),
}
} }
fn format(&self, _data: &Self::Data) -> menu::Row { fn format(&self, _data: &Self::Data) -> menu::Row {
let deprecated = self.item.deprecated.unwrap_or_default() let deprecated = match self {
|| self.item.tags.as_ref().map_or(false, |tags| { CompletionItem::Lsp(LspCompletionItem { item, .. }) => {
item.deprecated.unwrap_or_default()
|| item.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED) tags.contains(&lsp::CompletionItemTag::DEPRECATED)
}); })
}
CompletionItem::Other(_) => false,
};
menu::Row::new(vec![ let label = match self {
menu::Cell::from(Span::styled( CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.label.as_str(),
self.item.label.as_str(), CompletionItem::Other(core::CompletionItem { label, .. }) => label,
if deprecated { };
Style::default().add_modifier(Modifier::CROSSED_OUT)
} else { let kind = match self {
Style::default() CompletionItem::Lsp(LspCompletionItem { item, .. }) => match item.kind {
},
)),
menu::Cell::from(match self.item.kind {
Some(lsp::CompletionItemKind::TEXT) => "text", Some(lsp::CompletionItemKind::TEXT) => "text",
Some(lsp::CompletionItemKind::METHOD) => "method", Some(lsp::CompletionItemKind::METHOD) => "method",
Some(lsp::CompletionItemKind::FUNCTION) => "function", Some(lsp::CompletionItemKind::FUNCTION) => "function",
@ -82,18 +90,24 @@ fn format(&self, _data: &Self::Data) -> menu::Row {
"" ""
} }
None => "", None => "",
}), },
CompletionItem::Other(core::CompletionItem { kind, .. }) => kind,
};
menu::Row::new([
menu::Cell::from(Span::styled(
label,
if deprecated {
Style::default().add_modifier(Modifier::CROSSED_OUT)
} else {
Style::default()
},
)),
menu::Cell::from(kind),
]) ])
} }
} }
#[derive(Debug, PartialEq, Default, Clone)]
pub struct CompletionItem {
pub item: lsp::CompletionItem,
pub provider: LanguageServerId,
pub resolved: bool,
}
/// Wraps a Menu. /// Wraps a Menu.
pub struct Completion { pub struct Completion {
popup: Popup<Menu<CompletionItem>>, popup: Popup<Menu<CompletionItem>>,
@ -115,11 +129,11 @@ pub fn new(
let preview_completion_insert = editor.config().preview_completion_insert; let preview_completion_insert = editor.config().preview_completion_insert;
let replace_mode = editor.config().completion_replace; let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server) // Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.item.preselect.unwrap_or(false)); items.sort_by_key(|item| !item.preselect());
// Then create the menu // Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction( fn lsp_item_to_transaction(
doc: &Document, doc: &Document,
view_id: ViewId, view_id: ViewId,
item: &lsp::CompletionItem, item: &lsp::CompletionItem,
@ -257,7 +271,9 @@ macro_rules! language_server {
// always present here // always present here
let item = item.unwrap(); let item = item.unwrap();
let transaction = item_to_transaction( match item {
CompletionItem::Lsp(item) => doc.apply_temporary(
&lsp_item_to_transaction(
doc, doc,
view.id, view.id,
&item.item, &item.item,
@ -265,8 +281,13 @@ macro_rules! language_server {
trigger_offset, trigger_offset,
true, true,
replace_mode, replace_mode,
); ),
doc.apply_temporary(&transaction, view.id); view.id,
),
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
doc.apply_temporary(transaction, view.id)
}
};
} }
PromptEvent::Update => {} PromptEvent::Update => {}
PromptEvent::Validate => { PromptEvent::Validate => {
@ -275,32 +296,46 @@ macro_rules! language_server {
{ {
doc.restore(view, &savepoint, false); doc.restore(view, &savepoint, false);
} }
// always present here
let mut item = item.unwrap().clone();
let language_server = language_server!(item);
let offset_encoding = language_server.offset_encoding();
if !item.resolved {
if let Some(resolved) =
Self::resolve_completion_item(language_server, item.item.clone())
{
item.item = resolved;
}
};
// if more text was entered, remove it // if more text was entered, remove it
doc.restore(view, &savepoint, true); doc.restore(view, &savepoint, true);
// save an undo checkpoint before the completion // save an undo checkpoint before the completion
doc.append_changes_to_history(view); doc.append_changes_to_history(view);
let transaction = item_to_transaction(
// item always present here
let (transaction, additional_edits) = match item.unwrap().clone() {
CompletionItem::Lsp(mut item) => {
let language_server = language_server!(item);
// resolve item if not yet resolved
if !item.resolved {
if let Some(resolved_item) = Self::resolve_completion_item(
language_server,
item.item.clone(),
) {
item.item = resolved_item;
}
};
let encoding = language_server.offset_encoding();
let transaction = lsp_item_to_transaction(
doc, doc,
view.id, view.id,
&item.item, &item.item,
offset_encoding, encoding,
trigger_offset, trigger_offset,
false, false,
replace_mode, replace_mode,
); );
let add_edits = item.item.additional_text_edits;
(transaction, add_edits.map(|edits| (edits, encoding)))
}
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
(transaction, None)
}
};
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction::Applied { editor.last_completion = Some(CompleteAction::Applied {
@ -309,7 +344,7 @@ macro_rules! language_server {
}); });
// TODO: add additional _edits to completion_changes? // TODO: add additional _edits to completion_changes?
if let Some(additional_edits) = item.item.additional_text_edits { if let Some((additional_edits, offset_encoding)) = additional_edits {
if !additional_edits.is_empty() { if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits( let transaction = util::generate_transaction_from_edits(
doc.text(), doc.text(),
@ -414,7 +449,11 @@ pub fn is_empty(&self) -> bool {
self.popup.contents().is_empty() self.popup.contents().is_empty()
} }
pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) { pub fn replace_item(
&mut self,
old_item: &impl PartialEq<CompletionItem>,
new_item: CompletionItem,
) {
self.popup.contents_mut().replace_option(old_item, new_item); self.popup.contents_mut().replace_option(old_item, new_item);
} }
@ -440,7 +479,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
Some(option) => option, Some(option) => option,
None => return, None => return,
}; };
if !option.resolved { if let CompletionItem::Lsp(option) = option {
self.resolve_handler.ensure_item_resolved(cx.editor, option); self.resolve_handler.ensure_item_resolved(cx.editor, option);
} }
// need to render: // need to render:
@ -465,7 +504,8 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
Markdown::new(md, cx.editor.syn_loader.clone()) Markdown::new(md, cx.editor.syn_loader.clone())
}; };
let mut markdown_doc = match &option.item.documentation { let mut markdown_doc = match option {
CompletionItem::Lsp(option) => match &option.item.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText, kind: lsp::MarkupKind::PlainText,
@ -486,6 +526,10 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
markdowned(language, option.item.detail.as_deref(), None) markdowned(language, option.item.detail.as_deref(), None)
} }
None => return, None => return,
},
CompletionItem::Other(option) => {
markdowned(language, None, Some(&option.documentation))
}
}; };
let popup_area = self.popup.area(area, cx.editor); let popup_area = self.popup.area(area, cx.editor);

View File

@ -2,13 +2,14 @@
commands::{self, OnKeyCallback}, commands::{self, OnKeyCallback},
compositor::{Component, Context, Event, EventResult}, compositor::{Component, Context, Event, EventResult},
events::{OnModeSwitch, PostCommand}, events::{OnModeSwitch, PostCommand},
handlers::completion::CompletionItem,
key, key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{ ui::{
document::{render_document, LinePos, TextRenderer}, document::{render_document, LinePos, TextRenderer},
statusline, statusline,
text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics}, text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics},
Completion, CompletionItem, ProgressSpinners, Completion, ProgressSpinners,
}, },
}; };

View File

@ -228,7 +228,7 @@ pub fn len(&self) -> usize {
} }
impl<T: Item + PartialEq> Menu<T> { impl<T: Item + PartialEq> Menu<T> {
pub fn replace_option(&mut self, old_option: &T, new_option: T) { pub fn replace_option(&mut self, old_option: &impl PartialEq<T>, new_option: T) {
for option in &mut self.options { for option in &mut self.options {
if old_option == option { if old_option == option {
*option = new_option; *option = new_option;

View File

@ -17,7 +17,7 @@
use crate::compositor::Compositor; use crate::compositor::Compositor;
use crate::filter_picker_entry; use crate::filter_picker_entry;
use crate::job::{self, Callback}; use crate::job::{self, Callback};
pub use completion::{Completion, CompletionItem}; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
use helix_stdx::rope; use helix_stdx::rope;
pub use markdown::Markdown; pub use markdown::Markdown;

View File

@ -1713,6 +1713,12 @@ pub fn version(&self) -> i32 {
self.version self.version
} }
pub fn path_completion_enabled(&self) -> bool {
self.language_config()
.and_then(|lang_config| lang_config.path_completion)
.unwrap_or_else(|| self.config.load().path_completion)
}
/// maintains the order as configured in the language_servers TOML array /// maintains the order as configured in the language_servers TOML array
pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> { pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
self.language_config().into_iter().flat_map(move |config| { self.language_config().into_iter().flat_map(move |config| {

View File

@ -268,6 +268,11 @@ pub struct Config {
pub auto_pairs: AutoPairConfig, pub auto_pairs: AutoPairConfig,
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true. /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
pub auto_completion: bool, pub auto_completion: bool,
/// Enable filepath completion.
/// Show files and directories if an existing path at the cursor was recognized,
/// either absolute or relative to the current opened document or current working directory (if the buffer is not yet saved).
/// Defaults to true.
pub path_completion: bool,
/// Automatic formatting on save. Defaults to true. /// Automatic formatting on save. Defaults to true.
pub auto_format: bool, pub auto_format: bool,
/// Default register used for yank/paste. Defaults to '"' /// Default register used for yank/paste. Defaults to '"'
@ -952,6 +957,7 @@ fn default() -> Self {
middle_click_paste: true, middle_click_paste: true,
auto_pairs: AutoPairConfig::default(), auto_pairs: AutoPairConfig::default(),
auto_completion: true, auto_completion: true,
path_completion: true,
auto_format: true, auto_format: true,
default_yank_register: '"', default_yank_register: '"',
auto_save: AutoSave::default(), auto_save: AutoSave::default(),

View File

@ -1172,7 +1172,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "julia" name = "julia"
source = { git = "https://github.com/tree-sitter/tree-sitter-julia", rev = "8fb38abff74652c4faddbf04d2d5bbbc6b4bae25" } source = { git = "https://github.com/tree-sitter/tree-sitter-julia", rev = "e84f10db8eeb8b9807786bfc658808edaa1b4fa2" }
[[language]] [[language]]
name = "java" name = "java"
@ -1688,7 +1688,7 @@ language-servers = [ "docker-langserver" ]
[[grammar]] [[grammar]]
name = "dockerfile" name = "dockerfile"
source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev = "8ee3a0f7587b2bd8c45c8cb7d28bd414604aec62" } source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev = "868e44ce378deb68aac902a9db68ff82d2299dd0" }
[[language]] [[language]]
name = "docker-compose" name = "docker-compose"
@ -1948,7 +1948,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "ron" name = "ron"
source = { git = "https://github.com/zee-editor/tree-sitter-ron", rev = "7762d709a0f7c1f9e269d0125a2e8a7a69006146" } source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-ron", rev = "78938553b93075e638035f624973083451b29055" }
[[language]] [[language]]
name = "robot" name = "robot"
@ -2439,7 +2439,7 @@ formatter = { command = "cue", args = ["fmt", "-"] }
[[grammar]] [[grammar]]
name = "cue" name = "cue"
source = { git = "https://github.com/eonpatapon/tree-sitter-cue", rev = "61843e3beebf19417e4fede4e8be4df1084317ad" } source = { git = "https://github.com/eonpatapon/tree-sitter-cue", rev = "8a5f273bfa281c66354da562f2307c2d394b6c81" }
[[language]] [[language]]
name = "slint" name = "slint"
@ -3931,3 +3931,14 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "spade" name = "spade"
source = { git = "https://gitlab.com/spade-lang/tree-sitter-spade/", rev = "4d5b141017c61fe7e168e0a5c5721ee62b0d9572" } source = { git = "https://gitlab.com/spade-lang/tree-sitter-spade/", rev = "4d5b141017c61fe7e168e0a5c5721ee62b0d9572" }
[[language]]
name = "amber"
scope = "source.ab"
file-types = ["ab"]
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "amber"
source = { git = "https://github.com/amber-lang/tree-sitter-amber", rev = "c6df3ec2ec243ed76550c525e7ac3d9a10c6c814" }

View File

@ -0,0 +1,60 @@
(comment) @comment
[
"if"
"loop"
"for"
"return"
"fun"
"else"
"then"
"break"
"continue"
"and"
"or"
"not"
"let"
"pub"
"main"
"echo"
"exit"
"fun"
"import"
"from"
"as"
"in"
"fail"
"failed"
"silent"
"nameof"
"is"
"unsafe"
"trust"
] @keyword
; Literals
(boolean) @constant.builtin.boolean
(number) @constant.numeric
(null) @constant.numeric
(string) @string
(status) @keyword
(command) @string
(handler) @keyword
(block) @punctuation.delimiter
(variable_init) @keyword
(variable_assignment) @punctuation.delimiter
(variable) @variable
(escape_sequence) @constant.character.escape
(type_name_symbol) @type
(interpolation) @punctuation.delimiter
(reference) @keyword
(preprocessor_directive) @comment
(shebang) @comment
(function_definition
name: (variable) @function.method)
(function_call
name: (variable) @function.method)
(import_statement
"pub" @keyword
"import" @keyword
"from" @keyword)

View File

@ -109,14 +109,15 @@
(number) @constant.numeric (number) @constant.numeric
(string) @string (string) @string
(func_call name: (identifier) @function)
(func_def name: (identifier) @function)
(field_ref (_) @variable)
[ [
(identifier) (identifier)
(field_ref) (field_ref)
] @variable ] @variable
(func_call name: (identifier) @function)
(func_def name: (identifier) @function)
(field_ref (_) @variable)
(ns_qualified_name "::" @operator) (ns_qualified_name "::" @operator)
(ns_qualified_name (namespace) @namespace) (ns_qualified_name (namespace) @namespace)

View File

@ -12,7 +12,10 @@
(command_name) @function (command_name) @function
(variable_name) @variable.other.member (variable_name) @variable
((variable_name) @constant
(#match? @constant "^[A-Z][A-Z_0-9]*$"))
[ [
"if" "if"
@ -48,6 +51,9 @@
(comment) @comment (comment) @comment
((word) @constant.builtin.boolean
(#any-of? @constant.builtin.boolean "true" "false"))
(function_definition name: (word) @function) (function_definition name: (word) @function)
(file_descriptor) @constant.numeric.integer (file_descriptor) @constant.numeric.integer
@ -56,7 +62,7 @@
(command_substitution) (command_substitution)
(process_substitution) (process_substitution)
(expansion) (expansion)
]@embedded ] @embedded
[ [
"$" "$"

View File

@ -1,3 +1,9 @@
(identifier) @variable
((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
"sizeof" @keyword "sizeof" @keyword
[ [
@ -109,6 +115,12 @@
(char_literal) @constant.character (char_literal) @constant.character
(escape_sequence) @constant.character.escape (escape_sequence) @constant.character.escape
(field_identifier) @variable.other.member
(statement_identifier) @label
(type_identifier) @type
(primitive_type) @type.builtin
(sized_type_specifier) @type.builtin
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)
(call_expression (call_expression
@ -128,15 +140,4 @@
(attribute (attribute
name: (identifier) @attribute) name: (identifier) @attribute)
(field_identifier) @variable.other.member
(statement_identifier) @label
(type_identifier) @type
(primitive_type) @type.builtin
(sized_type_specifier) @type.builtin
((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
(identifier) @variable
(comment) @comment (comment) @comment

View File

@ -1,7 +1,9 @@
; Preproc ; Preproc
(unique_id) @keyword.directive [
(top_level_annotation_body) @keyword.directive (unique_id)
(top_level_annotation_body)
] @keyword.directive
; Includes ; Includes
@ -9,6 +11,7 @@
"import" "import"
"$import" "$import"
"embed" "embed"
"using"
] @keyword.control.import ] @keyword.control.import
(import_path) @string (import_path) @string
@ -84,10 +87,10 @@
"union" "union"
] @keyword.storage.type ] @keyword.storage.type
"extends" @keyword
[ [
"extends"
"namespace" "namespace"
"using"
(annotation_target) (annotation_target)
] @special ] @special

View File

@ -22,16 +22,10 @@
"in" "in"
] @keyword ] @keyword
; Function calls
(call_expression
function: (identifier) @function)
(member_call_expression
function: (identifier) @function)
; Identifiers ; Identifiers
(identifier) @variable.other.member
(select_expression (select_expression
operand: (identifier) @type) operand: (identifier) @type)
@ -39,7 +33,13 @@
operand: (select_expression operand: (select_expression
member: (identifier) @type)) member: (identifier) @type))
(identifier) @variable.other.member ; Function calls
(call_expression
function: (identifier) @function)
(member_call_expression
function: (identifier) @function)
; Literals ; Literals

View File

@ -10,6 +10,14 @@
(comment) @comment (comment) @comment
;; other symbols
(sym_lit) @variable
;; other calls
(list_lit
.
(sym_lit) @function)
;; metadata experiment ;; metadata experiment
(meta_lit (meta_lit
marker: "^" @punctuation) marker: "^" @punctuation)
@ -61,20 +69,12 @@
((sym_lit) @operator ((sym_lit) @operator
(#match? @operator "^%")) (#match? @operator "^%"))
;; other calls
(list_lit
.
(sym_lit) @function)
;; interop-ish ;; interop-ish
(list_lit (list_lit
. .
(sym_lit) @function.method (sym_lit) @function.method
(#match? @function.method "^\\.")) (#match? @function.method "^\\."))
;; other symbols
(sym_lit) @variable
;; quote ;; quote
(quoting_lit) @constant.character.escape (quoting_lit) @constant.character.escape

View File

@ -1,3 +1,5 @@
; inherits: c
; Functions ; Functions
; These casts are parsed as function calls, but are not. ; These casts are parsed as function calls, but are not.
@ -132,5 +134,3 @@
; Strings ; Strings
(raw_string_literal) @string (raw_string_literal) @string
; inherits: c

View File

@ -1,12 +1,46 @@
(package_clause "package" @keyword.control.import) ; Includes
[
"package"
"import"
] @keyword.control.import
(package_identifier) @variable ; Namespaces
(package_identifier) @namespace
(import_declaration "import" @keyword.control.import) (import_spec
[
"."
"_"
] @punctuation.special)
[ [
"!" (attr_path)
(package_path)
] @string.special.url ; In attributes
; Attributes
(attribute) @attribute
; Conditionals
"if" @keyword.control.conditional
; Repeats
"for" @keyword.control.repeat
(for_clause
"_" @punctuation.special)
; Keywords
"let" @keyword
"in" @keyword.operator
; Operators
[
"+"
"-"
"*" "*"
"/"
"|" "|"
"&" "&"
"||" "||"
@ -19,92 +53,103 @@
">=" ">="
"=~" "=~"
"!~" "!~"
"+" "!"
"-" "="
"*"
"/"
] @operator ] @operator
(unary_expression "*" @operator.default) ; Fields & Properties
(field
(label
(identifier) @variable.other.member))
(unary_expression "=~" @operator.regexp) (selector_expression
(_)
(identifier) @variable.other.member)
(unary_expression "!~" @operator.regexp) ; Functions
(call_expression
function: (identifier) @function)
(binary_expression _ "&" @operator.unify _) (call_expression
function: (selector_expression
(_)
(identifier) @function))
(binary_expression _ "|" @operator.disjunct _) (call_expression
function: (builtin_function) @function)
(builtin) @function.builtin (builtin_function) @function.builtin
(qualified_identifier) @function.builtin ; Variables
(identifier) @variable
(let_clause "let" @keyword.storage.type) ; Types
(primitive_type) @type.builtin
(for_clause "for" @keyword.control.repeat) ((identifier) @type
(for_clause "in" @keyword.control.repeat) (#match? @type "^_?#"))
(guard_clause "if" @keyword.control.conditional)
(comment) @comment
[ [
(string_type) (slice_type)
(simple_string_lit) (pointer_type)
(multiline_string_lit) ] @type ; In attributes
(bytes_type)
(simple_bytes_lit)
(multiline_bytes_lit)
] @string
[
(number_type)
(int_lit)
(int_type)
(uint_type)
] @constant.numeric.integer
[
(float_lit)
(float_type)
] @constant.numeric.float
[
(bool_type)
(true)
(false)
] @constant.builtin.boolean
(null) @constant.builtin
(ellipsis) @punctuation.bracket
; Punctuation
[ [
"," ","
":" ":"
] @punctuation.delimiter ] @punctuation.delimiter
[ [
"("
")"
"["
"]"
"{" "{"
"}" "}"
"["
"]"
"("
")"
"<"
">"
] @punctuation.bracket ] @punctuation.bracket
(interpolation "\\(" @punctuation.bracket (_) ")" @punctuation.bracket) @variable.other.member [
(ellipsis)
"?"
] @punctuation.special
(field (label (identifier) @variable.other.member)) ; Literals
(string) @string
( [
(identifier) @keyword.storage.type (escape_char)
(#match? @keyword.storage.type "^#") (escape_unicode)
) ] @constant.character.escape
(field (label alias: (identifier) @label)) (number) @constant.numeric
(let_clause left: (identifier) @label) (float) @constant.numeric.float
(si_unit
(float)
(_) @string.special.symbol)
(attribute (identifier) @tag) (boolean) @constant.builtin.boolean
[
(null)
(top)
(bottom)
] @constant.builtin
; Interpolations
(interpolation
"\\(" @punctuation.special
(_)
")" @punctuation.special)
(interpolation
"\\("
(identifier) @variable
")")
; Comments
(comment) @comment

View File

@ -51,14 +51,14 @@
(integer_literal) @constant.numeric.integer (integer_literal) @constant.numeric.integer
(identifier) @variable
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)
(labeled_item (labeled_item
label: (identifier) @label) label: (identifier) @label)
(identifier) @variable
(unit_address) @tag (unit_address) @tag
(reference) @constant (reference) @constant

View File

@ -37,6 +37,15 @@
(double_quoted_string) @string (double_quoted_string) @string
[
(heredoc_marker)
(heredoc_end)
] @label
((heredoc_block
(heredoc_line) @string)
(#set! "priority" 90))
(expansion (expansion
[ [
"$" "$"
@ -52,3 +61,6 @@
(param) (param)
(mount_param) (mount_param)
] @constant ] @constant
(expose_instruction
(expose_port) @constant.numeric.integer)

View File

@ -1,3 +1,5 @@
(identifier) @variable
(keyword) @keyword (keyword) @keyword
(string_literal) @string (string_literal) @string
(number_literal) @constant.numeric (number_literal) @constant.numeric
@ -33,11 +35,9 @@
(identifier) @constant) (identifier) @constant)
) )
[ (comment) @comment
(comment)
(preproc) (preproc) @keyword.directive
] @comment
(ERROR) @error (ERROR) @error
(identifier) @variable

View File

@ -1,3 +1,45 @@
; Comments
(tripledot) @comment.discard
[(comment) (line_comment) (shebang)] @comment
; Basic types
(variable) @variable
((atom) @constant.builtin.boolean
(#match? @constant.builtin.boolean "^(true|false)$"))
(atom) @string.special.symbol
[(string) (sigil)] @string
(character) @constant.character
(escape_sequence) @constant.character.escape
(integer) @constant.numeric.integer
(float) @constant.numeric.float
; Punctuation
["," "." "-" ";"] @punctuation.delimiter
["(" ")" "#" "{" "}" "[" "]" "<<" ">>"] @punctuation.bracket
; Operators
(binary_operator
left: (atom) @function
operator: "/"
right: (integer) @constant.numeric.integer)
((binary_operator operator: _ @keyword.operator)
(#match? @keyword.operator "^\\w+$"))
((unary_operator operator: _ @keyword.operator)
(#match? @keyword.operator "^\\w+$"))
(binary_operator operator: _ @operator)
(unary_operator operator: _ @operator)
["/" ":" "->"] @operator
; Keywords
(attribute name: (atom) @keyword)
["case" "fun" "if" "of" "when" "end" "receive" "try" "catch" "after" "begin" "maybe"] @keyword
; Attributes ; Attributes
; module declaration ; module declaration
(attribute (attribute
@ -122,46 +164,3 @@
(record field: (atom) @variable.other.member) (record field: (atom) @variable.other.member)
(record name: (atom) @type) (record name: (atom) @type)
; Keywords
(attribute name: (atom) @keyword)
["case" "fun" "if" "of" "when" "end" "receive" "try" "catch" "after" "begin" "maybe"] @keyword
; Operators
(binary_operator
left: (atom) @function
operator: "/"
right: (integer) @constant.numeric.integer)
((binary_operator operator: _ @keyword.operator)
(#match? @keyword.operator "^\\w+$"))
((unary_operator operator: _ @keyword.operator)
(#match? @keyword.operator "^\\w+$"))
(binary_operator operator: _ @operator)
(unary_operator operator: _ @operator)
["/" ":" "->"] @operator
; Comments
(tripledot) @comment.discard
[(comment) (line_comment) (shebang)] @comment
; Basic types
(variable) @variable
((atom) @constant.builtin.boolean
(#match? @constant.builtin.boolean "^(true|false)$"))
(atom) @string.special.symbol
[(string) (sigil)] @string
(character) @constant.character
(escape_sequence) @constant.character.escape
(integer) @constant.numeric.integer
(float) @constant.numeric.float
; Punctuation
["," "." "-" ";"] @punctuation.delimiter
["(" ")" "#" "{" "}" "[" "]" "<<" ">>"] @punctuation.bracket
; (ERROR) @error

View File

@ -1,8 +1,34 @@
; Function calls
(call_expression ; Identifiers
function: (identifier) @function.builtin
(#match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$")) (field_identifier) @variable.other.member
(identifier) @variable
(package_identifier) @namespace
(parameter_declaration (identifier) @variable.parameter)
(variadic_parameter_declaration (identifier) @variable.parameter)
(const_spec
name: (identifier) @constant)
(type_spec
name: (type_identifier) @constructor)
(keyed_element (literal_element (identifier) @variable.other.member))
(field_declaration
name: (field_identifier) @variable.other.member)
(parameter_declaration (identifier) @variable.parameter)
(variadic_parameter_declaration (identifier) @variable.parameter)
(label_name) @label
(const_spec
name: (identifier) @constant)
; Function calls
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)
@ -11,9 +37,14 @@
function: (selector_expression function: (selector_expression
field: (field_identifier) @function.method)) field: (field_identifier) @function.method))
(call_expression
function: (identifier) @function.builtin
(#match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$"))
; Types ; Types
(type_identifier) @type
(type_parameter_list (type_parameter_list
(parameter_declaration (parameter_declaration
name: (identifier) @type.parameter)) name: (identifier) @type.parameter))
@ -21,8 +52,6 @@
((type_identifier) @type.builtin ((type_identifier) @type.builtin
(#match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$")) (#match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$"))
(type_identifier) @type
; Function definitions ; Function definitions
(function_declaration (function_declaration
@ -34,28 +63,6 @@
(method_spec (method_spec
name: (field_identifier) @function.method) name: (field_identifier) @function.method)
; Identifiers
(const_spec
name: (identifier) @constant)
(parameter_declaration (identifier) @variable.parameter)
(variadic_parameter_declaration (identifier) @variable.parameter)
(type_spec
name: (type_identifier) @constructor)
(field_identifier) @variable.other.member
(keyed_element (literal_element (identifier) @variable.other.member))
(identifier) @variable
(package_identifier) @namespace
(parameter_declaration (identifier) @variable.parameter)
(variadic_parameter_declaration (identifier) @variable.parameter)
(label_name) @label
(const_spec
name: (identifier) @constant)
; Operators ; Operators

View File

@ -1,17 +1,3 @@
;; ----------------------------------------------------------------------------
;; Literals and comments
(integer) @constant.numeric.integer
(exp_negation) @constant.numeric.integer
(exp_literal (float)) @constant.numeric.float
(char) @constant.character
(string) @string
(con_unit) @constant.builtin ; unit, as in ()
(comment) @comment
;; ---------------------------------------------------------------------------- ;; ----------------------------------------------------------------------------
;; Punctuation ;; Punctuation
@ -30,6 +16,20 @@
] @punctuation.delimiter ] @punctuation.delimiter
;; ----------------------------------------------------------------------------
;; Literals and comments
(integer) @constant.numeric.integer
(exp_negation) @constant.numeric.integer
(exp_literal (float)) @constant.numeric.float
(char) @constant.character
(string) @string
(comment) @comment
(con_unit [ "(" ")" ] @constant.builtin) ; unit, as in ()
;; ---------------------------------------------------------------------------- ;; ----------------------------------------------------------------------------
;; Keywords, operators, includes ;; Keywords, operators, includes
@ -103,6 +103,8 @@
;; ---------------------------------------------------------------------------- ;; ----------------------------------------------------------------------------
;; Functions and variables ;; Functions and variables
(variable) @variable
(signature name: (variable) @type) (signature name: (variable) @type)
(function (function
name: (variable) @function name: (variable) @function
@ -117,7 +119,6 @@
(exp_apply . (exp_name (variable) @function)) (exp_apply . (exp_name (variable) @function))
(exp_apply . (exp_name (qualified_variable (variable) @function))) (exp_apply . (exp_name (qualified_variable (variable) @function)))
(variable) @variable
(pat_wildcard) @variable (pat_wildcard) @variable
;; ---------------------------------------------------------------------------- ;; ----------------------------------------------------------------------------

View File

@ -10,12 +10,15 @@
(num_lit) @constant.numeric (num_lit) @constant.numeric
[(bool_lit) (nil_lit)] @constant.builtin (bool_lit) @constant.builtin.boolean
(nil_lit) @constant.builtin
(comment) @comment (comment) @comment
((sym_lit) @variable (sym_lit) @variable
(#match? @variable "^\\*.+\\*$"))
((sym_lit) @variable.builtin
(#match? @variable.builtin "^\\*.+\\*$"))
(short_fn_lit (short_fn_lit
. .
@ -57,8 +60,6 @@
. .
(sym_lit) @function) (sym_lit) @function)
(sym_lit) @variable
["{" "@{" "}" ["{" "@{" "}"
"[" "@[" "]" "[" "@[" "]"
"(" "@(" ")"] @punctuation.bracket "(" "@(" ")"] @punctuation.bracket

View File

@ -1,3 +1,5 @@
(identifier) @variable
; Methods ; Methods
(method_declaration (method_declaration
@ -54,8 +56,6 @@
((identifier) @constant ((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z\\d_]+$")) (#match? @constant "^_*[A-Z][A-Z\\d_]+$"))
(identifier) @variable
(this) @variable.builtin (this) @variable.builtin
; Literals ; Literals

View File

@ -3,9 +3,12 @@
(struct_definition) (struct_definition)
(macro_definition) (macro_definition)
(function_definition) (function_definition)
(compound_expression) ; begin blocks
(let_statement)
(if_statement) (if_statement)
(try_statement)
(for_statement) (for_statement)
(while_statement) (while_statement)
(let_statement)
(quote_statement)
(do_clause)
(compound_statement) ; begin block
] @fold ] @fold

View File

@ -1,38 +1,47 @@
; ---------- ; ------------
; Primitives ; Variables identifiers
; ---------- ; ------------
[ (identifier) @variable
(line_comment)
(block_comment)
] @comment
; Remaining identifiers that start with capital letters should be types (PascalCase)
( (
((identifier) @constant.builtin) (identifier) @type
(#match? @constant.builtin "^(nothing|missing|undef)$")) (#match? @type "^[A-Z]"))
[
(true)
(false)
] @constant.builtin.boolean
(integer_literal) @constant.numeric.integer
(float_literal) @constant.numeric.float
; SCREAMING_SNAKE_CASE
( (
((identifier) @constant.numeric.float) (identifier) @constant
(#match? @constant.numeric.float "^((Inf|NaN)(16|32|64)?)$")) (#match? @constant "^[A-Z][A-Z0-9_]*$"))
(character_literal) @constant.character (const_statement
(escape_sequence) @constant.character.escape (assignment
. (identifier) @constant))
(string_literal) @string ; Field expressions are either module content or struct fields.
; Module types and constants should already be captured, so this
(prefixed_string_literal ; assumes the remaining identifiers to be struct fields.
prefix: (identifier) @function.macro) @string (field_expression
(_)
(identifier) @variable.other.member)
(quote_expression (quote_expression
(identifier) @string.special.symbol) ":" @string.special.symbol
[
(identifier)
(operator)
] @string.special.symbol)
; ------
; Macros
; ------
(macro_definition
name: (identifier) @function.macro)
(macro_identifier
"@" @function.macro
(identifier) @function.macro)
; ------------------- ; -------------------
; Modules and Imports ; Modules and Imports
@ -50,49 +59,6 @@
(scoped_identifier (scoped_identifier
(identifier) @namespace) (identifier) @namespace)
; -----
; Types
; -----
(abstract_definition
name: (identifier) @type)
(primitive_definition
name: (identifier) @type)
(struct_definition
name: (identifier) @type)
(struct_definition
. (_)
(identifier) @variable.other.member)
(struct_definition
. (_)
(typed_expression
. (identifier) @variable.other.member))
(type_parameter_list
(identifier) @type)
(constrained_type_parameter
(identifier) @type)
(subtype_clause
(identifier) @type)
(typed_expression
(identifier) @type . )
(parameterized_identifier
(identifier) @type)
(type_argument_list
(identifier) @type)
(where_clause
(identifier) @type)
; ------------------- ; -------------------
; Function definition ; Function definition
; ------------------- ; -------------------
@ -119,22 +85,6 @@
; prevent constructors (PascalCase) to be highlighted as functions ; prevent constructors (PascalCase) to be highlighted as functions
(#match? @function "^[^A-Z]")) (#match? @function "^[^A-Z]"))
(parameter_list
(identifier) @variable.parameter)
(typed_parameter
(identifier) @variable.parameter
(identifier)? @type)
(optional_parameter
. (identifier) @variable.parameter)
(slurp_parameter
(identifier) @variable.parameter)
(function_expression
. (identifier) @variable.parameter)
; --------------- ; ---------------
; Functions calls ; Functions calls
; --------------- ; ---------------
@ -145,93 +95,294 @@
; prevent constructors (PascalCase) to be highlighted as functions ; prevent constructors (PascalCase) to be highlighted as functions
(#match? @function "^[^A-Z]")) (#match? @function "^[^A-Z]"))
(
(broadcast_call_expression
(identifier) @function)
(#match? @function "^[^A-Z]"))
( (
(call_expression (call_expression
(field_expression (identifier) @function .)) (field_expression (identifier) @function .))
(#match? @function "^[^A-Z]")) (#match? @function "^[^A-Z]"))
(
(broadcast_call_expression
(identifier) @function)
(#match? @function "^[^A-Z]"))
( (
(broadcast_call_expression (broadcast_call_expression
(field_expression (identifier) @function .)) (field_expression (identifier) @function .))
(#match? @function "^[^A-Z]")) (#match? @function "^[^A-Z]"))
; ------
; Macros
; ------
(macro_definition ; -------------------
name: (identifier) @function.macro) ; Functions builtins
; -------------------
((identifier) @function.builtin
(#any-of? @function.builtin
"_abstracttype" "_apply_iterate" "_apply_pure" "_call_in_world" "_call_in_world_total"
"_call_latest" "_equiv_typedef" "_expr" "_primitivetype" "_setsuper!" "_structtype" "_typebody!"
"_typevar" "applicable" "apply_type" "arrayref" "arrayset" "arraysize" "const_arrayref"
"donotdelete" "fieldtype" "get_binding_type" "getfield" "ifelse" "invoke" "isa" "isdefined"
"modifyfield!" "nfields" "replacefield!" "set_binding_type!" "setfield!" "sizeof" "svec"
"swapfield!" "throw" "tuple" "typeassert" "typeof"))
; -----------
; Parameters
; -----------
(parameter_list
(identifier) @variable.parameter)
(optional_parameter
. (identifier) @variable.parameter)
(slurp_parameter
(identifier) @variable.parameter)
(typed_parameter
parameter: (identifier)? @variable.parameter
type: (_) @type)
(function_expression
. (identifier) @variable.parameter) ; Single parameter arrow functions
; -----
; Types
; -----
; Definitions
(abstract_definition
name: (identifier) @type.definition) @keyword
(primitive_definition
name: (identifier) @type.definition) @keyword
(struct_definition
name: (identifier) @type)
(struct_definition
. (_)
(identifier) @variable.other.member)
(struct_definition
. (_)
(typed_expression
. (identifier) @variable.other.member))
(type_clause
[
(identifier) @type
(field_expression
(identifier) @type .)
])
; Annotations
(parametrized_type_expression
(_) @type
(curly_expression
(_) @type))
(type_parameter_list
(identifier) @type)
(typed_expression
(identifier) @type . )
(function_definition
return_type: (identifier) @type)
(short_function_definition
return_type: (identifier) @type)
(where_clause
(identifier) @type)
(where_clause
(curly_expression
(_) @type))
; ---------
; Builtins
; ---------
; This list was generated with:
;
; istype(x) = typeof(x) === DataType || typeof(x) === UnionAll
; get_types(m) = filter(x -> istype(Base.eval(m, x)), names(m))
; type_names = sort(union(get_types(Core), get_types(Base)))
;
((identifier) @type.builtin
(#any-of? @type.builtin
"AbstractArray" "AbstractChannel" "AbstractChar" "AbstractDict" "AbstractDisplay"
"AbstractFloat" "AbstractIrrational" "AbstractLock" "AbstractMatch" "AbstractMatrix"
"AbstractPattern" "AbstractRange" "AbstractSet" "AbstractSlices" "AbstractString"
"AbstractUnitRange" "AbstractVecOrMat" "AbstractVector" "Any" "ArgumentError" "Array"
"AssertionError" "Atomic" "BigFloat" "BigInt" "BitArray" "BitMatrix" "BitSet" "BitVector" "Bool"
"BoundsError" "By" "CanonicalIndexError" "CapturedException" "CartesianIndex" "CartesianIndices"
"Cchar" "Cdouble" "Cfloat" "Channel" "Char" "Cint" "Cintmax_t" "Clong" "Clonglong" "Cmd" "Colon"
"ColumnSlices" "Complex" "ComplexF16" "ComplexF32" "ComplexF64" "ComposedFunction"
"CompositeException" "ConcurrencyViolationError" "Condition" "Cptrdiff_t" "Cshort" "Csize_t"
"Cssize_t" "Cstring" "Cuchar" "Cuint" "Cuintmax_t" "Culong" "Culonglong" "Cushort" "Cvoid"
"Cwchar_t" "Cwstring" "DataType" "DenseArray" "DenseMatrix" "DenseVecOrMat" "DenseVector" "Dict"
"DimensionMismatch" "Dims" "DivideError" "DomainError" "EOFError" "Enum" "ErrorException"
"Exception" "ExponentialBackOff" "Expr" "Float16" "Float32" "Float64" "Function" "GlobalRef"
"HTML" "IO" "IOBuffer" "IOContext" "IOStream" "IdDict" "IndexCartesian" "IndexLinear"
"IndexStyle" "InexactError" "InitError" "Int" "Int128" "Int16" "Int32" "Int64" "Int8" "Integer"
"InterruptException" "InvalidStateException" "Irrational" "KeyError" "LazyString" "LinRange"
"LineNumberNode" "LinearIndices" "LoadError" "Lt" "MIME" "Matrix" "Method" "MethodError"
"Missing" "MissingException" "Module" "NTuple" "NamedTuple" "Nothing" "Number" "Ordering"
"OrdinalRange" "OutOfMemoryError" "OverflowError" "Pair" "ParseError" "PartialQuickSort" "Perm"
"PermutedDimsArray" "Pipe" "ProcessFailedException" "Ptr" "QuoteNode" "Rational" "RawFD"
"ReadOnlyMemoryError" "Real" "ReentrantLock" "Ref" "Regex" "RegexMatch" "Returns"
"ReverseOrdering" "RoundingMode" "RowSlices" "SegmentationFault" "Set" "Signed" "Slices" "Some"
"SpinLock" "StackFrame" "StackOverflowError" "StackTrace" "Stateful" "StepRange" "StepRangeLen"
"StridedArray" "StridedMatrix" "StridedVecOrMat" "StridedVector" "String" "StringIndexError"
"SubArray" "SubString" "SubstitutionString" "Symbol" "SystemError" "Task" "TaskFailedException"
"Text" "TextDisplay" "Timer" "Tmstruct" "Tuple" "Type" "TypeError" "TypeVar" "UInt" "UInt128"
"UInt16" "UInt32" "UInt64" "UInt8" "UndefInitializer" "UndefKeywordError" "UndefRefError"
"UndefVarError" "Union" "UnionAll" "UnitRange" "Unsigned" "Val" "VecElement" "VecOrMat" "Vector"
"VersionNumber" "WeakKeyDict" "WeakRef"))
((identifier) @variable.builtin
(#any-of? @variable.builtin "begin" "end")
(#has-ancestor? @variable.builtin index_expression))
((identifier) @variable.builtin
(#any-of? @variable.builtin "begin" "end")
(#has-ancestor? @variable.builtin range_expression))
(macro_identifier
"@" @function.macro
(identifier) @function.macro)
; -------- ; --------
; Keywords ; Keywords
; -------- ; --------
(function_definition [
["function" "end"] @keyword.function) "global"
"local"
] @keyword
(compound_statement
[
"begin"
"end"
] @keyword)
(quote_statement
[
"quote"
"end"
] @keyword)
(let_statement
[
"let"
"end"
] @keyword)
(if_statement (if_statement
["if" "end"] @keyword.control.conditional) [
(elseif_clause "if"
["elseif"] @keyword.control.conditional) "end"
(else_clause ] @keyword.control.conditional)
["else"] @keyword.control.conditional)
(ternary_expression
["?" ":"] @keyword.control.conditional)
(for_statement (elseif_clause
["for" "end"] @keyword.control.repeat) "elseif" @keyword.control.conditional)
(while_statement
["while" "end"] @keyword.control.repeat) (else_clause
(break_statement) @keyword.control.repeat "else" @keyword.control.conditional)
(continue_statement) @keyword.control.repeat
(for_binding (if_clause
"in" @keyword.control.repeat) "if" @keyword.control.conditional) ; `if` clause in comprehensions
(for_clause
"for" @keyword.control.repeat) (ternary_expression
[
"?"
":"
] @keyword.control.conditional)
(try_statement (try_statement
["try" "end" ] @keyword.control.exception) [
"try"
"end"
] @keyword.control.exception)
(finally_clause (finally_clause
"finally" @keyword.control.exception) "finally" @keyword.control.exception)
(catch_clause (catch_clause
"catch" @keyword.control.exception) "catch" @keyword.control.exception)
(for_statement
[
"for"
"end"
] @keyword.control.repeat)
(while_statement
[
"while"
"end"
] @keyword.control.repeat)
(for_clause
"for" @keyword.control.repeat)
[ [
"export" (break_statement)
(continue_statement)
] @keyword.control.repeat
(module_definition
[
"module"
"baremodule"
"end"
] @keyword.control.import)
(import_statement
[
"import" "import"
"using" "using"
] @keyword.control.import ] @keyword.control.import)
[ (import_alias
"abstract" "as" @keyword.control.import)
"baremodule"
"begin" (export_statement
"const" "export" @keyword.control.import)
(selected_import
":" @punctuation.delimiter)
(struct_definition
[
"struct"
"end"
] @keyword)
(macro_definition
[
"macro"
"end"
] @keyword)
(function_definition
[
"function"
"end"
] @keyword.function)
(do_clause
[
"do" "do"
"end" "end"
"let" ] @keyword.function)
"macro"
"module"
"mutable"
"primitive"
"quote"
"return"
"struct"
"type"
"where"
] @keyword
; TODO: fix this (return_statement
((identifier) @keyword (#match? @keyword "global|local")) "return" @keyword.control.return)
[
"const"
"mutable"
] @keyword.storage.modifier
; --------- ; ---------
; Operators ; Operators
@ -239,14 +390,34 @@
[ [
(operator) (operator)
"::" "="
"<:" "∈"
":"
"=>"
"..."
"$"
] @operator ] @operator
(adjoint_expression
"'" @operator)
(range_expression
":" @operator)
((operator) @keyword.operator
(#any-of? @keyword.operator "in" "isa"))
(for_binding
"in" @keyword.operator)
(where_clause
"where" @keyword.operator)
(where_expression
"where" @keyword.operator)
(binary_expression
(_)
(operator) @operator
(identifier) @function
(#any-of? @operator "|>" ".|>"))
; ------------ ; ------------
; Punctuations ; Punctuations
; ------------ ; ------------
@ -255,40 +426,58 @@
"." "."
"," ","
";" ";"
"::"
"->"
] @punctuation.delimiter ] @punctuation.delimiter
"..." @punctuation.special
[ [
"["
"]"
"(" "("
")" ")"
"["
"]"
"{" "{"
"}" "}"
] @punctuation.bracket ] @punctuation.bracket
; --------------------- ; ---------
; Remaining identifiers ; Literals
; --------------------- ; ---------
(const_statement (boolean_literal) @constant.builtin.boolean
(variable_declaration
. (identifier) @constant)) (integer_literal) @constant.numeric.integer
(float_literal) @constant.numeric.float
; SCREAMING_SNAKE_CASE
( (
(identifier) @constant ((identifier) @constant.numeric.float)
(#match? @constant "^[A-Z][A-Z0-9_]*$")) (#match? @constant.numeric.float "^((Inf|NaN)(16|32|64)?)$"))
; remaining identifiers that start with capital letters should be types (PascalCase)
( (
(identifier) @type ((identifier) @constant.builtin)
(#match? @type "^[A-Z]")) (#match? @constant.builtin "^(nothing|missing|undef)$"))
; Field expressions are either module content or struct fields. (character_literal) @constant.character
; Module types and constants should already be captured, so this
; assumes the remaining identifiers to be struct fields.
(field_expression
(_)
(identifier) @variable.other.member)
(identifier) @variable (escape_sequence) @constant.character.escape
(string_literal) @string
(prefixed_string_literal
prefix: (identifier) @function.macro) @string
(command_literal) @string
(prefixed_command_literal
prefix: (identifier) @function.macro) @string
; ---------
; Comments
; ---------
[
(line_comment)
(block_comment)
] @comment

View File

@ -2,15 +2,39 @@
(struct_definition) (struct_definition)
(macro_definition) (macro_definition)
(function_definition) (function_definition)
(compound_expression) (compound_statement)
(let_statement)
(if_statement) (if_statement)
(try_statement)
(for_statement) (for_statement)
(while_statement) (while_statement)
(let_statement)
(quote_statement)
(do_clause) (do_clause)
(parameter_list) (assignment)
(for_binding)
(call_expression)
(parenthesized_expression)
(tuple_expression)
(comprehension_expression)
(matrix_expression)
(vector_expression)
] @indent ] @indent
[ [
"end" "end"
")"
"]"
"}"
] @outdent ] @outdent
(argument_list
. (_) @anchor
(#set! "scope" "tail")) @align
(parameter_list
. (_) @anchor
(#set! "scope" "tail")) @align
(curly_expression
. (_) @anchor
(#set! "scope" "tail")) @align

View File

@ -9,7 +9,8 @@
(primitive_definition) (primitive_definition)
(abstract_definition) (abstract_definition)
(struct_definition) (struct_definition)
(assignment_expression) (short_function_definition)
(assignment)
(const_statement) (const_statement)
]) ])
(#set! injection.language "markdown")) (#set! injection.language "markdown"))
@ -21,10 +22,17 @@
] @injection.content ] @injection.content
(#set! injection.language "comment")) (#set! injection.language "comment"))
(
[
(command_literal)
(prefixed_command_literal)
] @injection.content
(#set! injection.language "sh"))
( (
(prefixed_string_literal (prefixed_string_literal
prefix: (identifier) @function.macro) @injection.content prefix: (identifier) @function.macro) @injection.content
(#eq? @function.macro "re") (#eq? @function.macro "r")
(#set! injection.language "regex")) (#set! injection.language "regex"))
( (

View File

@ -2,43 +2,100 @@
; Definitions ; Definitions
; ----------- ; -----------
; Imports ; Variables
(import_statement (assignment
(identifier) @local.definition) (identifier) @local.definition)
(assignment
(tuple_expression
(identifier) @local.definition))
; Constants ; Constants
(const_statement (const_statement
(variable_declaration (assignment
. (identifier) @local.definition)) . (identifier) @local.definition))
; let/const bindings
(let_binding
(identifier) @local.definition)
(let_binding
(tuple_expression
(identifier) @local.definition))
; For bindings
(for_binding
(identifier) @local.definition)
(for_binding
(tuple_expression
(identifier) @local.definition))
; Types
(struct_definition
name: (identifier) @local.definition)
(abstract_definition
name: (identifier) @local.definition)
(abstract_definition
name: (identifier) @local.definition)
(type_parameter_list
(identifier) @local.definition)
; Module imports
(import_statement
(identifier) @local.definition)
; Parameters ; Parameters
(parameter_list (parameter_list
(identifier) @local.definition) (identifier) @local.definition)
(typed_parameter (optional_parameter
. (identifier) @local.definition) .
(optional_parameter .
(identifier) @local.definition) (identifier) @local.definition)
(slurp_parameter (slurp_parameter
(identifier) @local.definition) (identifier) @local.definition)
(typed_parameter
parameter: (identifier) @local.definition
(_))
; Single parameter arrow function
(function_expression (function_expression
. (identifier) @local.definition) .
(identifier) @local.definition)
; ------ ; Function/macro definitions
; Scopes (function_definition
; ------ name: (identifier) @local.definition) @local.scope
[ (short_function_definition
(function_definition) name: (identifier) @local.definition) @local.scope
(short_function_definition)
(macro_definition) (macro_definition
] @local.scope name: (identifier) @local.definition) @local.scope
; ---------- ; ----------
; References ; References
; ---------- ; ----------
(identifier) @local.reference (identifier) @local.reference
; ------
; Scopes
; ------
[
(for_statement)
(while_statement)
(try_statement)
(catch_clause)
(finally_clause)
(let_statement)
(quote_statement)
(do_clause)
] @local.scope

View File

@ -1,11 +1,11 @@
; Identifiers ; Identifiers
[(NAME) (SYMBOLNAME)] @variable
(section (section
. .
(NAME) @namespace) (NAME) @namespace)
[(NAME) (SYMBOLNAME)] @variable
; Operators ; Operators
[ [

View File

@ -23,12 +23,16 @@
(borrow_expression "&" @keyword.storage.modifier.ref) (borrow_expression "&" @keyword.storage.modifier.ref)
(borrow_expression "&mut" @keyword.storage.modifier.mut) (borrow_expression "&mut" @keyword.storage.modifier.mut)
(identifier) @variable
(constant_identifier) @constant (constant_identifier) @constant
((identifier) @constant ((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$")) (#match? @constant "^[A-Z][A-Z\\d_]*$"))
(function_identifier) @function (function_identifier) @function
(primitive_type) @type.builtin
(struct_identifier) @type (struct_identifier) @type
(pack_expression (pack_expression
access: (module_access access: (module_access
@ -152,6 +156,3 @@
"with" "with"
] @keyword ] @keyword
(primitive_type) @type.buildin
(identifier) @variable

View File

@ -17,6 +17,18 @@
"with" "with"
] @keyword ] @keyword
(variable_expression name: (identifier) @variable)
(select_expression
attrpath: (attrpath attr: (identifier)) @variable.other.member)
(apply_expression
function: [
(variable_expression name: (identifier) @function)
(select_expression
attrpath: (attrpath
attr: (identifier) @function .))])
((identifier) @variable.builtin ((identifier) @variable.builtin
(#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins)$") (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins)$")
(#is-not? local)) (#is-not? local))
@ -59,28 +71,16 @@
name: (identifier) @variable.parameter name: (identifier) @variable.parameter
"?"? @punctuation.delimiter) "?"? @punctuation.delimiter)
(select_expression
attrpath: (attrpath attr: (identifier)) @variable.other.member)
(interpolation (interpolation
"${" @punctuation.special "${" @punctuation.special
"}" @punctuation.special) @embedded "}" @punctuation.special) @embedded
(apply_expression
function: [
(variable_expression name: (identifier) @function)
(select_expression
attrpath: (attrpath
attr: (identifier) @function .))])
(unary_expression (unary_expression
operator: _ @operator) operator: _ @operator)
(binary_expression (binary_expression
operator: _ @operator) operator: _ @operator)
(variable_expression name: (identifier) @variable)
(binding (binding
attrpath: (attrpath attr: (identifier)) @variable.other.member) attrpath: (attrpath attr: (identifier)) @variable.other.member)

View File

@ -3,13 +3,14 @@
(boolean) @constant.builtin.boolean (boolean) @constant.builtin.boolean
(include_path) @string.special.path (include_path) @string.special.path
(identifier) @variable
(parameters_declaration (identifier) @variable.parameter) (parameters_declaration (identifier) @variable.parameter)
(function_declaration name: (identifier) @function) (function_declaration name: (identifier) @function)
(function_call function: (identifier) @function) (function_call function: (identifier) @function)
(module_call name: (identifier) @function) (module_call name: (identifier) @function)
(identifier) @variable
(special_variable) @variable.builtin (special_variable) @variable.builtin
[ [

View File

@ -1,19 +1,3 @@
; ----------------------------------------------------------------------------
; Record fields would need to come before literal strings in order to be captured correctly
(record_accessor
field: [ (variable)
(string)
(triple_quote_string)
] @variable.other.member)
(exp_record_access
field: [ (variable)
(string)
(triple_quote_string)
] @variable.other.member)
; ---------------------------------------------------------------------------- ; ----------------------------------------------------------------------------
; Literals and comments ; Literals and comments
@ -21,6 +5,7 @@
(exp_negation) @constant.numeric.integer (exp_negation) @constant.numeric.integer
(exp_literal (number)) @constant.numeric.float (exp_literal (number)) @constant.numeric.float
(char) @constant.character (char) @constant.character
[ [
(string) (string)
(triple_quote_string) (triple_quote_string)
@ -28,7 +13,6 @@
(comment) @comment (comment) @comment
; ---------------------------------------------------------------------------- ; ----------------------------------------------------------------------------
; Punctuation ; Punctuation
@ -41,18 +25,19 @@
"]" "]"
] @punctuation.bracket ] @punctuation.bracket
[ (comma) @punctuation.delimiter
(comma)
";"
] @punctuation.delimiter
; ----------------------------------------------------------------------------
; Types
(type) @type
(constructor) @constructor
; ---------------------------------------------------------------------------- ; ----------------------------------------------------------------------------
; Keywords, operators, includes ; Keywords, operators, includes
; This needs to come before the other "else" in (module) @namespace
; order to be highlighted correctly
(class_instance "else" @keyword)
[ [
"if" "if"
@ -95,7 +80,6 @@
] @operator ] @operator
(qualified_module (module) @constructor) (qualified_module (module) @constructor)
(module) @namespace
(qualified_type (module) @namespace) (qualified_type (module) @namespace)
(qualified_variable (module) @namespace) (qualified_variable (module) @namespace)
(import (module) @namespace) (import (module) @namespace)
@ -122,6 +106,11 @@
"infixr" "infixr"
] @keyword ] @keyword
; NOTE
; Needs to come after the other `else` in
; order to be highlighted correctly
(class_instance "else" @keyword)
(type_role_declaration (type_role_declaration
"role" @keyword "role" @keyword
role: (type_role) @keyword) role: (type_role) @keyword)
@ -131,10 +120,27 @@
; ---------------------------------------------------------------------------- ; ----------------------------------------------------------------------------
; Functions and variables ; Functions and variables
(variable) @variable
(row_field (field_name) @variable.other.member) (row_field (field_name) @variable.other.member)
(record_field (field_name) @variable.other.member) (record_field (field_name) @variable.other.member)
(record_field (field_pun) @variable.other.member) (record_field (field_pun) @variable.other.member)
; NOTE
; Record fields must come after literal strings and
; plain variables in order to be highlighted correctly
(record_accessor
field: [ (variable)
(string)
(triple_quote_string)
] @variable.other.member)
(exp_record_access
field: [ (variable)
(string)
(triple_quote_string)
] @variable.other.member)
(signature name: (variable) @type) (signature name: (variable) @type)
(function name: (variable) @function) (function name: (variable) @function)
(class_instance (instance_name) @function) (class_instance (instance_name) @function)
@ -151,14 +157,5 @@
(exp_ticked (exp_name (variable) @operator)) (exp_ticked (exp_name (variable) @operator))
(exp_ticked (exp_name (qualified_variable (variable) @operator))) (exp_ticked (exp_name (qualified_variable (variable) @operator)))
(variable) @variable (patterns (pat_as "@" @namespace))
("@" @namespace) ; "as" pattern operator, e.g. x@Constructor
; ----------------------------------------------------------------------------
; Types
(type) @type
(constructor) @constructor

View File

@ -1,3 +1,11 @@
; Structs
;------------
(enum_variant) @type.enum.variant
(struct_entry (_) @variable.other.member ":")
(struct_name (identifier)) @type
(unit_struct) @type.builtin
; Literals ; Literals
;------------ ;------------
@ -7,16 +15,6 @@
(float) @constant.numeric.float (float) @constant.numeric.float
(char) @constant.character (char) @constant.character
; Structs
;------------
(enum_variant) @type.enum.variant
(struct_entry (_) @variable.other.member ":")
(struct_name (identifier)) @type
; Comments ; Comments
;------------ ;------------
@ -37,6 +35,7 @@
"{" @punctuation.bracket "{" @punctuation.bracket
"}" @punctuation.bracket "}" @punctuation.bracket
"-" @operator
; Special ; Special
;------------ ;------------

View File

@ -10,32 +10,28 @@
(block_comment) @comment.block (block_comment) @comment.block
(directive) @keyword.directive (directive) @keyword.directive
; operators ; variables
((symbol) @operator ((symbol) @variable.builtin
(#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) (#eq? @variable.builtin "..."))
; keywords ((symbol) @variable.builtin
(#eq? @variable.builtin "."))
(symbol) @variable
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
(quote "'") @operator
(unquote_splicing ",@") @operator
(unquote ",") @operator
(quasiquote "`") @operator
; procedure
(list (list
. .
((symbol) @keyword.conditional (symbol) @function)
(#match? @keyword.conditional "^(if|cond|case|when|unless)$"
)))
(list
.
(symbol) @keyword
(#match? @keyword
"^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$"
))
(list
.
(symbol) @function.builtin
(#match? @function.builtin
"^(caar|cadr|call-with-input-file|call-with-output-file|cdar|cddr|list|open-input-file|open-output-file|with-input-from-file|with-output-to-file|\\*|\\+|-|/|<|<=|=|>|>=|abs|acos|angle|append|apply|asin|assoc|assq|assv|atan|boolean\\?|caaaar|caaadr|caaar|caadar|caaddr|caadr|cadaar|cadadr|cadar|caddar|cadddr|caddr|call-with-current-continuation|call-with-values|car|cdaaar|cdaadr|cdaar|cdadar|cdaddr|cdadr|cddaar|cddadr|cddar|cdddar|cddddr|cdddr|cdr|ceiling|char->integer|char-alphabetic\\?|char-ci<=\\?|char-ci<\\?|char-ci=\\?|char-ci>=\\?|char-ci>\\?|char-downcase|char-lower-case\\?|char-numeric\\?|char-ready\\?|char-upcase|char-upper-case\\?|char-whitespace\\?|char<=\\?|char<\\?|char=\\?|char>=\\?|char>\\?|char\\?|close-input-port|close-output-port|complex\\?|cons|cos|current-error-port|current-input-port|current-output-port|denominator|display|dynamic-wind|eof-object\\?|eq\\?|equal\\?|eqv\\?|eval|even\\?|exact->inexact|exact\\?|exp|expt|floor|flush-output|for-each|force|gcd|imag-part|inexact->exact|inexact\\?|input-port\\?|integer->char|integer\\?|interaction-environment|lcm|length|list->string|list->vector|list-ref|list-tail|list\\?|load|log|magnitude|make-polar|make-rectangular|make-string|make-vector|map|max|member|memq|memv|min|modulo|negative\\?|newline|not|null-environment|null\\?|number->string|number\\?|numerator|odd\\?|output-port\\?|pair\\?|peek-char|positive\\?|procedure\\?|quotient|rational\\?|rationalize|read|read-char|real-part|real\\?|remainder|reverse|round|scheme-report-environment|set-car!|set-cdr!|sin|sqrt|string|string->list|string->number|string->symbol|string-append|string-ci<=\\?|string-ci<\\?|string-ci=\\?|string-ci>=\\?|string-ci>\\?|string-copy|string-fill!|string-length|string-ref|string-set!|string<=\\?|string<\\?|string=\\?|string>=\\?|string>\\?|string\\?|substring|symbol->string|symbol\\?|tan|transcript-off|transcript-on|truncate|values|vector|vector->list|vector-fill!|vector-length|vector-ref|vector-set!|vector\\?|write|write-char|zero\\?)$"
))
; special forms ; special forms
@ -62,12 +58,10 @@
(#match? @_f (#match? @_f
"^(let|let\\*|let-syntax|let-values|let\\*-values|letrec|letrec\\*|letrec-syntax)$")) "^(let|let\\*|let-syntax|let-values|let\\*-values|letrec|letrec\\*|letrec-syntax)$"))
; quote ; operators
(list ((symbol) @operator
. (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
(symbol) @_f
(#eq? @_f "quote")) @string.symbol
; library ; library
@ -79,26 +73,31 @@
(#eq? @_lib "library")) (#eq? @_lib "library"))
; procedure ; quote
(list (list
. .
(symbol) @function) (symbol) @_f
(#eq? @_f "quote")) @string.symbol
;; variables ; keywords
((symbol) @variable.builtin (list
(#eq? @variable.builtin "...")) .
((symbol) @keyword.conditional
(#match? @keyword.conditional "^(if|cond|case|when|unless)$"
)))
((symbol) @variable.builtin (list
(#eq? @variable.builtin ".")) .
(symbol) @keyword
(symbol) @variable (#match? @keyword
"^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$"
["(" ")" "[" "]" "{" "}"] @punctuation.bracket ))
(quote "'") @operator
(unquote_splicing ",@") @operator
(unquote ",") @operator
(quasiquote "`") @operator
(list
.
(symbol) @function.builtin
(#match? @function.builtin
"^(caar|cadr|call-with-input-file|call-with-output-file|cdar|cddr|list|open-input-file|open-output-file|with-input-from-file|with-output-to-file|\\*|\\+|-|/|<|<=|=|>|>=|abs|acos|angle|append|apply|asin|assoc|assq|assv|atan|boolean\\?|caaaar|caaadr|caaar|caadar|caaddr|caadr|cadaar|cadadr|cadar|caddar|cadddr|caddr|call-with-current-continuation|call-with-values|car|cdaaar|cdaadr|cdaar|cdadar|cdaddr|cdadr|cddaar|cddadr|cddar|cdddar|cddddr|cdddr|cdr|ceiling|char->integer|char-alphabetic\\?|char-ci<=\\?|char-ci<\\?|char-ci=\\?|char-ci>=\\?|char-ci>\\?|char-downcase|char-lower-case\\?|char-numeric\\?|char-ready\\?|char-upcase|char-upper-case\\?|char-whitespace\\?|char<=\\?|char<\\?|char=\\?|char>=\\?|char>\\?|char\\?|close-input-port|close-output-port|complex\\?|cons|cos|current-error-port|current-input-port|current-output-port|denominator|display|dynamic-wind|eof-object\\?|eq\\?|equal\\?|eqv\\?|eval|even\\?|exact->inexact|exact\\?|exp|expt|floor|flush-output|for-each|force|gcd|imag-part|inexact->exact|inexact\\?|input-port\\?|integer->char|integer\\?|interaction-environment|lcm|length|list->string|list->vector|list-ref|list-tail|list\\?|load|log|magnitude|make-polar|make-rectangular|make-string|make-vector|map|max|member|memq|memv|min|modulo|negative\\?|newline|not|null-environment|null\\?|number->string|number\\?|numerator|odd\\?|output-port\\?|pair\\?|peek-char|positive\\?|procedure\\?|quotient|rational\\?|rationalize|read|read-char|real-part|real\\?|remainder|reverse|round|scheme-report-environment|set-car!|set-cdr!|sin|sqrt|string|string->list|string->number|string->symbol|string-append|string-ci<=\\?|string-ci<\\?|string-ci=\\?|string-ci>=\\?|string-ci>\\?|string-copy|string-fill!|string-length|string-ref|string-set!|string<=\\?|string<\\?|string=\\?|string>=\\?|string>\\?|string\\?|substring|symbol->string|symbol\\?|tan|transcript-off|transcript-on|truncate|values|vector|vector->list|vector-fill!|vector-length|vector-ref|vector-set!|vector\\?|write|write-char|zero\\?)$"
))

View File

@ -12,6 +12,8 @@
(unicode_string_literal) (unicode_string_literal)
(yul_string_literal) (yul_string_literal)
] @string ] @string
(hex_string_literal "hex" @string.special.symbol)
(unicode_string_literal "unicode" @string.special.symbol)
[ [
(number_literal) (number_literal)
(yul_decimal_number) (yul_decimal_number)
@ -20,6 +22,7 @@
[ [
(true) (true)
(false) (false)
(yul_boolean)
] @constant.builtin.boolean ] @constant.builtin.boolean
(comment) @comment (comment) @comment

View File

@ -1,4 +1,4 @@
(syscall) @function (syscall) @function.builtin
(integer) @constant.numeric (integer) @constant.numeric
(pointer) @constant.numeric (pointer) @constant.numeric
(value) @label (value) @label

View File

@ -40,6 +40,7 @@
(float) @constant.numeric.float (float) @constant.numeric.float
(integer) @constant.numeric.integer (integer) @constant.numeric.integer
(comment) @comment (comment) @comment
[(path) (string) (json)] @string.special.path [(string) (json)] @string.special.path
(path) @string.special.path
(time) @string.special.symbol (time) @string.special.symbol
(boolean) @constant.builtin.boolean (boolean) @constant.builtin.boolean

View File

@ -1,13 +1,3 @@
(block_mapping_pair
key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member))
(block_mapping_pair
key: (flow_node (plain_scalar (string_scalar) @variable.other.member)))
(flow_mapping
(_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member)))
(flow_mapping
(_ key: (flow_node (plain_scalar (string_scalar) @variable.other.member))))
(boolean_scalar) @constant.builtin.boolean (boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin (null_scalar) @constant.builtin
(double_quote_scalar) @string (double_quote_scalar) @string
@ -24,6 +14,16 @@
(yaml_directive) @keyword (yaml_directive) @keyword
(ERROR) @error (ERROR) @error
(block_mapping_pair
key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member))
(block_mapping_pair
key: (flow_node (plain_scalar (string_scalar) @variable.other.member)))
(flow_mapping
(_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member)))
(flow_mapping
(_ key: (flow_node (plain_scalar (string_scalar) @variable.other.member))))
[ [
"," ","
"-" "-"

View File

@ -7,6 +7,18 @@
(line_comment) (line_comment)
] @comment.line ] @comment.line
[
variable: (IDENTIFIER)
variable_type_function: (IDENTIFIER)
] @variable
parameter: (IDENTIFIER) @variable.parameter
[
field_member: (IDENTIFIER)
field_access: (IDENTIFIER)
] @variable.other.member
;; assume TitleCase is a type ;; assume TitleCase is a type
( (
[ [
@ -36,6 +48,13 @@
(#match? @constant "^[A-Z][A-Z_0-9]+$") (#match? @constant "^[A-Z][A-Z_0-9]+$")
) )
[
function_call: (IDENTIFIER)
function: (IDENTIFIER)
] @function
exception: "!" @keyword.control.exception
;; _ ;; _
( (
(IDENTIFIER) @variable.builtin (IDENTIFIER) @variable.builtin
@ -45,25 +64,6 @@
;; C Pointers [*c]T ;; C Pointers [*c]T
(PtrTypeStart "c" @variable.builtin) (PtrTypeStart "c" @variable.builtin)
[
variable: (IDENTIFIER)
variable_type_function: (IDENTIFIER)
] @variable
parameter: (IDENTIFIER) @variable.parameter
[
field_member: (IDENTIFIER)
field_access: (IDENTIFIER)
] @variable.other.member
[
function_call: (IDENTIFIER)
function: (IDENTIFIER)
] @function
exception: "!" @keyword.control.exception
field_constant: (IDENTIFIER) @constant field_constant: (IDENTIFIER) @constant
(BUILTINIDENTIFIER) @function.builtin (BUILTINIDENTIFIER) @function.builtin