Lsp completion logic

This commit is contained in:
2026-01-04 03:27:17 +00:00
parent ac04754318
commit a905e333fc
24 changed files with 624 additions and 136 deletions

346
src/editor/completions.cc Normal file
View File

@@ -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<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;
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<char> &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<std::string>();
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);
}

View File

@@ -1,6 +1,8 @@
#include "editor/editor.h"
#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)
@@ -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<char *>(text), len);
}

View File

@@ -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);

View File

@@ -1,5 +1,14 @@
#include "editor/editor.h"
void apply_lsp_edits(Editor *editor, std::vector<TextEdit> 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") {

View File

@@ -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);
}

View File

@@ -76,8 +76,34 @@ std::shared_ptr<LSPInstance> get_or_init_lsp(uint8_t lsp_id) {
lsp->allow_hover = caps["hoverProvider"].get<bool>();
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<bool>();
if (caps["completionProvider"].contains("triggerCharacters")) {
auto &chars = caps["completionProvider"]["triggerCharacters"];
if (chars.is_array()) {
for (auto &c : chars) {
std::string str = c.get<std::string>();
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<std::string>();
if (str.size() != 1)
continue;
lsp->end_chars.push_back(str[0]);
}
}
}
}
}
lsp->initialized = true;
json initialized = {{"jsonrpc", "2.0"},

View File

@@ -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:

5
src/ui/completionbox.cc Normal file
View File

@@ -0,0 +1,5 @@
#include "ui/completionbox.h"
void CompletionBox::render_update() {}
void CompletionBox::render(Coord pos) {}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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;
}