Merge branch 'seperate_code_action'

pull/6/head
trivernis 2 years ago
commit 8b789b6b86
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

18
Cargo.lock generated

@ -523,6 +523,7 @@ dependencies = [
"futures-util", "futures-util",
"helix-core", "helix-core",
"helix-dap", "helix-dap",
"helix-loader",
"helix-lsp", "helix-lsp",
"helix-tui", "helix-tui",
"log", "log",
@ -1024,9 +1025,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.9.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]] [[package]]
name = "smartstring" name = "smartstring"
@ -1111,18 +1112,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.36" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122" checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.36" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1164,9 +1165,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.21.1" version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -1174,7 +1175,6 @@ dependencies = [
"memchr", "memchr",
"mio", "mio",
"num_cpus", "num_cpus",
"once_cell",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",

@ -14,9 +14,6 @@ default-members = [
"helix-term" "helix-term"
] ]
[profile.dev]
split-debuginfo = "unpacked"
[profile.release] [profile.release]
lto = "thin" lto = "thin"
# debug = true # debug = true

@ -28,6 +28,10 @@ hidden = false
You may also specify a file to use for configuration with the `-c` or You may also specify a file to use for configuration with the `-c` or
`--config` CLI argument: `hx -c path/to/custom-config.toml`. `--config` CLI argument: `hx -c path/to/custom-config.toml`.
It is also possible to trigger configuration file reloading by sending the `USR1`
signal to the helix process, e.g. via `pkill -USR1 hx`. This is only supported
on unix operating systems.
## Editor ## Editor
### `[editor]` Section ### `[editor]` Section
@ -68,17 +72,32 @@ left = ["mode", "spinner"]
center = ["file-name"] center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"] right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "│" separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"
``` ```
The `[editor.statusline]` key takes the following sub-keys:
| Key | Description | Default |
| --- | --- | --- |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name"]` |
| `center` | A list of elements aligned to the middle of the statusline | `[]` |
| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "position", "file-encoding"]` |
| `separator` | The character used to separate elements in the statusline | `"│"` |
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
The following elements can be configured: The following statusline elements can be configured:
| Key | Description | | Key | Description |
| ------ | ----------- | | ------ | ----------- |
| `mode` | The current editor mode (`NOR`/`INS`/`SEL`) | | `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
| `spinner` | A progress spinner indicating LSP activity | | `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file | | `file-name` | The path/name of the opened file |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 | | `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) | | `file-line-ending` | The file line endings (CRLF or LF) |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file | | `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors | | `diagnostics` | The number of warnings and/or errors |
| `selections` | The number of active selections | | `selections` | The number of active selections |
@ -232,11 +251,24 @@ Sets explorer side width and style.
Options for rendering vertical indent guides. Options for rendering vertical indent guides.
<<<<<<< HEAD
| Key | Description | Default | | Key | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `render` | Whether to render indent guides. | `false` | | `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` | | `character` | Literal character to use for rendering the indent guide | `│` |
| `rainbow` | Whether or not the indent guides shall have changing colors. | `false` | | `rainbow` | Whether or not the indent guides shall have changing colors. | `false` |
||||||| 60aa7d36
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
=======
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `skip-levels` | Number of indent levels to skip | `0` |
>>>>>>> seperate_code_action
Example: Example:
@ -244,7 +276,12 @@ Example:
[editor.indent-guides] [editor.indent-guides]
render = true render = true
character = "╎" character = "╎"
<<<<<<< HEAD
rainbow = true rainbow = true
||||||| 60aa7d36
=======
skip-levels = 1
>>>>>>> seperate_code_action
``` ```
### `[editor.explorer]` Section ### `[editor.explorer]` Section

@ -119,6 +119,8 @@
| vala | ✓ | | | `vala-language-server` | | vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` | | verilog | ✓ | ✓ | | `svlangserver` |
| vue | ✓ | | | `vls` | | vue | ✓ | | | `vls` |
| wast | ✓ | | | |
| wat | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` | | wgsl | ✓ | | | `wgsl_analyzer` |
| xit | ✓ | | | | | xit | ✓ | | | |
| yaml | ✓ | | ✓ | `yaml-language-server` | | yaml | ✓ | | ✓ | `yaml-language-server` |

@ -68,8 +68,8 @@
| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` | | `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| `i` | Insert before selection | `insert_mode` | | `i` | Insert before selection | `insert_mode` |
| `a` | Insert after selection (append) | `append_mode` | | `a` | Insert after selection (append) | `append_mode` |
| `I` | Insert at the start of the line | `prepend_to_line` | | `I` | Insert at the start of the line | `insert_at_line_start` |
| `A` | Insert at the end of the line | `append_to_line` | | `A` | Insert at the end of the line | `insert_at_line_end` |
| `o` | Open new line below selection | `open_below` | | `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` | | `O` | Open new line above selection | `open_above` |
| `.` | Repeat last insert | N/A | | `.` | Repeat last insert | N/A |
@ -129,6 +129,7 @@
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` | | `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` |
| `J` | Join lines inside selection | `join_selections` | | `J` | Join lines inside selection | `join_selections` |
| `A-J` | Join lines inside selection and select space | `join_selections_space` |
| `K` | Keep selections matching the regex | `keep_selections` | | `K` | Keep selections matching the regex | `keep_selections` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` | | `Alt-K` | Remove selections matching the regex | `remove_selections` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
@ -315,7 +316,7 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| `[space` | Add newline above | `add_newline_above` | | `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` | | `]space` | Add newline below | `add_newline_below` |
## Insert Mode ## Insert mode
Insert mode bindings are somewhat minimal by default. Helix is designed to Insert mode bindings are somewhat minimal by default. Helix is designed to
be a modal editor, and this is reflected in the user experience and internal be a modal editor, and this is reflected in the user experience and internal
@ -324,44 +325,47 @@ escaping from insert mode to normal mode. For this reason, new users are
strongly encouraged to learn the modal editing paradigm to get the smoothest strongly encouraged to learn the modal editing paradigm to get the smoothest
experience. experience.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` | | `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` | | `Ctrl-s` | Commit undo checkpoint | `commit_undo_checkpoint` |
| `Ctrl-r` | Insert a register content | `insert_register` | | `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-w`, `Alt-Backspace`, `Ctrl-Backspace` | Delete previous word | `delete_word_backward` | | `Ctrl-r` | Insert a register content | `insert_register` |
| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word | `delete_word_forward` | | `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` |
| `Ctrl-u` | Delete to start of line | `kill_to_line_start` | | `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` |
| `Ctrl-k` | Delete to end of line | `kill_to_line_end` | | `Ctrl-u` | Delete to start of line | `kill_to_line_start` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | | `Ctrl-k` | Delete to end of line | `kill_to_line_end` |
| `Backspace`, `Ctrl-h` | Delete previous char | `delete_char_backward` | | `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` |
| `Delete`, `Ctrl-d` | Delete next char | `delete_char_forward` | | `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` |
However, if you really want navigation in insert mode, this is supported. An
example config that gives the ability to use arrow keys while still in insert These keys are not recommended, but are included for new users less familiar
mode: with modal editors.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Up` | Move to previous line | `move_line_up` |
| `Down` | Move to next line | `move_line_down` |
| `Left` | Backward a char | `move_char_left` |
| `Right` | Forward a char | `move_char_right` |
| `PageUp` | Move one page up | `page_up` |
| `PageDown` | Move one page down | `page_down` |
| `Home` | Move to line start | `goto_line_start` |
| `End` | Move to line end | `goto_line_end_newline` |
If you want to disable them in insert mode as you become more comfortable with modal editing, you can use
the following in your `config.toml`:
```toml ```toml
[keys.insert] [keys.insert]
"up" = "move_line_up" up = "no_op"
"down" = "move_line_down" down = "no_op"
"left" = "move_char_left" left = "no_op"
"right" = "move_char_right" right = "no_op"
"C-b" = "move_char_left" pageup = "no_op"
"C-f" = "move_char_right" pagedown = "no_op"
"A-b" = "move_prev_word_end" home = "no_op"
"C-left" = "move_prev_word_end" end = "no_op"
"A-f" = "move_next_word_start"
"C-right" = "move_next_word_start"
"A-<" = "goto_file_start"
"A->" = "goto_file_end"
"pageup" = "page_up"
"pagedown" = "page_down"
"home" = "goto_line_start"
"C-a" = "goto_line_start"
"end" = "goto_line_end_newline"
"C-e" = "goto_line_end_newline"
"A-left" = "goto_line_start"
``` ```
## Select / extend mode ## Select / extend mode

@ -89,6 +89,7 @@ Less common modifiers might not be supported by your terminal emulator.
| `hidden` | | `hidden` |
| `crossed_out` | | `crossed_out` |
<<<<<<< HEAD
### Rainbow ### Rainbow
The `rainbow` key is used for rainbow highlight for matching brackets. The `rainbow` key is used for rainbow highlight for matching brackets.
@ -100,6 +101,24 @@ rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold
Colors from the palette and modifiers may be used. Colors from the palette and modifiers may be used.
||||||| 60aa7d36
=======
### Inheritance
Extend upon other themes by setting the `inherits` property to an existing theme.
```toml
inherits = "boo_berry"
# Override the theming for "keyword"s:
"keyword" = { fg = "gold" }
# Override colors in the palette:
[palette]
berry = "#2A2A4D"
```
>>>>>>> seperate_code_action
### Scopes ### Scopes
The following is a list of scopes available to use for styling. The following is a list of scopes available to use for styling.
@ -230,6 +249,8 @@ These scopes are used for theming the editor interface.
| `ui.cursor.select` | | | `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. | | `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection | | `ui.cursor.primary` | Cursor with primary selection |
| `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.linenr` | Line numbers | | `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline | | `ui.statusline` | Statusline |

@ -18,7 +18,7 @@ integration = []
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.5", default-features = false, features = ["simd"] } ropey = { version = "1.5", default-features = false, features = ["simd"] }
smallvec = "1.9" smallvec = "1.10"
smartstring = "1.0.1" smartstring = "1.0.1"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
unicode-width = "0.1" unicode-width = "0.1"

@ -389,6 +389,8 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
} }
} }
/// Finds the range of the next or previous textobject in the syntax sub-tree of `node`.
/// Returns the range in the forwards direction.
pub fn goto_treesitter_object( pub fn goto_treesitter_object(
slice: RopeSlice, slice: RopeSlice,
range: Range, range: Range,
@ -419,8 +421,8 @@ pub fn goto_treesitter_object(
.filter(|n| n.start_byte() > byte_pos) .filter(|n| n.start_byte() > byte_pos)
.min_by_key(|n| n.start_byte())?, .min_by_key(|n| n.start_byte())?,
Direction::Backward => nodes Direction::Backward => nodes
.filter(|n| n.start_byte() < byte_pos) .filter(|n| n.end_byte() < byte_pos)
.max_by_key(|n| n.start_byte())?, .max_by_key(|n| n.end_byte())?,
}; };
let len = slice.len_bytes(); let len = slice.len_bytes();
@ -434,7 +436,7 @@ pub fn goto_treesitter_object(
let end_char = slice.byte_to_char(end_byte); let end_char = slice.byte_to_char(end_byte);
// head of range should be at beginning // head of range should be at beginning
Some(Range::new(end_char, start_char)) Some(Range::new(start_char, end_char))
}; };
(0..count).fold(range, |range, _| get_range(range).unwrap_or(range)) (0..count).fold(range, |range, _| get_range(range).unwrap_or(range))
} }

@ -122,7 +122,7 @@ impl Range {
} }
} }
// flips the direction of the selection /// Flips the direction of the selection
pub fn flip(&self) -> Self { pub fn flip(&self) -> Self {
Self { Self {
anchor: self.head, anchor: self.head,
@ -131,6 +131,16 @@ impl Range {
} }
} }
/// Returns the selection if it goes in the direction of `direction`,
/// flipping the selection otherwise.
pub fn with_direction(self, direction: Direction) -> Self {
if self.direction() == direction {
self
} else {
self.flip()
}
}
/// Check two ranges for overlap. /// Check two ranges for overlap.
#[must_use] #[must_use]
pub fn overlaps(&self, other: &Self) -> bool { pub fn overlaps(&self, other: &Self) -> bool {

@ -49,6 +49,7 @@ impl Client {
root_markers: &[String], root_markers: &[String],
id: usize, id: usize,
req_timeout: u64, req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> { ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
// Resolve path to the binary // Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
@ -72,7 +73,10 @@ impl Client {
let (server_rx, server_tx, initialize_notify) = let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id); Transport::start(reader, writer, stderr, id);
let root_path = find_root(None, root_markers); let root_path = find_root(
doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())),
root_markers,
);
let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();

@ -14,6 +14,7 @@ use tokio::sync::mpsc::UnboundedReceiver;
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{hash_map::Entry, HashMap},
path::PathBuf,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Arc,
@ -206,6 +207,20 @@ pub mod util {
// in reverse order. // in reverse order.
edits.sort_unstable_by_key(|edit| edit.range.start); edits.sort_unstable_by_key(|edit| edit.range.start);
// Generate a diff if the edit is a full document replacement.
#[allow(clippy::collapsible_if)]
if edits.len() == 1 {
let is_document_replacement = edits.first().and_then(|edit| {
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding)?;
Some(start..end)
}) == Some(0..doc.len_chars());
if is_document_replacement {
let new_text = Rope::from(edits.pop().unwrap().new_text);
return helix_core::diff::compare_ropes(doc, &new_text);
}
}
Transaction::change( Transaction::change(
doc, doc,
edits.into_iter().map(|edit| { edits.into_iter().map(|edit| {
@ -338,7 +353,11 @@ impl Registry {
.map(|(_, client)| client.as_ref()) .map(|(_, client)| client.as_ref())
} }
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Option<Arc<Client>>> { pub fn get(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server { let config = match &language_config.language_server {
Some(config) => config, Some(config) => config,
None => return Ok(None), None => return Ok(None),
@ -350,7 +369,8 @@ impl Registry {
// initialize a new client // initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = start_client(id, language_config, config)?; let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
entry.insert((id, client.clone())); entry.insert((id, client.clone()));
@ -359,7 +379,11 @@ impl Registry {
} }
} }
pub fn restart(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> { pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
path: Option<&PathBuf>,
) -> Result<Arc<Client>> {
let config = language_config let config = language_config
.language_server .language_server
.as_ref() .as_ref()
@ -369,7 +393,7 @@ impl Registry {
.get(&language_config.scope) .get(&language_config.scope)
.ok_or(Error::LspNotDefined)? .ok_or(Error::LspNotDefined)?
.0; .0;
let new_client = self.initialize_client(language_config, config, id)?; let new_client = self.initialize_client(language_config, config, id, path)?;
let (_, client) = self let (_, client) = self
.inner .inner
.get_mut(&language_config.scope) .get_mut(&language_config.scope)
@ -384,6 +408,7 @@ impl Registry {
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
config: &helix_core::syntax::LanguageServerConfiguration, config: &helix_core::syntax::LanguageServerConfiguration,
id: usize, id: usize,
path: Option<&PathBuf>,
) -> Result<Arc<Client>> { ) -> Result<Arc<Client>> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&config.command, &config.command,
@ -392,6 +417,7 @@ impl Registry {
&language_config.roots, &language_config.roots,
id, id,
config.timeout, config.timeout,
path,
)?; )?;
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client); let client = Arc::new(client);
@ -517,6 +543,7 @@ fn start_client(
id: usize, id: usize,
config: &LanguageConfiguration, config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration, ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<NewClientResult> { ) -> Result<NewClientResult> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&ls_config.command, &ls_config.command,
@ -525,6 +552,7 @@ fn start_client(
&config.roots, &config.roots,
id, id,
ls_config.timeout, ls_config.timeout,
doc_path,
)?; )?;
let client = Arc::new(client); let client = Arc::new(client);

@ -74,6 +74,6 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
[dev-dependencies] [dev-dependencies]
smallvec = "1.9" smallvec = "1.10"
indoc = "1.0.6" indoc = "1.0.6"
tempfile = "3.3.0" tempfile = "3.3.0"

@ -236,8 +236,8 @@ impl Application {
#[cfg(windows)] #[cfg(windows)]
let signals = futures_util::stream::empty(); let signals = futures_util::stream::empty();
#[cfg(not(windows))] #[cfg(not(windows))]
let signals = let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1])
Signals::new(&[signal::SIGTSTP, signal::SIGCONT]).context("build signal handler")?; .context("build signal handler")?;
let app = Self { let app = Self {
compositor, compositor,
@ -438,6 +438,10 @@ impl Application {
self.compositor.load_cursor(); self.compositor.load_cursor();
self.render(); self.render();
} }
signal::SIGUSR1 => {
self.refresh_config();
self.render();
}
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -878,9 +882,16 @@ impl Application {
})); }));
self.event_loop(input_stream).await; self.event_loop(input_stream).await;
self.close().await?;
let err = self.close().await.err();
restore_term()?; restore_term()?;
if let Some(err) = err {
self.editor.exit_code = 1;
eprintln!("Error: {}", err);
}
Ok(self.editor.exit_code) Ok(self.editor.exit_code)
} }

@ -274,8 +274,8 @@ impl MappableCommand {
diagnostics_picker, "Open diagnostic picker", diagnostics_picker, "Open diagnostic picker",
workspace_diagnostics_picker, "Open workspace diagnostic picker", workspace_diagnostics_picker, "Open workspace diagnostic picker",
last_picker, "Open last picker", last_picker, "Open last picker",
prepend_to_line, "Insert at start of line", insert_at_line_start, "Insert at start of line",
append_to_line, "Append to end of line", insert_at_line_end, "Insert at end of line",
open_below, "Open new line below selection", open_below, "Open new line below selection",
open_above, "Open new line above selection", open_above, "Open new line above selection",
normal_mode, "Enter normal mode", normal_mode, "Enter normal mode",
@ -347,6 +347,7 @@ impl MappableCommand {
unindent, "Unindent selection", unindent, "Unindent selection",
format_selections, "Format selection", format_selections, "Format selection",
join_selections, "Join lines inside selection", join_selections, "Join lines inside selection",
join_selections_space, "Join lines inside selection and select spaces",
keep_selections, "Keep selections matching regex", keep_selections, "Keep selections matching regex",
remove_selections, "Remove selections matching regex", remove_selections, "Remove selections matching regex",
align_selections, "Align selections in column", align_selections, "Align selections in column",
@ -889,8 +890,12 @@ fn goto_window(cx: &mut Context, align: Align) {
.min(last_line.saturating_sub(scrolloff)); .min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line); let pos = doc.text().line_to_char(line);
let text = doc.text().slice(..);
doc.set_selection(view.id, Selection::point(pos)); let selection = doc
.selection(view.id)
.clone()
.transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select));
doc.set_selection(view.id, selection);
} }
fn goto_window_top(cx: &mut Context) { fn goto_window_top(cx: &mut Context) {
@ -1515,7 +1520,8 @@ fn select_regex(cx: &mut Context) {
"select:".into(), "select:".into(),
Some(reg), Some(reg),
ui::completers::none, ui::completers::none,
move |view, doc, regex, event| { move |editor, regex, event| {
let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return; return;
} }
@ -1536,7 +1542,8 @@ fn split_selection(cx: &mut Context) {
"split:".into(), "split:".into(),
Some(reg), Some(reg),
ui::completers::none, ui::completers::none,
move |view, doc, regex, event| { move |editor, regex, event| {
let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return; return;
} }
@ -1560,15 +1567,16 @@ fn split_selection_on_newline(cx: &mut Context) {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn search_impl( fn search_impl(
doc: &mut Document, editor: &mut Editor,
view: &mut View,
contents: &str, contents: &str,
regex: &Regex, regex: &Regex,
movement: Movement, movement: Movement,
direction: Direction, direction: Direction,
scrolloff: usize, scrolloff: usize,
wrap_around: bool, wrap_around: bool,
show_warnings: bool,
) { ) {
let (view, doc) = current!(editor);
let text = doc.text().slice(..); let text = doc.text().slice(..);
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
@ -1598,17 +1606,29 @@ fn search_impl(
Direction::Backward => regex.find_iter(&contents[..start]).last(), Direction::Backward => regex.find_iter(&contents[..start]).last(),
}; };
if wrap_around && mat.is_none() { if mat.is_none() {
mat = match direction { if wrap_around {
Direction::Forward => regex.find(contents), mat = match direction {
Direction::Backward => { Direction::Forward => regex.find(contents),
offset = start; Direction::Backward => {
regex.find_iter(&contents[start..]).last() offset = start;
regex.find_iter(&contents[start..]).last()
}
};
}
if show_warnings {
if wrap_around && mat.is_some() {
editor.set_status("Wrapped around document");
} else {
editor.set_error("No more matches");
} }
} }
// TODO: message on wraparound
} }
let (view, doc) = current!(editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
if let Some(mat) = mat { if let Some(mat) = mat {
let start = text.byte_to_char(mat.start() + offset); let start = text.byte_to_char(mat.start() + offset);
let end = text.byte_to_char(mat.end() + offset); let end = text.byte_to_char(mat.end() + offset);
@ -1684,19 +1704,19 @@ fn searcher(cx: &mut Context, direction: Direction) {
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect() .collect()
}, },
move |view, doc, regex, event| { move |editor, regex, event| {
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return; return;
} }
search_impl( search_impl(
doc, editor,
view,
&contents, &contents,
&regex, &regex,
Movement::Move, Movement::Move,
direction, direction,
scrolloff, scrolloff,
wrap_around, wrap_around,
false,
); );
}, },
); );
@ -1706,7 +1726,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
let count = cx.count(); let count = cx.count();
let config = cx.editor.config(); let config = cx.editor.config();
let scrolloff = config.scrolloff; let scrolloff = config.scrolloff;
let (view, doc) = current!(cx.editor); let (_, doc) = current!(cx.editor);
let registers = &cx.editor.registers; let registers = &cx.editor.registers;
if let Some(query) = registers.read('/').and_then(|query| query.last()) { if let Some(query) = registers.read('/').and_then(|query| query.last()) {
let contents = doc.text().slice(..).to_string(); let contents = doc.text().slice(..).to_string();
@ -1724,14 +1744,14 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
{ {
for _ in 0..count { for _ in 0..count {
search_impl( search_impl(
doc, cx.editor,
view,
&contents, &contents,
&regex, &regex,
movement, movement,
direction, direction,
scrolloff, scrolloff,
wrap_around, wrap_around,
true,
); );
} }
} else { } else {
@ -1829,7 +1849,7 @@ fn global_search(cx: &mut Context) {
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect() .collect()
}, },
move |_view, _doc, regex, event| { move |_editor, regex, event| {
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return; return;
} }
@ -2165,10 +2185,7 @@ fn ensure_selections_forward(cx: &mut Context) {
let selection = doc let selection = doc
.selection(view.id) .selection(view.id)
.clone() .clone()
.transform(|r| match r.direction() { .transform(|r| r.with_direction(Direction::Forward));
Direction::Forward => r,
Direction::Backward => r.flip(),
});
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
} }
@ -2451,11 +2468,11 @@ impl ui::menu::Item for MappableCommand {
match self { match self {
MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) {
Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
None => doc.as_str().into(), None => doc.as_str().into(),
}, },
MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
None => (*doc).into(), None => (*doc).into(),
}, },
} }
@ -2506,13 +2523,13 @@ fn last_picker(cx: &mut Context) {
} }
// I inserts at the first nonwhitespace character of each line with a selection // I inserts at the first nonwhitespace character of each line with a selection
fn prepend_to_line(cx: &mut Context) { fn insert_at_line_start(cx: &mut Context) {
goto_first_nonwhitespace(cx); goto_first_nonwhitespace(cx);
enter_insert_mode(cx); enter_insert_mode(cx);
} }
// A inserts at the end of each line with a selection // A inserts at the end of each line with a selection
fn append_to_line(cx: &mut Context) { fn insert_at_line_end(cx: &mut Context) {
enter_insert_mode(cx); enter_insert_mode(cx);
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -2545,18 +2562,23 @@ async fn make_format_callback(
) -> anyhow::Result<job::Callback> { ) -> anyhow::Result<job::Callback> {
let format = format.await?; let format = format.await?;
let call: job::Callback = Box::new(move |editor, _compositor| { let call: job::Callback = Box::new(move |editor, _compositor| {
let view_id = view!(editor).id; if !editor.documents.contains_key(&doc_id) {
if let Some(doc) = editor.document_mut(doc_id) { return;
if doc.version() == doc_version { }
doc.apply(&format, view_id);
doc.append_changes_to_history(view_id); let scrolloff = editor.config().scrolloff;
doc.detect_indent_and_line_ending(); let doc = doc_mut!(editor, &doc_id);
if let Modified::SetUnmodified = modified { let view = view_mut!(editor);
doc.reset_modified(); if doc.version() == doc_version {
} doc.apply(&format, view.id);
} else { doc.append_changes_to_history(view.id);
log::info!("discarded formatting changes because the document changed"); doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
if let Modified::SetUnmodified = modified {
doc.reset_modified();
} }
} else {
log::info!("discarded formatting changes because the document changed");
} }
}); });
Ok(call) Ok(call)
@ -3772,7 +3794,7 @@ fn format_selections(cx: &mut Context) {
} }
} }
fn join_selections(cx: &mut Context) { fn join_selections_inner(cx: &mut Context, select_space: bool) {
use movement::skip_while; use movement::skip_while;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let text = doc.text(); let text = doc.text();
@ -3807,9 +3829,21 @@ fn join_selections(cx: &mut Context) {
// TODO: joining multiple empty lines should be replaced by a single space. // TODO: joining multiple empty lines should be replaced by a single space.
// need to merge change ranges that touch // need to merge change ranges that touch
let transaction = Transaction::change(doc.text(), changes.into_iter()); // select inserted spaces
// TODO: select inserted spaces let transaction = if select_space {
// .with_selection(selection); let ranges: SmallVec<_> = changes
.iter()
.scan(0, |offset, change| {
let range = Range::point(change.0 - *offset);
*offset += change.1 - change.0 - 1; // -1 because cursor is 0-sized
Some(range)
})
.collect();
let selection = Selection::new(ranges, 0);
Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
} else {
Transaction::change(doc.text(), changes.into_iter())
};
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }
@ -3822,7 +3856,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
if remove { "remove:" } else { "keep:" }.into(), if remove { "remove:" } else { "keep:" }.into(),
Some(reg), Some(reg),
ui::completers::none, ui::completers::none,
move |view, doc, regex, event| { move |editor, regex, event| {
let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return; return;
} }
@ -3837,6 +3872,14 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
) )
} }
fn join_selections(cx: &mut Context) {
join_selections_inner(cx, false)
}
fn join_selections_space(cx: &mut Context) {
join_selections_inner(cx, true)
}
fn keep_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) {
keep_or_remove_selections_impl(cx, false) keep_or_remove_selections_impl(cx, false)
} }
@ -4345,7 +4388,7 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
let root = syntax.tree().root_node(); let root = syntax.tree().root_node();
let selection = doc.selection(view.id).clone().transform(|range| { let selection = doc.selection(view.id).clone().transform(|range| {
movement::goto_treesitter_object( let new_range = movement::goto_treesitter_object(
text, text,
range, range,
object, object,
@ -4353,7 +4396,19 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
root, root,
lang_config, lang_config,
count, count,
) );
if editor.mode == Mode::Select {
let head = if new_range.head < range.anchor {
new_range.anchor
} else {
new_range.head
};
Range::new(range.anchor, head)
} else {
new_range.with_direction(direction)
}
}); });
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
@ -4418,7 +4473,6 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
cx.on_next_key(move |cx, event| { cx.on_next_key(move |cx, event| {
cx.editor.autoinfo = None; cx.editor.autoinfo = None;
cx.editor.pseudo_pending = None;
if let Some(ch) = event.char() { if let Some(ch) = event.char() {
let textobject = move |editor: &mut Editor| { let textobject = move |editor: &mut Editor| {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
@ -4467,33 +4521,31 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
} }
}); });
if let Some((title, abbrev)) = match objtype { let title = match objtype {
textobject::TextObject::Inside => Some(("Match inside", "mi")), textobject::TextObject::Inside => "Match inside",
textobject::TextObject::Around => Some(("Match around", "ma")), textobject::TextObject::Around => "Match around",
_ => return, _ => return,
} {
let help_text = [
("w", "Word"),
("W", "WORD"),
("p", "Paragraph"),
("c", "Class (tree-sitter)"),
("f", "Function (tree-sitter)"),
("a", "Argument/parameter (tree-sitter)"),
("o", "Comment (tree-sitter)"),
("t", "Test (tree-sitter)"),
("m", "Closest surrounding pair to cursor"),
(" ", "... or any character acting as a pair"),
];
cx.editor.autoinfo = Some(Info::new(
title,
help_text
.into_iter()
.map(|(col1, col2)| (col1.to_string(), col2.to_string()))
.collect(),
));
cx.editor.pseudo_pending = Some(abbrev.to_string());
}; };
let help_text = [
("w", "Word"),
("W", "WORD"),
("p", "Paragraph"),
("c", "Class (tree-sitter)"),
("f", "Function (tree-sitter)"),
("a", "Argument/parameter (tree-sitter)"),
("o", "Comment (tree-sitter)"),
("t", "Test (tree-sitter)"),
("m", "Closest surrounding pair to cursor"),
(" ", "... or any character acting as a pair"),
];
cx.editor.autoinfo = Some(Info::new(
title,
help_text
.into_iter()
.map(|(col1, col2)| (col1.to_string(), col2.to_string()))
.collect(),
));
} }
fn surround_add(cx: &mut Context) { fn surround_add(cx: &mut Context) {

@ -452,43 +452,73 @@ pub fn code_action(cx: &mut Context) {
cx.callback( cx.callback(
future, future,
move |editor, compositor, response: Option<lsp::CodeActionResponse>| { move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
let actions = match response { let mut actions = match response {
Some(a) => a, Some(a) => a,
None => return, None => return,
}; };
if actions.is_empty() { if actions.is_empty() {
editor.set_status("No code actions available"); editor.set_status("No code actions available");
return; return;
} }
let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { // sort by CodeActionKind
if event != PromptEvent::Validate { // this ensures that the most relevant codeactions (quickfix) show up first
return; // while more situational commands (like refactors) show up later
// this behaviour is modeled after the behaviour of vscode (https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts)
actions.sort_by_key(|action| match &action {
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
kind: Some(kind), ..
}) => {
let mut components = kind.as_str().split('.');
match components.next() {
Some("quickfix") => 0,
Some("refactor") => match components.next() {
Some("extract") => 1,
Some("inline") => 2,
Some("rewrite") => 3,
Some("move") => 4,
Some("surround") => 5,
_ => 7,
},
Some("source") => 6,
_ => 7,
}
} }
_ => 7,
});
// always present here let mut picker =
let code_action = code_action.unwrap(); ui::Menu::new(actions, false, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
match code_action { return;
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone());
} }
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit // always present here
// should be applied and then the command let code_action = code_action.unwrap();
if let Some(command) = &code_action.command {
match code_action {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone()); execute_lsp_command(editor, command.clone());
} }
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some(command) = &code_action.command {
execute_lsp_command(editor, command.clone());
}
}
} }
} });
});
picker.move_down(); // pre-select the first item picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker); let popup = Popup::new("code-action", picker);

@ -2,7 +2,7 @@ use std::ops::Deref;
use super::*; use super::*;
use helix_view::editor::{Action, ConfigEvent}; use helix_view::editor::{Action, CloseError, ConfigEvent};
use ui::completers::{self, Completer}; use ui::completers::{self, Completer};
#[derive(Clone)] #[derive(Clone)]
@ -71,8 +71,29 @@ fn buffer_close_by_ids_impl(
doc_ids: &[DocumentId], doc_ids: &[DocumentId],
force: bool, force: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
for &doc_id in doc_ids { let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
editor.close_document(doc_id, force)?; .iter()
.filter_map(|&doc_id| {
if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) {
Some((doc_id, name))
} else {
None
}
})
.unzip();
if let Some(first) = modified_ids.first() {
let current = doc!(editor);
// If the current document is unmodified, and there are modified
// documents, switch focus to the first modified doc.
if !modified_ids.contains(&current.id()) {
editor.switch(*first, Action::Replace);
}
bail!(
"{} unsaved buffer(s) remaining: {:?}",
modified_names.len(),
modified_names
);
} }
Ok(()) Ok(())
@ -538,23 +559,26 @@ fn force_write_quit(
force_quit(cx, &[], event) force_quit(cx, &[], event)
} }
/// Results an error if there are modified buffers remaining and sets editor error, /// Results in an error if there are modified buffers remaining and sets editor
/// otherwise returns `Ok(())` /// error, otherwise returns `Ok(())`. If the current document is unmodified,
/// and there are modified documents, switches focus to one of them.
pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
let modified: Vec<_> = editor let (modified_ids, modified_names): (Vec<_>, Vec<_>) = editor
.documents() .documents()
.filter(|doc| doc.is_modified()) .filter(|doc| doc.is_modified())
.map(|doc| { .map(|doc| (doc.id(), doc.display_name()))
doc.relative_path() .unzip();
.map(|path| path.to_string_lossy().to_string()) if let Some(first) = modified_ids.first() {
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()) let current = doc!(editor);
}) // If the current document is unmodified, and there are modified
.collect(); // documents, switch focus to the first modified doc.
if !modified.is_empty() { if !modified_ids.contains(&current.id()) {
editor.switch(*first, Action::Replace);
}
bail!( bail!(
"{} unsaved buffer(s) remaining: {:?}", "{} unsaved buffer(s) remaining: {:?}",
modified.len(), modified_names.len(),
modified modified_names
); );
} }
Ok(()) Ok(())
@ -1025,7 +1049,7 @@ fn lsp_restart(
.context("LSP not defined for the current document")?; .context("LSP not defined for the current document")?;
let scope = config.scope.clone(); let scope = config.scope.clone();
cx.editor.language_servers.restart(config)?; cx.editor.language_servers.restart(config, doc.path())?;
// This collect is needed because refresh_language_server would need to re-borrow editor. // This collect is needed because refresh_language_server would need to re-borrow editor.
let document_ids_to_refresh: Vec<DocumentId> = cx let document_ids_to_refresh: Vec<DocumentId> = cx
@ -1221,18 +1245,41 @@ pub(super) fn goto_line_number(
args: &[Cow<str>], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if event != PromptEvent::Validate { match event {
return Ok(()); PromptEvent::Abort => {
if let Some(line_number) = cx.editor.last_line_number {
goto_line_impl(cx.editor, NonZeroUsize::new(line_number));
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, line_number);
cx.editor.last_line_number = None;
}
return Ok(());
}
PromptEvent::Validate => {
ensure!(!args.is_empty(), "Line number required");
cx.editor.last_line_number = None;
}
PromptEvent::Update => {
if args.is_empty() {
if let Some(line_number) = cx.editor.last_line_number {
// When a user hits backspace and there are no numbers left,
// we can bring them back to their original line
goto_line_impl(cx.editor, NonZeroUsize::new(line_number));
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, line_number);
cx.editor.last_line_number = None;
}
return Ok(());
}
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let line = doc.selection(view.id).primary().cursor_line(text);
cx.editor.last_line_number.get_or_insert(line + 1);
}
} }
ensure!(!args.is_empty(), "Line number required");
let line = args[0].parse::<usize>()?; let line = args[0].parse::<usize>()?;
goto_line_impl(cx.editor, NonZeroUsize::new(line)); goto_line_impl(cx.editor, NonZeroUsize::new(line));
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, line); view.ensure_cursor_in_view(doc, line);
Ok(()) Ok(())
} }

@ -61,9 +61,9 @@ pub fn default() -> HashMap<Mode, Keymap> {
":" => command_mode, ":" => command_mode,
"i" => insert_mode, "i" => insert_mode,
"I" => prepend_to_line, "I" => insert_at_line_start,
"a" => append_mode, "a" => append_mode,
"A" => append_to_line, "A" => insert_at_line_end,
"o" => open_below, "o" => open_below,
"O" => open_above, "O" => open_above,
@ -146,6 +146,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"<" => unindent, "<" => unindent,
"=" => format_selections, "=" => format_selections,
"J" => join_selections, "J" => join_selections,
"A-J" => join_selections_space,
"K" => keep_selections, "K" => keep_selections,
"A-K" => remove_selections, "A-K" => remove_selections,
@ -347,24 +348,27 @@ pub fn default() -> HashMap<Mode, Keymap> {
let insert = keymap!({ "Insert mode" let insert = keymap!({ "Insert mode"
"esc" => normal_mode, "esc" => normal_mode,
"backspace" => delete_char_backward,
"C-h" => delete_char_backward,
"del" => delete_char_forward,
"C-d" => delete_char_forward,
"ret" => insert_newline,
"C-j" => insert_newline,
"tab" => insert_tab,
"C-w" => delete_word_backward,
"A-backspace" => delete_word_backward,
"A-d" => delete_word_forward,
"A-del" => delete_word_forward,
"C-s" => commit_undo_checkpoint, "C-s" => commit_undo_checkpoint,
"C-x" => completion,
"C-r" => insert_register,
"C-k" => kill_to_line_end, "C-w" | "A-backspace" => delete_word_backward,
"A-d" | "A-del" => delete_word_forward,
"C-u" => kill_to_line_start, "C-u" => kill_to_line_start,
"C-k" => kill_to_line_end,
"C-h" | "backspace" => delete_char_backward,
"C-d" | "del" => delete_char_forward,
"C-j" | "ret" => insert_newline,
"tab" => insert_tab,
"C-x" => completion, "up" => move_line_up,
"C-r" => insert_register, "down" => move_line_down,
"left" => move_char_left,
"right" => move_char_right,
"pageup" => page_up,
"pagedown" => page_down,
"home" => goto_line_start,
"end" => goto_line_end_newline,
}); });
hashmap!( hashmap!(
Mode::Normal => Keymap::new(normal), Mode::Normal => Keymap::new(normal),

@ -97,7 +97,7 @@ impl Completion {
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction( fn item_to_transaction(
doc: &Document, doc: &Document,
item: &CompletionItem, item: &CompletionItem,

@ -33,6 +33,7 @@ use super::statusline;
pub struct EditorView { pub struct EditorView {
pub keymaps: Keymaps, pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
pseudo_pending: Vec<KeyEvent>,
last_insert: (commands::MappableCommand, Vec<InsertEvent>), last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
@ -57,6 +58,7 @@ impl EditorView {
Self { Self {
keymaps, keymaps,
on_next_key: None, on_next_key: None,
pseudo_pending: Vec::new(),
last_insert: (commands::MappableCommand::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
@ -438,7 +440,8 @@ impl EditorView {
return; return;
} }
let starting_indent = (offset.col / tab_width) as u16; let starting_indent =
(offset.col / tab_width) as u16 + config.indent_guides.skip_levels;
// TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some // TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some
// extra loops if the code is deeply nested. // extra loops if the code is deeply nested.
@ -705,6 +708,7 @@ impl EditorView {
let mut offset = 0; let mut offset = 0;
let gutter_style = theme.get("ui.gutter"); let gutter_style = theme.get("ui.gutter");
let gutter_selected_style = theme.get("ui.gutter.selected");
// avoid lots of small allocations by reusing a text buffer for each line // avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8); let mut text = String::with_capacity(8);
@ -717,6 +721,12 @@ impl EditorView {
let x = viewport.x + offset; let x = viewport.x + offset;
let y = viewport.y + i as u16; let y = viewport.y + i as u16;
let gutter_style = if selected {
gutter_selected_style
} else {
gutter_style
};
if let Some(style) = gutter(line, selected, &mut text) { if let Some(style) = gutter(line, selected, &mut text) {
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style)); surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
} else { } else {
@ -840,6 +850,7 @@ impl EditorView {
event: KeyEvent, event: KeyEvent,
) -> Option<KeymapResult> { ) -> Option<KeymapResult> {
let mut last_mode = mode; let mut last_mode = mode;
self.pseudo_pending.extend(self.keymaps.pending());
let key_result = self.keymaps.get(mode, event); let key_result = self.keymaps.get(mode, event);
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
@ -1317,6 +1328,11 @@ impl Component for EditorView {
} }
self.on_next_key = cx.on_next_key_callback.take(); self.on_next_key = cx.on_next_key_callback.take();
match self.on_next_key {
Some(_) => self.pseudo_pending.push(key),
None => self.pseudo_pending.clear(),
}
// appease borrowck // appease borrowck
let callback = cx.callback.take(); let callback = cx.callback.take();
@ -1432,8 +1448,8 @@ impl Component for EditorView {
for key in self.keymaps.pending() { for key in self.keymaps.pending() {
disp.push_str(&key.key_sequence_format()); disp.push_str(&key.key_sequence_format());
} }
if let Some(pseudo_pending) = &cx.editor.pseudo_pending { for key in &self.pseudo_pending {
disp.push_str(pseudo_pending.as_str()) disp.push_str(&key.key_sequence_format());
} }
let style = cx.editor.theme.get("ui.text"); let style = cx.editor.theme.get("ui.text");
let macro_width = if cx.editor.macro_recording.is_some() { let macro_width = if cx.editor.macro_recording.is_some() {

@ -68,8 +68,9 @@ impl Component for SignatureHelp {
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
let sig_text_area = area.clip_top(1).with_height(sig_text_height); let sig_text_area = area.clip_top(1).with_height(sig_text_height);
let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area);
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false }); let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
sig_text_para.render(sig_text_area.inner(&margin), surface); sig_text_para.render(sig_text_area, surface);
if self.signature_doc.is_none() { if self.signature_doc.is_none() {
return; return;

@ -74,6 +74,7 @@ impl<T: Item> Menu<T> {
// rendering) // rendering)
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
sort: bool,
editor_data: <T as Item>::Data, editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self { ) -> Self {
@ -91,8 +92,12 @@ impl<T: Item> Menu<T> {
recalculate: true, recalculate: true,
}; };
// TODO: scoring on empty input should just use a fastpath if sort {
menu.score(""); // TODO: scoring on empty input should just use a fastpath
menu.score("");
} else {
menu.matches = (0..menu.options.len()).map(|i| (i, 0)).collect();
}
menu menu
} }
@ -112,10 +117,7 @@ impl<T: Item> Menu<T> {
.map(|score| (index, score)) .map(|score| (index, score))
}), }),
); );
// matches.sort_unstable_by_key(|(_, score)| -score); self.matches.sort_unstable_by_key(|(_, score)| -score);
self.matches.sort_unstable_by_key(|(index, _score)| {
self.options[*index].sort_text(&self.editor_data)
});
// reset cursor position // reset cursor position
self.cursor = None; self.cursor = None;

@ -14,6 +14,8 @@ mod statusline;
mod text; mod text;
mod tree; mod tree;
use crate::compositor::{Component, Compositor};
use crate::job;
pub use completion::Completion; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
pub use explore::Explorer; pub use explore::Explorer;
@ -28,7 +30,7 @@ pub use tree::{Tree, TreeItem, TreeOp};
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder; use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View}; use helix_view::Editor;
use std::path::PathBuf; use std::path::PathBuf;
@ -63,7 +65,7 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
) { ) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let doc_id = view.doc; let doc_id = view.doc;
@ -110,11 +112,42 @@ pub fn regex_prompt(
view.jumps.push((doc_id, snapshot.clone())); view.jumps.push((doc_id, snapshot.clone()));
} }
fun(view, doc, regex, event); fun(cx.editor, regex, event);
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff); view.ensure_cursor_in_view(doc, config.scrolloff);
} }
Err(_err) => (), // TODO: mark command line as error Err(err) => {
let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, snapshot.clone());
view.offset = offset_snapshot;
if event == PromptEvent::Validate {
let callback = async move {
let call: job::Callback = Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let contents = Text::new(format!("{}", err));
let size = compositor.size();
let mut popup = Popup::new("invalid-regex", contents)
.position(Some(helix_core::Position::new(
size.height as usize - 2, // 2 = statusline + commandline
0,
)))
.auto_close(true);
popup.required_size((size.width, size.height));
compositor.replace_or_push("invalid-regex", popup);
},
);
Ok(call)
};
cx.jobs.callback(callback);
} else {
// Update
// TODO: mark command line as error
}
}
} }
} }
} }

@ -144,6 +144,7 @@ where
helix_view::editor::StatusLineElement::Selections => render_selections, helix_view::editor::StatusLineElement::Selections => render_selections,
helix_view::editor::StatusLineElement::Position => render_position, helix_view::editor::StatusLineElement::Position => render_position,
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage, helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers,
helix_view::editor::StatusLineElement::Separator => render_separator, helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::Spacer => render_spacer,
} }
@ -154,16 +155,16 @@ where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{ {
let visible = context.focused; let visible = context.focused;
let modenames = &context.editor.config().statusline.mode;
write( write(
context, context,
format!( format!(
" {} ", " {} ",
if visible { if visible {
match context.editor.mode() { match context.editor.mode() {
Mode::Insert => "INS", Mode::Insert => &modenames.insert,
Mode::Select => "SEL", Mode::Select => &modenames.select,
Mode::Normal => "NOR", Mode::Normal => &modenames.normal,
} }
} else { } else {
// If not focused, explicitly leave an empty space instead of returning None. // If not focused, explicitly leave an empty space instead of returning None.
@ -276,6 +277,15 @@ where
); );
} }
fn render_total_line_numbers<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let total_line_numbers = context.doc.text().len_lines();
write(context, format!(" {} ", total_line_numbers), None);
}
fn render_position_percentage<F>(context: &mut RenderContext, write: F) fn render_position_percentage<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,

@ -85,3 +85,131 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::Result<()> {
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
(
helpers::platform_line(indoc! {"\
#[/|]#// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"})
.as_ref(),
"]fv]f",
helpers::platform_line(indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"})
.as_ref(),
),
)
.await?;
Ok(())
}
#[tokio::test]
async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Result<()> {
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
(
helpers::platform_line(indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"})
.as_ref(),
"v[f",
helpers::platform_line(indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }|]#
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"})
.as_ref(),
),
)
.await?;
Ok(())
}
#[tokio::test]
async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> anyhow::Result<()> {
// Note: the anchor stays put and the head moves back.
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
(
helpers::platform_line(indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"})
.as_ref(),
"v[f",
helpers::platform_line(indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
#[|fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"})
.as_ref(),
),
)
.await?;
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
(
helpers::platform_line(indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"})
.as_ref(),
"v[f[f",
helpers::platform_line(indoc! {"\
/// Increments
#[|fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"})
.as_ref(),
),
)
.await?;
Ok(())
}

@ -17,6 +17,7 @@ term = ["crossterm"]
bitflags = "1.3" bitflags = "1.3"
anyhow = "1" anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.25", optional = true } crossterm = { version = "0.25", optional = true }

@ -5,6 +5,7 @@ use helix_core::auto_pairs::AutoPairs;
use helix_core::Range; use helix_core::Range;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize; use serde::Serialize;
use std::borrow::Cow;
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
@ -1054,6 +1055,12 @@ impl Document {
.map(helix_core::path::get_relative_path) .map(helix_core::path::get_relative_path)
} }
pub fn display_name(&self) -> Cow<'static, str> {
self.relative_path()
.map(|path| path.to_string_lossy().to_string().into())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
}
// transact(Fn) ? // transact(Fn) ?
// -- LSP methods // -- LSP methods

@ -1,6 +1,6 @@
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::{Mode, SCRATCH_BUFFER_NAME}, document::Mode,
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -28,7 +28,7 @@ use tokio::{
time::{sleep, Duration, Instant, Sleep}, time::{sleep, Duration, Instant, Sleep},
}; };
use anyhow::{bail, Error}; use anyhow::Error;
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; pub use helix_core::register::Registers;
@ -313,6 +313,7 @@ pub struct StatusLineConfig {
pub center: Vec<StatusLineElement>, pub center: Vec<StatusLineElement>,
pub right: Vec<StatusLineElement>, pub right: Vec<StatusLineElement>,
pub separator: String, pub separator: String,
pub mode: ModeConfig,
} }
impl Default for StatusLineConfig { impl Default for StatusLineConfig {
@ -333,6 +334,25 @@ impl Default for StatusLineConfig {
E::FileType, E::FileType,
], ],
separator: String::from("│"), separator: String::from("│"),
mode: ModeConfig::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ModeConfig {
pub normal: String,
pub insert: String,
pub select: String,
}
impl Default for ModeConfig {
fn default() -> Self {
Self {
normal: String::from("NOR"),
insert: String::from("INS"),
select: String::from("SEL"),
} }
} }
} }
@ -373,6 +393,9 @@ pub enum StatusLineElement {
/// The cursor position as a percent of the total file /// The cursor position as a percent of the total file
PositionPercentage, PositionPercentage,
/// The total line numbers of the current file
TotalLineNumbers,
/// A single space /// A single space
Spacer, Spacer,
} }
@ -591,17 +614,19 @@ impl Default for WhitespaceCharacters {
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default, rename_all = "kebab-case")]
pub struct IndentGuidesConfig { pub struct IndentGuidesConfig {
pub render: bool, pub render: bool,
pub character: char, pub character: char,
pub rainbow: bool, pub rainbow: bool,
pub skip_levels: u16,
} }
impl Default for IndentGuidesConfig { impl Default for IndentGuidesConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
render: true, render: true,
skip_levels: 0,
character: '', character: '',
rainbow: false, rainbow: false,
} }
@ -708,7 +733,7 @@ pub struct Editor {
/// The currently applied editor theme. While previewing a theme, the previewed theme /// The currently applied editor theme. While previewing a theme, the previewed theme
/// is set here. /// is set here.
pub theme: Theme, pub theme: Theme,
pub last_line_number: Option<usize>,
pub status_msg: Option<(Cow<'static, str>, Severity)>, pub status_msg: Option<(Cow<'static, str>, Severity)>,
pub autoinfo: Option<Info>, pub autoinfo: Option<Info>,
@ -717,7 +742,6 @@ pub struct Editor {
pub idle_timer: Pin<Box<Sleep>>, pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>, pub last_motion: Option<Motion>,
pub pseudo_pending: Option<String>,
pub last_completion: Option<CompleteAction>, pub last_completion: Option<CompleteAction>,
@ -751,6 +775,26 @@ pub enum Action {
VerticalSplit, VerticalSplit,
} }
/// Error thrown on failed document closed
#[derive(Debug, Clone)]
pub enum CloseError {
/// Document doesn't exist
DoesNotExist,
/// Buffer is modified
BufferModified(String),
}
impl std::error::Error for CloseError {}
impl std::fmt::Display for CloseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CloseError::DoesNotExist => "Buffer does not exist".fmt(f),
CloseError::BufferModified(s) => write!(f, "The buffer {s} has been modified"),
}
}
}
impl Editor { impl Editor {
pub fn new( pub fn new(
mut area: Rect, mut area: Rect,
@ -782,6 +826,7 @@ impl Editor {
syn_loader, syn_loader,
theme_loader, theme_loader,
last_theme: None, last_theme: None,
last_line_number: None,
registers: Registers::default(), registers: Registers::default(),
clipboard_provider: get_clipboard_provider(), clipboard_provider: get_clipboard_provider(),
status_msg: None, status_msg: None,
@ -789,7 +834,6 @@ impl Editor {
idle_timer: Box::pin(sleep(conf.idle_timeout)), idle_timer: Box::pin(sleep(conf.idle_timeout)),
last_motion: None, last_motion: None,
last_completion: None, last_completion: None,
pseudo_pending: None,
config, config,
auto_pairs, auto_pairs,
exit_code: 0, exit_code: 0,
@ -910,13 +954,17 @@ impl Editor {
pub fn restart_language_server(&mut self, doc_id: DocumentId) -> Option<()> { pub fn restart_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?; let doc = self.documents.get_mut(&doc_id)?;
if let Some(language) = doc.language.as_ref() { if let Some(language) = doc.language.as_ref() {
if let Ok(client) = self.language_servers.restart(&*language).map_err(|e| { if let Ok(client) = self
log::error!( .language_servers
"Failed to restart the LSP for `{}` {{ {} }}", .restart(&*language, doc.path())
language.scope(), .map_err(|e| {
e log::error!(
) "Failed to restart the LSP for `{}` {{ {} }}",
}) { language.scope(),
e
)
})
{
doc.set_language_server(Some(client)); doc.set_language_server(Some(client));
} }
}; };
@ -930,7 +978,7 @@ impl Editor {
// try to find a language server based on the language name // try to find a language server based on the language name
let language_server = doc.language.as_ref().and_then(|language| { let language_server = doc.language.as_ref().and_then(|language| {
ls.get(language) ls.get(language, doc.path())
.map_err(|e| { .map_err(|e| {
log::error!( log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}", "Failed to initialize the LSP for `{}` {{ {} }}",
@ -1130,19 +1178,14 @@ impl Editor {
self._refresh(); self._refresh();
} }
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> { pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
let doc = match self.documents.get(&doc_id) { let doc = match self.documents.get(&doc_id) {
Some(doc) => doc, Some(doc) => doc,
None => bail!("document does not exist"), None => return Err(CloseError::DoesNotExist),
}; };
if !force && doc.is_modified() { if !force && doc.is_modified() {
bail!( return Err(CloseError::BufferModified(doc.display_name().into_owned()));
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
);
} }
if let Some(language_server) = doc.language_server() { if let Some(language_server) = doc.language_server() {

@ -3,19 +3,28 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::Context; use anyhow::{anyhow, Context, Result};
use helix_core::hashmap; use helix_core::hashmap;
use helix_loader::merge_toml_values;
use log::warn; use log::warn;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use toml::Value; use toml::{map::Map, Value};
pub use crate::graphics::{Color, Modifier, Style}; pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
// let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml"))
// .expect("Failed to parse default theme");
// Theme::from(raw_theme)
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
}); });
pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
// let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml"))
// .expect("Failed to parse base 16 default theme");
// Theme::from(raw_theme)
toml::from_slice(include_bytes!("../../base16_theme.toml")) toml::from_slice(include_bytes!("../../base16_theme.toml"))
.expect("Failed to parse base 16 default theme") .expect("Failed to parse base 16 default theme")
}); });
@ -35,24 +44,54 @@ impl Loader {
} }
/// Loads a theme first looking in the `user_dir` then in `default_dir` /// Loads a theme first looking in the `user_dir` then in `default_dir`
pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> { pub fn load(&self, name: &str) -> Result<Theme> {
if name == "default" { if name == "default" {
return Ok(self.default()); return Ok(self.default());
} }
if name == "base16_default" { if name == "base16_default" {
return Ok(self.base16_default()); return Ok(self.base16_default());
} }
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename); let value = self.load_theme(name, name, false)?;
let path = if user_path.exists() { let theme = Theme::deserialize(value)?;
user_path
Ok(theme)
}
// 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_them_name: &str,
only_default_dir: bool,
) -> Result<Value> {
let path = self.path(name, only_default_dir);
let theme_toml = self.load_toml(path)?;
let inherits = theme_toml.get("inherits");
let theme_toml = if let Some(parent_theme_name) = inherits {
let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| {
anyhow!(
"Theme: expected 'inherits' to be a string: {}",
parent_theme_name
)
})?;
let parent_theme_toml = self.load_theme(
parent_theme_name,
base_them_name,
base_them_name == parent_theme_name,
)?;
self.merge_themes(parent_theme_toml, theme_toml)
} else { } else {
self.default_dir.join(filename) theme_toml
}; };
let data = std::fs::read(&path)?; Ok(theme_toml)
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
} }
pub fn read_names(path: &Path) -> Vec<String> { pub fn read_names(path: &Path) -> Vec<String> {
@ -70,6 +109,53 @@ impl Loader {
.unwrap_or_default() .unwrap_or_default()
} }
// merge one theme into the parent theme
fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value {
let parent_palette = parent_theme_toml.get("palette");
let palette = theme_toml.get("palette");
// handle the table seperately since it needs a `merge_depth` of 2
// this would conflict with the rest of the theme merge strategy
let palette_values = match (parent_palette, palette) {
(Some(parent_palette), Some(palette)) => {
merge_toml_values(parent_palette.clone(), palette.clone(), 2)
}
(Some(parent_palette), None) => parent_palette.clone(),
(None, Some(palette)) => palette.clone(),
(None, None) => Map::new().into(),
};
// add the palette correctly as nested table
let mut palette = Map::new();
palette.insert(String::from("palette"), palette_values);
// merge the theme into the parent theme
let theme = merge_toml_values(parent_theme_toml, theme_toml, 1);
// merge the before specially handled palette into the theme
merge_toml_values(theme, palette.into(), 1)
}
// Loads the theme data as `toml::Value` first from the user_dir then in default_dir
fn load_toml(&self, path: PathBuf) -> Result<Value> {
let data = std::fs::read(&path)?;
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
}
// 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 {
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 /// Lists all theme names available in default and user directory
pub fn names(&self) -> Vec<String> { pub fn names(&self) -> Vec<String> {
let mut names = Self::read_names(&self.user_dir); let mut names = Self::read_names(&self.user_dir);
@ -106,6 +192,26 @@ pub struct Theme {
rainbow_length: usize, rainbow_length: usize,
} }
// impl From<Value> for Theme {
// fn from(value: Value) -> Self {
// let values: Result<HashMap<String, Value>> =
// toml::from_str(&value.to_string()).context("Failed to load theme");
// let (styles, scopes, highlights) = build_theme_values(values);
// let rainbow_length = styles
// .iter()
// .filter(|s| s.0.starts_with("rainbow."))
// .count();
// Self {
// styles,
// scopes,
// highlights,
// rainbow_length,
// }
// }
// }
impl<'de> Deserialize<'de> for Theme { impl<'de> Deserialize<'de> for Theme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
@ -166,8 +272,8 @@ impl<'de> Deserialize<'de> for Theme {
} }
Ok(Self { Ok(Self {
scopes,
styles, styles,
scopes,
highlights, highlights,
rainbow_length, rainbow_length,
}) })

@ -911,7 +911,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "markdown" name = "markdown"
source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "142a5b4a1b092b64c9f5db8f11558f9dd4009a1b", subpath = "tree-sitter-markdown" } source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "d5740f0fe4b8e4603f2229df107c5c9ef5eec389", subpath = "tree-sitter-markdown" }
[[language]] [[language]]
name = "markdown.inline" name = "markdown.inline"
@ -923,7 +923,7 @@ grammar = "markdown_inline"
[[grammar]] [[grammar]]
name = "markdown_inline" name = "markdown_inline"
source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "142a5b4a1b092b64c9f5db8f11558f9dd4009a1b", subpath = "tree-sitter-markdown-inline" } source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "d5740f0fe4b8e4603f2229df107c5c9ef5eec389", subpath = "tree-sitter-markdown-inline" }
[[language]] [[language]]
name = "dart" name = "dart"
@ -1781,3 +1781,25 @@ language-server = { command = "bass", args = ["--lsp"] }
[[grammar]] [[grammar]]
name = "bass" name = "bass"
source = { git = "https://github.com/vito/tree-sitter-bass", rev = "501133e260d768ed4e1fd7374912ed5c86d6fd90" } source = { git = "https://github.com/vito/tree-sitter-bass", rev = "501133e260d768ed4e1fd7374912ed5c86d6fd90" }
[[language]]
name = "wat"
scope = "source.wat"
comment-token = ";;"
file-types = ["wat"]
roots = []
[[grammar]]
name = "wat"
source = { git = "https://github.com/wasm-lsp/tree-sitter-wasm", rev = "2ca28a9f9d709847bf7a3de0942a84e912f59088", subpath = "wat" }
[[language]]
name = "wast"
scope = "source.wast"
comment-token = ";;"
file-types = ["wast"]
roots = []
[[grammar]]
name = "wast"
source = { git = "https://github.com/wasm-lsp/tree-sitter-wasm", rev = "2ca28a9f9d709847bf7a3de0942a84e912f59088", subpath = "wast" }

@ -7,6 +7,8 @@
((html_block) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children)) ((html_block) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children))
((pipe_table_cell) @injection.content (#set! injection.language "markdown.inline") (#set! injection.include-unnamed-children))
((minus_metadata) @injection.content (#set! injection.language "yaml") (#set! injection.include-unnamed-children)) ((minus_metadata) @injection.content (#set! injection.language "yaml") (#set! injection.include-unnamed-children))
((plus_metadata) @injection.content (#set! injection.language "toml") (#set! injection.include-unnamed-children)) ((plus_metadata) @injection.content (#set! injection.language "toml") (#set! injection.include-unnamed-children))

@ -253,6 +253,9 @@
(function_item (function_item
name: (identifier) @function) name: (identifier) @function)
(function_signature_item
name: (identifier) @function)
; --- ; ---
; Macros ; Macros
; --- ; ---

@ -0,0 +1,21 @@
; inherits: wat
[
"assert_return"
"assert_trap"
"assert_exhaustion"
"assert_malformed"
"assert_invalid"
"assert_unlinkable"
"assert_trap"
"invoke"
"get"
"script"
"input"
"output"
"binary"
"quote"
] @keyword

@ -0,0 +1,17 @@
["module" "func" "param" "result" "type" "memory" "elem" "data" "table" "global"] @keyword
["import" "export"] @keyword.control.import
["local"] @keyword.storage.type
[(name) (string)] @string
(identifier) @function
[(comment_block) (comment_line)] @comment
[(nat) (float) (align_offset_value)] @constant.numeric.integer
(value_type) @type
["(" ")"] @punctuation.bracket

@ -1,32 +1,32 @@
# Author : Wojciech Kępka <wojciech@wkepka.dev> # Author : Wojciech Kępka <wojciech@wkepka.dev>
"attribute" = "#dc7759" "attribute" = "bogster0"
"keyword" = { fg = "#dcb659", modifiers = ["bold"] } "keyword" = { fg = "bogster1", modifiers = ["bold"] }
"keyword.directive" = "#dcb659" "keyword.directive" = "bogster1"
"namespace" = "#d32c5d" "namespace" = "bogster2"
"punctuation" = "#dc7759" "punctuation" = "bogster0"
"punctuation.delimiter" = "#dc7759" "punctuation.delimiter" = "bogster0"
"operator" = { fg = "#dc7759", modifiers = ["bold"] } "operator" = { fg = "bogster0", modifiers = ["bold"] }
"special" = "#7fdc59" "special" = "bogster3"
"variable.other.member" = "#c6b8ad" "variable.other.member" = "bogster4"
"variable" = "#c6b8ad" "variable" = "bogster4"
"variable.parameter" = "#c6b8ad" "variable.parameter" = "bogster4"
"type" = "#dc597f" "type" = "bogster5"
"type.builtin" = { fg = "#d32c5d", modifiers = ["bold"] } "type.builtin" = { fg = "bogster2", modifiers = ["bold"] }
"constructor" = "#dc597f" "constructor" = "bogster5"
"function" = "#59dcd8" "function" = "bogster6"
"function.macro" = { fg = "#dc7759", modifiers = ["bold"] } "function.macro" = { fg = "bogster0", modifiers = ["bold"] }
"function.builtin" = { fg = "#59dcd8", modifiers = ["bold"] } "function.builtin" = { fg = "bogster6", modifiers = ["bold"] }
"comment" = "#627d9d" "comment" = "bogster7"
"variable.builtin" = "#c6b8ad" "variable.builtin" = "bogster4"
"constant" = "#59dcb7" "constant" = "bogster8"
"constant.builtin" = "#59dcb7" "constant.builtin" = "bogster8"
"string" = "#59dcb7" "string" = "bogster8"
"constant.numeric" = "#59c0dc" "constant.numeric" = "bogster9"
"constant.character.escape" = { fg = "#7fdc59", modifiers = ["bold"] } "constant.character.escape" = { fg = "bogster3", modifiers = ["bold"] }
"label" = "#59c0dc" "label" = "bogster9"
"module" = "#d32c5d" "module" = "bogster2"
# TODO # TODO
"markup.heading" = "blue" "markup.heading" = "blue"
@ -38,45 +38,70 @@
"markup.quote" = "cyan" "markup.quote" = "cyan"
"markup.raw" = "green" "markup.raw" = "green"
"diff.plus" = "#59dcb7" "diff.plus" = "bogster8"
"diff.delta" = "#dc7759" "diff.delta" = "bogster0"
"diff.minus" = "#dc597f" "diff.minus" = "bogster5"
"ui.background" = { bg = "#161c23" } "ui.background" = { bg = "bogster10" }
"ui.linenr" = { fg = "#415367" } "ui.linenr" = { fg = "bogster11" }
"ui.linenr.selected" = { fg = "#e5ded6" } # TODO "ui.linenr.selected" = { fg = "bogster12" } # TODO
"ui.cursorline" = { bg = "#131920" } "ui.cursorline" = { bg = "bogster13" }
"ui.statusline" = { fg = "#e5ded6", bg = "#232d38" } "ui.statusline" = { fg = "bogster12", bg = "bogster14" }
"ui.statusline.inactive" = { fg = "#c6b8ad", bg = "#232d38" } "ui.statusline.inactive" = { fg = "bogster4", bg = "bogster14" }
"ui.bufferline" = { fg = "#627d9d", bg = "#131920" } "ui.popup" = { bg = "bogster14" }
"ui.bufferline.active" = { fg = "#e5ded6", bg = "#232d38" } "ui.window" = { bg = "bogster14" }
"ui.popup" = { bg = "#232d38" } "ui.help" = { bg = "bogster14", fg = "bogster12" }
"ui.window" = { bg = "#232d38" }
"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
"ui.text" = { fg = "#e5ded6" } "ui.statusline.normal" = { fg = "bogster10", bg = "bogster9", modifiers = [ "bold" ]}
"ui.text.focus" = { fg = "#e5ded6", modifiers= ["bold"] } "ui.statusline.insert" = { fg = "bogster10", bg = "bogster3", modifiers = [ "bold" ]}
"ui.virtual.whitespace" = "#627d9d" "ui.statusline.select" = { fg = "bogster10", bg = "bogster2", modifiers = [ "bold" ] }
"ui.virtual.ruler" = { bg = "#131920" }
"ui.selection" = { bg = "#313f4e" } "ui.text" = { fg = "bogster12" }
"ui.text.focus" = { fg = "bogster12", modifiers= ["bold"] }
"ui.virtual.whitespace" = "bogster7"
"ui.virtual.ruler" = { bg = "bogster13" }
"ui.selection" = { bg = "bogster15" }
# "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported # "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported
"ui.cursor.match" = { fg = "#313f4e", bg = "#dc7759" } "ui.cursor.match" = { fg = "bogster15", bg = "bogster0" }
"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] } "ui.cursor" = { fg = "bogster16", modifiers = ["reversed"] }
"ui.menu" = { fg = "#e5ded6bg", bg = "#232d38" } "ui.menu" = { fg = "bogster12", bg = "bogster14" }
"ui.menu.selected" = { bg = "#313f4e" } "ui.menu.selected" = { bg = "bogster15" }
"warning" = "#dc7759" "warning" = "bogster0"
"error" = "#dc597f" "error" = "bogster5"
"info" = "#59dcb7" "info" = "bogster8"
"hint" = "#59c0dc" "hint" = "bogster9"
# make diagnostic underlined, to distinguish with selection text. # make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] } diagnostic = { modifiers = ["underlined"] }
<<<<<<< HEAD
"ui.explorer.file" = { fg = "#e5ded6" } "ui.explorer.file" = { fg = "#e5ded6" }
"ui.explorer.dir" = { fg = "#59dcd8" } "ui.explorer.dir" = { fg = "#59dcd8" }
"ui.explorer.exe" = { fg = "#e5ded6" } "ui.explorer.exe" = { fg = "#e5ded6" }
"ui.explorer.focus" = { modifiers = ["reversed"] } "ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#313f4e" } "ui.explorer.unfocus" = { bg = "#313f4e" }
||||||| 60aa7d36
=======
[palette]
bogster0 = "#dc7759"
bogster1 = "#dcb659"
bogster2 = "#d32c5d"
bogster3 = "#7fdc59"
bogster4 = "#c6b8ad"
bogster5 = "#dc597f"
bogster6 = "#59dcd8"
bogster7 = "#627d9d"
bogster8 = "#59dcb7"
bogster9 = "#59c0dc"
bogster10 = "#161c23"
bogster11 = "#415367"
bogster12 = "#e5ded6"
bogster13 = "#131920"
bogster14 = "#232d38"
bogster15 = "#313f4e"
bogster16 = "#ABB2BF"
>>>>>>> seperate_code_action

@ -0,0 +1,105 @@
# GreasySlug : dark high contrast <9619abgoni@gmail.com>
# Interface
"ui.background" = { bg = "black" }
"ui.window" = { fg = "aqua" }
"special" = "orange" # file picker fuzzy match
"ui.background.separator" = { fg = "white" }
"ui.text" = "white"
"ui.text.focus" = { modifiers = ["reversed"] } # file picker selected
"ui.virtual.whitespace" = "gray"
"ui.virtual.ruler" = { fg = "gray", bg = "white", modifiers = ["reversed"] }
"ui.virtual.indent-guide" = "white"
"ui.statusline" = { fg = "white", bg = "deep_blue" }
"ui.statusline.inactive" = { fg = "gray" }
"ui.statusline.normal" = { fg = "black", bg = "aqua" }
"ui.statusline.insert" = { fg = "black", bg = "orange" }
"ui.statusline.select" = { fg = "black", bg = "purple" }
# "ui.statusline.separator" = { fg = "aqua", bg = "white" }
"ui.cursor" = { fg = "black", bg = "white" }
"ui.cursor.insert" = { fg = "black", bg = "white" }
"ui.cursor.select" = { fg = "black", bg = "white" }
"ui.cursor.match" = { bg = "white", modifiers = ["dim"] }
"ui.cursor.primary" = { fg = "black", bg = "white", modifiers = ["slow_blink"] }
"ui.cursor.secondary" = "white"
"ui.cursorline.primary" = { fg = "orange", modifiers = ["underlined"] }
"ui.cursorline.secondary" = { fg = "orange", modifiers = ["underlined"] }
"ui.selection" = { fg = "deep_blue", bg = "white" }
"ui.menu" = { fg = "white", bg = "deep_blue" }
"ui.menu.selected" = { fg = "orange", modifiers = ["underlined"] }
"ui.menu.scroll" = { fg = "aqua", bg = "black" }
"ui.help" = { fg = "white", bg = "deep_blue" }
"ui.popup" = { bg = "deep_blue" }
"ui.popup.info" = { fg = "white", bg = "deep_blue" }
"ui.gutter" = { bg = "black" }
"ui.linenr" = { fg = "white", bg = "black" }
"ui.linenr.selected" = { fg = "orange", bg = "black", modifiers = ["bold"] }
# Diagnostic
"diagnostic" = "white"
"diagnostic.info" = { bg = "white", modifiers = ["underlined"] }
"diagnostic.hint" = { fg = "yellow", modifiers = ["underlined"] }
"diagnostic.warning" = { fg = "orange", modifiers = ["underlined"] }
"diagnostic.error" = { fg = "red", modifiers = ["underlined"] }
"info" = "white"
"hint" = "yellow"
"warning" = "orange"
"error" = "red"
"debug" = "red"
"diff.plus" = "green"
"diff.delta" ="blue"
"diff.minus" = "pink"
# Syntax high light
"type" = { fg = "emerald_green" }
"type.buildin" = "emerald_green"
"comment" = { fg = "white" }
"operator" = "white"
"variable" = { fg = "aqua" }
"variable.other.member" = "brown"
"constant" = "aqua"
"constant.numeric" = "emerald_green"
"attributes" = { fg = "blue" }
"namespace" = "blue"
"string" = "brown"
"string.special.url" = { fg = "brown", modifiers =["underlined"]}
"constant.character.escape" = "yellow"
"constructor" = "aqua"
"keyword" = { fg = "blue", modifiers = ["underlined"] }
"keyword.control" = "purple"
"function" = "yellow"
"label" = "blue"
# Markup
"markup.heading" = { fg = "yellow", modifiers = ["bold", "underlined"] }
"markup.list" = { fg = "pink" }
"markup.bold" = { fg = "emerald_green", modifiers = ["bold"] }
"markup.italic" = { fg = "blue", modifiers = ["italic"] }
"markup.link.url" = { fg = "blue", modifiers = ["underlined"] }
"markup.link.text" = "pink"
"markup.quote" = "yellow"
"markup.raw" = "brown"
[palette]
black = "#000000"
gray = "#585858"
white = "#ffffff"
blue = "#66a4ff"
deep_blue = "#142743"
aqua = "#6fc3df"
purple = "#c586c0"
red = "#b65f5f"
pink = "#ff5c8d"
orange = "#f38518"
brown = "#ce9178"
yellow = "#cedc84"
green = "#427a2d"
emerald_green = "#4ec9b0"

@ -0,0 +1,97 @@
# Author : nuid32 <lvkuzvesov@proton.me>
"attribute" = { fg = "yellow" }
"comment" = { fg = "light-gray", modifiers = ["italic"] }
"constant" = { fg = "gold" }
"constant.numeric" = { fg = "gold" }
"constant.builtin" = { fg = "gold" }
"constant.character.escape" = { fg = "gold" }
"constructor" = { fg = "blue" }
"function" = { fg = "white" }
"function.builtin" = { fg = "blue" }
"function.macro" = { fg = "purple" }
"keyword" = { fg = "purple" }
"keyword.control" = { fg = "purple" }
"keyword.control.import" = { fg = "purple" }
"keyword.directive" = { fg = "purple" }
"label" = { fg = "purple" }
"namespace" = { fg = "purple" }
"operator" = { fg = "white" }
"keyword.operator" = { fg = "white" }
"special" = { fg = "blue" }
"string" = { fg = "green" }
"type" = { fg = "yellow" }
"variable" = { fg = "white" }
"variable.builtin" = { fg = "red" }
"variable.parameter" = { fg = "red" }
"variable.other.member" = { fg = "blue" }
"markup.heading" = { fg = "red" }
"markup.raw.inline" = { fg = "green" }
"markup.raw.block" = { fg = "white" }
"markup.bold" = { fg = "gold", modifiers = ["bold"] }
"markup.italic" = { fg = "purple", modifiers = ["italic"] }
"markup.list" = { fg = "red" }
"markup.quote" = { fg = "yellow" }
"markup.link.url" = { fg = "blue", modifiers = ["underlined"]}
"markup.link.text" = { fg = "white" }
"markup.link.label" = { fg = "white" }
"diff.plus" = "green"
"diff.delta" = "gold"
"diff.minus" = "red"
diagnostic = { modifiers = ["underlined"] }
"info" = { fg = "blue", modifiers = ["bold"] }
"hint" = { fg = "green", modifiers = ["bold"] }
"warning" = { fg = "yellow", modifiers = ["bold"] }
"error" = { fg = "red", modifiers = ["bold"] }
"ui.background" = { bg = "black" }
"ui.virtual" = { fg = "faint-gray" }
"ui.virtual.indent-guide" = { fg = "faint-gray" }
"ui.virtual.whitespace" = { fg = "light-gray" }
"ui.virtual.ruler" = { bg = "gray" }
"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] }
"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]}
"ui.selection" = { bg = "light-gray" }
"ui.selection.primary" = { bg = "gray" }
"ui.cursorline.primary" = { bg = "light-black" }
"ui.linenr" = { fg = "linenr" }
"ui.linenr.selected" = { fg = "white" }
"ui.statusline" = { fg = "white", bg = "light-black" }
"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" }
"ui.statusline.normal" = { fg = "light-black", bg = "purple" }
"ui.statusline.insert" = { fg = "light-black", bg = "green" }
"ui.statusline.select" = { fg = "light-black", bg = "cyan" }
"ui.text" = { fg = "white" }
"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] }
"ui.help" = { fg = "white", bg = "gray" }
"ui.popup" = { bg = "gray" }
"ui.window" = { fg = "gray" }
"ui.menu" = { fg = "white", bg = "gray" }
"ui.menu.selected" = { fg = "black", bg = "blue" }
"ui.menu.scroll" = { fg = "white", bg = "light-gray" }
[palette]
yellow = "#D5B06B"
blue = "#519FDF"
red = "#D05C65"
purple = "#B668CD"
green = "#7DA869"
gold = "#D19A66"
cyan = "#46A6B2"
white = "#ABB2BF"
black = "#16181A"
light-black = "#2C323C"
gray = "#252D30"
faint-gray = "#ABB2BF"
light-gray = "#636C6E"
linenr = "#282C34"

@ -58,7 +58,7 @@
"ui.selection" = { bg = "bg4" } "ui.selection" = { bg = "bg4" }
"ui.linenr" = "grey" "ui.linenr" = "grey"
"ui.linenr.selected" = "fg" "ui.linenr.selected" = "fg"
"ui.cursorline.primary" = { bg = "bg2" } "ui.cursorline.primary" = { bg = "bg1" }
"ui.statusline" = { fg = "fg", bg = "bg3" } "ui.statusline" = { fg = "fg", bg = "bg3" }
"ui.statusline.inactive" = { fg = "grey", bg = "bg1" } "ui.statusline.inactive" = { fg = "grey", bg = "bg1" }
"ui.popup" = { fg = "grey", bg = "bg2" } "ui.popup" = { fg = "grey", bg = "bg2" }
@ -86,7 +86,7 @@ bg0 = "#2c2e34"
bg1 = "#33353f" bg1 = "#33353f"
bg2 = "#363944" bg2 = "#363944"
bg3 = "#3b3e48" bg3 = "#3b3e48"
bg4 = "#414550" bg4 = "#545862"
bg_red = "#ff6077" bg_red = "#ff6077"
diff_red = "#55393d" diff_red = "#55393d"
bg_green = "#a7df78" bg_green = "#a7df78"

@ -967,7 +967,7 @@ lines.
================================================================= =================================================================
= 10.1 CYCLING AND REMOVING SELECIONS = = 10.1 CYCLING AND REMOVING SELECTIONS =
================================================================= =================================================================
Type ) and ( to cycle the primary selection forward and backward Type ) and ( to cycle the primary selection forward and backward
@ -1054,6 +1054,24 @@ letters! that is not good grammar. you can fix this.
=================================================================
= =
=================================================================
================================================================= =================================================================
This tutorial is still a work-in-progress. This tutorial is still a work-in-progress.
More sections are planned. More sections are planned.

Loading…
Cancel
Save