Completions bug fixes

This commit is contained in:
2026-01-10 07:56:40 +00:00
parent e9da17eb34
commit b2a64f219f
21 changed files with 400 additions and 193 deletions

View File

@@ -6,10 +6,13 @@ A TUI IDE.
# TODO # TODO
- [ ] Normalize completions edits if local filtering is used
- [ ] Check why fish is behaving soo off with completions filtering - [ ] Check why fish is behaving soo off with completions filtering
- [ ] Also why clangd doesnt show any completions but applys them at 0,0 - [ ] Normalize completions edits if local filtering is used
- [ ] Also why solargraph applys edits at 0,0 - [ ] 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. - [ ] Finish autocomplete box style functions.
- [ ] Add status bar & RUNNER mode - [ ] Add status bar & RUNNER mode
- [ ] Get code context from tree-sitter - [ ] Get code context from tree-sitter

View File

@@ -506,29 +506,29 @@
; Macros & directives ; Macros & directives
; ============================================================ ; ============================================================
;; #F29CC3 #000000 0 0 0 0 2 ;; #F29CC3 #000000 0 0 0 0 3
(preproc_def (preproc_def
name: (_) @constant.macro) name: (_) @constant.macro)
;; #F29CC3 #000000 0 0 0 0 2 ;; #F29CC3 #000000 0 0 0 0 3
(preproc_call (preproc_call
directive: (preproc_directive) @_u directive: (preproc_directive) @_u
argument: (_) @constant.macro argument: (_) @constant.macro
(#match? @_u "^#undef$")) (#match? @_u "^#undef$"))
;; #F29CC3 #000000 0 0 0 0 2 ;; #F29CC3 #000000 0 0 0 0 3
(preproc_ifdef (preproc_ifdef
name: (identifier) @constant.macro) name: (identifier) @constant.macro)
;; #F29CC3 #000000 0 0 0 0 2 ;; #F29CC3 #000000 0 0 0 0 3
(preproc_elifdef (preproc_elifdef
name: (identifier) @constant.macro) name: (identifier) @constant.macro)
;; #F29CC3 #000000 0 0 0 0 2 ;; #F29CC3 #000000 0 0 0 0 3
(preproc_defined (preproc_defined
(identifier) @constant.macro) (identifier) @constant.macro)
;; #F29CC3 #000000 0 0 0 0 2 ;; #F29CC3 #000000 0 0 0 0 3
(preproc_defined) @function.macro (preproc_defined) @function.macro
; ============================================================ ; ============================================================

View File

@@ -34,6 +34,8 @@ struct CompletionSession {
std::optional<char> trigger_char; std::optional<char> trigger_char;
uint8_t trigger = 0; uint8_t trigger = 0;
CompletionBox box; CompletionBox box;
HoverBox hover;
uint32_t doc = UINT32_MAX;
CompletionSession() : box(this) {} CompletionSession() : box(this) {}
}; };

View File

@@ -4,9 +4,7 @@
#include "utils/utils.h" #include "utils/utils.h"
struct TextEdit { struct TextEdit {
// NOTE: start.col is in utf16 index and not clusters or utf8
Coord start; Coord start;
// NOTE: end.col is in utf16 index and not clusters or utf8
Coord end; Coord end;
std::string text; std::string text;
}; };

View File

@@ -18,6 +18,8 @@
#define EXTRA_META 4 #define EXTRA_META 4
#define INDENT_WIDTH 2 #define INDENT_WIDTH 2
// autocomplete lua// Bracket closing / tab on enter
struct Editor { struct Editor {
std::string filename; std::string filename;
std::string uri; 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); uint32_t get_indent(Editor *editor, Coord cursor);
bool closing_after_cursor(const char *line, uint32_t len, uint32_t col); bool closing_after_cursor(const char *line, uint32_t len, uint32_t col);
void editor_lsp_handle(Editor *editor, json msg); void editor_lsp_handle(Editor *editor, json msg);
void apply_lsp_edits(Editor *editor, std::vector<TextEdit> edits); void apply_lsp_edits(Editor *editor, std::vector<TextEdit> edits, bool move);
void completion_resolve_doc(Editor *editor); void completion_resolve_doc(Editor *editor);
void complete_accept(Editor *editor); void complete_accept(Editor *editor);
void complete_next(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; 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 #endif

View File

@@ -35,6 +35,9 @@ struct LSPInstance {
bool allow_hover = false; bool allow_hover = false;
bool allow_completion = false; bool allow_completion = false;
bool allow_resolve = false; bool allow_resolve = false;
bool allow_formatting = false;
bool allow_formatting_on_type = false;
std::vector<char> format_chars;
std::vector<char> trigger_chars; std::vector<char> trigger_chars;
std::vector<char> end_chars; std::vector<char> end_chars;
uint32_t last_id = 0; uint32_t last_id = 0;
@@ -53,6 +56,8 @@ static json client_capabilities = {
{"textDocument", {"textDocument",
{{"publishDiagnostics", {{"relatedInformation", true}}}, {{"publishDiagnostics", {{"relatedInformation", true}}},
{"hover", {{"contentFormat", {"markdown", "plaintext"}}}}, {"hover", {{"contentFormat", {"markdown", "plaintext"}}}},
{"formatting", {{"dynamicRegistration", false}}},
{"onTypeFormatting", {{"dynamicRegistration", false}}},
{"completion", {"completion",
{{"completionItem", {{"completionItem",
{{"commitCharactersSupport", true}, {{"commitCharactersSupport", true},

View File

@@ -11,8 +11,9 @@ extern std::unordered_map<std::string, pcre2_code *> regex_cache;
TSQuery *load_query(const char *query_path, TSSetBase *set); TSQuery *load_query(const char *query_path, TSSetBase *set);
void ts_collect_spans(Editor *editor); void ts_collect_spans(Editor *editor);
bool ts_predicate(TSQuery *query, const TSQueryMatch &match, bool ts_predicate(
std::function<std::string(const TSNode *)> subject_fn); TSQuery *query, const TSQueryMatch &match,
std::function<char *(const TSNode *, uint32_t *len)> subject_fn);
void clear_regex_cache(); void clear_regex_cache();
#endif #endif

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env fish #!/usr/bin/env fish
# Fish highlighting torture test 🐟 # Fish highlighting torture test
# === Variables === # === Variables ===
set normal_var "hello" set normal_var hello
set -l local_var 123 set -l local_var 123
set -gx GLOBAL_VAR "world" set -gx GLOBAL_VAR world
set PATH $PATH /usr/local/bin set PATH $PATH /usr/local/bin
set --erase OLD_VAR set --erase OLD_VAR
@@ -17,15 +17,15 @@ set double "double quoted $normal_var"
set escaped "newline\n tab\t dollar\$" set escaped "newline\n tab\t dollar\$"
# === Conditionals === # === Conditionals ===
if test $normal_var = "hello" if test $normal_var = hello
echo "equal" echo equal
else if test $normal_var != "world" else if test $normal_var != world
echo "not equal" echo "not equal"
end end
# === Logical operators === # === Logical operators ===
true and echo "yes" true and echo yes
false or echo "fallback" false or echo fallback
not false not false
# === Arithmetic === # === Arithmetic ===
@@ -50,14 +50,14 @@ function greet --argument name
echo "Hello $name" echo "Hello $name"
end end
greet "world" greet world
# === Command substitution === # === Command substitution ===
set files (ls | grep ".fish") set files (ls | grep ".fish")
# === Redirections === # === Redirections ===
echo "output" > /tmp/fish_test.txt echo output >/tmp/fish_test.txt
cat < /tmp/fish_test.txt >> /tmp/fish_log.txt cat </tmp/fish_test.txt >>/tmp/fish_log.txt
# === Process substitution === # === Process substitution ===
diff (ls /bin) (ls /usr/bin) diff (ls /bin) (ls /usr/bin)
@@ -65,11 +65,11 @@ diff (ls /bin) (ls /usr/bin)
# === Case statement === # === Case statement ===
switch $argv[1] switch $argv[1]
case start case start
echo "Starting" echo Starting
case stop case stop
echo "Stopping" echo Stopping
case '*' case '*'
echo "Unknown" echo Unknown
end end
# === Subshell === # === Subshell ===
@@ -79,10 +79,10 @@ end
# === Comments & operators === # === Comments & operators ===
# && || | & ! should all highlight # && || | & ! should all highlight
true && echo "ok" || echo "fail" true && echo ok || echo fail
# === Regex === # === Regex ===
string match -r '^[a-z]+$' "hello" string match -r '^[a-z]+$' hello
# === Test builtin === # === Test builtin ===
test -f /etc/passwd test -f /etc/passwd
@@ -90,3 +90,4 @@ test ! -d /does/not/exist
# === Exit === # === Exit ===
exit 0 exit 0

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>PHP Syntax Stress Test</title> <title>PHP Syntax Stress Test</title>
@@ -33,98 +34,103 @@
<body> <body>
<?php <?php
// Basic variables // Basic variables
$number = 42; $number = 42;
$text = "Hello PHP"; $text = "Hello PHP";
$truth = true; $truth = true;
$nothing = null; $nothing = null;
// Constants // Constants
define("APP_NAME", "SyntaxTester"); define("APP_NAME", "SyntaxTester");
// Arrays // Arrays
$list = [1, 2, 3]; $list = [1, 2, 3];
$assoc = [ $assoc = [
"one" => 1, "one" => 1,
"two" => 2 "two" => 2
]; ];
// Function // Function
function add(int $a, int $b): int { function add(int $a, int $b): int
return $a + $b; {
} 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++;
} }
public function greet(): string { // Class + methods
return "Hello {$this->name}"; 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 // Object usage
$user = new User("Alice"); $user = new User("Alice");
echo $user->greet(); echo $user->greet();
// Control flow // Control flow
if ($number > 10) { if ($number > 10) {
echo "Big number"; echo "Big number";
} elseif ($number === 10) { } elseif ($number === 10) {
echo "Exactly ten"; echo "Exactly ten";
} else { } else {
echo "Small number"; echo "Small number";
} }
// Loop // Loop
foreach ($list as $item) { foreach ($list as $item) {
echo $item; echo $item;
} }
// Match expression // Match expression
$result = match ($number) { $result = match ($number) {
1 => "one", 1 => "one",
2 => "two", 2 => "two",
default => "many" default => "many"
}; };
// Try / catch // Try / catch
try { try {
throw new Exception("Test exception"); throw new Exception("Test exception");
} catch (Exception $e) { } catch (Exception $e) {
echo $e->getMessage(); echo $e->getMessage();
} }
// Anonymous function // Anonymous function
$double = fn($x) => $x * 2; $double = fn($x) => $x * 2;
// Nullsafe operator // Nullsafe operator
$len = $user?->name ? strlen($user->name) : 0; $len = $user?->name ? strlen($user->name) : 0;
// Ternary // Ternary
$status = $truth ? "yes" : "no"; $status = $truth ? "yes" : "no";
// Include / require // Include / require
require_once "config.php"; require_once "config.php";
// Output // Output
echo "<div class='box'>"; echo "<div class='box'>";
echo htmlspecialchars($text); echo htmlspecialchars($text);
echo "</div>"; echo "</div>";
?> ?>
<script> <script>
// JS interacting with PHP output // JS interacting with PHP output
const phpValue = <?= json_encode($number) ?>; const phpValue = <?= json_encode($number) ?>;
console.log("Value from PHP:", phpValue); console.log("Value from PHP:", phpValue);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,3 +1,4 @@
from __future__ import annotations
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test file for Python Tree-sitter highlighting.""" """Test file for Python Tree-sitter highlighting."""
@@ -17,7 +18,6 @@ __name__ # builtin constant
import os import os
import sys as system import sys as system
from re import compile as re_compile from re import compile as re_compile
from __future__ import annotations
from math import * from math import *
# ============================== # ==============================
@@ -94,7 +94,7 @@ while x > 0:
try: try:
1 / 0 1 / 0
except ZeroDivisionError as e: except ZeroDivisionError as err:
raise raise
finally: finally:
pass pass
@@ -105,7 +105,7 @@ finally:
a, b = 5, 10 a, b = 5, 10
c = a + b * 2 // 3 % 4 ** 2 c = a + b * 2 // 3 % 4 ** 2
d = (a << 2) & b | c ^ ~a d = (a << 2) & b | c ^ ~a
e = not a or b and c ef = not a or b and c
# ============================== # ==============================
# f-strings / interpolation # f-strings / interpolation
@@ -131,7 +131,7 @@ def static_func():
def cls_func(cls): def cls_func(cls):
return cls return cls
@custom_decorator # @custom_decorator
def decorated_func(): def decorated_func():
return None return None

View File

@@ -1,9 +1,11 @@
#include "editor/decl.h"
#include "editor/editor.h" #include "editor/editor.h"
#include "io/knot.h" #include "io/knot.h"
#include "io/sysio.h" #include "io/sysio.h"
#include "lsp/lsp.h" #include "lsp/lsp.h"
#include "main.h" #include "main.h"
#include "utils/utils.h" #include "utils/utils.h"
#include <regex>
inline static std::string completion_prefix(Editor *editor) { inline static std::string completion_prefix(Editor *editor) {
Coord hook = editor->completion.hook; Coord hook = editor->completion.hook;
@@ -50,23 +52,15 @@ void completion_filter(Editor *editor) {
void completion_request(Editor *editor) { void completion_request(Editor *editor) {
Coord hook = editor->cursor; Coord hook = editor->cursor;
word_boundaries(editor, editor->cursor, &hook.col, nullptr, nullptr, nullptr); 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.active = true;
editor->completion.items.clear(); editor->completion.items.clear();
editor->completion.visible.clear(); editor->completion.visible.clear();
editor->completion.select = 0; editor->completion.select = 0;
hook.col = utf8_byte_offset_to_utf16(line, hook.col);
editor->completion.hook = hook; editor->completion.hook = hook;
LSPPending *pending = new LSPPending(); LSPPending *pending = new LSPPending();
pending->editor = editor; pending->editor = editor;
pending->method = "textDocument/completion"; 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; auto &session = editor->completion;
std::unique_lock lock(session.mtx); std::unique_lock lock(session.mtx);
std::vector<json> items_json; std::vector<json> items_json;
@@ -115,8 +109,15 @@ void completion_request(Editor *editor) {
item.is_markup = item.is_markup =
item_json["documentation"]["kind"].get<std::string>() == item_json["documentation"]["kind"].get<std::string>() ==
"markdown"; "markdown";
item.documentation = std::string documentation =
item_json["documentation"]["value"].get<std::string>(); item_json["documentation"]["value"].get<std::string>();
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") && if (item_json.contains("deprecated") &&
@@ -147,26 +148,27 @@ void completion_request(Editor *editor) {
edit.start.col = te["insert"]["start"]["character"]; edit.start.col = te["insert"]["start"]["character"];
edit.end.row = te["insert"]["end"]["line"]; edit.end.row = te["insert"]["end"]["line"];
edit.end.col = te["insert"]["end"]["character"]; 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") && } else if (item_json.contains("insertText") &&
item_json["insertText"].is_string()) { item_json["insertText"].is_string()) {
edit.text = item_json["insertText"].get<std::string>(); edit.text = item_json["insertText"].get<std::string>();
edit.start = session.hook; edit.start = session.hook;
uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); edit.end = editor->cursor;
edit.end = {editor->cursor.row, col};
} else { } else {
edit.text = item.label; edit.text = item.label;
edit.start = session.hook; edit.start = session.hook;
uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); edit.end = editor->cursor;
edit.end = {editor->cursor.row, col};
} }
utf8_normalize_edit(editor, &edit);
item.edits.push_back(edit); item.edits.push_back(edit);
if (item_json.contains("additionalTextEdits")) { if (item_json.contains("additionalTextEdits")) {
for (auto &te : item_json["additionalTextEdits"]) { for (auto &te : item_json["additionalTextEdits"]) {
@@ -176,6 +178,7 @@ void completion_request(Editor *editor) {
edit.start.col = te["range"]["start"]["character"]; edit.start.col = te["range"]["start"]["character"];
edit.end.row = te["range"]["end"]["line"]; edit.end.row = te["range"]["end"]["line"];
edit.end.col = te["range"]["end"]["character"]; edit.end.col = te["range"]["end"]["character"];
utf8_normalize_edit(editor, &edit);
item.edits.push_back(edit); item.edits.push_back(edit);
} }
} }
@@ -187,15 +190,23 @@ void completion_request(Editor *editor) {
if (c.is_string() && c.get<std::string>().size() == 1) if (c.is_string() && c.get<std::string>().size() == 1)
item.end_chars.push_back(c.get<std::string>()[0]); item.end_chars.push_back(c.get<std::string>()[0]);
session.items.push_back(std::move(item)); session.items.push_back(std::move(item));
session.visible.push_back(session.items.size() - 1);
} }
completion_filter(editor); completion_filter(editor);
session.box.hidden = false; session.box.hidden = false;
session.box.render_update(); 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->buffer);
free(it); free(it);
}; return;
}
uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col); uint32_t col = utf8_byte_offset_to_utf16(line, editor->cursor.col);
free(it->buffer);
free(it);
lock.unlock();
json message = { json message = {
{"jsonrpc", "2.0"}, {"jsonrpc", "2.0"},
{"method", "textDocument/completion"}, {"method", "textDocument/completion"},
@@ -315,10 +326,26 @@ void completion_resolve_doc(Editor *editor) {
pending->callback = [](Editor *editor, std::string, json message) { pending->callback = [](Editor *editor, std::string, json message) {
std::unique_lock lock(editor->completion.mtx); std::unique_lock lock(editor->completion.mtx);
auto &item = editor->completion.items[editor->completion.select]; auto &item = editor->completion.items[editor->completion.select];
if (message.contains("documentation")) if (message["result"].contains("documentation")) {
item.documentation = message["documentation"].get<std::string>(); if (message["result"]["documentation"].is_string()) {
else item.documentation =
item.documentation = ""; message["result"]["documentation"].get<std::string>();
} else if (message["result"]["documentation"].contains("value") &&
message["result"]["documentation"]["value"].is_string()) {
item.is_markup =
message["result"]["documentation"]["kind"].get<std::string>() ==
"markdown";
std::string documentation =
message["result"]["documentation"]["value"].get<std::string>();
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(); editor->completion.box.render_update();
}; };
json message = {{"jsonrpc", "2.0"}, json message = {{"jsonrpc", "2.0"},
@@ -332,7 +359,7 @@ void complete_accept(Editor *editor) {
return; return;
auto &item = editor->completion.items[editor->completion.select]; auto &item = editor->completion.items[editor->completion.select];
// TODO: support snippets here // TODO: support snippets here
apply_lsp_edits(editor, item.edits); apply_lsp_edits(editor, item.edits, true);
editor->completion.active = false; editor->completion.active = false;
} }

View File

@@ -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, void edit_replace(Editor *editor, Coord start, Coord end, const char *text,
uint32_t len) { uint32_t len) {
std::shared_lock lock(editor->knot_mtx); std::shared_lock lock(editor->knot_mtx);
uint32_t start_line_byte = line_to_byte(editor->root, start.row, nullptr); uint32_t start_byte =
uint32_t end_len; line_to_byte(editor->root, start.row, nullptr) + start.col;
uint32_t end_line_byte_start = line_to_byte(editor->root, end.row, &end_len); uint32_t end_byte = line_to_byte(editor->root, end.row, nullptr) + end.col;
uint32_t end_line_byte = end_line_byte_start + len;
lock.unlock(); lock.unlock();
char *buf = char *buf = read(editor->root, start_byte, end_byte - start_byte);
read(editor->root, start_line_byte, end_line_byte - start_line_byte);
if (!buf) if (!buf)
return; 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 = uint32_t erase_len =
count_clusters(buf, end_line_byte - start_line_byte, start_col, count_clusters(buf, end_byte - start_byte, 0, end_byte - start_byte);
(end_line_byte_start - start_line_byte) + end_col);
free(buf); free(buf);
if (erase_len != 0) if (erase_len != 0)
edit_erase(editor, start, erase_len); edit_erase(editor, start, erase_len);

View File

@@ -1,6 +1,8 @@
#include "editor/editor.h" #include "editor/editor.h"
#include "editor/decl.h"
#include "lsp/lsp.h" #include "lsp/lsp.h"
#include "utils/utils.h" #include "utils/utils.h"
#include <shared_mutex>
Editor *new_editor(const char *filename_arg, Coord position, Coord size) { Editor *new_editor(const char *filename_arg, Coord position, Coord size) {
Editor *editor = new Editor(); Editor *editor = new Editor();
@@ -66,15 +68,62 @@ void free_editor(Editor *editor) {
void save_file(Editor *editor) { void save_file(Editor *editor) {
if (!editor || !editor->root) if (!editor || !editor->root)
return; return;
std::shared_lock lock(editor->knot_mtx);
char *str = read(editor->root, 0, editor->root->char_count); char *str = read(editor->root, 0, editor->root->char_count);
if (!str) if (!str)
return; return;
std::ofstream out(editor->filename); std::ofstream out(editor->filename);
out.write(str, editor->root->char_count); out.write(str, editor->root->char_count);
out.close();
free(str); free(str);
json msg = {{"jsonrpc", "2.0"}, lock.unlock();
{"method", "textDocument/didSave"}, if (editor->lsp) {
{"params", {{"textDocument", {{"uri", editor->uri}}}}}}; json save_msg = {{"jsonrpc", "2.0"},
if (editor->lsp) {"method", "textDocument/didSave"},
lsp_send(editor->lsp, msg, nullptr); {"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<TextEdit> 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);
}
}
} }

View File

@@ -612,9 +612,9 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
mode = NORMAL; mode = NORMAL;
break; break;
} }
ensure_scroll(editor);
if (old_mode == mode || mode != INSERT) if (old_mode == mode || mode != INSERT)
handle_completion(editor, event); handle_completion(editor, event);
ensure_scroll(editor);
if ((event.key_type == KEY_CHAR || event.key_type == KEY_PASTE) && event.c) if ((event.key_type == KEY_CHAR || event.key_type == KEY_PASTE) && event.c)
free(event.c); free(event.c);
} }

View File

@@ -1,17 +1,37 @@
#include "editor/decl.h"
#include "editor/editor.h" #include "editor/editor.h"
void apply_lsp_edits(Editor *editor, std::vector<TextEdit> edits) { void apply_lsp_edits(Editor *editor, std::vector<TextEdit> edits, bool move) {
if (!edits.size())
return;
TextEdit first = edits[0];
Coord cursor = editor->cursor;
std::sort( std::sort(
edits.begin(), edits.end(), edits.begin(), edits.end(),
[](const TextEdit &a, const TextEdit &b) { return a.start > b.start; }); [](const TextEdit &a, const TextEdit &b) { return a.start > b.start; });
for (const auto &edit : edits) for (const auto &edit : edits)
edit_replace(editor, edit.start, edit.end, edit.text.c_str(), edit_replace(editor, edit.start, edit.end, edit.text.c_str(),
edit.text.size()); edit.text.size());
editor->cursor = edits[0].start; if (move) {
editor->cursor = move_right_pure(editor, editor->cursor, std::shared_lock lock(editor->knot_mtx);
count_clusters(edits[0].text.c_str(), editor->cursor = first.start;
edits[0].text.size(), 0, editor->cursor =
edits[0].text.size())); 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) { void editor_lsp_handle(Editor *editor, json msg) {

View File

@@ -72,10 +72,35 @@ std::shared_ptr<LSPInstance> get_or_init_lsp(uint8_t lsp_id) {
lsp->incremental_sync = (change_type == 2); lsp->incremental_sync = (change_type == 2);
} }
} }
if (caps.contains("hoverProvider")) lsp->allow_formatting = caps.value("documentFormattingProvider", false);
lsp->allow_hover = caps["hoverProvider"].get<bool>(); if (caps.contains("documentOnTypeFormattingProvider")) {
else auto &fmt = caps["documentOnTypeFormattingProvider"];
if (fmt.is_object()) {
if (fmt.contains("firstTriggerCharacter")) {
std::string s = fmt["firstTriggerCharacter"].get<std::string>();
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<std::string>();
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<bool>();
}
}
if (caps.contains("hoverProvider")) {
auto &hover = caps["hoverProvider"];
lsp->allow_hover =
hover.is_boolean() ? hover.get<bool>() : hover.is_object();
} else {
lsp->allow_hover = false; lsp->allow_hover = false;
}
if (caps.contains("completionProvider")) { if (caps.contains("completionProvider")) {
lsp->allow_completion = true; lsp->allow_completion = true;
if (caps["completionProvider"].contains("resolveProvider")) if (caps["completionProvider"].contains("resolveProvider"))

View File

@@ -24,6 +24,7 @@ void background_lsp() {
} }
void input_listener() { void input_listener() {
while (running) { while (running) {
KeyEvent event = throttle(1ms, read_key); KeyEvent event = throttle(1ms, read_key);
if (event.key_type == KEY_NONE) if (event.key_type == KEY_NONE)
@@ -130,3 +131,4 @@ int main(int argc, char *argv[]) {
clear_regex_cache(); clear_regex_cache();
return 0; return 0;
} }

View File

@@ -90,15 +90,17 @@ void ts_collect_spans(Editor *editor) {
ts_query_cursor_exec(cursor, q, ts_tree_root_node(item.tsset->tree)); ts_query_cursor_exec(cursor, q, ts_tree_root_node(item.tsset->tree));
std::unordered_map<std::string, PendingRanges> pending_injections; std::unordered_map<std::string, PendingRanges> pending_injections;
TSQueryMatch match; 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)) { 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)) if (!ts_predicate(q, match, subject_fn))
continue; continue;
for (uint32_t i = 0; i < match.capture_count; i++) { for (uint32_t i = 0; i < match.capture_count; i++) {

View File

@@ -1,6 +1,7 @@
#include "config.h" #include "config.h"
#include "io/sysio.h" #include "io/sysio.h"
#include "ts/ts.h" #include "ts/ts.h"
#include <cstdint>
std::unordered_map<std::string, pcre2_code *> regex_cache; std::unordered_map<std::string, pcre2_code *> regex_cache;
@@ -114,8 +115,9 @@ TSQuery *load_query(const char *query_path, TSSetBase *set) {
return q; return q;
} }
bool ts_predicate(TSQuery *query, const TSQueryMatch &match, bool ts_predicate(
std::function<std::string(const TSNode *)> subject_fn) { TSQuery *query, const TSQueryMatch &match,
std::function<char *(const TSNode *, uint32_t *len)> subject_fn) {
uint32_t step_count; uint32_t step_count;
const TSQueryPredicateStep *steps = const TSQueryPredicateStep *steps =
ts_query_predicates_for_pattern(query, match.pattern_index, &step_count); 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); const TSNode *node = find_capture_node(match, subject_id);
pcre2_code *re = get_re(regex_txt); 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); 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, int rc = pcre2_match(re, (PCRE2_SPTR)subject, len, 0, 0, md, nullptr);
md, nullptr);
pcre2_match_data_free(md); pcre2_match_data_free(md);
bool ok = (rc >= 0); bool ok = (rc >= 0);
free(subject);
return (command == "match?" ? ok : !ok); return (command == "match?" ? ok : !ok);
} }

View File

@@ -1,8 +1,6 @@
#include "ui/completionbox.h" #include "ui/completionbox.h"
#include "editor/completions.h" #include "editor/completions.h"
#include "utils/utils.h" #include "utils/utils.h"
#include <cstdint>
#include <string>
std::string item_kind_name(uint8_t kind) { std::string item_kind_name(uint8_t kind) {
switch (kind) { switch (kind) {
@@ -76,11 +74,11 @@ void CompletionBox::render_update() {
if (i >= session->items.size()) if (i >= session->items.size())
continue; continue;
auto &item = session->items[i]; 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) 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 = 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.row = session->visible.size() + 2;
size.col = 2 + 2 + max_label_len + 1 + max_detail_len + 2 + max_kind_len + 1; 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}; cells[r * size.col + c] = {std::string(text), 0, fg, bg, flags, 0};
}; };
uint32_t border_fg = 0x82AAFF; uint32_t border_fg = 0x82AAFF;
uint32_t sel_bg = 0xFFFF00; uint32_t sel_bg = 0x174225;
set(0, 0, "", border_fg, 0, 0); set(0, 0, "", border_fg, 0, 0);
for (uint32_t c = 1; c < size.col - 1; c++) for (uint32_t c = 1; c < size.col - 1; c++)
set(0, c, "", border_fg, 0, 0); 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++) { for (uint32_t row_idx = 0; row_idx < session->visible.size(); row_idx++) {
uint32_t r = row_idx + 1; uint32_t r = row_idx + 1;
auto &item = session->items[session->visible[row_idx]]; 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; uint32_t fg = 0xFFFFFF;
set(r, 0, "", border_fg, 0, 0); set(r, 0, "", border_fg, 0, 0);
uint32_t c = 1; uint32_t c = 1;
@@ -134,7 +132,7 @@ void CompletionBox::render_update() {
} }
void CompletionBox::render(Coord pos) { void CompletionBox::render(Coord pos) {
if (hidden) if (hidden || session->visible.empty())
return; return;
std::shared_lock lock(mtx); std::shared_lock lock(mtx);
int32_t start_row = (int32_t)pos.row - (int32_t)size.row; 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, 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].fg, cells[r * size.col + c].bg,
cells[r * size.col + c].flags); 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});
}
}
}
} }

View File

@@ -44,12 +44,13 @@ void HoverBox::render_first(bool scroll) {
TSQueryCursor *cursor = ts_query_cursor_new(); TSQueryCursor *cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, ts.query, ts_tree_root_node(ts.tree)); ts_query_cursor_exec(cursor, ts.query, ts_tree_root_node(ts.tree));
TSQueryMatch match; 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)) { 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)) if (!ts_predicate(ts.query, match, subject_fn))
continue; continue;
for (uint32_t i = 0; i < match.capture_count; i++) { 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)); ts_tree_root_node(inj_ts.tree));
TSQueryMatch inj_match; TSQueryMatch inj_match;
while (ts_query_cursor_next_match(inj_cursor, &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)) if (!ts_predicate(inj_ts.query, inj_match, subject_fn))
continue; continue;
for (uint32_t i = 0; i < inj_match.capture_count; i++) { for (uint32_t i = 0; i < inj_match.capture_count; i++) {