diff --git a/book/src/configuration.md b/book/src/configuration.md index c55426c01..83ab3f6b9 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -255,8 +255,8 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r | Key | Description | Default | |-----|-------------|---------| -| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` | -| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below | +| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `"none"` | +| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below | Example @@ -267,11 +267,14 @@ render = "all" [editor.whitespace.render] space = "all" tab = "all" +nbsp = "none" +nnbsp = "none" newline = "none" [editor.whitespace.characters] space = "·" nbsp = "⍽" +nnbsp = "␣" tab = "→" newline = "⏎" tabpad = "·" # Tabs will look like "→···" (depending on tab width) diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index b74a8cdb7..2030949eb 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -128,6 +128,7 @@ | ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml-interface | ✓ | | | `ocamllsp` | | odin | ✓ | | ✓ | `ols` | +| ohm | ✓ | ✓ | ✓ | | | opencl | ✓ | ✓ | ✓ | `clangd` | | openscad | ✓ | | | `openscad-lsp` | | org | ✓ | | | | diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index dc61ca2e3..b571b83c2 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -341,6 +341,7 @@ pub struct TextRenderer<'a> { pub indent_guide_style: Style, pub newline: String, pub nbsp: String, + pub nnbsp: String, pub space: String, pub tab: String, pub virtual_tab: String, @@ -395,6 +396,11 @@ impl<'a> TextRenderer<'a> { } else { " ".to_owned() }; + let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All { + ws_chars.nnbsp.into() + } else { + " ".to_owned() + }; let text_style = theme.get("ui.text"); @@ -405,6 +411,7 @@ impl<'a> TextRenderer<'a> { indent_guide_char: editor_config.indent_guides.character.into(), newline, nbsp, + nnbsp, space, tab, virtual_tab, @@ -448,6 +455,7 @@ impl<'a> TextRenderer<'a> { let width = grapheme.width(); let space = if is_virtual { " " } else { &self.space }; let nbsp = if is_virtual { " " } else { &self.nbsp }; + let nnbsp = if is_virtual { " " } else { &self.nnbsp }; let tab = if is_virtual { &self.virtual_tab } else { @@ -461,6 +469,7 @@ impl<'a> TextRenderer<'a> { // TODO special rendering for other whitespaces? Grapheme::Other { ref g } if g == " " => space, Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp, + Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp, Grapheme::Other { ref g } => g, Grapheme::Newline => &self.newline, }; diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs index c30aa6e03..67c4db443 100644 --- a/helix-tui/src/widgets/reflow.rs +++ b/helix-tui/src/widgets/reflow.rs @@ -4,6 +4,7 @@ use helix_core::unicode::width::UnicodeWidthStr; use unicode_segmentation::UnicodeSegmentation; const NBSP: &str = "\u{00a0}"; +const NNBSP: &str = "\u{202f}"; /// A state machine to pack styled symbols into lines. /// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming @@ -58,7 +59,8 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> { let mut symbols_exhausted = true; for StyledGrapheme { symbol, style } in &mut self.symbols { symbols_exhausted = false; - let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP; + let symbol_whitespace = + symbol.chars().all(&char::is_whitespace) && symbol != NBSP && symbol != NNBSP; // Ignore characters wider that the total max width. if symbol.width() as u16 > self.max_line_width @@ -496,6 +498,20 @@ mod test { assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]); } + #[test] + fn line_composer_word_wrapper_nnbsp() { + let width = 20; + let text = "AAAAAAAAAAAAAAA AAAA\u{202f}AAA"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); + assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{202f}AAA",]); + + // Ensure that if the character was a regular space, it would be wrapped differently. + let text_space = text.replace('\u{202f}', " "); + let (word_wrapper_space, _) = + run_composer(Composer::WordWrapper { trim: true }, &text_space, width); + assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]); + } + #[test] fn line_composer_word_wrapper_preserve_indentation() { let width = 20; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 712fd72f0..7f60356b6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -722,6 +722,7 @@ pub enum WhitespaceRender { default: Option, space: Option, nbsp: Option, + nnbsp: Option, tab: Option, newline: Option, }, @@ -753,6 +754,14 @@ impl WhitespaceRender { } } } + pub fn nnbsp(&self) -> WhitespaceRenderValue { + match *self { + Self::Basic(val) => val, + Self::Specific { default, nnbsp, .. } => { + nnbsp.or(default).unwrap_or(WhitespaceRenderValue::None) + } + } + } pub fn tab(&self) -> WhitespaceRenderValue { match *self { Self::Basic(val) => val, @@ -776,6 +785,7 @@ impl WhitespaceRender { pub struct WhitespaceCharacters { pub space: char, pub nbsp: char, + pub nnbsp: char, pub tab: char, pub tabpad: char, pub newline: char, @@ -786,6 +796,7 @@ impl Default for WhitespaceCharacters { Self { space: '·', // U+00B7 nbsp: '⍽', // U+237D + nnbsp: '␣', // U+2423 tab: '→', // U+2192 newline: '⏎', // U+23CE tabpad: ' ', diff --git a/languages.toml b/languages.toml index cbbfc5a91..9eb80414f 100644 --- a/languages.toml +++ b/languages.toml @@ -3408,3 +3408,25 @@ formatter = { command = "prettier", args = ['--parser', 'glimmer'] } [[grammar]] name = "glimmer" source = { git = "https://github.com/ember-tooling/tree-sitter-glimmer", rev = "5dc6d1040e8ff8978ff3680e818d85447bbc10aa" } + +[[language]] +name = "ohm" +scope = "source.ohm" +injection-regex = "ohm" +file-types = ["ohm"] +comment-token = "//" +block-comment-tokens = [ + { start = "/*", end = "*/" }, + { start = "/**", end = "*/" }, +] +indent = { tab-width = 2, unit = " " } + +[language.auto-pairs] +'"' = '"' +'{' = '}' +'(' = ')' +'<' = '>' + +[[grammar]] +name = "ohm" +source = { git = "https://github.com/novusnota/tree-sitter-ohm", rev = "80f14f0e477ddacc1e137d5ed8e830329e3fb7a3" } diff --git a/runtime/queries/ohm/highlights.scm b/runtime/queries/ohm/highlights.scm new file mode 100644 index 000000000..68477aacc --- /dev/null +++ b/runtime/queries/ohm/highlights.scm @@ -0,0 +1,122 @@ +; See: https://docs.helix-editor.com/master/themes.html#syntax-highlighting + +; attribute +; --------- + +(case_name) @attribute + +; comment.line +; ------------ + +[ + (singleline_comment) + (rule_descr) +] @comment.line + +; comment.block +; ------------- + +(multiline_comment) @comment.block + +; function.method +; --------------- + +(rule + name: (identifier) @function.method) + +; function.builtin +; ---------------- + +; Lexical +((identifier) @function.builtin + (#any-of? @function.builtin + "any" + "alnum" + "end" + "digit" "hexDigit" + "letter" + "space" + "lower" "upper" "caseInsensitive" + "listOf" "nonemptyListOf" "emptyListOf" + "applySyntactic") + (#is-not? local)) + +; Syntactic +((identifier) @function.builtin + (#any-of? @function.builtin "ListOf" "NonemptyListOf" "EmptyListOf") + (#is-not? local)) + +; function.method (continuing) +; --------------- + +(term + base: (identifier) @function.method) + +; string.special +; -------------- + +(escape_char) @constant.character.escape + +; string +; ------ + +[ + (terminal_string) + (one_char_terminal) +] @string + +; type +; ---- + +(super_grammar + name: (identifier) @type) + +(grammar + name: (identifier) @type) + +; operator +; -------- + +[ + ; "=" ":=" "+=" + (define) (override) (extend) + + ; "&" "~" + (lookahead) (negative_lookahead) + + ; "#" + (lexification) + + ; "*" "+" "?" + (zero_or_more) (one_or_more) (zero_or_one) + + ; "..." + (super_splice) + + "<:" ".." "|" +] @operator + +; punctuation.bracket +; ------------------- + +[ + "<" + ">" + "{" + "}" +] @punctuation.bracket + +(alt + "(" @punctuation.bracket + ")" @punctuation.bracket) + +; punctuation.delimiter +; --------------------- + +"," @punctuation.delimiter + +; variable.parameter +; ------------------ + +(formals + (identifier) @variable.parameter) diff --git a/runtime/queries/ohm/indents.scm b/runtime/queries/ohm/indents.scm new file mode 100644 index 000000000..f56119aec --- /dev/null +++ b/runtime/queries/ohm/indents.scm @@ -0,0 +1,37 @@ +; See: https://docs.helix-editor.com/guides/indent.html + +; indent +; ------ + +[ + ; <..., ...> + (formals) + (params) + + ; (...| ...) + (alt) +] @indent + +; outdent +; ------- + +[ + "}" + ")" + ">" +] @outdent + +; align +; ----- + +; | ... | ... +(rule_body + . (top_level_term) @anchor + (#set! "scope" "tail")) @align + +; N/A or unused: +; -------------- +; indent.always +; outdent.always +; extend +; extend.prevent-once diff --git a/runtime/queries/ohm/injections.scm b/runtime/queries/ohm/injections.scm new file mode 100644 index 000000000..dc50a7654 --- /dev/null +++ b/runtime/queries/ohm/injections.scm @@ -0,0 +1,7 @@ +; See: https://docs.helix-editor.com/guides/injection.html + +((singleline_comment) @injection.content + (#set! injection.language "comment")) + +((multiline_comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/ohm/textobjects.scm b/runtime/queries/ohm/textobjects.scm new file mode 100644 index 000000000..855ad8de7 --- /dev/null +++ b/runtime/queries/ohm/textobjects.scm @@ -0,0 +1,40 @@ +; See: https://docs.helix-editor.com/guides/textobject.html + +; function.inside & around +; ------------------------ + +(rule + body: (_) @function.inside) @function.around + +; class.inside & around +; --------------------- + +(grammar + body: (_) @class.inside) @class.around + +; parameter.inside & around +; ------------------------- + +(formals + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(rule_body + ((_) @parameter.inside . "|"? @parameter.around) @parameter.around) + +(params + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(alt + ((_) @parameter.inside . "|"? @parameter.around) @parameter.around) + +; comment.inside +; -------------- + +(multiline_comment)+ @comment.inside +(singleline_comment)+ @comment.inside + +; comment.around +; -------------- + +(multiline_comment)+ @comment.around +(singleline_comment)+ @comment.around diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 65ba76e4b..5fcb72317 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -89,6 +89,7 @@ "ui.virtual.ruler" = { bg = "surface0" } "ui.virtual.indent-guide" = "surface0" "ui.virtual.inlay-hint" = { fg = "overlay0", bg = "base" } +"ui.virtual.jump-label" = { fg = "rosewater", modifiers = ["bold"] } "ui.selection" = { bg = "surface1" } diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 819cd62aa..11be7fd6a 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -90,6 +90,7 @@ "ui.virtual.ruler" = { bg = "borders" } "ui.virtual.indent-guide" = { fg = "dark_gray4" } "ui.virtual.inlay-hint" = { fg = "dark_gray5"} +"ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] } "warning" = { fg = "gold2" } "error" = { fg = "red" } diff --git a/runtime/themes/horizon-dark.toml b/runtime/themes/horizon-dark.toml index 2ab10e911..b879b432c 100644 --- a/runtime/themes/horizon-dark.toml +++ b/runtime/themes/horizon-dark.toml @@ -27,8 +27,8 @@ namespace = "orange" "ui.gutter" = "gray" "ui.gutter.selected" = "light-gray" "ui.selection" = { bg = "selection" } -"ui.virtual.indent-guide" = { fg = "gray"} -"ui.virtual.whitespace" = { fg = "light-gray"} +"ui.virtual.indent-guide" = { fg = "gray" } +"ui.virtual.whitespace" = { fg = "light-gray" } "ui.statusline" = { bg = "dark-bg", fg = "light-gray" } "ui.popup" = { bg = "dark-bg", fg = "orange" } "ui.help" = { bg = "dark-bg", fg = "orange" } @@ -37,13 +37,14 @@ namespace = "orange" "ui.window" = "selection" "ui.bufferline" = { bg = "dark-bg", fg = "light-gray" } "ui.bufferline.active" = { bg = "dark-bg", fg = "orange" } +"ui.virtual.jump-label" = { fg = "pink", modifiers = ["bold"] } # Diagnostics "diagnostic" = { underline = { style = "curl" } } "diagnostic.hint" = { underline = { color = "green", style = "curl" } } "diagnostic.info" = { underline = { color = "blue", style = "curl" } } -"diagnostics.error" = { underline = { color = "red", style = "curl"} } -"diagnostics.warning" = { underline = { color = "orange", style = "curl"} } +"diagnostics.error" = { underline = { color = "red", style = "curl" } } +"diagnostics.warning" = { underline = { color = "orange", style = "curl" } } warning = { fg = "orange", modifiers = ["bold"] } error = { fg = "red", modifiers = ["bold"] } @@ -62,7 +63,7 @@ hint = { fg = "green", modifiers = ["bold"] } "markup.quote" = "orange" "markup.raw" = "orange" -"diff.plus" = "green" +"diff.plus" = "green" "diff.minus" = "red" "diff.delta" = "orange" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index eae11172e..5b049aff9 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -56,6 +56,7 @@ "ui.virtual.whitespace" = { fg = "light-gray" } "ui.virtual.ruler" = { bg = "gray" } "ui.virtual.inlay-hint" = { fg = "light-gray" } +"ui.virtual.jump-label" = { fg = "light-gray", modifiers = ["bold"] } "ui.cursor" = { fg = "white", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index 456237e94..27b7a630e 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -37,6 +37,7 @@ "ui.text.focus" = { bg = "overlay" } "ui.text.info" = { fg = "subtle" } +"ui.virtual.jump-label" = { fg = "love", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "overlay" } "ui.virtual.whitespace" = { fg = "highlight_high" } "ui.virtual.indent-guide" = { fg = "muted" }