From a905e333fcefd895f4fa0c3a6800684c8b2eca6c Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Sun, 4 Jan 2026 03:27:17 +0000 Subject: [PATCH] Lsp completion logic --- README.md | 27 +-- include/editor/completions.h | 44 +++++ include/editor/decl.h | 8 + include/editor/editor.h | 12 ++ include/io/sysio.h | 17 +- include/lsp/lsp.h | 13 +- include/pch.h | 6 +- include/ts/decl.h | 6 +- include/ui/completionbox.h | 20 ++ include/ui/diagnostics.h | 3 +- include/ui/hover.h | 3 +- include/utils/utils.h | 3 + src/editor/completions.cc | 346 +++++++++++++++++++++++++++++++++++ src/editor/edit.cc | 27 +++ src/editor/events.cc | 39 ---- src/editor/lsp.cc | 9 + src/io/renderer.cc | 25 +-- src/lsp/process.cc | 28 ++- src/ui/bar.cc | 3 +- src/ui/completionbox.cc | 5 + src/ui/diagnostics.cc | 47 +++-- src/ui/hover.cc | 45 +++-- src/utils/system.cc | 2 +- src/utils/unicode.cc | 22 +++ 24 files changed, 624 insertions(+), 136 deletions(-) create mode 100644 include/editor/completions.h create mode 100644 include/ui/completionbox.h create mode 100644 src/editor/completions.cc create mode 100644 src/ui/completionbox.cc diff --git a/README.md b/README.md index 232bc88..4cf37c4 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,13 @@ A TUI IDE. # TODO +- [ ] Do auto complete box rendering functions. - [ ] Finish autocomplete box. - [ ] Add status bar & RUNNER mode - [ ] Get code context from tree-sitter - [ ] Maybe hide boxes in !`normal` mode - [ ] expand color regex to match css colors if in css file -- [ ] Fix indentation logic +- [ ] Fix indentation logic - tree-sitter indents too if possible - Make it work by one getting the identation used in a file by first checking if it has any line with 2 or more spaces then the least one is set to be the indent or if it is tabs then tabs but if there are none then use a table of file type to its indentation or use 2 spaces as default. store this info as `1 = tab` and `2 or more = those many spaces`. - Use this when indenting and unindenting. And also when getting the identation of a line. - Also indent when going immediately to newline should follow indent of previous line regardless of file default. @@ -19,25 +20,11 @@ A TUI IDE. - [ ] For `"insertTextFormat": 2` in `clangd` and similar use only the last word in the signature when replacing - [ ] Keep a list of words in the current buffer. (for auto completion) (maybe?) - [ ] 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) - - `textDocument/selectionRange` // - - `textDocument/completion` - Obviously - - `textDocument/onTypeFormatting` - seems promising for auto formatting (indentation etc) - - `textDocument/formatting` & `textDocument/rangeFormatting` - - `textDocument/semanticTokens/*` (probably tree-sitter is enough) - - `textDocument/linkedEditingRange` - probably useful - - `textDocument/foldingRange` - i will never use this for folding but it might be useful for other things. - - `textDocument/rename` & `textDocument/prepareRename` - probably useful - - And a lot more (just go through each for `clangd` and then expand to say `solargraph`). - - Make a universal plug for lsp. So focus more on making a general purpose solid communication interface. Instead of something specific. - - With a 4ish pass system. (more like each returned value from the lsp is used in 4 ways) - 1. One for stuff like jump to x position. or rename symbol x to y. (stuff that explicitly requires user request to do something) - - Maybe even hover goes here - 2. One for stuff that only affects highlighting and styles . like symbol highlighting etc. - 3. One for Warnings/errors and inlay hints etc. (stuff that adds virtual text to the editor) - 4. One for fromatting and stuff like that. (stuff that edits the buffer text) +- [ ] Switch to like `RapidJSON` ro something more basic but faster than rn + - also decrease use of `std::string` so much in ui stuff and lsp and warnings etc. +- [ ] Add lsp jumping support for goto definition, hover etc. +- [ ] Add lsp rename support for renaming a symbol. (also see what tree-sitter can do here) +- [ ] Check into more lsp stuff i can add. - [ ] Add codeium/copilot support for auto-completion (uses the VAI virtual text) as a test phase. - [ ] Add a whitespace highlighter (nerd font). for spaces and tabs at start/end of line. not as virtual but instead at render time. - [ ] Once renderer is proven to work well (i.e. redo this commit) merge `experimental` branch into `main`. commit `43f443e` on `experimental`. diff --git a/include/editor/completions.h b/include/editor/completions.h new file mode 100644 index 0000000..26f4dfe --- /dev/null +++ b/include/editor/completions.h @@ -0,0 +1,44 @@ +#ifndef EDITOR_COMPLETIONS_H +#define EDITOR_COMPLETIONS_H + +#include "editor/decl.h" +#include "pch.h" +#include "ui/completionbox.h" +#include "utils/utils.h" + +struct CompletionItem { + std::string label; // Shown in the autocomplete box + uint8_t kind; // Function, variable, class, etc. + std::optional detail; // Shown greyed in autocomplete box + std::optional documentation; // Hover box (can be lazy-loaded) + bool is_markup = false; + bool deprecated = false; // Shown with strikethrough, may push down in list + std::string sort; // Used for sorting + std::string filter; // Used for filtering (default: label) + bool snippet = false; + std::vector edits; + json original; + std::vector end_chars; // Ends completion session if typed +}; + +struct CompletionSession { + std::shared_mutex mtx; + + bool active = false; + Coord hook; // set to start of word + std::optional prefix; // text between hook and cursor + uint8_t select = 0; // index of selected item (defualts to preselcted one + // when data requested) + std::vector items; + std::vector visible; + bool complete = true; // If false, client may request more items on filter + // (but doesnt try filtering on its own) + std::optional trigger_char; // Character that triggered completion sent + // to lsp for isIncomplete resolving + uint8_t trigger = 0; // Type of trigger (1: manual, 2: trigger char, 3: auto) + CompletionBox box; + + CompletionSession() : box(this) {} +}; + +#endif diff --git a/include/editor/decl.h b/include/editor/decl.h index 35ce80b..1b10beb 100644 --- a/include/editor/decl.h +++ b/include/editor/decl.h @@ -3,6 +3,14 @@ #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; +}; + struct Fold { uint32_t start; uint32_t end; diff --git a/include/editor/editor.h b/include/editor/editor.h index 34b1084..cf10e96 100644 --- a/include/editor/editor.h +++ b/include/editor/editor.h @@ -1,10 +1,12 @@ #ifndef EDITOR_H #define EDITOR_H +#include "editor/completions.h" #include "editor/spans.h" #include "io/knot.h" #include "io/sysio.h" #include "ts/decl.h" +#include "ui/completionbox.h" #include "ui/diagnostics.h" #include "ui/hover.h" #include "utils/utils.h" @@ -49,6 +51,7 @@ struct Editor { bool diagnostics_active; DiagnosticBox diagnostics; int lsp_version = 1; + CompletionSession completion; }; Editor *new_editor(const char *filename_arg, Coord position, Coord size); @@ -73,6 +76,8 @@ void ensure_scroll(Editor *editor); void handle_editor_event(Editor *editor, KeyEvent event); void edit_erase(Editor *editor, Coord pos, int64_t len); 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); Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y); char *get_selection(Editor *editor, uint32_t *out_len, Coord *out_start); void editor_worker(Editor *editor); @@ -90,6 +95,13 @@ 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 completion_resolve_doc(Editor *editor); +void complete_accept(Editor *editor); +void complete_next(Editor *editor); +void complete_prev(Editor *editor); +void complete_select(Editor *editor, uint8_t index); +void handle_completion(Editor *editor, KeyEvent event); inline void apply_hook_insertion(Editor *editor, uint32_t line, uint32_t rows) { for (auto &hook : editor->hooks) diff --git a/include/io/sysio.h b/include/io/sysio.h index 9380e37..901db49 100644 --- a/include/io/sysio.h +++ b/include/io/sysio.h @@ -62,19 +62,33 @@ struct ScreenCell { }; struct KeyEvent { + /* KEY_CHAR, KEY_SPECIAL, KEY_MOUSE, KEY_PASTE, KEY_NONE */ uint8_t key_type; + /* the character / string if key_type == KEY_CHAR or KEY_PASTE */ char *c; + /* length of c */ uint32_t len; + /* KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_DELETE if key_type == + * KEY_SPECIAL */ uint8_t special_key; + /* ALT, CNTRL, CNTRL_ALT, SHIFT if key_type == KEY_SPECIAL */ uint8_t special_modifier; + /* column of mouse click */ uint8_t mouse_x; + /* row of mouse click */ uint8_t mouse_y; + /* LEFT_BTN, MIDDLE_BTN, RIGHT_BTN, SCROLL_BTN, NONE_BTN if key_type == + * KEY_MOUSE */ uint8_t mouse_button; + /* PRESS, RELEASE, DRAG, SCROLL if key_type == KEY_MOUSE */ uint8_t mouse_state; + /* SCROLL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT if key_type == + * KEY_MOUSE and mouse_state == SCROLL */ uint8_t mouse_direction; + /* ALT, CNTRL, CNTRL_ALT, SHIFT if key_type == KEY_MOUSE */ uint8_t mouse_modifier; }; @@ -97,7 +111,8 @@ 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 set_cursor(uint32_t row, uint32_t col, uint32_t type, + bool show_cursor_param); void render(); Coord get_size(); diff --git a/include/lsp/lsp.h b/include/lsp/lsp.h index 7f968d5..1934fef 100644 --- a/include/lsp/lsp.h +++ b/include/lsp/lsp.h @@ -34,7 +34,9 @@ struct LSPInstance { bool incremental_sync = false; bool allow_hover = false; bool allow_completion = false; - std::string trigger_chars; + bool allow_resolve = false; + std::vector trigger_chars; + std::vector end_chars; uint32_t last_id = 0; Queue inbox; Queue outbox; @@ -53,12 +55,15 @@ static json client_capabilities = { {"hover", {{"contentFormat", {"markdown", "plaintext"}}}}, {"completion", {{"completionItem", - {{"snippetSupport", true}, + {{"commitCharactersSupport", true}, + {"dynamicRegistration", false}, + {"snippetSupport", true}, {"documentationFormat", {"markdown", "plaintext"}}, - {"resolveSupport", {{"properties", {"documentation", "detail"}}}}, + {"resolveSupport", {{"properties", {"documentation"}}}}, {"insertReplaceSupport", true}, {"labelDetailsSupport", true}, - {"insertTextModeSupport", {{"valueSet", {1}}}}}}, + {"insertTextModeSupport", {{"valueSet", {1}}}}, + {"deprecatedSupport", true}}}, {"completionItemKind", {{"valueSet", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}}}, {"contextSupport", true}, {"insertTextMode", 1}}}}}}; diff --git a/include/pch.h b/include/pch.h index 43b9098..c1b3684 100644 --- a/include/pch.h +++ b/include/pch.h @@ -4,6 +4,9 @@ #define PCRE2_CODE_UNIT_WIDTH 8 #define PCRE_WORKSPACE_SIZE 512 +#include +#include +#include extern "C" { #include "libgrapheme/grapheme.h" #include "unicode_width/unicode_width.h" @@ -25,12 +28,9 @@ extern "C" { #include #include #include -#include #include #include -#include #include -#include #include #include #include diff --git a/include/ts/decl.h b/include/ts/decl.h index 1374afa..a797186 100644 --- a/include/ts/decl.h +++ b/include/ts/decl.h @@ -7,9 +7,9 @@ #define TS_DEF(name) extern "C" const TSLanguage *LANG(name)() struct Language { - std::string name; - const TSLanguage *(*fn)(); - uint8_t lsp_id; + std::string name = "unknown"; + const TSLanguage *(*fn)() = nullptr; + uint8_t lsp_id = 0; uint32_t color = 0xFFFFFF; const char *symbol = " "; }; diff --git a/include/ui/completionbox.h b/include/ui/completionbox.h new file mode 100644 index 0000000..e99d2c2 --- /dev/null +++ b/include/ui/completionbox.h @@ -0,0 +1,20 @@ +#ifndef UI_COMPLETIONBOX_H +#define UI_COMPLETIONBOX_H + +#include "io/sysio.h" +#include "pch.h" +#include "utils/utils.h" + +struct CompletionBox { + struct CompletionSession *session; + bool hidden = true; + std::vector cells; + Coord size; + Coord position; + + CompletionBox(CompletionSession *s) : session(s) {} + void render_update(); + void render(Coord pos); +}; + +#endif diff --git a/include/ui/diagnostics.h b/include/ui/diagnostics.h index c1fe466..fe9fd88 100644 --- a/include/ui/diagnostics.h +++ b/include/ui/diagnostics.h @@ -9,8 +9,7 @@ struct DiagnosticBox { std::vector warnings; std::vector cells; - uint32_t box_width; - uint32_t box_height; + Coord size; void clear(); void render_first(); diff --git a/include/ui/hover.h b/include/ui/hover.h index cfc4db8..0b54cd7 100644 --- a/include/ui/hover.h +++ b/include/ui/hover.h @@ -12,8 +12,7 @@ struct HoverBox { std::atomic is_markup; uint32_t scroll_; std::vector cells; - uint32_t box_width; - uint32_t box_height; + Coord size; std::vector highlights; std::vector hover_spans; diff --git a/include/utils/utils.h b/include/utils/utils.h index 16744da..42572e9 100644 --- a/include/utils/utils.h +++ b/include/utils/utils.h @@ -61,6 +61,8 @@ struct Match { #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define UNUSED(x) (void)(x) +#define USING(x) UNUSED(sizeof(x)) std::string clean_text(const std::string &input); std::string percent_encode(const std::string &s); @@ -74,6 +76,7 @@ uint32_t get_visual_col_from_bytes(const char *line, uint32_t len, uint32_t get_bytes_from_visual_col(const char *line, uint32_t len, uint32_t target_visual_col); int utf8_byte_offset_to_utf16(const char *s, size_t byte_pos); +size_t utf16_offset_to_utf8(const char *s, int utf16_pos); void log(const char *fmt, ...); diff --git a/src/editor/completions.cc b/src/editor/completions.cc new file mode 100644 index 0000000..ecfe6ee --- /dev/null +++ b/src/editor/completions.cc @@ -0,0 +1,346 @@ +#include "editor/editor.h" +#include "io/knot.h" +#include "io/sysio.h" +#include "lsp/lsp.h" +#include "main.h" +#include "utils/utils.h" + +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; + } + 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) { + auto &session = editor->completion; + std::unique_lock lock(session.mtx); + session.active = true; + session.items.clear(); + session.select = 0; + std::vector items_json; + std::vector end_chars_def; + int insert_text_format = 1; + if (message.contains("result")) { + auto &result = message["result"]; + if (result.is_array()) { + items_json = result.get>(); + session.complete = true; + } else if (result.is_object() && result.contains("items")) { + auto &list = result; + items_json = list["items"].get>(); + session.complete = !list.value("isIncomplete", false); + if (list.contains("itemDefaults")) { + auto &defs = list["itemDefaults"]; + if (defs.contains("insertTextFormat")) + insert_text_format = defs["insertTextFormat"].get(); + if (defs.contains("textEdit")) + if (defs["textEdit"].is_array()) + for (auto &c : defs["textEdit"]) { + std::string str = c.get(); + if (str.size() != 1) + continue; + end_chars_def.push_back(str[0]); + } + } + } + } + session.items.reserve(items_json.size()); + for (auto &item_json : items_json) { + CompletionItem item; + item.original = item_json; + item.label = item_json.value("label", ""); + item.kind = item_json.value("kind", 0); + if (item_json.contains("detail")) + item.detail = item_json["detail"].get(); + if (item_json.contains("documentation")) { + if (item_json["documentation"].is_string()) { + item.documentation = item_json["documentation"].get(); + } else if (item_json["documentation"].contains("value")) { + item.is_markup = + item_json["documentation"]["kind"].get() == + "markdown"; + item.documentation = + item_json["documentation"]["value"].get(); + } + } + if (item_json.contains("deprecated")) + item.deprecated = item_json["deprecated"].get(); + auto tags = item_json.value("tags", std::vector()); + for (auto tag : tags) + if (tag == 1) + item.deprecated = true; + item.sort = item_json.value("sortText", item.label); + item.filter = item_json.value("filterText", item.label); + if (item_json.contains("preselect") && item_json["preselect"].get()) + session.select = session.items.size() - 1; + TextEdit edit; + if (item_json.contains("textEdit")) { + auto &te = item_json["textEdit"]; + if (te.contains("newText")) { + edit.text = te.value("newText", ""); + if (te.contains("replace")) { + edit.start.row = te["replace"]["start"]["line"]; + edit.start.col = te["replace"]["start"]["character"]; + edit.end.row = te["replace"]["end"]["line"]; + edit.end.col = te["replace"]["end"]["character"]; + } else if (te.contains("insert")) { + edit.start.row = te["insert"]["start"]["line"]; + edit.start.col = te["insert"]["start"]["character"]; + edit.end.row = te["insert"]["end"]["line"]; + edit.end.col = te["insert"]["end"]["character"]; + } + } 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")) { + 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}; + } + item.edits.push_back(edit); + if (item_json.contains("additionalTextEdits")) { + for (auto &te : item_json["additionalTextEdits"]) { + TextEdit edit; + 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"]; + item.edits.push_back(edit); + } + } + item.snippet = insert_text_format == 2; + if (item_json.contains("insertTextFormat")) + item.snippet = item_json["insertTextFormat"].get() == 2; + if (item_json.contains("commitCharacters")) + for (auto &c : item_json["commitCharacters"]) + 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); + } + session.box.hidden = false; + session.box.render_update(); + free(it->buffer); + free(it); + }; + uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); + json message = { + {"jsonrpc", "2.0"}, + {"method", "textDocument/completion"}, + {"params", + {{"textDocument", {{"uri", editor->uri}}}, + {"position", {{"line", editor->cursor.row}, {"character", col}}}}}}; + if (editor->completion.trigger > 0) { + json context = {{"triggerKind", editor->completion.trigger}}; + if (editor->completion.trigger == 2 && editor->completion.trigger_char) + context["triggerCharacter"] = + std::string(1, *editor->completion.trigger_char); + message["params"]["context"] = context; + } + lsp_send(editor->lsp, message, pending); +} + +inline static std::string completion_prefix(Editor *editor) { + Coord hook = editor->completion.hook; + Coord cur = editor->cursor; + if (hook.row != cur.row || cur.col < hook.col) + return ""; + 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 start = utf16_offset_to_utf8(line, hook.col); + uint32_t end = editor->cursor.col; + std::string prefix(line + start, end - start); + free(it->buffer); + free(it); + return prefix; +} + +void completion_filter(Editor *editor) { + auto &session = editor->completion; + std::string prefix = completion_prefix(editor); + session.visible.clear(); + for (size_t i = 0; i < session.items.size(); ++i) { + const auto &item = session.items[i]; + const std::string &key = item.filter.empty() ? item.label : item.filter; + if (key.size() >= prefix.size() && key.substr(0, prefix.size()) == prefix) + session.visible.push_back(i); + } + if (session.visible.empty()) { + session.box.hidden = true; + return; + } + session.box.hidden = false; + session.box.render_update(); + bool found = false; + for (int i : session.visible) + if (i == session.select) { + found = true; + break; + } + if (!found) + session.select = session.visible[0]; +} + +void handle_completion(Editor *editor, KeyEvent event) { + if (!editor->lsp || !editor->lsp->allow_completion) + return; + if (mode != INSERT) { + editor->completion.active = false; + return; + } + if (event.key_type == KEY_PASTE) { + editor->completion.active = false; + return; + } else if (event.key_type == KEY_CHAR) { + char ch = *event.c; + if (!editor->completion.active) { + for (char c : editor->lsp->trigger_chars) + if (c == ch) { + editor->completion.trigger = 2; + editor->completion.trigger_char = c; + completion_request(editor); + return; + } + } else { + const auto &item = editor->completion.items[editor->completion.select]; + const std::vector &end_chars = + item.end_chars.empty() ? editor->lsp->end_chars : item.end_chars; + for (char c : end_chars) + if (c == ch) { + complete_accept(editor); + return; + } + } + if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || + ch >= '0' && ch <= '9' || ch == '_') { + if (editor->completion.active) { + if (editor->completion.complete) + completion_filter(editor); + else + completion_request(editor); + } else { + editor->completion.trigger = 3; + completion_request(editor); + } + } else if (ch == CTRL('\\')) { + if (editor->completion.active) { + complete_accept(editor); + } else { + editor->completion.trigger = 1; + completion_request(editor); + } + } else if (ch == CTRL(']')) { + if (editor->completion.active) + complete_next(editor); + } else if (ch == CTRL('[')) { + if (editor->completion.active) + complete_prev(editor); + } else if (ch == 0x7F || ch == 0x08) { + if (editor->completion.complete) + completion_filter(editor); + else + completion_request(editor); + } else { + editor->completion.active = false; + } + } else if (event.key_type == KEY_MOUSE && event.mouse_modifier == 0) { + auto &box = editor->completion.box; + Coord normalized = {event.mouse_y - box.position.row, + event.mouse_x - box.position.col}; + if (normalized.row >= 0 && normalized.row < box.size.row && + normalized.col >= 0 && normalized.col < box.size.col) { + uint8_t idx = 0; + /* todo: calculate idx based on scroll and mouse position */ + complete_select(editor, idx); + } + } +} + +void completion_resolve_doc(Editor *editor) { + std::unique_lock lock(editor->completion.mtx); + auto &item = editor->completion.items[editor->completion.select]; + if (item.documentation) + return; + item.documentation = ""; + LSPPending *pending = new LSPPending(); + pending->editor = editor; + pending->method = "completionItem/resolve"; + 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 = ""; + }; + json message = {{"jsonrpc", "2.0"}, + {"method", "completionItem/resolve"}, + {"params", item.original}}; + lsp_send(editor->lsp, message, pending); +} + +void complete_accept(Editor *editor) { + if (!editor->completion.active || editor->completion.box.hidden) + return; + auto &item = editor->completion.items[editor->completion.select]; + apply_lsp_edits(editor, item.edits); + editor->completion.active = false; +} + +inline static int visible_index(const CompletionSession &s) { + for (size_t i = 0; i < s.visible.size(); ++i) + if (s.visible[i] == s.select) + return (int)i; + return -1; +} + +void complete_next(Editor *editor) { + auto &s = editor->completion; + if (!s.active || s.box.hidden || s.visible.empty()) + return; + int vi = visible_index(s); + if (vi < 0) + vi = 0; + else + vi = (vi + 1) % s.visible.size(); + s.select = s.visible[vi]; + completion_resolve_doc(editor); +} + +void complete_prev(Editor *editor) { + auto &s = editor->completion; + if (!s.active || s.box.hidden || s.visible.empty()) + return; + int vi = visible_index(s); + if (vi < 0) + vi = 0; + else + vi = (vi + s.visible.size() - 1) % s.visible.size(); + s.select = s.visible[vi]; + completion_resolve_doc(editor); +} + +void complete_select(Editor *editor, uint8_t index) { + editor->completion.select = index; + complete_accept(editor); +} diff --git a/src/editor/edit.cc b/src/editor/edit.cc index 30a2f95..d4169a2 100644 --- a/src/editor/edit.cc +++ b/src/editor/edit.cc @@ -1,6 +1,8 @@ #include "editor/editor.h" #include "editor/folds.h" #include "lsp/lsp.h" +#include "utils/utils.h" +#include void edit_erase(Editor *editor, Coord pos, int64_t len) { if (len == 0) @@ -278,3 +280,28 @@ 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; + lock.unlock(); + char *buf = + read(editor->root, start_line_byte, end_line_byte - start_line_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); + free(buf); + if (erase_len != 0) + edit_erase(editor, end, -erase_len); + if (len > 0) + edit_insert(editor, start, const_cast(text), len); +} diff --git a/src/editor/events.cc b/src/editor/events.cc index fd248bc..e482b8c 100644 --- a/src/editor/events.cc +++ b/src/editor/events.cc @@ -364,45 +364,6 @@ void handle_editor_event(Editor *editor, KeyEvent event) { case CTRL('s'): save_file(editor); break; - case CTRL(' '): - if (editor->lsp) { - json msg = { - {"jsonrpc", "2.0"}, - {"method", "textDocument/completion"}, - { - "params", - { - { - "textDocument", - { - {"uri", editor->uri}, - }, - }, - { - "position", - { - {"line", editor->cursor.row}, - {"character", editor->cursor.col}, - }, - }, - { - "context", - { - {"triggerKind", 1}, - }, - }, - }, - }, - }; - LSPPending *pending = new LSPPending(); - pending->editor = editor; - pending->method = "textDocument/completion"; - pending->callback = [](Editor *editor, std::string, json completion) { - log("%s\n", completion.dump().c_str()); - }; - lsp_send(editor->lsp, msg, pending); - } - break; case 'p': uint32_t len; char *text = get_from_clipboard(&len); diff --git a/src/editor/lsp.cc b/src/editor/lsp.cc index e44771f..5107f50 100644 --- a/src/editor/lsp.cc +++ b/src/editor/lsp.cc @@ -1,5 +1,14 @@ #include "editor/editor.h" +void apply_lsp_edits(Editor *editor, std::vector edits) { + 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()); +} + void editor_lsp_handle(Editor *editor, json msg) { if (msg.contains("method") && msg["method"] == "textDocument/publishDiagnostics") { diff --git a/src/io/renderer.cc b/src/io/renderer.cc index 1a79671..5752ca9 100644 --- a/src/io/renderer.cc +++ b/src/io/renderer.cc @@ -130,8 +130,8 @@ void render() { first_render = false; } for (uint32_t row = 0; row < rows; ++row) { - int first_change_col = -1; - int last_change_col = -1; + uint32_t first_change_col = -1; + uint32_t last_change_col = -1; for (uint32_t col = 0; col < cols; ++col) { uint32_t idx = row * cols + col; ScreenCell &old_cell = old_screen[idx]; @@ -144,7 +144,7 @@ void render() { 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; + for (uint32_t back = 1; back <= 3 && first_change_col - back >= 0; ++back) { ScreenCell &prev_cell = screen[row * cols + (first_change_col - back)]; @@ -163,15 +163,15 @@ void render() { char buf[64]; snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, first_change_col + 1); out.append(buf); - for (int col = first_change_col; col <= last_change_col; ++col) { - int idx = row * cols + col; + for (uint32_t col = first_change_col; col <= last_change_col; ++col) { + uint32_t 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; + uint32_t 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; + for (uint32_t i = 1; i < width; ++i) { + uint32_t next_col = col + i; if (next_col >= cols) break; const ScreenCell &next = screen[row * cols + next_col]; @@ -241,7 +241,7 @@ void render() { current_underline = underline; } if (width > 1 && overlap) { - for (int i = 1; i < width; ++i) + for (uint32_t i = 1; i < width; ++i) out.push_back(' '); } else { if (!new_cell.utf8.empty()) { @@ -282,10 +282,11 @@ void render() { } } -void set_cursor(int row, int col, int type, bool show_cursor_param) { +void set_cursor(uint32_t row, uint32_t col, uint32_t type, + bool show_cursor_param) { char buf[32]; - int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH\x1b[%d q", row + 1, col + 1, - type); + uint32_t n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH\x1b[%d q", row + 1, + col + 1, type); show_cursor = show_cursor_param; write(STDOUT_FILENO, buf, n); } diff --git a/src/lsp/process.cc b/src/lsp/process.cc index 5d0d73b..67a8c06 100644 --- a/src/lsp/process.cc +++ b/src/lsp/process.cc @@ -76,8 +76,34 @@ std::shared_ptr get_or_init_lsp(uint8_t lsp_id) { lsp->allow_hover = caps["hoverProvider"].get(); else lsp->allow_hover = false; - if (caps.contains("completionProvider")) + if (caps.contains("completionProvider")) { lsp->allow_completion = true; + if (caps["completionProvider"].contains("resolveProvider")) + lsp->allow_resolve = + caps["completionProvider"]["resolveProvider"].get(); + if (caps["completionProvider"].contains("triggerCharacters")) { + auto &chars = caps["completionProvider"]["triggerCharacters"]; + if (chars.is_array()) { + for (auto &c : chars) { + std::string str = c.get(); + if (str.size() != 1) + continue; + lsp->trigger_chars.push_back(str[0]); + } + } + } + if (caps["completionProvider"].contains("allCommitCharacters")) { + auto &chars = caps["completionProvider"]["allCommitCharacters"]; + if (chars.is_array()) { + for (auto &c : chars) { + std::string str = c.get(); + if (str.size() != 1) + continue; + lsp->end_chars.push_back(str[0]); + } + } + } + } } lsp->initialized = true; json initialized = {{"jsonrpc", "2.0"}, diff --git a/src/ui/bar.cc b/src/ui/bar.cc index 2204adb..e8b2539 100644 --- a/src/ui/bar.cc +++ b/src/ui/bar.cc @@ -7,6 +7,7 @@ void Bar::render() { uint32_t row = screen.row - 2; uint32_t col = 0; uint32_t width = screen.col; + UNUSED(width); uint32_t color = 0; uint32_t black = 0x0b0e14; uint32_t grey = 0x33363c; @@ -80,7 +81,7 @@ void Bar::handle(KeyEvent event) { cursor--; break; case KEY_RIGHT: - if (cursor < command.length()) + if (cursor < (uint32_t)command.length()) cursor++; break; case KEY_UP: diff --git a/src/ui/completionbox.cc b/src/ui/completionbox.cc new file mode 100644 index 0000000..8c04666 --- /dev/null +++ b/src/ui/completionbox.cc @@ -0,0 +1,5 @@ +#include "ui/completionbox.h" + +void CompletionBox::render_update() {} + +void CompletionBox::render(Coord pos) {} diff --git a/src/ui/diagnostics.cc b/src/ui/diagnostics.cc index 99d1ccc..6151494 100644 --- a/src/ui/diagnostics.cc +++ b/src/ui/diagnostics.cc @@ -3,9 +3,8 @@ void DiagnosticBox::clear() { warnings.clear(); cells.clear(); - box_width = 0; - box_height = 0; -} + size = {0, 0}; +}; void DiagnosticBox::render_first() { if (warnings.empty()) @@ -18,11 +17,11 @@ void DiagnosticBox::render_first() { 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}); + size.col = content_width + 2; + cells.assign(size.col * 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}; + cells[r * size.col + c] = {std::string(text), 0, fg, bg, flags, 0}; }; uint32_t base_bg = 0; uint32_t border_fg = 0x82AAFF; @@ -116,35 +115,35 @@ void DiagnosticBox::render_first() { }; idx++; } - box_height = 2 + r; + size.row = 2 + r; set(0, 0, "┌", border_fg, base_bg, 0); - for (uint32_t i = 1; i < box_width - 1; i++) + for (uint32_t i = 1; i < size.col - 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(0, size.col - 1, "┐", border_fg, base_bg, 0); + for (uint32_t r = 1; r < size.row - 1; r++) { set(r, 0, "│", border_fg, base_bg, 0); - set(r, box_width - 1, "│", border_fg, base_bg, 0); + set(r, size.col - 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); + set(size.row - 1, 0, "└", border_fg, base_bg, 0); + for (uint32_t i = 1; i < size.col - 1; i++) + set(size.row - 1, i, "─", border_fg, base_bg, 0); + set(size.row - 1, size.col - 1, "┘", border_fg, base_bg, 0); + cells.resize(size.col * size.row); } void DiagnosticBox::render(Coord pos) { - int32_t start_row = (int32_t)pos.row - (int32_t)box_height; + int32_t start_row = (int32_t)pos.row - (int32_t)size.row; 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 + size.col > cols) { + start_col = cols - size.col; 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); + for (uint32_t r = 0; r < size.row; r++) + for (uint32_t c = 0; c < size.col; c++) + 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); } diff --git a/src/ui/hover.cc b/src/ui/hover.cc index 2b97d5c..70214a6 100644 --- a/src/ui/hover.cc +++ b/src/ui/hover.cc @@ -5,8 +5,7 @@ void HoverBox::clear() { text = ""; scroll_ = 0; is_markup = false; - box_width = 0; - box_height = 0; + size = {0, 0}; cells.clear(); highlights.clear(); hover_spans.clear(); @@ -139,7 +138,7 @@ void HoverBox::render_first(bool scroll) { // 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.col = content_width + 2; size_t i = 0; size_t lines_skipped = 0; while (i < text.length() && lines_skipped < scroll_) { @@ -153,10 +152,10 @@ void HoverBox::render_first(bool scroll) { uint32_t base_bg = 0; SpanCursor span_cursor(spans); span_cursor.sync(i); - cells.assign(box_width * 26, ScreenCell{" ", 0, 0, 0, 0, 0}); + cells.assign(size.col * 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}; + cells[r * size.col + c] = {std::string(text), 0, fg, bg, flags, 0}; }; uint32_t r = 0; while (i < text.length() && r < 24) { @@ -186,35 +185,35 @@ void HoverBox::render_first(bool scroll) { r++; } if (!scroll) - box_height = r + 2; + size.row = r + 2; set(0, 0, "┌", border_fg, base_bg, 0); - for (uint32_t i = 1; i < box_width - 1; i++) + for (uint32_t i = 1; i < size.col - 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(0, size.col - 1, "┐", border_fg, base_bg, 0); + for (uint32_t r = 1; r < size.row - 1; r++) { set(r, 0, "│", border_fg, base_bg, 0); - set(r, box_width - 1, "│", border_fg, base_bg, 0); + set(r, size.col - 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); + set(size.row - 1, 0, "└", border_fg, base_bg, 0); + for (uint32_t i = 1; i < size.col - 1; i++) + set(size.row - 1, i, "─", border_fg, base_bg, 0); + set(size.row - 1, size.col - 1, "┘", border_fg, base_bg, 0); + cells.resize(size.col * size.row); } void HoverBox::render(Coord pos) { - int32_t start_row = (int32_t)pos.row - (int32_t)box_height; + int32_t start_row = (int32_t)pos.row - (int32_t)size.row; 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 + size.col > cols) { + start_col = cols - size.col; 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); + for (uint32_t r = 0; r < size.row; r++) + for (uint32_t c = 0; c < size.col; c++) + 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); } diff --git a/src/utils/system.cc b/src/utils/system.cc index a87c15e..6d97080 100644 --- a/src/utils/system.cc +++ b/src/utils/system.cc @@ -120,7 +120,7 @@ Language language_for_file(const char *filename) { if (it != kMimeToLang.end()) return kLanguages.find(it->second)->second; } - return {"unknown", nullptr}; + return Language{}; } char *get_from_clipboard(uint32_t *out_len) { diff --git a/src/utils/unicode.cc b/src/utils/unicode.cc index 82dfdfe..02c6685 100644 --- a/src/utils/unicode.cc +++ b/src/utils/unicode.cc @@ -107,3 +107,25 @@ int utf8_byte_offset_to_utf16(const char *s, size_t byte_pos) { } return utf16_units; } + +size_t utf16_offset_to_utf8(const char *s, int utf16_pos) { + int utf16_units = 0; + size_t i = 0; + while (utf16_units < utf16_pos) { + unsigned char c = s[i]; + if ((c & 0x80) == 0x00) { + i += 1; + utf16_units += 1; + } else if ((c & 0xE0) == 0xC0) { + i += 2; + utf16_units += 1; + } else if ((c & 0xF0) == 0xE0) { + i += 3; + utf16_units += 1; + } else { + i += 4; + utf16_units += 2; + } + } + return i; +}