From 2113b1bb2fe4d69eada555d562cbcf723201998a Mon Sep 17 00:00:00 2001 From: nuid32 <91177333+nuid32@users.noreply.github.com> Date: Sat, 1 Oct 2022 19:11:15 +0500 Subject: [PATCH 01/48] themes: Add onedarker (#3980) --- runtime/themes/onedarker.toml | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 runtime/themes/onedarker.toml diff --git a/runtime/themes/onedarker.toml b/runtime/themes/onedarker.toml new file mode 100644 index 00000000..1ad6f7c3 --- /dev/null +++ b/runtime/themes/onedarker.toml @@ -0,0 +1,97 @@ +# Author : nuid32 + +"attribute" = { fg = "yellow" } +"comment" = { fg = "light-gray", modifiers = ["italic"] } +"constant" = { fg = "green" } +"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 = "purple" } +"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 = "#454D50" +faint-gray = "#ABB2BF" +light-gray = "#C8CCD4" +linenr = "#282C34" From cc257e9bf9a7eee7e68e04d04523a8fae10807cd Mon Sep 17 00:00:00 2001 From: Roberto Vidal Date: Sat, 1 Oct 2022 16:13:52 +0200 Subject: [PATCH 02/48] Add support for webassembly text format (#4040) --- book/src/generated/lang-support.md | 2 ++ languages.toml | 22 ++++++++++++++++++++++ runtime/queries/wast/highlights.scm | 21 +++++++++++++++++++++ runtime/queries/wat/highlights.scm | 17 +++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 runtime/queries/wast/highlights.scm create mode 100644 runtime/queries/wat/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 86c26042..5c64d097 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -119,6 +119,8 @@ | vala | ✓ | | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | | vue | ✓ | | | `vls` | +| wast | ✓ | | | | +| wat | ✓ | | | | | wgsl | ✓ | | | `wgsl_analyzer` | | xit | ✓ | | | | | yaml | ✓ | | ✓ | `yaml-language-server` | diff --git a/languages.toml b/languages.toml index ab204fcc..d7909b6d 100644 --- a/languages.toml +++ b/languages.toml @@ -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" } diff --git a/runtime/queries/wast/highlights.scm b/runtime/queries/wast/highlights.scm new file mode 100644 index 00000000..ef5b547e --- /dev/null +++ b/runtime/queries/wast/highlights.scm @@ -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 diff --git a/runtime/queries/wat/highlights.scm b/runtime/queries/wat/highlights.scm new file mode 100644 index 00000000..007e3bbf --- /dev/null +++ b/runtime/queries/wat/highlights.scm @@ -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 From c9584251f321a8540cf530561896b2f48f0b76a2 Mon Sep 17 00:00:00 2001 From: zensayyy <99101223+zensayyy@users.noreply.github.com> Date: Sat, 1 Oct 2022 16:32:09 +0200 Subject: [PATCH 03/48] Ensure cursor in view after format (#4047) Co-authored-by: Michael Davis --- helix-term/src/commands.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c87ad0ca..fb1a4b38 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2504,18 +2504,23 @@ async fn make_format_callback( ) -> anyhow::Result { 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) From 5b5f1bd39ae46fdf68ba758b29bdb29200394c86 Mon Sep 17 00:00:00 2001 From: nuid32 <91177333+nuid32@users.noreply.github.com> Date: Sat, 1 Oct 2022 20:37:18 +0500 Subject: [PATCH 04/48] Adjust light-gray in onedarker theme (#4060) --- runtime/themes/onedarker.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/themes/onedarker.toml b/runtime/themes/onedarker.toml index 1ad6f7c3..1eb1a90c 100644 --- a/runtime/themes/onedarker.toml +++ b/runtime/themes/onedarker.toml @@ -93,5 +93,5 @@ black = "#16181A" light-black = "#2C323C" gray = "#454D50" faint-gray = "#ABB2BF" -light-gray = "#C8CCD4" +light-gray = "#636C6E" linenr = "#282C34" From 6caa7a7f566e67c50537014b3e97fa8e65a8b7b3 Mon Sep 17 00:00:00 2001 From: nuid32 <91177333+nuid32@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:19:55 +0500 Subject: [PATCH 05/48] Onedarker theme: some improvements (#4069) --- runtime/themes/onedarker.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/themes/onedarker.toml b/runtime/themes/onedarker.toml index 1eb1a90c..665f10ed 100644 --- a/runtime/themes/onedarker.toml +++ b/runtime/themes/onedarker.toml @@ -2,7 +2,7 @@ "attribute" = { fg = "yellow" } "comment" = { fg = "light-gray", modifiers = ["italic"] } -"constant" = { fg = "green" } +"constant" = { fg = "gold" } "constant.numeric" = { fg = "gold" } "constant.builtin" = { fg = "gold" } "constant.character.escape" = { fg = "gold" } @@ -69,7 +69,7 @@ diagnostic = { modifiers = ["underlined"] } "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 = "purple" } +"ui.text" = { fg = "white" } "ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] } "ui.help" = { fg = "white", bg = "gray" } @@ -91,7 +91,7 @@ cyan = "#46A6B2" white = "#ABB2BF" black = "#16181A" light-black = "#2C323C" -gray = "#454D50" +gray = "#252D30" faint-gray = "#ABB2BF" light-gray = "#636C6E" linenr = "#282C34" From 8c2cc4301742d9d759a8c2964ade0719d4b15645 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sun, 2 Oct 2022 15:23:23 -0400 Subject: [PATCH 06/48] diff full-doc LSP edits (#4041) Co-authored-by: Michael Davis --- helix-lsp/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index cb234357..0717fc04 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -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| { From 9d1793c45b22a6dce0a08937717887189b46c492 Mon Sep 17 00:00:00 2001 From: Matt Freitas-Stavola Date: Sun, 2 Oct 2022 12:32:30 -0700 Subject: [PATCH 07/48] Add pseudo_pending for t/T/f/F (#4062) --- helix-term/src/commands.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fb1a4b38..264ab5bb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1086,18 +1086,27 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } -fn will_find_char(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) -where +fn will_find_char( + cx: &mut Context, + search_fn: F, + inclusive: bool, + extend: bool, + pseudo_pending: &str, +) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, { // TODO: count is reset to 1 before next key so we move it into the closure here. // Would be nice to carry over. let count = cx.count(); + cx.editor.pseudo_pending = Some(pseudo_pending.to_string()); + // need to wait for next key // TODO: should this be done by grapheme rather than char? For example, // we can't properly handle the line-ending CRLF case here in terms of char. cx.on_next_key(move |cx, event| { + cx.editor.pseudo_pending = None; + let ch = match event { KeyEvent { code: KeyCode::Enter, @@ -1200,35 +1209,35 @@ fn find_prev_char_impl( } fn find_till_char(cx: &mut Context) { - will_find_char(cx, find_next_char_impl, false, false) + will_find_char(cx, find_next_char_impl, false, false, "t") } fn find_next_char(cx: &mut Context) { - will_find_char(cx, find_next_char_impl, true, false) + will_find_char(cx, find_next_char_impl, true, false, "f") } fn extend_till_char(cx: &mut Context) { - will_find_char(cx, find_next_char_impl, false, true) + will_find_char(cx, find_next_char_impl, false, true, "t") } fn extend_next_char(cx: &mut Context) { - will_find_char(cx, find_next_char_impl, true, true) + will_find_char(cx, find_next_char_impl, true, true, "f") } fn till_prev_char(cx: &mut Context) { - will_find_char(cx, find_prev_char_impl, false, false) + will_find_char(cx, find_prev_char_impl, false, false, "T") } fn find_prev_char(cx: &mut Context) { - will_find_char(cx, find_prev_char_impl, true, false) + will_find_char(cx, find_prev_char_impl, true, false, "F") } fn extend_till_prev_char(cx: &mut Context) { - will_find_char(cx, find_prev_char_impl, false, true) + will_find_char(cx, find_prev_char_impl, false, true, "T") } fn extend_prev_char(cx: &mut Context) { - will_find_char(cx, find_prev_char_impl, true, true) + will_find_char(cx, find_prev_char_impl, true, true, "F") } fn repeat_last_motion(cx: &mut Context) { From 18f6ec7a8eb9ff7d46d4ec1bba52f48364e9c9d7 Mon Sep 17 00:00:00 2001 From: David <12832280+David-Else@users.noreply.github.com> Date: Mon, 3 Oct 2022 15:14:16 +0100 Subject: [PATCH 08/48] Update treesitter markdown (#4078) * Update treesitter markdown * Update inline and add table injections --- languages.toml | 4 ++-- runtime/queries/markdown/injections.scm | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index d7909b6d..def55182 100644 --- a/languages.toml +++ b/languages.toml @@ -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" diff --git a/runtime/queries/markdown/injections.scm b/runtime/queries/markdown/injections.scm index f94b9f98..e184db15 100644 --- a/runtime/queries/markdown/injections.scm +++ b/runtime/queries/markdown/injections.scm @@ -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)) From bcba5d67f9b8650936c391f6c113945291941f51 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 3 Oct 2022 10:33:20 -0400 Subject: [PATCH 09/48] Add goto preview (#2982) --- helix-term/src/commands/typed.rs | 39 +++++++++++++++++++++++++------- helix-view/src/editor.rs | 3 ++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 6d0ced65..d5a368a2 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1196,18 +1196,41 @@ pub(super) fn goto_line_number( args: &[Cow], 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::()?; - goto_line_impl(cx.editor, NonZeroUsize::new(line)); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, line); Ok(()) } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5eff9983..e804a864 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -643,7 +643,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, pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option, @@ -717,6 +717,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, From 57dc5fbe3aab5807e5895e37e609310b684e2c15 Mon Sep 17 00:00:00 2001 From: A-Walrus <58790821+A-Walrus@users.noreply.github.com> Date: Mon, 3 Oct 2022 17:33:48 +0300 Subject: [PATCH 10/48] Show "Invalid regex" message on enter (Validate) (#3049) * Show "Invalid regex" message on enter (Validate) * Reset selection on invalid regex * Add popup for invalid regex * Replace set_position with position * Make popup auto close --- helix-term/src/ui/mod.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 60ad3b24..ba809d9b 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -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; @@ -110,7 +112,37 @@ pub fn regex_prompt( 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 + } + } } } } From 2fac9e24e565e976a8af8d82a4b6f2755a82a074 Mon Sep 17 00:00:00 2001 From: Christoph Schmidler Date: Mon, 3 Oct 2022 16:34:29 +0200 Subject: [PATCH 11/48] Inherit theme (#3067) * Add RawTheme to handle inheritance with theme palette * Add a intermediate step in theme loading it uses RawTheme struct to load the original ThemePalette, so we can merge it with the inherited one. * Load default themes via RawThemes, remove Theme deserialization * Allow naming custom theme same as inherited one * Remove RawTheme and use toml::Value directly * Resolve all review changes resulting in a cleaner code * Simplify return for Loader::load * Add implementation to avoid extra step for loading of base themes --- Cargo.lock | 1 + helix-view/Cargo.toml | 1 + helix-view/src/theme.rs | 192 +++++++++++++++++++++++++++++++--------- 3 files changed, 152 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5edcaac..6537543a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,6 +523,7 @@ dependencies = [ "futures-util", "helix-core", "helix-dap", + "helix-loader", "helix-lsp", "helix-tui", "log", diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 266a5732..b96a537d 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -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 } diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index fa5fa702..85f5cc13 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -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 = 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 = 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 { + pub fn load(&self, name: &str) -> Result { 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 { + 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 { @@ -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 { + 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 { let mut names = Self::read_names(&self.user_dir); @@ -105,52 +188,77 @@ pub struct Theme { highlights: Vec