Merge branch 'master' into colored-indent-guides

pull/6/head
s0LA1337 2 years ago
commit dbd0932921

18
Cargo.lock generated

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

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

@ -28,13 +28,17 @@ hidden = false
You may also specify a file to use for configuration with the `-c` or
`--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]` Section
| Key | Description | Default |
|--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` |
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `5` |
| `mouse` | Enable mouse mode. | `true` |
| `middle-click-paste` | Middle click paste support. | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
@ -68,17 +72,32 @@ left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
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 |
| ------ | ----------- |
| `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 |
| `file-name` | The path/name of the opened file |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `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 |
| `diagnostics` | The number of warnings and/or errors |
| `selections` | The number of active selections |
@ -223,6 +242,7 @@ Options for rendering vertical indent guides.
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `rainbow` | Whether or not the indent guides shall have changing colors. | `false` |
| `skip-levels` | Number of indent levels to skip | `0` |
Example:
@ -231,4 +251,5 @@ Example:
render = true
character = "╎"
rainbow = true
skip-levels = 1
```

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

@ -68,8 +68,8 @@
| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| `i` | Insert before selection | `insert_mode` |
| `a` | Insert after selection (append) | `append_mode` |
| `I` | Insert at the start of the line | `prepend_to_line` |
| `A` | Insert at the end of the line | `append_to_line` |
| `I` | Insert at the start of the line | `insert_at_line_start` |
| `A` | Insert at the end of the line | `insert_at_line_end` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last insert | N/A |
@ -129,6 +129,7 @@
| `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` |
| `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` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
@ -313,7 +314,7 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
## Insert Mode
## Insert mode
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
@ -322,44 +323,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
experience.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w`, `Alt-Backspace`, `Ctrl-Backspace` | Delete previous word | `delete_word_backward` |
| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word | `delete_word_forward` |
| `Ctrl-u` | Delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | Delete to end of line | `kill_to_line_end` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` |
| `Backspace`, `Ctrl-h` | Delete previous char | `delete_char_backward` |
| `Delete`, `Ctrl-d` | Delete next char | `delete_char_forward` |
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
mode:
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-s` | Commit undo checkpoint | `commit_undo_checkpoint` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` |
| `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` |
| `Ctrl-u` | Delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | Delete to end of line | `kill_to_line_end` |
| `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` |
| `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` |
These keys are not recommended, but are included for new users less familiar
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
[keys.insert]
"up" = "move_line_up"
"down" = "move_line_down"
"left" = "move_char_left"
"right" = "move_char_right"
"C-b" = "move_char_left"
"C-f" = "move_char_right"
"A-b" = "move_prev_word_end"
"C-left" = "move_prev_word_end"
"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"
up = "no_op"
down = "no_op"
left = "no_op"
right = "no_op"
pageup = "no_op"
pagedown = "no_op"
home = "no_op"
end = "no_op"
```
## Select / extend mode

@ -100,6 +100,21 @@ rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold
Colors from the palette and modifiers may be used.
### 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"
```
### Scopes
The following is a list of scopes available to use for styling.
@ -230,6 +245,8 @@ These scopes are used for theming the editor interface.
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `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.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline |

@ -18,7 +18,7 @@ integration = []
helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.5", default-features = false, features = ["simd"] }
smallvec = "1.9"
smallvec = "1.10"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
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(
slice: RopeSlice,
range: Range,
@ -419,8 +421,8 @@ pub fn goto_treesitter_object(
.filter(|n| n.start_byte() > byte_pos)
.min_by_key(|n| n.start_byte())?,
Direction::Backward => nodes
.filter(|n| n.start_byte() < byte_pos)
.max_by_key(|n| n.start_byte())?,
.filter(|n| n.end_byte() < byte_pos)
.max_by_key(|n| n.end_byte())?,
};
let len = slice.len_bytes();
@ -434,7 +436,7 @@ pub fn goto_treesitter_object(
let end_char = slice.byte_to_char(end_byte);
// 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))
}

@ -122,7 +122,7 @@ impl Range {
}
}
// flips the direction of the selection
/// Flips the direction of the selection
pub fn flip(&self) -> Self {
Self {
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.
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {

@ -49,6 +49,7 @@ impl Client {
root_markers: &[String],
id: usize,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
// Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
@ -72,7 +73,10 @@ impl Client {
let (server_rx, server_tx, initialize_notify) =
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();

@ -204,6 +204,20 @@ pub mod util {
// in reverse order.
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(
doc,
edits.into_iter().map(|edit| {
@ -339,6 +353,7 @@ impl Registry {
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
@ -353,17 +368,26 @@ impl Registry {
// initialize a new client
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));
entry.insert((id, client.clone()));
let (_, old_client) = entry.insert((id, client.clone()));
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
Ok(Some(client))
}
}
}
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 {
Some(config) => config,
None => return Ok(None),
@ -375,7 +399,8 @@ impl Registry {
// initialize a new client
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));
entry.insert((id, client.clone()));
@ -475,6 +500,7 @@ fn start_client(
id: usize,
config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<NewClientResult> {
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
@ -483,6 +509,7 @@ fn start_client(
&config.roots,
id,
ls_config.timeout,
doc_path,
)?;
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" }
[dev-dependencies]
smallvec = "1.9"
smallvec = "1.10"
indoc = "1.0.6"
tempfile = "3.3.0"

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

@ -273,8 +273,8 @@ impl MappableCommand {
diagnostics_picker, "Open diagnostic picker",
workspace_diagnostics_picker, "Open workspace diagnostic picker",
last_picker, "Open last picker",
prepend_to_line, "Insert at start of line",
append_to_line, "Append to end of line",
insert_at_line_start, "Insert at start of line",
insert_at_line_end, "Insert at end of line",
open_below, "Open new line below selection",
open_above, "Open new line above selection",
normal_mode, "Enter normal mode",
@ -346,6 +346,7 @@ impl MappableCommand {
unindent, "Unindent selection",
format_selections, "Format selection",
join_selections, "Join lines inside selection",
join_selections_space, "Join lines inside selection and select spaces",
keep_selections, "Keep selections matching regex",
remove_selections, "Remove selections matching regex",
align_selections, "Align selections in column",
@ -885,8 +886,12 @@ fn goto_window(cx: &mut Context, align: Align) {
.min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line);
doc.set_selection(view.id, Selection::point(pos));
let text = doc.text().slice(..);
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) {
@ -1511,7 +1516,8 @@ fn select_regex(cx: &mut Context) {
"select:".into(),
Some(reg),
ui::completers::none,
move |view, doc, regex, event| {
move |editor, regex, event| {
let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
@ -1532,7 +1538,8 @@ fn split_selection(cx: &mut Context) {
"split:".into(),
Some(reg),
ui::completers::none,
move |view, doc, regex, event| {
move |editor, regex, event| {
let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
@ -1556,15 +1563,16 @@ fn split_selection_on_newline(cx: &mut Context) {
#[allow(clippy::too_many_arguments)]
fn search_impl(
doc: &mut Document,
view: &mut View,
editor: &mut Editor,
contents: &str,
regex: &Regex,
movement: Movement,
direction: Direction,
scrolloff: usize,
wrap_around: bool,
show_warnings: bool,
) {
let (view, doc) = current!(editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
@ -1594,17 +1602,29 @@ fn search_impl(
Direction::Backward => regex.find_iter(&contents[..start]).last(),
};
if wrap_around && mat.is_none() {
mat = match direction {
Direction::Forward => regex.find(contents),
Direction::Backward => {
offset = start;
regex.find_iter(&contents[start..]).last()
if mat.is_none() {
if wrap_around {
mat = match direction {
Direction::Forward => regex.find(contents),
Direction::Backward => {
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 {
let start = text.byte_to_char(mat.start() + offset);
let end = text.byte_to_char(mat.end() + offset);
@ -1680,19 +1700,19 @@ fn searcher(cx: &mut Context, direction: Direction) {
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect()
},
move |view, doc, regex, event| {
move |editor, regex, event| {
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
search_impl(
doc,
view,
editor,
&contents,
&regex,
Movement::Move,
direction,
scrolloff,
wrap_around,
false,
);
},
);
@ -1702,7 +1722,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
let count = cx.count();
let config = cx.editor.config();
let scrolloff = config.scrolloff;
let (view, doc) = current!(cx.editor);
let (_, doc) = current!(cx.editor);
let registers = &cx.editor.registers;
if let Some(query) = registers.read('/').and_then(|query| query.last()) {
let contents = doc.text().slice(..).to_string();
@ -1720,14 +1740,14 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
{
for _ in 0..count {
search_impl(
doc,
view,
cx.editor,
&contents,
&regex,
movement,
direction,
scrolloff,
wrap_around,
true,
);
}
} else {
@ -1825,7 +1845,7 @@ fn global_search(cx: &mut Context) {
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect()
},
move |_view, _doc, regex, event| {
move |_editor, regex, event| {
if event != PromptEvent::Validate {
return;
}
@ -2161,10 +2181,7 @@ fn ensure_selections_forward(cx: &mut Context) {
let selection = doc
.selection(view.id)
.clone()
.transform(|r| match r.direction() {
Direction::Forward => r,
Direction::Backward => r.flip(),
});
.transform(|r| r.with_direction(Direction::Forward));
doc.set_selection(view.id, selection);
}
@ -2410,11 +2427,11 @@ impl ui::menu::Item for MappableCommand {
match self {
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(),
},
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(),
},
}
@ -2465,13 +2482,13 @@ fn last_picker(cx: &mut Context) {
}
// 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);
enter_insert_mode(cx);
}
// 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);
let (view, doc) = current!(cx.editor);
@ -2504,18 +2521,23 @@ async fn make_format_callback(
) -> anyhow::Result<job::Callback> {
let format = format.await?;
let call: job::Callback = Box::new(move |editor, _compositor| {
let view_id = view!(editor).id;
if let Some(doc) = editor.document_mut(doc_id) {
if doc.version() == doc_version {
doc.apply(&format, view_id);
doc.append_changes_to_history(view_id);
doc.detect_indent_and_line_ending();
if let Modified::SetUnmodified = modified {
doc.reset_modified();
}
} else {
log::info!("discarded formatting changes because the document changed");
if !editor.documents.contains_key(&doc_id) {
return;
}
let scrolloff = editor.config().scrolloff;
let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor);
if doc.version() == doc_version {
doc.apply(&format, view.id);
doc.append_changes_to_history(view.id);
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)
@ -3709,7 +3731,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;
let (view, doc) = current!(cx.editor);
let text = doc.text();
@ -3744,9 +3766,21 @@ fn join_selections(cx: &mut Context) {
// TODO: joining multiple empty lines should be replaced by a single space.
// need to merge change ranges that touch
let transaction = Transaction::change(doc.text(), changes.into_iter());
// TODO: select inserted spaces
// .with_selection(selection);
// select inserted spaces
let transaction = if select_space {
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);
}
@ -3759,7 +3793,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
if remove { "remove:" } else { "keep:" }.into(),
Some(reg),
ui::completers::none,
move |view, doc, regex, event| {
move |editor, regex, event| {
let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
@ -3774,6 +3809,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) {
keep_or_remove_selections_impl(cx, false)
}
@ -4263,7 +4306,7 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
let root = syntax.tree().root_node();
let selection = doc.selection(view.id).clone().transform(|range| {
movement::goto_treesitter_object(
let new_range = movement::goto_treesitter_object(
text,
range,
object,
@ -4271,7 +4314,19 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
root,
lang_config,
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);
@ -4336,7 +4391,6 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
cx.on_next_key(move |cx, event| {
cx.editor.autoinfo = None;
cx.editor.pseudo_pending = None;
if let Some(ch) = event.char() {
let textobject = move |editor: &mut Editor| {
let (view, doc) = current!(editor);
@ -4385,33 +4439,31 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
}
});
if let Some((title, abbrev)) = match objtype {
textobject::TextObject::Inside => Some(("Match inside", "mi")),
textobject::TextObject::Around => Some(("Match around", "ma")),
let title = match objtype {
textobject::TextObject::Inside => "Match inside",
textobject::TextObject::Around => "Match around",
_ => 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) {

@ -2,7 +2,7 @@ use std::ops::Deref;
use super::*;
use helix_view::editor::{Action, ConfigEvent};
use helix_view::editor::{Action, CloseError, ConfigEvent};
use ui::completers::{self, Completer};
#[derive(Clone)]
@ -71,8 +71,29 @@ fn buffer_close_by_ids_impl(
doc_ids: &[DocumentId],
force: bool,
) -> anyhow::Result<()> {
for &doc_id in doc_ids {
editor.close_document(doc_id, force)?;
let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
.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(())
@ -513,23 +534,26 @@ fn force_write_quit(
force_quit(cx, &[], event)
}
/// Results an error if there are modified buffers remaining and sets editor error,
/// otherwise returns `Ok(())`
/// Results in an error if there are modified buffers remaining and sets editor
/// 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<()> {
let modified: Vec<_> = editor
let (modified_ids, modified_names): (Vec<_>, Vec<_>) = editor
.documents()
.filter(|doc| doc.is_modified())
.map(|doc| {
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
})
.collect();
if !modified.is_empty() {
.map(|doc| (doc.id(), doc.display_name()))
.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.len(),
modified
modified_names.len(),
modified_names
);
}
Ok(())
@ -1000,7 +1024,7 @@ fn lsp_restart(
.context("LSP not defined for the current document")?;
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.
let document_ids_to_refresh: Vec<DocumentId> = cx
@ -1196,18 +1220,41 @@ pub(super) fn goto_line_number(
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
match event {
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>()?;
goto_line_impl(cx.editor, NonZeroUsize::new(line));
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, line);
Ok(())
}

@ -59,9 +59,9 @@ pub fn default() -> HashMap<Mode, Keymap> {
":" => command_mode,
"i" => insert_mode,
"I" => prepend_to_line,
"I" => insert_at_line_start,
"a" => append_mode,
"A" => append_to_line,
"A" => insert_at_line_end,
"o" => open_below,
"O" => open_above,
@ -144,6 +144,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"<" => unindent,
"=" => format_selections,
"J" => join_selections,
"A-J" => join_selections_space,
"K" => keep_selections,
"A-K" => remove_selections,
@ -342,24 +343,27 @@ pub fn default() -> HashMap<Mode, Keymap> {
let insert = keymap!({ "Insert 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-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-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,
"C-r" => insert_register,
"up" => move_line_up,
"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!(
Mode::Normal => Keymap::new(normal),

@ -33,6 +33,7 @@ use super::statusline;
pub struct EditorView {
pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
pseudo_pending: Vec<KeyEvent>,
last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
@ -56,6 +57,7 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
pseudo_pending: Vec::new(),
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
@ -436,7 +438,8 @@ impl EditorView {
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
// extra loops if the code is deeply nested.
@ -702,6 +705,7 @@ impl EditorView {
let mut offset = 0;
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
let mut text = String::with_capacity(8);
@ -714,6 +718,12 @@ impl EditorView {
let x = viewport.x + offset;
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) {
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
} else {
@ -837,6 +847,7 @@ impl EditorView {
event: KeyEvent,
) -> Option<KeymapResult> {
let mut last_mode = mode;
self.pseudo_pending.extend(self.keymaps.pending());
let key_result = self.keymaps.get(mode, event);
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
@ -1314,6 +1325,11 @@ impl Component for EditorView {
}
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
let callback = cx.callback.take();
@ -1414,8 +1430,8 @@ impl Component for EditorView {
for key in self.keymaps.pending() {
disp.push_str(&key.key_sequence_format());
}
if let Some(pseudo_pending) = &cx.editor.pseudo_pending {
disp.push_str(pseudo_pending.as_str())
for key in &self.pseudo_pending {
disp.push_str(&key.key_sequence_format());
}
let style = cx.editor.theme.get("ui.text");
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_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 });
sig_text_para.render(sig_text_area.inner(&margin), surface);
sig_text_para.render(sig_text_area, surface);
if self.signature_doc.is_none() {
return;

@ -12,6 +12,8 @@ mod spinner;
mod statusline;
mod text;
use crate::compositor::{Component, Compositor};
use crate::job;
pub use completion::Completion;
pub use editor::EditorView;
pub use markdown::Markdown;
@ -24,7 +26,7 @@ pub use text::Text;
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
use helix_view::Editor;
use std::path::PathBuf;
@ -59,7 +61,7 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
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 doc_id = view.doc;
@ -106,11 +108,42 @@ pub fn regex_prompt(
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);
}
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::Position => render_position,
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::Spacer => render_spacer,
}
@ -154,16 +155,16 @@ where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
let modenames = &context.editor.config().statusline.mode;
write(
context,
format!(
" {} ",
if visible {
match context.editor.mode() {
Mode::Insert => "INS",
Mode::Select => "SEL",
Mode::Normal => "NOR",
Mode::Insert => &modenames.insert,
Mode::Select => &modenames.select,
Mode::Normal => &modenames.normal,
}
} else {
// 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)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,

@ -85,3 +85,131 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
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"
anyhow = "1"
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-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.25", optional = true }

@ -5,6 +5,7 @@ use helix_core::auto_pairs::AutoPairs;
use helix_core::Range;
use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize;
use std::borrow::Cow;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display;
@ -1038,6 +1039,12 @@ impl Document {
.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) ?
// -- LSP methods

@ -1,6 +1,6 @@
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
document::{Mode, SCRATCH_BUFFER_NAME},
document::Mode,
graphics::{CursorKind, Rect},
info::Info,
input::KeyEvent,
@ -28,7 +28,7 @@ use tokio::{
time::{sleep, Duration, Instant, Sleep},
};
use anyhow::{bail, Error};
use anyhow::Error;
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@ -260,6 +260,7 @@ pub struct StatusLineConfig {
pub center: Vec<StatusLineElement>,
pub right: Vec<StatusLineElement>,
pub separator: String,
pub mode: ModeConfig,
}
impl Default for StatusLineConfig {
@ -271,6 +272,25 @@ impl Default for StatusLineConfig {
center: vec![],
right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding],
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"),
}
}
}
@ -311,6 +331,9 @@ pub enum StatusLineElement {
/// The cursor position as a percent of the total file
PositionPercentage,
/// The total line numbers of the current file
TotalLineNumbers,
/// A single space
Spacer,
}
@ -529,18 +552,19 @@ impl Default for WhitespaceCharacters {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
#[serde(default, rename_all = "kebab-case")]
pub struct IndentGuidesConfig {
pub render: bool,
pub character: char,
// TODO: Make enum to also support background rainbow for indent guides
pub rainbow: bool,
pub skip_levels: u16,
}
impl Default for IndentGuidesConfig {
fn default() -> Self {
Self {
skip_levels: 0,
render: false,
character: '',
rainbow: false,
@ -647,7 +671,7 @@ pub struct Editor {
/// The currently applied editor theme. While previewing a theme, the previewed theme
/// is set here.
pub theme: Theme,
pub last_line_number: Option<usize>,
pub status_msg: Option<(Cow<'static, str>, Severity)>,
pub autoinfo: Option<Info>,
@ -656,7 +680,6 @@ pub struct Editor {
pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>,
pub pseudo_pending: Option<String>,
pub last_completion: Option<CompleteAction>,
@ -690,6 +713,14 @@ pub enum Action {
VerticalSplit,
}
/// Error thrown on failed document closed
pub enum CloseError {
/// Document doesn't exist
DoesNotExist,
/// Buffer is modified
BufferModified(String),
}
impl Editor {
pub fn new(
mut area: Rect,
@ -721,6 +752,7 @@ impl Editor {
syn_loader,
theme_loader,
last_theme: None,
last_line_number: None,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
@ -728,7 +760,6 @@ impl Editor {
idle_timer: Box::pin(sleep(conf.idle_timeout)),
last_motion: None,
last_completion: None,
pseudo_pending: None,
config,
auto_pairs,
exit_code: 0,
@ -848,7 +879,7 @@ impl Editor {
// try to find a language server based on the language name
let language_server = doc.language.as_ref().and_then(|language| {
ls.get(language)
ls.get(language, doc.path())
.map_err(|e| {
log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}",
@ -1048,19 +1079,14 @@ impl Editor {
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) {
Some(doc) => doc,
None => bail!("document does not exist"),
None => return Err(CloseError::DoesNotExist),
};
if !force && doc.is_modified() {
bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
);
return Err(CloseError::BufferModified(doc.display_name().into_owned()));
}
if let Some(language_server) = doc.language_server() {

@ -3,19 +3,28 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::Context;
use anyhow::{anyhow, Context, Result};
use helix_core::hashmap;
use helix_loader::merge_toml_values;
use log::warn;
use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer};
use toml::Value;
use toml::{map::Map, Value};
pub use crate::graphics::{Color, Modifier, Style};
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")
});
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"))
.expect("Failed to parse base 16 default theme")
});
@ -35,24 +44,51 @@ impl Loader {
}
/// 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" {
return Ok(self.default());
}
if name == "base16_default" {
return Ok(self.base16_default());
}
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
let path = if user_path.exists() {
user_path
self.load_theme(name, name, false).map(Theme::from)
}
// 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 {
self.default_dir.join(filename)
theme_toml
};
let data = std::fs::read(&path)?;
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
Ok(theme_toml)
}
pub fn read_names(path: &Path) -> Vec<String> {
@ -70,6 +106,53 @@ impl Loader {
.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
pub fn names(&self) -> Vec<String> {
let mut names = Self::read_names(&self.user_dir);
@ -106,6 +189,21 @@ pub struct Theme {
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);
Self {
styles,
scopes,
highlights,
}
}
}
impl<'de> Deserialize<'de> for Theme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@ -166,14 +264,53 @@ impl<'de> Deserialize<'de> for Theme {
}
Ok(Self {
scopes,
styles,
scopes,
highlights,
rainbow_length,
})
}
}
fn build_theme_values(
values: Result<HashMap<String, Value>>,
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
let mut styles = HashMap::new();
let mut scopes = Vec::new();
let mut highlights = Vec::new();
if let Ok(mut colors) = values {
// TODO: alert user of parsing failures in editor
let palette = colors
.remove("palette")
.map(|value| {
ThemePalette::try_from(value).unwrap_or_else(|err| {
warn!("{}", err);
ThemePalette::default()
})
})
.unwrap_or_default();
// remove inherits from value to prevent errors
let _ = colors.remove("inherits");
styles.reserve(colors.len());
scopes.reserve(colors.len());
highlights.reserve(colors.len());
for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}
// these are used both as UI and as highlights
styles.insert(name.clone(), style);
scopes.push(name);
highlights.push(style);
}
}
(styles, scopes, highlights)
}
impl Theme {
#[inline]
pub fn highlight(&self, index: usize) -> Style {

@ -911,7 +911,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
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]]
name = "markdown.inline"
@ -923,7 +923,7 @@ grammar = "markdown_inline"
[[grammar]]
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]]
name = "dart"
@ -1781,3 +1781,25 @@ language-server = { command = "bass", args = ["--lsp"] }
[[grammar]]
name = "bass"
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))
((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))
((plus_metadata) @injection.content (#set! injection.language "toml") (#set! injection.include-unnamed-children))

@ -253,6 +253,9 @@
(function_item
name: (identifier) @function)
(function_signature_item
name: (identifier) @function)
; ---
; 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>
"attribute" = "#dc7759"
"keyword" = { fg = "#dcb659", modifiers = ["bold"] }
"keyword.directive" = "#dcb659"
"namespace" = "#d32c5d"
"punctuation" = "#dc7759"
"punctuation.delimiter" = "#dc7759"
"operator" = { fg = "#dc7759", modifiers = ["bold"] }
"special" = "#7fdc59"
"variable.other.member" = "#c6b8ad"
"variable" = "#c6b8ad"
"variable.parameter" = "#c6b8ad"
"type" = "#dc597f"
"type.builtin" = { fg = "#d32c5d", modifiers = ["bold"] }
"constructor" = "#dc597f"
"function" = "#59dcd8"
"function.macro" = { fg = "#dc7759", modifiers = ["bold"] }
"function.builtin" = { fg = "#59dcd8", modifiers = ["bold"] }
"comment" = "#627d9d"
"variable.builtin" = "#c6b8ad"
"constant" = "#59dcb7"
"constant.builtin" = "#59dcb7"
"string" = "#59dcb7"
"constant.numeric" = "#59c0dc"
"constant.character.escape" = { fg = "#7fdc59", modifiers = ["bold"] }
"label" = "#59c0dc"
"attribute" = "bogster0"
"keyword" = { fg = "bogster1", modifiers = ["bold"] }
"keyword.directive" = "bogster1"
"namespace" = "bogster2"
"punctuation" = "bogster0"
"punctuation.delimiter" = "bogster0"
"operator" = { fg = "bogster0", modifiers = ["bold"] }
"special" = "bogster3"
"variable.other.member" = "bogster4"
"variable" = "bogster4"
"variable.parameter" = "bogster4"
"type" = "bogster5"
"type.builtin" = { fg = "bogster2", modifiers = ["bold"] }
"constructor" = "bogster5"
"function" = "bogster6"
"function.macro" = { fg = "bogster0", modifiers = ["bold"] }
"function.builtin" = { fg = "bogster6", modifiers = ["bold"] }
"comment" = "bogster7"
"variable.builtin" = "bogster4"
"constant" = "bogster8"
"constant.builtin" = "bogster8"
"string" = "bogster8"
"constant.numeric" = "bogster9"
"constant.character.escape" = { fg = "bogster3", modifiers = ["bold"] }
"label" = "bogster9"
"module" = "#d32c5d"
"module" = "bogster2"
# TODO
"markup.heading" = "blue"
@ -38,39 +38,60 @@
"markup.quote" = "cyan"
"markup.raw" = "green"
"diff.plus" = "#59dcb7"
"diff.delta" = "#dc7759"
"diff.minus" = "#dc597f"
"diff.plus" = "bogster8"
"diff.delta" = "bogster0"
"diff.minus" = "bogster5"
"ui.background" = { bg = "#161c23" }
"ui.linenr" = { fg = "#415367" }
"ui.linenr.selected" = { fg = "#e5ded6" } # TODO
"ui.cursorline" = { bg = "#131920" }
"ui.statusline" = { fg = "#e5ded6", bg = "#232d38" }
"ui.statusline.inactive" = { fg = "#c6b8ad", bg = "#232d38" }
"ui.bufferline" = { fg = "#627d9d", bg = "#131920" }
"ui.bufferline.active" = { fg = "#e5ded6", bg = "#232d38" }
"ui.popup" = { bg = "#232d38" }
"ui.window" = { bg = "#232d38" }
"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
"ui.background" = { bg = "bogster10" }
"ui.linenr" = { fg = "bogster11" }
"ui.linenr.selected" = { fg = "bogster12" } # TODO
"ui.cursorline" = { bg = "bogster13" }
"ui.statusline" = { fg = "bogster12", bg = "bogster14" }
"ui.statusline.inactive" = { fg = "bogster4", bg = "bogster14" }
"ui.popup" = { bg = "bogster14" }
"ui.window" = { bg = "bogster14" }
"ui.help" = { bg = "bogster14", fg = "bogster12" }
"ui.text" = { fg = "#e5ded6" }
"ui.text.focus" = { fg = "#e5ded6", modifiers= ["bold"] }
"ui.virtual.whitespace" = "#627d9d"
"ui.virtual.ruler" = { bg = "#131920" }
"ui.statusline.normal" = { fg = "bogster10", bg = "bogster9", modifiers = [ "bold" ]}
"ui.statusline.insert" = { fg = "bogster10", bg = "bogster3", modifiers = [ "bold" ]}
"ui.statusline.select" = { fg = "bogster10", bg = "bogster2", modifiers = [ "bold" ] }
"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" = { fg = "#313f4e", bg = "#dc7759" }
"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] }
"ui.cursor.match" = { fg = "bogster15", bg = "bogster0" }
"ui.cursor" = { fg = "bogster16", modifiers = ["reversed"] }
"ui.menu" = { fg = "#e5ded6bg", bg = "#232d38" }
"ui.menu.selected" = { bg = "#313f4e" }
"ui.menu" = { fg = "bogster12", bg = "bogster14" }
"ui.menu.selected" = { bg = "bogster15" }
"warning" = "#dc7759"
"error" = "#dc597f"
"info" = "#59dcb7"
"hint" = "#59c0dc"
"warning" = "bogster0"
"error" = "bogster5"
"info" = "bogster8"
"hint" = "bogster9"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[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"

@ -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.linenr" = "grey"
"ui.linenr.selected" = "fg"
"ui.cursorline.primary" = { bg = "bg2" }
"ui.cursorline.primary" = { bg = "bg1" }
"ui.statusline" = { fg = "fg", bg = "bg3" }
"ui.statusline.inactive" = { fg = "grey", bg = "bg1" }
"ui.popup" = { fg = "grey", bg = "bg2" }
@ -86,7 +86,7 @@ bg0 = "#2c2e34"
bg1 = "#33353f"
bg2 = "#363944"
bg3 = "#3b3e48"
bg4 = "#414550"
bg4 = "#545862"
bg_red = "#ff6077"
diff_red = "#55393d"
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
@ -1054,6 +1054,24 @@ letters! that is not good grammar. you can fix this.
=================================================================
= =
=================================================================
=================================================================
This tutorial is still a work-in-progress.
More sections are planned.

Loading…
Cancel
Save