From 4459b8b3aa9d73895391785a99dcc87134e80601 Mon Sep 17 00:00:00 2001 From: Albert Cervin Date: Tue, 17 Sep 2024 08:47:03 +0200 Subject: More lsp support This makes the LSP support complete for now: - Completion - Diagnostics - Goto implementation/declaration - Rename - Documentation - Find references --- src/main/completion/buffer.c | 148 +++++++++++++++++++++++ src/main/completion/buffer.h | 18 +++ src/main/completion/command.c | 151 ++++++++++++++++++++++++ src/main/completion/command.h | 17 +++ src/main/completion/path.c | 268 ++++++++++++++++++++++++++++++++++++++++++ src/main/completion/path.h | 14 +++ 6 files changed, 616 insertions(+) create mode 100644 src/main/completion/buffer.c create mode 100644 src/main/completion/buffer.h create mode 100644 src/main/completion/command.c create mode 100644 src/main/completion/command.h create mode 100644 src/main/completion/path.c create mode 100644 src/main/completion/path.h (limited to 'src/main/completion') 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 + +#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 +#include + +#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 +#include +#include +#include +#include +#include +#include +#include + +#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 -- cgit v1.2.3