Basic completion support

This commit is contained in:
2026-01-06 11:39:17 +00:00
parent a905e333fc
commit e9da17eb34
15 changed files with 423 additions and 219 deletions

View File

@@ -42,8 +42,11 @@ void word_boundaries_exclusive(Editor *editor, Coord coord, uint32_t *prev_col,
return;
uint32_t line_len;
char *line = next_line(it, &line_len);
if (!line)
if (!line) {
free(it->buffer);
free(it);
return;
}
if (line_len && line[line_len - 1] == '\n')
line_len--;
uint32_t col = coord.col;

View File

@@ -5,156 +5,6 @@
#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<json> items_json;
std::vector<char> end_chars_def;
int insert_text_format = 1;
if (message.contains("result")) {
auto &result = message["result"];
if (result.is_array()) {
items_json = result.get<std::vector<json>>();
session.complete = true;
} else if (result.is_object() && result.contains("items")) {
auto &list = result;
items_json = list["items"].get<std::vector<json>>();
session.complete = !list.value("isIncomplete", false);
if (list.contains("itemDefaults")) {
auto &defs = list["itemDefaults"];
if (defs.contains("insertTextFormat"))
insert_text_format = defs["insertTextFormat"].get<int>();
if (defs.contains("textEdit"))
if (defs["textEdit"].is_array())
for (auto &c : defs["textEdit"]) {
std::string str = c.get<std::string>();
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<std::string>();
if (item_json.contains("documentation")) {
if (item_json["documentation"].is_string()) {
item.documentation = item_json["documentation"].get<std::string>();
} else if (item_json["documentation"].contains("value")) {
item.is_markup =
item_json["documentation"]["kind"].get<std::string>() ==
"markdown";
item.documentation =
item_json["documentation"]["value"].get<std::string>();
}
}
if (item_json.contains("deprecated"))
item.deprecated = item_json["deprecated"].get<bool>();
auto tags = item_json.value("tags", std::vector<int>());
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<bool>())
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<std::string>();
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<int>() == 2;
if (item_json.contains("commitCharacters"))
for (auto &c : item_json["commitCharacters"])
if (c.is_string() && c.get<std::string>().size() == 1)
item.end_chars.push_back(c.get<std::string>()[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;
@@ -181,24 +31,185 @@ void completion_filter(Editor *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)
const std::string &key = item.filter;
if (key.size() >= prefix.size() &&
key.compare(0, prefix.size(), prefix) == 0)
session.visible.push_back(i);
}
if (session.visible.empty()) {
session.box.hidden = true;
return;
}
if (std::find(session.visible.begin(), session.visible.end(),
session.select) == session.visible.end())
session.select = session.visible[0];
session.box.hidden = false;
session.box.render_update();
bool found = false;
for (int i : session.visible)
if (i == session.select) {
found = true;
break;
}
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) {
auto &session = editor->completion;
std::unique_lock lock(session.mtx);
std::vector<json> items_json;
std::vector<char> end_chars_def;
int insert_text_format = 1;
if (message.contains("result")) {
auto &result = message["result"];
if (result.is_array()) {
items_json = result.get<std::vector<json>>();
session.complete = true;
} else if (result.is_object() && result.contains("items")) {
auto &list = result;
items_json = list["items"].get<std::vector<json>>();
session.complete = !list.value("isIncomplete", false);
if (list.contains("itemDefaults") && list["itemDefaults"].is_object()) {
auto &defs = list["itemDefaults"];
if (defs.contains("insertTextFormat") &&
defs["insertTextFormat"].is_number())
insert_text_format = defs["insertTextFormat"].get<int>();
if (defs.contains("textEdit"))
if (defs["textEdit"].is_array())
for (auto &c : defs["textEdit"]) {
if (!c.is_string())
continue;
std::string str = c.get<std::string>();
if (str.size() != 1)
continue;
end_chars_def.push_back(str[0]);
}
}
}
}
if (!found)
session.select = session.visible[0];
session.items.reserve(items_json.size() + 1);
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_json["detail"].is_string())
item.detail = item_json["detail"].get<std::string>();
if (item_json.contains("documentation")) {
if (item_json["documentation"].is_string()) {
item.documentation = item_json["documentation"].get<std::string>();
} else if (item_json["documentation"].contains("value") &&
item_json["documentation"]["value"].is_string()) {
item.is_markup =
item_json["documentation"]["kind"].get<std::string>() ==
"markdown";
item.documentation =
item_json["documentation"]["value"].get<std::string>();
}
}
if (item_json.contains("deprecated") &&
item_json["deprecated"].is_boolean())
item.deprecated = item_json["deprecated"].get<bool>();
auto tags = item_json.value("tags", std::vector<int>());
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"].is_boolean() &&
item_json["preselect"].get<bool>())
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") &&
item_json["insertText"].is_string()) {
edit.text = item_json["insertText"].get<std::string>();
edit.start = session.hook;
uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col);
edit.end = {editor->cursor.row, col};
} 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};
}
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<int>() == 2;
if (item_json.contains("commitCharacters"))
for (auto &c : item_json["commitCharacters"])
if (c.is_string() && c.get<std::string>().size() == 1)
item.end_chars.push_back(c.get<std::string>()[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();
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);
}
void handle_completion(Editor *editor, KeyEvent event) {
@@ -208,6 +219,7 @@ void handle_completion(Editor *editor, KeyEvent event) {
editor->completion.active = false;
return;
}
std::unique_lock lock(editor->completion.mtx);
if (event.key_type == KEY_PASTE) {
editor->completion.active = false;
return;
@@ -222,6 +234,8 @@ void handle_completion(Editor *editor, KeyEvent event) {
return;
}
} else {
if (editor->completion.items.empty())
return;
const auto &item = editor->completion.items[editor->completion.select];
const std::vector<char> &end_chars =
item.end_chars.empty() ? editor->lsp->end_chars : item.end_chars;
@@ -231,8 +245,8 @@ void handle_completion(Editor *editor, KeyEvent event) {
return;
}
}
if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' ||
ch >= '0' && ch <= '9' || ch == '_') {
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);
@@ -249,35 +263,48 @@ void handle_completion(Editor *editor, KeyEvent event) {
editor->completion.trigger = 1;
completion_request(editor);
}
} else if (ch == CTRL(']')) {
} else if (ch == CTRL('p')) {
if (editor->completion.active)
complete_next(editor);
} else if (ch == CTRL('[')) {
} else if (ch == CTRL('o')) {
if (editor->completion.active)
complete_prev(editor);
} else if (ch == 0x7F || ch == 0x08) {
if (editor->completion.complete)
completion_filter(editor);
else
completion_request(editor);
if (editor->completion.active) {
if (editor->completion.complete) {
if (editor->cursor <= editor->completion.hook)
editor->completion.active = false;
else
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);
}
// Prolly remove mouse support here
// auto &box = editor->completion.box;
// if (event.mouse_y >= box.position.row &&
// event.mouse_x >= box.position.col) {
// uint32_t row = event.mouse_y - box.position.row;
// uint32_t col = event.mouse_x - box.position.col;
// if (row < box.size.row && col < box.size.col) {
// uint8_t idx = 0;
// /* TODO: fix index relative to scroll */
// complete_select(editor, idx);
// }
// }
// if it is being implemented then stop main event handler from processing
// when click inside the box
editor->completion.active = false;
} else {
editor->completion.active = false;
}
}
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;
@@ -292,6 +319,7 @@ void completion_resolve_doc(Editor *editor) {
item.documentation = message["documentation"].get<std::string>();
else
item.documentation = "";
editor->completion.box.render_update();
};
json message = {{"jsonrpc", "2.0"},
{"method", "completionItem/resolve"},
@@ -303,6 +331,7 @@ void complete_accept(Editor *editor) {
if (!editor->completion.active || editor->completion.box.hidden)
return;
auto &item = editor->completion.items[editor->completion.select];
// TODO: support snippets here
apply_lsp_edits(editor, item.edits);
editor->completion.active = false;
}
@@ -325,6 +354,7 @@ void complete_next(Editor *editor) {
vi = (vi + 1) % s.visible.size();
s.select = s.visible[vi];
completion_resolve_doc(editor);
editor->completion.box.render_update();
}
void complete_prev(Editor *editor) {
@@ -338,6 +368,7 @@ void complete_prev(Editor *editor) {
vi = (vi + s.visible.size() - 1) % s.visible.size();
s.select = s.visible[vi];
completion_resolve_doc(editor);
editor->completion.box.render_update();
}
void complete_select(Editor *editor, uint8_t index) {

View File

@@ -2,7 +2,6 @@
#include "editor/folds.h"
#include "lsp/lsp.h"
#include "utils/utils.h"
#include <cstdint>
void edit_erase(Editor *editor, Coord pos, int64_t len) {
if (len == 0)
@@ -301,7 +300,7 @@ void edit_replace(Editor *editor, Coord start, Coord end, const char *text,
(end_line_byte_start - start_line_byte) + end_col);
free(buf);
if (erase_len != 0)
edit_erase(editor, end, -erase_len);
edit_erase(editor, start, erase_len);
if (len > 0)
edit_insert(editor, start, const_cast<char *>(text), len);
}

View File

@@ -9,6 +9,7 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
static uint32_t click_count = 0;
static Coord last_click_pos = {UINT32_MAX, UINT32_MAX};
Coord start = editor->cursor;
uint8_t old_mode = mode;
if (editor->hover_active)
editor->hover_active = false;
if (event.key_type == KEY_MOUSE) {
@@ -612,6 +613,8 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
break;
}
ensure_scroll(editor);
if (old_mode == mode || mode != INSERT)
handle_completion(editor, event);
if ((event.key_type == KEY_CHAR || event.key_type == KEY_PASTE) && event.c)
free(event.c);
}

View File

@@ -7,6 +7,11 @@ void apply_lsp_edits(Editor *editor, std::vector<TextEdit> edits) {
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()));
}
void editor_lsp_handle(Editor *editor, json msg) {

View File

@@ -468,6 +468,12 @@ void render_editor(Editor *editor) {
global_byte_offset += line_len + 1;
line_index++;
}
while (rendered_rows < editor->size.row) {
for (uint32_t col = 0; col < editor->size.col; col++)
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
rendered_rows++;
}
if (cursor.row != UINT32_MAX && cursor.col != UINT32_MAX) {
int type = 0;
switch (mode) {
@@ -483,17 +489,13 @@ void render_editor(Editor *editor) {
break;
}
set_cursor(cursor.row, cursor.col, type, true);
if (editor->hover_active)
if (editor->completion.active && !editor->completion.box.hidden)
editor->completion.box.render(cursor);
else 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++)
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
rendered_rows++;
}
free(it->buffer);
free(it);
}