diff options
| -rw-r--r-- | README | 3 | ||||
| -rw-r--r-- | doc/sciteco.7.template | 13 | ||||
| -rw-r--r-- | src/cmdline.c | 22 | ||||
| -rw-r--r-- | src/lsp.c | 206 | ||||
| -rw-r--r-- | src/lsp.h | 4 |
5 files changed, 244 insertions, 4 deletions
@@ -86,6 +86,9 @@ Features in single letter Q-Register names. * 8-bit clean: SciTECO can be used to edit binary files if the encoding is changed to ANSI and automatic EOL conversion is turned off (easiest with `--8bit`). +* Supports lookups and auto-completions via + [language servers](https://microsoft.github.io/language-server-protocol/) (LSP). + Use [ctags-lsp](https://microsoft.github.io/language-server-protocol/) to support Ctags. * Self-documenting: An integrated indexed help system allows browsing formatted documentation about commands, macros and concepts within SciTECO (`?` command). Macro packages can be documented with the `tedoc` tool, generating man pages. diff --git a/doc/sciteco.7.template b/doc/sciteco.7.template index b3b2aaa..4b6987a 100644 --- a/doc/sciteco.7.template +++ b/doc/sciteco.7.template @@ -654,9 +654,22 @@ effectively opening the corresponding \fIwomanpage\fP, except if the \(lq{\(rq terminator is used. T} T{ +Auto complete via LSP +T};9;^I, Tab;T{ +Text insertion arguments +.br +(modifier \fIenabled\fP and language server started) +T};T{ +Auto completes the symbol at the current buffer position using the +language server protocol (LSP). +See also \fBFT\fP command. +T} +T{ Indent to next tab stop T};9;^I, Tab;T{ Text insertion arguments +.br +(modifier \fIdisabled\fP) T};T{ Expands to spaces so that following text is inserted at the next tab stop into the buffer. diff --git a/src/cmdline.c b/src/cmdline.c index a4e2189..1abf8df 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -911,8 +911,28 @@ teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *par return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); switch (key) { - case '\t': { /* insert <TAB> indention */ + case '\t': { /* insert <TAB> indention or auto complete */ + if (teco_cmdline.modifier_enabled && !teco_qreg_current && teco_lsp_is_running()) { + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_scroll(); + return TRUE; + } + + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_lsp_buffer_auto_complete(teco_ring_current, &new_chars); + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, + new_chars.len, &new_chars_escaped); + if (unambiguous) { + teco_string_append_c(&new_chars_escaped, ' '); + teco_cmdline_modifier_set(FALSE); + } + + return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error); + } + if (teco_cmdline.modifier_enabled || teco_interface_ssm(SCI_GETUSETABS, 0, 0)) + /* perhaps auto-complete file name */ break; teco_interface_popup_clear(); @@ -193,6 +193,12 @@ teco_json_unescape(sj_Value val) return str; } +gboolean +teco_lsp_is_running(void) +{ + return teco_lsp.pid != 0; +} + static teco_lsp_result_t * teco_lsp_result_new(const gchar *filename, guint line, guint column) { @@ -1023,7 +1029,7 @@ teco_lsp_symbol_auto_complete(const gchar *symbol, teco_string_t *insert) !teco_json_eq(key, "name")); gchar *name = teco_json_unescape(val); - if (strncmp(name, symbol, symbol_len) != 0) { + if (!g_str_has_prefix(name, symbol)) { g_free(name); continue; } @@ -1033,8 +1039,8 @@ teco_lsp_symbol_auto_complete(const gchar *symbol, teco_string_t *insert) list_str.data = (gchar *)list->data + symbol_len; list_str.len = strlen(list_str.data); - gsize len = teco_string_casediff(list_str, (gchar *)name + symbol_len, - strlen(name) - symbol_len); + gsize len = teco_string_diff(list_str, (gchar *)name + symbol_len, + strlen(name) - symbol_len); if (len < prefix_len) prefix_len = len; } else { @@ -1211,6 +1217,196 @@ teco_lsp_lookup_references(teco_buffer_t *buffer, teco_int_t pos, GError **error } static gboolean +teco_lsp_parse_range(teco_buffer_t *buffer, sj_Reader *reader, + sj_Value obj, Sci_Position *start, Sci_Position *end) +{ + if (obj.type != SJ_OBJECT) + return FALSE; + + sj_Value key, val; + while (sj_iter_object(reader, obj, &key, &val)) { + Sci_Position *p = NULL; + + if (teco_json_eq(key, "start")) + p = start; + else if (teco_json_eq(key, "end")) + p = end; + else + continue; + + sj_Value obj = val; + if (obj.type != SJ_OBJECT) + return FALSE; + + gint character = 0, line = 0; + while (sj_iter_object(reader, obj, &key, &val)) + if (teco_json_eq(key, "character")) + character = atoi(val.start); + else if (teco_json_eq(key, "line")) + line = atoi(val.start); + + *p = teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, line, 0) + character; + } + + return TRUE; +} + +/** + * Auto complete a symbol from a buffer at dot. + * + * This is not using the native Scintilla autocompletion popups + * because we cannot reliably inhibit mouse interactions and we + * cannot programmatically scroll through the list as well. + */ +gboolean +teco_lsp_buffer_auto_complete(teco_buffer_t *buffer, teco_string_t *insert) +{ + memset(insert, 0, sizeof(*insert)); + + /* server is started on demand */ + if (G_UNLIKELY(!teco_lsp.pid) && !teco_lsp_launch(NULL)) + return FALSE; + + gsize dot_bytes = teco_view_ssm(buffer->view, SCI_GETCURRENTPOS, 0, 0); + guint line = teco_view_ssm(buffer->view, SCI_LINEFROMPOSITION, dot_bytes, 0); + guint column = dot_bytes - teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, line, 0); + + g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, NULL); + if (!uri) + return FALSE; + g_autofree gchar *uri_escaped = teco_json_escape(uri, strlen(uri)); + g_autofree gchar *req = g_strdup_printf("{" + "\"jsonrpc\":\"2.0\"," + "\"id\":" TECO_LSP_ID "," + "\"method\":\"textDocument/completion\"," + "\"params\":{" + "\"textDocument\":{" + "\"uri\":\"%s\"" + "}," + "\"position\":{\"line\":%u,\"character\":%u}," + "\"context\":{" + "\"triggerKind\":1" /* manual */ + "}" + "}" + "}", uri_escaped, line, column); + if (!teco_lsp_send(req, NULL)) + return FALSE; + + g_auto(teco_string_t) resp = {NULL, 0}; + if (!teco_lsp_recv(&resp, NULL)) + return FALSE; + + sj_Reader reader = sj_reader(resp.data, resp.len); + sj_Value obj = sj_read(&reader); + g_assert(obj.type == SJ_OBJECT); + + sj_Value key, val = {0}; + while (sj_iter_object(&reader, obj, &key, &val) && + !teco_json_eq(key, "result")); + if (val.type != SJ_OBJECT) + return FALSE; + + obj = val; + while (sj_iter_object(&reader, obj, &key, &val) && + !teco_json_eq(key, "items")); + sj_Value arr = val; + if (arr.type != SJ_ARRAY) + /* can also be `null` in some LSPs */ + return FALSE; + + GSList *list = NULL; + guint list_len = 0; + gsize symbol_len = 0; + /** length of common prefix among all matching results */ + gsize prefix_len = 0; + + while (sj_iter_array(&reader, arr, &obj)) { + if (obj.type != SJ_OBJECT) + /* shouldn't happen */ + continue; + + while (sj_iter_object(&reader, obj, &key, &val) && + !teco_json_eq(key, "textEdit")); + sj_Value obj = val; + if (obj.type != SJ_OBJECT) + continue; + + g_autofree gchar *name = NULL; + struct Sci_TextRangeFull range = {0}; + + /* + * Will usually contain the entire symbol name in "newText" + * and a "range" to delete first. + * We cannot really delete anything before dot, so we just try + * to skip the prefix that is already in the buffer. + */ + while (sj_iter_object(&reader, obj, &key, &val)) + if (teco_json_eq(key, "newText")) + name = teco_json_unescape(val); + else if (teco_json_eq(key, "range")) + teco_lsp_parse_range(buffer, &reader, val, + &range.chrg.cpMin, &range.chrg.cpMax); + if (!name || range.chrg.cpMin > range.chrg.cpMax || + range.chrg.cpMax - range.chrg.cpMin >= strlen(name)) + continue; + + /* + * We are supposed to delete the range from rang.chrg, + * but this doesn't fit into SciTECO's command line editing where + * we can only guarantee inserting strings (`I` argument could still be empty). + * clangd will offer completions that are not suffixes of what's + * under dot, so we have to filter those out. + */ + symbol_len = range.chrg.cpMax - range.chrg.cpMin; + g_autofree gchar *range_buf = g_malloc(symbol_len + 1); + range.lpstrText = range_buf; + teco_view_ssm(buffer->view, SCI_GETTEXTRANGEFULL, 0, (sptr_t)&range); + if (!g_str_has_prefix(name, range_buf)) + continue; + + if (list) { + teco_string_t list_str; + list_str.data = (gchar *)list->data + symbol_len; + list_str.len = strlen(list_str.data); + + gsize len = teco_string_diff(list_str, (gchar *)name + symbol_len, + strlen(name) - symbol_len); + if (!prefix_len || len < prefix_len) + prefix_len = len; + } else { + prefix_len = strlen(name) - symbol_len; + } + + /* + * There are also "label" keys, but the "labels" contain + * entire function signatures. + */ + list = g_slist_prepend(list, g_steal_pointer(&name)); + list_len++; + } + + if (prefix_len > 0) { + teco_string_init(insert, (gchar *)list->data + symbol_len, prefix_len); + } else if (list_len > 1) { + /* + * Assume that the order we receive completions is significant, + * so we don't just alphabetically sort here. + */ + list = g_slist_reverse(list); + + for (GSList *entry = list; entry != NULL; entry = g_slist_next(entry)) { + const gchar *name = entry->data; + teco_interface_popup_add(TECO_POPUP_PLAIN, name, strlen(name), FALSE); + } + + teco_interface_popup_show(symbol_len); + } + + g_slist_free_full(list, g_free); + return list_len == 1; +} + +static gboolean teco_lsp_shutdown(GError **error) { if (!teco_lsp.pid) @@ -1380,6 +1576,10 @@ gboolean teco_state_lsp_lookup_insert_completion(teco_machine_main_t *ctx, teco_ * Therefore the first lookup may well fail. You can use * \(lqFT\fB$\fP\(rq to force a language server startup. * \(lq\-FT\fB$\fP\(rq will shut down any running language server. + * + * Once the language server has been launched, you can + * auto-complete symbols in buffers by pressing Tab after activating + * the immediate editing modifier (CTRL+G). */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_lsp_lookup, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_lsp_lookup_process_edit_cmd, @@ -19,8 +19,11 @@ #include <glib.h> #include "parser.h" +#include "string-utils.h" #include "ring.h" +gboolean teco_lsp_is_running(void); + gboolean teco_lsp_didopen(teco_buffer_t *buffer, GError **error); gboolean teco_lsp_didchange_insert(teco_buffer_t *buffer, gsize pos, gsize len, const gchar *text, GError **error); @@ -31,5 +34,6 @@ gboolean teco_lsp_didclose(teco_buffer_t *buffer, GError **error); gboolean teco_lsp_sync(teco_buffer_t *buffer, GError **error); gboolean teco_lsp_symbol_auto_complete(const gchar *symbol, teco_string_t *insert); +gboolean teco_lsp_buffer_auto_complete(teco_buffer_t *buffer, teco_string_t *insert); extern teco_state_t teco_state_lsp_lookup; |
