Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for path completion #2608

Merged
merged 19 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
| `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"]` |
| `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` |
Philipp-M marked this conversation as resolved.
Show resolved Hide resolved
| `auto-format` | Enable automatic formatting on save | `true` |
| `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` |
Expand Down
1 change: 1 addition & 0 deletions book/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ These configuration keys are available:
| `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)
| `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. |
| `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.

Expand Down
12 changes: 12 additions & 0 deletions helix-core/src/completion.rs
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 2 additions & 0 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use encoding_rs as encoding;
pub mod auto_pairs;
pub mod chars;
pub mod comment;
pub mod completion;
pub mod config;
pub mod diagnostic;
pub mod diff;
Expand Down Expand Up @@ -63,6 +64,7 @@ pub use selection::{Range, Selection};
pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax;

pub use completion::CompletionItem;
pub use diagnostic::Diagnostic;

pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
Expand Down
3 changes: 3 additions & 0 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ pub struct LanguageConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub formatter: Option<FormatterConfiguration>,

/// If set, overrides `editor.path-completion`.
pub path_completion: Option<bool>,

#[serde(default)]
pub diagnostic_severity: Severity,

Expand Down
2 changes: 2 additions & 0 deletions helix-stdx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ ropey = { version = "1.6.1", default-features = false }
which = "6.0"
regex-cursor = "0.1.4"
bitflags = "2.6"
once_cell = "1.19"
regex-automata = "0.4.8"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
Expand Down
126 changes: 124 additions & 2 deletions helix-stdx/src/env.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::{
ffi::OsStr,
borrow::Cow,
ffi::{OsStr, OsString},
path::{Path, PathBuf},
sync::RwLock,
};

use once_cell::sync::Lazy;

static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);

// Get the current working directory.
Expand Down Expand Up @@ -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)]
pub struct ExecutableNotFoundError {
command: String,
Expand All @@ -75,7 +165,9 @@ impl std::error::Error for ExecutableNotFoundError {}

#[cfg(test)]
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]
fn current_dir_is_set() {
Expand All @@ -88,4 +180,34 @@ mod tests {
let cwd = current_working_dir();
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");
}
}
Loading