Feat: add hover boxes and diagnostics from lsp

This commit is contained in:
2025-12-29 15:56:51 +00:00
parent 6108f78be3
commit c7068d33d7
31 changed files with 1782 additions and 183 deletions

View File

@@ -1,13 +1,13 @@
#ifndef EDITOR_H
#define EDITOR_H
#include "./hover.h"
#include "./knot.h"
#include "./pch.h"
#include "./spans.h"
#include "./ts_def.h"
#include "./ui.h"
#include "./utils.h"
#include "ts_def.h"
#include <cstdint>
#include <shared_mutex>
#define CHAR 0
#define WORD 1
@@ -16,122 +16,6 @@
#define EXTRA_META 4
#define INDENT_WIDTH 2
struct Highlight {
uint32_t fg;
uint32_t bg;
uint32_t flags;
uint8_t priority;
};
struct Span {
uint32_t start;
uint32_t end;
Highlight *hl;
bool operator<(const Span &other) const { return start < other.start; }
};
struct Spans {
std::vector<Span> spans;
Queue<std::pair<uint32_t, int64_t>> edits;
bool mid_parse = false;
std::shared_mutex mtx;
};
struct Fold {
uint32_t start;
uint32_t end;
bool contains(uint32_t line) const { return line >= start && line <= end; }
bool operator<(const Fold &other) const { return start < other.start; }
};
struct SpanCursor {
Spans &spans;
size_t index = 0;
std::vector<Span *> active;
std::shared_lock<std::shared_mutex> lock;
SpanCursor(Spans &s) : spans(s) {}
Highlight *get_highlight(uint32_t byte_offset) {
for (int i = (int)active.size() - 1; i >= 0; i--)
if (active[i]->end <= byte_offset)
active.erase(active.begin() + i);
while (index < spans.spans.size() &&
spans.spans[index].start <= byte_offset) {
if (spans.spans[index].end > byte_offset)
active.push_back(const_cast<Span *>(&spans.spans[index]));
index++;
}
Highlight *best = nullptr;
int max_prio = -1;
for (auto *s : active)
if (s->hl->priority > max_prio) {
max_prio = s->hl->priority;
best = s->hl;
}
return best;
}
void sync(uint32_t byte_offset) {
lock = std::shared_lock(spans.mtx);
active.clear();
size_t left = 0, right = spans.spans.size();
while (left < right) {
size_t mid = (left + right) / 2;
if (spans.spans[mid].start <= byte_offset)
left = mid + 1;
else
right = mid;
}
index = left;
while (left > 0) {
left--;
if (spans.spans[left].end > byte_offset)
active.push_back(const_cast<Span *>(&spans.spans[left]));
else if (byte_offset - spans.spans[left].end > 1000)
break;
}
}
};
struct VWarn {
uint32_t line;
std::string text;
int8_t type;
uint32_t start;
uint32_t end{UINT32_MAX};
bool operator<(const VWarn &other) const { return line < other.line; }
};
struct VAI {
Coord pos;
char *text;
uint32_t len;
uint32_t lines; // number of \n in text for speed .. the ai part will not
// line wrap but multiline ones need to have its own lines
// after the first one
};
struct TSSetBase {
std::string lang;
TSParser *parser;
std::string query_file;
TSQuery *query;
TSTree *tree;
std::map<uint16_t, Highlight> query_map;
std::map<uint16_t, Language> injection_map;
const TSLanguage *language;
};
struct TSSet : TSSetBase {
std::vector<TSRange> ranges;
};
struct TSSetMain : TSSetBase {
std::unordered_map<std::string, TSSet> injections;
};
struct Editor {
std::string filename;
std::string uri;
@@ -154,9 +38,14 @@ struct Editor {
bool jumper_set;
std::shared_mutex v_mtx;
std::vector<VWarn> warnings;
bool warnings_dirty;
VAI ai;
std::shared_mutex lsp_mtx;
std::shared_ptr<struct LSPInstance> lsp;
bool hover_active;
HoverBox hover;
bool diagnostics_active;
DiagnosticBox diagnostics;
int lsp_version = 1;
};
@@ -219,6 +108,7 @@ void apply_hook_deletion(Editor *editor, uint32_t removal_start,
uint32_t removal_end);
Editor *new_editor(const char *filename_arg, Coord position, Coord size);
void save_file(Editor *editor);
void hover_diagnostic(Editor *editor);
void free_editor(Editor *editor);
void render_editor(Editor *editor);
void fold(Editor *editor, uint32_t start_line, uint32_t end_line);

37
include/hover.h Normal file
View File

@@ -0,0 +1,37 @@
#ifndef HOVER_H
#define HOVER_H
#include "./pch.h"
#include "./spans.h"
#include "./ts_def.h"
#include "./ui.h"
#include "./utils.h"
struct HoverBox {
std::string text;
std::atomic<bool> is_markup;
uint32_t scroll_;
std::vector<ScreenCell> cells;
uint32_t box_width;
uint32_t box_height;
std::vector<Highlight> highlights;
std::vector<Span> hover_spans;
void clear();
void scroll(int32_t number);
void render_first(bool scroll = false);
void render(Coord pos);
};
struct DiagnosticBox {
std::vector<VWarn> warnings;
std::vector<ScreenCell> cells;
uint32_t box_width;
uint32_t box_height;
void clear();
void render_first();
void render(Coord pos);
};
#endif

View File

@@ -32,6 +32,7 @@ struct LSPInstance {
std::atomic<bool> initialized = false;
std::atomic<bool> exited = false;
bool incremental_sync = false;
bool allow_hover = false;
uint32_t last_id = 0;
Queue<json> inbox;
Queue<json> outbox;

View File

@@ -178,6 +178,7 @@ static const std::unordered_map<std::string, Language> kLanguages = {
{"haskell", {"haskell", LANG(haskell), 9}},
{"html", {"html", LANG(html), 10}},
{"javascript", {"javascript", LANG(javascript), 11}},
{"typescript", {"typescript", LANG(tsx), 11}},
{"json", {"json", LANG(json), 6}},
{"jsonc", {"jsonc", LANG(json), 6}},
{"erb", {"erb", LANG(embedded_template), 10}},
@@ -195,6 +196,7 @@ static const std::unordered_map<std::string, Language> kLanguages = {
// config to connect to database
{"make", {"make", LANG(make), 21}},
{"gdscript", {"gdscript", LANG(gdscript)}}, // TODO: connect to godot
{"man", {"man", LANG(man)}},
{"diff", {"diff", LANG(diff)}},
{"gitattributes", {"gitattributes", LANG(gitattributes)}},
{"gitignore", {"gitignore", LANG(gitignore)}},
@@ -222,11 +224,15 @@ static const std::unordered_map<std::string, std::string> kExtToLang = {
{"htm", "html"},
{"js", "javascript"},
{"jsx", "javascript"},
{"ts", "typescript"},
{"tsx", "typescript"},
{"json", "json"},
{"jsonc", "jsonc"},
{"lua", "lua"},
{"make", "make"},
{"mk", "make"},
{"makefile", "make"},
{"man", "man"},
{"py", "python"},
{"rb", "ruby"},
{"rs", "rust"},
@@ -279,6 +285,7 @@ static const std::unordered_map<std::string, std::string> kMimeToLang = {
{"text/x-sql", "sql"},
{"text/x-toml", "toml"},
{"text/x-yaml", "yaml"},
{"text/x-man", "man"},
};
#endif

101
include/spans.h Normal file
View File

@@ -0,0 +1,101 @@
#ifndef SPANS_H
#define SPANS_H
#include "./pch.h"
#include "./utils.h"
struct VWarn {
uint32_t line;
std::string text;
std::string text_full;
std::string source;
std::string code;
std::vector<std::string> see_also;
int8_t type;
uint32_t start;
uint32_t end{UINT32_MAX};
bool operator<(const VWarn &other) const { return line < other.line; }
};
struct VAI {
Coord pos;
char *text;
uint32_t len;
uint32_t lines; // number of \n in text for speed .. the ai part will not
// line wrap but multiline ones need to have its own lines
// after the first one
};
struct Fold {
uint32_t start;
uint32_t end;
bool contains(uint32_t line) const { return line >= start && line <= end; }
bool operator<(const Fold &other) const { return start < other.start; }
};
struct Span {
uint32_t start;
uint32_t end;
Highlight *hl;
bool operator<(const Span &other) const { return start < other.start; }
};
struct Spans {
std::vector<Span> spans;
Queue<std::pair<uint32_t, int64_t>> edits;
bool mid_parse = false;
std::shared_mutex mtx;
};
struct SpanCursor {
Spans &spans;
size_t index = 0;
std::vector<Span *> active;
std::shared_lock<std::shared_mutex> lock;
SpanCursor(Spans &s) : spans(s) {}
Highlight *get_highlight(uint32_t byte_offset) {
for (int i = (int)active.size() - 1; i >= 0; i--)
if (active[i]->end <= byte_offset)
active.erase(active.begin() + i);
while (index < spans.spans.size() &&
spans.spans[index].start <= byte_offset) {
if (spans.spans[index].end > byte_offset)
active.push_back(const_cast<Span *>(&spans.spans[index]));
index++;
}
Highlight *best = nullptr;
int max_prio = -1;
for (auto *s : active)
if (s->hl->priority > max_prio) {
max_prio = s->hl->priority;
best = s->hl;
}
return best;
}
void sync(uint32_t byte_offset) {
lock = std::shared_lock(spans.mtx);
active.clear();
size_t left = 0, right = spans.spans.size();
while (left < right) {
size_t mid = (left + right) / 2;
if (spans.spans[mid].start <= byte_offset)
left = mid + 1;
else
right = mid;
}
index = left;
while (left > 0) {
left--;
if (spans.spans[left].end > byte_offset)
active.push_back(const_cast<Span *>(&spans.spans[left]));
else if (byte_offset - spans.spans[left].end > 1000)
break;
}
}
};
#endif

View File

@@ -11,6 +11,15 @@ extern std::unordered_map<std::string, pcre2_code *> regex_cache;
TSQuery *load_query(const char *query_path, TSSetBase *set);
void ts_collect_spans(Editor *editor);
bool ts_predicate(TSQuery *query, const TSQueryMatch &match,
std::function<std::string(const TSNode *)> subject_fn);
void clear_regex_cache();
template <typename T>
inline T *safe_get(std::map<uint16_t, T> &m, uint16_t key) {
auto it = m.find(key);
if (it == m.end())
return nullptr;
return &it->second;
}
#endif

View File

@@ -12,6 +12,32 @@ struct Language {
uint8_t lsp_id = 0;
};
struct Highlight {
uint32_t fg;
uint32_t bg;
uint32_t flags;
uint8_t priority;
};
struct TSSetBase {
std::string lang;
TSParser *parser;
std::string query_file;
TSQuery *query;
TSTree *tree;
std::map<uint16_t, Highlight> query_map;
std::map<uint16_t, Language> injection_map;
const TSLanguage *language;
};
struct TSSet : TSSetBase {
std::vector<TSRange> ranges;
};
struct TSSetMain : TSSetBase {
std::unordered_map<std::string, TSSet> injections;
};
TS_DEF(ruby);
TS_DEF(bash);
TS_DEF(cpp);
@@ -21,6 +47,8 @@ TS_DEF(go);
TS_DEF(haskell);
TS_DEF(html);
TS_DEF(javascript);
TS_DEF(tsx);
TS_DEF(man);
TS_DEF(json);
TS_DEF(lua);
TS_DEF(regex);

View File

@@ -3,6 +3,7 @@
#include "./pch.h"
#include "./utils.h"
#include <cstdint>
#define KEY_CHAR 0
#define KEY_SPECIAL 1
@@ -48,14 +49,16 @@ enum CellFlags : uint8_t {
CF_NONE = 0,
CF_ITALIC = 1 << 0,
CF_BOLD = 1 << 1,
CF_UNDERLINE = 1 << 2,
CF_UNDERLINE = 1 << 2
};
struct ScreenCell {
std::string utf8 = std::string("");
uint8_t width = 1;
uint32_t fg = 0;
uint32_t bg = 0;
uint8_t flags = CF_NONE;
uint32_t ul_color = 0;
};
struct KeyEvent {
@@ -86,6 +89,10 @@ void update(uint32_t row, uint32_t col, std::string utf8, uint32_t fg,
uint32_t bg, uint8_t flags);
void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg,
uint32_t bg, uint8_t flags);
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 render();
Coord get_size();

View File

@@ -53,7 +53,17 @@ struct Coord {
bool operator>=(const Coord &other) const { return !(*this < other); }
};
struct Match {
size_t start;
size_t end;
std::string text;
};
std::vector<Match> find_all_matches(const std::string &subject,
const std::string &pattern);
std::string clean_text(const std::string &input);
std::string percent_encode(const std::string &s);
std::string percent_decode(const std::string &s);
std::string path_abs(const std::string &path_str);
std::string path_to_file_uri(const std::string &path_str);
int display_width(const char *str, size_t len);
@@ -70,6 +80,7 @@ Language language_for_file(const char *filename);
void copy_to_clipboard(const char *text, size_t len);
char *get_from_clipboard(uint32_t *out_len);
uint32_t count_clusters(const char *line, size_t len, size_t from, size_t to);
std::string trim(const std::string &s);
template <typename Func, typename... Args>
auto throttle(std::chrono::milliseconds min_duration, Func &&func,