Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore

pull/9/head
wongjiahau 1 year ago
commit 20241fb256

1
Cargo.lock generated

@ -1281,6 +1281,7 @@ dependencies = [
name = "helix-vcs"
version = "0.6.0"
dependencies = [
"arc-swap",
"gix",
"helix-core",
"imara-diff",

@ -50,6 +50,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
| `auto-info` | Whether to display info boxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
@ -110,6 +111,7 @@ The following statusline elements can be configured:
| `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
| `version-control` | The current branch name or detached commit hash of the opened workspace |
### `[editor.lsp]` Section
@ -118,9 +120,12 @@ The following statusline elements can be configured:
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix.
Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances, please report any bugs you see so we can fix them!
### `[editor.cursor-shape]` Section

@ -137,8 +137,8 @@ cargo install --path helix-term --locked
```
This command will create the `hx` executable and construct the tree-sitter
grammars either in the `runtime` folder, or in the folder specified in `HELIX_RUNTIME`
(as described below). To build the tree-sitter grammars requires a c++ compiler to be installed, for example `gcc-c++`.
grammars in the local `runtime` folder. To build the tree-sitter grammars requires
a c++ compiler to be installed, for example `gcc-c++`.
> 💡 If you are using the musl-libc instead of glibc the following environment variable must be set during the build
> to ensure tree-sitter grammars can be loaded correctly:
@ -149,11 +149,13 @@ grammars either in the `runtime` folder, or in the folder specified in `HELIX_RU
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
> grammars with `hx --grammar fetch` (requires `git`) and compile them with
> `hx --grammar build` (requires a C++ compiler).
> `hx --grammar build` (requires a C++ compiler). This will install them in
> the `runtime` directory within the user's helix config directory (more
> [details below](#multiple-runtime-directories)).
### Configuring Helix's runtime files
- **Linux and macOS**
#### Linux and macOS
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent:
@ -167,7 +169,7 @@ Or, create a symlink in `~/.config/helix` that links to the source code director
ln -s $PWD/runtime ~/.config/helix/runtime
```
- **Windows**
#### Windows
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
`Edit environment variables for your account`) or use the `setx` command in
@ -182,13 +184,27 @@ setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
| Method | Command |
| ---------- | -------------------------------------------------------------------------------------- |
| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
| Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
| Method | Command |
| ---------- | -------------------------------------------------------------------------------------- |
| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
| Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
> 💡 On Windows, creating a symbolic link may require running PowerShell or
> Cmd as an administrator.
> 💡 On Windows, creating a symbolic link may require running PowerShell or
> Cmd as an administrator.
#### Multiple runtime directories
When Helix finds multiple runtime directories it will search through them for files in the
following order:
1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
developing and testing helix only).
2. `runtime/` subdirectory of OS-dependent helix user config directory.
3. `$HELIX_RUNTIME`.
4. `runtime/` subdirectory of path to Helix executable.
This order also sets the priority for selecting which file will be used if multiple runtime
directories have files with the same name.
### Validating the installation

@ -215,6 +215,7 @@ We use a similar set of scopes as
- `special` (preprocessor in C)
- `tag` - Tags (e.g. `<body>` in HTML)
- `builtin`
- `namespace`
@ -229,6 +230,7 @@ We use a similar set of scopes as
- `numbered`
- `bold`
- `italic`
- `strikethrough`
- `link`
- `url` - URLs pointed to by links
- `label` - non-URL link references
@ -261,58 +263,61 @@ These scopes are used for theming the editor interface:
- `hover` - for hover popup UI
| Key | Notes |
| --- | --- |
| `ui.background` | |
| `ui.background.separator` | Picker separator below input line |
| `ui.cursor` | |
| `ui.cursor.normal` | |
| `ui.cursor.insert` | |
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.cursor.primary.normal` | |
| `ui.cursor.primary.insert` | |
| `ui.cursor.primary.select` | |
| `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
| `ui.virtual.whitespace` | Visible whitespace characters |
| `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
| Key | Notes |
| --- | --- |
| `ui.background` | |
| `ui.background.separator` | Picker separator below input line |
| `ui.cursor` | |
| `ui.cursor.normal` | |
| `ui.cursor.insert` | |
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.cursor.primary.normal` | |
| `ui.cursor.primary.insert` | |
| `ui.cursor.primary.select` | |
| `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
| `ui.virtual.whitespace` | Visible whitespace characters |
| `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.virtual.inlay-hint` | Default style for inlay hints of all kinds |
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
[editor-section]: ./configuration.md#editor-section

@ -33,7 +33,13 @@ inside the project. We use [xtask][xtask] as an ad-hoc task runner and
thus do not require any dependencies other than `cargo` (You don't have
to `cargo install` anything either).
# Integration tests
# Testing
## Unit tests/Documentation tests
Run `cargo test --workspace` to run unit tests and documentation tests in all packages.
## Integration tests
Integration tests for helix-term can be run with `cargo integration-test`. Code
contributors are strongly encouraged to write integration tests for their code.

@ -35,7 +35,7 @@ pub enum DiagnosticTag {
Deprecated,
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.94.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,

@ -119,16 +119,7 @@ fn overlay() {
"foobar",
0,
false,
&[
Overlay {
char_idx: 0,
grapheme: "X".into(),
},
Overlay {
char_idx: 2,
grapheme: "\t".into(),
},
]
&[Overlay::new(0, "X"), Overlay::new(2, "\t")],
),
"Xo bar "
);
@ -138,18 +129,9 @@ fn overlay() {
0,
true,
&[
Overlay {
char_idx: 2,
grapheme: "\t".into(),
},
Overlay {
char_idx: 5,
grapheme: "\t".into(),
},
Overlay {
char_idx: 16,
grapheme: "X".into(),
},
Overlay::new(2, "\t"),
Overlay::new(5, "\t"),
Overlay::new(16, "X"),
]
),
"fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
@ -170,24 +152,14 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
#[test]
fn annotation() {
assert_eq!(
annotate_text(
"bar",
false,
&[InlineAnnotation {
char_idx: 0,
text: "foo".into(),
}]
),
annotate_text("bar", false, &[InlineAnnotation::new(0, "foo")]),
"foobar "
);
assert_eq!(
annotate_text(
&"foo ".repeat(10),
true,
&[InlineAnnotation {
char_idx: 0,
text: "foo ".into(),
}]
&[InlineAnnotation::new(0, "foo ")]
),
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
);
@ -199,20 +171,8 @@ fn annotation_and_overlay() {
"bbar".into(),
&TextFormat::new_test(false),
TextAnnotations::default()
.add_inline_annotations(
Rc::new([InlineAnnotation {
char_idx: 0,
text: "fooo".into(),
}]),
None
)
.add_overlay(
Rc::new([Overlay {
char_idx: 0,
grapheme: "\t".into(),
}]),
None
),
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
0,
)
.0

@ -4,6 +4,7 @@ use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
graphemes::tab_width_at,
syntax::{LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
Rope, RopeSlice,
@ -189,7 +190,7 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
let mut len = 0;
for ch in line.chars() {
match ch {
'\t' => len += tab_width,
'\t' => len += tab_width_at(len, tab_width as u16),
' ' => len += 1,
_ => break,
}

@ -661,6 +661,15 @@ impl<'a> IntoIterator for &'a Selection {
}
}
impl IntoIterator for Selection {
type Item = Range;
type IntoIter = smallvec::IntoIter<[Range; 1]>;
fn into_iter(self) -> smallvec::IntoIter<[Range; 1]> {
self.ranges.into_iter()
}
}
// TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_or_remove_matches(

@ -15,6 +15,15 @@ pub struct InlineAnnotation {
pub char_idx: usize,
}
impl InlineAnnotation {
pub fn new(char_idx: usize, text: impl Into<Tendril>) -> Self {
Self {
char_idx,
text: text.into(),
}
}
}
/// Represents a **single Grapheme** that is part of the document
/// that start at `char_idx` that will be replaced with
/// a different `grapheme`.
@ -33,22 +42,13 @@ pub struct InlineAnnotation {
/// use helix_core::text_annotations::Overlay;
///
/// // replaces a
/// Overlay {
/// char_idx: 0,
/// grapheme: "X".into(),
/// };
/// Overlay::new(0, "X");
///
/// // replaces X͎̊͢͜͝͡
/// Overlay{
/// char_idx: 1,
/// grapheme: "\t".into(),
/// };
/// Overlay::new(1, "\t");
///
/// // replaces b
/// Overlay{
/// char_idx: 6,
/// grapheme: "X̢̢̟͖̲͌̋̇͑͝".into(),
/// };
/// Overlay::new(6, "X̢̢̟͖̲͌̋̇͑͝");
/// ```
///
/// The following examples are invalid uses
@ -57,16 +57,10 @@ pub struct InlineAnnotation {
/// use helix_core::text_annotations::Overlay;
///
/// // overlay is not aligned at grapheme boundary
/// Overlay{
/// char_idx: 3,
/// grapheme: "x".into(),
/// };
/// Overlay::new(3, "x");
///
/// // overlay contains multiple graphemes
/// Overlay{
/// char_idx: 0,
/// grapheme: "xy".into(),
/// };
/// Overlay::new(0, "xy");
/// ```
#[derive(Debug, Clone)]
pub struct Overlay {
@ -74,6 +68,15 @@ pub struct Overlay {
pub grapheme: Tendril,
}
impl Overlay {
pub fn new(char_idx: usize, grapheme: impl Into<Tendril>) -> Self {
Self {
char_idx,
grapheme: grapheme.into(),
}
}
}
/// Line annotations allow for virtual text between normal
/// text lines. They cause `height` empty lines to be inserted
/// below the document line that contains `anchor_char_idx`.

@ -1,3 +1,5 @@
use smallvec::SmallVec;
use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow;
@ -466,6 +468,33 @@ impl Transaction {
self
}
/// Generate a transaction from a set of potentially overlapping changes. The `change_ranges`
/// iterator yield the range (of removed text) in the old document for each edit. If any change
/// overlaps with a range overlaps with a previous range then that range is ignored.
///
/// The `process_change` callback is called for each edit that is not ignored (in the order
/// yielded by `changes`) and should return the new text that the associated range will be
/// replaced with.
///
/// To make this function more flexible the iterator can yield additional data for each change
/// that is passed to `process_change`
pub fn change_ignore_overlapping<T>(
doc: &Rope,
change_ranges: impl Iterator<Item = (usize, usize, T)>,
mut process_change: impl FnMut(usize, usize, T) -> Option<Tendril>,
) -> Self {
let mut last = 0;
let changes = change_ranges.filter_map(|(from, to, data)| {
if from < last {
return None;
}
let tendril = process_change(from, to, data);
last = to;
Some((from, to, tendril))
});
Self::change(doc, changes)
}
/// Generate a transaction from a set of changes.
pub fn change<I>(doc: &Rope, changes: I) -> Self
where
@ -513,6 +542,44 @@ impl Transaction {
Self::change(doc, selection.iter().map(f))
}
pub fn change_by_selection_ignore_overlapping(
doc: &Rope,
selection: &Selection,
mut change_range: impl FnMut(&Range) -> (usize, usize),
mut create_tendril: impl FnMut(usize, usize) -> Option<Tendril>,
) -> (Transaction, Selection) {
let mut last_selection_idx = None;
let mut new_primary_idx = None;
let mut ranges: SmallVec<[Range; 1]> = SmallVec::new();
let process_change = |change_start, change_end, (idx, range): (usize, &Range)| {
// update the primary idx
if idx == selection.primary_index() {
new_primary_idx = Some(idx);
} else if new_primary_idx.is_none() {
if idx > selection.primary_index() {
new_primary_idx = last_selection_idx;
} else {
last_selection_idx = Some(idx);
}
}
ranges.push(*range);
create_tendril(change_start, change_end)
};
let transaction = Self::change_ignore_overlapping(
doc,
selection.iter().enumerate().map(|range| {
let (change_start, change_end) = change_range(range.1);
(change_start, change_end, range)
}),
process_change,
);
(
transaction,
Selection::new(ranges, new_primary_idx.unwrap_or(0)),
)
}
/// Insert text at each selection head.
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
Self::change_by_selection(doc, selection, |range| {

@ -1,5 +1,5 @@
use helix_core::{
indent::{treesitter_indent_for_pos, IndentStyle},
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::Loader,
Syntax,
};
@ -17,6 +17,39 @@ fn test_treesitter_indent_rust_2() {
// test_treesitter_indent("commands.rs", "source.rust");
}
#[test]
fn test_indent_level_for_line_with_spaces() {
let tab_width: usize = 4;
let indent_width: usize = 4;
let line = ropey::Rope::from_str(" Indented with 8 spaces");
let indent_level = indent_level_for_line(line.slice(0..), tab_width, indent_width);
assert_eq!(indent_level, 2)
}
#[test]
fn test_indent_level_for_line_with_tabs() {
let tab_width: usize = 4;
let indent_width: usize = 4;
let line = ropey::Rope::from_str("\t\tIndented with 2 tabs");
let indent_level = indent_level_for_line(line.slice(0..), tab_width, indent_width);
assert_eq!(indent_level, 2)
}
#[test]
fn test_indent_level_for_line_with_spaces_and_tabs() {
let tab_width: usize = 4;
let indent_width: usize = 4;
let line = ropey::Rope::from_str(" \t \tIndented with mix of spaces and tabs");
let indent_level = indent_level_for_line(line.slice(0..), tab_width, indent_width);
assert_eq!(indent_level, 2)
}
fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("tests/data/indent");

@ -67,8 +67,9 @@ pub fn get_language(name: &str) -> Result<Language> {
#[cfg(not(target_arch = "wasm32"))]
pub fn get_language(name: &str) -> Result<Language> {
use libloading::{Library, Symbol};
let mut library_path = crate::runtime_dir().join("grammars").join(name);
library_path.set_extension(DYLIB_EXTENSION);
let mut rel_library_path = PathBuf::new().join("grammars").join(name);
rel_library_path.set_extension(DYLIB_EXTENSION);
let library_path = crate::runtime_file(&rel_library_path);
let library = unsafe { Library::new(&library_path) }
.with_context(|| format!("Error opening dynamic library {:?}", library_path))?;
@ -252,7 +253,9 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
remote, revision, ..
} = grammar.source
{
let grammar_dir = crate::runtime_dir()
let grammar_dir = crate::runtime_dirs()
.first()
.expect("No runtime directories provided") // guaranteed by post-condition
.join("grammars")
.join("sources")
.join(&grammar.grammar_id);
@ -350,7 +353,9 @@ fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result<
let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
PathBuf::from(&path)
} else {
crate::runtime_dir()
crate::runtime_dirs()
.first()
.expect("No runtime directories provided") // guaranteed by post-condition
.join("grammars")
.join("sources")
.join(&grammar.grammar_id)
@ -401,7 +406,10 @@ fn build_tree_sitter_library(
None
}
};
let parser_lib_path = crate::runtime_dir().join("grammars");
let parser_lib_path = crate::runtime_dirs()
.first()
.expect("No runtime directories provided") // guaranteed by post-condition
.join("grammars");
let mut library_path = parser_lib_path.join(&grammar.grammar_id);
library_path.set_extension(DYLIB_EXTENSION);
@ -511,9 +519,6 @@ fn mtime(path: &Path) -> Result<SystemTime> {
/// Gives the contents of a file from a language's `runtime/queries/<lang>`
/// directory
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
.join(language)
.join(filename);
let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename));
std::fs::read_to_string(path)
}

@ -2,11 +2,12 @@ pub mod config;
pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
pub static RUNTIME_DIR: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(runtime_dir);
static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
once_cell::sync::Lazy::new(prioritize_runtime_dirs);
static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
@ -25,31 +26,83 @@ pub fn initialize_config_file(specified_file: Option<PathBuf>) {
CONFIG_FILE.set(config_file).ok();
}
pub fn runtime_dir() -> PathBuf {
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
return dir.into();
}
/// A list of runtime directories from highest to lowest priority
///
/// The priority is:
///
/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set)
/// 2. subdirectory of user config directory (always included)
/// 3. `HELIX_RUNTIME` (if environment variable is set)
/// 4. subdirectory of path to helix executable (always included)
///
/// Postcondition: returns at least two paths (they might not exist).
fn prioritize_runtime_dirs() -> Vec<PathBuf> {
const RT_DIR: &str = "runtime";
// Adding higher priority first
let mut rt_dirs = Vec::new();
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
log::debug!("runtime dir: {}", path.to_string_lossy());
return path;
rt_dirs.push(path);
}
const RT_DIR: &str = "runtime";
let conf_dir = config_dir().join(RT_DIR);
if conf_dir.exists() {
return conf_dir;
let conf_rt_dir = config_dir().join(RT_DIR);
rt_dirs.push(conf_rt_dir);
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
rt_dirs.push(dir.into());
}
// fallback to location of the executable being run
// canonicalize the path in case the executable is symlinked
std::env::current_exe()
let exe_rt_dir = std::env::current_exe()
.ok()
.and_then(|path| std::fs::canonicalize(path).ok())
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
.unwrap()
.unwrap();
rt_dirs.push(exe_rt_dir);
rt_dirs
}
/// Runtime directories ordered from highest to lowest priority
///
/// All directories should be checked when looking for files.
///
/// Postcondition: returns at least one path (it might not exist).
pub fn runtime_dirs() -> &'static [PathBuf] {
&RUNTIME_DIRS
}
/// Find file with path relative to runtime directory
///
/// `rel_path` should be the relative path from within the `runtime/` directory.
/// The valid runtime directories are searched in priority order and the first
/// file found to exist is returned, otherwise None.
fn find_runtime_file(rel_path: &Path) -> Option<PathBuf> {
RUNTIME_DIRS.iter().find_map(|rt_dir| {
let path = rt_dir.join(rel_path);
if path.exists() {
Some(path)
} else {
None
}
})
}
/// Find file with path relative to runtime directory
///
/// `rel_path` should be the relative path from within the `runtime/` directory.
/// The valid runtime directories are searched in priority order and the first
/// file found to exist is returned, otherwise the path to the final attempt
/// that failed.
pub fn runtime_file(rel_path: &Path) -> PathBuf {
find_runtime_file(rel_path).unwrap_or_else(|| {
RUNTIME_DIRS
.last()
.map(|dir| dir.join(rel_path))
.unwrap_or_default()
})
}
pub fn config_dir() -> PathBuf {

@ -315,6 +315,9 @@ impl Client {
execute_command: Some(lsp::DynamicRegistrationClientCapabilities {
dynamic_registration: Some(false),
}),
inlay_hint: Some(lsp::InlayHintWorkspaceClientCapabilities {
refresh_support: Some(false),
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -386,6 +389,10 @@ impl Client {
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
..Default::default()
}),
inlay_hint: Some(lsp::InlayHintClientCapabilities {
dynamic_registration: Some(false),
resolve_support: None,
}),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
@ -726,6 +733,31 @@ impl Client {
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}
pub fn text_document_range_inlay_hints(
&self,
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
match capabilities.inlay_hint_provider {
Some(
lsp::OneOf::Left(true)
| lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
) => (),
_ => return None,
}
let params = lsp::InlayHintParams {
text_document,
range,
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
Some(self.call::<lsp::request::InlayHintRequest>(params))
}
pub fn text_document_hover(
&self,
text_document: lsp::TextDocumentIdentifier,

@ -59,8 +59,8 @@ pub enum OffsetEncoding {
pub mod util {
use super::*;
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
use helix_core::{chars, RopeSlice, SmallVec};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
use helix_core::{smallvec, SmallVec};
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
@ -247,13 +247,56 @@ pub mod util {
Some(Range::new(start, end))
}
/// If the LS did not provide a range for the completion or the range of the
/// primary cursor can not be used for the secondary cursor, this function
/// can be used to find the completion range for a cursor
fn find_completion_range(text: RopeSlice, replace_mode: bool, cursor: usize) -> (usize, usize) {
let start = cursor
- text
.chars_at(cursor)
.reversed()
.take_while(|ch| chars::char_is_word(*ch))
.count();
let mut end = cursor;
if replace_mode {
end += text
.chars_at(cursor)
.skip(1)
.take_while(|ch| chars::char_is_word(*ch))
.count();
}
(start, end)
}
fn completion_range(
text: RopeSlice,
edit_offset: Option<(i128, i128)>,
replace_mode: bool,
cursor: usize,
) -> Option<(usize, usize)> {
let res = match edit_offset {
Some((start_offset, end_offset)) => {
let start_offset = cursor as i128 + start_offset;
if start_offset < 0 {
return None;
}
let end_offset = cursor as i128 + end_offset;
if end_offset > text.len_chars() as i128 {
return None;
}
(start_offset as usize, end_offset as usize)
}
None => find_completion_range(text, replace_mode, cursor),
};
Some(res)
}
/// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
/// The transaction applies the edit to all cursors.
pub fn generate_transaction_from_completion_edit(
doc: &Rope,
selection: &Selection,
start_offset: i128,
end_offset: i128,
edit_offset: Option<(i128, i128)>,
replace_mode: bool,
new_text: String,
) -> Transaction {
let replacement: Option<Tendril> = if new_text.is_empty() {
@ -263,83 +306,168 @@ pub mod util {
};
let text = doc.slice(..);
let (removed_start, removed_end) = completion_range(
text,
edit_offset,
replace_mode,
selection.primary().cursor(text),
)
.expect("transaction must be valid for primary selection");
let removed_text = text.slice(removed_start..removed_end);
Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
(
(cursor as i128 + start_offset) as usize,
(cursor as i128 + end_offset) as usize,
replacement.clone(),
)
})
let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
let cursor = range.cursor(text);
completion_range(text, edit_offset, replace_mode, cursor)
.filter(|(start, end)| text.slice(start..end) == removed_text)
.unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
},
|_, _| replacement.clone(),
);
if transaction.changes().is_empty() {
return transaction;
}
selection = selection.map(transaction.changes());
transaction.with_selection(selection)
}
/// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
/// The transaction applies the edit to all cursors.
#[allow(clippy::too_many_arguments)]
pub fn generate_transaction_from_snippet(
doc: &Rope,
selection: &Selection,
start_offset: i128,
end_offset: i128,
edit_offset: Option<(i128, i128)>,
replace_mode: bool,
snippet: snippet::Snippet,
line_ending: &str,
include_placeholder: bool,
tab_width: usize,
indent_width: usize,
) -> Transaction {
let text = doc.slice(..);
// For each cursor store offsets for the first tabstop
let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
let replacement_start = (cursor as i128 + start_offset) as usize;
let replacement_end = (cursor as i128 + end_offset) as usize;
let newline_with_offset = format!(
"{line_ending}{blank:width$}",
line_ending = line_ending,
width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
blank = ""
);
let (replacement, tabstops) =
snippet::render(&snippet, newline_with_offset, include_placeholder);
let replacement_len = replacement.chars().count();
cursor_tabstop_offsets.push(
tabstops
.first()
.unwrap_or(&smallvec![(replacement_len, replacement_len)])
.iter()
.map(|(from, to)| -> (i128, i128) {
(
*from as i128 - replacement_len as i128,
*to as i128 - replacement_len as i128,
)
})
.collect(),
);
(replacement_start, replacement_end, Some(replacement.into()))
});
let mut off = 0i128;
let mut mapped_doc = doc.clone();
let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
let (removed_start, removed_end) = completion_range(
text,
edit_offset,
replace_mode,
selection.primary().cursor(text),
)
.expect("transaction must be valid for primary selection");
let removed_text = text.slice(removed_start..removed_end);
// Create new selection based on the cursor tabstop from above
let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
let selection = selection
.clone()
.map(transaction.changes())
.transform_iter(|range| {
cursor_tabstop_offsets_iter
.next()
.unwrap()
.iter()
.map(move |(from, to)| {
Range::new(
(range.anchor as i128 + *from) as usize,
(range.anchor as i128 + *to) as usize,
)
})
});
let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
let cursor = range.cursor(text);
completion_range(text, edit_offset, replace_mode, cursor)
.filter(|(start, end)| text.slice(start..end) == removed_text)
.unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
},
|replacement_start, replacement_end| {
let mapped_replacement_start = (replacement_start as i128 + off) as usize;
let mapped_replacement_end = (replacement_end as i128 + off) as usize;
let line_idx = mapped_doc.char_to_line(mapped_replacement_start);
let indent_level = helix_core::indent::indent_level_for_line(
mapped_doc.line(line_idx),
tab_width,
indent_width,
) * indent_width;
let newline_with_offset = format!(
"{line_ending}{blank:indent_level$}",
line_ending = line_ending,
blank = ""
);
let (replacement, tabstops) =
snippet::render(&snippet, &newline_with_offset, include_placeholder);
selection_tabstops.push((mapped_replacement_start, tabstops));
mapped_doc.remove(mapped_replacement_start..mapped_replacement_end);
mapped_doc.insert(mapped_replacement_start, &replacement);
off +=
replacement_start as i128 - replacement_end as i128 + replacement.len() as i128;
Some(replacement)
},
);
transaction.with_selection(selection)
let changes = transaction.changes();
if changes.is_empty() {
return transaction;
}
let mut mapped_selection = SmallVec::with_capacity(selection.len());
let mut mapped_primary_idx = 0;
let primary_range = selection.primary();
for (range, (tabstop_anchor, tabstops)) in selection.into_iter().zip(selection_tabstops) {
if range == primary_range {
mapped_primary_idx = mapped_selection.len()
}
let range = range.map(changes);
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
let Some(tabstops) = tabstops else{
// no tabstop normal mapping
mapped_selection.push(range);
continue;
};
// expand the selection to cover the tabstop to retain the helix selection semantic
// the tabstop closest to the range simply replaces `head` while anchor remains in place
// the remaining tabstops receive their own single-width cursor
if range.head < range.anchor {
let first_tabstop = tabstop_anchor + tabstops[0].1;
// if selection is forward but was moved to the right it is
// contained entirely in the replacement text, just do a point
// selection (fallback below)
if range.anchor >= first_tabstop {
let range = Range::new(range.anchor, first_tabstop);
mapped_selection.push(range);
let rem_tabstops = tabstops[1..]
.iter()
.map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
mapped_selection.extend(rem_tabstops);
continue;
}
} else {
let last_idx = tabstops.len() - 1;
let last_tabstop = tabstop_anchor + tabstops[last_idx].1;
// if selection is forward but was moved to the right it is
// contained entirely in the replacement text, just do a point
// selection (fallback below)
if range.anchor <= last_tabstop {
// we can't properly compute the the next grapheme
// here because the transaction hasn't been applied yet
// that is not a problem because the range gets grapheme aligned anyway
// tough so just adding one will always cause head to be grapheme
// aligned correctly when applied to the document
let range = Range::new(range.anchor, last_tabstop + 1);
mapped_selection.push(range);
let rem_tabstops = tabstops[..last_idx]
.iter()
.map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
mapped_selection.extend(rem_tabstops);
continue;
}
};
let tabstops = tabstops
.iter()
.map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
mapped_selection.extend(tabstops);
}
transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx))
}
pub fn generate_transaction_from_edits(
@ -398,6 +526,7 @@ pub enum MethodCall {
ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams),
WorkspaceFolders,
WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams),
}
impl MethodCall {
@ -417,6 +546,10 @@ impl MethodCall {
let params: lsp::ConfigurationParams = params.parse()?;
Self::WorkspaceConfiguration(params)
}
lsp::request::RegisterCapability::METHOD => {
let params: lsp::RegistrationParams = params.parse()?;
Self::RegisterCapability(params)
}
_ => {
return Err(Error::Unhandled);
}

@ -1,7 +1,7 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
use helix_core::{smallvec, SmallVec};
use helix_core::{smallvec, SmallVec, Tendril};
#[derive(Debug, PartialEq, Eq)]
pub enum CaseChange {
@ -57,10 +57,10 @@ pub fn parse(s: &str) -> Result<Snippet<'_>> {
fn render_elements(
snippet_elements: &[SnippetElement<'_>],
insert: &mut String,
insert: &mut Tendril,
offset: &mut usize,
tabstops: &mut Vec<(usize, (usize, usize))>,
newline_with_offset: &String,
newline_with_offset: &str,
include_placeholer: bool,
) {
use SnippetElement::*;
@ -121,10 +121,10 @@ fn render_elements(
#[allow(clippy::type_complexity)] // only used one time
pub fn render(
snippet: &Snippet<'_>,
newline_with_offset: String,
newline_with_offset: &str,
include_placeholer: bool,
) -> (String, Vec<SmallVec<[(usize, usize); 1]>>) {
let mut insert = String::new();
) -> (Tendril, Vec<SmallVec<[(usize, usize); 1]>>) {
let mut insert = Tendril::new();
let mut tabstops = Vec::new();
let mut offset = 0;
@ -133,7 +133,7 @@ pub fn render(
&mut insert,
&mut offset,
&mut tabstops,
&newline_with_offset,
newline_with_offset,
include_placeholer,
);

@ -31,6 +31,7 @@ use crate::{
use log::{debug, error, warn};
use std::{
io::{stdin, stdout},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
@ -113,10 +114,9 @@ impl Application {
use helix_view::editor::Action;
let theme_loader = std::sync::Arc::new(theme::Loader::new(
&helix_loader::config_dir(),
&helix_loader::runtime_dir(),
));
let mut theme_parent_dirs = vec![helix_loader::config_dir()];
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));
let true_color = config.editor.true_color || crate::true_color();
let theme = config
@ -162,7 +162,7 @@ impl Application {
compositor.push(editor_view);
if args.load_tutor {
let path = helix_loader::runtime_dir().join("tutor");
let path = helix_loader::runtime_file(Path::new("tutor"));
editor.open(&path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
@ -1014,6 +1014,17 @@ impl Application {
.collect();
Ok(json!(result))
}
Ok(MethodCall::RegisterCapability(_params)) => {
log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
// Language Servers based on the `vscode-languageserver-node` library often send
// client/registerCapability even though we do not enable dynamic registration
// for any capabilities. We should send a MethodNotFound JSONRPC error in this
// case but that rejects the registration promise in the server which causes an
// exit. So we work around this by ignoring the request and sending back an OK
// response.
Ok(serde_json::Value::Null)
}
};
let language_server = match self.editor.language_servers.get_by_id(server_id) {

@ -114,17 +114,7 @@ impl<'a> Context<'a> {
T: for<'de> serde::Deserialize<'de> + Send + 'static,
F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
{
let callback = Box::pin(async move {
let json = call.await?;
let response = serde_json::from_value(json)?;
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response)
},
));
Ok(call)
});
self.jobs.callback(callback);
self.jobs.callback(make_job_callback(call, callback));
}
/// Returns 1 if no explicit count was provided
@ -134,6 +124,27 @@ impl<'a> Context<'a> {
}
}
#[inline]
fn make_job_callback<T, F>(
call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
callback: F,
) -> std::pin::Pin<Box<impl Future<Output = Result<Callback, anyhow::Error>>>>
where
T: for<'de> serde::Deserialize<'de> + Send + 'static,
F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
{
Box::pin(async move {
let json = call.await?;
let response = serde_json::from_value(json)?;
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response)
},
));
Ok(call)
})
}
use helix_view::{align_view, Align};
/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like

@ -15,8 +15,13 @@ use tui::{
use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::{path, Selection};
use helix_view::{document::Mode, editor::Action, theme::Style};
use helix_core::{path, text_annotations::InlineAnnotation, Selection};
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action,
theme::Style,
Document, View,
};
use crate::{
compositor::{self, Compositor},
@ -27,7 +32,8 @@ use crate::{
};
use std::{
borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, path::PathBuf, sync::Arc,
borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf,
sync::Arc,
};
/// Gets the language server that is attached to a document, and
@ -1391,3 +1397,174 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
},
);
}
pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) {
if !editor.config().lsp.display_inlay_hints {
return;
}
for (view, _) in editor.tree.views() {
let doc = match editor.documents.get(&view.doc) {
Some(doc) => doc,
None => continue,
};
if let Some(callback) = compute_inlay_hints_for_view(view, doc) {
jobs.callback(callback);
}
}
}
fn compute_inlay_hints_for_view(
view: &View,
doc: &Document,
) -> Option<std::pin::Pin<Box<impl Future<Output = Result<crate::job::Callback, anyhow::Error>>>>> {
let view_id = view.id;
let doc_id = view.doc;
let language_server = doc.language_server()?;
let capabilities = language_server.capabilities();
let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider {
Some(
lsp::OneOf::Left(true)
| lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
) => {
let doc_text = doc.text();
let len_lines = doc_text.len_lines();
// Compute ~3 times the current view height of inlay hints, that way some scrolling
// will not show half the view with hints and half without while still being faster
// than computing all the hints for the full file (which could be dozens of time
// longer than the view is).
let view_height = view.inner_height();
let first_visible_line = doc_text.char_to_line(view.offset.anchor);
let first_line = first_visible_line.saturating_sub(view_height);
let last_line = first_visible_line
.saturating_add(view_height.saturating_mul(2))
.min(len_lines);
let new_doc_inlay_hint_id = DocumentInlayHintsId {
first_line,
last_line,
};
// Don't recompute the annotations in case nothing has changed about the view
if !doc.inlay_hints_oudated
&& doc
.inlay_hints(view_id)
.map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
{
return None;
}
let doc_slice = doc_text.slice(..);
let first_char_in_range = doc_slice.line_to_char(first_line);
let last_char_in_range = doc_slice.line_to_char(last_line);
let range = helix_lsp::util::range_to_lsp_range(
doc_text,
helix_core::Range::new(first_char_in_range, last_char_in_range),
language_server.offset_encoding(),
);
(
language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
new_doc_inlay_hint_id,
)
}
_ => return None,
};
let callback = super::make_job_callback(
future?,
move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
// The config was modified or the window was closed while the request was in flight
if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
return;
}
// Add annotations to relevant document, not the current one (it may have changed in between)
let doc = match editor.documents.get_mut(&doc_id) {
Some(doc) => doc,
None => return,
};
// If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
let (mut hints, offset_encoding) = match (response, doc.language_server()) {
(Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
_ => {
doc.set_inlay_hints(
view_id,
DocumentInlayHints::empty_with_id(new_doc_inlay_hints_id),
);
doc.inlay_hints_oudated = false;
return;
}
};
// Most language servers will already send them sorted but ensure this is the case to
// avoid errors on our end.
hints.sort_unstable_by_key(|inlay_hint| inlay_hint.position);
let mut padding_before_inlay_hints = Vec::new();
let mut type_inlay_hints = Vec::new();
let mut parameter_inlay_hints = Vec::new();
let mut other_inlay_hints = Vec::new();
let mut padding_after_inlay_hints = Vec::new();
let doc_text = doc.text();
for hint in hints {
let char_idx =
match helix_lsp::util::lsp_pos_to_pos(doc_text, hint.position, offset_encoding)
{
Some(pos) => pos,
// Skip inlay hints that have no "real" position
None => continue,
};
let label = match hint.label {
lsp::InlayHintLabel::String(s) => s,
lsp::InlayHintLabel::LabelParts(parts) => parts
.into_iter()
.map(|p| p.value)
.collect::<Vec<_>>()
.join(""),
};
let inlay_hints_vec = match hint.kind {
Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints,
Some(lsp::InlayHintKind::PARAMETER) => &mut parameter_inlay_hints,
// We can't warn on unknown kind here since LSPs are free to set it or not, for
// example Rust Analyzer does not: every kind will be `None`.
_ => &mut other_inlay_hints,
};
if let Some(true) = hint.padding_left {
padding_before_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
}
inlay_hints_vec.push(InlineAnnotation::new(char_idx, label));
if let Some(true) = hint.padding_right {
padding_after_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
}
}
doc.set_inlay_hints(
view_id,
DocumentInlayHints {
id: new_doc_inlay_hints_id,
type_inlay_hints: type_inlay_hints.into(),
parameter_inlay_hints: parameter_inlay_hints.into(),
other_inlay_hints: other_inlay_hints.into(),
padding_before_inlay_hints: padding_before_inlay_hints.into(),
padding_after_inlay_hints: padding_after_inlay_hints.into(),
},
);
doc.inlay_hints_oudated = false;
},
);
Some(callback)
}

@ -1565,7 +1565,7 @@ fn tutor(
return Ok(());
}
let path = helix_loader::runtime_dir().join("tutor");
let path = helix_loader::runtime_file(Path::new("tutor"));
cx.editor.open(&path, Action::Replace)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(cx.editor).set_path(None)?;

@ -52,7 +52,7 @@ pub fn general() -> std::io::Result<()> {
let config_file = helix_loader::config_file();
let lang_file = helix_loader::lang_config_file();
let log_file = helix_loader::log_file();
let rt_dir = helix_loader::runtime_dir();
let rt_dirs = helix_loader::runtime_dirs();
let clipboard_provider = get_clipboard_provider();
if config_file.exists() {
@ -66,17 +66,31 @@ pub fn general() -> std::io::Result<()> {
writeln!(stdout, "Language file: default")?;
}
writeln!(stdout, "Log file: {}", log_file.display())?;
writeln!(stdout, "Runtime directory: {}", rt_dir.display())?;
if let Ok(path) = std::fs::read_link(&rt_dir) {
let msg = format!("Runtime directory is symlinked to {}", path.display());
writeln!(stdout, "{}", msg.yellow())?;
}
if !rt_dir.exists() {
writeln!(stdout, "{}", "Runtime directory does not exist.".red())?;
}
if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) {
writeln!(stdout, "{}", "Runtime directory is empty.".red())?;
writeln!(
stdout,
"Runtime directories: {}",
rt_dirs
.iter()
.map(|d| d.to_string_lossy())
.collect::<Vec<_>>()
.join(";")
)?;
for rt_dir in rt_dirs.iter() {
if let Ok(path) = std::fs::read_link(rt_dir) {
let msg = format!(
"Runtime directory {} is symlinked to: {}",
rt_dir.display(),
path.display()
);
writeln!(stdout, "{}", msg.yellow())?;
}
if !rt_dir.exists() {
let msg = format!("Runtime directory does not exist: {}", rt_dir.display());
writeln!(stdout, "{}", msg.yellow())?;
} else if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) {
let msg = format!("Runtime directory is empty: {}", rt_dir.display());
writeln!(stdout, "{}", msg.yellow())?;
}
}
writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;

@ -108,6 +108,7 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.preselect.unwrap_or(false));
@ -118,23 +119,27 @@ impl Completion {
view_id: ViewId,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
include_placeholder: bool,
replace_mode: bool,
) -> Transaction {
use helix_lsp::snippet;
let selection = doc.selection(view_id);
let text = doc.text().slice(..);
let primary_cursor = selection.primary().cursor(text);
let (start_offset, end_offset, new_text) = if let Some(edit) = &item.text_edit {
let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
// TODO: support using "insert" instead of "replace" via user config
lsp::TextEdit::new(item.replace, item.new_text.clone())
let range = if replace_mode {
item.replace
} else {
item.insert
};
lsp::TextEdit::new(range, item.new_text.clone())
}
};
let text = doc.text().slice(..);
let primary_cursor = selection.primary().cursor(text);
let start_offset =
match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) {
@ -147,26 +152,18 @@ impl Completion {
None => return Transaction::new(doc.text()),
};
(start_offset, end_offset, edit.new_text)
(Some((start_offset, end_offset)), edit.new_text)
} else {
let new_text = item.insert_text.as_ref().unwrap_or(&item.label);
// Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
// in these cases we need to check for a common prefix and remove it
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
let new_text = new_text.trim_start_matches::<&str>(&prefix);
// TODO: this needs to be true for the numbers to work out correctly
// in the closure below. It's passed in to a callback as this same
// formula, but can the value change between the LSP request and
// response? If it does, can we recover?
debug_assert!(
doc.selection(view_id)
.primary()
.cursor(doc.text().slice(..))
== trigger_offset
);
(0, 0, new_text.into())
let new_text = item
.insert_text
.clone()
.unwrap_or_else(|| item.label.clone());
// check that we are still at the correct savepoint
// we can still generate a transaction regardless but if the
// document changed (and not just the selection) then we will
// likely delete the wrong text (same if we applied an edit sent by the LS)
debug_assert!(primary_cursor == trigger_offset);
(None, new_text)
};
if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
@ -179,11 +176,13 @@ impl Completion {
Ok(snippet) => util::generate_transaction_from_snippet(
doc.text(),
selection,
start_offset,
end_offset,
edit_offset,
replace_mode,
snippet,
doc.line_ending.as_str(),
include_placeholder,
doc.tab_width(),
doc.indent_width(),
),
Err(err) => {
log::error!(
@ -198,8 +197,8 @@ impl Completion {
util::generate_transaction_from_completion_edit(
doc.text(),
selection,
start_offset,
end_offset,
edit_offset,
replace_mode,
new_text,
)
}
@ -230,9 +229,9 @@ impl Completion {
view.id,
item,
offset_encoding,
start_offset,
trigger_offset,
true,
replace_mode,
);
// initialize a savepoint
@ -252,9 +251,9 @@ impl Completion {
view.id,
item,
offset_encoding,
start_offset,
trigger_offset,
false,
replace_mode,
);
doc.apply(&transaction, view.id);

@ -992,6 +992,8 @@ impl EditorView {
}
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs);
if let Some(completion) = &mut self.completion {
return if completion.ensure_item_resolved(cx) {
EventResult::Consumed(None)
@ -1016,6 +1018,10 @@ impl EditorView {
event: &MouseEvent,
cxt: &mut commands::Context,
) -> EventResult {
if event.kind != MouseEventKind::Moved {
cxt.editor.reset_idle_timer();
}
let config = cxt.editor.config();
let MouseEvent {
kind,

@ -284,10 +284,10 @@ pub mod completers {
}
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes"));
names.extend(theme::Loader::read_names(
&helix_loader::config_dir().join("themes"),
));
let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
for rt_dir in helix_loader::runtime_dirs() {
names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
}
names.push("default".into());
names.push("base16_default".into());
names.sort();

@ -225,6 +225,9 @@ impl<T: Item> FilePicker<T> {
let loader = cx.editor.syn_loader.clone();
doc.detect_language(loader);
}
// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
// but it could be interesting in the future
}
EventResult::Consumed(None)
@ -339,6 +342,7 @@ impl<T: Item + 'static> Component for FilePicker<T> {
inner,
doc,
offset,
// TODO: compute text annotations asynchronously here (like inlay hints)
&TextAnnotations::default(),
highlights,
&cx.editor.theme,

@ -159,6 +159,7 @@ where
helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers,
helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer,
helix_view::editor::StatusLineElement::VersionControl => render_version_control,
}
}
@ -476,3 +477,16 @@ where
{
write(context, String::from(" "), None);
}
fn render_version_control<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let head = context
.doc
.version_control_head()
.unwrap_or_default()
.to_string();
write(context, head, None);
}

@ -15,6 +15,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
parking_lot = "0.12"
arc-swap = { version = "1.6.0" }
gix = { version = "0.39.0", default-features = false , optional = true }
imara-diff = "0.1.5"

@ -1,4 +1,6 @@
use arc_swap::ArcSwap;
use std::path::Path;
use std::sync::Arc;
use gix::objs::tree::EntryMode;
use gix::sec::trust::DefaultForLevel;
@ -87,6 +89,21 @@ impl DiffProvider for Git {
}
Some(data)
}
fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> {
debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute());
let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
let head_ref = repo.head_ref().ok()?;
let head_commit = repo.head_commit().ok()?;
let name = match head_ref {
Some(reference) => reference.name().shorten().to_string(),
None => head_commit.id.to_hex_with_len(8).to_string(),
};
Some(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
}
}
/// Finds the object that contains the contents of a file at a specific commit.

@ -1,4 +1,5 @@
use std::path::Path;
use arc_swap::ArcSwap;
use std::{path::Path, sync::Arc};
#[cfg(feature = "git")]
pub use git::Git;
@ -18,6 +19,7 @@ pub trait DiffProvider {
/// The data is returned as raw byte without any decoding or encoding performed
/// to ensure all file encodings are handled correctly.
fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>>;
fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>>;
}
#[doc(hidden)]
@ -26,6 +28,10 @@ impl DiffProvider for Dummy {
fn get_diff_base(&self, _file: &Path) -> Option<Vec<u8>> {
None
}
fn get_current_head_name(&self, _file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> {
None
}
}
pub struct DiffProviderRegistry {
@ -38,6 +44,12 @@ impl DiffProviderRegistry {
.iter()
.find_map(|provider| provider.get_diff_base(file))
}
pub fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> {
self.providers
.iter()
.find_map(|provider| provider.get_current_head_name(file))
}
}
impl Default for DiffProviderRegistry {

@ -1,11 +1,12 @@
use anyhow::{anyhow, bail, Context, Error};
use arc_swap::access::DynAccess;
use arc_swap::ArcSwap;
use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
use helix_core::doc_formatter::TextFormat;
use helix_core::syntax::Highlight;
use helix_core::text_annotations::TextAnnotations;
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
use helix_core::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry};
@ -18,6 +19,7 @@ use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::SystemTime;
@ -118,6 +120,14 @@ pub struct Document {
text: Rope,
selections: HashMap<ViewId, Selection>,
/// Inlay hints annotations for the document, by view.
///
/// To know if they're up-to-date, check the `id` field in `DocumentInlayHints`.
pub(crate) inlay_hints: HashMap<ViewId, DocumentInlayHints>,
/// Set to `true` when the document is updated, reset to `false` on the next inlay hints
/// update from the LSP
pub inlay_hints_oudated: bool,
path: Option<PathBuf>,
encoding: &'static encoding::Encoding,
@ -158,6 +168,74 @@ pub struct Document {
language_server: Option<Arc<helix_lsp::Client>>,
diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
}
/// Inlay hints for a single `(Document, View)` combo.
///
/// There are `*_inlay_hints` field for each kind of hints an LSP can send since we offer the
/// option to style theme differently in the theme according to the (currently supported) kinds
/// (`type`, `parameter` and the rest).
///
/// Inlay hints are always `InlineAnnotation`s, not overlays or line-ones: LSP may choose to place
/// them anywhere in the text and will sometime offer config options to move them where the user
/// wants them but it shouldn't be Helix who decides that so we use the most precise positioning.
///
/// The padding for inlay hints needs to be stored separately for before and after (the LSP spec
/// uses 'left' and 'right' but not all text is left to right so let's be correct) padding because
/// the 'before' padding must be added to a layer *before* the regular inlay hints and the 'after'
/// padding comes ... after.
#[derive(Debug, Clone)]
pub struct DocumentInlayHints {
/// Identifier for the inlay hints stored in this structure. To be checked to know if they have
/// to be recomputed on idle or not.
pub id: DocumentInlayHintsId,
/// Inlay hints of `TYPE` kind, if any.
pub type_inlay_hints: Rc<[InlineAnnotation]>,
/// Inlay hints of `PARAMETER` kind, if any.
pub parameter_inlay_hints: Rc<[InlineAnnotation]>,
/// Inlay hints that are neither `TYPE` nor `PARAMETER`.
///
/// LSPs are not required to associate a kind to their inlay hints, for example Rust-Analyzer
/// currently never does (February 2023) and the LSP spec may add new kinds in the future that
/// we want to display even if we don't have some special highlighting for them.
pub other_inlay_hints: Rc<[InlineAnnotation]>,
/// Inlay hint padding. When creating the final `TextAnnotations`, the `before` padding must be
/// added first, then the regular inlay hints, then the `after` padding.
pub padding_before_inlay_hints: Rc<[InlineAnnotation]>,
pub padding_after_inlay_hints: Rc<[InlineAnnotation]>,
}
impl DocumentInlayHints {
/// Generate an empty list of inlay hints with the given ID.
pub fn empty_with_id(id: DocumentInlayHintsId) -> Self {
Self {
id,
type_inlay_hints: Rc::new([]),
parameter_inlay_hints: Rc::new([]),
other_inlay_hints: Rc::new([]),
padding_before_inlay_hints: Rc::new([]),
padding_after_inlay_hints: Rc::new([]),
}
}
}
/// Associated with a [`Document`] and [`ViewId`], uniquely identifies the state of inlay hints for
/// for that document and view: if this changed since the last save, the inlay hints for the view
/// should be recomputed.
///
/// We can't store the `ViewOffset` instead of the first and last asked-for lines because if
/// softwrapping changes, the `ViewOffset` may not change while the displayed lines will.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct DocumentInlayHintsId {
/// First line for which the inlay hints were requested.
pub first_line: usize,
/// Last line for which the inlay hints were requested.
pub last_line: usize,
}
use std::{fmt, mem};
@ -167,6 +245,8 @@ impl fmt::Debug for Document {
.field("id", &self.id)
.field("text", &self.text)
.field("selections", &self.selections)
.field("inlay_hints_oudated", &self.inlay_hints_oudated)
.field("text_annotations", &self.inlay_hints)
.field("path", &self.path)
.field("encoding", &self.encoding)
.field("restore_cursor", &self.restore_cursor)
@ -185,6 +265,15 @@ impl fmt::Debug for Document {
}
}
impl fmt::Debug for DocumentInlayHintsId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Much more agreable to read when debugging
f.debug_struct("DocumentInlayHintsId")
.field("lines", &(self.first_line..self.last_line))
.finish()
}
}
// The documentation and implementation of this function should be up-to-date with
// its sibling function, `to_writer()`.
//
@ -387,6 +476,8 @@ impl Document {
encoding,
text,
selections: HashMap::default(),
inlay_hints: HashMap::default(),
inlay_hints_oudated: false,
indent_style: DEFAULT_INDENT,
line_ending: DEFAULT_LINE_ENDING,
restore_cursor: false,
@ -404,6 +495,7 @@ impl Document {
language_server: None,
diff_handle: None,
config,
version_control_head: None,
}
}
pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self {
@ -707,6 +799,8 @@ impl Document {
None => self.diff_handle = None,
}
self.version_control_head = provider_registry.get_current_head_name(&path);
Ok(())
}
@ -814,13 +908,16 @@ impl Document {
}
}
/// Remove a view's selection from this document.
/// Remove a view's selection and inlay hints from this document.
pub fn remove_view(&mut self, view_id: ViewId) {
self.selections.remove(&view_id);
self.inlay_hints.remove(&view_id);
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
use helix_core::Assoc;
let old_doc = self.text().clone();
let success = transaction.changes().apply(&mut self.text);
@ -876,10 +973,10 @@ impl Document {
.unwrap();
}
let changes = transaction.changes();
// map state.diagnostics over changes::map_pos too
for diagnostic in &mut self.diagnostics {
use helix_core::Assoc;
let changes = transaction.changes();
diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
@ -887,13 +984,40 @@ impl Document {
self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range);
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
if let Some(data) = Rc::get_mut(annotations) {
for inline in data.iter_mut() {
inline.char_idx = changes.map_pos(inline.char_idx, Assoc::After);
}
}
};
self.inlay_hints_oudated = true;
for text_annotation in self.inlay_hints.values_mut() {
let DocumentInlayHints {
id: _,
type_inlay_hints,
parameter_inlay_hints,
other_inlay_hints,
padding_before_inlay_hints,
padding_after_inlay_hints,
} = text_annotation;
apply_inlay_hint_changes(padding_before_inlay_hints);
apply_inlay_hint_changes(type_inlay_hints);
apply_inlay_hint_changes(parameter_inlay_hints);
apply_inlay_hint_changes(other_inlay_hints);
apply_inlay_hint_changes(padding_after_inlay_hints);
}
// emit lsp notification
if let Some(language_server) = self.language_server() {
let notify = language_server.text_document_did_change(
self.versioned_identifier(),
&old_doc,
self.text(),
transaction.changes(),
changes,
);
if let Some(notify) = notify {
@ -1158,6 +1282,17 @@ impl Document {
}
}
pub fn version_control_head(&self) -> Option<Arc<Box<str>>> {
self.version_control_head.as_ref().map(|a| a.load_full())
}
pub fn set_version_control_head(
&mut self,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
) {
self.version_control_head = version_control_head;
}
#[inline]
/// Tree-sitter AST tree
pub fn syntax(&self) -> Option<&Syntax> {
@ -1201,6 +1336,7 @@ impl Document {
&self.selections[&view_id]
}
#[inline]
pub fn selections(&self) -> &HashMap<ViewId, Selection> {
&self.selections
}
@ -1339,9 +1475,27 @@ impl Document {
}
}
/// Get the text annotations that apply to the whole document, those that do not apply to any
/// specific view.
pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations {
TextAnnotations::default()
}
/// Set the inlay hints for this document and `view_id`.
pub fn set_inlay_hints(&mut self, view_id: ViewId, inlay_hints: DocumentInlayHints) {
self.inlay_hints.insert(view_id, inlay_hints);
}
/// Get the inlay hints for this document and `view_id`.
pub fn inlay_hints(&self, view_id: ViewId) -> Option<&DocumentInlayHints> {
self.inlay_hints.get(&view_id)
}
/// Completely removes all the inlay hints saved for the document, dropping them to free memory
/// (since it often means inlay hints have been fully deactivated).
pub fn reset_all_inlay_hints(&mut self) {
self.inlay_hints = Default::default();
}
}
#[derive(Clone, Debug)]

@ -295,6 +295,9 @@ pub struct Config {
)]
pub idle_timeout: Duration,
pub completion_trigger_len: u8,
/// Whether to instruct the LSP to replace the entire word when applying a completion
/// or to only insert new text
pub completion_replace: bool,
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
pub file_picker: FilePickerConfig,
@ -388,6 +391,8 @@ pub struct LspConfig {
pub auto_signature_help: bool,
/// Display docs under signature help popup
pub display_signature_help_docs: bool,
/// Display inlay hints
pub display_inlay_hints: bool,
}
impl Default for LspConfig {
@ -397,6 +402,7 @@ impl Default for LspConfig {
display_messages: false,
auto_signature_help: true,
display_signature_help_docs: true,
display_inlay_hints: false,
}
}
}
@ -510,6 +516,9 @@ pub enum StatusLineElement {
/// A single space
Spacer,
/// Current version control information
VersionControl,
}
// Cursor shape is read and used on every rendered frame and so needs
@ -785,6 +794,7 @@ impl Default for Config {
explorer: ExplorerConfig::default(),
soft_wrap: SoftWrap::default(),
text_width: 80,
completion_replace: false,
}
}
}
@ -1185,6 +1195,19 @@ impl Editor {
fn _refresh(&mut self) {
let config = self.config();
// Reset the inlay hints annotations *before* updating the views, that way we ensure they
// will disappear during the `.sync_change(doc)` call below.
//
// We can't simply check this config when rendering because inlay hints are only parts of
// the possible annotations, and others could still be active, so we need to selectively
// drop the inlay hints.
if !config.lsp.display_inlay_hints {
for doc in self.documents_mut() {
doc.reset_all_inlay_hints();
}
}
for (view, _) in self.tree.views_mut() {
let doc = doc_mut!(self, &view.doc);
view.sync_changes(doc);
@ -1352,6 +1375,7 @@ impl Editor {
if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
doc.set_diff_base(diff_base, self.redraw_handle.clone());
}
doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
let id = self.new_document(doc);
let _ = self.launch_language_server(id);

@ -1,5 +1,5 @@
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
path::{Path, PathBuf},
str,
};
@ -37,19 +37,21 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
#[derive(Clone, Debug)]
pub struct Loader {
user_dir: PathBuf,
default_dir: PathBuf,
/// Theme directories to search from highest to lowest priority
theme_dirs: Vec<PathBuf>,
}
impl Loader {
/// Creates a new loader that can load themes from two directories.
pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
/// Creates a new loader that can load themes from multiple directories.
///
/// The provided directories should be ordered from highest to lowest priority.
/// The directories will have their "themes" subdirectory searched.
pub fn new(dirs: &[PathBuf]) -> Self {
Self {
user_dir: user_dir.as_ref().join("themes"),
default_dir: default_dir.as_ref().join("themes"),
theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(),
}
}
/// Loads a theme first looking in the `user_dir` then in `default_dir`
/// Loads a theme searching directories in priority order.
pub fn load(&self, name: &str) -> Result<Theme> {
if name == "default" {
return Ok(self.default());
@ -58,7 +60,8 @@ impl Loader {
return Ok(self.base16_default());
}
let theme = self.load_theme(name, name, false).map(Theme::from)?;
let mut visited_paths = HashSet::new();
let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?;
Ok(Theme {
name: name.into(),
@ -66,16 +69,18 @@ impl Loader {
})
}
// load the theme and its parent recursively and merge them
// `base_theme_name` is the theme from the config.toml,
// used to prevent some circular loading scenarios
fn load_theme(
&self,
name: &str,
base_theme_name: &str,
only_default_dir: bool,
) -> Result<Value> {
let path = self.path(name, only_default_dir);
/// Recursively load a theme, merging with any inherited parent themes.
///
/// The paths that have been visited in the inheritance hierarchy are tracked
/// to detect and avoid cycling.
///
/// It is possible for one file to inherit from another file with the same name
/// so long as the second file is in a themes directory with lower priority.
/// However, it is not recommended that users do this as it will make tracing
/// errors more difficult.
fn load_theme(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<Value> {
let path = self.path(name, visited_paths)?;
let theme_toml = self.load_toml(path)?;
let inherits = theme_toml.get("inherits");
@ -92,11 +97,7 @@ impl Loader {
// load default themes's toml from const.
"default" => DEFAULT_THEME_DATA.clone(),
"base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
_ => self.load_theme(
parent_theme_name,
base_theme_name,
base_theme_name == parent_theme_name,
)?,
_ => self.load_theme(parent_theme_name, visited_paths)?,
};
self.merge_themes(parent_theme_toml, theme_toml)
@ -148,7 +149,7 @@ impl Loader {
merge_toml_values(theme, palette.into(), 1)
}
// Loads the theme data as `toml::Value` first from the user_dir then in default_dir
// Loads the theme data as `toml::Value`
fn load_toml(&self, path: PathBuf) -> Result<Value> {
let data = std::fs::read_to_string(path)?;
let value = toml::from_str(&data)?;
@ -156,25 +157,35 @@ impl Loader {
Ok(value)
}
// Returns the path to the theme with the name
// With `only_default_dir` as false the path will first search for the user path
// disabled it ignores the user path and returns only the default path
fn path(&self, name: &str, only_default_dir: bool) -> PathBuf {
/// Returns the path to the theme with the given name
///
/// Ignores paths already visited and follows directory priority order.
fn path(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<PathBuf> {
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
if !only_default_dir && user_path.exists() {
user_path
} else {
self.default_dir.join(filename)
}
}
/// Lists all theme names available in default and user directory
pub fn names(&self) -> Vec<String> {
let mut names = Self::read_names(&self.user_dir);
names.extend(Self::read_names(&self.default_dir));
names
let mut cycle_found = false; // track if there was a path, but it was in a cycle
self.theme_dirs
.iter()
.find_map(|dir| {
let path = dir.join(&filename);
if !path.exists() {
None
} else if visited_paths.contains(&path) {
// Avoiding cycle, continuing to look in lower priority directories
cycle_found = true;
None
} else {
visited_paths.insert(path.clone());
Some(path)
}
})
.ok_or_else(|| {
if cycle_found {
anyhow!("Theme: cycle found in inheriting: {}", name)
} else {
anyhow!("Theme: file not found for: {}", name)
}
})
}
pub fn default_theme(&self, true_color: bool) -> Theme {

@ -278,16 +278,15 @@ impl Tree {
self.try_get(index).unwrap()
}
/// Try to get reference to a [View] by index. Returns `None` if node content is not a [Content::View]
/// # Panics
/// Try to get reference to a [View] by index. Returns `None` if node content is not a [`Content::View`].
///
/// Panics if `index` is not in self.nodes. This can be checked with [Self::contains]
/// Does not panic if the view does not exists anymore.
pub fn try_get(&self, index: ViewId) -> Option<&View> {
match &self.nodes[index] {
Node {
match self.nodes.get(index) {
Some(Node {
content: Content::View(view),
..
} => Some(view),
}) => Some(view),
_ => None,
}
}

@ -1,19 +1,21 @@
use crate::{
align_view,
document::DocumentInlayHints,
editor::{GutterConfig, GutterType},
graphics::Rect,
Align, Document, DocumentId, Theme, ViewId,
};
use helix_core::{
char_idx_at_visual_offset, doc_formatter::TextFormat, text_annotations::TextAnnotations,
visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection,
Transaction,
char_idx_at_visual_offset, doc_formatter::TextFormat, syntax::Highlight,
text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block,
Position, RopeSlice, Selection, Transaction,
};
use std::{
collections::{HashMap, VecDeque},
fmt,
rc::Rc,
};
const JUMP_LIST_CAPACITY: usize = 30;
@ -402,9 +404,50 @@ impl View {
Some(pos)
}
/// Get the text annotations to display in the current view for the given document and theme.
pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations {
// TODO custom annotations for custom views like side by side diffs
doc.text_annotations(theme)
let mut text_annotations = doc.text_annotations(theme);
let DocumentInlayHints {
id: _,
type_inlay_hints,
parameter_inlay_hints,
other_inlay_hints,
padding_before_inlay_hints,
padding_after_inlay_hints,
} = match doc.inlay_hints.get(&self.id) {
Some(doc_inlay_hints) => doc_inlay_hints,
None => return text_annotations,
};
let type_style = theme
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type"))
.map(Highlight);
let parameter_style = theme
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter"))
.map(Highlight);
let other_style = theme
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint"))
.map(Highlight);
let mut add_annotations = |annotations: &Rc<[_]>, style| {
if !annotations.is_empty() {
text_annotations.add_inline_annotations(Rc::clone(annotations), style);
}
};
// Overlapping annotations are ignored apart from the first so the order here is not random:
// types -> parameters -> others should hopefully be the "correct" order for most use cases,
// with the padding coming before and after as expected.
add_annotations(padding_before_inlay_hints, None);
add_annotations(type_inlay_hints, type_style);
add_annotations(parameter_inlay_hints, parameter_style);
add_annotations(other_inlay_hints, other_style);
add_annotations(padding_after_inlay_hints, None);
text_annotations
}
pub fn text_pos_at_screen_coords(

@ -19,6 +19,14 @@ indent = { tab-width = 4, unit = " " }
'"' = '"'
'`' = '`'
[language.config]
inlayHints.bindingModeHints.enable = false
inlayHints.closingBraceHints.minLines = 10
inlayHints.closureReturnTypeHints.enable = "with_block"
inlayHints.discriminantHints.enable = "fieldless"
inlayHints.lifetimeElisionHints.enable = "skip_trivial"
inlayHints.typeHints.hideClosureInitialization = false
[language.debugger]
name = "lldb-vscode"
transport = "stdio"
@ -291,6 +299,14 @@ language-server = { command = "gopls" }
# TODO: gopls needs utf-8 offsets?
indent = { tab-width = 4, unit = "\t" }
[language.config.hints]
assignVariableTypes = true
compositeLiteralFields = true
constantValues = true
functionTypeParameters = true
parameterNames = true
rangeVariableTypes = true
[language.debugger]
name = "go"
transport = "tcp"
@ -382,6 +398,18 @@ comment-token = "//"
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
indent = { tab-width = 2, unit = " " }
[language.config]
hostInfo = "helix"
[language.config.javascript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[language.debugger]
name = "node-debug2"
transport = "stdio"
@ -409,6 +437,18 @@ language-server = { command = "typescript-language-server", args = ["--stdio"],
indent = { tab-width = 2, unit = " " }
grammar = "javascript"
[language.config]
hostInfo = "helix"
[language.config.javascript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[language]]
name = "typescript"
scope = "source.ts"
@ -420,6 +460,18 @@ roots = []
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
indent = { tab-width = 2, unit = " " }
[language.config]
hostInfo = "helix"
[language.config.typescript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[grammar]]
name = "typescript"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
@ -434,6 +486,18 @@ roots = []
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
indent = { tab-width = 2, unit = " " }
[language.config]
hostInfo = "helix"
[language.config.typescript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[grammar]]
name = "tsx"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@ -740,6 +804,14 @@ comment-token = "--"
indent = { tab-width = 2, unit = " " }
language-server = { command = "lua-language-server", args = [] }
[language.config.Lua.hint]
enable = true
arrayIndex = "Enable"
setType = true
paramName = "All"
paramType = true
await = true
[[grammar]]
name = "lua"
source = { git = "https://github.com/MunifTanjim/tree-sitter-lua", rev = "887dfd4e83c469300c279314ff1619b1d0b85b91" }
@ -2206,7 +2278,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "yuck"
source = { git = "https://github.com/Philipp-M/tree-sitter-yuck", rev = "9e97da5773f82123a8c8cccf8f7e795d140ed7d1" }
source = { git = "https://github.com/Philipp-M/tree-sitter-yuck", rev = "e3d91a3c65decdea467adebe4127b8366fa47919" }
[[language]]
name = "prql"

@ -1,5 +1,6 @@
[
(compound_statement)
(declaration_list)
(field_declaration_list)
(enumerator_list)
(parameter_list)

@ -4,3 +4,22 @@
((comment) @injection.content
(#set! injection.language "comment"))
((function_call_expression
function: (name) @_function
arguments: (arguments . (argument (_ (string_value) @injection.content))))
(#match? @_function "^preg_")
(#set! injection.language "regex"))
((function_call_expression
function: (name) @_function
arguments: (arguments (_) (argument (_ (string_value) @injection.content))))
(#match? @_function "^mysqli_")
(#set! injection.language "sql"))
((member_call_expression
object: (_)
name: (name) @_function
arguments: (arguments . (argument (_ (string_value) @injection.content))))
(#match? @_function "^(prepare|query)$")
(#set! injection.language "sql"))

@ -51,3 +51,30 @@
[(string_literal) (raw_string_literal)] @injection.content
)
(#set! injection.language "sql"))
; Highlight SQL in `sqlx::query_unchecked!()`
(macro_invocation
macro: (scoped_identifier
path: (identifier) @_sqlx (#eq? @_sqlx "sqlx")
name: (identifier) @_query_as (#eq? @_query_as "query_unchecked"))
(token_tree
; Only the first argument is SQL
.
[(string_literal) (raw_string_literal)] @injection.content
)
(#set! injection.language "sql"))
; Highlight SQL in `sqlx::query_as_unchecked!()`
(macro_invocation
macro: (scoped_identifier
path: (identifier) @_sqlx (#eq? @_sqlx "sqlx")
name: (identifier) @_query_as (#eq? @_query_as "query_as_unchecked"))
(token_tree
; Only the second argument is SQL
.
; Allow anything as the first argument in case the user has lower case type
; names for some reason
(_)
[(string_literal) (raw_string_literal)] @injection.content
)
(#set! injection.language "sql"))

@ -1,66 +1,107 @@
; Errors
(ERROR) @error
(line_comment) @comment
; Comments
; keywords and symbols
(comment) @comment
(keyword) @keyword
(symbol) @tag
; Operators
; literals
[
"+"
"-"
"*"
"/"
"%"
"||"
"&&"
"=="
"!="
"=~"
">"
"<"
">="
"<="
"!"
"?."
"?:"
] @operator
(bool_literal) @constant.builtin.boolean
(num_literal) @constant.numeric
(ternary_expression
["?" ":"] @operator)
; strings
(string_interpolation
(string_interpolation_start) @punctuation.special
(string_interpolation_end) @punctuation.special)
; Punctuation
[ ":" "." "," ] @punctuation.delimiter
[ "{" "}" "[" "]" "(" ")" ] @punctuation.bracket
; Literals
(number (float)) @constant.numeric.float
(number (integer)) @constant.numeric.integer
(boolean) @constant.builtin.boolean
; Strings
(escape_sequence) @constant.character.escape
(string
[
(unescaped_single_quote_string_fragment)
(unescaped_double_quote_string_fragment)
(unescaped_backtick_string_fragment)
"\""
"'"
"`"
]) @string
(string_interpolation
"${" @punctuation.special
"}" @punctuation.special)
; operators and general punctuation
[ (string_fragment) "\"" "'" "`" ] @string
(unary_expression
operator: _ @operator)
; Attributes & Fields
(binary_expression
operator: _ @operator)
(keyword) @attribute
(ternary_expression
operator: _ @operator)
; Functions
[
":"
"."
","
] @punctuation.delimiter
(function_call
name: (ident) @function)
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
[
":"
"."
","
] @punctuation.delimiter
; Variables
; Rest (general identifiers that are not yet catched)
(ident) @variable
(array
(symbol) @variable)
; Builtin widgets
(list .
((symbol) @tag.builtin
(#match? @tag.builtin "^(box|button|calendar|centerbox|checkbox|circular-progress|color-button|color-chooser|combo-box-text|eventbox|expander|graph|image|input|label|literal|overlay|progress|revealer|scale|scroll|transform)$")))
; Keywords
; I think there's a bug in tree-sitter the anchor doesn't seem to be working, see
; https://github.com/tree-sitter/tree-sitter/pull/2107
(list .
((symbol) @keyword
(#match? @keyword "^(defwindow|defwidget|defvar|defpoll|deflisten|geometry|children|struts)$")))
(list .
((symbol) @keyword.control.import
(#eq? @keyword.control.import "include")))
; Loop
(loop_widget . "for" @keyword.control.repeat . (symbol) @variable . "in" @keyword.operator . (symbol) @variable)
(loop_widget . "for" @keyword.control.repeat . (symbol) @variable . "in" @keyword.operator)
; Tags
; TODO apply to every symbol in list? I think it should probably only be applied to the first child of the list
(list
(symbol) @tag)
; Other stuff that has not been catched by the previous queries yet
(index) @variable
(ident) @variable
(index) @variable

@ -1,2 +1,2 @@
((line_comment) @injection.content
((comment) @injection.content
(#set! injection.language "comment"))

@ -47,6 +47,8 @@
"ui.help" = { fg = "my_gray6", bg = "my_gray2" }
"ui.virtual.whitespace" = { fg = "my_gray5" }
"ui.virtual.ruler" = { bg = "my_gray1" }
"ui.virtual.inlay-hint.parameter" = { fg = "my_gray4", modifiers = ["normal"] }
"ui.virtual.inlay-hint.type" = { fg = "my_gray4", modifiers = ["italic"] }
"markup.heading" = "my_yellow1"
"markup.list" = "my_white2"

@ -89,6 +89,7 @@
"ui.virtual" = "overlay0"
"ui.virtual.ruler" = { bg = "surface0" }
"ui.virtual.indent-guide" = "surface0"
"ui.virtual.inlay-hint" = { fg = "surface1", bg = "mantle" }
"ui.selection" = { bg = "surface1" }

@ -89,6 +89,7 @@
"ui.virtual.whitespace" = { fg = "dark_gray" }
"ui.virtual.ruler" = { bg = "borders" }
"ui.virtual.indent-guide" = { fg = "dark_gray4" }
"ui.virtual.inlay-hint" = { fg = "white", bg = "#444444" }
"warning" = { fg = "gold2" }
"error" = { fg = "red" }

@ -92,6 +92,7 @@
"ui.text.focus" = { fg = "White", bg = "Blue 40" }
"ui.virtual" = "Gray 80" # .whitespace
"ui.virtual.inlay-hint" = "Purple 20"
# "ui.virtual.ruler" = { bg = "darker"}
"hint" = "Gray 80"

@ -1,28 +1,28 @@
# Author : erasin<erasinoo@gmail.com>
"attribute" = { fg = "yellow" }
"constructor" = { fg = "brown" }
"label" = { fg = "cyan" }
"operator" = { fg = "red" }
"tag" = { fg = "cyan" }
"namespace" = { fg = "blue" }
"special" = { fg = "deep-purple" }
"property" = { fg = "purple" }
"module" = { fg = "cyan" }
"constructor" = { fg = "blue" }
"namespace" = { fg = "blue" }
"module" = { fg = "blue" }
"type" = { fg = "gold" }
"type.builtin" = { fg = "light-blue" }
"type.builtin" = { fg = "yellow" }
"type.enum" = { fg = "cyan" }
"type.enum.variant" = { fg = "cyan" }
"constant" = { fg = "cyan", modifiers = ["bold"] }
"constant.builtin" = { fg = "deep-purple" }
"constant.builtin.boolean" = { fg = "deep-purple" }
"constant.builtin.boolean" = { fg = "purple" }
"constant.character" = { fg = "green" }
"constant.character.escape" = { fg = "brown" }
"constant.numeric" = { fg = "gold" }
"constant.numeric.integer" = { fg = "gold" }
"constant.numeric.float" = { fg = "gold" }
"constant.numeric" = { fg = "brown" }
"constant.numeric.integer" = { fg = "brown" }
"constant.numeric.float" = { fg = "brown" }
"string" = { fg = "green" }
"string.regexp" = { fg = "purple" }
@ -55,7 +55,7 @@
"keyword.control.return" = { fg = "deep-purple", modifiers = ["bold"] }
"keyword.control.exception" = { fg = "purple" }
"keyword.operator" = { fg = "red" }
"keyword.directive" = { fg = "purple" }
"keyword.directive" = { fg = "deep-purple" }
"keyword.function" = { fg = "purple" }
"keyword.storage" = { fg = "purple" }
"keyword.storage.type" = { fg = "purple" }
@ -82,6 +82,7 @@
"markup.list.numbered" = { fg = "light-blue" }
"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
"markup.italic" = { fg = "purple", modifiers = ["italic"] }
"markup.strikethrough" = { modifiers = ["crossed_out"] }
"markup.link" = { fg = "light-blue" }
"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] }
"markup.link.text" = { fg = "light-blue" }
@ -103,7 +104,7 @@
"ui.cursor.normal" = { fg = "white", bg = "grey" }
"ui.cursor.insert" = { fg = "white", bg = "grey" }
"ui.cursor.select" = { fg = "white", bg = "grey" }
"ui.cursor.match" = { bg = "light-white", modifiers = ["bold"] }
"ui.cursor.match" = { bg = "grey-300", modifiers = ["bold"] }
"ui.cursor.primary" = { fg = "white", bg = "black" }
"ui.cursor.primary.normal" = { fg = "white", bg = "black" }
"ui.cursor.primary.insert" = { fg = "red", bg = "black" }
@ -115,11 +116,11 @@
"ui.linenr" = { fg = "grey-500" }
"ui.linenr.selected" = { fg = "black", modifiers = ["bold"] }
"ui.statusline" = { fg = "black", bg = "light-white" }
"ui.statusline" = { fg = "black", bg = "grey-300" }
"ui.statusline.inactive" = { fg = "grey", bg = "grey-200" }
"ui.statusline.normal" = { fg = "light-white", bg = "light-blue" }
"ui.statusline.insert" = { fg = "light-white", bg = "green" }
"ui.statusline.select" = { fg = "light-white", bg = "purple" }
"ui.statusline.normal" = { fg = "grey-300", bg = "light-blue" }
"ui.statusline.insert" = { fg = "grey-300", bg = "green" }
"ui.statusline.select" = { fg = "grey-300", bg = "purple" }
"ui.popup" = { fg = "black", bg = "grey-200" }
"ui.popup.info" = { fg = "black", bg = "grey-200" }
@ -127,23 +128,25 @@
"ui.help" = { fg = "black", bg = "grey-200" }
"ui.text" = { fg = "black" }
"ui.text.focus" = { fg = "red", bg = "light-white", modifiers = ["bold"] }
"ui.text.focus" = { fg = "red", bg = "grey-300", modifiers = ["bold"] }
"ui.text.inactive" = { fg = "grey" }
"ui.text.info" = { fg = "black" }
"ui.virtual" = { fg = "light-white" }
"ui.virtual.ruler" = { bg = "light-white" }
"ui.virtual.wrap" = { bg = "light-white" }
"ui.virtual.whitespace" = { fg = "light-white" }
"ui.virtual" = { fg = "grey-500" }
"ui.virtual.ruler" = { bg = "grey-200" }
"ui.virtual.wrap" = { fg = "grey-500" }
"ui.virtual.whitespace" = { fg = "grey-300" }
"ui.virtual.indent-guide" = { fg = "grey-500" }
"ui.virtual.inlay-hint" = { fg = "grey" }
"ui.virtual.inlay-hint" = { fg = "grey-500" }
"ui.virtual.inlay-hint.parameter" = { fg = "grey-500", modifiers = ["italic"] }
"ui.virtual.inlay-hint.type" = { fg = "grey-500" }
"ui.menu" = { fg = "black", bg = "light-white" }
"ui.menu" = { fg = "black", bg = "grey-300" }
"ui.menu.selected" = { fg = "white", bg = "light-blue" }
"ui.menu.scroll" = { fg = "light-blue", bg = "white" }
"ui.selection" = { bg = "light-white", modifiers = ["dim"] }
"ui.selection.primary" = { bg = "light-white" }
"ui.selection" = { bg = "grey-300", modifiers = ["dim"] }
"ui.selection.primary" = { bg = "grey-300" }
"ui.cursorline.primary" = { fg = "white", bg = "grey-100" }
"ui.cursorline.secondary" = { fg = "white", bg = "grey-200" }
@ -151,7 +154,7 @@
"ui.cursorcolumn.primary" = { fg = "white", bg = "grey-100" }
"ui.cursorcolumn.secondary" = { fg = "white", bg = "grey-200" }
"ui.highlight" = { bg = "light-white" }
"ui.highlight" = { bg = "grey-300" }
"diagnostic.info" = { underline = { color = "blue", style = "dotted" } }
"diagnostic.hint" = { underline = { color = "green", style = "dashed" } }
@ -167,7 +170,7 @@
white = "#FAFAFA"
yellow = "#FF6F00"
gold = "#D35400"
brown = "#4E342E"
brown = "#795548"
blue = "#0061FF"
light-blue = "#0091EA"
red = "#D50000"
@ -177,8 +180,9 @@ deep-purple = "#651FFF"
green = "#24A443"
cyan = "#0086C1"
black = "#282C34"
light-white = "#E3E3E3"
grey = "#5C6370"
grey-100 = "#F3F3F3"
grey-200 = "#EDEDED"
grey-500 = "#9E9E9E"
grey-400 = "#BDBDBD"
grey-300 = "#E0E0E0"
grey-200 = "#EEEEEE"
grey-100 = "#F2F2F2"

Loading…
Cancel
Save