From c7068d33d7f325ef3624f2a18ab37c8418829dbb Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Mon, 29 Dec 2025 15:56:51 +0000 Subject: [PATCH] Feat: add hover boxes and diagnostics from lsp --- .gitignore | 2 + .gitmodules | 8 + Makefile | 8 + README.md | 3 + grammar/hover.scm | 328 +++++++++++++++++++++++++++++++ grammar/javascript.scm | 3 +- grammar/man.scm | 23 +++ grammar/markdown.scm | 6 + grammar/ruby.scm | 6 + grammar/typescript.scm | 316 ++++++++++++++++++++++++++++++ include/editor.h | 128 +------------ include/hover.h | 37 ++++ include/lsp.h | 1 + include/maps.h | 7 + include/spans.h | 101 ++++++++++ include/ts.h | 9 + include/ts_def.h | 28 +++ include/ui.h | 9 +- include/utils.h | 11 ++ libs/tree-sitter-man | 1 + libs/tree-sitter-typescript | 1 + samples/lua.lua | 2 +- samples/markdown.md | 7 +- samples/ruby.rb | 7 + src/editor.cc | 107 ++++++++++- src/editor_events.cc | 138 ++++++++++++- src/hover.cc | 373 ++++++++++++++++++++++++++++++++++++ src/lsp.cc | 27 ++- src/renderer.cc | 119 ++++++++++-- src/ts.cc | 29 ++- src/utils.cc | 120 +++++++++++- 31 files changed, 1782 insertions(+), 183 deletions(-) create mode 100644 grammar/hover.scm create mode 100644 grammar/man.scm create mode 100644 grammar/typescript.scm create mode 100644 include/hover.h create mode 100644 include/spans.h create mode 160000 libs/tree-sitter-man create mode 160000 libs/tree-sitter-typescript create mode 100644 src/hover.cc diff --git a/.gitignore b/.gitignore index 7e6ef83..c5dba65 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ samples/t_* build bin +grammar/.*.scm + __old__ diff --git a/.gitmodules b/.gitmodules index cb88702..0ea80f4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -130,3 +130,11 @@ path = libs/tree-sitter-markdown url = https://github.com/tree-sitter-grammars/tree-sitter-markdown.git ignore = dirty +[submodule "libs/tree-sitter-typescript"] + path = libs/tree-sitter-typescript + url = https://github.com/tree-sitter/tree-sitter-typescript.git + ignore = dirty +[submodule "libs/tree-sitter-man"] + path = libs/tree-sitter-man + url = https://github.com/ribru17/tree-sitter-man.git + ignore = dirty diff --git a/Makefile b/Makefile index d371a86..72e03dc 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,8 @@ TREE_SITTER_LIBS := $(wildcard libs/tree-sitter-*/libtree-sitter*.a) PHP_LIB := libs/tree-sitter-php/php/libtree-sitter-php.a +TSX_LIB := libs/tree-sitter-typescript/tsx/libtree-sitter-tsx.a + NGINX_OBJ_PARSER := libs/tree-sitter-nginx/build/Release/obj.target/tree_sitter_nginx_binding/src/parser.o GITIGNORE_OBJ_PARSER := libs/tree-sitter-gitignore/build/Release/obj.target/tree_sitter_ignore_binding/src/parser.o @@ -41,6 +43,9 @@ GITIGNORE_OBJ_PARSER := libs/tree-sitter-gitignore/build/Release/obj.target/tree FISH_OBJ_PARSER := libs/tree-sitter-fish/build/Release/obj.target/tree_sitter_fish_binding/src/parser.o FISH_OBJ_SCANNER := libs/tree-sitter-fish/build/Release/obj.target/tree_sitter_fish_binding/src/scanner.o +MAN_OBJ_PARSER := libs/tree-sitter-man/build/Release/obj.target/tree_sitter_man_binding/src/parser.o +MAN_OBJ_SCANNER := libs/tree-sitter-man/build/Release/obj.target/tree_sitter_man_binding/src/scanner.o + MD_OBJ_PARSER := libs/tree-sitter-markdown/build/Release/obj.target/tree_sitter_markdown_binding/tree-sitter-markdown/src/parser.o MD_OBJ_SCANNER := libs/tree-sitter-markdown/build/Release/obj.target/tree_sitter_markdown_binding/tree-sitter-markdown/src/scanner.o @@ -52,10 +57,13 @@ LIBS := \ libs/tree-sitter/libtree-sitter.a \ $(TREE_SITTER_LIBS) \ $(PHP_LIB) \ + $(TSX_LIB) \ $(NGINX_OBJ_PARSER) \ $(GITIGNORE_OBJ_PARSER) \ $(FISH_OBJ_PARSER) \ $(FISH_OBJ_SCANNER) \ + $(MAN_OBJ_PARSER) \ + $(MAN_OBJ_SCANNER) \ $(MD_OBJ_PARSER) \ $(MD_OBJ_SCANNER) \ $(MD_I_OBJ_PARSER) \ diff --git a/README.md b/README.md index 907fa3f..866049c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A TUI IDE. # TODO +- [ ] Add ecma to js and make tsx - [ ] Add support for LSP & autocomplete / snippets. - First research - `textDocument/documentHighlight` - for highlighting stuff (probably tree-sitter is enough) @@ -51,3 +52,5 @@ A TUI IDE. - (only on the first time) and sets mode to `WORD`. - [ ] Redo folding system and its relation to move_line_* functions. (Currently its a mess) - [ ] Make whole thing event driven and not clock driven. + +- [ ] Fix in kutu.rb such that windows arent focused on hover . they are only when they are true windows and not just popups . also popus are focused even without hover when they open. diff --git a/grammar/hover.scm b/grammar/hover.scm new file mode 100644 index 0000000..4573fc5 --- /dev/null +++ b/grammar/hover.scm @@ -0,0 +1,328 @@ +;; #82AAFF #000000 1 0 1 4 +(setext_heading + (paragraph) @markup.heading.1 + (setext_h1_underline) @markup.heading.1) + +;; #82AAFF #000000 1 0 1 4 +(setext_heading + (paragraph) @markup.heading.2 + (setext_h2_underline) @markup.heading.2) + +(atx_heading + (atx_h1_marker)) @markup.heading.1 + +(atx_heading + (atx_h2_marker)) @markup.heading.2 + +;; #82AAFF #000000 1 0 0 4 +(atx_heading + (atx_h3_marker)) @markup.heading.3 + +;; #82AAFF #000000 1 0 0 4 +(atx_heading + (atx_h4_marker)) @markup.heading.4 + +;; #82AAFF #000000 1 0 0 4 +(atx_heading + (atx_h5_marker)) @markup.heading.5 + +;; #82AAFF #000000 1 0 0 4 +(atx_heading + (atx_h6_marker)) @markup.heading.6 + +;; #82AAFF #000000 0 0 0 4 +(info_string) @label + +;; #FF6347 #000000 0 0 0 4 +(pipe_table_header + (pipe_table_cell) @markup.heading) + +;; #FF8F40 #000000 0 0 0 4 +(pipe_table_header + "|" @punctuation.special) + +(pipe_table_row + "|" @punctuation.special) + +(pipe_table_delimiter_row + "|" @punctuation.special) + +(pipe_table_delimiter_cell) @punctuation.special + +;; #AAD94C #000000 0 0 0 2 +(indented_code_block) @markup.raw.block + +(fenced_code_block) @markup.raw.block + +(fenced_code_block + (fenced_code_block_delimiter) @markup.raw.block) + +(fenced_code_block + (info_string + (language) @label)) + +;; #7dcfff #000000 0 0 1 6 +(link_destination) @markup.link.url + +;; #7dcfff #000000 0 0 1 6 +[ + (link_title) + (link_label) +] @markup.link.label + +;; #FF8F40 #000000 0 0 0 4 +((link_label) + . + ":" @punctuation.delimiter) + +;; #9ADE7A #000000 0 0 0 4 +[ + (list_marker_plus) + (list_marker_minus) + (list_marker_star) + (list_marker_dot) + (list_marker_parenthesis) +] @markup.list + +(thematic_break) @punctuation.special + +;; #FF8F40 #000000 0 0 0 4 +(task_list_marker_unchecked) @markup.list.unchecked + +;; #AAD94C #000000 0 0 0 4 +(task_list_marker_checked) @markup.list.checked + +[ + (plus_metadata) + (minus_metadata) +] @keyword.directive + +[ + (block_continuation) + (block_quote_marker) +] @punctuation.special + +;; #AAD94C #000000 0 0 0 6 +(backslash_escape) @string.escape + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^ruby$")) +;; !ruby + (code_fence_content) @injection.ruby) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^bash$")) +;; !bash + (code_fence_content) @injection.bash) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^cpp$")) +;; !cpp + (code_fence_content) @injection.cpp) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^h$")) +;; !h + (code_fence_content) @injection.h) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^c$")) +;; !c + (code_fence_content) @injection.h) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^css$")) +;; !css + (code_fence_content) @injection.css) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^fish$")) +;; !fish + (code_fence_content) @injection.fish) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^go$")) +;; !go + (code_fence_content) @injection.go) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^haskell$")) +;; !haskell + (code_fence_content) @injection.haskell) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^html$")) +;; !html + (code_fence_content) @injection.html) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^javascript$")) +;; !javascript + (code_fence_content) @injection.javascript) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^json$")) +;; !json + (code_fence_content) @injection.json) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^lua$")) +; - lua format in hover boxes is typed making it unparsable as normal lua +; - TODO: add a lua grammar with typing or remove this injection + (code_fence_content) @injection.lua) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^regex$")) +;; !regex + (code_fence_content) @injection.regex) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^query$")) +;; !query + (code_fence_content) @injection.query) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^markdown$")) +;; !markdown + (code_fence_content) @injection.markdown) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^markdown_inline$")) +;; !markdown_inline + (code_fence_content) @injection.markdown_inline) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^erb$")) +;; !embedded_template + (code_fence_content) @injection.embedded_template) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^python$")) +;; !python + (code_fence_content) @injection.python) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^php$")) +;; !php + (code_fence_content) @injection.php) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^rust$")) +;; !rust + (code_fence_content) @injection.rust) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^sql$")) +;; !sql + (code_fence_content) @injection.sql) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^gitattributes$")) +;; !gitattributes + (code_fence_content) @injection.gitattributes) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^gitignore$")) +;; !gitignore + (code_fence_content) @injection.gitignore) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^gdscript$")) +;; !gdscript + (code_fence_content) @injection.gdscript) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^make$")) +;; !make + (code_fence_content) @injection.make) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^diff$")) +;; !diff + (code_fence_content) @injection.diff) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^ini$")) +;; !ini + (code_fence_content) @injection.ini) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^nginx$")) +;; !nginx + (code_fence_content) @injection.nginx) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^toml$")) +;; !toml + (code_fence_content) @injection.toml) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^yaml$")) +;; !yaml + (code_fence_content) @injection.yaml) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^gomod$")) +;; !gomod + (code_fence_content) @injection.gomod) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^man$")) +;; !man + (code_fence_content) @injection.man) + +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^cabal$")) +;; !cabal + (code_fence_content) @injection.cabal) + +;; !html +(html_block) @injection.html + +;; !yaml +(minus_metadata) @injection.yaml + +;; !toml +(plus_metadata) @injection.toml + +;; !markdown_inline +(paragraph) @inline + +(pipe_table_row + (pipe_table_cell) @inline) + +(block_quote ((paragraph) @inline)) diff --git a/grammar/javascript.scm b/grammar/javascript.scm index 98ebddd..de3bd29 100644 --- a/grammar/javascript.scm +++ b/grammar/javascript.scm @@ -85,8 +85,7 @@ value: [(arrow_function) (function_expression)]) @definition.function ;; #59C2FF #000000 0 0 0 0 -( - (call_expression +((call_expression function: (identifier) @name) @reference.call (#not-match? @name "^(require)$")) diff --git a/grammar/man.scm b/grammar/man.scm new file mode 100644 index 0000000..68ed5b0 --- /dev/null +++ b/grammar/man.scm @@ -0,0 +1,23 @@ +;; #82AAFF #000000 1 0 1 2 +(title) @markup.heading.1 + +;; #ccefc9 #000000 0 0 0 0 +(section_title) @markup.heading.2 + +;; #FF8F40 #000000 1 0 0 2 +(subsection_title) @markup.heading.3 + +;; #AAD94C #000000 0 0 0 3 +(option) @variable.parameter + +;; #FFD700 #000000 1 0 0 3 +(reference) @markup.link.label + +;; #C792EA #000000 0 0 0 3 +(footer) @markup.heading + +(section_heading + (section_title) @_title +;; #FFD700 #000000 1 0 0 1 + (block) @injection.content + (#match? @_title "SYNOPSIS")) diff --git a/grammar/markdown.scm b/grammar/markdown.scm index 7f04b7b..e757c02 100644 --- a/grammar/markdown.scm +++ b/grammar/markdown.scm @@ -297,6 +297,12 @@ ;; !gomod (code_fence_content) @injection.gomod) +(fenced_code_block + (info_string + (language) @injection.language (#match? @injection.language "^man$")) +;; !man + (code_fence_content) @injection.man) + (fenced_code_block (info_string (language) @injection.language (#match? @injection.language "^cabal$")) diff --git a/grammar/ruby.scm b/grammar/ruby.scm index 625666a..b2affa7 100644 --- a/grammar/ruby.scm +++ b/grammar/ruby.scm @@ -507,3 +507,9 @@ (heredoc_content) @cabal_injection ((heredoc_end) @lang (#match? @lang "CABAL"))) + +(heredoc_body +;; !man + (heredoc_content) @man_injection + ((heredoc_end) @lang + (#match? @lang "MAN"))) diff --git a/grammar/typescript.scm b/grammar/typescript.scm new file mode 100644 index 0000000..de3bd29 --- /dev/null +++ b/grammar/typescript.scm @@ -0,0 +1,316 @@ +; ============================================================ +; Identifiers +; ============================================================ + +;; #FFFFFF #000000 0 0 0 1 +(identifier) @variable + +;; #D2A6FF #000000 0 0 0 2 +((identifier) @constant + (#match? @constant "^[A-Z_][A-Z0-9_]*$")) + +;; #F07178 #000000 0 0 0 3 +((identifier) @variable.builtin + (#match? @variable.builtin + "^(arguments|console|window|document|globalThis|process|module|exports)$")) + +;; #59C2FF #000000 0 0 0 1 +((identifier) @constructor + (#match? @constructor "^[A-Z][a-zA-Z0-9]*$")) + +; ============================================================ +; Properties +; ============================================================ + +;; #F07178 #000000 0 0 0 1 +(property_identifier) @property + +; ============================================================ +; Functions +; ============================================================ + +;; #FFB454 #000000 0 0 0 3 +(function_declaration + name: (identifier) @function) + +(function_expression + name: (identifier) @function) + +;; #FFB454 #000000 0 0 0 2 +(method_definition + name: (property_identifier) @function.method) + +(variable_declarator + name: (identifier) @function + value: [(function_expression) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function_expression) (arrow_function)]) + +(pair + key: (property_identifier) @function.method + value: [(function_expression) (arrow_function)]) + +; ------------------------------------------------------------ +; Function calls +; ------------------------------------------------------------ + +;; #FFB454 #000000 0 0 0 2 +(call_expression + function: (identifier) @function.call) + +;; #FFB454 #000000 0 0 0 2 +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; ============================================================ +; Highlighted definitions & references +; ============================================================ + +;; #FFB454 #000000 0 0 0 3 +(assignment_expression + left: [ + (identifier) @name + (member_expression + property: (property_identifier) @name) + ] + right: [(arrow_function) (function_expression)] +) @definition.function + +;; #FFB454 #000000 0 0 0 3 +(pair + key: (property_identifier) @name + value: [(arrow_function) (function_expression)]) @definition.function + +;; #59C2FF #000000 0 0 0 0 +((call_expression + function: (identifier) @name) @reference.call + (#not-match? @name "^(require)$")) + +;; #7dcfff #000000 0 0 0 2 +(new_expression + constructor: (_) @name) @reference.class + +;; #D2A6FF #000000 0 0 0 2 +(export_statement value: (assignment_expression left: (identifier) @name right: ([ + (number) + (string) + (identifier) + (undefined) + (null) + (new_expression) + (binary_expression) + (call_expression) +]))) @definition.constant + +; ============================================================ +; Parameters +; ============================================================ + +;; #D2A6FF #000000 0 0 0 1 +(formal_parameters + [ + (identifier) @variable.parameter + (array_pattern + (identifier) @variable.parameter) + (object_pattern + [ + (pair_pattern value: (identifier) @variable.parameter) + (shorthand_property_identifier_pattern) @variable.parameter + ]) + ]) + +; ============================================================ +; Keywords (split into semantic groups) +; ============================================================ + +;; #FF8F40 #000000 0 0 0 1 +; Declarations +[ + "var" + "let" + "const" + "function" + "class" +] @keyword.declaration + +;; #FF8F40 #000000 0 0 0 1 +; Control flow +[ + "if" + "else" + "switch" + "case" + "default" + "for" + "while" + "do" + "break" + "continue" + "return" + "throw" + "try" + "catch" + "finally" + "extends" +] @keyword.control + +;; #FF8F40 #000000 0 0 0 1 +; Imports / exports +[ + "import" + "export" + "from" + "as" +] @keyword.import + +;; #F29668 #000000 0 0 0 1 +; Operators-as-keywords +[ + "in" + "instanceof" + "new" + "delete" + "typeof" + "void" + "await" + "yield" +] @keyword.operator + +;; #FF8F40 #000000 0 0 0 1 +; Modifiers +[ + "async" + "static" + "get" + "set" +] @keyword.modifier + +; ============================================================ +; Literals +; ============================================================ + +;; #F07178 #000000 0 0 0 1 +(this) @variable.builtin +(super) @variable.builtin + +;; #D2A6FF #000000 0 0 0 4 +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +;; #D2A6FF #000000 0 0 0 2 +(number) @number + +;; #D2A6FF #000000 0 1 0 2 +((string) @use_strict + (#match? @use_strict "^['\"]use strict['\"]$")) + +;; #AAD94C #000000 0 0 0 0 +(string) @string + +;; #AAD94C #000000 0 0 0 0 +(template_string) @string.special + +;; #99ADBF #000000 0 1 0 1 +(comment) @comment + +; ============================================================ +; Operators & punctuation +; ============================================================ + +;; #F29668 #000000 0 1 0 1 +[ + "+" + "-" + "*" + "/" + "%" + "**" + "++" + "--" + "==" + "!=" + "===" + "!==" + "<" + "<=" + ">" + ">=" + "&&" + "||" + "??" + "!" + "~" + "&" + "|" + "^" + "<<" + ">>" + ">>>" + "=" + "+=" + "-=" + "*=" + "/=" + "%=" + "<<=" + ">>=" + ">>>=" + "&=" + "|=" + "^=" + "&&=" + "||=" + "??=" + "=>" +] @operator + +;; #BFBDB6 #000000 0 0 0 1 +[ + "." + "," + ";" +] @punctuation.delimiter + +;; #BFBDB6 #000000 0 0 0 1 +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; #7dcfff #000000 0 0 0 2 +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) + +; ============================================================ +; JSX +; ============================================================ + +;; #59C2FF #000000 0 0 0 4 +(jsx_opening_element (identifier) @tag2) +(jsx_closing_element (identifier) @tag2) +(jsx_self_closing_element (identifier) @tag2) + +;; #F07178 #000000 0 0 0 3 +(jsx_attribute (property_identifier) @attribute2) + +;; #BFBDB6 #000000 0 0 0 3 +(jsx_opening_element (["<" ">"]) @punctuation.bracket2) +(jsx_closing_element ([""]) @punctuation.bracket2) +(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket2) + +; Injections + +;; !regex +(regex) @string.regex diff --git a/include/editor.h b/include/editor.h index 53794fb..acf1513 100644 --- a/include/editor.h +++ b/include/editor.h @@ -1,13 +1,13 @@ #ifndef EDITOR_H #define EDITOR_H +#include "./hover.h" #include "./knot.h" #include "./pch.h" +#include "./spans.h" +#include "./ts_def.h" #include "./ui.h" #include "./utils.h" -#include "ts_def.h" -#include -#include #define CHAR 0 #define WORD 1 @@ -16,122 +16,6 @@ #define EXTRA_META 4 #define INDENT_WIDTH 2 -struct Highlight { - uint32_t fg; - uint32_t bg; - uint32_t flags; - uint8_t priority; -}; - -struct Span { - uint32_t start; - uint32_t end; - Highlight *hl; - - bool operator<(const Span &other) const { return start < other.start; } -}; - -struct Spans { - std::vector spans; - Queue> edits; - bool mid_parse = false; - std::shared_mutex mtx; -}; - -struct Fold { - uint32_t start; - uint32_t end; - - bool contains(uint32_t line) const { return line >= start && line <= end; } - bool operator<(const Fold &other) const { return start < other.start; } -}; - -struct SpanCursor { - Spans &spans; - size_t index = 0; - std::vector active; - std::shared_lock lock; - - SpanCursor(Spans &s) : spans(s) {} - Highlight *get_highlight(uint32_t byte_offset) { - for (int i = (int)active.size() - 1; i >= 0; i--) - if (active[i]->end <= byte_offset) - active.erase(active.begin() + i); - while (index < spans.spans.size() && - spans.spans[index].start <= byte_offset) { - if (spans.spans[index].end > byte_offset) - active.push_back(const_cast(&spans.spans[index])); - index++; - } - Highlight *best = nullptr; - int max_prio = -1; - for (auto *s : active) - if (s->hl->priority > max_prio) { - max_prio = s->hl->priority; - best = s->hl; - } - return best; - } - void sync(uint32_t byte_offset) { - lock = std::shared_lock(spans.mtx); - active.clear(); - size_t left = 0, right = spans.spans.size(); - while (left < right) { - size_t mid = (left + right) / 2; - if (spans.spans[mid].start <= byte_offset) - left = mid + 1; - else - right = mid; - } - index = left; - while (left > 0) { - left--; - if (spans.spans[left].end > byte_offset) - active.push_back(const_cast(&spans.spans[left])); - else if (byte_offset - spans.spans[left].end > 1000) - break; - } - } -}; - -struct VWarn { - uint32_t line; - std::string text; - int8_t type; - uint32_t start; - uint32_t end{UINT32_MAX}; - - bool operator<(const VWarn &other) const { return line < other.line; } -}; - -struct VAI { - Coord pos; - char *text; - uint32_t len; - uint32_t lines; // number of \n in text for speed .. the ai part will not - // line wrap but multiline ones need to have its own lines - // after the first one -}; - -struct TSSetBase { - std::string lang; - TSParser *parser; - std::string query_file; - TSQuery *query; - TSTree *tree; - std::map query_map; - std::map injection_map; - const TSLanguage *language; -}; - -struct TSSet : TSSetBase { - std::vector ranges; -}; - -struct TSSetMain : TSSetBase { - std::unordered_map injections; -}; - struct Editor { std::string filename; std::string uri; @@ -154,9 +38,14 @@ struct Editor { bool jumper_set; std::shared_mutex v_mtx; std::vector warnings; + bool warnings_dirty; VAI ai; std::shared_mutex lsp_mtx; std::shared_ptr lsp; + bool hover_active; + HoverBox hover; + bool diagnostics_active; + DiagnosticBox diagnostics; int lsp_version = 1; }; @@ -219,6 +108,7 @@ void apply_hook_deletion(Editor *editor, uint32_t removal_start, uint32_t removal_end); Editor *new_editor(const char *filename_arg, Coord position, Coord size); void save_file(Editor *editor); +void hover_diagnostic(Editor *editor); void free_editor(Editor *editor); void render_editor(Editor *editor); void fold(Editor *editor, uint32_t start_line, uint32_t end_line); diff --git a/include/hover.h b/include/hover.h new file mode 100644 index 0000000..ea1da7d --- /dev/null +++ b/include/hover.h @@ -0,0 +1,37 @@ +#ifndef HOVER_H +#define HOVER_H + +#include "./pch.h" +#include "./spans.h" +#include "./ts_def.h" +#include "./ui.h" +#include "./utils.h" + +struct HoverBox { + std::string text; + std::atomic is_markup; + uint32_t scroll_; + std::vector cells; + uint32_t box_width; + uint32_t box_height; + std::vector highlights; + std::vector hover_spans; + + void clear(); + void scroll(int32_t number); + void render_first(bool scroll = false); + void render(Coord pos); +}; + +struct DiagnosticBox { + std::vector warnings; + std::vector cells; + uint32_t box_width; + uint32_t box_height; + + void clear(); + void render_first(); + void render(Coord pos); +}; + +#endif diff --git a/include/lsp.h b/include/lsp.h index fc47459..f6d17c6 100644 --- a/include/lsp.h +++ b/include/lsp.h @@ -32,6 +32,7 @@ struct LSPInstance { std::atomic initialized = false; std::atomic exited = false; bool incremental_sync = false; + bool allow_hover = false; uint32_t last_id = 0; Queue inbox; Queue outbox; diff --git a/include/maps.h b/include/maps.h index 479946a..41ff098 100644 --- a/include/maps.h +++ b/include/maps.h @@ -178,6 +178,7 @@ static const std::unordered_map kLanguages = { {"haskell", {"haskell", LANG(haskell), 9}}, {"html", {"html", LANG(html), 10}}, {"javascript", {"javascript", LANG(javascript), 11}}, + {"typescript", {"typescript", LANG(tsx), 11}}, {"json", {"json", LANG(json), 6}}, {"jsonc", {"jsonc", LANG(json), 6}}, {"erb", {"erb", LANG(embedded_template), 10}}, @@ -195,6 +196,7 @@ static const std::unordered_map kLanguages = { // config to connect to database {"make", {"make", LANG(make), 21}}, {"gdscript", {"gdscript", LANG(gdscript)}}, // TODO: connect to godot + {"man", {"man", LANG(man)}}, {"diff", {"diff", LANG(diff)}}, {"gitattributes", {"gitattributes", LANG(gitattributes)}}, {"gitignore", {"gitignore", LANG(gitignore)}}, @@ -222,11 +224,15 @@ static const std::unordered_map kExtToLang = { {"htm", "html"}, {"js", "javascript"}, {"jsx", "javascript"}, + {"ts", "typescript"}, + {"tsx", "typescript"}, {"json", "json"}, {"jsonc", "jsonc"}, {"lua", "lua"}, + {"make", "make"}, {"mk", "make"}, {"makefile", "make"}, + {"man", "man"}, {"py", "python"}, {"rb", "ruby"}, {"rs", "rust"}, @@ -279,6 +285,7 @@ static const std::unordered_map kMimeToLang = { {"text/x-sql", "sql"}, {"text/x-toml", "toml"}, {"text/x-yaml", "yaml"}, + {"text/x-man", "man"}, }; #endif diff --git a/include/spans.h b/include/spans.h new file mode 100644 index 0000000..5d87701 --- /dev/null +++ b/include/spans.h @@ -0,0 +1,101 @@ +#ifndef SPANS_H +#define SPANS_H + +#include "./pch.h" +#include "./utils.h" + +struct VWarn { + uint32_t line; + std::string text; + std::string text_full; + std::string source; + std::string code; + std::vector see_also; + int8_t type; + uint32_t start; + uint32_t end{UINT32_MAX}; + + bool operator<(const VWarn &other) const { return line < other.line; } +}; + +struct VAI { + Coord pos; + char *text; + uint32_t len; + uint32_t lines; // number of \n in text for speed .. the ai part will not + // line wrap but multiline ones need to have its own lines + // after the first one +}; + +struct Fold { + uint32_t start; + uint32_t end; + + bool contains(uint32_t line) const { return line >= start && line <= end; } + bool operator<(const Fold &other) const { return start < other.start; } +}; + +struct Span { + uint32_t start; + uint32_t end; + Highlight *hl; + + bool operator<(const Span &other) const { return start < other.start; } +}; + +struct Spans { + std::vector spans; + Queue> edits; + bool mid_parse = false; + std::shared_mutex mtx; +}; + +struct SpanCursor { + Spans &spans; + size_t index = 0; + std::vector active; + std::shared_lock lock; + + SpanCursor(Spans &s) : spans(s) {} + Highlight *get_highlight(uint32_t byte_offset) { + for (int i = (int)active.size() - 1; i >= 0; i--) + if (active[i]->end <= byte_offset) + active.erase(active.begin() + i); + while (index < spans.spans.size() && + spans.spans[index].start <= byte_offset) { + if (spans.spans[index].end > byte_offset) + active.push_back(const_cast(&spans.spans[index])); + index++; + } + Highlight *best = nullptr; + int max_prio = -1; + for (auto *s : active) + if (s->hl->priority > max_prio) { + max_prio = s->hl->priority; + best = s->hl; + } + return best; + } + void sync(uint32_t byte_offset) { + lock = std::shared_lock(spans.mtx); + active.clear(); + size_t left = 0, right = spans.spans.size(); + while (left < right) { + size_t mid = (left + right) / 2; + if (spans.spans[mid].start <= byte_offset) + left = mid + 1; + else + right = mid; + } + index = left; + while (left > 0) { + left--; + if (spans.spans[left].end > byte_offset) + active.push_back(const_cast(&spans.spans[left])); + else if (byte_offset - spans.spans[left].end > 1000) + break; + } + } +}; + +#endif diff --git a/include/ts.h b/include/ts.h index 5f66f38..c88314f 100644 --- a/include/ts.h +++ b/include/ts.h @@ -11,6 +11,15 @@ extern std::unordered_map regex_cache; TSQuery *load_query(const char *query_path, TSSetBase *set); void ts_collect_spans(Editor *editor); +bool ts_predicate(TSQuery *query, const TSQueryMatch &match, + std::function subject_fn); void clear_regex_cache(); +template +inline T *safe_get(std::map &m, uint16_t key) { + auto it = m.find(key); + if (it == m.end()) + return nullptr; + return &it->second; +} #endif diff --git a/include/ts_def.h b/include/ts_def.h index 05103a1..93e8a75 100644 --- a/include/ts_def.h +++ b/include/ts_def.h @@ -12,6 +12,32 @@ struct Language { uint8_t lsp_id = 0; }; +struct Highlight { + uint32_t fg; + uint32_t bg; + uint32_t flags; + uint8_t priority; +}; + +struct TSSetBase { + std::string lang; + TSParser *parser; + std::string query_file; + TSQuery *query; + TSTree *tree; + std::map query_map; + std::map injection_map; + const TSLanguage *language; +}; + +struct TSSet : TSSetBase { + std::vector ranges; +}; + +struct TSSetMain : TSSetBase { + std::unordered_map injections; +}; + TS_DEF(ruby); TS_DEF(bash); TS_DEF(cpp); @@ -21,6 +47,8 @@ TS_DEF(go); TS_DEF(haskell); TS_DEF(html); TS_DEF(javascript); +TS_DEF(tsx); +TS_DEF(man); TS_DEF(json); TS_DEF(lua); TS_DEF(regex); diff --git a/include/ui.h b/include/ui.h index a3b34df..6fcb82c 100644 --- a/include/ui.h +++ b/include/ui.h @@ -3,6 +3,7 @@ #include "./pch.h" #include "./utils.h" +#include #define KEY_CHAR 0 #define KEY_SPECIAL 1 @@ -48,14 +49,16 @@ enum CellFlags : uint8_t { CF_NONE = 0, CF_ITALIC = 1 << 0, CF_BOLD = 1 << 1, - CF_UNDERLINE = 1 << 2, + CF_UNDERLINE = 1 << 2 }; struct ScreenCell { std::string utf8 = std::string(""); + uint8_t width = 1; uint32_t fg = 0; uint32_t bg = 0; uint8_t flags = CF_NONE; + uint32_t ul_color = 0; }; struct KeyEvent { @@ -86,6 +89,10 @@ void update(uint32_t row, uint32_t col, std::string utf8, uint32_t fg, uint32_t bg, uint8_t flags); void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg, uint32_t bg, uint8_t flags); +void update(uint32_t row, uint32_t col, std::string utf8, uint32_t fg, + uint32_t bg, uint8_t flags, uint32_t ul_color); +void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg, + uint32_t bg, uint8_t flags, uint32_t ul_color); void set_cursor(int row, int col, int type, bool show_cursor_param); void render(); Coord get_size(); diff --git a/include/utils.h b/include/utils.h index d9a0375..0623d87 100644 --- a/include/utils.h +++ b/include/utils.h @@ -53,7 +53,17 @@ struct Coord { bool operator>=(const Coord &other) const { return !(*this < other); } }; +struct Match { + size_t start; + size_t end; + std::string text; +}; + +std::vector find_all_matches(const std::string &subject, + const std::string &pattern); +std::string clean_text(const std::string &input); std::string percent_encode(const std::string &s); +std::string percent_decode(const std::string &s); std::string path_abs(const std::string &path_str); std::string path_to_file_uri(const std::string &path_str); int display_width(const char *str, size_t len); @@ -70,6 +80,7 @@ Language language_for_file(const char *filename); void copy_to_clipboard(const char *text, size_t len); char *get_from_clipboard(uint32_t *out_len); uint32_t count_clusters(const char *line, size_t len, size_t from, size_t to); +std::string trim(const std::string &s); template auto throttle(std::chrono::milliseconds min_duration, Func &&func, diff --git a/libs/tree-sitter-man b/libs/tree-sitter-man new file mode 160000 index 0000000..e332ea9 --- /dev/null +++ b/libs/tree-sitter-man @@ -0,0 +1 @@ +Subproject commit e332ea95d5c921d1108a46d1b2b0f017079e1fd8 diff --git a/libs/tree-sitter-typescript b/libs/tree-sitter-typescript new file mode 160000 index 0000000..75b3874 --- /dev/null +++ b/libs/tree-sitter-typescript @@ -0,0 +1 @@ +Subproject commit 75b3874edb2dc714fb1fd77a32013d0f8699989f diff --git a/samples/lua.lua b/samples/lua.lua index e5088ba..933588c 100644 --- a/samples/lua.lua +++ b/samples/lua.lua @@ -13,7 +13,7 @@ local name = "Lua" print(self) -- Functions -function greet(user) +local function greet(user) print("Hello, " .. user) end diff --git a/samples/markdown.md b/samples/markdown.md index 839d847..c9282ec 100644 --- a/samples/markdown.md +++ b/samples/markdown.md @@ -31,9 +31,10 @@ This is a paragraph with **bold text**, *italic text*, ~~strikethrough~~, and `i `Inline code` example and a fenced code block: -```python -def hello_world(): - print("Hello, world!") +```lua +local s2 = [[Long +multi-line +string]] ``` ![Image](https://example.com/image.jpg) diff --git a/samples/ruby.rb b/samples/ruby.rb index 2bc5286..553ab29 100644 --- a/samples/ruby.rb +++ b/samples/ruby.rb @@ -13,6 +13,8 @@ end emojis = "πŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒπŸ‘‹πŸŒ" # Mixed-width CJK blocks +# +# https://chatgpt.com/c/695232e9-febc-8331-88c0-b47020948e5b cjk_samples = [ "ζΌ’ε­—γƒ†γ‚Ήγƒˆ", "測試中文字串", @@ -173,6 +175,11 @@ end # Method definition def greet_person(name) puts "#{Utilities.random_greeting}, #{name}!" + if (name == "harry") + return true + else + return "s" + end end # Calling methods diff --git a/src/editor.cc b/src/editor.cc index 9d10a94..69752df 100644 --- a/src/editor.cc +++ b/src/editor.cc @@ -22,6 +22,12 @@ Editor *new_editor(const char *filename_arg, Coord position, Coord size) { editor->position = position; editor->size = size; editor->cursor_preffered = UINT32_MAX; + if (len == 0) { + free(str); + str = (char *)malloc(1); + *str = '\n'; + len = 1; + } editor->root = load(str, len, optimal_chunk_size(len)); free(str); Language language = language_for_file(filename.c_str()); @@ -29,8 +35,6 @@ Editor *new_editor(const char *filename_arg, Coord position, Coord size) { editor->ts.parser = ts_parser_new(); editor->ts.language = language.fn(); ts_parser_set_language(editor->ts.parser, editor->ts.language); - log("set language %s\n", language.name.c_str()); - log("lsp_id: %d\n", language.lsp_id); editor->ts.query_file = get_exe_dir() + "/../grammar/" + language.name + ".scm"; request_add_to_lsp(language, editor); @@ -254,29 +258,34 @@ void render_editor(Editor *editor) { uint8_t fl = hl ? hl->flags : 0; if (def_hl) { if (def_hl->fg != 0) - fg = def_hl->fg; + fg |= def_hl->fg; if (def_hl->bg != 0) - bg = def_hl->bg; + bg |= def_hl->bg; fl |= def_hl->flags; } if (editor->selection_active && absolute_byte_pos >= sel_start && absolute_byte_pos < sel_end) bg = 0x555555; + uint32_t u_color = 0; for (const auto &w : line_warnings) { if (w.start <= current_byte_offset + local_render_offset && current_byte_offset + local_render_offset < w.end) { switch (w.type) { case 1: - bg = 0x500000; + u_color = 0xff0000; + fl |= CF_UNDERLINE; break; case 2: - bg = 0x505000; + u_color = 0xffff00; + fl |= CF_UNDERLINE; break; case 3: - bg = 0x500050; + u_color = 0xff00ff; + fl |= CF_UNDERLINE; break; case 4: - bg = 0x505050; + u_color = 0xA0A0A0; + fl |= CF_UNDERLINE; break; } } @@ -289,7 +298,7 @@ void render_editor(Editor *editor) { if (col + width > render_width) break; update(editor->position.row + rendered_rows, render_x + col, - cluster.c_str(), fg, bg | color, fl); + cluster.c_str(), fg, bg | color, fl, u_color); local_render_offset += cluster_len; line_left -= cluster_len; col += width; @@ -383,6 +392,7 @@ void render_editor(Editor *editor) { update(editor->position.row + rendered_rows, render_x + col - width, "\x1b", fg_color, color, 0); } + line_warnings.clear(); } while (col < render_width) { update(editor->position.row + rendered_rows, render_x + col, " ", 0, @@ -424,6 +434,81 @@ void render_editor(Editor *editor) { 0x555555 | color, 0); col++; } + if (!line_warnings.empty()) { + VWarn warn = line_warnings.front(); + update(editor->position.row + rendered_rows, render_x + col, " ", 0, + color, 0); + col++; + for (size_t i = 0; i < line_warnings.size(); i++) { + if (line_warnings[i].type < warn.type) + warn = line_warnings[i]; + std::string err_sym = " "; + uint32_t fg_color = 0; + switch (line_warnings[i].type) { + case 1: + err_sym = "ο”°"; + fg_color = 0xFF0000; + goto final2; + case 2: + err_sym = ""; + fg_color = 0xFFFF00; + goto final2; + case 3: + err_sym = ""; + fg_color = 0xFF00FF; + goto final2; + case 4: + err_sym = ""; + fg_color = 0xAAAAAA; + goto final2; + final2: + if (col < render_width) { + update(editor->position.row + rendered_rows, render_x + col, + err_sym, fg_color, color, 0); + col++; + update(editor->position.row + rendered_rows, render_x + col, " ", + fg_color, color, 0); + col++; + } + } + } + if (col < render_width) { + update(editor->position.row + rendered_rows, render_x + col, " ", 0, + 0 | color, 0); + col++; + } + size_t warn_idx = 0; + uint32_t fg_color = 0; + switch (warn.type) { + case 1: + fg_color = 0xFF0000; + break; + case 2: + fg_color = 0xFFFF00; + break; + case 3: + fg_color = 0xFF00FF; + break; + case 4: + fg_color = 0xAAAAAA; + break; + } + while (col < render_width && warn_idx < warn.text.length()) { + uint32_t cluster_len = grapheme_next_character_break_utf8( + warn.text.c_str() + warn_idx, warn.text.length() - warn_idx); + std::string cluster = warn.text.substr(warn_idx, cluster_len); + int width = display_width(cluster.c_str(), cluster_len); + if (col + width > render_width) + break; + update(editor->position.row + rendered_rows, render_x + col, + cluster.c_str(), fg_color, color, 0); + col += width; + warn_idx += cluster_len; + while (width-- > 1) + update(editor->position.row + rendered_rows, render_x + col - width, + "\x1b", fg_color, color, 0); + } + } while (col < render_width) { update(editor->position.row + rendered_rows, render_x + col, " ", 0, 0 | color, 0); @@ -449,6 +534,10 @@ void render_editor(Editor *editor) { break; } set_cursor(cursor.row, cursor.col, type, true); + if (editor->hover_active) + editor->hover.render(cursor); + else if (editor->diagnostics_active) + editor->diagnostics.render(cursor); } while (rendered_rows < editor->size.row) { for (uint32_t col = 0; col < editor->size.col; col++) diff --git a/src/editor_events.cc b/src/editor_events.cc index c840641..c410073 100644 --- a/src/editor_events.cc +++ b/src/editor_events.cc @@ -1,4 +1,5 @@ #include "../include/editor.h" +#include "../include/lsp.h" #include "../include/main.h" #include "../include/ts.h" #include @@ -9,6 +10,9 @@ void handle_editor_event(Editor *editor, KeyEvent event) { std::chrono::steady_clock::now(); static uint32_t click_count = 0; static Coord last_click_pos = {UINT32_MAX, UINT32_MAX}; + Coord start = editor->cursor; + if (editor->hover_active) + editor->hover_active = false; if (event.key_type == KEY_MOUSE) { auto now = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast( @@ -205,7 +209,6 @@ void handle_editor_event(Editor *editor, KeyEvent event) { switch (mode) { case NORMAL: if (event.key_type == KEY_CHAR && event.len == 1) { - Coord start = editor->cursor; switch (event.c[0]) { case 'u': if (editor->root->line_count > 0) { @@ -230,6 +233,72 @@ void handle_editor_event(Editor *editor, KeyEvent event) { editor->selection_type = LINE; } break; + case CTRL('h'): + editor->hover.scroll(-1); + editor->hover_active = true; + break; + case CTRL('l'): + editor->hover.scroll(1); + editor->hover_active = true; + break; + case 'h': + if (editor->lsp && editor->lsp->allow_hover) { + LineIterator *it = begin_l_iter(editor->root, editor->cursor.row); + char *line = next_line(it, nullptr); + if (!line) { + free(it->buffer); + free(it); + break; + } + uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); + free(it->buffer); + free(it); + json hover_request = { + {"jsonrpc", "2.0"}, + {"method", "textDocument/hover"}, + {"params", + {{"textDocument", {{"uri", editor->uri}}}, + {"position", + {{"line", editor->cursor.row}, {"character", col}}}}}}; + LSPPending *pending = new LSPPending(); + pending->editor = editor; + pending->method = "textDocument/hover"; + pending->callback = [](Editor *editor, std::string, json hover) { + log("%s\n", hover.dump().c_str()); + if (hover.contains("result") && !hover["result"].is_null()) { + auto &contents = hover["result"]["contents"]; + std::string hover_text = ""; + bool is_markup = false; + if (contents.is_object()) { + hover_text += contents["value"].get(); + is_markup = (contents["kind"].get() == "markdown"); + } else if (contents.is_array()) { + for (auto &block : contents) { + if (block.is_string()) { + hover_text += block.get() + "\n"; + } else if (block.is_object() && block.contains("language") && + block.contains("value")) { + std::string lang = block["language"].get(); + std::string val = block["value"].get(); + is_markup = true; + hover_text += "```" + lang + "\n" + val + "\n```\n"; + } + } + } else if (contents.is_string()) { + hover_text += contents.get(); + } + if (!hover_text.empty()) { + editor->hover.clear(); + editor->hover.text = clean_text(hover_text); + editor->hover.is_markup = is_markup; + editor->hover.render_first(); + editor->hover_active = true; + } + } + }; + lsp_send(editor->lsp, hover_request, pending); + } + break; case 'a': mode = INSERT; cursor_right(editor, 1); @@ -559,6 +628,30 @@ void handle_editor_event(Editor *editor, KeyEvent event) { free(event.c); } +void hover_diagnostic(Editor *editor) { + std::shared_lock lock(editor->v_mtx); + static uint32_t last_line = UINT32_MAX; + if (last_line == editor->cursor.row && !editor->warnings_dirty) + return; + VWarn dummy; + dummy.line = editor->cursor.row; + editor->warnings_dirty = false; + last_line = editor->cursor.row; + auto first = + std::lower_bound(editor->warnings.begin(), editor->warnings.end(), dummy); + auto last = + std::upper_bound(editor->warnings.begin(), editor->warnings.end(), dummy); + std::vector warnings_at_line(first, last); + if (warnings_at_line.size() == 0) { + editor->diagnostics_active = false; + return; + } + editor->diagnostics.clear(); + editor->diagnostics.warnings.swap(warnings_at_line); + editor->diagnostics.render_first(); + editor->diagnostics_active = true; +} + static Highlight HL_UNDERLINE = {0, 0, 1 << 2, 100}; void editor_worker(Editor *editor) { @@ -599,6 +692,7 @@ void editor_worker(Editor *editor) { editor->def_spans.spans.clear(); lock.unlock(); } + hover_diagnostic(editor); } void editor_lsp_handle(Editor *editor, json msg) { @@ -611,18 +705,54 @@ void editor_lsp_handle(Editor *editor, json msg) { json d = diagnostics[i]; VWarn w; // HACK: convert back to utf-8 but as this is only visually affecting it - // is not worth the performance hit + // is not worth getting the line string from the rope. w.line = d["range"]["start"]["line"]; w.start = d["range"]["start"]["character"]; uint32_t end = d["range"]["end"]["character"]; if (d["range"]["end"]["line"] == w.line) w.end = end; - std::string text = d["message"].get(); + std::string text = trim(d["message"].get()); + w.text_full = text; auto pos = text.find('\n'); w.text = (pos == std::string::npos) ? text : text.substr(0, pos); - w.type = d["severity"].get(); + if (d.contains("source")) + w.source = d["source"].get(); + if (d.contains("code")) { + w.code = "["; + if (d["code"].is_string()) + w.code += d["code"].get() + "] "; + else if (d["code"].is_number()) + w.code += std::to_string(d["code"].get()) + "] "; + else + w.code.clear(); + if (d.contains("codeDescription") && + d["codeDescription"].contains("href")) + w.code += d["codeDescription"]["href"].get(); + } + if (d.contains("relatedInformation")) { + json related = d["relatedInformation"]; + for (size_t j = 0; j < related.size(); j++) { + json rel = related[j]; + std::string message = rel["message"].get(); + auto pos = message.find('\n'); + message = + (pos == std::string::npos) ? message : message.substr(0, pos); + std::string uri = + percent_decode(rel["location"]["uri"].get()); + auto pos2 = uri.find_last_of('/'); + if (pos2 != std::string::npos) + uri = uri.substr(pos2 + 1); + std::string row = std::to_string( + rel["location"]["range"]["start"]["line"].get()); + w.see_also.push_back(uri + ":" + row + ": " + message); + } + } + w.type = 1; + if (d.contains("severity")) + w.type = d["severity"].get(); editor->warnings.push_back(w); } std::sort(editor->warnings.begin(), editor->warnings.end()); + editor->warnings_dirty = true; } } diff --git a/src/hover.cc b/src/hover.cc new file mode 100644 index 0000000..923135b --- /dev/null +++ b/src/hover.cc @@ -0,0 +1,373 @@ +extern "C" { +#include "../libs/libgrapheme/grapheme.h" +} +#include "../include/hover.h" +#include "../include/ts.h" +#include "../include/ui.h" + +void HoverBox::clear() { + text = ""; + scroll_ = 0; + is_markup = false; + box_width = 0; + box_height = 0; + cells.clear(); + highlights.clear(); + hover_spans.clear(); +} + +void HoverBox::scroll(int32_t number) { + if (text.empty() || number == 0) + return; + uint32_t line_count = 0; + for (uint32_t i = 0; i < text.length(); i++) + if (text[i] == '\n') + line_count++; + scroll_ = MAX((int32_t)scroll_ + number, 0); + if (scroll_ > line_count) + scroll_ = line_count; + render_first(true); +} + +void HoverBox::render_first(bool scroll) { + if (!scroll) { + std::vector base_spans; + std::vector injected_spans; + TSSetBase ts = TSSetBase{}; + if (is_markup) { + highlights.reserve(1024); + base_spans.reserve(1024); + injected_spans.reserve(1024); + hover_spans.reserve(1024); + std::string query_path = get_exe_dir() + "/../grammar/hover.scm"; + ts.language = LANG(markdown)(); + ts.query = load_query(query_path.c_str(), &ts); + ts.parser = ts_parser_new(); + ts_parser_set_language(ts.parser, ts.language); + ts.tree = ts_parser_parse_string(ts.parser, nullptr, text.c_str(), + text.length()); + TSQueryCursor *cursor = ts_query_cursor_new(); + ts_query_cursor_exec(cursor, ts.query, ts_tree_root_node(ts.tree)); + TSQueryMatch match; + while (ts_query_cursor_next_match(cursor, &match)) { + auto subject_fn = [&](const TSNode *node) -> std::string { + uint32_t start = ts_node_start_byte(*node); + uint32_t end = ts_node_end_byte(*node); + return text.substr(start, end - start); + }; + if (!ts_predicate(ts.query, match, subject_fn)) + continue; + for (uint32_t i = 0; i < match.capture_count; i++) { + TSQueryCapture cap = match.captures[i]; + uint32_t start = ts_node_start_byte(cap.node); + uint32_t end = ts_node_end_byte(cap.node); + if (Language *inj_lang = safe_get(ts.injection_map, cap.index)) { + TSSetBase inj_ts = TSSetBase{}; + inj_ts.language = inj_lang->fn(); + inj_ts.query_file = + get_exe_dir() + "/../grammar/" + inj_lang->name + ".scm"; + inj_ts.query = load_query(inj_ts.query_file.c_str(), &inj_ts); + inj_ts.parser = ts_parser_new(); + ts_parser_set_language(inj_ts.parser, inj_ts.language); + TSPoint start_p = ts_node_start_point(cap.node); + TSPoint end_p = ts_node_end_point(cap.node); + std::vector ranges = {{start_p, end_p, start, end}}; + ts_parser_set_included_ranges(inj_ts.parser, ranges.data(), 1); + inj_ts.tree = ts_parser_parse_string(inj_ts.parser, nullptr, + text.c_str(), text.length()); + TSQueryCursor *inj_cursor = ts_query_cursor_new(); + ts_query_cursor_exec(inj_cursor, inj_ts.query, + ts_tree_root_node(inj_ts.tree)); + TSQueryMatch inj_match; + while (ts_query_cursor_next_match(inj_cursor, &inj_match)) { + auto subject_fn = [&](const TSNode *node) -> std::string { + uint32_t start = ts_node_start_byte(*node); + uint32_t end = ts_node_end_byte(*node); + return text.substr(start, end - start); + }; + if (!ts_predicate(inj_ts.query, inj_match, subject_fn)) + continue; + for (uint32_t i = 0; i < inj_match.capture_count; i++) { + TSQueryCapture inj_cap = inj_match.captures[i]; + uint32_t start = ts_node_start_byte(inj_cap.node); + uint32_t end = ts_node_end_byte(inj_cap.node); + if (Highlight *hl = safe_get(inj_ts.query_map, inj_cap.index)) { + highlights.push_back(*hl); + Highlight *hl_f = &highlights.back(); + injected_spans.push_back({start, end, hl_f}); + } + } + } + ts_query_cursor_delete(inj_cursor); + ts_tree_delete(inj_ts.tree); + ts_parser_delete(inj_ts.parser); + ts_query_delete(inj_ts.query); + continue; + } + if (Highlight *hl = safe_get(ts.query_map, cap.index)) { + highlights.push_back(*hl); + Highlight *hl_f = &highlights.back(); + base_spans.push_back({start, end, hl_f}); + } + } + } + ts_query_cursor_delete(cursor); + ts_query_delete(ts.query); + ts_tree_delete(ts.tree); + ts_parser_delete(ts.parser); + } + for (const auto &inj : injected_spans) { + base_spans.erase(std::remove_if(base_spans.begin(), base_spans.end(), + [&](const Span &base) { + return !(base.end <= inj.start || + base.start >= inj.end); + }), + base_spans.end()); + } + hover_spans.insert(hover_spans.end(), base_spans.begin(), base_spans.end()); + hover_spans.insert(hover_spans.end(), injected_spans.begin(), + injected_spans.end()); + std::sort(hover_spans.begin(), hover_spans.end()); + } + uint32_t longest_line = 0; + uint32_t current_width = 0; + for (size_t j = 0; j < text.length(); j++) { + if (text[j] == '\n') { + longest_line = std::max(longest_line, current_width); + current_width = 0; + } else { + current_width += 1; + } + } + // HACK: the 1 is added so the longest line doesnt wrap which should be fixed + // in the loop instead as it was never meant to wrap in the first place + longest_line = MAX(longest_line, current_width) + 1; + uint32_t content_width = MIN(longest_line, 130u); + box_width = content_width + 2; + size_t i = 0; + size_t lines_skipped = 0; + while (i < text.length() && lines_skipped < scroll_) { + if (text[i] == '\n') + lines_skipped++; + i++; + } + Spans spans{}; + spans.spans = hover_spans; + uint32_t border_fg = 0x82AAFF; + uint32_t base_bg = 0; + SpanCursor span_cursor(spans); + span_cursor.sync(i); + cells.assign(box_width * 26, ScreenCell{" ", 0, 0, 0, 0, 0}); + auto set = [&](uint32_t r, uint32_t c, const char *text, uint32_t fg, + uint32_t bg, uint8_t flags) { + cells[r * box_width + c] = {std::string(text), 0, fg, bg, flags, 0}; + }; + uint32_t r = 0; + while (i < text.length() && r < 24) { + uint32_t c = 0; + while (c < content_width && i < text.length()) { + if (text[i] == '\n') { + while (i < text.length() && text[i] == '\n') + i++; + break; + } + uint32_t cluster_len = grapheme_next_character_break_utf8( + text.c_str() + i, text.length() - i); + std::string cluster = text.substr(i, cluster_len); + int width = display_width(cluster.c_str(), cluster_len); + if (c + width > content_width) + break; + Highlight *hl = span_cursor.get_highlight(i); + uint32_t fg = hl ? hl->fg : 0xFFFFFF; + uint32_t bg = hl ? hl->bg : 0; + uint32_t flags = hl ? hl->flags : 0; + set(r + 1, c + 1, cluster.c_str(), fg, bg | base_bg, flags); + c += width; + i += cluster_len; + for (int w = 1; w < width; w++) + set(r + 1, c - w + 1, "\x1b", 0xFFFFFF, base_bg, 0); + } + r++; + } + if (!scroll) + box_height = r + 2; + set(0, 0, "β”Œ", border_fg, base_bg, 0); + for (uint32_t i = 1; i < box_width - 1; i++) + set(0, i, "─", border_fg, base_bg, 0); + set(0, box_width - 1, "┐", border_fg, base_bg, 0); + for (uint32_t r = 1; r < box_height - 1; r++) { + set(r, 0, "β”‚", border_fg, base_bg, 0); + set(r, box_width - 1, "β”‚", border_fg, base_bg, 0); + } + set(box_height - 1, 0, "β””", border_fg, base_bg, 0); + for (uint32_t i = 1; i < box_width - 1; i++) + set(box_height - 1, i, "─", border_fg, base_bg, 0); + set(box_height - 1, box_width - 1, "β”˜", border_fg, base_bg, 0); + cells.resize(box_width * box_height); +} + +void HoverBox::render(Coord pos) { + int32_t start_row = (int32_t)pos.row - (int32_t)box_height; + if (start_row < 0) + start_row = pos.row + 1; + int32_t start_col = pos.col; + if (start_col + box_width > cols) { + start_col = cols - box_width; + if (start_col < 0) + start_col = 0; + } + for (uint32_t r = 0; r < box_height; r++) + for (uint32_t c = 0; c < box_width; c++) + update(start_row + r, start_col + c, cells[r * box_width + c].utf8, + cells[r * box_width + c].fg, cells[r * box_width + c].bg, + cells[r * box_width + c].flags); +} + +void DiagnosticBox::clear() { + warnings.clear(); + cells.clear(); + box_width = 0; + box_height = 0; +} + +void DiagnosticBox::render_first() { + if (warnings.empty()) + return; + uint32_t longest_line = 8 + warnings[0].source.length(); + for (auto &warn : warnings) { + longest_line = MAX(longest_line, (uint32_t)warn.text.length() + 7); + longest_line = MAX(longest_line, (uint32_t)warn.code.length() + 4); + for (auto &see_also : warn.see_also) + longest_line = MAX(longest_line, (uint32_t)see_also.length() + 4); + } + uint32_t content_width = MIN(longest_line, 150u); + box_width = content_width + 2; + cells.assign(box_width * 25, {" ", 0, 0, 0, 0, 0}); + auto set = [&](uint32_t r, uint32_t c, const char *text, uint32_t fg, + uint32_t bg, uint8_t flags) { + cells[r * box_width + c] = {std::string(text), 0, fg, bg, flags, 0}; + }; + uint32_t base_bg = 0; + uint32_t border_fg = 0x82AAFF; + uint32_t r = 0; + if (warnings[0].source != "") { + std::string src_txt = "Source: "; + for (uint32_t i = 0; i < src_txt.length() && i < content_width; i++) + set(1, i + 1, (char[2]){src_txt[i], 0}, 0x3EAAFF, base_bg, 0); + for (uint32_t i = 0; i < warnings[0].source.length() && i < content_width; + i++) + set(1, i + 1 + src_txt.length(), (char[2]){warnings[0].source[i], 0}, + 0xffffff, base_bg, 0); + r++; + } + int idx = 1; + for (auto &warn : warnings) { + char buf[4]; + std::snprintf(buf, sizeof(buf), "%2d", idx % 100); + std::string line_txt = std::string(buf) + ". "; + for (uint32_t i = 0; i < line_txt.length(); i++) + set(r + 1, i + 1, (char[2]){line_txt[i], 0}, 0xffffff, base_bg, 0); + if (r >= 23) + break; + const char *err_sym = ""; + uint32_t c_sym = 0xAAAAAA; + switch (warn.type) { + case 1: + err_sym = "ο”°"; + c_sym = 0xFF0000; + break; + case 2: + err_sym = ""; + c_sym = 0xFFFF00; + break; + case 3: + err_sym = ""; + c_sym = 0xFF00FF; + break; + case 4: + err_sym = ""; + c_sym = 0xAAAAAA; + break; + } + std::string text = warn.text_full + " " + err_sym; + uint32_t i = 0; + while (i < text.length() && r < 23) { + uint32_t c = 4; + while (c < content_width && i < text.length()) { + if (text[i] == '\n') { + while (i < text.length() && text[i] == '\n') + i++; + break; + } + uint32_t cluster_len = grapheme_next_character_break_utf8( + text.c_str() + i, text.length() - i); + std::string cluster = text.substr(i, cluster_len); + int width = display_width(cluster.c_str(), cluster_len); + if (c + width > content_width) + break; + set(r + 1, c + 1, cluster.c_str(), c_sym, base_bg, 0); + c += width; + i += cluster_len; + for (int w = 1; w < width; w++) + set(r + 1, c - w + 1, "\x1b", c_sym, base_bg, 0); + } + r++; + } + if (r >= 23) + break; + if (warn.code != "") { + for (uint32_t i = 0; i < warn.code.length() && i + 5 < content_width; i++) + set(r + 1, i + 5, (char[2]){warn.code[i], 0}, 0x81cdc6, base_bg, 0); + r++; + } + if (r >= 23) + break; + for (std::string &see_also : warn.see_also) { + uint32_t fg = 0xB55EFF; + uint8_t colon_count = 0; + for (uint32_t i = 0; i < see_also.length() && i + 5 < content_width; + i++) { + set(r + 1, i + 5, (char[2]){see_also[i], 0}, fg, base_bg, 0); + if (see_also[i] == ':') + colon_count++; + if (colon_count == 2) + fg = 0xFFFFFF; + } + r++; + if (r >= 23) + break; + }; + idx++; + } + box_height = 2 + r; + set(0, 0, "β”Œ", border_fg, base_bg, 0); + for (uint32_t i = 1; i < box_width - 1; i++) + set(0, i, "─", border_fg, base_bg, 0); + set(0, box_width - 1, "┐", border_fg, base_bg, 0); + for (uint32_t r = 1; r < box_height - 1; r++) { + set(r, 0, "β”‚", border_fg, base_bg, 0); + set(r, box_width - 1, "β”‚", border_fg, base_bg, 0); + } + set(box_height - 1, 0, "β””", border_fg, base_bg, 0); + for (uint32_t i = 1; i < box_width - 1; i++) + set(box_height - 1, i, "─", border_fg, base_bg, 0); + set(box_height - 1, box_width - 1, "β”˜", border_fg, base_bg, 0); + cells.resize(box_width * box_height); +} + +void DiagnosticBox::render(Coord pos) { + int32_t start_row = (int32_t)pos.row - (int32_t)box_height; + if (start_row < 0) + start_row = pos.row + 1; + int32_t start_col = pos.col; + if (start_col + box_width > cols) { + start_col = cols - box_width; + if (start_col < 0) + start_col = 0; + } + for (uint32_t r = 0; r < box_height; r++) + for (uint32_t c = 0; c < box_width; c++) + update(start_row + r, start_col + c, cells[r * box_width + c].utf8, + cells[r * box_width + c].fg, cells[r * box_width + c].bg, + cells[r * box_width + c].flags); +} diff --git a/src/lsp.cc b/src/lsp.cc index 6ee6c26..667c742 100644 --- a/src/lsp.cc +++ b/src/lsp.cc @@ -14,7 +14,6 @@ std::unordered_map> active_lsps; Queue lsp_open_queue; static bool init_lsp(std::shared_ptr lsp) { - log("initializing %s\n", lsp->lsp->command); int in_pipe[2]; int out_pipe[2]; if (pipe(in_pipe) == -1 || pipe(out_pipe) == -1) { @@ -29,16 +28,26 @@ static bool init_lsp(std::shared_ptr lsp) { if (pid == 0) { dup2(in_pipe[0], STDIN_FILENO); dup2(out_pipe[1], STDOUT_FILENO); +#ifdef __clang__ int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); } +#else + int log = open("/tmp/lsp.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (log >= 0) { + dup2(log, STDERR_FILENO); + close(log); + } +#endif + close(in_pipe[0]); close(in_pipe[1]); close(out_pipe[0]); + close(out_pipe[1]); execvp(lsp->lsp->command, (char *const *)(lsp->lsp->args.data())); perror("execvp"); - return false; + _exit(127); } lsp->pid = pid; lsp->stdin_fd = in_pipe[1]; @@ -59,7 +68,6 @@ std::shared_ptr get_or_init_lsp(uint8_t lsp_id) { lsp->lsp = &map_it->second; if (!init_lsp(lsp)) return nullptr; - log("starting %s\n", lsp->lsp->command); LSPPending *pending = new LSPPending(); pending->method = "initialize"; pending->editor = nullptr; @@ -76,14 +84,17 @@ std::shared_ptr get_or_init_lsp(uint8_t lsp_id) { lsp->incremental_sync = (change_type == 2); } } + if (caps.contains("hoverProvider")) { + lsp->allow_hover = caps["hoverProvider"].get(); + } else { + lsp->allow_hover = false; + } } - log("incremental_sync %d\n", lsp->incremental_sync); lsp->initialized = true; json initialized = {{"jsonrpc", "2.0"}, {"method", "initialized"}, {"params", json::object()}}; lsp_send(lsp, initialized, nullptr); - log("initialized %s\n", lsp->lsp->command); while (!lsp->open_queue.empty()) { std::pair request; lsp->open_queue.pop(request); @@ -98,7 +109,8 @@ std::shared_ptr get_or_init_lsp(uint8_t lsp_id) { {"rootUri", "file://" + percent_encode(path_abs("."))}, {"capabilities", {{"textDocument", - {{"publishDiagnostics", {{"relatedInformation", true}}}}}}}}}}; + {{"publishDiagnostics", {{"relatedInformation", true}}}, + {"hover", {{"contentFormat", {"markdown", "plaintext"}}}}}}}}}}}; lsp_send(lsp, init_message, pending); active_lsps[lsp_id] = lsp; return lsp; @@ -185,7 +197,6 @@ static std::optional read_lsp_message(int fd) { return std::nullopt; got += n; } - log("%s\n", body.c_str()); return json::parse(body); } @@ -200,7 +211,6 @@ static Editor *editor_for_uri(std::shared_ptr lsp, } static void clean_lsp(std::shared_ptr lsp, uint8_t lsp_id) { - log("cleaning up lsp %d\n", lsp_id); for (auto &kv : lsp->pending) delete kv.second; lsp->pid = -1; @@ -317,7 +327,6 @@ void lsp_worker() { } void request_add_to_lsp(Language language, Editor *editor) { - log("request_add_to_lsp %d\n", language.lsp_id); lsp_open_queue.push({language, editor}); } diff --git a/src/renderer.cc b/src/renderer.cc index a4c40de..37bcd36 100644 --- a/src/renderer.cc +++ b/src/renderer.cc @@ -1,4 +1,5 @@ #include "../include/ui.h" +#include "../include/utils.h" uint32_t rows, cols; bool show_cursor = 0; @@ -55,6 +56,9 @@ void update(uint32_t row, uint32_t col, std::string utf8, uint32_t fg, uint32_t idx = row * cols + col; std::lock_guard lock(screen_mutex); screen[idx].utf8 = utf8 != "" ? utf8 : ""; + if (utf8 == "") + return; + screen[idx].width = display_width(utf8.c_str(), utf8.size()); screen[idx].fg = fg; screen[idx].bg = bg; screen[idx].flags = flags; @@ -67,15 +71,55 @@ void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg, uint32_t idx = row * cols + col; std::lock_guard lock(screen_mutex); screen[idx].utf8 = utf8 ? utf8 : ""; + if (utf8 == nullptr) + return; + screen[idx].width = display_width(utf8, strlen(utf8)); screen[idx].fg = fg; screen[idx].bg = bg; screen[idx].flags = flags; } +void update(uint32_t row, uint32_t col, std::string utf8, uint32_t fg, + uint32_t bg, uint8_t flags, uint32_t ul_color) { + if (row >= rows || col >= cols) + return; + uint32_t idx = row * cols + col; + std::lock_guard lock(screen_mutex); + screen[idx].utf8 = utf8 != "" ? utf8 : ""; + if (utf8 == "") + return; + screen[idx].width = display_width(utf8.c_str(), utf8.size()); + screen[idx].fg = fg; + screen[idx].bg = bg; + screen[idx].flags = flags; + screen[idx].ul_color = ul_color; +} + +void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg, + uint32_t bg, uint8_t flags, uint32_t ul_color) { + if (row >= rows || col >= cols) + return; + uint32_t idx = row * cols + col; + std::lock_guard lock(screen_mutex); + screen[idx].utf8 = utf8 ? utf8 : ""; + if (utf8 == nullptr) + return; + screen[idx].width = display_width(utf8, strlen(utf8)); + screen[idx].fg = fg; + screen[idx].bg = bg; + screen[idx].flags = flags; + screen[idx].ul_color = ul_color; +} + +inline bool is_empty_cell(const ScreenCell &c) { + return c.utf8.empty() || c.utf8 == " " || c.utf8 == "\x1b"; +} + void render() { static bool first_render = true; uint32_t current_fg = 0; uint32_t current_bg = 0; + uint32_t current_ul_color = 0; bool current_italic = false; bool current_bold = false; bool current_underline = false; @@ -94,15 +138,25 @@ void render() { uint32_t idx = row * cols + col; ScreenCell &old_cell = old_screen[idx]; ScreenCell &new_cell = screen[idx]; - bool content_changed = old_cell.utf8 != new_cell.utf8; - bool style_changed = - (old_cell.fg != new_cell.fg) || (old_cell.bg != new_cell.bg) || - ((old_cell.flags & CF_ITALIC) != (new_cell.flags & CF_ITALIC)) || - ((old_cell.flags & CF_BOLD) != (new_cell.flags & CF_BOLD)) || - ((old_cell.flags & CF_UNDERLINE) != (new_cell.flags & CF_UNDERLINE)); - if (content_changed || style_changed) { - if (first_change_col == -1) + bool content_changed = + old_cell.utf8 != new_cell.utf8 || old_cell.fg != new_cell.fg || + old_cell.bg != new_cell.bg || old_cell.flags != new_cell.flags || + old_cell.ul_color != new_cell.ul_color; + if (content_changed) { + if (first_change_col == -1) { first_change_col = col; + if (first_change_col > 0) { + for (int back = 1; back <= 3 && first_change_col - back >= 0; + ++back) { + ScreenCell &prev_cell = + screen[row * cols + (first_change_col - back)]; + if (prev_cell.width > 1) { + first_change_col -= back; + break; + } + } + } + } last_change_col = col; } } @@ -115,6 +169,20 @@ void render() { int idx = row * cols + col; ScreenCell &old_cell = old_screen[idx]; ScreenCell &new_cell = screen[idx]; + int width = new_cell.width > 0 ? new_cell.width : 1; + bool overlap = false; + if (width > 1) { + for (int i = 1; i < width; ++i) { + int next_col = col + i; + if (next_col >= cols) + break; + const ScreenCell &next = screen[row * cols + next_col]; + if (!is_empty_cell(next)) { + overlap = true; + break; + } + } + } if (current_fg != new_cell.fg) { if (new_cell.fg) { char fb[64]; @@ -150,24 +218,43 @@ void render() { current_bold = bold; } bool underline = (new_cell.flags & CF_UNDERLINE) != 0; + if (underline) { + if (new_cell.ul_color != current_ul_color) { + if (new_cell.ul_color) { + char ubuf[64]; + snprintf(ubuf, sizeof(ubuf), "\x1b[58;2;%d;%d;%dm", + (new_cell.ul_color >> 16) & 0xFF, + (new_cell.ul_color >> 8) & 0xFF, + (new_cell.ul_color >> 0) & 0xFF); + out.append(ubuf); + } else { + out += "\x1b[59m"; + } + current_ul_color = new_cell.ul_color; + } + } if (underline != current_underline) { out += underline ? "\x1b[4m" : "\x1b[24m"; current_underline = underline; } - if (!new_cell.utf8.empty()) { - if (new_cell.utf8[0] == '\t') - out.append(" "); - else if (new_cell.utf8[0] == '\x1b') - out.append(""); - else - out.append(new_cell.utf8); + if (width > 1 && overlap) { + for (int i = 1; i < width; ++i) + out.push_back(' '); } else { - out.append(1, ' '); + if (!new_cell.utf8.empty()) { + if (new_cell.utf8[0] == '\t') + out.append(" "); + else if (new_cell.utf8[0] != '\x1b') + out.append(new_cell.utf8); + } else { + out.push_back(' '); + } } old_cell.utf8 = new_cell.utf8; old_cell.fg = new_cell.fg; old_cell.bg = new_cell.bg; old_cell.flags = new_cell.flags; + old_cell.width = new_cell.width; } } out += "\x1b[0m"; diff --git a/src/ts.cc b/src/ts.cc index 908f4ee..7742f90 100644 --- a/src/ts.cc +++ b/src/ts.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -118,24 +119,21 @@ static inline const TSNode *find_capture_node(const TSQueryMatch &match, return nullptr; } -static inline std::string node_text(const TSNode &node, Knot *source) { - uint32_t start = ts_node_start_byte(node); - uint32_t end = ts_node_end_byte(node); +static inline std::string node_text(uint32_t start, uint32_t end, + Knot *source) { char *text = read(source, start, end - start); std::string final = std::string(text, end - start); free(text); return final; } -static inline bool ts_predicate(TSQuery *query, const TSQueryMatch &match, - Knot *source) { +bool ts_predicate(TSQuery *query, const TSQueryMatch &match, + std::function subject_fn) { uint32_t step_count; const TSQueryPredicateStep *steps = ts_query_predicates_for_pattern(query, match.pattern_index, &step_count); if (!steps || step_count != 4) return true; - if (source->char_count >= (1024 * 64)) - return false; std::string command; std::string regex_txt; uint32_t subject_id = 0; @@ -163,8 +161,8 @@ static inline bool ts_predicate(TSQuery *query, const TSQueryMatch &match, } } const TSNode *node = find_capture_node(match, subject_id); - std::string subject = node_text(*node, source); pcre2_code *re = get_re(regex_txt); + std::string subject = subject_fn(node); pcre2_match_data *md = pcre2_match_data_create_from_pattern(re, nullptr); int rc = pcre2_match(re, (PCRE2_SPTR)subject.c_str(), subject.size(), 0, 0, md, nullptr); @@ -183,14 +181,6 @@ const char *read_ts(void *payload, uint32_t byte_index, TSPoint, return leaf_from_offset(editor->root, byte_index, bytes_read); } -template -static inline T *safe_get(std::map &m, uint16_t key) { - auto it = m.find(key); - if (it == m.end()) - return nullptr; - return &it->second; -} - void ts_collect_spans(Editor *editor) { static int parse_counter = 0; if (!editor->ts.parser || !editor->root || !editor->ts.query) @@ -283,7 +273,12 @@ void ts_collect_spans(Editor *editor) { std::unordered_map pending_injections; TSQueryMatch match; while (ts_query_cursor_next_match(cursor, &match)) { - if (!ts_predicate(q, match, editor->root)) + auto subject_fn = [&](const TSNode *node) -> std::string { + uint32_t start = ts_node_start_byte(*node); + uint32_t end = ts_node_end_byte(*node); + return node_text(start, end, editor->root); + }; + if (!ts_predicate(q, match, subject_fn)) continue; for (uint32_t i = 0; i < match.capture_count; i++) { TSQueryCapture cap = match.captures[i]; diff --git a/src/utils.cc b/src/utils.cc index c9bbf03..008c6b3 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -5,9 +5,65 @@ extern "C" { #include "../include/maps.h" #include "../include/utils.h" +std::vector find_all_matches(const std::string &subject, + const std::string &pattern) { + std::vector results; + int errornumber; + PCRE2_SIZE erroroffset; + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern.c_str(), pattern.size(), 0, + &errornumber, &erroroffset, nullptr); + if (!re) + return results; + pcre2_match_data *match_data = + pcre2_match_data_create_from_pattern(re, nullptr); + PCRE2_SIZE offset = 0; + int rc; + while ((rc = pcre2_match(re, (PCRE2_SPTR)subject.c_str(), subject.size(), + offset, 0, match_data, nullptr)) >= 0) { + PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data); + for (int i = 0; i < rc; ++i) { + size_t start = ovector[2 * i]; + size_t end = ovector[2 * i + 1]; + results.push_back({start, end, subject.substr(start, end - start)}); + } + offset = (ovector[1] == offset) ? offset + 1 : ovector[1]; + if (offset > subject.size()) + break; + } + pcre2_match_data_free(match_data); + pcre2_code_free(re); + return results; +} + +std::string percent_decode(const std::string &s) { + std::string out; + out.reserve(s.size()); + for (size_t i = 0; i < s.size(); ++i) { + if (s[i] == '%' && i + 2 < s.size() && std::isxdigit(s[i + 1]) && + std::isxdigit(s[i + 2])) { + auto hex = [](char c) -> int { + if ('0' <= c && c <= '9') + return c - '0'; + if ('a' <= c && c <= 'f') + return c - 'a' + 10; + if ('A' <= c && c <= 'F') + return c - 'A' + 10; + return 0; + }; + char decoded = (hex(s[i + 1]) << 4) | hex(s[i + 2]); + out.push_back(decoded); + i += 2; + } else { + out.push_back(s[i]); + } + } + return out; +} + std::string percent_encode(const std::string &s) { static const char *hex = "0123456789ABCDEF"; std::string out; + out.reserve(s.size() * 3); for (unsigned char c : s) { if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') { @@ -185,8 +241,69 @@ uint32_t count_clusters(const char *line, size_t len, size_t from, size_t to) { return count; } +std::string trim(const std::string &s) { + size_t start = s.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) + return ""; + size_t end = s.find_last_not_of(" \t\n\r"); + return s.substr(start, end - start + 1); +} + +std::string clean_text(const std::string &input) { + std::string result = input; + static const std::unordered_map entities = { + {" ", " "}, {"<", "<"}, {">", ">"}, + {"&", "&"}, {""", "\""}, {"'", "'"}}; + for (const auto &e : entities) { + size_t pos = 0; + while ((pos = result.find(e.first, pos)) != std::string::npos) { + result.replace(pos, e.first.length(), e.second); + pos += e.second.length(); + } + } + int errorcode; + PCRE2_SIZE erroroffset; + pcre2_code *re = + pcre2_compile((PCRE2_SPTR) "(\n\\s*)+", PCRE2_ZERO_TERMINATED, 0, + &errorcode, &erroroffset, nullptr); + if (!re) + return result; + pcre2_match_data *match_data = + pcre2_match_data_create_from_pattern(re, nullptr); + PCRE2_SIZE offset = 0; + std::string clean; + while (offset < result.size()) { + int rc = pcre2_match(re, (PCRE2_SPTR)result.c_str(), result.size(), offset, + 0, match_data, nullptr); + if (rc < 0) { + clean += result.substr(offset); + break; + } + PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data); + clean += result.substr(offset, ovector[0] - offset) + "\n"; + offset = ovector[1]; + } + pcre2_match_data_free(match_data); + pcre2_code_free(re); + std::string final_str; + size_t start = 0; + while (start < clean.size()) { + size_t end = clean.find('\n', start); + if (end == std::string::npos) + end = clean.size(); + std::string line = clean.substr(start, end - start); + size_t first = line.find_first_not_of(" \t\r"); + size_t last = line.find_last_not_of(" \t\r"); + if (first != std::string::npos) + final_str += line.substr(first, last - first + 1) + "\n"; + start = end + 1; + } + if (!final_str.empty() && final_str.back() == '\n') + final_str.pop_back(); + return final_str; +} + void log(const char *fmt, ...) { -#if defined(__GNUC__) && !defined(__clang__) FILE *fp = fopen("/tmp/log.txt", "a"); if (!fp) return; @@ -196,7 +313,6 @@ void log(const char *fmt, ...) { va_end(args); fputc('\n', fp); fclose(fp); -#endif } char *load_file(const char *path, uint32_t *out_len) {