diff --git a/README.md b/README.md index d401ade..f9b99f0 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,13 @@ A TUI IDE. # TODO -- [ ] Normalize completions edits if local filtering is used - [ ] Check why fish is behaving soo off with completions filtering -- [ ] Also why clangd doesnt show any completions but applys them at 0,0 -- [ ] Also why solargraph applys edits at 0,0 +- [ ] Normalize completions edits if local filtering is used +- [ ] Capture ctrl+h,l for scrolling documentation +- [ ] Documentation fix position and make it async first render +- [ ] Allow completion list to be scrolled up and down and show only x max at a time +- [ ] Dont filter case sensitive. +- [ ] Do not recompute word under cursor if not changed - [ ] Finish autocomplete box style functions. - [ ] Add status bar & RUNNER mode - [ ] Get code context from tree-sitter diff --git a/grammar/h.scm b/grammar/h.scm index 306a7a0..b60f3fa 100644 --- a/grammar/h.scm +++ b/grammar/h.scm @@ -506,29 +506,29 @@ ; Macros & directives ; ============================================================ -;; #F29CC3 #000000 0 0 0 0 2 +;; #F29CC3 #000000 0 0 0 0 3 (preproc_def name: (_) @constant.macro) -;; #F29CC3 #000000 0 0 0 0 2 +;; #F29CC3 #000000 0 0 0 0 3 (preproc_call directive: (preproc_directive) @_u argument: (_) @constant.macro (#match? @_u "^#undef$")) -;; #F29CC3 #000000 0 0 0 0 2 +;; #F29CC3 #000000 0 0 0 0 3 (preproc_ifdef name: (identifier) @constant.macro) -;; #F29CC3 #000000 0 0 0 0 2 +;; #F29CC3 #000000 0 0 0 0 3 (preproc_elifdef name: (identifier) @constant.macro) -;; #F29CC3 #000000 0 0 0 0 2 +;; #F29CC3 #000000 0 0 0 0 3 (preproc_defined (identifier) @constant.macro) -;; #F29CC3 #000000 0 0 0 0 2 +;; #F29CC3 #000000 0 0 0 0 3 (preproc_defined) @function.macro ; ============================================================ diff --git a/include/editor/completions.h b/include/editor/completions.h index 8a44512..dfc52d2 100644 --- a/include/editor/completions.h +++ b/include/editor/completions.h @@ -34,6 +34,8 @@ struct CompletionSession { std::optional trigger_char; uint8_t trigger = 0; CompletionBox box; + HoverBox hover; + uint32_t doc = UINT32_MAX; CompletionSession() : box(this) {} }; diff --git a/include/editor/decl.h b/include/editor/decl.h index 1b10beb..6830f37 100644 --- a/include/editor/decl.h +++ b/include/editor/decl.h @@ -4,9 +4,7 @@ #include "utils/utils.h" struct TextEdit { - // NOTE: start.col is in utf16 index and not clusters or utf8 Coord start; - // NOTE: end.col is in utf16 index and not clusters or utf8 Coord end; std::string text; }; diff --git a/include/editor/editor.h b/include/editor/editor.h index cf10e96..fc4f4b5 100644 --- a/include/editor/editor.h +++ b/include/editor/editor.h @@ -18,6 +18,8 @@ #define EXTRA_META 4 #define INDENT_WIDTH 2 +// autocomplete lua// Bracket closing / tab on enter + struct Editor { std::string filename; std::string uri; @@ -95,7 +97,7 @@ uint32_t leading_indent(const char *line, uint32_t len); uint32_t get_indent(Editor *editor, Coord cursor); bool closing_after_cursor(const char *line, uint32_t len, uint32_t col); void editor_lsp_handle(Editor *editor, json msg); -void apply_lsp_edits(Editor *editor, std::vector edits); +void apply_lsp_edits(Editor *editor, std::vector edits, bool move); void completion_resolve_doc(Editor *editor); void complete_accept(Editor *editor); void complete_next(Editor *editor); @@ -116,4 +118,56 @@ inline void apply_hook_deletion(Editor *editor, uint32_t removal_start, hook -= removal_end - removal_start + 1; } +inline static void utf8_normalize_edit(Editor *editor, TextEdit *edit) { + std::shared_lock lock(editor->knot_mtx); + if (edit->start.row > editor->root->line_count) { + edit->start.row = editor->root->line_count; + edit->start.col = UINT32_MAX; + } + if (edit->end.row > editor->root->line_count) { + edit->end.row = editor->root->line_count; + edit->end.col = UINT32_MAX; + } + LineIterator *it = begin_l_iter(editor->root, edit->start.row); + if (!it) + return; + uint32_t len; + char *line = next_line(it, &len); + if (!line) { + free(it->buffer); + free(it); + return; + } + if (edit->start.col < len) + edit->start.col = utf16_offset_to_utf8(line, edit->start.col); + else + edit->start.col = len; + if (edit->end.row == edit->start.row) { + if (edit->end.col < len) + edit->end.col = utf16_offset_to_utf8(line, edit->end.col); + else + edit->end.col = len; + free(it->buffer); + free(it); + return; + } + free(it->buffer); + free(it); + it = begin_l_iter(editor->root, edit->end.row); + if (!it) + return; + line = next_line(it, &len); + if (!line) { + free(it->buffer); + free(it); + return; + } + if (edit->end.col < len) + edit->end.col = utf16_offset_to_utf8(line, edit->end.col); + else + edit->end.col = len; + free(it->buffer); + free(it); +} + #endif diff --git a/include/lsp/lsp.h b/include/lsp/lsp.h index d67a336..096527d 100644 --- a/include/lsp/lsp.h +++ b/include/lsp/lsp.h @@ -35,6 +35,9 @@ struct LSPInstance { bool allow_hover = false; bool allow_completion = false; bool allow_resolve = false; + bool allow_formatting = false; + bool allow_formatting_on_type = false; + std::vector format_chars; std::vector trigger_chars; std::vector end_chars; uint32_t last_id = 0; @@ -53,6 +56,8 @@ static json client_capabilities = { {"textDocument", {{"publishDiagnostics", {{"relatedInformation", true}}}, {"hover", {{"contentFormat", {"markdown", "plaintext"}}}}, + {"formatting", {{"dynamicRegistration", false}}}, + {"onTypeFormatting", {{"dynamicRegistration", false}}}, {"completion", {{"completionItem", {{"commitCharactersSupport", true}, diff --git a/include/ts/ts.h b/include/ts/ts.h index 849cb74..4f5abb2 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -11,8 +11,9 @@ 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); +bool ts_predicate( + TSQuery *query, const TSQueryMatch &match, + std::function subject_fn); void clear_regex_cache(); #endif diff --git a/samples/fish.fish b/samples/fish.fish index 720e527..1b55b60 100644 --- a/samples/fish.fish +++ b/samples/fish.fish @@ -1,10 +1,10 @@ #!/usr/bin/env fish -# Fish highlighting torture test 🐟 +# Fish highlighting torture test # === Variables === -set normal_var "hello" +set normal_var hello set -l local_var 123 -set -gx GLOBAL_VAR "world" +set -gx GLOBAL_VAR world set PATH $PATH /usr/local/bin set --erase OLD_VAR @@ -17,15 +17,15 @@ set double "double quoted $normal_var" set escaped "newline\n tab\t dollar\$" # === Conditionals === -if test $normal_var = "hello" - echo "equal" -else if test $normal_var != "world" +if test $normal_var = hello + echo equal +else if test $normal_var != world echo "not equal" end # === Logical operators === -true and echo "yes" -false or echo "fallback" +true and echo yes +false or echo fallback not false # === Arithmetic === @@ -50,14 +50,14 @@ function greet --argument name echo "Hello $name" end -greet "world" +greet world # === Command substitution === set files (ls | grep ".fish") # === Redirections === -echo "output" > /tmp/fish_test.txt -cat < /tmp/fish_test.txt >> /tmp/fish_log.txt +echo output >/tmp/fish_test.txt +cat >/tmp/fish_log.txt # === Process substitution === diff (ls /bin) (ls /usr/bin) @@ -65,11 +65,11 @@ diff (ls /bin) (ls /usr/bin) # === Case statement === switch $argv[1] case start - echo "Starting" + echo Starting case stop - echo "Stopping" + echo Stopping case '*' - echo "Unknown" + echo Unknown end # === Subshell === @@ -79,10 +79,10 @@ end # === Comments & operators === # && || | & ! should all highlight -true && echo "ok" || echo "fail" +true && echo ok || echo fail # === Regex === -string match -r '^[a-z]+$' "hello" +string match -r '^[a-z]+$' hello # === Test builtin === test -f /etc/passwd @@ -90,3 +90,4 @@ test ! -d /does/not/exist # === Exit === exit 0 + diff --git a/samples/php.php b/samples/php.php index de79654..7086cc4 100644 --- a/samples/php.php +++ b/samples/php.php @@ -1,5 +1,6 @@ + PHP Syntax Stress Test @@ -33,98 +34,103 @@ - 1, - "two" => 2 -]; + // Arrays + $list = [1, 2, 3]; + $assoc = [ + "one" => 1, + "two" => 2 + ]; -// Function -function add(int $a, int $b): int { - return $a + $b; -} - -// Class + methods -class User { - private string $name; - public static int $count = 0; - - public function __construct(string $name) { - $this->name = $name; - self::$count++; + // Function + function add(int $a, int $b): int + { + return $a + $b; } - public function greet(): string { - return "Hello {$this->name}"; + // Class + methods + class User + { + private string $name; + public static int $count = 0; + + public function __construct(string $name) + { + $this->name = $name; + self::$count++; + } + + public function greet(): string + { + return "Hello {$this->name}"; + } } -} -// Object usage -$user = new User("Alice"); -echo $user->greet(); + // Object usage + $user = new User("Alice"); + echo $user->greet(); -// Control flow -if ($number > 10) { - echo "Big number"; -} elseif ($number === 10) { - echo "Exactly ten"; -} else { - echo "Small number"; -} + // Control flow + if ($number > 10) { + echo "Big number"; + } elseif ($number === 10) { + echo "Exactly ten"; + } else { + echo "Small number"; + } -// Loop -foreach ($list as $item) { - echo $item; -} + // Loop + foreach ($list as $item) { + echo $item; + } -// Match expression -$result = match ($number) { - 1 => "one", - 2 => "two", - default => "many" -}; + // Match expression + $result = match ($number) { + 1 => "one", + 2 => "two", + default => "many" + }; -// Try / catch -try { - throw new Exception("Test exception"); -} catch (Exception $e) { - echo $e->getMessage(); -} + // Try / catch + try { + throw new Exception("Test exception"); + } catch (Exception $e) { + echo $e->getMessage(); + } -// Anonymous function -$double = fn($x) => $x * 2; + // Anonymous function + $double = fn($x) => $x * 2; -// Nullsafe operator -$len = $user?->name ? strlen($user->name) : 0; + // Nullsafe operator + $len = $user?->name ? strlen($user->name) : 0; -// Ternary -$status = $truth ? "yes" : "no"; + // Ternary + $status = $truth ? "yes" : "no"; -// Include / require -require_once "config.php"; + // Include / require + require_once "config.php"; -// Output -echo "
"; -echo htmlspecialchars($text); -echo "
"; -?> + // Output + echo "
"; + echo htmlspecialchars($text); + echo "
"; + ?> - + + diff --git a/samples/python.py b/samples/python.py index 85648e3..0b6aaef 100644 --- a/samples/python.py +++ b/samples/python.py @@ -1,3 +1,4 @@ +from __future__ import annotations #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Test file for Python Tree-sitter highlighting.""" @@ -17,7 +18,6 @@ __name__ # builtin constant import os import sys as system from re import compile as re_compile -from __future__ import annotations from math import * # ============================== @@ -94,7 +94,7 @@ while x > 0: try: 1 / 0 -except ZeroDivisionError as e: +except ZeroDivisionError as err: raise finally: pass @@ -105,7 +105,7 @@ finally: a, b = 5, 10 c = a + b * 2 // 3 % 4 ** 2 d = (a << 2) & b | c ^ ~a -e = not a or b and c +ef = not a or b and c # ============================== # f-strings / interpolation @@ -131,7 +131,7 @@ def static_func(): def cls_func(cls): return cls -@custom_decorator +# @custom_decorator def decorated_func(): return None diff --git a/src/editor/completions.cc b/src/editor/completions.cc index 55ff86a..e1028d8 100644 --- a/src/editor/completions.cc +++ b/src/editor/completions.cc @@ -1,9 +1,11 @@ +#include "editor/decl.h" #include "editor/editor.h" #include "io/knot.h" #include "io/sysio.h" #include "lsp/lsp.h" #include "main.h" #include "utils/utils.h" +#include inline static std::string completion_prefix(Editor *editor) { Coord hook = editor->completion.hook; @@ -50,23 +52,15 @@ void completion_filter(Editor *editor) { void completion_request(Editor *editor) { Coord hook = editor->cursor; word_boundaries(editor, editor->cursor, &hook.col, nullptr, nullptr, nullptr); - LineIterator *it = begin_l_iter(editor->root, hook.row); - char *line = next_line(it, nullptr); - if (!line) { - free(it->buffer); - free(it); - return; - } editor->completion.active = true; editor->completion.items.clear(); editor->completion.visible.clear(); editor->completion.select = 0; - hook.col = utf8_byte_offset_to_utf16(line, hook.col); editor->completion.hook = hook; LSPPending *pending = new LSPPending(); pending->editor = editor; pending->method = "textDocument/completion"; - pending->callback = [line, it](Editor *editor, std::string, json message) { + pending->callback = [](Editor *editor, std::string, json message) { auto &session = editor->completion; std::unique_lock lock(session.mtx); std::vector items_json; @@ -115,8 +109,15 @@ void completion_request(Editor *editor) { item.is_markup = item_json["documentation"]["kind"].get() == "markdown"; - item.documentation = + std::string documentation = item_json["documentation"]["value"].get(); + if (item.is_markup) { + static const std::regex fence_no_lang("```(\\s*\\n)"); + item.documentation = std::regex_replace( + documentation, fence_no_lang, "```" + editor->lang.name + "$1"); + } else { + item.documentation = documentation; + } } } if (item_json.contains("deprecated") && @@ -147,26 +148,27 @@ void completion_request(Editor *editor) { edit.start.col = te["insert"]["start"]["character"]; edit.end.row = te["insert"]["end"]["line"]; edit.end.col = te["insert"]["end"]["character"]; + } else if (te.contains("range")) { + edit.start.row = te["range"]["start"]["line"]; + edit.start.col = te["range"]["start"]["character"]; + edit.end.row = te["range"]["end"]["line"]; + edit.end.col = te["range"]["end"]["character"]; + } else { + edit.start = session.hook; + edit.end = editor->cursor; } - } else { - edit.text = te.value("newText", ""); - edit.start.row = te["range"]["start"]["line"]; - edit.start.col = te["range"]["start"]["character"]; - edit.end.row = te["range"]["end"]["line"]; - edit.end.col = te["range"]["end"]["character"]; } } else if (item_json.contains("insertText") && item_json["insertText"].is_string()) { edit.text = item_json["insertText"].get(); edit.start = session.hook; - uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); - edit.end = {editor->cursor.row, col}; + edit.end = editor->cursor; } else { edit.text = item.label; edit.start = session.hook; - uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); - edit.end = {editor->cursor.row, col}; + edit.end = editor->cursor; } + utf8_normalize_edit(editor, &edit); item.edits.push_back(edit); if (item_json.contains("additionalTextEdits")) { for (auto &te : item_json["additionalTextEdits"]) { @@ -176,6 +178,7 @@ void completion_request(Editor *editor) { edit.start.col = te["range"]["start"]["character"]; edit.end.row = te["range"]["end"]["line"]; edit.end.col = te["range"]["end"]["character"]; + utf8_normalize_edit(editor, &edit); item.edits.push_back(edit); } } @@ -187,15 +190,23 @@ void completion_request(Editor *editor) { if (c.is_string() && c.get().size() == 1) item.end_chars.push_back(c.get()[0]); session.items.push_back(std::move(item)); - session.visible.push_back(session.items.size() - 1); } completion_filter(editor); session.box.hidden = false; session.box.render_update(); + }; + std::shared_lock lock(editor->knot_mtx); + LineIterator *it = begin_l_iter(editor->root, hook.row); + char *line = next_line(it, nullptr); + if (!line) { free(it->buffer); free(it); - }; + return; + } uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); + free(it->buffer); + free(it); + lock.unlock(); json message = { {"jsonrpc", "2.0"}, {"method", "textDocument/completion"}, @@ -315,10 +326,26 @@ void completion_resolve_doc(Editor *editor) { pending->callback = [](Editor *editor, std::string, json message) { std::unique_lock lock(editor->completion.mtx); auto &item = editor->completion.items[editor->completion.select]; - if (message.contains("documentation")) - item.documentation = message["documentation"].get(); - else - item.documentation = ""; + if (message["result"].contains("documentation")) { + if (message["result"]["documentation"].is_string()) { + item.documentation = + message["result"]["documentation"].get(); + } else if (message["result"]["documentation"].contains("value") && + message["result"]["documentation"]["value"].is_string()) { + item.is_markup = + message["result"]["documentation"]["kind"].get() == + "markdown"; + std::string documentation = + message["result"]["documentation"]["value"].get(); + if (item.is_markup) { + static const std::regex fence_no_lang("```(\\s*\\n)"); + item.documentation = std::regex_replace( + documentation, fence_no_lang, "```" + editor->lang.name + "$1"); + } else { + item.documentation = documentation; + } + } + } editor->completion.box.render_update(); }; json message = {{"jsonrpc", "2.0"}, @@ -332,7 +359,7 @@ void complete_accept(Editor *editor) { return; auto &item = editor->completion.items[editor->completion.select]; // TODO: support snippets here - apply_lsp_edits(editor, item.edits); + apply_lsp_edits(editor, item.edits, true); editor->completion.active = false; } diff --git a/src/editor/edit.cc b/src/editor/edit.cc index 914a838..18e47cf 100644 --- a/src/editor/edit.cc +++ b/src/editor/edit.cc @@ -283,21 +283,15 @@ void edit_insert(Editor *editor, Coord pos, char *data, uint32_t len) { void edit_replace(Editor *editor, Coord start, Coord end, const char *text, uint32_t len) { std::shared_lock lock(editor->knot_mtx); - uint32_t start_line_byte = line_to_byte(editor->root, start.row, nullptr); - uint32_t end_len; - uint32_t end_line_byte_start = line_to_byte(editor->root, end.row, &end_len); - uint32_t end_line_byte = end_line_byte_start + len; + uint32_t start_byte = + line_to_byte(editor->root, start.row, nullptr) + start.col; + uint32_t end_byte = line_to_byte(editor->root, end.row, nullptr) + end.col; lock.unlock(); - char *buf = - read(editor->root, start_line_byte, end_line_byte - start_line_byte); + char *buf = read(editor->root, start_byte, end_byte - start_byte); if (!buf) return; - uint32_t start_col = utf16_offset_to_utf8(buf, start.col); - uint32_t end_col = utf16_offset_to_utf8( - buf + (end_line_byte_start - start_line_byte), end.col); uint32_t erase_len = - count_clusters(buf, end_line_byte - start_line_byte, start_col, - (end_line_byte_start - start_line_byte) + end_col); + count_clusters(buf, end_byte - start_byte, 0, end_byte - start_byte); free(buf); if (erase_len != 0) edit_erase(editor, start, erase_len); diff --git a/src/editor/editor.cc b/src/editor/editor.cc index 27eac56..45bac7a 100644 --- a/src/editor/editor.cc +++ b/src/editor/editor.cc @@ -1,6 +1,8 @@ #include "editor/editor.h" +#include "editor/decl.h" #include "lsp/lsp.h" #include "utils/utils.h" +#include Editor *new_editor(const char *filename_arg, Coord position, Coord size) { Editor *editor = new Editor(); @@ -66,15 +68,62 @@ void free_editor(Editor *editor) { void save_file(Editor *editor) { if (!editor || !editor->root) return; + std::shared_lock lock(editor->knot_mtx); char *str = read(editor->root, 0, editor->root->char_count); if (!str) return; std::ofstream out(editor->filename); out.write(str, editor->root->char_count); + out.close(); free(str); - json msg = {{"jsonrpc", "2.0"}, - {"method", "textDocument/didSave"}, - {"params", {{"textDocument", {{"uri", editor->uri}}}}}}; - if (editor->lsp) - lsp_send(editor->lsp, msg, nullptr); + lock.unlock(); + if (editor->lsp) { + json save_msg = {{"jsonrpc", "2.0"}, + {"method", "textDocument/didSave"}, + {"params", {{"textDocument", {{"uri", editor->uri}}}}}}; + lsp_send(editor->lsp, save_msg, nullptr); + if (editor->lsp->allow_formatting) { + json msg = {{"jsonrpc", "2.0"}, + {"method", "textDocument/formatting"}, + {"params", + {{"textDocument", {{"uri", editor->uri}}}, + {"options", + {{"tabSize", 2}, + {"insertSpaces", true}, + {"trimTrailingWhitespace", true}, + {"trimFinalNewlines", true}}}}}}; + LSPPending *pending = new LSPPending(); + pending->editor = editor; + pending->method = "textDocument/formatting"; + pending->callback = [save_msg](Editor *editor, std::string, + json message) { + auto &edits = message["result"]; + if (edits.is_array()) { + std::vector t_edits; + t_edits.reserve(edits.size()); + for (auto &edit : edits) { + TextEdit t_edit; + t_edit.text = edit.value("newText", ""); + t_edit.start.row = edit["range"]["start"]["line"]; + t_edit.start.col = edit["range"]["start"]["character"]; + t_edit.end.row = edit["range"]["end"]["line"]; + t_edit.end.col = edit["range"]["end"]["character"]; + utf8_normalize_edit(editor, &t_edit); + t_edits.push_back(t_edit); + } + apply_lsp_edits(editor, t_edits, false); + ensure_scroll(editor); + std::unique_lock lock(editor->knot_mtx); + char *str = read(editor->root, 0, editor->root->char_count); + if (!str) + return; + std::ofstream out(editor->filename); + out.write(str, editor->root->char_count); + free(str); + lsp_send(editor->lsp, save_msg, nullptr); + } + }; + lsp_send(editor->lsp, msg, pending); + } + } } diff --git a/src/editor/events.cc b/src/editor/events.cc index 6228c38..236ae7c 100644 --- a/src/editor/events.cc +++ b/src/editor/events.cc @@ -612,9 +612,9 @@ void handle_editor_event(Editor *editor, KeyEvent event) { mode = NORMAL; break; } - ensure_scroll(editor); if (old_mode == mode || mode != INSERT) handle_completion(editor, event); + ensure_scroll(editor); if ((event.key_type == KEY_CHAR || event.key_type == KEY_PASTE) && event.c) free(event.c); } diff --git a/src/editor/lsp.cc b/src/editor/lsp.cc index 13055bf..698a17b 100644 --- a/src/editor/lsp.cc +++ b/src/editor/lsp.cc @@ -1,17 +1,37 @@ +#include "editor/decl.h" #include "editor/editor.h" -void apply_lsp_edits(Editor *editor, std::vector edits) { +void apply_lsp_edits(Editor *editor, std::vector edits, bool move) { + if (!edits.size()) + return; + TextEdit first = edits[0]; + Coord cursor = editor->cursor; std::sort( edits.begin(), edits.end(), [](const TextEdit &a, const TextEdit &b) { return a.start > b.start; }); for (const auto &edit : edits) edit_replace(editor, edit.start, edit.end, edit.text.c_str(), edit.text.size()); - editor->cursor = edits[0].start; - editor->cursor = move_right_pure(editor, editor->cursor, - count_clusters(edits[0].text.c_str(), - edits[0].text.size(), 0, - edits[0].text.size())); + if (move) { + std::shared_lock lock(editor->knot_mtx); + editor->cursor = first.start; + editor->cursor = + move_right_pure(editor, editor->cursor, + count_clusters(first.text.c_str(), first.text.size(), 0, + first.text.size())); + } else { + if (cursor.row >= editor->root->line_count) { + editor->cursor.row = editor->root->line_count - 1; + editor->cursor.col = 0; + } else { + std::shared_lock lock(editor->knot_mtx); + uint32_t len; + line_to_byte(editor->root, cursor.row, &len); + len--; + editor->cursor.row = cursor.row; + editor->cursor.col = cursor.col < len ? cursor.col : len; + } + } } void editor_lsp_handle(Editor *editor, json msg) { diff --git a/src/lsp/process.cc b/src/lsp/process.cc index 67a8c06..83878b3 100644 --- a/src/lsp/process.cc +++ b/src/lsp/process.cc @@ -72,10 +72,35 @@ 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_formatting = caps.value("documentFormattingProvider", false); + if (caps.contains("documentOnTypeFormattingProvider")) { + auto &fmt = caps["documentOnTypeFormattingProvider"]; + if (fmt.is_object()) { + if (fmt.contains("firstTriggerCharacter")) { + std::string s = fmt["firstTriggerCharacter"].get(); + if (s.size() == 1) + lsp->format_chars.push_back(s[0]); + } + if (fmt.contains("moreTriggerCharacter")) { + for (auto &c : fmt["moreTriggerCharacter"]) { + std::string s = c.get(); + if (s.size() == 1) + lsp->format_chars.push_back(s[0]); + } + } + lsp->allow_formatting_on_type = true; + } else if (fmt.is_boolean()) { + lsp->allow_formatting_on_type = fmt.get(); + } + } + + if (caps.contains("hoverProvider")) { + auto &hover = caps["hoverProvider"]; + lsp->allow_hover = + hover.is_boolean() ? hover.get() : hover.is_object(); + } else { lsp->allow_hover = false; + } if (caps.contains("completionProvider")) { lsp->allow_completion = true; if (caps["completionProvider"].contains("resolveProvider")) diff --git a/src/main.cc b/src/main.cc index ddef93c..aa3bb8b 100644 --- a/src/main.cc +++ b/src/main.cc @@ -24,6 +24,7 @@ void background_lsp() { } void input_listener() { + while (running) { KeyEvent event = throttle(1ms, read_key); if (event.key_type == KEY_NONE) @@ -130,3 +131,4 @@ int main(int argc, char *argv[]) { clear_regex_cache(); return 0; } + diff --git a/src/ts/ts.cc b/src/ts/ts.cc index ad295e0..0193d0a 100644 --- a/src/ts/ts.cc +++ b/src/ts/ts.cc @@ -90,15 +90,17 @@ void ts_collect_spans(Editor *editor) { ts_query_cursor_exec(cursor, q, ts_tree_root_node(item.tsset->tree)); std::unordered_map pending_injections; TSQueryMatch match; + auto subject_fn = [&](const TSNode *node, uint32_t *len) -> char * { + uint32_t start = ts_node_start_byte(*node); + uint32_t end = ts_node_end_byte(*node); + if (start == end || end > editor->root->char_count) + return nullptr; + std::shared_lock lock(editor->knot_mtx); + char *text = read(editor->root, start, end - start); + *len = end - start; + return text; + }; 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); - char *text = read(editor->root, start, end - start); - std::string final = std::string(text, end - start); - free(text); - return final; - }; if (!ts_predicate(q, match, subject_fn)) continue; for (uint32_t i = 0; i < match.capture_count; i++) { diff --git a/src/ts/utils.cc b/src/ts/utils.cc index dad6956..2f30a17 100644 --- a/src/ts/utils.cc +++ b/src/ts/utils.cc @@ -1,6 +1,7 @@ #include "config.h" #include "io/sysio.h" #include "ts/ts.h" +#include std::unordered_map regex_cache; @@ -114,8 +115,9 @@ TSQuery *load_query(const char *query_path, TSSetBase *set) { return q; } -bool ts_predicate(TSQuery *query, const TSQueryMatch &match, - std::function subject_fn) { +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); @@ -149,11 +151,14 @@ bool ts_predicate(TSQuery *query, const TSQueryMatch &match, } const TSNode *node = find_capture_node(match, subject_id); pcre2_code *re = get_re(regex_txt); - std::string subject = subject_fn(node); + uint32_t len; + char *subject = subject_fn(node, &len); + if (!subject) + return false; 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); + int rc = pcre2_match(re, (PCRE2_SPTR)subject, len, 0, 0, md, nullptr); pcre2_match_data_free(md); bool ok = (rc >= 0); + free(subject); return (command == "match?" ? ok : !ok); } diff --git a/src/ui/completionbox.cc b/src/ui/completionbox.cc index 41cf9a3..9e37bfb 100644 --- a/src/ui/completionbox.cc +++ b/src/ui/completionbox.cc @@ -1,8 +1,6 @@ #include "ui/completionbox.h" #include "editor/completions.h" #include "utils/utils.h" -#include -#include std::string item_kind_name(uint8_t kind) { switch (kind) { @@ -76,11 +74,11 @@ void CompletionBox::render_update() { if (i >= session->items.size()) continue; auto &item = session->items[i]; - max_label_len = std::max(max_label_len, (uint32_t)item.label.size()); + max_label_len = MAX(max_label_len, (uint32_t)item.label.size()); if (item.detail) - max_detail_len = std::max(max_detail_len, (uint32_t)item.detail->size()); + max_detail_len = MAX(max_detail_len, (uint32_t)item.detail->size()); max_kind_len = - std::max(max_kind_len, (uint32_t)item_kind_name(item.kind).size()); + MAX(max_kind_len, (uint32_t)item_kind_name(item.kind).size()); } size.row = session->visible.size() + 2; size.col = 2 + 2 + max_label_len + 1 + max_detail_len + 2 + max_kind_len + 1; @@ -91,7 +89,7 @@ void CompletionBox::render_update() { cells[r * size.col + c] = {std::string(text), 0, fg, bg, flags, 0}; }; uint32_t border_fg = 0x82AAFF; - uint32_t sel_bg = 0xFFFF00; + uint32_t sel_bg = 0x174225; set(0, 0, "┌", border_fg, 0, 0); for (uint32_t c = 1; c < size.col - 1; c++) set(0, c, "─", border_fg, 0, 0); @@ -99,7 +97,7 @@ void CompletionBox::render_update() { for (uint32_t row_idx = 0; row_idx < session->visible.size(); row_idx++) { uint32_t r = row_idx + 1; auto &item = session->items[session->visible[row_idx]]; - uint32_t bg = (session->visible[row_idx] == session->select) ? sel_bg : 0; + uint32_t bg = (session->visible[row_idx] == session->select) ? sel_bg : 1; uint32_t fg = 0xFFFFFF; set(r, 0, "│", border_fg, 0, 0); uint32_t c = 1; @@ -134,7 +132,7 @@ void CompletionBox::render_update() { } void CompletionBox::render(Coord pos) { - if (hidden) + if (hidden || session->visible.empty()) return; std::shared_lock lock(mtx); int32_t start_row = (int32_t)pos.row - (int32_t)size.row; @@ -152,4 +150,23 @@ void CompletionBox::render(Coord pos) { update(start_row + r, start_col + c, cells[r * size.col + c].utf8, cells[r * size.col + c].fg, cells[r * size.col + c].bg, cells[r * size.col + c].flags); + if (session->items.size() > session->select && + session->items[session->select].documentation && + *session->items[session->select].documentation != "") { + if (session->doc != session->select) { + session->doc = session->select; + session->hover.clear(); + session->hover.text = *session->items[session->select].documentation; + session->hover.is_markup = true; + session->hover.render_first(); + } else { + if ((int32_t)position.col - (int32_t)session->hover.size.col > 0) { + session->hover.render({position.row + session->hover.size.row, + position.col - session->hover.size.col}); + } else { + session->hover.render( + {position.row + session->hover.size.row, position.col + size.col}); + } + } + } } diff --git a/src/ui/hover.cc b/src/ui/hover.cc index 70214a6..0c43d11 100644 --- a/src/ui/hover.cc +++ b/src/ui/hover.cc @@ -44,12 +44,13 @@ void HoverBox::render_first(bool scroll) { TSQueryCursor *cursor = ts_query_cursor_new(); ts_query_cursor_exec(cursor, ts.query, ts_tree_root_node(ts.tree)); TSQueryMatch match; + auto subject_fn = [&](const TSNode *node, uint32_t *len) -> char * { + uint32_t start = ts_node_start_byte(*node); + uint32_t end = ts_node_end_byte(*node); + *len = end - start; + return text.data() + start; + }; 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++) { @@ -75,11 +76,6 @@ void HoverBox::render_first(bool scroll) { 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++) {