summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/bindings.c25
-rw-r--r--src/main/bindings.h9
-rw-r--r--src/main/cmds.c149
-rw-r--r--src/main/completion.c774
-rw-r--r--src/main/completion.h140
-rw-r--r--src/main/completion/buffer.c148
-rw-r--r--src/main/completion/buffer.h18
-rw-r--r--src/main/completion/command.c151
-rw-r--r--src/main/completion/command.h17
-rw-r--r--src/main/completion/path.c268
-rw-r--r--src/main/completion/path.h14
-rw-r--r--src/main/frame-hooks.c27
-rw-r--r--src/main/frame-hooks.h13
-rw-r--r--src/main/lsp.c917
-rw-r--r--src/main/lsp.h44
-rw-r--r--src/main/lsp/actions.c129
-rw-r--r--src/main/lsp/actions.h10
-rw-r--r--src/main/lsp/choice-buffer.c201
-rw-r--r--src/main/lsp/choice-buffer.h23
-rw-r--r--src/main/lsp/completion.c405
-rw-r--r--src/main/lsp/completion.h18
-rw-r--r--src/main/lsp/diagnostics.c386
-rw-r--r--src/main/lsp/diagnostics.h26
-rw-r--r--src/main/lsp/format.c149
-rw-r--r--src/main/lsp/format.h18
-rw-r--r--src/main/lsp/goto.c297
-rw-r--r--src/main/lsp/goto.h23
-rw-r--r--src/main/lsp/help.c101
-rw-r--r--src/main/lsp/help.h16
-rw-r--r--src/main/lsp/references.c248
-rw-r--r--src/main/lsp/references.h19
-rw-r--r--src/main/lsp/rename.c61
-rw-r--r--src/main/lsp/rename.h16
-rw-r--r--src/main/lsp/types.c1081
-rw-r--r--src/main/lsp/types.h385
-rw-r--r--src/main/main.c79
36 files changed, 5690 insertions, 715 deletions
diff --git a/src/main/bindings.c b/src/main/bindings.c
index 889c32b..db6c924 100644
--- a/src/main/bindings.c
+++ b/src/main/bindings.c
@@ -8,6 +8,11 @@
static struct keymap g_global_keymap, g_ctrlx_map, g_windows_keymap,
g_buffer_default_keymap;
+HOOK_IMPL(buffer_keymaps, buffer_keymaps_cb);
+
+static buffer_keymaps_hook_vec g_buffer_keymaps_hooks;
+uint32_t g_buffer_keymaps_hook_id = 0;
+
struct buffer_keymap {
buffer_keymap_id id;
struct buffer *buffer;
@@ -17,6 +22,8 @@ struct buffer_keymap {
static VEC(struct buffer_keymap) g_buffer_keymaps;
static buffer_keymap_id g_current_keymap_id;
+struct keymap *buffer_default_keymap(void) { return &g_buffer_default_keymap; }
+
void set_default_buffer_bindings(struct keymap *keymap) {
struct binding buffer_bindings[] = {
BINDING(Ctrl, 'B', "backward-char"),
@@ -144,6 +151,8 @@ void init_bindings(void) {
VEC_INIT(&g_buffer_keymaps, 32);
g_current_keymap_id = 0;
+ VEC_INIT(&g_buffer_keymaps_hooks, 16);
+
/* Minibuffer binds.
* This map is actually never removed so forget about the id.
*/
@@ -204,6 +213,14 @@ uint32_t buffer_keymaps(struct buffer *buffer, struct keymap *keymaps[],
}
}
+ // hooks
+ VEC_FOR_EACH(&g_buffer_keymaps_hooks, struct buffer_keymaps_hook * hook) {
+ if (nkeymaps < max_nkeymaps) {
+ nkeymaps += hook->callback(buffer, &keymaps[nkeymaps],
+ max_nkeymaps - nkeymaps, hook->userdata);
+ }
+ }
+
return nkeymaps;
}
@@ -219,3 +236,11 @@ void destroy_bindings(void) {
VEC_DESTROY(&g_buffer_keymaps);
}
+
+uint32_t buffer_add_keymaps_hook(buffer_keymaps_cb callback, void *userdata) {
+ return insert_buffer_keymaps_hook(
+ &g_buffer_keymaps_hooks, &g_buffer_keymaps_hook_id, callback, userdata);
+}
+void buffer_remove_keymaps_hook(uint32_t id, remove_hook_cb callback) {
+ remove_buffer_keymaps_hook(&g_buffer_keymaps_hooks, id, callback);
+}
diff --git a/src/main/bindings.h b/src/main/bindings.h
index 96f20fd..74f43e3 100644
--- a/src/main/bindings.h
+++ b/src/main/bindings.h
@@ -1,15 +1,24 @@
#include <stdint.h>
+#include "dged/hook.h"
+
struct keymap;
struct buffer;
struct binding;
void init_bindings(void);
+struct keymap *buffer_default_keymap(void);
+
typedef uint64_t buffer_keymap_id;
buffer_keymap_id buffer_add_keymap(struct buffer *buffer, struct keymap keymap);
void buffer_remove_keymap(buffer_keymap_id id);
uint32_t buffer_keymaps(struct buffer *buffer, struct keymap *keymaps[],
uint32_t max_nkeymaps);
+typedef uint32_t (*buffer_keymaps_cb)(struct buffer *, struct keymap **,
+ uint32_t, void *);
+uint32_t buffer_add_keymaps_hook(buffer_keymaps_cb callback, void *userdata);
+void buffer_remove_keymaps_hook(uint32_t id, remove_hook_cb callback);
+
void destroy_bindings(void);
diff --git a/src/main/cmds.c b/src/main/cmds.c
index fdd1d87..7d63661 100644
--- a/src/main/cmds.c
+++ b/src/main/cmds.c
@@ -5,6 +5,7 @@
#include <sys/stat.h>
#include "dged/binding.h"
+
#include "dged/buffer.h"
#include "dged/buffer_view.h"
#include "dged/buffers.h"
@@ -18,6 +19,9 @@
#include "bindings.h"
#include "completion.h"
+#include "completion/buffer.h"
+#include "completion/command.h"
+#include "completion/path.h"
#include "search-replace.h"
static void (*g_terminate_cb)(void) = NULL;
@@ -32,7 +36,7 @@ static int32_t _abort(struct command_ctx ctx, int argc, const char *argv[]) {
disable_completion(minibuffer_buffer());
minibuffer_abort_prompt();
buffer_view_clear_mark(window_buffer_view(ctx.active_window));
- minibuffer_echo_timeout(4, "💣 aborted");
+ minibuffer_display_timeout(4, "💣 aborted");
return 0;
}
@@ -63,17 +67,13 @@ static int32_t write_file(struct command_ctx ctx, int argc,
const char *argv[]) {
const char *pth = NULL;
if (argc == 0) {
- struct completion_provider providers[] = {path_provider()};
- enable_completion(minibuffer_buffer(),
- ((struct completion_trigger){
- .kind = CompletionTrigger_Input,
- .data.input =
- (struct completion_trigger_input){
- .nchars = 0, .trigger_initially = false}}),
- providers, 1, write_file_comp_inserted);
+ struct completion_provider providers[] = {
+ create_path_provider(write_file_comp_inserted)};
+ add_completion_providers(minibuffer_buffer(), providers, 1);
return minibuffer_prompt(ctx, "write to file: ");
}
+ disable_completion(minibuffer_buffer());
pth = argv[0];
buffer_set_filename(window_buffer(ctx.active_window), pth);
buffer_to_file(window_buffer(ctx.active_window));
@@ -81,18 +81,16 @@ static int32_t write_file(struct command_ctx ctx, int argc,
return 0;
}
-static void run_interactive_comp_inserted(void) { minibuffer_execute(); }
+static void run_interactive_comp_inserted(struct command *cmd) {
+ (void)cmd;
+ minibuffer_execute();
+}
int32_t run_interactive(struct command_ctx ctx, int argc, const char *argv[]) {
if (argc == 0) {
- struct completion_provider providers[] = {commands_provider()};
- enable_completion(minibuffer_buffer(),
- ((struct completion_trigger){
- .kind = CompletionTrigger_Input,
- .data.input =
- (struct completion_trigger_input){
- .nchars = 0, .trigger_initially = false}}),
- providers, 1, run_interactive_comp_inserted);
+ struct completion_provider providers[] = {
+ create_commands_provider(ctx.commands, run_interactive_comp_inserted)};
+ add_completion_providers(minibuffer_buffer(), providers, 1);
return minibuffer_prompt(ctx, "execute: ");
}
@@ -134,19 +132,18 @@ int32_t do_switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) {
COMMAND_FN("do-switch-buffer", do_switch_buffer, do_switch_buffer, NULL)
-static void switch_buffer_comp_inserted(void) { minibuffer_execute(); }
+static void switch_buffer_comp_inserted(struct buffer *buffer) {
+ // TODO: do useful stuff with buffer here
+ (void)buffer;
+ minibuffer_execute();
+}
int32_t switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) {
if (argc == 0) {
minibuffer_clear();
- struct completion_provider providers[] = {buffer_provider()};
- enable_completion(minibuffer_buffer(),
- ((struct completion_trigger){
- .kind = CompletionTrigger_Input,
- .data.input =
- (struct completion_trigger_input){
- .nchars = 0, .trigger_initially = false}}),
- providers, 1, switch_buffer_comp_inserted);
+ struct completion_provider providers[] = {
+ create_buffer_provider(ctx.buffers, switch_buffer_comp_inserted)};
+ add_completion_providers(minibuffer_buffer(), providers, 1);
ctx.self = &do_switch_buffer_command;
if (window_has_prev_buffer_view(ctx.active_window)) {
@@ -184,19 +181,18 @@ int32_t do_kill_buffer(struct command_ctx ctx, int argc, const char *argv[]) {
COMMAND_FN("do-kill-buffer", do_kill_buffer, do_kill_buffer, NULL)
-static void kill_buffer_comp_inserted(void) { minibuffer_execute(); }
+static void kill_buffer_comp_inserted(struct buffer *buffer) {
+ // TODO: do something with buffer
+ (void)buffer;
+ minibuffer_execute();
+}
int32_t kill_buffer(struct command_ctx ctx, int argc, const char *argv[]) {
if (argc == 0) {
minibuffer_clear();
- struct completion_provider providers[] = {buffer_provider()};
- enable_completion(minibuffer_buffer(),
- ((struct completion_trigger){
- .kind = CompletionTrigger_Input,
- .data.input =
- (struct completion_trigger_input){
- .nchars = 0, .trigger_initially = false}}),
- providers, 1, kill_buffer_comp_inserted);
+ struct completion_provider providers[] = {
+ create_buffer_provider(ctx.buffers, kill_buffer_comp_inserted)};
+ add_completion_providers(minibuffer_buffer(), providers, 1);
ctx.self = &do_kill_buffer_command;
return minibuffer_prompt(ctx, "kill buffer (default %s): ",
@@ -254,8 +250,11 @@ void buffer_to_list_line(struct buffer *buffer, void *userdata) {
struct buffer *listbuf = (struct buffer *)userdata;
const char *path = buffer->filename != NULL ? buffer->filename : "<no-file>";
+ const char *modified =
+ buffer->filename != NULL && buffer->modified ? "*" : "";
char buf[1024];
- size_t written = snprintf(buf, 1024, "%-24s %s", buffer->name, path);
+ size_t written =
+ snprintf(buf, 1024, "%-24s %s%s", buffer->name, path, modified);
if (written > 0) {
struct location begin = buffer_end(listbuf);
@@ -275,9 +274,9 @@ void buffer_to_list_line(struct buffer *buffer, void *userdata) {
size_t pathlen = strlen(path);
uint32_t nchars_path = utf8_nchars((uint8_t *)path, pathlen);
buffer_add_text_property(
- listbuf, (struct location){.line = begin.line, .col = begin.col + 24},
+ listbuf, (struct location){.line = begin.line, .col = begin.col + 25},
(struct location){.line = begin.line,
- .col = begin.col + 24 + nchars_path},
+ .col = begin.col + 25 + nchars_path},
(struct text_property){.type = TextProperty_Colors,
.data.colors = (struct text_property_colors){
.set_bg = false,
@@ -294,8 +293,7 @@ void buffer_to_list_line(struct buffer *buffer, void *userdata) {
}
}
-int32_t buflist_visit_cmd(struct command_ctx ctx, int argc,
- const char *argv[]) {
+int32_t buflist_visit_cmd(struct command_ctx ctx, int argc, const char **argv) {
(void)argc;
(void)argv;
@@ -321,7 +319,6 @@ int32_t buflist_close_cmd(struct command_ctx ctx, int argc,
const char *argv[]) {
return execute_command(&do_switch_buffer_command, ctx.commands,
ctx.active_window, ctx.buffers, argc, argv);
- return 0;
}
void buflist_refresh(struct buffer *buffer, void *userdata) {
@@ -371,6 +368,35 @@ int32_t buflist_kill_cmd(struct command_ctx ctx, int argc, const char *argv[]) {
return 0;
}
+int32_t buflist_save_cmd(struct command_ctx ctx, int argc, const char *argv[]) {
+ (void)argc;
+ (void)argv;
+
+ struct window *w = ctx.active_window;
+
+ struct buffer_view *bv = window_buffer_view(w);
+ struct text_chunk text = buffer_line(bv->buffer, bv->dot.line);
+
+ char *end = (char *)memchr(text.text, ' ', text.nbytes);
+
+ if (end != NULL) {
+ uint32_t len = end - (char *)text.text;
+ char *bufname = (char *)malloc(len + 1);
+ strncpy(bufname, (const char *)text.text, len);
+ bufname[len] = '\0';
+
+ struct buffer *buffer = buffers_find(ctx.buffers, bufname);
+ if (buffer != NULL) {
+ buffer_to_file(buffer);
+ }
+ free(bufname);
+ execute_command(&buflist_refresh_command, ctx.commands, ctx.active_window,
+ ctx.buffers, 0, NULL);
+ }
+
+ return 0;
+}
+
int32_t buffer_list(struct command_ctx ctx, int argc, const char *argv[]) {
(void)argc;
(void)argv;
@@ -401,10 +427,16 @@ int32_t buffer_list(struct command_ctx ctx, int argc, const char *argv[]) {
.fn = buflist_close_cmd,
};
+ static struct command buflist_save = {
+ .name = "buflist-save",
+ .fn = buflist_save_cmd,
+ };
+
struct binding bindings[] = {
ANONYMOUS_BINDING(ENTER, &buflist_visit),
ANONYMOUS_BINDING(None, 'k', &buflist_kill),
ANONYMOUS_BINDING(None, 'q', &buflist_close),
+ ANONYMOUS_BINDING(None, 's', &buflist_save),
ANONYMOUS_BINDING(None, 'g', &buflist_refresh_command),
};
struct keymap km = keymap_create("buflist", 8);
@@ -456,15 +488,16 @@ static int32_t open_file(struct buffers *buffers, struct window *active_window,
int32_t find_file(struct command_ctx ctx, int argc, const char *argv[]) {
if (argc == 0) {
minibuffer_clear();
- struct completion_provider providers[] = {path_provider()};
- enable_completion(minibuffer_buffer(),
- ((struct completion_trigger){
- .kind = CompletionTrigger_Input,
- .data.input =
- (struct completion_trigger_input){
- .nchars = 0, .trigger_initially = true}}),
- providers, 1, find_file_comp_inserted);
- return minibuffer_prompt(ctx, "find file: ");
+ struct completion_provider providers[] = {
+ create_path_provider(find_file_comp_inserted)};
+ add_completion_providers(minibuffer_buffer(), providers, 1);
+
+ int32_t r = minibuffer_prompt(ctx, "find file: ");
+
+ // Trigger directly
+ complete(minibuffer_buffer(), buffer_end(minibuffer_buffer()));
+
+ return r;
}
disable_completion(minibuffer_buffer());
@@ -487,14 +520,9 @@ int32_t find_file_relative(struct command_ctx ctx, int argc,
size_t dirlen = strlen(dir);
if (argc == 0) {
minibuffer_clear();
- struct completion_provider providers[] = {path_provider()};
- enable_completion(minibuffer_buffer(),
- ((struct completion_trigger){
- .kind = CompletionTrigger_Input,
- .data.input =
- (struct completion_trigger_input){
- .nchars = 0, .trigger_initially = true}}),
- providers, 1, find_file_comp_inserted);
+ struct completion_provider providers[] = {
+ create_path_provider(find_file_comp_inserted)};
+ add_completion_providers(minibuffer_buffer(), providers, 1);
ctx.self = &find_file_command;
@@ -505,6 +533,9 @@ int32_t find_file_relative(struct command_ctx ctx, int argc,
minibuffer_prompt_initial(ctx, dir_with_slash, "find file: ");
free(filename);
free(dir_with_slash);
+
+ complete(minibuffer_buffer(), buffer_end(minibuffer_buffer()));
+
return 0;
}
diff --git a/src/main/completion.c b/src/main/completion.c
index 38d75ab..d777408 100644
--- a/src/main/completion.c
+++ b/src/main/completion.c
@@ -12,91 +12,75 @@
#include "dged/buffer.h"
#include "dged/buffer_view.h"
#include "dged/buffers.h"
+#include "dged/display.h"
#include "dged/minibuffer.h"
#include "dged/path.h"
#include "dged/window.h"
#include "bindings.h"
+#include "frame-hooks.h"
-struct active_completion_ctx {
- struct completion_trigger trigger;
- uint32_t trigger_current_nchars;
- struct completion_provider *providers;
- uint32_t nproviders;
- insert_cb on_completion_inserted;
-};
-
-struct completion_state {
- struct completion completions[50];
- uint32_t ncompletions;
- uint32_t current_completion;
- bool active;
- buffer_keymap_id keymap_id;
- bool keymap_active;
- struct active_completion_ctx *ctx;
-} g_state = {0};
-
-static struct buffer *g_target_buffer = NULL;
-
-static void hide_completion(void);
-
-static bool is_space(const struct codepoint *c) {
- // TODO: utf8 whitespace and other whitespace
- return c->codepoint == ' ';
-}
-
-static uint32_t complete_path(struct completion_context ctx, void *userdata);
-static struct completion_provider g_path_provider = {
- .name = "path",
- .complete = complete_path,
- .userdata = NULL,
-};
+struct buffer_completion {
+ struct buffer *buffer;
+ uint32_t insert_hook_id;
+ uint32_t remove_hook_id;
-static uint32_t complete_buffers(struct completion_context ctx, void *userdata);
-static struct completion_provider g_buffer_provider = {
- .name = "buffers",
- .complete = complete_buffers,
- .userdata = NULL,
+ VEC(struct completion_provider) providers;
};
-static uint32_t complete_commands(struct completion_context ctx,
- void *userdata);
-static struct completion_provider g_commands_provider = {
- .name = "commands",
- .complete = complete_commands,
- .userdata = NULL,
+struct completion_item {
+ struct region area;
+ struct completion completion;
};
-struct completion_provider path_provider(void) { return g_path_provider; }
+static struct completion_state {
+ VEC(struct buffer_completion) buffer_completions;
+ VEC(struct completion_item) completions;
+ uint64_t completion_index;
+ struct buffer *completions_buffer;
+ buffer_keymap_id keymap_id;
+ struct buffer *target;
+ layer_id highlight_current_layer;
+ bool insert_in_progress;
+ bool paused;
+} g_state;
-struct completion_provider buffer_provider(void) { return g_buffer_provider; }
+static struct region active_completion_region(struct completion_state *state) {
+ struct region reg =
+ region_new((struct location){0, 0}, (struct location){0, 0});
+ if (state->completion_index < VEC_SIZE(&state->completions)) {
+ reg = VEC_ENTRIES(&state->completions)[state->completion_index].area;
+ }
-struct completion_provider commands_provider(void) {
- return g_commands_provider;
+ return reg;
}
-struct active_completion {
- struct buffer *buffer;
- uint32_t insert_hook_id;
- uint32_t remove_hook_id;
-};
-
-VEC(struct active_completion) g_active_completions;
-
static int32_t goto_next_completion(struct command_ctx ctx, int argc,
const char *argv[]) {
(void)ctx;
(void)argc;
(void)argv;
- if (g_state.current_completion < g_state.ncompletions - 1) {
- ++g_state.current_completion;
+ if (!completion_active()) {
+ return 0;
+ }
+
+ if (VEC_EMPTY(&g_state.completions)) {
+ g_state.completion_index = 0;
+ return 0;
+ }
+
+ size_t ncompletions = VEC_SIZE(&g_state.completions);
+ if (g_state.completion_index >= ncompletions - 1) {
+ g_state.completion_index = ncompletions - 1;
+ return 0;
}
+ ++g_state.completion_index;
+
if (completion_active()) {
- buffer_view_goto(
- window_buffer_view(popup_window()),
- ((struct location){.line = g_state.current_completion, .col = 0}));
+ buffer_view_goto(window_buffer_view(popup_window()),
+ active_completion_region(&g_state).begin);
}
return 0;
@@ -108,14 +92,19 @@ static int32_t goto_prev_completion(struct command_ctx ctx, int argc,
(void)argc;
(void)argv;
- if (g_state.current_completion > 0) {
- --g_state.current_completion;
+ if (!completion_active()) {
+ return 0;
}
+ if (g_state.completion_index == 0) {
+ return 0;
+ }
+
+ --g_state.completion_index;
+
if (completion_active()) {
- buffer_view_goto(
- window_buffer_view(popup_window()),
- ((struct location){.line = g_state.current_completion, .col = 0}));
+ buffer_view_goto(window_buffer_view(popup_window()),
+ active_completion_region(&g_state).begin);
}
return 0;
@@ -127,524 +116,325 @@ static int32_t insert_completion(struct command_ctx ctx, int argc,
(void)argc;
(void)argv;
- // is it in the popup?
- struct completion *comp = &g_state.completions[g_state.current_completion];
- bool done = comp->complete;
- const char *ins = comp->insert;
- size_t inslen = strlen(ins);
- buffer_view_add(window_buffer_view(windows_get_active()), (uint8_t *)ins,
- inslen);
+ if (!completion_active()) {
+ return 0;
+ }
- if (done) {
- g_state.ctx->on_completion_inserted();
- abort_completion();
+ struct buffer_view *bv = window_buffer_view(popup_window());
+ struct window *target_window = windows_get_active();
+ struct buffer_view *target = window_buffer_view(target_window);
+ VEC_FOR_EACH(&g_state.completions, struct completion_item * item) {
+ if (region_is_inside(item->area, bv->dot)) {
+ g_state.insert_in_progress = true;
+ item->completion.selected(item->completion.data, target);
+ g_state.insert_in_progress = false;
+ return 0;
+ }
}
return 0;
}
-static void clear_completions(void) {
- for (uint32_t ci = 0; ci < g_state.ncompletions; ++ci) {
- free((void *)g_state.completions[ci].display);
- free((void *)g_state.completions[ci].insert);
- g_state.completions[ci].display = NULL;
- g_state.completions[ci].insert = NULL;
- g_state.completions[ci].complete = false;
- }
- g_state.ncompletions = 0;
-}
-
COMMAND_FN("next-completion", next_completion, goto_next_completion, NULL)
COMMAND_FN("prev-completion", prev_completion, goto_prev_completion, NULL)
COMMAND_FN("insert-completion", insert_completion, insert_completion, NULL)
-static void update_completions(struct buffer *buffer,
- struct active_completion_ctx *ctx,
- struct location location) {
- clear_completions();
- for (uint32_t pi = 0; pi < ctx->nproviders; ++pi) {
- struct completion_provider *provider = &ctx->providers[pi];
-
- struct completion_context comp_ctx = (struct completion_context){
- .buffer = buffer,
- .location = location,
- .max_ncompletions = 50 - g_state.ncompletions,
- .completions = g_state.completions,
- };
-
- g_state.ncompletions += provider->complete(comp_ctx, provider->userdata);
- }
-
- window_set_buffer_e(popup_window(), g_target_buffer, false, false);
- struct buffer_view *v = window_buffer_view(popup_window());
-
- size_t max_width = 0;
- uint32_t prev_selection = g_state.current_completion;
-
- buffer_clear(v->buffer);
- buffer_view_goto(v, (struct location){.line = 0, .col = 0});
- if (g_state.ncompletions > 0) {
- for (uint32_t compi = 0; compi < g_state.ncompletions; ++compi) {
- const char *disp = g_state.completions[compi].display;
- size_t width = strlen(disp);
- if (width > max_width) {
- max_width = width;
- }
- buffer_view_add(v, (uint8_t *)disp, width);
- buffer_view_add(v, (uint8_t *)"\n", 1);
+static void clear_completions(struct completion_state *state) {
+ buffer_clear(state->completions_buffer);
+ VEC_FOR_EACH(&state->completions, struct completion_item * item) {
+ if (item->completion.cleanup != NULL) {
+ item->completion.cleanup(item->completion.data);
}
-
- // select the closest one to previous selection
- g_state.current_completion = prev_selection < g_state.ncompletions
- ? prev_selection
- : g_state.ncompletions - 1;
-
- buffer_view_goto(
- v, (struct location){.line = g_state.current_completion, .col = 0});
-
- struct window *target_window = window_find_by_buffer(buffer);
- struct window_position winpos = window_position(target_window);
- struct buffer_view *view = window_buffer_view(target_window);
- uint32_t height = g_state.ncompletions > 10 ? 10 : g_state.ncompletions;
- windows_show_popup(winpos.y + location.line - height - 1,
- winpos.x + view->fringe_width + location.col + 1,
- max_width + 2, height);
-
- if (!g_state.keymap_active) {
- struct keymap km = keymap_create("completion", 8);
- struct binding comp_bindings[] = {
- ANONYMOUS_BINDING(Ctrl, 'N', &next_completion_command),
- ANONYMOUS_BINDING(Ctrl, 'P', &prev_completion_command),
- ANONYMOUS_BINDING(ENTER, &insert_completion_command),
- };
- keymap_bind_keys(&km, comp_bindings,
- sizeof(comp_bindings) / sizeof(comp_bindings[0]));
- g_state.keymap_id = buffer_add_keymap(buffer, km);
- g_state.keymap_active = true;
- }
- } else {
- hide_completion();
- }
-}
-
-static void on_buffer_delete(struct buffer *buffer,
- struct edit_location deleted, void *userdata) {
- struct active_completion_ctx *ctx = (struct active_completion_ctx *)userdata;
-
- if (g_state.active) {
- update_completions(buffer, ctx, deleted.coordinates.begin);
}
-}
-static void on_buffer_insert(struct buffer *buffer,
- struct edit_location inserted, void *userdata) {
- struct active_completion_ctx *ctx = (struct active_completion_ctx *)userdata;
-
- if (!g_state.active) {
- uint32_t nchars = 0;
- switch (ctx->trigger.kind) {
- case CompletionTrigger_Input:
- for (uint32_t line = inserted.coordinates.begin.line;
- line <= inserted.coordinates.end.line; ++line) {
- nchars += buffer_line_length(buffer, line);
- }
- nchars -= inserted.coordinates.begin.col +
- (buffer_line_length(buffer, inserted.coordinates.end.line) -
- inserted.coordinates.end.col);
-
- ctx->trigger_current_nchars += nchars;
-
- if (ctx->trigger_current_nchars < ctx->trigger.data.input.nchars) {
- return;
- }
-
- ctx->trigger_current_nchars = 0;
- break;
+ VEC_CLEAR(&state->completions);
+ state->completion_index = 0;
- case CompletionTrigger_Char:
- // TODO
- break;
- }
-
- // activate completion
- g_state.active = true;
- g_state.ctx = ctx;
+ if (completion_active()) {
+ buffer_view_goto(window_buffer_view(popup_window()),
+ (struct location){0, 0});
}
-
- update_completions(buffer, ctx, inserted.coordinates.end);
-}
-
-static void update_completion_buffer(struct buffer *buffer, void *userdata) {
- (void)buffer;
- (void)userdata;
-
- buffer_add_text_property(
- g_target_buffer,
- (struct location){.line = g_state.current_completion, .col = 0},
- (struct location){.line = g_state.current_completion,
- .col = buffer_line_length(g_target_buffer,
- g_state.current_completion)},
- (struct text_property){.type = TextProperty_Colors,
- .data.colors = (struct text_property_colors){
- .set_bg = false,
- .bg = 0,
- .set_fg = true,
- .fg = 4,
- }});
}
-void init_completion(struct buffers *buffers, struct commands *commands) {
- if (g_target_buffer == NULL) {
- g_target_buffer = buffers_add(buffers, buffer_create("*completions*"));
- buffer_add_update_hook(g_target_buffer, update_completion_buffer, NULL);
- }
+static void update_window_position(struct completion_state *state) {
- g_buffer_provider.userdata = buffers;
- g_commands_provider.userdata = commands;
- VEC_INIT(&g_active_completions, 32);
-}
+ size_t ncompletions = VEC_SIZE(&state->completions);
-struct oneshot_completion {
- uint32_t hook_id;
- struct active_completion_ctx *ctx;
-};
+ struct window *target_window = windows_get_active();
+ struct window *root_wind = root_window();
-static void cleanup_oneshot(void *userdata) { free(userdata); }
+ size_t nlines = buffer_num_lines(state->completions_buffer);
+ size_t max_width = 10;
-static void oneshot_completion_hook(struct buffer *buffer, void *userdata) {
- struct oneshot_completion *comp = (struct oneshot_completion *)userdata;
+ window_set_buffer_e(popup_window(), state->completions_buffer, false, false);
+ struct window_position winpos = window_position(target_window);
+ struct buffer_view *view = window_buffer_view(target_window);
+ uint32_t height = ncompletions > 10 ? 10 : ncompletions;
- // activate completion
- g_state.active = true;
- g_state.ctx = comp->ctx;
+ size_t xpos =
+ winpos.x + view->fringe_width + (view->dot.col - view->scroll.col) + 1;
- struct window *w = window_find_by_buffer(buffer);
- if (w != NULL) {
- struct buffer_view *v = window_buffer_view(w);
- update_completions(buffer, comp->ctx, v->dot);
+ // should it be over or under?
+ size_t relative_line = (view->dot.line - view->scroll.line);
+ size_t ypos = winpos.y + relative_line;
+ if (ypos > 10) {
+ ypos -= height + 1;
} else {
- update_completions(buffer, comp->ctx,
- (struct location){.line = 0, .col = 0});
+ ypos += 3;
}
- // this is a oneshot after all
- buffer_remove_update_hook(buffer, comp->hook_id, cleanup_oneshot);
-}
-
-void enable_completion(struct buffer *source, struct completion_trigger trigger,
- struct completion_provider *providers,
- uint32_t nproviders, insert_cb on_completion_inserted) {
- // check if we are already active
- VEC_FOR_EACH(&g_active_completions, struct active_completion * c) {
- if (c->buffer == source) {
- disable_completion(source);
+ for (uint64_t i = 0; i < nlines; ++i) {
+ size_t linelen = buffer_line_length(state->completions_buffer, i);
+ if (linelen > max_width) {
+ max_width = linelen;
}
}
- struct active_completion_ctx *ctx =
- calloc(1, sizeof(struct active_completion_ctx));
- ctx->trigger = trigger;
- ctx->on_completion_inserted = on_completion_inserted;
- ctx->nproviders = nproviders;
- ctx->providers = calloc(nproviders, sizeof(struct completion_provider));
- memcpy(ctx->providers, providers,
- sizeof(struct completion_provider) * nproviders);
-
- uint32_t insert_hook_id =
- buffer_add_insert_hook(source, on_buffer_insert, ctx);
- uint32_t remove_hook_id =
- buffer_add_delete_hook(source, on_buffer_delete, ctx);
-
- VEC_PUSH(&g_active_completions, ((struct active_completion){
- .buffer = source,
- .insert_hook_id = insert_hook_id,
- .remove_hook_id = remove_hook_id,
- }));
-
- // do we want to trigger initially?
- if (ctx->trigger.kind == CompletionTrigger_Input &&
- ctx->trigger.data.input.trigger_initially) {
- struct oneshot_completion *comp =
- calloc(1, sizeof(struct oneshot_completion));
- comp->ctx = ctx;
- comp->hook_id =
- buffer_add_update_hook(source, oneshot_completion_hook, comp);
- }
-}
+ size_t available = window_width(root_wind) - xpos - 5;
+ max_width = max_width >= available ? available : max_width;
-static void hide_completion(void) {
- windows_close_popup();
- if (g_state.active) {
- buffer_remove_keymap(g_state.keymap_id);
- g_state.keymap_active = false;
- }
+ windows_show_popup(ypos, xpos, max_width, height);
}
-void abort_completion(void) {
- hide_completion();
- g_state.active = false;
- clear_completions();
+static void update_window_pos_frame_hook(void *data) {
+ struct completion_state *state = (struct completion_state *)data;
+ update_window_position(state);
}
-bool completion_active(void) {
- return popup_window_visible() &&
- window_buffer(popup_window()) == g_target_buffer && g_state.active;
-}
+static void open_completion(struct completion_state *state) {
-static void cleanup_active_comp_ctx(void *userdata) {
- struct active_completion_ctx *ctx = (struct active_completion_ctx *)userdata;
+ size_t ncompletions = VEC_SIZE(&state->completions);
- if (g_state.ctx == ctx && g_state.active) {
+ if (ncompletions == 0) {
abort_completion();
+ return;
}
- free(ctx->providers);
- free(ctx);
-}
+ struct window *target_window = windows_get_active();
+ struct buffer *buffer = window_buffer(target_window);
+ if (!completion_active() || state->target != buffer) {
-static void do_nothing(void *userdata) { (void)userdata; }
+ // clear any previous keymaps
+ abort_completion();
-static void cleanup_active_completion(struct active_completion *comp) {
- buffer_remove_delete_hook(comp->buffer, comp->remove_hook_id, do_nothing);
- buffer_remove_insert_hook(comp->buffer, comp->insert_hook_id,
- cleanup_active_comp_ctx);
-}
+ struct keymap km = keymap_create("completion", 8);
+ struct binding comp_bindings[] = {
+ ANONYMOUS_BINDING(Ctrl, 'N', &next_completion_command),
+ ANONYMOUS_BINDING(Ctrl, 'P', &prev_completion_command),
+ ANONYMOUS_BINDING(ENTER, &insert_completion_command),
+ };
+ keymap_bind_keys(&km, comp_bindings,
+ sizeof(comp_bindings) / sizeof(comp_bindings[0]));
-void disable_completion(struct buffer *buffer) {
- VEC_FOR_EACH_INDEXED(&g_active_completions, struct active_completion * comp,
- i) {
- if (buffer == comp->buffer) {
- VEC_SWAP(&g_active_completions, i, VEC_SIZE(&g_active_completions) - 1);
- VEC_POP(&g_active_completions, struct active_completion removed);
- cleanup_active_completion(&removed);
- }
+ state->keymap_id = buffer_add_keymap(buffer, km);
}
-}
-void destroy_completion(void) {
- // clean up any active completions we might have
- VEC_FOR_EACH(&g_active_completions, struct active_completion * comp) {
- cleanup_active_completion(comp);
- }
- VEC_DESTROY(&g_active_completions);
+ // need to run next frame to have the correct position
+ run_next_frame(update_window_pos_frame_hook, state);
}
-static bool is_hidden(const char *filename) {
- return filename[0] == '.' && filename[1] != '\0' && filename[1] != '.';
-}
+static void add_completions_impl(struct completion *completions,
+ size_t ncompletions) {
+ for (uint32_t i = 0; i < ncompletions; ++i) {
+ struct completion *c = &completions[i];
+ struct region area = c->render(c->data, g_state.completions_buffer);
+ VEC_APPEND(&g_state.completions, struct completion_item * new);
+ new->area = area;
+ new->completion = *c;
+ }
-static int cmp_completions(const void *comp_a, const void *comp_b) {
- struct completion *a = (struct completion *)comp_a;
- struct completion *b = (struct completion *)comp_b;
- return strcmp(a->display, b->display);
+ open_completion(&g_state);
}
-static uint32_t complete_path(struct completion_context ctx, void *userdata) {
- (void)userdata;
-
- // obtain path from the buffer
- struct text_chunk txt = {0};
- if (ctx.buffer == minibuffer_buffer()) {
- txt = minibuffer_content();
- } else {
- struct match_result start =
- buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space);
- if (!start.found) {
- start.at = (struct location){.line = ctx.location.line, .col = 0};
- return 0;
+static void update_completions(struct completion_state *state,
+ struct buffer *buffer, struct location location,
+ bool deletion) {
+ clear_completions(state);
+ struct buffer_completion *buffer_config = NULL;
+ VEC_FOR_EACH(&state->buffer_completions, struct buffer_completion * bc) {
+ if (buffer == bc->buffer) {
+ buffer_config = bc;
+ break;
}
- txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location));
}
- char *path = calloc(txt.nbytes + 1, sizeof(char));
- memcpy(path, txt.text, txt.nbytes);
- path[txt.nbytes] = '\0';
-
- if (txt.allocated) {
- free(txt.text);
+ if (buffer_config == NULL) {
+ return;
}
- uint32_t n = 0;
- char *p1 = to_abspath(path);
- char *p2 = strdup(p1);
-
- size_t inlen = strlen(path);
+ VEC_FOR_EACH(&buffer_config->providers,
+ struct completion_provider * provider) {
+ struct completion_context comp_ctx = (struct completion_context){
+ .buffer = buffer,
+ .location = location,
+ .add_completions = add_completions_impl,
+ };
- if (ctx.max_ncompletions == 0) {
- goto done;
+ provider->complete(comp_ctx, deletion, provider->userdata);
}
+}
- const char *dir = p1;
- const char *file = "";
+static void update_comp_buffer(struct buffer *buffer, void *userdata) {
+ struct completion_state *state = (struct completion_state *)userdata;
- // check the input path here since
- // to_abspath removes trailing slashes
- if (inlen == 0 || path[inlen - 1] != '/') {
- dir = dirname(p1);
- file = basename(p2);
+ buffer_clear_text_property_layer(buffer, state->highlight_current_layer);
+
+ if (buffer_is_empty(buffer)) {
+ abort_completion();
}
- DIR *d = opendir(dir);
- if (d == NULL) {
- goto done;
+ struct region reg = active_completion_region(state);
+ if (region_has_size(reg)) {
+ buffer_add_text_property_to_layer(buffer, reg.begin, reg.end,
+ (struct text_property){
+ .type = TextProperty_Colors,
+ .data.colors =
+ (struct text_property_colors){
+ .inverted = true,
+ .set_fg = false,
+ .set_bg = false,
+ .underline = false,
+ },
+ },
+ state->highlight_current_layer);
}
+}
- errno = 0;
- size_t filelen = strlen(file);
- bool file_is_curdir = (filelen == 1 && memcmp(file, ".", 1) == 0);
- while (n < ctx.max_ncompletions) {
- struct dirent *de = readdir(d);
- if (de == NULL && errno != 0) {
- // skip the erroring entry
- errno = 0;
- continue;
- } else if (de == NULL && errno == 0) {
- break;
- }
+static void on_buffer_changed(struct buffer *buffer, struct edit_location edit,
+ bool deletion, void *userdata) {
+ struct completion_state *state = (struct completion_state *)userdata;
- switch (de->d_type) {
- case DT_DIR:
- case DT_REG:
- case DT_LNK:
- if (!is_hidden(de->d_name) &&
- (filelen == 0 || file_is_curdir ||
- (filelen <= strlen(de->d_name) &&
- memcmp(file, de->d_name, filelen) == 0))) {
-
- const char *disp = strdup(de->d_name);
- ctx.completions[n] = (struct completion){
- .display = disp,
- .insert = strdup(disp + (file_is_curdir ? 0 : filelen)),
- .complete = de->d_type == DT_REG,
- };
- ++n;
- }
- break;
- }
+ if (state->insert_in_progress || state->paused) {
+ return;
}
- closedir(d);
+ update_completions(state, buffer, edit.coordinates.end, deletion);
+}
-done:
- free(path);
- free(p1);
- free(p2);
+static void on_buffer_insert(struct buffer *buffer, struct edit_location edit,
+ void *userdata) {
+ on_buffer_changed(buffer, edit, false, userdata);
+}
- qsort(ctx.completions, n, sizeof(struct completion), cmp_completions);
- return n;
+static void on_buffer_delete(struct buffer *buffer, struct edit_location edit,
+ void *userdata) {
+ on_buffer_changed(buffer, edit, true, userdata);
}
-struct needle_match_ctx {
- const char *needle;
- struct completion *completions;
- uint32_t max_ncompletions;
- uint32_t ncompletions;
-};
+void init_completion(struct buffers *buffers) {
+ if (g_state.completions_buffer == NULL) {
+ struct buffer b = buffer_create("*completions*");
+ b.lazy_row_add = false;
+ b.force_show_ws_off = true;
+ b.retain_properties = true;
+ g_state.completions_buffer = buffers_add(buffers, b);
+ }
-static void buffer_matches(struct buffer *buffer, void *userdata) {
- struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata;
+ g_state.highlight_current_layer =
+ buffer_add_text_property_layer(g_state.completions_buffer);
+ buffer_add_update_hook(g_state.completions_buffer, update_comp_buffer,
+ &g_state);
- if (strncmp(ctx->needle, buffer->name, strlen(ctx->needle)) == 0 &&
- ctx->ncompletions < ctx->max_ncompletions) {
- ctx->completions[ctx->ncompletions] = (struct completion){
- .display = strdup(buffer->name),
- .insert = strdup(buffer->name + strlen(ctx->needle)),
- .complete = true,
- };
- ++ctx->ncompletions;
- }
+ g_state.keymap_id = (uint64_t)-1;
+ g_state.target = NULL;
+
+ VEC_INIT(&g_state.buffer_completions, 50);
+ VEC_INIT(&g_state.completions, 50);
+ g_state.completion_index = 0;
+ g_state.insert_in_progress = false;
+ g_state.paused = false;
}
-static uint32_t complete_buffers(struct completion_context ctx,
- void *userdata) {
- struct buffers *buffers = (struct buffers *)userdata;
- if (buffers == NULL) {
- return 0;
- }
+void add_completion_providers(struct buffer *source,
+ struct completion_provider *providers,
+ uint32_t nproviders) {
- struct text_chunk txt = {0};
- if (ctx.buffer == minibuffer_buffer()) {
- txt = minibuffer_content();
- } else {
- struct match_result start =
- buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space);
- if (!start.found) {
- start.at = (struct location){.line = ctx.location.line, .col = 0};
- return 0;
+ struct buffer_completion *comp = NULL;
+ VEC_FOR_EACH(&g_state.buffer_completions, struct buffer_completion * c) {
+ if (c->buffer == source) {
+ comp = c;
+ break;
}
- txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location));
}
- char *needle = calloc(txt.nbytes + 1, sizeof(char));
- memcpy(needle, txt.text, txt.nbytes);
- needle[txt.nbytes] = '\0';
+ if (comp == NULL) {
+ VEC_APPEND(&g_state.buffer_completions,
+ struct buffer_completion * new_comp);
+
+ uint32_t insert_hook_id =
+ buffer_add_insert_hook(source, on_buffer_insert, &g_state);
+ uint32_t remove_hook_id =
+ buffer_add_delete_hook(source, on_buffer_delete, &g_state);
- if (txt.allocated) {
- free(txt.text);
+ new_comp->buffer = source;
+ new_comp->insert_hook_id = insert_hook_id;
+ new_comp->remove_hook_id = remove_hook_id;
+ VEC_INIT(&new_comp->providers, nproviders);
+ comp = new_comp;
}
- struct needle_match_ctx match_ctx = (struct needle_match_ctx){
- .needle = needle,
- .max_ncompletions = ctx.max_ncompletions,
- .completions = ctx.completions,
- .ncompletions = 0,
- };
- buffers_for_each(buffers, buffer_matches, &match_ctx);
+ for (uint32_t i = 0; i < nproviders; ++i) {
+ VEC_PUSH(&comp->providers, providers[i]);
+ }
+}
- free(needle);
- return match_ctx.ncompletions;
+void complete(struct buffer *buffer, struct location at) {
+ update_completions(&g_state, buffer, at, false);
}
-static void command_matches(struct command *command, void *userdata) {
- struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata;
+void abort_completion(void) {
+ windows_close_popup();
- if (strncmp(ctx->needle, command->name, strlen(ctx->needle)) == 0 &&
- ctx->ncompletions < ctx->max_ncompletions) {
- ctx->completions[ctx->ncompletions] = (struct completion){
- .display = strdup(command->name),
- .insert = strdup(command->name + strlen(ctx->needle)),
- .complete = true,
- };
- ++ctx->ncompletions;
+ if (g_state.keymap_id != (uint64_t)-1) {
+ buffer_remove_keymap(g_state.keymap_id);
}
+
+ g_state.keymap_id = (uint64_t)-1;
+ g_state.target = NULL;
}
-static uint32_t complete_commands(struct completion_context ctx,
- void *userdata) {
+bool completion_active(void) {
+ return popup_window_visible() &&
+ window_buffer(popup_window()) == g_state.completions_buffer;
+}
- struct commands *commands = (struct commands *)userdata;
- if (commands == NULL) {
- return 0;
- }
- struct text_chunk txt = {0};
- if (ctx.buffer == minibuffer_buffer()) {
- txt = minibuffer_content();
- } else {
- struct match_result start =
- buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space);
- if (!start.found) {
- start.at = (struct location){.line = ctx.location.line, .col = 0};
- return 0;
+static void do_nothing(void *userdata) { (void)userdata; }
+
+static void cleanup_buffer_completion(struct buffer_completion *comp) {
+ buffer_remove_delete_hook(comp->buffer, comp->remove_hook_id, do_nothing);
+ buffer_remove_insert_hook(comp->buffer, comp->insert_hook_id, do_nothing);
+
+ VEC_FOR_EACH(&comp->providers, struct completion_provider * provider) {
+ if (provider->cleanup != NULL) {
+ provider->cleanup(provider->userdata);
}
- txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location));
}
- char *needle = calloc(txt.nbytes + 1, sizeof(char));
- memcpy(needle, txt.text, txt.nbytes);
- needle[txt.nbytes] = '\0';
+ VEC_DESTROY(&comp->providers);
+}
- if (txt.allocated) {
- free(txt.text);
+void disable_completion(struct buffer *buffer) {
+ VEC_FOR_EACH_INDEXED(&g_state.buffer_completions,
+ struct buffer_completion * comp, i) {
+ if (buffer == comp->buffer) {
+ VEC_SWAP(&g_state.buffer_completions, i,
+ VEC_SIZE(&g_state.buffer_completions) - 1);
+ VEC_POP(&g_state.buffer_completions, struct buffer_completion removed);
+ cleanup_buffer_completion(&removed);
+ }
}
+}
- struct needle_match_ctx match_ctx = (struct needle_match_ctx){
- .needle = needle,
- .max_ncompletions = ctx.max_ncompletions,
- .completions = ctx.completions,
- .ncompletions = 0,
- };
- commands_for_each(commands, command_matches, &match_ctx);
-
- free(needle);
- return match_ctx.ncompletions;
+void destroy_completion(void) {
+ clear_completions(&g_state);
+ // clean up any active completions we might have
+ VEC_FOR_EACH(&g_state.buffer_completions, struct buffer_completion * comp) {
+ cleanup_buffer_completion(comp);
+ }
+ VEC_DESTROY(&g_state.buffer_completions);
+ VEC_DESTROY(&g_state.completions);
}
+
+void pause_completion() { g_state.paused = true; }
+
+void resume_completion() { g_state.paused = false; }
diff --git a/src/main/completion.h b/src/main/completion.h
index f2ce186..25f1ea2 100644
--- a/src/main/completion.h
+++ b/src/main/completion.h
@@ -1,6 +1,8 @@
#ifndef _COMPLETION_H
#define _COMPLETION_H
+#include <stddef.h>
+
#include "dged/location.h"
/** @file completion.h
@@ -9,29 +11,22 @@
struct buffer;
struct buffers;
+struct buffer_view;
struct commands;
-/**
- * A single completion.
- */
+typedef struct region (*completion_render_fn)(void *, struct buffer *);
+typedef void (*completion_selected_fn)(void *, struct buffer_view *);
+typedef void (*completion_cleanup_fn)(void *);
+
struct completion {
- /** The display text for the completion. */
- const char *display;
-
- /** The text to insert for this completion. */
- const char *insert;
-
- /**
- * True if this completion item represent a fully expanded value.
- *
- * One example might be when the file completion represents a
- * file (and not a directory) which means that there is not
- * going to be more to complete after picking this completion
- * item.
- */
- bool complete;
+ void *data;
+ completion_render_fn render;
+ completion_selected_fn selected;
+ completion_cleanup_fn cleanup;
};
+typedef void (*add_completions)(struct completion *, size_t);
+
/**
* Context for calculating completions.
*/
@@ -40,20 +35,19 @@ struct completion_context {
struct buffer *buffer;
/** The current location in the buffer. */
- const struct location location;
+ struct location location;
- /** The capacity of @ref completion_context.completions. */
- const uint32_t max_ncompletions;
-
- /** The resulting completions */
- struct completion *completions;
+ /** Callback for adding items to the completion list */
+ add_completions add_completions;
};
/**
* A function that provides completions.
*/
-typedef uint32_t (*completion_fn)(struct completion_context ctx,
- void *userdata);
+typedef void (*completion_fn)(struct completion_context ctx, bool deletion,
+ void *userdata);
+
+typedef void (*provider_cleanup_fn)(void *);
/**
* A completion provider.
@@ -62,54 +56,21 @@ struct completion_provider {
/** Name of the completion provider */
char name[16];
- /** Completion function. Called to get new completions. */
+ /** Completion function. Called to trigger retreival of new completions. */
completion_fn complete;
+ /** Cleanup function called when provider is destroyed. */
+ provider_cleanup_fn cleanup;
+
/** Userdata sent to @ref completion_provider.complete */
void *userdata;
};
/**
- * Type of event that triggers a completion.
- */
-enum completion_trigger_kind {
- /** Completion is triggered on any input. */
- CompletionTrigger_Input = 0,
-
- /** Completion is triggered on a specific char. */
- CompletionTrigger_Char = 1,
-};
-
-/**
- * Description for @c CompletionTrigger_Input.
- */
-struct completion_trigger_input {
- /** Trigger completion after this many chars */
- uint32_t nchars;
-
- /** Trigger an initial complete? */
- bool trigger_initially;
-};
-
-/**
- * Completion trigger descriptor.
- */
-struct completion_trigger {
- /** Type of trigger. */
- enum completion_trigger_kind kind;
- union completion_trigger_data {
- uint32_t c;
- struct completion_trigger_input input;
- } data;
-};
-
-/**
* Initialize the completion system.
*
- * @param buffers The buffer list to complete from.
- * @param commands The command list to complete from.
*/
-void init_completion(struct buffers *buffers, struct commands *commands);
+void init_completion(struct buffers *buffers);
/**
* Tear down the completion system.
@@ -117,48 +78,23 @@ void init_completion(struct buffers *buffers, struct commands *commands);
void destroy_completion(void);
/**
- * Callback for completion inserted.
- */
-typedef void (*insert_cb)(void);
-
-/**
* Enable completions in the buffer @p source.
*
* @param source [in] The buffer to provide completions for.
- * @param trigger [in] The completion trigger to use for this completion.
* @param providers [in] The completion providers to use.
* @param nproviders [in] The number of providers in @p providers.
- * @param on_completion_inserted [in] Callback to be called when a completion
- * has been inserted.
- */
-void enable_completion(struct buffer *source, struct completion_trigger trigger,
- struct completion_provider *providers,
- uint32_t nproviders, insert_cb on_completion_inserted);
-
-/**
- * Create a new path completion provider.
- *
- * This provider completes filesystem paths.
- * @returns A filesystem path @ref completion_provider.
*/
-struct completion_provider path_provider(void);
+void add_completion_providers(struct buffer *source,
+ struct completion_provider *providers,
+ uint32_t nproviders);
/**
- * Create a new buffer completion provider.
+ * Trigger a completion at @ref at in @ref buffer.
*
- * This provider completes buffer names from the
- * buffer list.
- * @returns A buffer name @ref completion_provider.
+ * @param buffer [in] Buffer to complete in.
+ * @param at [in] The location in @ref buffer to provide completions at.
*/
-struct completion_provider buffer_provider(void);
-
-/**
- * Create a new command completion provider.
- *
- * This provider completes registered command names.
- * @returns A command name @ref completion_provider.
- */
-struct completion_provider commands_provider(void);
+void complete(struct buffer *buffer, struct location at);
/**
* Abort any active completion.
@@ -173,10 +109,20 @@ void abort_completion(void);
bool completion_active(void);
/**
- * Disable completion for @ref buffer.
+ * Get a pointer to the buffer used to hold completion items.
+ *
+ * @returns A pointer to the buffer holding completions.
+ */
+struct buffer *completion_buffer(void);
+
+/**
+ * Disable completion for @ref buffer, removing all providers.
*
* @param buffer [in] Buffer to disable completions for.
*/
void disable_completion(struct buffer *buffer);
+void pause_completion();
+void resume_completion();
+
#endif
diff --git a/src/main/completion/buffer.c b/src/main/completion/buffer.c
new file mode 100644
index 0000000..8074414
--- /dev/null
+++ b/src/main/completion/buffer.c
@@ -0,0 +1,148 @@
+#include "buffer.h"
+
+#include <string.h>
+
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/buffers.h"
+#include "dged/minibuffer.h"
+
+#include "main/completion.h"
+
+static bool is_space(const struct codepoint *c) {
+ // TODO: utf8 whitespace and other whitespace
+ return c->codepoint == ' ';
+}
+
+typedef void (*on_buffer_selected_cb)(struct buffer *);
+
+struct buffer_completion {
+ struct buffer *buffer;
+ on_buffer_selected_cb on_buffer_selected;
+};
+
+struct buffer_provider_data {
+ struct buffers *buffers;
+ on_buffer_selected_cb on_buffer_selected;
+};
+
+static void buffer_comp_selected(void *data, struct buffer_view *target) {
+ struct buffer_completion *bc = (struct buffer_completion *)data;
+ buffer_set_text(target->buffer, (uint8_t *)bc->buffer->name,
+ strlen(bc->buffer->name));
+
+ abort_completion();
+ bc->on_buffer_selected(bc->buffer);
+}
+
+static struct region buffer_comp_render(void *data,
+ struct buffer *comp_buffer) {
+ struct buffer *buffer = ((struct buffer_completion *)data)->buffer;
+ struct location begin = buffer_end(comp_buffer);
+ buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)buffer->name,
+ strlen(buffer->name));
+
+ struct location end = buffer_end(comp_buffer);
+ buffer_newline(comp_buffer, buffer_end(comp_buffer));
+ return region_new(begin, end);
+}
+
+static void buffer_comp_cleanup(void *data) {
+ struct buffer_completion *bc = (struct buffer_completion *)data;
+ free(bc);
+}
+
+struct needle_match_ctx {
+ const char *needle;
+ struct completion *completions;
+ uint32_t max_ncompletions;
+ uint32_t ncompletions;
+ on_buffer_selected_cb on_buffer_selected;
+};
+
+static void buffer_matches(struct buffer *buffer, void *userdata) {
+ struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata;
+
+ if (strncmp(ctx->needle, buffer->name, strlen(ctx->needle)) == 0 &&
+ ctx->ncompletions < ctx->max_ncompletions) {
+
+ struct buffer_completion *comp_data =
+ calloc(1, sizeof(struct buffer_completion));
+ comp_data->buffer = buffer;
+ comp_data->on_buffer_selected = ctx->on_buffer_selected;
+ ctx->completions[ctx->ncompletions] = (struct completion){
+ .render = buffer_comp_render,
+ .selected = buffer_comp_selected,
+ .cleanup = buffer_comp_cleanup,
+ .data = comp_data,
+ };
+ ++ctx->ncompletions;
+ }
+}
+
+static void buffer_complete(struct completion_context ctx, bool deletion,
+ void *userdata) {
+ (void)deletion;
+ struct buffer_provider_data *pd = (struct buffer_provider_data *)userdata;
+ struct buffers *buffers = pd->buffers;
+ if (buffers == NULL) {
+ return;
+ }
+
+ struct text_chunk txt = {0};
+ if (ctx.buffer == minibuffer_buffer()) {
+ txt = minibuffer_content();
+ } else {
+ struct match_result start =
+ buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space);
+ if (!start.found) {
+ start.at = (struct location){.line = ctx.location.line, .col = 0};
+ return;
+ }
+ txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location));
+ }
+
+ char *needle = calloc(txt.nbytes + 1, sizeof(char));
+ memcpy(needle, txt.text, txt.nbytes);
+ needle[txt.nbytes] = '\0';
+
+ if (txt.allocated) {
+ free(txt.text);
+ }
+
+ struct completion *completions = calloc(50, sizeof(struct completion));
+
+ struct needle_match_ctx match_ctx = (struct needle_match_ctx){
+ .needle = needle,
+ .max_ncompletions = 50,
+ .completions = completions,
+ .ncompletions = 0,
+ .on_buffer_selected = pd->on_buffer_selected,
+ };
+
+ buffers_for_each(buffers, buffer_matches, &match_ctx);
+ ctx.add_completions(match_ctx.completions, match_ctx.ncompletions);
+ free(completions);
+ free(needle);
+}
+
+static void cleanup_provider(void *data) {
+ struct buffer_provider_data *bpd = (struct buffer_provider_data *)data;
+ free(bpd);
+}
+
+struct completion_provider
+create_buffer_provider(struct buffers *buffers,
+ on_buffer_selected_cb on_buffer_selected) {
+ struct buffer_provider_data *data =
+ calloc(1, sizeof(struct buffer_provider_data));
+ data->buffers = buffers;
+ data->on_buffer_selected = on_buffer_selected;
+
+ return (struct completion_provider){
+ .name = "buffers",
+ .complete = buffer_complete,
+ .userdata = data,
+ .cleanup = cleanup_provider,
+ };
+}
diff --git a/src/main/completion/buffer.h b/src/main/completion/buffer.h
new file mode 100644
index 0000000..c2b6d42
--- /dev/null
+++ b/src/main/completion/buffer.h
@@ -0,0 +1,18 @@
+#ifndef _MAIN_COMPLETION_BUFFER_H
+#define _MAIN_COMPLETION_BUFFER_H
+
+struct buffer;
+struct buffers;
+
+/**
+ * Create a new buffer completion provider.
+ *
+ * This provider completes buffer names from the
+ * buffer list.
+ * @returns A buffer name @ref completion_provider.
+ */
+struct completion_provider
+create_buffer_provider(struct buffers *buffers,
+ void (*on_buffer_selected)(struct buffer *));
+
+#endif
diff --git a/src/main/completion/command.c b/src/main/completion/command.c
new file mode 100644
index 0000000..e4900ed
--- /dev/null
+++ b/src/main/completion/command.c
@@ -0,0 +1,151 @@
+#include "command.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/command.h"
+#include "dged/minibuffer.h"
+#include "dged/utf8.h"
+
+#include "main/completion.h"
+
+static bool is_space(const struct codepoint *c) {
+ // TODO: utf8 whitespace and other whitespace
+ return c->codepoint == ' ';
+}
+
+typedef void (*on_command_selected_cb)(struct command *);
+
+struct command_completion {
+ struct command *command;
+ on_command_selected_cb on_command_selected;
+};
+
+struct command_provider_data {
+ struct commands *commands;
+ on_command_selected_cb on_command_selected;
+};
+
+static void command_comp_selected(void *data, struct buffer_view *target) {
+ struct command_completion *cc = (struct command_completion *)data;
+ buffer_set_text(target->buffer, (uint8_t *)cc->command->name,
+ strlen(cc->command->name));
+
+ abort_completion();
+ cc->on_command_selected(cc->command);
+}
+
+static struct region command_comp_render(void *data,
+ struct buffer *comp_buffer) {
+ struct command *command = ((struct command_completion *)data)->command;
+ struct location begin = buffer_end(comp_buffer);
+ buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)command->name,
+ strlen(command->name));
+
+ struct location end = buffer_end(comp_buffer);
+ buffer_newline(comp_buffer, buffer_end(comp_buffer));
+
+ return region_new(begin, end);
+}
+
+static void command_comp_cleanup(void *data) {
+ struct command_completion *cc = (struct command_completion *)data;
+ free(cc);
+}
+
+struct needle_match_ctx {
+ const char *needle;
+ struct completion *completions;
+ uint32_t max_ncompletions;
+ uint32_t ncompletions;
+ on_command_selected_cb on_command_selected;
+};
+
+static void command_matches(struct command *command, void *userdata) {
+ struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata;
+
+ if (strncmp(ctx->needle, command->name, strlen(ctx->needle)) == 0 &&
+ ctx->ncompletions < ctx->max_ncompletions) {
+
+ struct command_completion *comp_data =
+ calloc(1, sizeof(struct command_completion));
+ comp_data->command = command;
+ comp_data->on_command_selected = ctx->on_command_selected;
+ ctx->completions[ctx->ncompletions] = (struct completion){
+ .render = command_comp_render,
+ .selected = command_comp_selected,
+ .cleanup = command_comp_cleanup,
+ .data = comp_data,
+ };
+ ++ctx->ncompletions;
+ }
+}
+
+static void command_complete(struct completion_context ctx, bool deletion,
+ void *userdata) {
+ (void)deletion;
+ struct command_provider_data *pd = (struct command_provider_data *)userdata;
+ struct commands *commands = pd->commands;
+ if (commands == NULL) {
+ return;
+ }
+
+ struct text_chunk txt = {0};
+ if (ctx.buffer == minibuffer_buffer()) {
+ txt = minibuffer_content();
+ } else {
+ struct match_result start =
+ buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space);
+ if (!start.found) {
+ start.at = (struct location){.line = ctx.location.line, .col = 0};
+ return;
+ }
+ txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location));
+ }
+
+ char *needle = calloc(txt.nbytes + 1, sizeof(char));
+ memcpy(needle, txt.text, txt.nbytes);
+ needle[txt.nbytes] = '\0';
+
+ if (txt.allocated) {
+ free(txt.text);
+ }
+
+ struct completion *completions = calloc(50, sizeof(struct completion));
+
+ struct needle_match_ctx match_ctx = (struct needle_match_ctx){
+ .needle = needle,
+ .max_ncompletions = 50,
+ .completions = completions,
+ .ncompletions = 0,
+ .on_command_selected = pd->on_command_selected,
+ };
+
+ commands_for_each(commands, command_matches, &match_ctx);
+ ctx.add_completions(match_ctx.completions, match_ctx.ncompletions);
+ free(completions);
+ free(needle);
+}
+
+static void cleanup_provider(void *data) {
+ struct command_provider_data *cpd = (struct command_provider_data *)data;
+ free(cpd);
+}
+
+struct completion_provider
+create_commands_provider(struct commands *commands,
+ on_command_selected_cb on_command_selected) {
+ struct command_provider_data *data =
+ calloc(1, sizeof(struct command_provider_data));
+ data->commands = commands;
+ data->on_command_selected = on_command_selected;
+
+ return (struct completion_provider){
+ .name = "commands",
+ .complete = command_complete,
+ .userdata = data,
+ .cleanup = cleanup_provider,
+ };
+}
diff --git a/src/main/completion/command.h b/src/main/completion/command.h
new file mode 100644
index 0000000..c25df57
--- /dev/null
+++ b/src/main/completion/command.h
@@ -0,0 +1,17 @@
+#ifndef _MAIN_COMPLETION_COMMAND_H
+#define _MAIN_COMPLETION_COMMAND_H
+
+struct command;
+struct commands;
+
+/**
+ * Create a new command completion provider.
+ *
+ * This provider completes registered command names.
+ * @returns A command name @ref completion_provider.
+ */
+struct completion_provider
+create_commands_provider(struct commands *,
+ void (*on_command_selected)(struct command *));
+
+#endif
diff --git a/src/main/completion/path.c b/src/main/completion/path.c
new file mode 100644
index 0000000..708da3d
--- /dev/null
+++ b/src/main/completion/path.c
@@ -0,0 +1,268 @@
+#define _DEFAULT_SOURCE
+#include "path.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <libgen.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/display.h"
+#include "dged/minibuffer.h"
+#include "dged/path.h"
+#include "dged/s8.h"
+#include "dged/utf8.h"
+
+static bool is_space(const struct codepoint *c) {
+ // TODO: utf8 whitespace and other whitespace
+ return c->codepoint == ' ';
+}
+
+typedef void (*on_complete_path_cb)(void);
+
+struct path_completion {
+ struct s8 name;
+ struct region replace;
+ unsigned char type;
+ on_complete_path_cb on_complete_path;
+};
+
+static void path_selected(void *data, struct buffer_view *target) {
+ struct path_completion *comp_path = (struct path_completion *)data;
+ struct location loc = buffer_delete(target->buffer, comp_path->replace);
+ loc = buffer_add(target->buffer, loc, (uint8_t *)comp_path->name.s,
+ comp_path->name.l);
+ buffer_view_goto(target, loc);
+ switch (comp_path->type) {
+ case DT_DIR:
+ if (s8eq(comp_path->name, s8("."))) {
+ // trigger "dired" in this case
+ abort_completion();
+ comp_path->on_complete_path();
+ return;
+ }
+
+ buffer_view_add(target, (uint8_t *)"/", 1);
+ break;
+ default:
+ break;
+ }
+
+ // if the user selected a "normal" file,
+ // the completion is finished
+ if (comp_path->type == DT_REG) {
+ abort_completion();
+ comp_path->on_complete_path();
+ } else {
+ complete(target->buffer, target->dot);
+ }
+}
+
+static struct region path_render(void *data, struct buffer *comp_buffer) {
+ struct path_completion *comp_path = (struct path_completion *)data;
+
+ struct location start = buffer_end(comp_buffer);
+ buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)comp_path->name.s,
+ comp_path->name.l);
+ switch (comp_path->type) {
+ case DT_DIR:
+ if (!(s8eq(comp_path->name, s8(".")) || s8eq(comp_path->name, s8("..")))) {
+ buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)"/", 1);
+ struct location end = buffer_end(comp_buffer);
+ buffer_add_text_property(comp_buffer, start, end,
+ (struct text_property){
+ .start = start,
+ .end = end,
+ .type = TextProperty_Colors,
+ .data.colors =
+ (struct text_property_colors){
+ .set_fg = true,
+ .fg = Color_Magenta,
+ },
+ });
+ }
+ break;
+ case DT_LNK: {
+ struct location end = buffer_end(comp_buffer);
+ buffer_add_text_property(comp_buffer, start, end,
+ (struct text_property){
+ .start = start,
+ .end = end,
+ .type = TextProperty_Colors,
+ .data.colors =
+ (struct text_property_colors){
+ .set_fg = true,
+ .fg = Color_Green,
+ },
+ });
+ } break;
+ default:
+ break;
+ }
+
+ struct location end = buffer_end(comp_buffer);
+ buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)"\n", 1);
+
+ return region_new(start, end);
+}
+
+static void path_cleanup(void *data) {
+ struct path_completion *comp_path = (struct path_completion *)data;
+ s8delete(comp_path->name);
+ free(comp_path);
+}
+
+static int cmp_path_completions(const void *comp_a, const void *comp_b) {
+ struct completion *ca = (struct completion *)comp_a;
+ struct completion *cb = (struct completion *)comp_b;
+ struct path_completion *a = (struct path_completion *)ca->data;
+ struct path_completion *b = (struct path_completion *)cb->data;
+ return s8cmp(a->name, b->name);
+}
+
+static bool is_hidden(const char *filename) {
+ return filename[0] == '.' && filename[1] != '\0' && filename[1] != '.';
+}
+
+static bool fuzzy_match_filename(const char *haystack, const char *needle) {
+ for (; *haystack; ++haystack) {
+ const char *h = haystack;
+ const char *n = needle;
+
+ while (*h && *n && *h == *n) {
+ ++h;
+ ++n;
+ }
+
+ // if we reached the end of needle, we found a match
+ if (!*n) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static void path_complete(struct completion_context ctx, bool deletion,
+ void *on_complete_path) {
+ (void)deletion;
+
+ // obtain path from the buffer
+ struct text_chunk txt = {0};
+ struct location needle_end = ctx.location;
+ if (ctx.buffer == minibuffer_buffer()) {
+ txt = minibuffer_content();
+ needle_end = buffer_end(minibuffer_buffer());
+ } else {
+ struct match_result start =
+ buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space);
+ if (!start.found) {
+ start.at = (struct location){.line = ctx.location.line, .col = 0};
+ return;
+ }
+ txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location));
+ }
+
+ char *path = calloc(txt.nbytes + 1, sizeof(char));
+ memcpy(path, txt.text, txt.nbytes);
+ path[txt.nbytes] = '\0';
+
+ if (txt.allocated) {
+ free(txt.text);
+ }
+
+ uint32_t n = 0;
+ char *p1 = to_abspath(path);
+ char *p2 = strdup(p1);
+
+ size_t inlen = strlen(path);
+
+ const char *dir = p1;
+ const char *file = "";
+
+ // check the input path here since
+ // to_abspath removes trailing slashes
+ if (inlen > 0 && path[inlen - 1] != '/') {
+ dir = dirname(p1);
+ file = basename(p2);
+ }
+
+ struct completion *completions = calloc(50, sizeof(struct completion));
+
+ DIR *d = opendir(dir);
+ if (d == NULL) {
+ goto done;
+ }
+
+ errno = 0;
+ size_t filelen = strlen(file);
+ size_t file_nchars = utf8_nchars((uint8_t *)file, filelen);
+ struct location needle_start = (struct location){
+ .line = needle_end.line,
+ .col = needle_end.col - file_nchars,
+ };
+
+ bool file_is_curdir = filelen == 1 && file[0] == '.';
+ while (n < 50) {
+ struct dirent *de = readdir(d);
+ if (de == NULL && errno != 0) {
+ // skip the erroring entry
+ errno = 0;
+ continue;
+ } else if (de == NULL && errno == 0) {
+ break;
+ }
+
+ switch (de->d_type) {
+ case DT_DIR:
+ case DT_REG:
+ case DT_LNK:
+ if (!is_hidden(de->d_name) && (filelen == 0 || file_is_curdir ||
+ fuzzy_match_filename(de->d_name, file))) {
+
+ struct path_completion *comp_data =
+ calloc(1, sizeof(struct path_completion));
+ comp_data->name = s8new(de->d_name, strlen(de->d_name));
+ comp_data->replace = region_new(needle_start, needle_end);
+ comp_data->type = de->d_type;
+ comp_data->on_complete_path = on_complete_path;
+
+ completions[n] = (struct completion){
+ .data = comp_data,
+ .render = path_render,
+ .selected = path_selected,
+ .cleanup = path_cleanup,
+ };
+
+ ++n;
+ }
+ break;
+ }
+ }
+
+ closedir(d);
+
+done:
+ free(path);
+ free(p1);
+ free(p2);
+
+ qsort(completions, n, sizeof(struct completion), cmp_path_completions);
+ ctx.add_completions(completions, n);
+
+ free(completions);
+}
+
+struct completion_provider
+create_path_provider(void (*on_complete_path)(void)) {
+ return (struct completion_provider){
+ .name = "path",
+ .complete = path_complete,
+ .userdata = on_complete_path,
+ };
+}
diff --git a/src/main/completion/path.h b/src/main/completion/path.h
new file mode 100644
index 0000000..407cae7
--- /dev/null
+++ b/src/main/completion/path.h
@@ -0,0 +1,14 @@
+#ifndef _MAIN_COMPLETION_PATH_H
+#define _MAIN_COMPLETION_PATH_H
+
+#include "main/completion.h"
+
+/**
+ * Create a new path completion provider.
+ *
+ * This provider completes filesystem paths.
+ * @returns A filesystem path @ref completion_provider.
+ */
+struct completion_provider create_path_provider(void (*on_complete_path)(void));
+
+#endif
diff --git a/src/main/frame-hooks.c b/src/main/frame-hooks.c
new file mode 100644
index 0000000..ae7bc1e
--- /dev/null
+++ b/src/main/frame-hooks.c
@@ -0,0 +1,27 @@
+#include "frame-hooks.h"
+
+#include "dged/hook.h"
+
+HOOK_IMPL_NO_REMOVE(next_frame, next_frame_cb);
+
+static next_frame_hook_vec g_next_frame_hooks;
+static uint32_t g_next_frame_hook_id;
+
+void init_frame_hooks(void) { VEC_INIT(&g_next_frame_hooks, 16); }
+
+void teardown_frame_hooks(void) { VEC_DESTROY(&g_next_frame_hooks); }
+
+void run_next_frame(next_frame_cb callback, void *userdata) {
+ insert_next_frame_hook(&g_next_frame_hooks, &g_next_frame_hook_id, callback,
+ userdata);
+}
+
+size_t dispatch_next_frame_hooks() {
+ size_t nhooks = VEC_SIZE(&g_next_frame_hooks);
+ if (nhooks > 0) {
+ dispatch_hook_no_args(&g_next_frame_hooks, struct next_frame_hook);
+ VEC_CLEAR(&g_next_frame_hooks);
+ }
+
+ return nhooks;
+}
diff --git a/src/main/frame-hooks.h b/src/main/frame-hooks.h
new file mode 100644
index 0000000..fc382fc
--- /dev/null
+++ b/src/main/frame-hooks.h
@@ -0,0 +1,13 @@
+#ifndef _FRAME_HOOKS_H
+#define _FRAME_HOOKS_H
+
+#include <stddef.h>
+
+typedef void (*next_frame_cb)(void *);
+
+void init_frame_hooks(void);
+void teardown_frame_hooks(void);
+void run_next_frame(next_frame_cb callback, void *userdata);
+size_t dispatch_next_frame_hooks(void);
+
+#endif
diff --git a/src/main/lsp.c b/src/main/lsp.c
index d56ca07..5886ea7 100644
--- a/src/main/lsp.c
+++ b/src/main/lsp.c
@@ -1,38 +1,556 @@
#include "lsp.h"
+#include "dged/binding.h"
#include "dged/buffer.h"
+#include "dged/buffer_view.h"
#include "dged/buffers.h"
+#include "dged/command.h"
+#include "dged/display.h"
#include "dged/hash.h"
#include "dged/hashmap.h"
-#include "dged/lsp.h"
+#include "dged/lang.h"
#include "dged/minibuffer.h"
#include "dged/reactor.h"
#include "dged/settings.h"
+#include "dged/window.h"
-HASHMAP_ENTRY_TYPE(lsp_entry, struct lsp *);
+#include "lsp/references.h"
+#include "main/bindings.h"
+#include "main/completion.h"
-HASHMAP(struct lsp_entry) g_lsp_clients;
+#include "lsp/actions.h"
+#include "lsp/choice-buffer.h"
+#include "lsp/completion.h"
+#include "lsp/diagnostics.h"
+#include "lsp/format.h"
+#include "lsp/goto.h"
+#include "lsp/help.h"
+#include "lsp/rename.h"
+
+struct lsp_pending_request {
+ uint64_t request_id;
+ response_handler handler;
+ void *userdata;
+};
+
+struct lsp_server {
+ struct lsp *lsp;
+ uint32_t restarts;
+ struct s8 lang_id;
+ struct lsp_pending_request pending_requests[16];
+
+ bool initialized;
+
+ enum text_document_sync_kind sync_kind;
+ bool send_open_close;
+ bool send_save;
+
+ enum position_encoding_kind position_encoding;
+
+ struct lsp_diagnostics *diagnostics;
+ struct completion_ctx *completion_ctx;
+};
+
+HASHMAP_ENTRY_TYPE(lsp_entry, struct lsp_server);
+
+static struct lsp_data {
+ HASHMAP(struct lsp_entry) clients;
+ struct keymap keymap;
+ struct keymap all_keymap;
-static struct create_data {
struct reactor *reactor;
struct buffers *buffers;
-} g_create_data;
-static void log_message(int type, struct s8 msg) {
- (void)type;
- message("%s", msg);
+ struct buffer *current_diagnostic_buffer;
+ uint64_t current_request_id;
+} g_lsp_data;
+
+struct lsp *lsp_backend(struct lsp_server *server) { return server->lsp; }
+
+struct lsp_server *lsp_server_for_lang_id(const char *id) {
+ HASHMAP_GET(&g_lsp_data.clients, struct lsp_entry, id,
+ struct lsp_server * server);
+ return server;
}
-static void create_lsp_client(struct buffer *buffer, void *userdata) {
+static uint32_t bytepos_to_column(struct text_chunk *text, uint32_t bytecol) {
+ struct utf8_codepoint_iterator iter =
+ create_utf8_codepoint_iterator(text->text, text->nbytes, 0);
+ uint32_t ncols = 0, col = 0;
+ struct codepoint *codepoint = NULL;
+ while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < bytecol) {
+ col += codepoint->nbytes;
+ ncols += unicode_visual_char_width(codepoint);
+ }
+
+ return ncols;
+}
+
+static uint32_t codepoint_pos_to_column(struct text_chunk *text,
+ uint32_t codepoint_col) {
+ struct utf8_codepoint_iterator iter =
+ create_utf8_codepoint_iterator(text->text, text->nbytes, 0);
+ uint32_t ncols = 0, col = 0;
+ struct codepoint *codepoint = NULL;
+ while ((codepoint = utf8_next_codepoint(&iter)) != NULL &&
+ col < codepoint_col) {
+ ++col;
+ ncols += unicode_visual_char_width(codepoint);
+ }
+
+ return ncols;
+}
+
+static uint32_t codeunit_pos_to_column(struct text_chunk *text,
+ uint32_t codeunit_col) {
+ struct utf8_codepoint_iterator iter =
+ create_utf8_codepoint_iterator(text->text, text->nbytes, 0);
+ uint32_t ncols = 0, col = 0;
+ struct codepoint *codepoint = NULL;
+ while ((codepoint = utf8_next_codepoint(&iter)) != NULL &&
+ col < codeunit_col) {
+ col += codepoint->codepoint >= 0x010000 ? 2 : 1;
+ ncols += unicode_visual_char_width(codepoint);
+ }
+
+ return ncols;
+}
+
+struct region lsp_range_to_coordinates(struct lsp_server *server,
+ struct buffer *buffer,
+ struct region range) {
+
+ uint32_t (*col_converter)(struct text_chunk *, uint32_t) =
+ codeunit_pos_to_column;
+
+ switch (server->position_encoding) {
+ case PositionEncoding_Utf8:
+ col_converter = bytepos_to_column;
+ break;
+
+ case PositionEncoding_Utf16:
+ col_converter = codeunit_pos_to_column;
+ break;
+
+ case PositionEncoding_Utf32:
+ col_converter = codepoint_pos_to_column;
+ break;
+ }
+
+ struct region reg = range;
+ struct text_chunk beg_line = buffer_line(buffer, range.begin.line);
+ reg.begin.col = col_converter(&beg_line, range.begin.col);
+
+ struct text_chunk end_line = beg_line;
+ if (range.begin.line != range.end.line) {
+ end_line = buffer_line(buffer, range.end.line);
+ }
+ reg.end.col = col_converter(&end_line, range.end.col);
+
+ if (beg_line.allocated) {
+ free(beg_line.text);
+ }
+
+ if (range.begin.line != range.end.line && end_line.allocated) {
+ free(end_line.text);
+ }
+
+ return reg;
+}
+
+uint64_t new_pending_request(struct lsp_server *server,
+ response_handler handler, void *userdata) {
+ for (int i = 0; i < 16; ++i) {
+ if (server->pending_requests[i].request_id == (uint64_t)-1) {
+ ++g_lsp_data.current_request_id;
+ server->pending_requests[i].request_id = g_lsp_data.current_request_id;
+ server->pending_requests[i].handler = handler;
+ server->pending_requests[i].userdata = userdata;
+ return g_lsp_data.current_request_id;
+ }
+ }
+
+ return -1;
+}
+
+static bool
+request_response_received(struct lsp_server *server, uint64_t id,
+ struct lsp_pending_request **pending_request) {
+ for (int i = 0; i < 16; ++i) {
+ if (server->pending_requests[i].request_id == id) {
+ server->pending_requests[i].request_id = (uint64_t)-1;
+ *pending_request = &server->pending_requests[i];
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static void buffer_updated(struct buffer *buffer, void *userdata) {
+ struct lsp_server *server = (struct lsp_server *)userdata;
+
+ diagnostic_vec *diagnostics =
+ diagnostics_for_buffer(server->diagnostics, buffer);
+ if (diagnostics == NULL) {
+ return;
+ }
+
+ VEC_FOR_EACH(diagnostics, struct diagnostic * diag) {
+ struct text_property prop;
+ prop.type = TextProperty_Colors;
+ uint32_t color = diag_severity_color(diag->severity);
+ prop.data.colors = (struct text_property_colors){
+ .set_bg = true,
+ .set_fg = true,
+ .fg = Color_White,
+ .bg = color,
+ .underline = true,
+ };
+
+ struct region reg = region_new(
+ diag->region.begin, buffer_previous_char(buffer, diag->region.end));
+
+ buffer_add_text_property(buffer, reg.begin, reg.end, prop);
+
+ if (window_buffer(windows_get_active()) == buffer) {
+ struct buffer_view *bv = window_buffer_view(windows_get_active());
+
+ if (region_is_inside(diag->region, bv->dot)) {
+ size_t len = 0;
+ for (size_t i = 0; i < diag->message.l && diag->message.s[i] != '\n';
+ ++i) {
+ ++len;
+ }
+ minibuffer_display_timeout(1, "%s: %.*s",
+ diag_severity_to_str(diag->severity), len,
+ diag->message.s);
+ }
+ }
+ }
+}
+
+static void buffer_pre_save(struct buffer *buffer, void *userdata) {
+ (void)buffer;
(void)userdata;
+}
+
+static void format_on_save(struct buffer *buffer, struct lsp_server *server) {
+ struct setting *glob_fmt_on_save = settings_get("editor.format-on-save");
+ struct setting *fmt_on_save =
+ lang_setting(&buffer->lang, "language-server.format-on-save");
+
+ if ((glob_fmt_on_save != NULL && glob_fmt_on_save->value.data.bool_value) ||
+ (glob_fmt_on_save == NULL && fmt_on_save != NULL &&
+ fmt_on_save->value.data.bool_value)) {
+ format_document_save(server, buffer);
+ }
+}
+
+static void buffer_post_save(struct buffer *buffer, void *userdata) {
+ struct lsp_server *server = (struct lsp_server *)userdata;
+ if (server->send_save) {
+ struct versioned_text_document_identifier text_document =
+ versioned_identifier_from_buffer(buffer);
- struct create_data *data = &g_create_data;
+ struct did_save_text_document_params params = {
+ .text_document =
+ (struct text_document_identifier){
+ .uri = text_document.uri,
+ },
+ };
+
+ struct s8 json_payload = did_save_text_document_params_to_json(&params);
+
+ lsp_send(server->lsp,
+ lsp_create_notification(s8("textDocument/didSave"), json_payload));
+
+ versioned_text_document_identifier_free(&text_document);
+ s8delete(json_payload);
+ }
+
+ format_on_save(buffer, server);
+}
+
+static uint32_t count_codepoints(struct text_chunk *chunk,
+ uint32_t target_col) {
+ struct utf8_codepoint_iterator iter =
+ create_utf8_codepoint_iterator(chunk->text, chunk->nbytes, 0);
+ uint32_t ncodepoints = 0, col = 0;
+ struct codepoint *codepoint = NULL;
+ while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < target_col) {
+ col += unicode_visual_char_width(codepoint);
+ ++ncodepoints;
+ }
+
+ return ncodepoints;
+}
+
+static uint32_t count_codeunits(struct text_chunk *chunk, uint32_t target_col) {
+ struct utf8_codepoint_iterator iter =
+ create_utf8_codepoint_iterator(chunk->text, chunk->nbytes, 0);
+ uint32_t ncodeunits = 0, col = 0;
+ struct codepoint *codepoint = NULL;
+ while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < target_col) {
+ col += unicode_visual_char_width(codepoint);
+ ncodeunits += codepoint->codepoint >= 0x010000 ? 2 : 1;
+ }
+
+ return ncodeunits;
+}
+
+static uint32_t count_bytes(struct text_chunk *chunk, uint32_t target_col) {
+ struct utf8_codepoint_iterator iter =
+ create_utf8_codepoint_iterator(chunk->text, chunk->nbytes, 0);
+ uint32_t nbytes = 0, col = 0;
+ struct codepoint *codepoint = NULL;
+ while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < target_col) {
+ col += unicode_visual_char_width(codepoint);
+ nbytes += codepoint->nbytes;
+ }
+
+ return nbytes;
+}
+
+static struct region edit_location_to_lsp(struct buffer *buffer,
+ struct edit_location edit,
+ struct lsp_server *server) {
+
+ struct region res = edit.coordinates;
+ if (server->position_encoding == PositionEncoding_Utf8) {
+ /* In this case, the buffer hook has already
+ * done the job for us. */
+ res.begin.col = edit.bytes.begin.col;
+ res.end.col = edit.bytes.end.col;
+ return res;
+ }
+
+ return region_to_lsp(buffer, res, server);
+}
+
+struct region region_to_lsp(struct buffer *buffer, struct region region,
+ struct lsp_server *server) {
+ struct region res = region;
+
+ uint32_t (*col_counter)(struct text_chunk *, uint32_t) = count_codeunits;
+
+ switch (server->position_encoding) {
+ case PositionEncoding_Utf8:
+ col_counter = count_bytes;
+ return res;
+
+ case PositionEncoding_Utf16:
+ col_counter = count_codeunits;
+ break;
+
+ case PositionEncoding_Utf32:
+ col_counter = count_codepoints;
+ break;
+ }
+
+ struct text_chunk beg_line = buffer_line(buffer, region.begin.line);
+ res.begin.col = col_counter(&beg_line, region.begin.col);
+
+ struct text_chunk end_line = beg_line;
+ if (region.begin.line != region.end.line) {
+ end_line = buffer_line(buffer, region.end.line);
+ }
+
+ res.end.col = col_counter(&end_line, region.end.col);
+
+ if (beg_line.allocated) {
+ free(beg_line.text);
+ }
+
+ if (end_line.allocated && region.begin.line != region.end.line) {
+ free(end_line.text);
+ }
+
+ return res;
+}
+
+static void buffer_text_changed(struct buffer *buffer,
+ struct edit_location range, bool delete,
+ void *userdata) {
+ struct lsp_server *server = (struct lsp_server *)userdata;
+
+ struct text_chunk new_text = {0};
+ switch (server->sync_kind) {
+ case TextDocumentSync_None:
+ return;
+
+ case TextDocumentSync_Full:
+ new_text =
+ buffer_region(buffer, region_new((struct location){.line = 0, .col = 0},
+ buffer_end(buffer)));
+ break;
+
+ case TextDocumentSync_Incremental:
+ if (!delete) {
+ new_text = buffer_region(buffer, range.coordinates);
+ }
+ break;
+ }
+
+ struct region reg = edit_location_to_lsp(buffer, range, server);
+ reg = delete ? reg : region_new(reg.begin, reg.begin);
+ struct text_document_content_change_event evt = {
+ .full_document = server->sync_kind == TextDocumentSync_Full,
+ .text = s8new((const char *)new_text.text, new_text.nbytes),
+ .range = reg,
+ };
+
+ struct versioned_text_document_identifier text_document =
+ versioned_identifier_from_buffer(buffer);
+ struct did_change_text_document_params params = {
+ .text_document = text_document,
+ .content_changes = &evt,
+ .ncontent_changes = 1,
+ };
+
+ struct s8 json_payload = did_change_text_document_params_to_json(&params);
+
+ lsp_send(server->lsp,
+ lsp_create_notification(s8("textDocument/didChange"), json_payload));
+
+ versioned_text_document_identifier_free(&text_document);
+ s8delete(json_payload);
+ s8delete(evt.text);
+
+ if (new_text.allocated) {
+ free(new_text.text);
+ }
+}
+
+static void buffer_text_inserted(struct buffer *buffer,
+ struct edit_location inserted,
+ void *userdata) {
+ buffer_text_changed(buffer, inserted, false, userdata);
+}
+
+static void buffer_text_deleted(struct buffer *buffer,
+ struct edit_location deleted, void *userdata) {
+ buffer_text_changed(buffer, deleted, true, userdata);
+}
+
+static void send_did_open(struct lsp_server *server, struct buffer *buffer) {
+ if (!server->send_open_close) {
+ return;
+ }
+
+ struct text_document_item doc = text_document_item_from_buffer(buffer);
+ struct did_open_text_document_params params = {
+ .text_document = doc,
+ };
+
+ struct s8 json_payload = did_open_text_document_params_to_json(&params);
+ lsp_send(server->lsp,
+ lsp_create_notification(s8("textDocument/didOpen"), json_payload));
+
+ text_document_item_free(&doc);
+ s8delete(json_payload);
+}
+
+static void setup_completion(struct lsp_server *server, struct buffer *buffer) {
+ if (server->completion_ctx != NULL) {
+ enable_completion_for_buffer(server->completion_ctx, buffer);
+ }
+}
+
+static void lsp_buffer_initialized(struct lsp_server *server,
+ struct buffer *buffer) {
+ if (s8eq(server->lang_id, s8(buffer->lang.id))) {
+ /* Needs to be a pre-delete hook since we need
+ * access to the deleted content to derive the
+ * correct UTF-8/16/32 position.
+ */
+ buffer_add_pre_delete_hook(buffer, buffer_text_deleted, server);
+ buffer_add_insert_hook(buffer, buffer_text_inserted, server);
+ buffer_add_update_hook(buffer, buffer_updated, server);
+ buffer_add_pre_save_hook(buffer, buffer_pre_save, server);
+ buffer_add_post_save_hook(buffer, buffer_post_save, server);
+
+ send_did_open(server, buffer);
+ setup_completion(server, buffer);
+ }
+}
+
+static void apply_initialized(struct buffer *buffer, void *userdata) {
+ struct lsp_server *server = (struct lsp_server *)userdata;
+ lsp_buffer_initialized(server, buffer);
+}
+
+static void handle_initialize(struct lsp_server *server,
+ struct lsp_response *response, void *userdata) {
+ (void)userdata;
+
+ struct initialize_result res =
+ initialize_result_from_json(&response->value.result);
+ message("lsp server initialized: %.*s (%.*s)", res.server_info.name.l,
+ res.server_info.name.s, res.server_info.version.l,
+ res.server_info.version.s);
+
+ lsp_send(server->lsp, lsp_create_notification(s8("initialized"), s8("")));
+
+ struct text_document_sync *tsync = &res.capabilities.text_document_sync;
+ server->sync_kind = tsync->kind;
+ server->send_open_close = tsync->open_close;
+ server->send_save = tsync->save;
+ server->position_encoding = res.capabilities.position_encoding;
+
+ if (res.capabilities.supports_completion) {
+ struct completion_options *comp_opts = &res.capabilities.completion_options;
+ server->completion_ctx = create_completion_ctx(
+ server, (triggerchar_vec *)&comp_opts->trigger_characters);
+ }
+
+ initialize_result_free(&res);
+ buffers_for_each(g_lsp_data.buffers, apply_initialized, server);
+
+ server->initialized = true;
+}
+
+static void init_lsp_client(struct lsp_server *server) {
+ if (lsp_restart_server(server->lsp) < 0) {
+ minibuffer_echo("failed to start language server %s process.",
+ lsp_server_name(server->lsp));
+ return;
+ }
+
+ // send some init info
+ struct initialize_params params = {
+ .process_id = lsp_server_pid(server->lsp),
+ .client_info =
+ {
+ .name = s8("dged"),
+ .version = s8("dev"),
+ },
+ .client_capabilities = {},
+ .workspace_folders = NULL,
+ .nworkspace_folders = 0,
+ };
+
+ uint64_t id = new_pending_request(server, handle_initialize, NULL);
+ struct s8 json_payload = initialize_params_to_json(&params);
+ lsp_send(server->lsp, lsp_create_request(id, s8("initialize"), json_payload));
+
+ s8delete(json_payload);
+}
+
+static void create_lsp_client(struct buffer *buffer, void *userdata) {
+ (void)userdata;
const char *id = buffer->lang.id;
- HASHMAP_GET(&g_lsp_clients, struct lsp_entry, id, struct lsp * *lsp);
- if (lsp == NULL) {
+ HASHMAP_GET(&g_lsp_data.clients, struct lsp_entry, id,
+ struct lsp_server * server);
+ if (server == NULL) {
// we need to start a new server
- struct setting *s = lang_setting(&buffer->lang, "language-server");
- if (!s) { // no language server set
+ struct setting *s = lang_setting(&buffer->lang, "language-server.command");
+ if (!s) {
+ if (!lang_is_fundamental(&buffer->lang)) {
+ message("No language server set for %s. Set with "
+ "`languages.%s.language-server`.",
+ buffer->lang.id, buffer->lang.id);
+ }
return;
}
@@ -40,69 +558,388 @@ static void create_lsp_client(struct buffer *buffer, void *userdata) {
char bufname[1024] = {0};
snprintf(bufname, 1024, "*%s-lsp-stderr*", command[0]);
- struct buffer *stderr_buf = buffers_find(data->buffers, bufname);
+ struct buffer *stderr_buf = buffers_find(g_lsp_data.buffers, bufname);
if (stderr_buf == NULL) {
struct buffer buf = buffer_create(bufname);
buf.lazy_row_add = false;
- stderr_buf = buffers_add(data->buffers, buf);
+ stderr_buf = buffers_add(g_lsp_data.buffers, buf);
buffer_set_readonly(stderr_buf, true);
}
- struct lsp_client client_impl = {
- .log_message = log_message,
- };
struct lsp *new_lsp =
- lsp_create(command, data->reactor, stderr_buf, client_impl, NULL);
+ lsp_create(command, g_lsp_data.reactor, stderr_buf, command[0]);
if (new_lsp == NULL) {
minibuffer_echo("failed to create language server %s", command[0]);
- buffers_remove(data->buffers, bufname);
+ buffers_remove(g_lsp_data.buffers, bufname);
return;
}
- HASHMAP_APPEND(&g_lsp_clients, struct lsp_entry, id,
+ HASHMAP_APPEND(&g_lsp_data.clients, struct lsp_entry, id,
struct lsp_entry * new);
- new->value = new_lsp;
-
- if (lsp_start_server(new_lsp) < 0) {
- minibuffer_echo("failed to start language server %s process.",
- lsp_server_name(new_lsp));
- return;
+ new->value = (struct lsp_server){
+ .lsp = new_lsp,
+ .lang_id = s8new(id, strlen(id)),
+ .restarts = 0,
+ };
+ for (int i = 0; i < 16; ++i) {
+ new->value.pending_requests[i].request_id = (uint64_t)-1;
}
+
+ new->value.diagnostics = diagnostics_create();
+
+ // support for this is determined later
+ new->value.completion_ctx = NULL;
+
+ init_lsp_client(&new->value);
+ server = &new->value;
+ }
+
+ /* An lsp for the language for this buffer is already started.
+ * if server is not initialized, it will get picked
+ * up anyway when handling the initialize response. */
+ if (server->initialized) {
+ lsp_buffer_initialized(server, buffer);
}
}
-static void set_default_lsp(const char *lang_id, const char *server) {
+static void set_default_lsp(const char *lang_id, const char *command) {
struct language l = lang_from_id(lang_id);
if (!lang_is_fundamental(&l)) {
lang_setting_set_default(
- &l, "language-server",
+ &l, "language-server.command",
(struct setting_value){.type = Setting_String,
- .data.string_value = (char *)server});
+ .data.string_value = (char *)command});
+ lang_setting_set_default(
+ &l, "language-server.format-on-save",
+ (struct setting_value){.type = Setting_Bool, .data.bool_value = false});
+
lang_destroy(&l);
}
}
-void lang_servers_init(struct reactor *reactor, struct buffers *buffers) {
- HASHMAP_INIT(&g_lsp_clients, 32, hash_name);
+static struct s8 lsp_modeline(struct buffer_view *view, void *userdata) {
+ (void)userdata;
+ struct lsp_server *server = lsp_server_for_lang_id(view->buffer->lang.id);
+ if (server == NULL) {
+ return s8("");
+ }
+
+ return s8from_fmt(
+ "lsp: %s:%d", lsp_server_name(server->lsp),
+ lsp_server_running(server->lsp) ? lsp_server_pid(server->lsp) : 0);
+}
+
+static uint32_t lsp_keymap_hook(struct buffer *buffer, struct keymap *keymaps[],
+ uint32_t max_nkeymaps, void *userdata) {
+ (void)userdata;
+
+ if (max_nkeymaps < 2) {
+ return 0;
+ }
+
+ uint32_t nadded = 1;
+ keymaps[0] = &g_lsp_data.all_keymap;
+
+ if (lsp_server_for_lang_id(buffer->lang.id) != NULL) {
+ keymaps[1] = &g_lsp_data.keymap;
+ nadded = 2;
+ }
+
+ return nadded;
+}
+
+static int32_t lsp_restart_cmd(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)ctx;
+ (void)argc;
+ (void)argv;
+ struct buffer *b = window_buffer(windows_get_active());
+
+ struct lsp_server *server = lsp_server_for_buffer(b);
+ if (server == NULL) {
+ return 0;
+ }
+
+ lsp_restart_server(server->lsp);
+ return 0;
+}
+
+void lang_servers_init(struct reactor *reactor, struct buffers *buffers,
+ struct commands *commands) {
+ HASHMAP_INIT(&g_lsp_data.clients, 32, hash_name);
set_default_lsp("c", "clangd");
+ set_default_lsp("cxx", "clangd");
set_default_lsp("rs", "rust-analyzer");
set_default_lsp("python", "pylsp");
- g_create_data.reactor = reactor;
- g_create_data.buffers = buffers;
- buffer_add_create_hook(create_lsp_client, NULL);
+ g_lsp_data.current_request_id = 0;
+ g_lsp_data.reactor = reactor;
+ g_lsp_data.buffers = buffers;
+ buffers_add_add_hook(buffers, create_lsp_client, NULL);
+
+ struct command lsp_commands[] = {
+ {.name = "lsp-goto-definition", .fn = lsp_goto_def_cmd},
+ {.name = "lsp-goto-declaration", .fn = lsp_goto_decl_cmd},
+ {.name = "lsp-goto-implementation", .fn = lsp_goto_impl_cmd},
+ {.name = "lsp-goto", .fn = lsp_goto_cmd},
+ {.name = "lsp-goto-previous", .fn = lsp_goto_previous_cmd},
+ {.name = "lsp-references", .fn = lsp_references_cmd},
+ {.name = "lsp-restart", .fn = lsp_restart_cmd},
+ {.name = "lsp-diagnostics", .fn = diagnostics_cmd},
+ {.name = "lsp-next-diagnostic", .fn = next_diagnostic_cmd},
+ {.name = "lsp-prev-diagnostic", .fn = prev_diagnostic_cmd},
+ {.name = "lsp-code-actions", .fn = code_actions_cmd},
+ {.name = "lsp-format", .fn = format_cmd},
+ {.name = "lsp-rename", .fn = lsp_rename_cmd},
+ {.name = "lsp-help", .fn = lsp_help_cmd},
+ };
+
+ register_commands(commands, lsp_commands,
+ sizeof(lsp_commands) / sizeof(lsp_commands[0]));
+
+ struct binding lsp_binds[] = {
+ BINDING(Meta, '.', "lsp-goto-definition"),
+ BINDING(Meta, '/', "lsp-goto"),
+ BINDING(Meta, '[', "lsp-prev-diagnostic"),
+ BINDING(Meta, ']', "lsp-next-diagnostic"),
+ BINDING(Meta, 'a', "lsp-code-actions"),
+ BINDING(Meta, '=', "lsp-format"),
+ BINDING(Meta, 'r', "lsp-rename"),
+ BINDING(Meta, 'h', "lsp-help"),
+ };
+
+ struct binding global_binds[] = {
+ BINDING(Meta, ',', "lsp-goto-previous"),
+ };
+
+ g_lsp_data.keymap = keymap_create("lsp", 32);
+ keymap_bind_keys(&g_lsp_data.keymap, lsp_binds,
+ sizeof(lsp_binds) / sizeof(lsp_binds[0]));
+ g_lsp_data.all_keymap = keymap_create("lsp-global", 32);
+ keymap_bind_keys(&g_lsp_data.all_keymap, global_binds,
+ sizeof(global_binds) / sizeof(global_binds[0]));
+ buffer_add_keymaps_hook(lsp_keymap_hook, NULL);
+
+ buffer_view_add_modeline_hook(lsp_modeline, NULL);
+
+ init_goto(32, buffers);
+}
+
+void apply_edits_buffer(struct lsp_server *server, struct buffer *buffer,
+ text_edit_vec edits, struct location *point) {
+ VEC_FOR_EACH_REVERSE(&edits, struct text_edit * edit) {
+ struct region reg = lsp_range_to_coordinates(server, buffer, edit->range);
+ struct location at = reg.end;
+ if (region_has_size(reg)) {
+ if (point != NULL) {
+
+ if (reg.end.line == point->line) {
+ point->col -= reg.end.col > point->col ? 0 : point->col - reg.end.col;
+ }
+
+ uint64_t lines_deleted = reg.end.line - reg.begin.line;
+ if (lines_deleted > 0 && reg.end.line <= point->line) {
+ point->line -= lines_deleted;
+ }
+ }
+ at = buffer_delete(buffer, reg);
+ }
+
+ struct location after =
+ buffer_add(buffer, at, edit->new_text.s, edit->new_text.l);
+ if (point != NULL) {
+ if (after.line == point->line) {
+ point->col += after.col;
+ }
+
+ uint64_t lines_added = after.line - at.line;
+ if (lines_added > 0 && after.line <= point->line) {
+ point->line += lines_added;
+ }
+ }
+ }
+}
+
+bool apply_edits(struct lsp_server *server,
+ const struct workspace_edit *ws_edit) {
+ pause_completion();
+
+ VEC_FOR_EACH(&ws_edit->changes, struct text_edit_pair * pair) {
+ if (VEC_EMPTY(&pair->edits)) {
+ continue;
+ }
+
+ const char *p = s8tocstr(pair->uri);
+ struct buffer *b = buffers_find_by_filename(g_lsp_data.buffers, &p[7]);
+
+ if (b == NULL) {
+ struct buffer new_buf = buffer_from_file(&p[7]);
+ b = buffers_add(g_lsp_data.buffers, new_buf);
+ }
+
+ free((void *)p);
+ buffer_push_undo_boundary(b);
+ apply_edits_buffer(server, b, pair->edits, NULL);
+ buffer_push_undo_boundary(b);
+ }
+
+ resume_completion();
+ return true;
+}
+
+static void handle_request(struct lsp_server *server,
+ struct lsp_request request) {
+
+ struct s8 method = unescape_json_string(request.method);
+ if (s8eq(method, s8("workspace/applyEdit"))) {
+ struct workspace_edit ws_edit = workspace_edit_from_json(&request.params);
+ apply_edits(server, &ws_edit);
+ workspace_edit_free(&ws_edit);
+ } else {
+ message("unhandled lsp request (%s): id %d: %.*s",
+ lsp_server_name(server->lsp), request.id, request.method.l,
+ request.method.s);
+ }
+
+ s8delete(method);
+}
+
+static void handle_response(struct lsp_server *server,
+ struct lsp_response response) {
+ if (response.ok) {
+ struct lsp_pending_request *pending = NULL;
+ if (!request_response_received(server, response.id, &pending)) {
+ message("received response for id %d, server %s, which has no handler "
+ "registered",
+ response.id, lsp_server_name(server->lsp));
+ }
+
+ if (pending->handler != NULL) {
+ pending->handler(server, &response, pending->userdata);
+ }
+ } else {
+ struct s8 errmsg = response.value.error.message;
+ minibuffer_echo("lsp error (%s), id %d: %.*s", lsp_server_name(server->lsp),
+ response.id, errmsg.l, errmsg.s);
+ }
+}
+
+static void handle_notification(struct lsp_server *server,
+ struct lsp_notification notification) {
+ struct s8 method = unescape_json_string(notification.method);
+ if (s8eq(method, s8("textDocument/publishDiagnostics"))) {
+ handle_publish_diagnostics(server, g_lsp_data.buffers, &notification);
+ }
+
+ s8delete(method);
+}
+
+#define MAX_RESTARTS 10
+
+static void restart_if_needed(struct lsp_server *server) {
+ // if we successfully initialized the server, we can be sure
+ // it is up and running
+ if (lsp_server_running(server->lsp) && server->initialized) {
+ server->restarts = 0;
+ return;
+ }
+
+ if (!lsp_server_running(server->lsp)) {
+ if (server->restarts < MAX_RESTARTS) {
+ message("restarting \"%s\" (%d/%d)...", lsp_server_name(server->lsp),
+ server->restarts + 1, MAX_RESTARTS);
+ init_lsp_client(server);
+ ++server->restarts;
+
+ if (server->restarts == MAX_RESTARTS) {
+ minibuffer_echo("lsp \"%s\" has crashed %d times, giving up...",
+ lsp_server_name(server->lsp), MAX_RESTARTS);
+ }
+ } else {
+ // server is crashed and can only be restarted manually now
+ lsp_stop_server(server->lsp);
+ }
+ }
}
void lang_servers_update(void) {
- HASHMAP_FOR_EACH(&g_lsp_clients, struct lsp_entry * e) {
- lsp_update(e->value, NULL, 0);
+
+ HASHMAP_FOR_EACH(&g_lsp_data.clients, struct lsp_entry * e) {
+ restart_if_needed(&e->value);
+
+ struct lsp_message msgs[128];
+ uint32_t msgs_received = lsp_update(e->value.lsp, msgs, 128);
+
+ if (msgs_received == 0 || msgs_received == (uint32_t)-1) {
+ continue;
+ }
+
+ char bufname[1024] = {0};
+ snprintf(bufname, 1024, "*%s-lsp-messages*", lsp_server_name(e->value.lsp));
+ struct buffer *output_buf = buffers_find(g_lsp_data.buffers, bufname);
+ if (output_buf == NULL) {
+ struct buffer buf = buffer_create(bufname);
+ buf.lazy_row_add = false;
+ output_buf = buffers_add(g_lsp_data.buffers, buf);
+ }
+
+ buffer_set_readonly(output_buf, false);
+ for (uint32_t mi = 0; mi < msgs_received; ++mi) {
+ struct lsp_message *msg = &msgs[mi];
+ buffer_add(output_buf, buffer_end(output_buf), msg->payload.s,
+ msg->payload.l);
+ buffer_add(output_buf, buffer_end(output_buf), (uint8_t *)"\n", 1);
+
+ switch (msg->type) {
+ case Lsp_Response:
+ handle_response(&e->value, msg->message.response);
+ break;
+
+ case Lsp_Request:
+ handle_request(&e->value, msg->message.request);
+ break;
+
+ case Lsp_Notification:
+ handle_notification(&e->value, msg->message.notification);
+ break;
+ }
+
+ lsp_message_destroy(msg);
+ }
+
+ buffer_set_readonly(output_buf, true);
}
}
+static void lang_server_teardown(struct lsp_server *server) {
+ destroy_goto();
+ lsp_stop_server(server->lsp);
+ lsp_destroy(server->lsp);
+ s8delete(server->lang_id);
+}
+
void lang_servers_teardown(void) {
- HASHMAP_FOR_EACH(&g_lsp_clients, struct lsp_entry * e) {
- lsp_stop_server(e->value);
+ HASHMAP_FOR_EACH(&g_lsp_data.clients, struct lsp_entry * e) {
+ diagnostics_destroy(e->value.diagnostics);
+
+ if (e->value.completion_ctx != NULL) {
+ destroy_completion_ctx(e->value.completion_ctx);
+ }
+
+ lang_server_teardown(&e->value);
}
+
+ keymap_destroy(&g_lsp_data.keymap);
+ keymap_destroy(&g_lsp_data.all_keymap);
+ HASHMAP_DESTROY(&g_lsp_data.clients);
+}
+
+struct lsp_server *lsp_server_for_buffer(struct buffer *buffer) {
+ return lsp_server_for_lang_id(buffer->lang.id);
+}
+
+struct lsp_diagnostics *lsp_server_diagnostics(struct lsp_server *server) {
+ return server->diagnostics;
}
diff --git a/src/main/lsp.h b/src/main/lsp.h
index 736282d..27d8c93 100644
--- a/src/main/lsp.h
+++ b/src/main/lsp.h
@@ -1,11 +1,53 @@
#ifndef _MAIN_LSP_H
#define _MAIN_LSP_H
+#include <stddef.h>
+
+#include "dged/location.h"
+#include "dged/lsp.h"
+#include "dged/s8.h"
+#include "dged/vec.h"
+
+#include "lsp/types.h"
+
struct reactor;
struct buffers;
+struct commands;
-void lang_servers_init(struct reactor *reactor, struct buffers *buffers);
+void lang_servers_init(struct reactor *reactor, struct buffers *buffers,
+ struct commands *commands);
void lang_servers_update(void);
void lang_servers_teardown(void);
+struct lsp_server;
+struct buffer;
+struct workspace_edit;
+
+struct lsp_server *lsp_server_for_lang_id(const char *id);
+struct lsp_server *lsp_server_for_buffer(struct buffer *buffer);
+
+void lsp_server_reload(struct lsp_server *server);
+void lsp_server_shutdown(struct lsp_server *server);
+struct lsp *lsp_backend(struct lsp_server *server);
+
+bool apply_edits(struct lsp_server *server,
+ const struct workspace_edit *ws_edit);
+
+void apply_edits_buffer(struct lsp_server *, struct buffer *, text_edit_vec,
+ struct location *);
+
+typedef void (*response_handler)(struct lsp_server *, struct lsp_response *,
+ void *);
+uint64_t new_pending_request(struct lsp_server *server,
+ response_handler handler, void *userdata);
+
+struct region lsp_range_to_coordinates(struct lsp_server *server,
+ struct buffer *buffer,
+ struct region range);
+
+struct region region_to_lsp(struct buffer *buffer, struct region region,
+ struct lsp_server *server);
+
+struct lsp_diagnostics *lsp_server_diagnostics(struct lsp_server *server);
+
#endif
diff --git a/src/main/lsp/actions.c b/src/main/lsp/actions.c
new file mode 100644
index 0000000..ea792a1
--- /dev/null
+++ b/src/main/lsp/actions.c
@@ -0,0 +1,129 @@
+#include "actions.h"
+
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/lsp.h"
+#include "dged/minibuffer.h"
+#include "dged/window.h"
+#include "main/lsp.h"
+#include "main/lsp/diagnostics.h"
+
+#include "choice-buffer.h"
+#include "types.h"
+
+static struct code_actions g_code_actions_result = {};
+
+static void code_action_command_selected(void *selected, void *userdata) {
+ struct lsp_server *server = (struct lsp_server *)userdata;
+ struct lsp_command *command = (struct lsp_command *)selected;
+ struct s8 json_payload = lsp_command_to_json(command);
+
+ uint64_t id = new_pending_request(server, NULL, NULL);
+ lsp_send(
+ lsp_backend(server),
+ lsp_create_request(id, s8("workspace/executeCommand"), json_payload));
+
+ s8delete(json_payload);
+}
+
+static void code_action_selected(void *selected, void *userdata) {
+ struct lsp_server *server = (struct lsp_server *)userdata;
+ struct code_action *action = (struct code_action *)selected;
+
+ if (action->has_edit) {
+ apply_edits(server, &action->edit);
+ }
+
+ if (action->has_command) {
+ struct s8 json_payload = lsp_command_to_json(&action->command);
+
+ uint64_t id = new_pending_request(server, NULL, NULL);
+ lsp_send(
+ lsp_backend(server),
+ lsp_create_request(id, s8("workspace/executeCommand"), json_payload));
+ s8delete(json_payload);
+ }
+}
+
+static void code_action_closed(void *userdata) {
+ (void)userdata;
+ lsp_code_actions_free(&g_code_actions_result);
+}
+
+static void handle_code_actions_response(struct lsp_server *server,
+ struct lsp_response *response,
+ void *userdata) {
+ struct code_actions actions =
+ lsp_code_actions_from_json(&response->value.result);
+
+ struct buffers *buffers = (struct buffers *)userdata;
+
+ if (VEC_SIZE(&actions.commands) == 0 &&
+ VEC_SIZE(&actions.code_actions) == 0) {
+ minibuffer_echo_timeout(4, "no code actions available");
+ lsp_code_actions_free(&actions);
+ } else {
+ g_code_actions_result = actions;
+ struct choice_buffer *buf =
+ choice_buffer_create(s8("Code Actions"), buffers, code_action_selected,
+ code_action_closed, NULL, server);
+
+ VEC_FOR_EACH(&actions.code_actions, struct code_action * action) {
+ struct s8 line =
+ s8from_fmt("%.*s, (%.*s)", action->title.l, action->title.s,
+ action->kind.l, action->kind.s);
+ choice_buffer_add_choice_with_callback(buf, line, action,
+ code_action_selected);
+ s8delete(line);
+ }
+
+ VEC_FOR_EACH(&actions.commands, struct lsp_command * command) {
+ struct s8 line = s8from_fmt("%.*s", command->title.l, command->title.s);
+ choice_buffer_add_choice_with_callback(buf, line, command,
+ code_action_command_selected);
+ s8delete(line);
+ }
+ }
+}
+
+int32_t code_actions_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(windows_get_active());
+
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ return 0;
+ }
+
+ uint64_t id =
+ new_pending_request(server, handle_code_actions_response, ctx.buffers);
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(bv->buffer);
+ struct code_action_params params = {
+ .text_document.uri = doc.uri,
+ .range = region_new(bv->dot, bv->dot),
+ };
+
+ VEC_INIT(&params.context.diagnostics, 8);
+
+ diagnostic_vec *d =
+ diagnostics_for_buffer(lsp_server_diagnostics(server), bv->buffer);
+ if (d != NULL) {
+ VEC_FOR_EACH(d, struct diagnostic * diag) {
+ if (location_is_between(bv->dot, diag->region.begin, diag->region.end)) {
+ VEC_PUSH(&params.context.diagnostics, *diag);
+ }
+ }
+ }
+
+ struct s8 json_payload = code_action_params_to_json(&params);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/codeAction"), json_payload));
+
+ VEC_DESTROY(&params.context.diagnostics);
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+ return 0;
+}
diff --git a/src/main/lsp/actions.h b/src/main/lsp/actions.h
new file mode 100644
index 0000000..59b4d36
--- /dev/null
+++ b/src/main/lsp/actions.h
@@ -0,0 +1,10 @@
+#ifndef _ACTIONS_H
+#define _ACTIONS_H
+
+#include <stdint.h>
+
+#include "dged/command.h"
+
+int32_t code_actions_cmd(struct command_ctx, int, const char **);
+
+#endif
diff --git a/src/main/lsp/choice-buffer.c b/src/main/lsp/choice-buffer.c
new file mode 100644
index 0000000..44186bd
--- /dev/null
+++ b/src/main/lsp/choice-buffer.c
@@ -0,0 +1,201 @@
+#include "choice-buffer.h"
+
+#include "dged/binding.h"
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/buffers.h"
+#include "dged/command.h"
+#include "dged/display.h"
+#include "dged/location.h"
+
+#include "main/bindings.h"
+
+struct choice {
+ struct region region;
+ void *data;
+ select_callback callback;
+};
+
+struct choice_buffer {
+ struct buffers *buffers;
+ struct buffer *buffer;
+ VEC(struct choice) choices;
+
+ abort_callback abort_cb;
+ select_callback select_cb;
+ update_callback update_cb;
+ void *userdata;
+
+ uint32_t buffer_removed_hook;
+
+ struct command enter_pressed;
+ struct command q_pressed;
+};
+
+static void delete_choice_buffer(struct choice_buffer *buffer,
+ bool delete_underlying);
+
+static void underlying_buffer_destroyed(struct buffer *buffer,
+ void *choice_buffer) {
+ (void)buffer;
+ struct choice_buffer *cb = (struct choice_buffer *)choice_buffer;
+
+ // run this with false since the underlying buffer is already
+ // being deleted
+ delete_choice_buffer(cb, false);
+}
+
+static int32_t enter_pressed_fn(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+ struct choice_buffer *cb = (struct choice_buffer *)ctx.userdata;
+ struct window *w = window_find_by_buffer(cb->buffer);
+ if (w == NULL) {
+ return 0;
+ }
+
+ struct buffer_view *bv = window_buffer_view(w);
+
+ VEC_FOR_EACH(&cb->choices, struct choice * choice) {
+ if (location_is_between(bv->dot, choice->region.begin,
+ choice->region.end)) {
+ if (choice->callback != NULL) {
+ choice->callback(choice->data, cb->userdata);
+ } else {
+ cb->select_cb(choice->data, cb->userdata);
+ }
+
+ delete_choice_buffer(cb, true);
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int32_t choice_buffer_close_fn(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct choice_buffer *cb = (struct choice_buffer *)ctx.userdata;
+ delete_choice_buffer(cb, true);
+ return 0;
+}
+
+struct choice_buffer *
+choice_buffer_create(struct s8 title, struct buffers *buffers,
+ select_callback selected, abort_callback aborted,
+ update_callback update, void *userdata) {
+
+ struct choice_buffer *b = calloc(1, sizeof(struct choice_buffer));
+ VEC_INIT(&b->choices, 16);
+ b->select_cb = selected;
+ b->abort_cb = aborted;
+ b->update_cb = update;
+ b->userdata = userdata;
+ b->buffers = buffers;
+
+ // set up
+ struct buffer buf = buffer_create("*something-choices*");
+ buf.lazy_row_add = false;
+ buf.retain_properties = true;
+ b->buffer = buffers_add(b->buffers, buf);
+ // TODO: error?
+ b->buffer_removed_hook =
+ buffer_add_destroy_hook(b->buffer, underlying_buffer_destroyed, b);
+
+ b->enter_pressed = (struct command){
+ .name = "choice-buffer-enter",
+ .fn = enter_pressed_fn,
+ .userdata = b,
+ };
+
+ b->q_pressed = (struct command){
+ .name = "choice-buffer-close",
+ .fn = choice_buffer_close_fn,
+ .userdata = b,
+ };
+
+ struct binding bindings[] = {
+ ANONYMOUS_BINDING(ENTER, &b->enter_pressed),
+ ANONYMOUS_BINDING(None, 'q', &b->q_pressed),
+ };
+
+ struct keymap km = keymap_create("choice_buffer", 8);
+ keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0]));
+ buffer_add_keymap(b->buffer, km);
+
+ struct location begin = buffer_end(b->buffer);
+ buffer_add(b->buffer, buffer_end(b->buffer), title.s, title.l);
+ buffer_newline(b->buffer, buffer_end(b->buffer));
+ buffer_add(b->buffer, buffer_end(b->buffer), (uint8_t *)"----------------",
+ 16);
+ struct location end = buffer_end(b->buffer);
+ buffer_add_text_property(b->buffer, begin, end,
+ (struct text_property){
+ .type = TextProperty_Colors,
+ .data.colors =
+ (struct text_property_colors){
+ .set_fg = true,
+ .fg = Color_Cyan,
+ },
+ });
+ buffer_newline(b->buffer, buffer_end(b->buffer));
+ buffer_newline(b->buffer, buffer_end(b->buffer));
+
+ struct window *w = windows_get_active();
+
+ window_set_buffer(w, b->buffer);
+ struct buffer_view *bv = window_buffer_view(w);
+ bv->dot = buffer_end(b->buffer);
+
+ buffer_set_readonly(b->buffer, true);
+
+ return b;
+}
+
+void choice_buffer_add_choice(struct choice_buffer *buffer, struct s8 text,
+ void *data) {
+ buffer_set_readonly(buffer->buffer, false);
+ VEC_APPEND(&buffer->choices, struct choice * new_choice);
+
+ new_choice->data = data;
+ new_choice->callback = NULL;
+ new_choice->region.begin = buffer_end(buffer->buffer);
+ buffer_add(buffer->buffer, buffer_end(buffer->buffer), (uint8_t *)"- ", 2);
+ buffer_add(buffer->buffer, buffer_end(buffer->buffer), text.s, text.l);
+ new_choice->region.end = buffer_end(buffer->buffer);
+ buffer_newline(buffer->buffer, buffer_end(buffer->buffer));
+ buffer_set_readonly(buffer->buffer, false);
+}
+
+void choice_buffer_add_choice_with_callback(struct choice_buffer *buffer,
+ struct s8 text, void *data,
+ select_callback callback) {
+ buffer_set_readonly(buffer->buffer, false);
+ VEC_APPEND(&buffer->choices, struct choice * new_choice);
+
+ new_choice->data = data;
+ new_choice->callback = callback;
+ new_choice->region.begin = buffer_end(buffer->buffer);
+ buffer_add(buffer->buffer, buffer_end(buffer->buffer), (uint8_t *)"- ", 2);
+ buffer_add(buffer->buffer, buffer_end(buffer->buffer), text.s, text.l);
+ new_choice->region.end = buffer_end(buffer->buffer);
+ buffer_newline(buffer->buffer, buffer_end(buffer->buffer));
+ buffer_set_readonly(buffer->buffer, false);
+}
+
+static void delete_choice_buffer(struct choice_buffer *buffer,
+ bool delete_underlying) {
+ buffer->abort_cb(buffer->userdata);
+ VEC_DESTROY(&buffer->choices);
+ if (delete_underlying) {
+ buffer_remove_destroy_hook(buffer->buffer, buffer->buffer_removed_hook,
+ NULL);
+ buffers_remove(buffer->buffers, buffer->buffer->name);
+ }
+
+ free(buffer);
+}
diff --git a/src/main/lsp/choice-buffer.h b/src/main/lsp/choice-buffer.h
new file mode 100644
index 0000000..c2a7c33
--- /dev/null
+++ b/src/main/lsp/choice-buffer.h
@@ -0,0 +1,23 @@
+#ifndef _CHOICE_BUFFER_H
+#define _CHOICE_BUFFER_H
+
+#include "dged/s8.h"
+
+typedef void (*abort_callback)(void *);
+typedef void (*select_callback)(void *, void *);
+typedef void (*update_callback)(void *);
+
+struct choice_buffer;
+struct buffers;
+
+struct choice_buffer *
+choice_buffer_create(struct s8 title, struct buffers *buffers,
+ select_callback selected, abort_callback aborted,
+ update_callback update, void *userdata);
+void choice_buffer_add_choice(struct choice_buffer *buffer, struct s8 text,
+ void *data);
+void choice_buffer_add_choice_with_callback(struct choice_buffer *buffer,
+ struct s8 text, void *data,
+ select_callback callback);
+
+#endif
diff --git a/src/main/lsp/completion.c b/src/main/lsp/completion.c
new file mode 100644
index 0000000..df89255
--- /dev/null
+++ b/src/main/lsp/completion.c
@@ -0,0 +1,405 @@
+#include "completion.h"
+
+#include <stddef.h>
+
+#include "dged/s8.h"
+#include "dged/vec.h"
+#include "types.h"
+
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/minibuffer.h"
+#include "main/completion.h"
+#include "main/lsp.h"
+
+struct completion_ctx {
+ struct lsp_server *server;
+ struct completion_context comp_ctx;
+ struct completion_list completions;
+ struct s8 cached_with;
+ struct completion *completion_data;
+ uint64_t last_request;
+
+ triggerchar_vec trigger_chars;
+};
+
+struct symbol {
+ struct s8 symbol;
+ struct region region;
+};
+
+static struct symbol current_symbol(struct buffer *buffer, struct location at) {
+ struct region word = buffer_word_at(buffer, at);
+ if (!region_has_size(word)) {
+ return (struct symbol){
+ .symbol =
+ (struct s8){
+ .s = NULL,
+ .l = 0,
+ },
+ .region = word,
+ };
+ };
+
+ struct text_chunk line = buffer_region(buffer, region_new(word.begin, at));
+ struct s8 symbol = s8new((const char *)line.text, line.nbytes);
+
+ if (line.allocated) {
+ free(line.text);
+ }
+
+ return (struct symbol){.symbol = symbol, .region = word};
+}
+
+struct completion_ctx *create_completion_ctx(struct lsp_server *server,
+ triggerchar_vec *trigger_chars) {
+ struct completion_ctx *ctx =
+ (struct completion_ctx *)calloc(1, sizeof(struct completion_ctx));
+
+ ctx->server = server;
+ ctx->completion_data = NULL;
+ ctx->completions.incomplete = false;
+ ctx->cached_with.s = NULL;
+ ctx->cached_with.l = 0;
+ VEC_INIT(&ctx->completions.items, 0);
+
+ VEC_INIT(&ctx->trigger_chars, VEC_SIZE(trigger_chars));
+ VEC_FOR_EACH(trigger_chars, struct s8 * s) {
+ VEC_PUSH(&ctx->trigger_chars, s8dup(*s));
+ }
+
+ return ctx;
+}
+
+void destroy_completion_ctx(struct completion_ctx *ctx) {
+ completion_list_free(&ctx->completions);
+
+ if (ctx->completion_data != NULL) {
+ free(ctx->completion_data);
+ }
+
+ s8delete(ctx->cached_with);
+
+ VEC_FOR_EACH(&ctx->trigger_chars, struct s8 * s) { s8delete(*s); }
+ VEC_DESTROY(&ctx->trigger_chars);
+
+ free(ctx);
+}
+
+static char *item_kind_to_str(enum completion_item_kind kind) {
+ switch (kind) {
+ case CompletionItem_Text:
+ return "tx";
+
+ case CompletionItem_Method:
+ return "mth";
+
+ case CompletionItem_Function:
+ return "fn";
+
+ case CompletionItem_Constructor:
+ return "cons";
+
+ case CompletionItem_Field:
+ return "field";
+
+ case CompletionItem_Variable:
+ return "var";
+
+ case CompletionItem_Class:
+ return "cls";
+
+ case CompletionItem_Interface:
+ return "iface";
+
+ case CompletionItem_Module:
+ return "mod";
+
+ case CompletionItem_Property:
+ return "prop";
+
+ case CompletionItem_Unit:
+ return "unit";
+
+ case CompletionItem_Value:
+ return "val";
+
+ case CompletionItem_Enum:
+ return "enum";
+
+ case CompletionItem_Keyword:
+ return "kw";
+
+ case CompletionItem_Snippet:
+ return "snp";
+
+ case CompletionItem_Color:
+ return "col";
+
+ case CompletionItem_File:
+ return "file";
+
+ case CompletionItem_Reference:
+ return "ref";
+
+ case CompletionItem_Folder:
+ return "fld";
+
+ case CompletionItem_EnumMember:
+ return "em";
+
+ case CompletionItem_Constant:
+ return "const";
+
+ case CompletionItem_Struct:
+ return "struct";
+
+ case CompletionItem_Event:
+ return "ev";
+
+ case CompletionItem_Operator:
+ return "op";
+
+ case CompletionItem_TypeParameter:
+ return "tp";
+
+ default:
+ return "";
+ }
+}
+
+static struct region lsp_item_render(void *data, struct buffer *buffer) {
+ struct lsp_completion_item *item = (struct lsp_completion_item *)data;
+ struct location begin = buffer_end(buffer);
+ struct s8 kind_str = s8from_fmt("(%s)", item_kind_to_str(item->kind));
+ struct s8 txt = s8from_fmt("%-8.*s%.*s", kind_str.l, kind_str.s,
+ item->label.l, item->label.s);
+ struct location end = buffer_add(buffer, begin, txt.s, txt.l);
+ s8delete(txt);
+ s8delete(kind_str);
+ buffer_newline(buffer, buffer_end(buffer));
+
+ return region_new(begin, end);
+}
+
+static void lsp_item_selected(void *data, struct buffer_view *view) {
+ struct lsp_completion_item *item = (struct lsp_completion_item *)data;
+ struct buffer *buffer = view->buffer;
+ struct lsp_server *lsp_server = lsp_server_for_buffer(buffer);
+
+ abort_completion();
+
+ if (lsp_server == NULL) {
+ return;
+ }
+
+ switch (item->edit_type) {
+ case TextEdit_None: {
+ struct symbol symbol = current_symbol(buffer, view->dot);
+ struct s8 insert = item->insert_text;
+
+ // FIXME: why does this happen?
+ if (symbol.symbol.l >= insert.l) {
+ s8delete(symbol.symbol);
+ return;
+ }
+
+ if (symbol.symbol.l > 0) {
+ insert.s += symbol.symbol.l;
+ insert.l -= symbol.symbol.l;
+ }
+
+ s8delete(symbol.symbol);
+
+ struct location at = buffer_add(buffer, view->dot, insert.s, insert.l);
+ buffer_view_goto(view, at);
+
+ } break;
+ case TextEdit_TextEdit: {
+ struct text_edit *ed = &item->edit.text_edit;
+ struct region reg = lsp_range_to_coordinates(lsp_server, buffer, ed->range);
+ struct location at = reg.begin;
+ if (!region_is_inside(reg, view->dot)) {
+ reg.end = view->dot;
+ }
+
+ if (region_has_size(reg)) {
+ at = buffer_delete(buffer, reg);
+ }
+
+ at = buffer_add(buffer, at, ed->new_text.s, ed->new_text.l);
+ buffer_view_goto(view, at);
+ } break;
+
+ case TextEdit_InsertReplaceEdit: {
+ struct insert_replace_edit *ed = &item->edit.insert_replace_edit;
+ struct region reg =
+ lsp_range_to_coordinates(lsp_server, buffer, ed->replace);
+
+ if (!region_is_inside(reg, view->dot)) {
+ reg.end = view->dot;
+ }
+
+ if (region_has_size(reg)) {
+ buffer_delete(buffer, reg);
+ }
+
+ struct location at =
+ buffer_add(buffer, ed->insert.begin, ed->new_text.s, ed->new_text.l);
+ buffer_view_goto(view, at);
+ } break;
+ }
+
+ if (!VEC_EMPTY(&item->additional_text_edits)) {
+ apply_edits_buffer(lsp_server, view->buffer, item->additional_text_edits,
+ &view->dot);
+ }
+}
+
+static void lsp_item_cleanup(void *data) { (void)data; }
+
+static struct s8 get_filter_text(struct lsp_completion_item *item) {
+ return item->filter_text.l > 0 ? item->filter_text : item->label;
+}
+
+static void fill_completions(struct completion_ctx *lsp_ctx, struct s8 needle) {
+ if (lsp_ctx->completion_data != NULL) {
+ free(lsp_ctx->completion_data);
+ lsp_ctx->completion_data = NULL;
+ }
+
+ size_t ncomps = VEC_SIZE(&lsp_ctx->completions.items);
+
+ // if there is more than a single item or the user has not typed that
+ // single item exactly, then add to the list of completions.
+ lsp_ctx->completion_data = calloc(ncomps, sizeof(struct completion));
+
+ ncomps = 0;
+ VEC_FOR_EACH(&lsp_ctx->completions.items,
+ struct lsp_completion_item * lsp_item) {
+ struct s8 filter_text = get_filter_text(lsp_item);
+ if (needle.l == 0 || s8startswith(filter_text, needle)) {
+ struct completion *c = &lsp_ctx->completion_data[ncomps];
+
+ c->data = lsp_item;
+ c->render = lsp_item_render;
+ c->selected = lsp_item_selected;
+ c->cleanup = lsp_item_cleanup;
+ ++ncomps;
+ }
+ }
+
+ // if there is only a single item that matches the needle exactly,
+ // don't add it to the list since the user has already won
+ if (ncomps == 1 && needle.l > 0 &&
+ s8eq(get_filter_text(lsp_ctx->completion_data[0].data), needle)) {
+ return;
+ }
+
+ if (ncomps > 0) {
+ lsp_ctx->comp_ctx.add_completions(lsp_ctx->completion_data, ncomps);
+ }
+}
+
+static void handle_completion_response(struct lsp_server *server,
+ struct lsp_response *response,
+ void *userdata) {
+ (void)server;
+ struct completion_ctx *lsp_ctx = (struct completion_ctx *)userdata;
+
+ if (response->id != lsp_ctx->last_request) {
+ // discard any old requests
+ return;
+ }
+
+ completion_list_free(&lsp_ctx->completions);
+ lsp_ctx->completions = completion_list_from_json(&response->value.result);
+
+ fill_completions(lsp_ctx, lsp_ctx->cached_with);
+}
+
+static void complete_with_lsp(struct completion_context ctx, bool deletion,
+ void *userdata) {
+ (void)deletion;
+ struct completion_ctx *lsp_ctx = (struct completion_ctx *)userdata;
+ lsp_ctx->comp_ctx = ctx;
+
+ struct symbol sym = current_symbol(ctx.buffer, ctx.location);
+ struct s8 symbol = sym.symbol;
+
+ // check if the symbol is too short for triggering completion
+ bool should_activate =
+ (symbol.l >= 3 || completion_active()) && !s8onlyws(symbol);
+
+ // use trigger chars as an alternative activation condition
+ if (!should_activate) {
+ struct location begin = buffer_previous_char(ctx.buffer, ctx.location);
+ struct location end = begin;
+ end.col += 4;
+ struct text_chunk txt = buffer_region(ctx.buffer, region_new(begin, end));
+ struct s8 t = {
+ .s = txt.text,
+ .l = txt.nbytes,
+ };
+
+ VEC_FOR_EACH(&lsp_ctx->trigger_chars, struct s8 * tc) {
+ if (s8startswith(t, *tc)) {
+ should_activate = true;
+ goto done;
+ }
+ }
+ done:
+ if (txt.allocated) {
+ free(txt.text);
+ }
+ }
+
+ // if we still should not activate, we give up
+ if (!should_activate) {
+ s8delete(symbol);
+ return;
+ }
+
+ bool has_completions = !VEC_EMPTY(&lsp_ctx->completions.items);
+ if (completion_active() && has_completions &&
+ !lsp_ctx->completions.incomplete && !s8empty(lsp_ctx->cached_with) &&
+ s8startswith(symbol, lsp_ctx->cached_with)) {
+ fill_completions(lsp_ctx, symbol);
+ } else {
+ uint64_t id = new_pending_request(lsp_ctx->server,
+ handle_completion_response, lsp_ctx);
+ lsp_ctx->last_request = id;
+
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(ctx.buffer);
+ struct text_document_position params = {
+ .uri = doc.uri,
+ .position = ctx.location,
+ };
+
+ s8delete(lsp_ctx->cached_with);
+ lsp_ctx->cached_with = s8dup(symbol);
+
+ struct s8 json_payload = document_position_to_json(&params);
+ lsp_send(
+ lsp_backend(lsp_ctx->server),
+ lsp_create_request(id, s8("textDocument/completion"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+ }
+
+ s8delete(symbol);
+}
+
+void enable_completion_for_buffer(struct completion_ctx *ctx,
+ struct buffer *buffer) {
+ struct completion_provider prov = {
+ .name = "lsp",
+ .complete = complete_with_lsp,
+ .userdata = ctx,
+ };
+ struct completion_provider providers[] = {prov};
+
+ add_completion_providers(buffer, providers, 1);
+}
diff --git a/src/main/lsp/completion.h b/src/main/lsp/completion.h
new file mode 100644
index 0000000..f3c51c0
--- /dev/null
+++ b/src/main/lsp/completion.h
@@ -0,0 +1,18 @@
+#ifndef _LSP_COMPLETION_H
+#define _LSP_COMPLETION_H
+
+#include "dged/vec.h"
+
+struct completion_ctx;
+struct buffer;
+struct lsp_server;
+
+typedef VEC(struct s8) triggerchar_vec;
+
+struct completion_ctx *create_completion_ctx(struct lsp_server *server,
+ triggerchar_vec *trigger_chars);
+void destroy_completion_ctx(struct completion_ctx *);
+
+void enable_completion_for_buffer(struct completion_ctx *, struct buffer *);
+
+#endif
diff --git a/src/main/lsp/diagnostics.c b/src/main/lsp/diagnostics.c
new file mode 100644
index 0000000..fbab4c0
--- /dev/null
+++ b/src/main/lsp/diagnostics.c
@@ -0,0 +1,386 @@
+#include "diagnostics.h"
+
+#include "dged/binding.h"
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/buffers.h"
+#include "dged/location.h"
+#include "dged/lsp.h"
+#include "dged/minibuffer.h"
+#include "dged/vec.h"
+#include "main/bindings.h"
+#include "main/lsp.h"
+
+struct lsp_buffer_diagnostics {
+ struct buffer *buffer;
+ diagnostic_vec diagnostics;
+};
+
+#define DIAGNOSTIC_BUFNAME "*lsp-diagnostics*"
+
+typedef VEC(struct lsp_buffer_diagnostics) buffer_diagnostics_vec;
+
+struct lsp_diagnostics {
+ buffer_diagnostics_vec buffer_diagnostics;
+};
+
+struct diagnostic_region {
+ struct diagnostic *diagnostic;
+ struct region region;
+};
+
+struct active_diagnostics {
+ struct buffer *buffer;
+ VEC(struct diagnostic_region) diag_regions;
+};
+
+static struct active_diagnostics g_active_diagnostic;
+
+static struct s8 diagnostics_modeline(struct buffer_view *view,
+ void *userdata) {
+ struct lsp_diagnostics *diag = (struct lsp_diagnostics *)userdata;
+
+ diagnostic_vec *diags = diagnostics_for_buffer(diag, view->buffer);
+
+ size_t nerrs = 0, nwarn = 0;
+ if (diags != NULL) {
+ VEC_FOR_EACH(diags, struct diagnostic * d) {
+ if (d->severity == LspDiagnostic_Error) {
+ ++nerrs;
+ } else if (d->severity == LspDiagnostic_Warning) {
+ ++nwarn;
+ }
+ }
+
+ return s8from_fmt("E: %d, W: %d", nerrs, nwarn);
+ }
+
+ return s8("");
+}
+
+struct lsp_diagnostics *diagnostics_create(void) {
+ struct lsp_diagnostics *d = calloc(1, sizeof(struct lsp_diagnostics));
+
+ VEC_INIT(&d->buffer_diagnostics, 16);
+ buffer_view_add_modeline_hook(diagnostics_modeline, d);
+ VEC_INIT(&g_active_diagnostic.diag_regions, 0);
+ g_active_diagnostic.buffer = NULL;
+ return d;
+}
+
+void diagnostics_destroy(struct lsp_diagnostics *d) {
+ VEC_FOR_EACH(&d->buffer_diagnostics, struct lsp_buffer_diagnostics * diag) {
+ VEC_FOR_EACH(&diag->diagnostics, struct diagnostic * d) {
+ diagnostic_free(d);
+ }
+ VEC_DESTROY(&diag->diagnostics);
+ }
+
+ VEC_DESTROY(&d->buffer_diagnostics);
+ VEC_DESTROY(&g_active_diagnostic.diag_regions);
+ free(d);
+}
+
+diagnostic_vec *diagnostics_for_buffer(struct lsp_diagnostics *d,
+ struct buffer *buffer) {
+ VEC_FOR_EACH(&d->buffer_diagnostics, struct lsp_buffer_diagnostics * diag) {
+ if (diag->buffer == buffer) {
+ return &diag->diagnostics;
+ }
+ }
+
+ return NULL;
+}
+
+static int32_t diagnostics_goto_fn(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer *db = buffers_find(ctx.buffers, DIAGNOSTIC_BUFNAME);
+ if (db == NULL) {
+ return 0;
+ }
+
+ struct window *w = window_find_by_buffer(db);
+ if (w == NULL) {
+ return 0;
+ }
+
+ if (g_active_diagnostic.buffer == NULL) {
+ return 0;
+ }
+
+ struct buffer_view *bv = window_buffer_view(w);
+
+ VEC_FOR_EACH(&g_active_diagnostic.diag_regions,
+ struct diagnostic_region * reg) {
+ if (region_is_inside(reg->region, bv->dot)) {
+ struct window *target_win =
+ window_find_by_buffer(g_active_diagnostic.buffer);
+ if (target_win == NULL) {
+ // if the buffer is not open, reuse the diagnostic buffer
+ target_win = w;
+ window_set_buffer(target_win, g_active_diagnostic.buffer);
+ }
+
+ buffer_view_goto(window_buffer_view(target_win),
+ reg->diagnostic->region.begin);
+ windows_set_active(target_win);
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int32_t diagnostics_close_fn(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ if (window_has_prev_buffer_view(ctx.active_window)) {
+ window_set_buffer(ctx.active_window,
+ window_prev_buffer_view(ctx.active_window)->buffer);
+ }
+
+ return 0;
+}
+
+static struct buffer *update_diagnostics_buffer(struct lsp_server *server,
+ struct buffers *buffers,
+ diagnostic_vec diagnostics,
+ struct buffer *buffer) {
+ char buf[2048];
+ struct buffer *db = buffers_find(buffers, DIAGNOSTIC_BUFNAME);
+ if (db == NULL) {
+ struct buffer buf = buffer_create(DIAGNOSTIC_BUFNAME);
+ buf.lazy_row_add = false;
+ buf.retain_properties = true;
+ db = buffers_add(buffers, buf);
+
+ static struct command diagnostics_goto = {
+ .name = "diagnostics-goto",
+ .fn = diagnostics_goto_fn,
+ };
+
+ static struct command diagnostics_close = {
+ .name = "diagnostics-close",
+ .fn = diagnostics_close_fn,
+ };
+
+ struct binding bindings[] = {
+ ANONYMOUS_BINDING(ENTER, &diagnostics_goto),
+ ANONYMOUS_BINDING(None, 'q', &diagnostics_close),
+ };
+ struct keymap km = keymap_create("diagnostics", 8);
+ keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0]));
+ buffer_add_keymap(db, km);
+ }
+ buffer_set_readonly(db, false);
+ buffer_clear(db);
+ buffer_clear_text_properties(db);
+
+ g_active_diagnostic.buffer = buffer;
+ ssize_t len = snprintf(buf, 2048, "Diagnostics for %s:\n\n", buffer->name);
+ if (len != -1) {
+ buffer_add(db, buffer_end(db), (uint8_t *)buf, len);
+ buffer_add_text_property(
+ db, (struct location){.line = 0, .col = 0},
+ (struct location){.line = 1, .col = 0},
+ (struct text_property){.type = TextProperty_Colors,
+ .data.colors.underline = true});
+ }
+
+ VEC_DESTROY(&g_active_diagnostic.diag_regions);
+ VEC_INIT(&g_active_diagnostic.diag_regions, VEC_SIZE(&diagnostics));
+ VEC_FOR_EACH(&diagnostics, struct diagnostic * diag) {
+ struct location start = buffer_end(db);
+ char src[128];
+ size_t srclen = snprintf(src, 128, "%.*s%s", diag->source.l, diag->source.s,
+ diag->source.l > 0 ? ": " : "");
+ const char *severity_str = diag_severity_to_str(diag->severity);
+ size_t severity_str_len = strlen(severity_str);
+ struct region reg = lsp_range_to_coordinates(server, buffer, diag->region);
+ len = snprintf(buf, 2048,
+ "%s%s [%d, %d]: %.*s\n-------------------------------", src,
+ severity_str, reg.begin.line + 1, reg.begin.col,
+ diag->message.l, diag->message.s);
+
+ if (len != -1) {
+ buffer_add(db, buffer_end(db), (uint8_t *)buf, len);
+
+ struct location srcend = start;
+ srcend.col += srclen - 3;
+ buffer_add_text_property(
+ db, start, srcend,
+ (struct text_property){.type = TextProperty_Colors,
+ .data.colors.underline = true});
+
+ uint32_t color = diag_severity_color(diag->severity);
+ struct location sevstart = start;
+ sevstart.col += srclen;
+ struct location sevend = sevstart;
+ sevend.col += severity_str_len;
+ buffer_add_text_property(
+ db, sevstart, sevend,
+ (struct text_property){.type = TextProperty_Colors,
+ .data.colors.set_fg = true,
+ .data.colors.fg = color});
+
+ VEC_PUSH(&g_active_diagnostic.diag_regions,
+ ((struct diagnostic_region){
+ .diagnostic = diag,
+ .region = region_new(start, buffer_end(db)),
+ }));
+
+ buffer_newline(db, buffer_end(db));
+ }
+ }
+
+ buffer_set_readonly(db, true);
+ return db;
+}
+
+void handle_publish_diagnostics(struct lsp_server *server,
+ struct buffers *buffers,
+ struct lsp_notification *notification) {
+ struct publish_diagnostics_params params =
+ diagnostics_from_json(&notification->params);
+ if (s8startswith(params.uri, s8("file://"))) {
+ const char *p = s8tocstr(params.uri);
+ struct buffer *b = buffers_find_by_filename(buffers, &p[7]);
+ free((void *)p);
+
+ if (b != NULL) {
+ struct lsp_diagnostics *ld = lsp_server_diagnostics(server);
+ diagnostic_vec *diagnostics = diagnostics_for_buffer(ld, b);
+ if (diagnostics == NULL) {
+ VEC_APPEND(&ld->buffer_diagnostics,
+ struct lsp_buffer_diagnostics * new_diag);
+ new_diag->buffer = b;
+ new_diag->diagnostics.nentries = 0;
+ new_diag->diagnostics.capacity = 0;
+ new_diag->diagnostics.temp = NULL;
+ new_diag->diagnostics.entries = NULL;
+
+ diagnostics = &new_diag->diagnostics;
+ }
+
+ VEC_FOR_EACH(diagnostics, struct diagnostic * diag) {
+ diagnostic_free(diag);
+ }
+ VEC_DESTROY(diagnostics);
+
+ *diagnostics = params.diagnostics;
+ update_diagnostics_buffer(server, buffers, *diagnostics, b);
+ } else {
+ VEC_FOR_EACH(&params.diagnostics, struct diagnostic * diag) {
+ diagnostic_free(diag);
+ }
+ VEC_DESTROY(&params.diagnostics);
+ message("failed to find buffer with URI: %.*s", params.uri.l,
+ params.uri.s);
+ }
+ } else {
+ message("warning: unsupported LSP URI: %.*s", params.uri.l, params.uri.s);
+ }
+
+ s8delete(params.uri);
+}
+
+static struct lsp_diagnostics *
+lsp_diagnostics_from_server(struct lsp_server *server) {
+ return lsp_server_diagnostics(server);
+}
+
+int32_t next_diagnostic_cmd(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)ctx;
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(windows_get_active());
+
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ return 0;
+ }
+
+ diagnostic_vec *diagnostics =
+ diagnostics_for_buffer(lsp_diagnostics_from_server(server), bv->buffer);
+ if (diagnostics == NULL) {
+ return 0;
+ }
+
+ if (VEC_EMPTY(diagnostics)) {
+ minibuffer_echo_timeout(4, "no more diagnostics");
+ return 0;
+ }
+
+ VEC_FOR_EACH(diagnostics, struct diagnostic * diag) {
+ if (location_compare(bv->dot, diag->region.begin) < 0) {
+ buffer_view_goto(bv, diag->region.begin);
+ return 0;
+ }
+ }
+
+ buffer_view_goto(bv, VEC_FRONT(diagnostics)->region.begin);
+ return 0;
+}
+
+int32_t prev_diagnostic_cmd(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)ctx;
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(windows_get_active());
+
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ return 0;
+ }
+
+ diagnostic_vec *diagnostics =
+ diagnostics_for_buffer(lsp_diagnostics_from_server(server), bv->buffer);
+
+ if (diagnostics == NULL) {
+ return 0;
+ }
+
+ if (VEC_EMPTY(diagnostics)) {
+ minibuffer_echo_timeout(4, "no more diagnostics");
+ return 0;
+ }
+
+ VEC_FOR_EACH(diagnostics, struct diagnostic * diag) {
+ if (location_compare(bv->dot, diag->region.begin) > 0) {
+ buffer_view_goto(bv, diag->region.begin);
+ return 0;
+ }
+ }
+
+ buffer_view_goto(bv, VEC_BACK(diagnostics)->region.begin);
+ return 0;
+}
+
+int32_t diagnostics_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer *b = window_buffer(ctx.active_window);
+ struct lsp_server *server = lsp_server_for_buffer(b);
+
+ if (server == NULL) {
+ minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name);
+ return 0;
+ }
+
+ diagnostic_vec *d =
+ diagnostics_for_buffer(lsp_diagnostics_from_server(server), b);
+ struct buffer *db = update_diagnostics_buffer(server, ctx.buffers, *d, b);
+ window_set_buffer(ctx.active_window, db);
+
+ return 0;
+}
diff --git a/src/main/lsp/diagnostics.h b/src/main/lsp/diagnostics.h
new file mode 100644
index 0000000..4357b8e
--- /dev/null
+++ b/src/main/lsp/diagnostics.h
@@ -0,0 +1,26 @@
+#ifndef _DIAGNOSTICS_H
+#define _DIAGNOSTICS_H
+
+#include "dged/command.h"
+#include "main/lsp/types.h"
+
+struct lsp_server;
+struct buffers;
+struct lsp_notification;
+
+struct lsp_diagnostics;
+
+struct lsp_diagnostics *diagnostics_create(void);
+void diagnostics_destroy(struct lsp_diagnostics *);
+
+diagnostic_vec *diagnostics_for_buffer(struct lsp_diagnostics *,
+ struct buffer *);
+void handle_publish_diagnostics(struct lsp_server *, struct buffers *,
+ struct lsp_notification *);
+
+/* COMMANDS */
+int32_t diagnostics_cmd(struct command_ctx, int, const char **);
+int32_t next_diagnostic_cmd(struct command_ctx, int, const char **);
+int32_t prev_diagnostic_cmd(struct command_ctx, int, const char **);
+
+#endif
diff --git a/src/main/lsp/format.c b/src/main/lsp/format.c
new file mode 100644
index 0000000..2019a90
--- /dev/null
+++ b/src/main/lsp/format.c
@@ -0,0 +1,149 @@
+#include "format.h"
+
+#include "completion.h"
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/minibuffer.h"
+#include "dged/settings.h"
+#include "dged/window.h"
+#include "main/completion.h"
+#include "main/lsp.h"
+
+struct formatted_buffer {
+ struct buffer *buffer;
+ bool save;
+};
+
+static uint32_t get_tab_width(struct buffer *buffer) {
+ struct setting *tw = lang_setting(&buffer->lang, "tab-width");
+ if (tw == NULL) {
+ tw = settings_get("editor.tab-width");
+ }
+
+ uint32_t tab_width = 4;
+ if (tw != NULL && tw->value.type == Setting_Number) {
+ tab_width = tw->value.data.number_value;
+ }
+ return tab_width;
+}
+
+static bool use_tabs(struct buffer *buffer) {
+ struct setting *ut = lang_setting(&buffer->lang, "use-tabs");
+ if (ut == NULL) {
+ ut = settings_get("editor.use-tabs");
+ }
+
+ bool use_tabs = false;
+ if (ut != NULL && ut->value.type == Setting_Bool) {
+ use_tabs = ut->value.data.bool_value;
+ }
+
+ return use_tabs;
+}
+
+static struct formatting_options options_from_lang(struct buffer *buffer) {
+ return (struct formatting_options){
+ .tab_size = get_tab_width(buffer),
+ .use_spaces = !use_tabs(buffer),
+ };
+}
+
+void handle_format_response(struct lsp_server *server,
+ struct lsp_response *response, void *userdata) {
+
+ text_edit_vec edits = text_edits_from_json(&response->value.result);
+ struct formatted_buffer *buffer = (struct formatted_buffer *)userdata;
+
+ pause_completion();
+ if (!VEC_EMPTY(&edits)) {
+ apply_edits_buffer(server, buffer->buffer, edits, NULL);
+
+ if (buffer->save) {
+ buffer_to_file(buffer->buffer);
+ }
+ }
+ resume_completion();
+
+ text_edits_free(edits);
+ free(buffer);
+}
+
+static void format_buffer(struct lsp_server *server, struct buffer *buffer,
+ bool save) {
+ struct formatted_buffer *b =
+ (struct formatted_buffer *)calloc(1, sizeof(struct formatted_buffer));
+ b->buffer = buffer;
+ b->save = save;
+
+ uint64_t id = new_pending_request(server, handle_format_response, b);
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(buffer);
+
+ struct document_formatting_params params = {
+ .text_document.uri = doc.uri,
+ .options = options_from_lang(buffer),
+ };
+
+ struct s8 json_payload = document_formatting_params_to_json(&params);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/formatting"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+}
+
+void format_document(struct lsp_server *server, struct buffer *buffer) {
+ format_buffer(server, buffer, false);
+}
+
+void format_document_save(struct lsp_server *server, struct buffer *buffer) {
+ format_buffer(server, buffer, true);
+}
+
+void format_region(struct lsp_server *server, struct buffer *buffer,
+ struct region region) {
+ struct formatted_buffer *b =
+ (struct formatted_buffer *)calloc(1, sizeof(struct formatted_buffer));
+ b->buffer = buffer;
+ b->save = false;
+
+ uint64_t id = new_pending_request(server, handle_format_response, b);
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(buffer);
+
+ struct document_range_formatting_params params = {
+ .text_document.uri = doc.uri,
+ .range = region_to_lsp(buffer, region, server),
+ .options = options_from_lang(buffer),
+ };
+
+ struct s8 json_payload = document_range_formatting_params_to_json(&params);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/formatting"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+}
+
+int32_t format_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)ctx;
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(windows_get_active());
+
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ return 0;
+ }
+
+ struct region reg = region_new(bv->dot, bv->mark);
+ if (bv->mark_set && region_has_size(reg)) {
+ buffer_view_clear_mark(bv);
+ format_region(server, bv->buffer, reg);
+ } else {
+ format_document(server, bv->buffer);
+ }
+
+ return 0;
+}
diff --git a/src/main/lsp/format.h b/src/main/lsp/format.h
new file mode 100644
index 0000000..8e90ab3
--- /dev/null
+++ b/src/main/lsp/format.h
@@ -0,0 +1,18 @@
+#ifndef _FORMAT_H
+#define _FORMAT_H
+
+#include "dged/command.h"
+#include "dged/location.h"
+
+struct buffer;
+struct lsp_server;
+struct lsp_response;
+
+void format_document(struct lsp_server *, struct buffer *);
+void format_document_save(struct lsp_server *, struct buffer *);
+void format_region(struct lsp_server *, struct buffer *, struct region);
+
+/* COMMANDS */
+int32_t format_cmd(struct command_ctx, int, const char **);
+
+#endif
diff --git a/src/main/lsp/goto.c b/src/main/lsp/goto.c
new file mode 100644
index 0000000..7d2d228
--- /dev/null
+++ b/src/main/lsp/goto.c
@@ -0,0 +1,297 @@
+#include "goto.h"
+
+#include "dged/binding.h"
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/buffers.h"
+#include "dged/location.h"
+#include "dged/minibuffer.h"
+#include "dged/window.h"
+#include "main/bindings.h"
+#include "main/lsp.h"
+
+#include "choice-buffer.h"
+
+static struct jump_stack {
+ buffer_keymap_id goto_keymap_id;
+ struct buffer_location *stack;
+ uint32_t top;
+ uint32_t size;
+ struct buffers *buffers;
+} g_jump_stack;
+
+static struct location_result g_location_result = {};
+
+struct buffer_location {
+ struct buffer *buffer;
+ struct location location;
+};
+
+void init_goto(size_t jump_stack_depth, struct buffers *buffers) {
+ g_jump_stack.size = jump_stack_depth;
+ g_jump_stack.top = 0;
+ g_jump_stack.goto_keymap_id = (buffer_keymap_id)-1;
+ g_jump_stack.stack = calloc(g_jump_stack.size, sizeof(struct jump_stack));
+ g_jump_stack.buffers = buffers;
+}
+
+void destroy_goto(void) {
+ free(g_jump_stack.stack);
+ g_jump_stack.stack = NULL;
+ g_jump_stack.top = 0;
+ g_jump_stack.size = 0;
+}
+
+void lsp_jump_to(struct text_document_location loc) {
+ if (s8startswith(loc.uri, s8("file://"))) {
+ const char *p = s8tocstr(loc.uri);
+ struct buffer *b = buffers_find_by_filename(g_jump_stack.buffers, &p[7]);
+
+ if (b == NULL) {
+ struct buffer new_buf = buffer_from_file(&p[7]);
+ b = buffers_add(g_jump_stack.buffers, new_buf);
+ }
+
+ free((void *)p);
+
+ struct window *w = windows_get_active();
+
+ struct buffer_view *old_bv = window_buffer_view(w);
+ g_jump_stack.stack[g_jump_stack.top] = (struct buffer_location){
+ .buffer = old_bv->buffer,
+ .location = old_bv->dot,
+ };
+ g_jump_stack.top = (g_jump_stack.top + 1) % g_jump_stack.size;
+
+ if (old_bv->buffer != b) {
+ struct window *tw = window_find_by_buffer(b);
+ if (tw == NULL) {
+ window_set_buffer(w, b);
+ } else {
+ w = tw;
+ windows_set_active(w);
+ }
+ }
+
+ struct buffer_view *bv = window_buffer_view(w);
+ buffer_view_goto(bv, loc.range.begin);
+ } else {
+ message("warning: unsupported LSP URI: %.*s", loc.uri.l, loc.uri.s);
+ }
+}
+
+static void location_selected(void *location, void *userdata) {
+ (void)userdata;
+ struct text_document_location *loc =
+ (struct text_document_location *)location;
+ lsp_jump_to(*loc);
+}
+
+static void location_buffer_close(void *userdata) {
+ (void)userdata;
+ location_result_free(&g_location_result);
+}
+
+static void handle_location_result(struct lsp_server *server,
+ struct lsp_response *response,
+ void *userdata) {
+ struct s8 title = s8((const char *)userdata);
+ struct location_result res =
+ location_result_from_json(&response->value.result);
+
+ if (res.type == Location_Null ||
+ (res.type == Location_Array && VEC_EMPTY(&res.location.array))) {
+ minibuffer_echo_timeout(2, "nothing found");
+ location_result_free(&res);
+ return;
+ }
+
+ if (res.type == Location_Single) {
+ lsp_jump_to(res.location.single);
+ location_result_free(&res);
+ } else if (res.type == Location_Array && VEC_SIZE(&res.location.array) == 1) {
+ lsp_jump_to(*VEC_FRONT(&res.location.array));
+ location_result_free(&res);
+ } else if (res.type == Location_Array) {
+
+ g_location_result = res;
+ struct choice_buffer *buf =
+ choice_buffer_create(title, g_jump_stack.buffers, location_selected,
+ location_buffer_close, NULL, server);
+
+ VEC_FOR_EACH(&res.location.array, struct text_document_location * loc) {
+ choice_buffer_add_choice(buf,
+ s8from_fmt("%.*s: %d, %d", loc->uri.l,
+ loc->uri.s, loc->range.begin.line,
+ loc->range.begin.col),
+ loc);
+ }
+ }
+}
+
+int32_t lsp_goto_def_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(ctx.active_window);
+ struct buffer *b = bv->buffer;
+ struct lsp_server *server = lsp_server_for_buffer(b);
+
+ if (server == NULL) {
+ minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name);
+ return 0;
+ }
+
+ uint64_t id = new_pending_request(server, handle_location_result,
+ (void *)"Definitions");
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(b);
+ struct text_document_position params = {
+ .uri = doc.uri,
+ .position = bv->dot,
+ };
+
+ struct s8 json_payload = document_position_to_json(&params);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/definition"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+
+ return 0;
+}
+
+int32_t lsp_goto_decl_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(ctx.active_window);
+ struct buffer *b = bv->buffer;
+ struct lsp_server *server = lsp_server_for_buffer(b);
+
+ if (server == NULL) {
+ minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name);
+ return 0;
+ }
+
+ uint64_t id = new_pending_request(server, handle_location_result,
+ (void *)"Declarations");
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(b);
+ struct text_document_position params = {
+ .uri = doc.uri,
+ .position = bv->dot,
+ };
+
+ struct s8 json_payload = document_position_to_json(&params);
+ lsp_send(
+ lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/declaration"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+
+ return 0;
+}
+
+int32_t lsp_goto_impl_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(ctx.active_window);
+ struct buffer *b = bv->buffer;
+ struct lsp_server *server = lsp_server_for_buffer(b);
+
+ if (server == NULL) {
+ minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name);
+ return 0;
+ }
+
+ uint64_t id = new_pending_request(server, handle_location_result,
+ (void *)"Implementations");
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(b);
+ struct text_document_position params = {
+ .uri = doc.uri,
+ .position = bv->dot,
+ };
+
+ struct s8 json_payload = document_position_to_json(&params);
+ lsp_send(
+ lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/implementation"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+
+ return 0;
+}
+
+static int32_t handle_lsp_goto_key(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ buffer_remove_keymap(g_jump_stack.goto_keymap_id);
+ minibuffer_abort_prompt();
+
+ struct command *cmd = lookup_command(ctx.commands, (char *)ctx.userdata);
+ if (cmd == NULL) {
+ return 0;
+ }
+
+ return execute_command(cmd, ctx.commands, windows_get_active(), ctx.buffers,
+ 0, NULL);
+}
+
+COMMAND_FN("lsp-goto-definition", goto_d_pressed, handle_lsp_goto_key,
+ "lsp-goto-definition");
+COMMAND_FN("lsp-goto-declaration", goto_f_pressed, handle_lsp_goto_key,
+ "lsp-goto-declaration");
+
+int32_t lsp_goto_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct binding bindings[] = {
+ ANONYMOUS_BINDING(None, 'd', &goto_d_pressed_command),
+ ANONYMOUS_BINDING(None, 'f', &goto_f_pressed_command),
+ };
+ struct keymap m = keymap_create("lsp-goto", 8);
+ keymap_bind_keys(&m, bindings, sizeof(bindings) / sizeof(bindings[0]));
+ g_jump_stack.goto_keymap_id = buffer_add_keymap(minibuffer_buffer(), m);
+ return minibuffer_keymap_prompt(ctx, "lsp-goto: ", &m);
+}
+
+int32_t lsp_goto_previous_cmd(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)ctx;
+ (void)argc;
+ (void)argv;
+
+ uint32_t index =
+ g_jump_stack.top == 0 ? g_jump_stack.size - 1 : g_jump_stack.top - 1;
+
+ struct buffer_location *loc = &g_jump_stack.stack[index];
+ if (loc->buffer == NULL) {
+ return 0;
+ }
+
+ struct window *w = windows_get_active();
+ if (window_buffer(w) != loc->buffer) {
+ struct window *tw = window_find_by_buffer(loc->buffer);
+ if (tw == NULL) {
+ window_set_buffer(w, loc->buffer);
+ } else {
+ w = tw;
+ windows_set_active(w);
+ }
+ }
+
+ buffer_view_goto(window_buffer_view(w), loc->location);
+
+ loc->buffer = NULL;
+ g_jump_stack.top = index;
+
+ return 0;
+}
diff --git a/src/main/lsp/goto.h b/src/main/lsp/goto.h
new file mode 100644
index 0000000..524772d
--- /dev/null
+++ b/src/main/lsp/goto.h
@@ -0,0 +1,23 @@
+#ifndef _GOTO_H
+#define _GOTO_H
+
+#include "dged/command.h"
+
+#include "types.h"
+
+struct lsp_server;
+struct buffers;
+
+void init_goto(size_t jump_stack_depth, struct buffers *);
+void destroy_goto(void);
+
+void lsp_jump_to(struct text_document_location loc);
+
+/* COMMANDS */
+int32_t lsp_goto_def_cmd(struct command_ctx, int, const char **);
+int32_t lsp_goto_decl_cmd(struct command_ctx, int, const char **);
+int32_t lsp_goto_impl_cmd(struct command_ctx, int, const char **);
+int32_t lsp_goto_cmd(struct command_ctx, int, const char **);
+int32_t lsp_goto_previous_cmd(struct command_ctx, int, const char **);
+
+#endif
diff --git a/src/main/lsp/help.c b/src/main/lsp/help.c
new file mode 100644
index 0000000..e5bcc28
--- /dev/null
+++ b/src/main/lsp/help.c
@@ -0,0 +1,101 @@
+#include "help.h"
+
+#include "dged/binding.h"
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/buffers.h"
+#include "dged/minibuffer.h"
+#include "dged/s8.h"
+#include "dged/window.h"
+
+#include "bindings.h"
+#include "lsp.h"
+
+static int32_t close_help(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ if (window_has_prev_buffer_view(ctx.active_window)) {
+ window_set_buffer(ctx.active_window,
+ window_prev_buffer_view(ctx.active_window)->buffer);
+ } else {
+ minibuffer_echo_timeout(4, "no previous buffer to go to");
+ }
+
+ return 0;
+}
+
+static void handle_help_response(struct lsp_server *server,
+ struct lsp_response *response,
+ void *userdata) {
+ (void)server;
+
+ struct buffers *buffers = (struct buffers *)userdata;
+ if (response->value.result.type == Json_Null) {
+ minibuffer_echo_timeout(4, "help: no help found");
+ return;
+ }
+
+ struct buffer *b = buffers_find(buffers, "*lsp-help*");
+ if (b == NULL) {
+ b = buffers_add(buffers, buffer_create("*lsp-help*"));
+ static struct command help_close = {
+ .name = "help_close",
+ .fn = close_help,
+ };
+
+ struct binding bindings[] = {
+ ANONYMOUS_BINDING(None, 'q', &help_close),
+ };
+ struct keymap km = keymap_create("help", 2);
+ keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0]));
+ buffer_add_keymap(b, km);
+ }
+
+ struct hover help = hover_from_json(&response->value.result);
+
+ buffer_set_readonly(b, false);
+ buffer_clear(b);
+ buffer_add(b, buffer_end(b), help.contents.s, help.contents.l);
+ buffer_set_readonly(b, true);
+
+ if (window_find_by_buffer(b) == NULL) {
+ window_set_buffer(windows_get_active(), b);
+ }
+ hover_free(&help);
+}
+
+void lsp_help(struct lsp_server *server, struct buffer *buffer,
+ struct location at, struct buffers *buffers) {
+ uint64_t id = new_pending_request(server, handle_help_response, buffers);
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(buffer);
+
+ struct text_document_position pos = {
+ .uri = doc.uri,
+ .position = at,
+ };
+
+ struct s8 json_payload = document_position_to_json(&pos);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/hover"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+}
+
+int32_t lsp_help_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(ctx.active_window);
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ minibuffer_echo_timeout(4, "no lsp server associated with %s",
+ bv->buffer->name);
+ return 0;
+ }
+
+ lsp_help(server, bv->buffer, bv->dot, ctx.buffers);
+ return 0;
+}
diff --git a/src/main/lsp/help.h b/src/main/lsp/help.h
new file mode 100644
index 0000000..98a4478
--- /dev/null
+++ b/src/main/lsp/help.h
@@ -0,0 +1,16 @@
+#ifndef _LSP_HELP_H
+#define _LSP_HELP_H
+
+#include "dged/command.h"
+#include "dged/location.h"
+
+struct buffer;
+struct buffers;
+struct lsp_server;
+
+void lsp_help(struct lsp_server *, struct buffer *, struct location,
+ struct buffers *);
+
+int32_t lsp_help_cmd(struct command_ctx, int, const char **);
+
+#endif
diff --git a/src/main/lsp/references.c b/src/main/lsp/references.c
new file mode 100644
index 0000000..c2438fa
--- /dev/null
+++ b/src/main/lsp/references.c
@@ -0,0 +1,248 @@
+#include "references.h"
+
+#include "dged/binding.h"
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/buffers.h"
+#include "dged/command.h"
+#include "dged/display.h"
+#include "dged/location.h"
+#include "dged/minibuffer.h"
+#include "dged/s8.h"
+#include "dged/text.h"
+#include "dged/vec.h"
+#include "dged/window.h"
+
+#include "bindings.h"
+#include "lsp.h"
+#include "lsp/goto.h"
+#include "lsp/types.h"
+#include "unistd.h"
+
+struct link {
+ struct s8 uri;
+ struct region target_region;
+ struct region region;
+};
+
+typedef VEC(struct link) link_vec;
+
+static link_vec g_links;
+static struct buffer *g_prev_buffer = NULL;
+static struct location g_prev_location;
+
+static int32_t references_close(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ if (g_prev_buffer != NULL) {
+ // validate that it is still a valid buffer
+ struct buffer *b =
+ buffers_find_by_filename(ctx.buffers, g_prev_buffer->filename);
+ window_set_buffer(ctx.active_window, b);
+ buffer_view_goto(window_buffer_view(ctx.active_window), g_prev_location);
+ } else if (window_has_prev_buffer_view(ctx.active_window)) {
+ window_set_buffer(ctx.active_window,
+ window_prev_buffer_view(ctx.active_window)->buffer);
+ } else {
+ minibuffer_echo_timeout(4, "no previous buffer to go to");
+ }
+
+ return 0;
+}
+
+static int32_t references_visit(struct command_ctx ctx, int argc,
+ const char **argv) {
+
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *view = window_buffer_view(ctx.active_window);
+
+ VEC_FOR_EACH(&g_links, struct link * link) {
+ if (region_is_inside(link->region, view->dot)) {
+ lsp_jump_to((struct text_document_location){
+ .range = link->target_region,
+ .uri = link->uri,
+ });
+
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static void reference_buffer_closed(struct buffer *buffer, void *userdata) {
+ (void)buffer;
+ link_vec *vec = (link_vec *)userdata;
+ VEC_FOR_EACH(vec, struct link * link) { s8delete(link->uri); }
+ VEC_DESTROY(vec);
+}
+
+static void handle_references_response(struct lsp_server *server,
+ struct lsp_response *response,
+ void *userdata) {
+ (void)server;
+
+ struct buffers *buffers = (struct buffers *)userdata;
+ if (response->value.result.type == Json_Null) {
+ minibuffer_echo_timeout(4, "references: no references found");
+ return;
+ }
+
+ struct location_result locations =
+ location_result_from_json(&response->value.result);
+
+ if (locations.type != Location_Array) {
+ minibuffer_echo_timeout(4, "references: expected location array");
+ return;
+ }
+
+ struct buffer *b = buffers_find(buffers, "*lsp-references*");
+ if (b == NULL) {
+ b = buffers_add(buffers, buffer_create("*lsp-references*"));
+ b->lazy_row_add = false;
+ b->retain_properties = true;
+ static struct command ref_close = {
+ .name = "ref_close",
+ .fn = references_close,
+ };
+
+ static struct command ref_visit_cmd = {
+ .name = "ref_visit",
+ .fn = references_visit,
+ };
+
+ struct binding bindings[] = {
+ ANONYMOUS_BINDING(None, 'q', &ref_close),
+ ANONYMOUS_BINDING(ENTER, &ref_visit_cmd),
+ };
+ struct keymap km = keymap_create("references", 2);
+ keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0]));
+ buffer_add_keymap(b, km);
+ buffer_add_destroy_hook(b, reference_buffer_closed, &g_links);
+ VEC_INIT(&g_links, 16);
+ }
+
+ buffer_set_readonly(b, false);
+ buffer_clear(b);
+
+ VEC_FOR_EACH(&g_links, struct link * link) { s8delete(link->uri); }
+ VEC_CLEAR(&g_links);
+
+ buffer_clear_text_properties(b);
+ VEC_FOR_EACH(&locations.location.array, struct text_document_location * loc) {
+ uint32_t found = 0, found_at = (uint32_t)-1;
+ for (uint32_t i = 0; i < loc->uri.l; ++i) {
+ uint8_t b = loc->uri.s[i];
+ if (b == ':') {
+ ++found;
+ } else if (b == '/' && found == 1) {
+ ++found;
+ } else if (b == '/' && found == 2) {
+ found_at = i;
+ } else {
+ found = 0;
+ }
+ }
+
+ struct s8 path = loc->uri;
+ if (found_at != (uint32_t)-1) {
+ path.s += found_at;
+ path.l -= found_at;
+ }
+
+ struct s8 relpath = path;
+ char *cwd = getcwd(NULL, 0);
+ if (s8startswith(relpath, s8(cwd))) {
+ size_t l = strlen(cwd);
+ // cwd does not end in /
+ relpath.s += l + 1;
+ relpath.l -= l + 1;
+ }
+ free(cwd);
+
+ struct location start = buffer_end(b);
+ struct location fileend = buffer_add(b, start, relpath.s, relpath.l);
+ buffer_add_text_property(b, start,
+ (struct location){
+ .line = fileend.line,
+ .col = fileend.col > 0 ? fileend.col - 1 : 0,
+ },
+ (struct text_property){
+ .type = TextProperty_Colors,
+ .data.colors =
+ (struct text_property_colors){
+ .set_bg = false,
+ .set_fg = true,
+ .fg = Color_Magenta,
+ .underline = true,
+ },
+ });
+
+ struct s8 line = s8from_fmt(":%d", loc->range.begin.line);
+ struct location end = buffer_add(b, buffer_end(b), line.s, line.l);
+ s8delete(line);
+
+ VEC_PUSH(&g_links, ((struct link){
+ .target_region = loc->range,
+ .region = region_new(start, end),
+ .uri = s8dup(loc->uri),
+ }));
+
+ buffer_newline(b, end);
+ }
+
+ buffer_set_readonly(b, true);
+
+ if (window_find_by_buffer(b) == NULL) {
+ struct window *w = windows_get_active();
+ g_prev_buffer = window_buffer(w);
+ g_prev_location = window_buffer_view(w)->dot;
+ window_set_buffer(w, b);
+ }
+ location_result_free(&locations);
+}
+
+void lsp_references(struct lsp_server *server, struct buffer *buffer,
+ struct location at, struct buffers *buffers) {
+ uint64_t id =
+ new_pending_request(server, handle_references_response, buffers);
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(buffer);
+
+ struct reference_params params = {
+ .position =
+ {
+ .uri = doc.uri,
+ .position = at,
+ },
+ .include_declaration = true,
+ };
+
+ struct s8 json_payload = reference_params_to_json(&params);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/references"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+}
+
+int32_t lsp_references_cmd(struct command_ctx ctx, int argc,
+ const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ struct buffer_view *bv = window_buffer_view(ctx.active_window);
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ minibuffer_echo_timeout(4, "no lsp server associated with %s",
+ bv->buffer->name);
+ return 0;
+ }
+
+ lsp_references(server, bv->buffer, bv->dot, ctx.buffers);
+ return 0;
+}
diff --git a/src/main/lsp/references.h b/src/main/lsp/references.h
new file mode 100644
index 0000000..ea51987
--- /dev/null
+++ b/src/main/lsp/references.h
@@ -0,0 +1,19 @@
+#ifndef _LSP_REFERENCES_H
+#define _LSP_REFERENCES_H
+
+#include <stdint.h>
+
+#include "dged/command.h"
+#include "dged/location.h"
+
+struct lsp_server;
+struct buffer;
+struct buffers;
+
+void lsp_references(struct lsp_server *server, struct buffer *buffer,
+ struct location at, struct buffers *buffers);
+
+int32_t lsp_references_cmd(struct command_ctx ctx, int argc,
+ const char *argv[]);
+
+#endif
diff --git a/src/main/lsp/rename.c b/src/main/lsp/rename.c
new file mode 100644
index 0000000..6adc9a1
--- /dev/null
+++ b/src/main/lsp/rename.c
@@ -0,0 +1,61 @@
+#include "rename.h"
+
+#include "dged/buffer.h"
+#include "dged/buffer_view.h"
+#include "dged/minibuffer.h"
+#include "dged/window.h"
+
+#include "lsp.h"
+
+static void handle_rename_response(struct lsp_server *server,
+ struct lsp_response *response,
+ void *userdata) {
+ (void)userdata;
+ if (response->value.result.type == Json_Null) {
+ minibuffer_echo_timeout(4, "rename: no edits");
+ return;
+ }
+
+ struct workspace_edit edit =
+ workspace_edit_from_json(&response->value.result);
+ apply_edits(server, &edit);
+ workspace_edit_free(&edit);
+}
+
+void lsp_rename(struct lsp_server *server, struct buffer *buffer,
+ struct location location, struct s8 new_name) {
+ uint64_t id = new_pending_request(server, handle_rename_response, NULL);
+ struct versioned_text_document_identifier doc =
+ versioned_identifier_from_buffer(buffer);
+
+ struct rename_params params = {
+ .position.uri = doc.uri,
+ .position.position = location,
+ .new_name = new_name,
+ };
+
+ struct s8 json_payload = rename_params_to_json(&params);
+ lsp_send(lsp_backend(server),
+ lsp_create_request(id, s8("textDocument/rename"), json_payload));
+
+ versioned_text_document_identifier_free(&doc);
+ s8delete(json_payload);
+}
+
+int32_t lsp_rename_cmd(struct command_ctx ctx, int argc, const char **argv) {
+ if (argc == 0) {
+ return minibuffer_prompt(ctx, "rename to: ");
+ }
+
+ struct buffer_view *bv = window_buffer_view(ctx.active_window);
+ struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id);
+ if (server == NULL) {
+ minibuffer_echo_timeout(4, "no lsp server associated with %s",
+ bv->buffer->name);
+ return 0;
+ }
+
+ lsp_rename(server, bv->buffer, bv->dot, s8(argv[0]));
+
+ return 0;
+}
diff --git a/src/main/lsp/rename.h b/src/main/lsp/rename.h
new file mode 100644
index 0000000..4fb8396
--- /dev/null
+++ b/src/main/lsp/rename.h
@@ -0,0 +1,16 @@
+#ifndef _LSP_RENAME_H
+#define _LSP_RENAME_H
+
+#include "dged/command.h"
+#include "dged/location.h"
+#include "dged/s8.h"
+
+struct lsp_server;
+struct buffer;
+
+void lsp_rename(struct lsp_server *, struct buffer *, struct location,
+ struct s8);
+
+int32_t lsp_rename_cmd(struct command_ctx, int, const char **);
+
+#endif
diff --git a/src/main/lsp/types.c b/src/main/lsp/types.c
new file mode 100644
index 0000000..bd87377
--- /dev/null
+++ b/src/main/lsp/types.c
@@ -0,0 +1,1081 @@
+#include "types.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "dged/buffer.h"
+#include "dged/display.h"
+#include "dged/path.h"
+#include "dged/s8.h"
+
+struct s8 initialize_params_to_json(struct initialize_params *params) {
+ char *cwd = getcwd(NULL, 0);
+ const char *fmt =
+ "{ \"processId\": %d, \"clientInfo\": { \"name\": "
+ "\"%.*s\", \"version\": \"%.*s\" },"
+ "\"capabilities\": { \"textDocument\": { "
+ "\"publishDiagnostics\": { },"
+ "\"hover\": { \"dynamicRegistration\": false, \"contentFormat\": [ "
+ "\"plaintext\", \"markdown\" ] },"
+ "\"signatureHelp\" : { \"dynamicRegistration\": false, "
+ "\"signatureInformation\": {"
+ " \"documentationFormat\": [ \"plaintext\", \"markdown\" ], "
+ "\"activeParameterSupport\": true } },"
+ "\"codeAction\": { \"codeActionLiteralSupport\": { \"codeActionKind\":"
+ "{ \"valueSet\": [ \"quickfix\", \"refactor\", \"source\", "
+ "\"refactor.extract\", "
+ "\"refactor.inline\", \"refactor.rewrite\", \"source.organizeImports\" ] "
+ "} } } },"
+ "\"general\": { \"positionEncodings\": [ \"utf-8\", "
+ "\"utf-32\", \"utf-16\" ] },"
+ "\"offsetEncoding\": [ \"utf-8\", \"utf-32\" ,\"utf-16\" ]"
+ "},"
+ "\"workspaceFolders\": [ { \"uri\": \"file://%s\", "
+ "\"name\": \"cwd\" } ] }";
+
+ struct s8 s =
+ s8from_fmt(fmt, params->process_id, params->client_info.name.l,
+ params->client_info.name.s, params->client_info.version.l,
+ params->client_info.version.s, cwd);
+
+ free(cwd);
+ return s;
+}
+
+static enum position_encoding_kind
+position_encoding_from_str(struct s8 encoding_kind) {
+ if (s8eq(encoding_kind, s8("utf-8"))) {
+ return PositionEncoding_Utf8;
+ } else if (s8eq(encoding_kind, s8("utf-32"))) {
+ return PositionEncoding_Utf32;
+ }
+
+ return PositionEncoding_Utf16;
+}
+
+struct s8 position_encoding_kind_str(enum position_encoding_kind kind) {
+ switch (kind) {
+ case PositionEncoding_Utf8:
+ return s8("utf-8");
+
+ case PositionEncoding_Utf32:
+ return s8("utf-32");
+
+ default:
+ break;
+ }
+
+ return s8("utf-16");
+}
+
+static struct server_capabilities
+parse_capabilities(struct json_object *root, struct json_value *capabilities) {
+ struct server_capabilities caps = {
+ .text_document_sync.kind = TextDocumentSync_Full,
+ .text_document_sync.open_close = false,
+ .text_document_sync.save = false,
+ .position_encoding = PositionEncoding_Utf16,
+ };
+
+ // clang has this legacy attribute for positionEncoding
+ // use with a lower prio than positionEncoding in capabilities
+ struct json_value *offset_encoding = json_get(root, s8("offsetEncoding"));
+ if (offset_encoding != NULL && offset_encoding->type == Json_String) {
+ caps.position_encoding =
+ position_encoding_from_str(offset_encoding->value.string);
+ }
+
+ if (capabilities == NULL || capabilities->type != Json_Object) {
+ return caps;
+ }
+
+ struct json_object *obj = capabilities->value.object;
+ // text document sync caps
+ struct json_value *text_doc_sync = json_get(obj, s8("textDocumentSync"));
+ if (text_doc_sync != NULL) {
+ if (text_doc_sync->type == Json_Number) {
+ caps.text_document_sync.kind =
+ (enum text_document_sync_kind)text_doc_sync->value.number;
+ } else {
+ struct json_object *tsync = text_doc_sync->value.object;
+ caps.text_document_sync.kind =
+ (enum text_document_sync_kind)json_get(tsync, s8("change"))
+ ->value.number;
+
+ struct json_value *open_close = json_get(tsync, s8("openClose"));
+ caps.text_document_sync.open_close =
+ open_close != NULL ? open_close->value.boolean : false;
+
+ struct json_value *save = json_get(tsync, s8("save"));
+ caps.text_document_sync.save =
+ save != NULL ? open_close->value.boolean : false;
+ }
+ }
+
+ // position encoding
+ struct json_value *pos_enc = json_get(obj, s8("positionEncoding"));
+ if (pos_enc != NULL && pos_enc->type == Json_String) {
+ caps.position_encoding = position_encoding_from_str(pos_enc->value.string);
+ }
+
+ struct json_value *completion_opts = json_get(obj, s8("completionProvider"));
+ caps.supports_completion = false;
+ if (completion_opts != NULL && completion_opts->type == Json_Object) {
+ caps.supports_completion = true;
+
+ // trigger chars
+ struct json_value *trigger_chars =
+ json_get(completion_opts->value.object, s8("triggerCharacters"));
+ if (trigger_chars != NULL && trigger_chars->type == Json_Array) {
+ uint64_t arrlen = json_array_len(trigger_chars->value.array);
+ VEC_INIT(&caps.completion_options.trigger_characters, arrlen);
+ for (uint32_t i = 0; i < arrlen; ++i) {
+ struct json_value *val = json_array_get(trigger_chars->value.array, i);
+ VEC_PUSH(&caps.completion_options.trigger_characters,
+ s8dup(val->value.string));
+ }
+ }
+
+ // all commit characters
+ struct json_value *commit_chars =
+ json_get(completion_opts->value.object, s8("allCommitCharacters"));
+ if (commit_chars != NULL && commit_chars->type == Json_Array) {
+ uint64_t arrlen = json_array_len(commit_chars->value.array);
+ VEC_INIT(&caps.completion_options.all_commit_characters, arrlen);
+ for (uint32_t i = 0; i < arrlen; ++i) {
+ struct json_value *val = json_array_get(commit_chars->value.array, i);
+ VEC_PUSH(&caps.completion_options.all_commit_characters,
+ s8dup(val->value.string));
+ }
+ }
+
+ // resolve provider
+ struct json_value *resolve_provider =
+ json_get(completion_opts->value.object, s8("resolveProvider"));
+ if (resolve_provider != NULL && resolve_provider->type == Json_Bool) {
+ caps.completion_options.resolve_provider =
+ resolve_provider->value.boolean;
+ }
+ }
+
+ return caps;
+}
+
+struct initialize_result initialize_result_from_json(struct json_value *json) {
+ struct json_object *obj = json->value.object;
+ struct json_object *server_info =
+ json_get(obj, s8("serverInfo"))->value.object;
+ return (struct initialize_result){
+ .capabilities =
+ parse_capabilities(obj, json_get(obj, s8("capabilities"))),
+ .server_info.name =
+ s8dup(json_get(server_info, s8("name"))->value.string),
+ .server_info.version =
+ s8dup(json_get(server_info, s8("version"))->value.string),
+ };
+}
+
+void initialize_result_free(struct initialize_result *res) {
+ s8delete(res->server_info.name);
+ s8delete(res->server_info.version);
+ if (res->capabilities.supports_completion) {
+ VEC_FOR_EACH(&res->capabilities.completion_options.trigger_characters,
+ struct s8 * s) {
+ s8delete(*s);
+ }
+
+ VEC_DESTROY(&res->capabilities.completion_options.trigger_characters);
+
+ VEC_FOR_EACH(&res->capabilities.completion_options.all_commit_characters,
+ struct s8 * s) {
+ s8delete(*s);
+ }
+
+ VEC_DESTROY(&res->capabilities.completion_options.all_commit_characters);
+ }
+}
+
+static struct s8 uri_from_buffer(struct buffer *buffer) {
+ if (buffer->filename != NULL) {
+ char *abspath = to_abspath(buffer->filename);
+ struct s8 ret = s8from_fmt("file://%s", abspath);
+ free(abspath);
+ return ret;
+ }
+
+ return s8from_fmt("file://invalid-file");
+}
+
+struct text_document_item
+text_document_item_from_buffer(struct buffer *buffer) {
+ struct text_chunk buffer_text =
+ buffer_region(buffer, region_new((struct location){.line = 0, .col = 0},
+ buffer_end(buffer)));
+ struct text_document_item item = {
+ .uri = uri_from_buffer(buffer),
+ .language_id = s8new(buffer->lang.id, strlen(buffer->lang.id)),
+ .version = buffer->version,
+ .text =
+ (struct s8){
+ .s = buffer_text.text,
+ .l = buffer_text.nbytes,
+ },
+ };
+
+ return item;
+}
+
+void text_document_item_free(struct text_document_item *item) {
+ s8delete(item->uri);
+ s8delete(item->language_id);
+ s8delete(item->text);
+}
+
+struct versioned_text_document_identifier
+versioned_identifier_from_buffer(struct buffer *buffer) {
+ struct versioned_text_document_identifier identifier = {
+ .uri = uri_from_buffer(buffer),
+ .version = buffer->version,
+ };
+
+ return identifier;
+}
+
+void versioned_text_document_identifier_free(
+ struct versioned_text_document_identifier *identifier) {
+ s8delete(identifier->uri);
+}
+
+struct s8 did_change_text_document_params_to_json(
+ struct did_change_text_document_params *params) {
+ size_t event_buf_size = 0;
+ for (size_t i = 0; i < params->ncontent_changes; ++i) {
+ struct text_document_content_change_event *ev = &params->content_changes[i];
+ struct s8 escaped = escape_json_string(ev->text);
+ if (!ev->full_document) {
+ const char *item_fmt =
+ "{ \"range\": { \"start\": { \"line\": %d, \"character\": %d}, "
+ "\"end\": { \"line\": %d, \"character\": %d } }, "
+ "\"text\": \"%.*s\" }%s";
+
+ ssize_t num =
+ snprintf(NULL, 0, item_fmt, ev->range.begin.line, ev->range.begin.col,
+ ev->range.end.line, ev->range.end.col, escaped.l, escaped.s,
+ i == params->ncontent_changes - 1 ? "" : ", ");
+
+ if (num < 0) {
+ return s8("");
+ }
+
+ event_buf_size += num;
+ } else {
+ const char *item_fmt = "{ \"text\", \"%.*s\" }%s";
+ ssize_t num = snprintf(NULL, 0, item_fmt, escaped.l, escaped.s,
+ i == params->ncontent_changes - 1 ? "" : ", ");
+
+ if (num < 0) {
+ return s8("");
+ }
+
+ event_buf_size += num;
+ }
+
+ s8delete(escaped);
+ }
+
+ ++event_buf_size;
+ char *buf = calloc(event_buf_size, 1);
+ size_t offset = 0;
+ for (size_t i = 0; i < params->ncontent_changes; ++i) {
+ struct text_document_content_change_event *ev = &params->content_changes[i];
+ struct s8 escaped = escape_json_string(ev->text);
+ if (!ev->full_document) {
+ const char *item_fmt =
+ "{ \"range\": { \"start\": { \"line\": %d, \"character\": %d}, "
+ "\"end\": { \"line\": %d, \"character\": %d } }, "
+ "\"text\": \"%.*s\" }%s";
+
+ ssize_t num = snprintf(
+ &buf[offset], event_buf_size - offset, item_fmt, ev->range.begin.line,
+ ev->range.begin.col, ev->range.end.line, ev->range.end.col, escaped.l,
+ escaped.s, i == params->ncontent_changes - 1 ? "" : ", ");
+
+ if (num < 0) {
+ return s8("");
+ }
+
+ offset += num;
+ } else {
+ const char *item_fmt = "{ \"text\", \"%.*s\" }%s";
+ ssize_t num =
+ snprintf(&buf[offset], event_buf_size - offset, item_fmt, escaped.l,
+ escaped.s, i == params->ncontent_changes - 1 ? "" : ", ");
+
+ if (num < 0) {
+ return s8("");
+ }
+
+ offset += num;
+ }
+
+ s8delete(escaped);
+ }
+
+ const char *fmt =
+ "{ \"textDocument\": { \"uri\": \"%.*s\", \"version\": %d }, "
+ "\"contentChanges\": [ %s ]"
+ "}";
+
+ struct versioned_text_document_identifier *doc = &params->text_document;
+ struct s8 json = s8from_fmt(fmt, doc->uri.l, doc->uri.s, doc->version, buf);
+
+ free(buf);
+ return json;
+}
+
+struct s8 did_open_text_document_params_to_json(
+ struct did_open_text_document_params *params) {
+ const char *fmt =
+ "{ \"textDocument\": { \"uri\": \"%.*s\", \"languageId\": \"%.*s\", "
+ "\"version\": %d, \"text\": \"%.*s\" }}";
+
+ struct text_document_item *item = &params->text_document;
+
+ struct s8 escaped_content = escape_json_string(item->text);
+ struct s8 json = s8from_fmt(
+ fmt, item->uri.l, item->uri.s, item->language_id.l, item->language_id.s,
+ item->version, escaped_content.l, escaped_content.s);
+
+ s8delete(escaped_content);
+ return json;
+}
+
+struct s8 did_save_text_document_params_to_json(
+ struct did_save_text_document_params *params) {
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" } }";
+
+ struct text_document_identifier *item = &params->text_document;
+ struct s8 json = s8from_fmt(fmt, item->uri.l, item->uri.s);
+ return json;
+}
+
+static struct region parse_region(struct json_object *obj) {
+ struct json_object *start = json_get(obj, s8("start"))->value.object;
+ struct json_object *end = json_get(obj, s8("end"))->value.object;
+
+ return region_new(
+ (struct location){.line = json_get(start, s8("line"))->value.number,
+ .col = json_get(start, s8("character"))->value.number},
+ (struct location){.line = json_get(end, s8("line"))->value.number,
+ .col = json_get(end, s8("character"))->value.number});
+}
+
+static void parse_diagnostic(uint64_t id, struct json_value *elem,
+ void *userdata) {
+ (void)id;
+ diagnostic_vec *vec = (diagnostic_vec *)userdata;
+ struct json_object *obj = elem->value.object;
+ struct json_value *severity = json_get(obj, s8("severity"));
+ struct json_value *source = json_get(obj, s8("source"));
+
+ struct diagnostic diag;
+ diag.message =
+ unescape_json_string(json_get(obj, s8("message"))->value.string);
+ diag.region = parse_region(json_get(obj, s8("range"))->value.object);
+ diag.severity = severity != NULL
+ ? (enum diagnostic_severity)severity->value.number
+ : LspDiagnostic_Error;
+ diag.source = source != NULL ? unescape_json_string(source->value.string)
+ : (struct s8){.l = 0, .s = NULL};
+
+ VEC_PUSH(vec, diag);
+}
+
+const char *diag_severity_to_str(enum diagnostic_severity severity) {
+
+ switch (severity) {
+ case LspDiagnostic_Error:
+ return "error";
+ case LspDiagnostic_Warning:
+ return "warning";
+ case LspDiagnostic_Information:
+ return "info";
+ case LspDiagnostic_Hint:
+ return "hint";
+ }
+
+ return "";
+}
+
+struct publish_diagnostics_params
+diagnostics_from_json(struct json_value *json) {
+ struct json_object *obj = json->value.object;
+ struct json_value *version = json_get(obj, s8("version"));
+ struct publish_diagnostics_params params = {
+ .uri = unescape_json_string(json_get(obj, s8("uri"))->value.string),
+ .version = version != NULL ? version->value.number : 0,
+ };
+
+ struct json_array *diagnostics =
+ json_get(obj, s8("diagnostics"))->value.array;
+ VEC_INIT(&params.diagnostics, json_array_len(diagnostics));
+ json_array_foreach(diagnostics, &params.diagnostics, parse_diagnostic);
+
+ return params;
+}
+
+void diagnostic_free(struct diagnostic *diag) {
+ s8delete(diag->message);
+ s8delete(diag->source);
+}
+
+struct s8 document_position_to_json(struct text_document_position *position) {
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, "
+ "\"position\": { \"line\": %d, \"character\": %d } }";
+
+ struct s8 json = s8from_fmt(fmt, position->uri.l, position->uri.s,
+ position->position.line, position->position.col);
+ return json;
+}
+
+static struct text_document_location
+location_from_json(struct json_value *json) {
+ struct text_document_location loc = {0};
+ if (json->type != Json_Object) {
+ return loc;
+ }
+
+ struct json_object *obj = json->value.object;
+ loc.uri = unescape_json_string(json_get(obj, s8("uri"))->value.string);
+ loc.range = parse_region(json_get(obj, s8("range"))->value.object);
+
+ return loc;
+}
+
+static void parse_text_doc_location(uint64_t id, struct json_value *elem,
+ void *userdata) {
+ (void)id;
+ location_vec *vec = (location_vec *)userdata;
+ VEC_PUSH(vec, location_from_json(elem));
+}
+
+struct location_result location_result_from_json(struct json_value *json) {
+ if (json->type == Json_Null) {
+ return (struct location_result){
+ .type = Location_Null,
+ };
+ } else if (json->type == Json_Object) {
+ return (struct location_result){
+ .type = Location_Single,
+ .location.single = location_from_json(json),
+ };
+ } else if (json->type == Json_Array) {
+ // location link or location
+ struct location_result res = {};
+ res.type = Location_Array;
+ struct json_array *locations = json->value.array;
+ VEC_INIT(&res.location.array, json_array_len(locations));
+ json_array_foreach(locations, &res.location.array, parse_text_doc_location);
+ return res;
+ }
+
+ return (struct location_result){.type = Location_Null};
+}
+
+void location_result_free(struct location_result *res) {
+ switch (res->type) {
+ case Location_Null:
+ break;
+ case Location_Single:
+ s8delete(res->location.single.uri);
+ break;
+ case Location_Array:
+ VEC_FOR_EACH(&res->location.array, struct text_document_location * loc) {
+ s8delete(loc->uri);
+ }
+ VEC_DESTROY(&res->location.array);
+ break;
+ case Location_Link:
+ // TODO
+ break;
+ }
+}
+
+static uint32_t severity_to_json(enum diagnostic_severity severity) {
+ return (uint32_t)severity;
+}
+
+static struct s8 region_to_json(struct region region) {
+ const char *fmt = "{ \"start\": { \"line\": %d, \"character\": %d }, "
+ "\"end\": { \"line\": %d, \"character\": %d } }";
+ return s8from_fmt(fmt, region.begin.line, region.begin.col, region.end.line,
+ region.end.col);
+}
+
+static struct s8 diagnostic_to_json(struct diagnostic *diag) {
+ const char *fmt =
+ "{ \"range\": %.*s, \"message\": \"%.*s\", \"severity\": %d }";
+
+ struct s8 range = region_to_json(diag->region);
+ struct s8 json =
+ s8from_fmt(fmt, range.l, range.s, diag->message.l, diag->message.s,
+ severity_to_json(diag->severity));
+
+ s8delete(range);
+ return json;
+}
+
+static struct s8 diagnostic_vec_to_json(diagnostic_vec diagnostics) {
+ size_t ndiags = VEC_SIZE(&diagnostics);
+ if (ndiags == 0) {
+ return s8new("[]", 2);
+ }
+
+ struct s8 *strings = calloc(ndiags, sizeof(struct s8));
+
+ size_t len = 1;
+ VEC_FOR_EACH_INDEXED(&diagnostics, struct diagnostic * diag, i) {
+ strings[i] = diagnostic_to_json(diag);
+ len += strings[i].l + 1;
+ }
+
+ uint8_t *final = (uint8_t *)calloc(len, 1);
+ struct s8 json = {
+ .s = final,
+ .l = len,
+ };
+
+ final[0] = '[';
+
+ size_t offset = 1;
+ for (uint32_t i = 0; i < ndiags; ++i) {
+ memcpy(&final[offset], strings[i].s, strings[i].l);
+ offset += strings[i].l;
+
+ s8delete(strings[i]);
+ }
+
+ final[len - 1] = ']';
+
+ free(strings);
+
+ return json;
+}
+
+struct s8 code_action_params_to_json(struct code_action_params *params) {
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, "
+ " \"range\": %.*s, "
+ " \"context\": { \"diagnostics\": %.*s } }";
+
+ struct s8 json_diags = diagnostic_vec_to_json(params->context.diagnostics);
+ struct s8 range = region_to_json(params->range);
+
+ struct s8 json =
+ s8from_fmt(fmt, params->text_document.uri.l, params->text_document.uri.s,
+ range.l, range.s, json_diags.l, json_diags.s);
+
+ s8delete(json_diags);
+ s8delete(range);
+ return json;
+}
+
+static struct lsp_command lsp_command_from_json(struct json_value *json) {
+ struct json_object *obj = json->value.object;
+ struct lsp_command command = {
+ .title = unescape_json_string(json_get(obj, s8("title"))->value.string),
+ .command =
+ unescape_json_string(json_get(obj, s8("command"))->value.string),
+ .arguments = s8(""),
+ };
+
+ struct json_value *arguments = json_get(obj, s8("arguments"));
+ if (arguments != NULL && arguments->type == Json_Array) {
+ size_t len = arguments->end - arguments->start;
+ command.arguments = s8new((const char *)arguments->start, len);
+ }
+
+ return command;
+}
+
+static void lsp_action_from_json(uint64_t id, struct json_value *json,
+ void *userdata) {
+ (void)id;
+ struct code_actions *actions = (struct code_actions *)userdata;
+
+ struct json_object *obj = json->value.object;
+ struct json_value *command_val = json_get(obj, s8("command"));
+ if (command_val != NULL && command_val->type == Json_String) {
+ VEC_PUSH(&actions->commands, lsp_command_from_json(json));
+ } else {
+ VEC_APPEND(&actions->code_actions, struct code_action * action);
+ action->title =
+ unescape_json_string(json_get(obj, s8("title"))->value.string);
+ action->kind = s8("");
+ action->has_edit = false;
+ action->has_command = false;
+
+ struct json_value *kind_val = json_get(obj, s8("kind"));
+ if (kind_val != NULL && kind_val->type == Json_String) {
+ action->kind = unescape_json_string(kind_val->value.string);
+ }
+
+ struct json_value *edit_val = json_get(obj, s8("edit"));
+ if (edit_val != NULL && edit_val->type == Json_Object) {
+ action->has_edit = true;
+ action->edit = workspace_edit_from_json(edit_val);
+ }
+
+ command_val = json_get(obj, s8("command"));
+ if (command_val != NULL && command_val->type == Json_Object) {
+ action->has_command = true;
+ action->command = lsp_command_from_json(command_val);
+ }
+ }
+}
+
+struct code_actions lsp_code_actions_from_json(struct json_value *json) {
+ struct code_actions actions;
+
+ if (json->type == Json_Array) {
+ struct json_array *jcmds = json->value.array;
+ VEC_INIT(&actions.commands, json_array_len(jcmds));
+ VEC_INIT(&actions.code_actions, json_array_len(jcmds));
+ json_array_foreach(jcmds, &actions, lsp_action_from_json);
+ } else { /* NULL or wrong type */
+ VEC_INIT(&actions.commands, 0);
+ VEC_INIT(&actions.code_actions, 0);
+ }
+
+ return actions;
+}
+
+static void lsp_command_free(struct lsp_command *command) {
+ s8delete(command->title);
+ s8delete(command->command);
+
+ if (command->arguments.l > 0) {
+ s8delete(command->arguments);
+ }
+}
+
+void lsp_code_actions_free(struct code_actions *actions) {
+ VEC_FOR_EACH(&actions->commands, struct lsp_command * command) {
+ lsp_command_free(command);
+ }
+
+ VEC_DESTROY(&actions->commands);
+
+ VEC_FOR_EACH(&actions->code_actions, struct code_action * action) {
+ s8delete(action->title);
+ s8delete(action->kind);
+
+ if (action->has_edit) {
+ workspace_edit_free(&action->edit);
+ }
+
+ if (action->has_command) {
+ lsp_command_free(&action->command);
+ }
+ }
+
+ VEC_DESTROY(&actions->code_actions);
+}
+
+struct s8 lsp_command_to_json(struct lsp_command *command) {
+ const char *fmt = "{ \"command\": \"%.*s\", \"arguments\": %.*s }";
+
+ return s8from_fmt(fmt, command->command.l, command->command.s,
+ command->arguments.l, command->arguments.s);
+}
+
+static void text_edit_from_json(uint64_t id, struct json_value *val,
+ void *userdata) {
+ (void)id;
+ text_edit_vec *vec = (text_edit_vec *)userdata;
+ struct json_object *obj = val->value.object;
+ struct text_edit edit = {
+ .range = parse_region(json_get(obj, s8("range"))->value.object),
+ .new_text =
+ unescape_json_string(json_get(obj, s8("newText"))->value.string),
+ };
+ VEC_PUSH(vec, edit);
+}
+
+text_edit_vec text_edits_from_json(struct json_value *json) {
+ text_edit_vec vec = {0};
+
+ if (json->type == Json_Array) {
+ struct json_array *arr = json->value.array;
+
+ VEC_INIT(&vec, json_array_len(arr));
+ json_array_foreach(arr, &vec, text_edit_from_json);
+ }
+
+ return vec;
+}
+
+static void changes_from_json(struct s8 key, struct json_value *json,
+ void *userdata) {
+ change_vec *vec = (change_vec *)userdata;
+
+ struct text_edit_pair pair = {
+ .uri = s8dup(key),
+ };
+
+ // pick out the edits for this key and create array
+ struct json_array *edits = json->value.array;
+ VEC_INIT(&pair.edits, json_array_len(edits));
+ json_array_foreach(edits, &pair.edits, text_edit_from_json);
+ VEC_PUSH(vec, pair);
+}
+
+struct workspace_edit workspace_edit_from_json(struct json_value *json) {
+ struct workspace_edit edit;
+ struct json_object *obj = json->value.object;
+ struct json_value *edit_container = json_get(obj, s8("edit"));
+ if (edit_container != NULL && edit_container->type == Json_Object) {
+ obj = edit_container->value.object;
+ }
+
+ struct json_value *changes = json_get(obj, s8("changes"));
+ if (changes != NULL) {
+ struct json_object *changes_obj = changes->value.object;
+ VEC_INIT(&edit.changes, json_len(changes_obj));
+ json_foreach(changes_obj, changes_from_json, &edit.changes);
+ } else {
+ VEC_INIT(&edit.changes, 0);
+ }
+
+ return edit;
+}
+
+void workspace_edit_free(struct workspace_edit *edit) {
+ VEC_FOR_EACH(&edit->changes, struct text_edit_pair * pair) {
+ s8delete(pair->uri);
+ VEC_FOR_EACH(&pair->edits, struct text_edit * edit) {
+ s8delete(edit->new_text);
+ }
+ VEC_DESTROY(&pair->edits);
+ }
+ VEC_DESTROY(&edit->changes);
+}
+
+uint32_t diag_severity_color(enum diagnostic_severity severity) {
+ switch (severity) {
+ case LspDiagnostic_Error:
+ return Color_BrightRed;
+ case LspDiagnostic_Warning:
+ return Color_BrightYellow;
+ default:
+ return Color_BrightBlack;
+ }
+
+ return Color_BrightBlack;
+}
+
+struct s8
+document_formatting_params_to_json(struct document_formatting_params *params) {
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, \"options\": { "
+ "\"tabSize\": %d, \"insertSpaces\": %s } }";
+
+ return s8from_fmt(fmt, params->text_document.uri.l,
+ params->text_document.uri.s, params->options.tab_size,
+ params->options.use_spaces ? "true" : "false");
+}
+
+struct s8 document_range_formatting_params_to_json(
+ struct document_range_formatting_params *params) {
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, \"range\": "
+ "%.*s, \"options\": { "
+ "\"tabSize\": %d, \"insertSpaces\": %s } }";
+
+ struct s8 range = region_to_json(params->range);
+ struct s8 json =
+ s8from_fmt(fmt, params->text_document.uri.l, params->text_document.uri.s,
+ range.l, range.s, params->options.tab_size,
+ params->options.use_spaces ? "true" : "false");
+
+ s8delete(range);
+ return json;
+}
+
+void text_edits_free(text_edit_vec edits) {
+ VEC_FOR_EACH(&edits, struct text_edit * edit) { s8delete(edit->new_text); }
+ VEC_DESTROY(&edits);
+}
+
+static void parse_completion_item(uint64_t id, struct json_value *json,
+ void *userdata) {
+
+ (void)id;
+ completions_vec *vec = (completions_vec *)userdata;
+
+ struct json_object *obj = json->value.object;
+
+ struct lsp_completion_item item = {0};
+ item.label = s8dup(json_get(obj, s8("label"))->value.string);
+
+ struct json_value *kind_val = json_get(obj, s8("kind"));
+ if (kind_val != NULL && kind_val->type == Json_Number) {
+ item.kind = (enum completion_item_kind)kind_val->value.number;
+ }
+
+ struct json_value *detail_val = json_get(obj, s8("detail"));
+ if (detail_val != NULL && detail_val->type == Json_String) {
+ item.detail = s8dup(detail_val->value.string);
+ }
+
+ struct json_value *sort_txt_val = json_get(obj, s8("sortText"));
+ if (sort_txt_val != NULL && sort_txt_val->type == Json_String) {
+ item.sort_text = s8dup(sort_txt_val->value.string);
+ }
+
+ struct json_value *filter_txt_val = json_get(obj, s8("filterText"));
+ if (filter_txt_val != NULL && filter_txt_val->type == Json_String) {
+ item.filter_text = s8dup(filter_txt_val->value.string);
+ }
+
+ struct json_value *insert_txt_val = json_get(obj, s8("insertText"));
+ if (insert_txt_val != NULL && insert_txt_val->type == Json_String) {
+ item.insert_text = s8dup(insert_txt_val->value.string);
+ }
+
+ // determine type of edit
+ struct json_value *edit_val = json_get(obj, s8("textEdit"));
+ item.edit_type = TextEdit_None;
+ if (edit_val != NULL && edit_val->type == Json_Object) {
+ struct json_object *edit_obj = edit_val->value.object;
+
+ struct json_value *insert_val = json_get(edit_obj, s8("insert"));
+
+ if (insert_val != NULL) {
+ item.edit_type = TextEdit_InsertReplaceEdit;
+ item.edit.insert_replace_edit = (struct insert_replace_edit){
+ .insert =
+ parse_region(json_get(edit_obj, s8("insert"))->value.object),
+ .replace =
+ parse_region(json_get(edit_obj, s8("replace"))->value.object),
+ .new_text = unescape_json_string(
+ json_get(edit_obj, s8("newText"))->value.string),
+ };
+ } else {
+ item.edit_type = TextEdit_TextEdit;
+ item.edit.text_edit = (struct text_edit){
+ .range = parse_region(json_get(edit_obj, s8("range"))->value.object),
+ .new_text = unescape_json_string(
+ json_get(edit_obj, s8("newText"))->value.string),
+ };
+ }
+ }
+
+ struct json_value *additional_txt_edits_val =
+ json_get(obj, s8("additionalTextEdits"));
+ if (additional_txt_edits_val != NULL &&
+ additional_txt_edits_val->type == Json_Array) {
+ item.additional_text_edits = text_edits_from_json(additional_txt_edits_val);
+ }
+
+ struct json_value *command_val = json_get(obj, s8("command"));
+ if (command_val != NULL && command_val->type == Json_Object) {
+ item.command = lsp_command_from_json(command_val);
+ }
+
+ VEC_PUSH(vec, item);
+}
+
+struct completion_list completion_list_from_json(struct json_value *json) {
+
+ if (json->type == Json_Null) {
+ return (struct completion_list){
+ .incomplete = false,
+ };
+ }
+
+ struct completion_list complist;
+ complist.incomplete = false;
+
+ struct json_array *js_items = NULL;
+ if (json->type == Json_Object) {
+ struct json_object *obj = json->value.object;
+ complist.incomplete = json_get(obj, s8("isIncomplete"))->value.boolean;
+ js_items = json_get(obj, s8("items"))->value.array;
+ } else if (json->type == Json_Array) {
+ js_items = json->value.array;
+ } else {
+ return (struct completion_list){
+ .incomplete = false,
+ };
+ }
+
+ // parse the list
+ VEC_INIT(&complist.items, json_array_len(js_items));
+ json_array_foreach(js_items, &complist.items, parse_completion_item);
+
+ return complist;
+}
+
+void completion_list_free(struct completion_list *complist) {
+ VEC_FOR_EACH(&complist->items, struct lsp_completion_item * item) {
+ s8delete(item->label);
+ s8delete(item->detail);
+ s8delete(item->sort_text);
+ s8delete(item->filter_text);
+ s8delete(item->insert_text);
+
+ if (item->edit_type == TextEdit_TextEdit) {
+ s8delete(item->edit.text_edit.new_text);
+ } else {
+ s8delete(item->edit.insert_replace_edit.new_text);
+ }
+
+ text_edits_free(item->additional_text_edits);
+ lsp_command_free(&item->command);
+ }
+
+ VEC_DESTROY(&complist->items);
+}
+
+struct s8 rename_params_to_json(struct rename_params *params) {
+
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, "
+ "\"position\": { \"line\": %d, \"character\": %d }, "
+ "\"newName\": \"%.*s\" }";
+
+ struct text_document_position *position = &params->position;
+ struct s8 escaped = escape_json_string(params->new_name);
+ struct s8 json =
+ s8from_fmt(fmt, position->uri.l, position->uri.s, position->position.line,
+ position->position.col, escaped.l, escaped.s);
+
+ s8delete(escaped);
+ return json;
+}
+
+static void parse_parameter(uint64_t id, struct json_value *json,
+ void *userdata) {
+
+ (void)id;
+ param_info_vec *vec = (param_info_vec *)userdata;
+ struct json_object *obj = json->value.object;
+
+ struct parameter_information info;
+ struct json_value *label = json_get(obj, s8("label"));
+ if (label != NULL && label->type == Json_String) {
+ info.label = s8dup(label->value.string);
+ }
+
+ struct json_value *doc = json_get(obj, s8("documentation"));
+ if (doc != NULL && doc->type == Json_String) {
+ info.documentation = s8dup(doc->value.string);
+ }
+ VEC_PUSH(vec, info);
+}
+
+static void parse_signature(uint64_t id, struct json_value *json,
+ void *userdata) {
+
+ (void)id;
+ signature_info_vec *vec = (signature_info_vec *)userdata;
+
+ struct json_object *obj = json->value.object;
+
+ struct signature_information info;
+ struct json_value *label = json_get(obj, s8("label"));
+ if (label != NULL && label->type == Json_String) {
+ info.label = s8dup(label->value.string);
+ }
+
+ struct json_value *doc = json_get(obj, s8("documentation"));
+ if (doc != NULL && doc->type == Json_String) {
+ info.documentation = s8dup(doc->value.string);
+ }
+
+ struct json_value *params = json_get(obj, s8("parameters"));
+ if (params != NULL && params->type == Json_Array) {
+ struct json_array *arr = params->value.array;
+ VEC_INIT(&info.parameters, json_array_len(arr));
+ json_array_foreach(arr, &info.parameters, parse_parameter);
+ }
+
+ VEC_PUSH(vec, info);
+}
+
+struct signature_help signature_help_from_json(struct json_value *value) {
+ struct signature_help help = {0};
+ struct json_object *obj = value->value.object;
+
+ struct json_value *active_sig = json_get(obj, s8("activeSignature"));
+ if (active_sig != NULL && active_sig->type == Json_Number) {
+ help.active_signature = active_sig->value.number;
+ }
+
+ struct json_value *sigs = json_get(obj, s8("signatures"));
+ if (sigs != NULL && sigs->type == Json_Array) {
+ struct json_array *arr = sigs->value.array;
+ VEC_INIT(&help.signatures, json_array_len(arr));
+ json_array_foreach(arr, &help.signatures, parse_signature);
+ }
+
+ return help;
+}
+
+void signature_help_free(struct signature_help *help) {
+ VEC_FOR_EACH(&help->signatures, struct signature_information * info) {
+ s8delete(info->label);
+ s8delete(info->documentation);
+
+ VEC_FOR_EACH(&info->parameters, struct parameter_information * pinfo) {
+ s8delete(pinfo->label);
+ s8delete(pinfo->documentation);
+ }
+
+ VEC_DESTROY(&info->parameters);
+ }
+
+ VEC_DESTROY(&help->signatures);
+}
+
+struct hover hover_from_json(struct json_value *value) {
+ struct hover hover = {0};
+ struct json_object *obj = value->value.object;
+
+ struct json_value *contents = json_get(obj, s8("contents"));
+ if (contents != NULL) {
+ switch (contents->type) {
+ case Json_String:
+ hover.contents = unescape_json_string(contents->value.string);
+ break;
+ case Json_Object: {
+ struct json_value *val = json_get(contents->value.object, s8("value"));
+ if (val != NULL && val->type == Json_String) {
+ hover.contents = unescape_json_string(val->value.string);
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+
+ struct json_value *range = json_get(obj, s8("range"));
+ if (range != NULL && range->type == Json_Object) {
+ hover.range = parse_region(range->value.object);
+ }
+
+ return hover;
+}
+
+void hover_free(struct hover *hover) { s8delete(hover->contents); }
+
+struct s8 reference_params_to_json(struct reference_params *params) {
+ const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, "
+ "\"position\": { \"line\": %d, \"character\": %d }, "
+ "\"includeDeclaration\": \"%s\" }";
+
+ struct text_document_position *position = &params->position;
+ struct s8 json = s8from_fmt(fmt, position->uri.l, position->uri.s,
+ position->position.line, position->position.col,
+ params->include_declaration ? "true" : "false");
+
+ return json;
+}
diff --git a/src/main/lsp/types.h b/src/main/lsp/types.h
new file mode 100644
index 0000000..7b6ba1a
--- /dev/null
+++ b/src/main/lsp/types.h
@@ -0,0 +1,385 @@
+#ifndef _LSP_TYPES_H
+#define _LSP_TYPES_H
+
+#include "dged/json.h"
+#include "dged/location.h"
+#include "dged/s8.h"
+#include "dged/vec.h"
+
+struct buffer;
+
+struct client_capabilities {};
+
+struct workspace_folder {
+ struct s8 uri;
+ struct s8 name;
+};
+
+struct initialize_params {
+ int process_id;
+ struct client_info {
+ struct s8 name;
+ struct s8 version;
+ } client_info;
+
+ struct client_capabilities client_capabilities;
+
+ struct workspace_folder *workspace_folders;
+ size_t nworkspace_folders;
+};
+
+enum text_document_sync_kind {
+ TextDocumentSync_None = 0,
+ TextDocumentSync_Full = 1,
+ TextDocumentSync_Incremental = 2,
+};
+
+struct text_document_sync {
+ enum text_document_sync_kind kind;
+ bool open_close;
+ bool save;
+};
+
+enum position_encoding_kind {
+ PositionEncoding_Utf8,
+ PositionEncoding_Utf16,
+ PositionEncoding_Utf32,
+};
+
+struct completion_options {
+ VEC(struct s8) trigger_characters;
+ VEC(struct s8) all_commit_characters;
+ bool resolve_provider;
+};
+
+struct server_capabilities {
+ struct text_document_sync text_document_sync;
+ enum position_encoding_kind position_encoding;
+ bool supports_completion;
+ struct completion_options completion_options;
+};
+
+struct initialize_result {
+ struct server_capabilities capabilities;
+ struct server_info {
+ struct s8 name;
+ struct s8 version;
+ } server_info;
+};
+
+struct s8 initialize_params_to_json(struct initialize_params *params);
+struct initialize_result initialize_result_from_json(struct json_value *json);
+void initialize_result_free(struct initialize_result *);
+struct s8 position_encoding_kind_str(enum position_encoding_kind);
+
+struct text_document_item {
+ struct s8 uri;
+ struct s8 language_id;
+ uint64_t version;
+ struct s8 text;
+};
+
+struct text_document_identifier {
+ struct s8 uri;
+};
+
+struct text_document_position {
+ struct s8 uri;
+ struct location position;
+};
+
+struct text_document_location {
+ struct s8 uri;
+ struct region range;
+};
+
+struct versioned_text_document_identifier {
+ struct s8 uri;
+ uint64_t version;
+};
+
+struct did_open_text_document_params {
+ struct text_document_item text_document;
+};
+
+enum location_type {
+ Location_Single,
+ Location_Array,
+ Location_Link,
+ Location_Null,
+};
+
+typedef VEC(struct text_document_location) location_vec;
+
+struct location_result {
+ enum location_type type;
+ union location_data {
+ struct text_document_location single;
+ location_vec array;
+ } location;
+};
+
+struct did_change_text_document_params {
+ struct versioned_text_document_identifier text_document;
+ struct text_document_content_change_event *content_changes;
+ size_t ncontent_changes;
+};
+
+struct did_save_text_document_params {
+ struct text_document_identifier text_document;
+};
+
+struct text_document_content_change_event {
+ struct region range;
+ struct s8 text;
+ bool full_document;
+};
+
+enum diagnostic_severity {
+ LspDiagnostic_Error = 1,
+ LspDiagnostic_Warning = 2,
+ LspDiagnostic_Information = 3,
+ LspDiagnostic_Hint = 4,
+};
+
+struct diagnostic {
+ struct s8 message;
+ struct s8 source;
+ struct region region;
+ enum diagnostic_severity severity;
+};
+
+typedef VEC(struct diagnostic) diagnostic_vec;
+
+struct publish_diagnostics_params {
+ struct s8 uri;
+ uint64_t version;
+ diagnostic_vec diagnostics;
+};
+
+struct code_action_context {
+ diagnostic_vec diagnostics;
+};
+
+struct code_action_params {
+ struct text_document_identifier text_document;
+ struct region range;
+ struct code_action_context context;
+};
+
+struct text_edit {
+ struct region range;
+ struct s8 new_text;
+};
+
+typedef VEC(struct text_edit) text_edit_vec;
+
+struct text_edit_pair {
+ struct s8 uri;
+ text_edit_vec edits;
+};
+
+typedef VEC(struct text_edit_pair) change_vec;
+
+struct workspace_edit {
+ change_vec changes;
+};
+
+struct lsp_command {
+ struct s8 title;
+ struct s8 command;
+ struct s8 arguments;
+};
+
+struct code_action {
+ struct s8 title;
+ struct s8 kind;
+
+ bool has_edit;
+ struct workspace_edit edit;
+
+ bool has_command;
+ struct lsp_command command;
+};
+
+typedef VEC(struct lsp_command) lsp_command_vec;
+typedef VEC(struct code_action) code_action_vec;
+
+struct code_actions {
+ lsp_command_vec commands;
+ code_action_vec code_actions;
+};
+
+struct formatting_options {
+ size_t tab_size;
+ bool use_spaces;
+};
+
+struct document_formatting_params {
+ struct text_document_identifier text_document;
+ struct formatting_options options;
+};
+
+struct document_range_formatting_params {
+ struct text_document_identifier text_document;
+ struct region range;
+ struct formatting_options options;
+};
+
+enum completion_item_kind {
+ CompletionItem_Text = 1,
+ CompletionItem_Method = 2,
+ CompletionItem_Function = 3,
+ CompletionItem_Constructor = 4,
+ CompletionItem_Field = 5,
+ CompletionItem_Variable = 6,
+ CompletionItem_Class = 7,
+ CompletionItem_Interface = 8,
+ CompletionItem_Module = 9,
+ CompletionItem_Property = 10,
+ CompletionItem_Unit = 11,
+ CompletionItem_Value = 12,
+ CompletionItem_Enum = 13,
+ CompletionItem_Keyword = 14,
+ CompletionItem_Snippet = 15,
+ CompletionItem_Color = 16,
+ CompletionItem_File = 17,
+ CompletionItem_Reference = 18,
+ CompletionItem_Folder = 19,
+ CompletionItem_EnumMember = 20,
+ CompletionItem_Constant = 21,
+ CompletionItem_Struct = 22,
+ CompletionItem_Event = 23,
+ CompletionItem_Operator = 24,
+ CompletionItem_TypeParameter = 25,
+};
+
+enum text_edit_type {
+ TextEdit_None,
+ TextEdit_TextEdit,
+ TextEdit_InsertReplaceEdit,
+};
+
+struct insert_replace_edit {
+ struct s8 new_text;
+ struct region insert;
+ struct region replace;
+};
+
+struct lsp_completion_item {
+ struct s8 label;
+ enum completion_item_kind kind;
+ struct s8 detail;
+ struct s8 sort_text;
+ struct s8 filter_text;
+ struct s8 insert_text;
+
+ enum text_edit_type edit_type;
+ union edit_ {
+ struct text_edit text_edit;
+ struct insert_replace_edit insert_replace_edit;
+ } edit;
+
+ text_edit_vec additional_text_edits;
+
+ struct lsp_command command;
+};
+
+typedef VEC(struct lsp_completion_item) completions_vec;
+
+struct completion_list {
+ bool incomplete;
+ completions_vec items;
+};
+
+struct rename_params {
+ struct text_document_position position;
+ struct s8 new_name;
+};
+
+struct parameter_information {
+ struct s8 label;
+ struct s8 documentation;
+};
+
+typedef VEC(struct parameter_information) param_info_vec;
+
+struct signature_information {
+ struct s8 label;
+ struct s8 documentation;
+ param_info_vec parameters;
+};
+
+typedef VEC(struct signature_information) signature_info_vec;
+
+struct signature_help {
+ uint32_t active_signature;
+ signature_info_vec signatures;
+};
+
+struct hover {
+ struct s8 contents;
+ struct region range;
+};
+
+struct reference_params {
+ struct text_document_position position;
+ bool include_declaration;
+};
+
+struct text_document_item text_document_item_from_buffer(struct buffer *buffer);
+struct versioned_text_document_identifier
+versioned_identifier_from_buffer(struct buffer *buffer);
+
+void versioned_text_document_identifier_free(
+ struct versioned_text_document_identifier *);
+void text_document_item_free(struct text_document_item *);
+
+struct s8 did_change_text_document_params_to_json(
+ struct did_change_text_document_params *);
+struct s8
+did_open_text_document_params_to_json(struct did_open_text_document_params *);
+struct s8
+did_save_text_document_params_to_json(struct did_save_text_document_params *);
+
+struct publish_diagnostics_params
+diagnostics_from_json(struct json_value *json);
+
+const char *diag_severity_to_str(enum diagnostic_severity severity);
+uint32_t diag_severity_color(enum diagnostic_severity severity);
+void diagnostic_free(struct diagnostic *);
+
+struct s8 document_position_to_json(struct text_document_position *position);
+struct location_result location_result_from_json(struct json_value *json);
+void location_result_free(struct location_result *res);
+
+struct s8 code_action_params_to_json(struct code_action_params *);
+
+struct code_actions lsp_code_actions_from_json(struct json_value *);
+void lsp_code_actions_free(struct code_actions *);
+struct s8 lsp_command_to_json(struct lsp_command *);
+
+text_edit_vec text_edits_from_json(struct json_value *);
+void text_edits_free(text_edit_vec);
+struct workspace_edit workspace_edit_from_json(struct json_value *);
+void workspace_edit_free(struct workspace_edit *);
+
+struct s8
+document_formatting_params_to_json(struct document_formatting_params *);
+struct s8 document_range_formatting_params_to_json(
+ struct document_range_formatting_params *);
+
+struct completion_list completion_list_from_json(struct json_value *);
+void completion_list_free(struct completion_list *);
+
+struct s8 rename_params_to_json(struct rename_params *);
+
+struct signature_help signature_help_from_json(struct json_value *);
+void signature_help_free(struct signature_help *);
+
+struct hover hover_from_json(struct json_value *);
+void hover_free(struct hover *);
+
+struct s8 reference_params_to_json(struct reference_params *);
+
+#endif
diff --git a/src/main/main.c b/src/main/main.c
index fa740e8..12ed1ec 100644
--- a/src/main/main.c
+++ b/src/main/main.c
@@ -38,6 +38,7 @@
#include "bindings.h"
#include "cmds.h"
#include "completion.h"
+#include "frame-hooks.h"
#include "version.h"
/* welcome.h is generated from welcome.inc with
@@ -86,12 +87,21 @@ void segfault(int sig) {
abort();
}
+/* void __asan_on_error() {
+ if (display != NULL) {
+ display_clear(display);
+ display_destroy(display);
+ }
+} */
+
#define INVALID_WATCH (uint32_t) - 1
static void clear_buffer_props(struct buffer *buffer, void *userdata) {
(void)userdata;
- buffer_clear_text_properties(buffer);
+ if (!buffer->retain_properties) {
+ buffer_clear_text_properties(buffer);
+ }
}
struct watched_file {
@@ -275,6 +285,10 @@ int main(int argc, char *argv[]) {
buffers_add_add_hook(&buflist, watch_file, (void *)reactor);
+ init_bindings();
+
+ init_completion(&buflist);
+
#ifdef SYNTAX_ENABLE
char *treesitter_path_env = getenv("TREESITTER_GRAMMARS");
struct setting *path_setting = settings_get("editor.grammars-path");
@@ -324,7 +338,7 @@ int main(int argc, char *argv[]) {
#endif
#ifdef LSP_ENABLE
- lang_servers_init(reactor, &buflist);
+ lang_servers_init(reactor, &buflist, &commands);
#endif
struct buffer initial_buffer = buffer_create("welcome");
@@ -361,20 +375,21 @@ int main(int argc, char *argv[]) {
register_settings_commands(&commands);
struct keymap *current_keymap = NULL;
- init_bindings();
-
- init_completion(&buflist, &commands);
timers_init();
+ init_frame_hooks();
float frame_time = 0.f;
static char keyname[64] = {0};
static uint32_t nkeychars = 0;
+ bool needs_render = true;
+
while (running) {
timers_start_frame();
if (display_resized) {
windows_resize(display_height(display), display_width(display));
display_resized = false;
+ needs_render = true;
}
// TODO: maybe this should be hidden behind something
@@ -383,7 +398,7 @@ int main(int argc, char *argv[]) {
/* Update all windows together with the buffers in them. */
struct timer *update_windows = timer_start("update-windows");
- windows_update(frame_alloc, frame_time);
+ needs_render |= windows_update(frame_alloc, frame_time);
timer_stop(update_windows);
struct window *active_window = windows_get_active();
@@ -392,26 +407,36 @@ int main(int argc, char *argv[]) {
* from updating the buffers.
*/
struct timer *update_display = timer_start("display");
- display_begin_render(display);
- windows_render(display);
- struct buffer_view *view = window_buffer_view(active_window);
- struct location cursor = buffer_view_dot_to_visual(view);
- struct window_position winpos = window_position(active_window);
- display_move_cursor(display, winpos.y + cursor.line, winpos.x + cursor.col);
- display_end_render(display);
+ if (needs_render) {
+ display_begin_render(display);
+ windows_render(display);
+ struct buffer_view *view = window_buffer_view(active_window);
+ struct location cursor = buffer_view_dot_to_visual(view);
+ struct window_position winpos = window_position(active_window);
+ display_move_cursor(display, winpos.y + cursor.line,
+ winpos.x + cursor.col);
+ display_end_render(display);
+ needs_render = false;
+ }
timer_stop(update_display);
- /* This blocks for events, so if nothing has happened we block here and let
- * the CPU do something more useful than updating this editor for no reason.
- * This is also the reason that there is no timed scope around this, it
- * simply makes no sense.
+ /* if we have dispatched frame hooks, they need a
+ * full cycle of updates.
*/
- reactor_update(reactor);
+ if (dispatch_next_frame_hooks() == 0) {
+ /* This blocks for events, so if nothing has happened we block here and
+ * let the CPU do something more useful than updating this editor for no
+ * reason. This is also the reason that there is no timed scope around
+ * this, it simply makes no sense.
+ */
+ reactor_update(reactor);
+ }
struct timer *update_keyboard = timer_start("update-keyboard");
struct keyboard_update kbd_upd =
keyboard_update(&kbd, reactor, frame_alloc);
+ needs_render |= kbd_upd.nkeys > 0;
for (uint32_t ki = 0; ki < kbd_upd.nkeys; ++ki) {
struct key *k = &kbd_upd.keys[ki];
@@ -457,7 +482,7 @@ int main(int argc, char *argv[]) {
if (nkeychars < 64) {
nkeychars += key_name(k, keyname + nkeychars, 64 - nkeychars);
- minibuffer_echo("%s", keyname);
+ minibuffer_display("%s", keyname);
}
current_keymap = res.data.keymap;
@@ -472,10 +497,10 @@ int main(int argc, char *argv[]) {
char keyname[16];
key_name(k, keyname, 16);
if (current_keymap == NULL) {
- minibuffer_echo_timeout(4, "key \"%s\" is not bound!", keyname);
+ minibuffer_display_timeout(4, "key \"%s\" is not bound!", keyname);
} else {
- minibuffer_echo_timeout(4, "key \"%s %s\" is not bound!",
- current_keymap->name, keyname);
+ minibuffer_display_timeout(4, "key \"%s %s\" is not bound!",
+ current_keymap->name, keyname);
}
current_keymap = NULL;
nkeychars = 0;
@@ -498,13 +523,10 @@ int main(int argc, char *argv[]) {
frame_allocator_clear(&frame_allocator);
}
+ teardown_frame_hooks();
timers_destroy();
teardown_global_commands();
- destroy_completion();
windows_destroy();
- minibuffer_destroy();
- buffer_destroy(&minibuffer);
- buffers_destroy(&buflist);
#ifdef SYNTAX_ENABLE
syntax_teardown();
@@ -514,6 +536,11 @@ int main(int argc, char *argv[]) {
lang_servers_teardown();
#endif
+ destroy_completion();
+ minibuffer_destroy();
+ buffer_destroy(&minibuffer);
+ buffers_destroy(&buflist);
+
display_clear(display);
display_destroy(display);
destroy_bindings();