aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/lsp.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lsp.c')
-rw-r--r--src/lsp.c1380
1 files changed, 1380 insertions, 0 deletions
diff --git a/src/lsp.c b/src/lsp.c
new file mode 100644
index 0000000..0b33885
--- /dev/null
+++ b/src/lsp.c
@@ -0,0 +1,1380 @@
+/*
+ * Copyright (C) 2012-2026 Robin Haberkorn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <glib.h>
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#define SJ_IMPL
+#include <sj.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "expressions.h"
+#include "error.h"
+#include "view.h"
+#include "undo.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "spawn.h"
+#include "ring.h"
+#include "list.h"
+#include "qreg.h"
+#include "lsp.h"
+
+static gboolean teco_lsp_shutdown(GError **error);
+
+/*
+ * FIXME: Should perhaps be an array.
+ * But then we'd have to iterate results array once
+ * to get the number of results.
+ */
+typedef struct {
+ teco_stailq_entry_t entry;
+
+ guint line, column;
+ gchar filename[];
+} teco_lsp_result_t;
+
+static struct {
+ /** Pid of the language server */
+ GPid pid;
+ GIOChannel *stdin_chan, *stdout_chan;
+
+ teco_stailq_head_t list;
+ teco_lsp_result_t *current;
+} teco_lsp = {
+ .pid = -1,
+ .list = TECO_STAILQ_HEAD_INITIALIZER(&teco_lsp.list)
+};
+
+/**
+ * Compare JSON value to string.
+ * If value is a SJ_STRING, it is \b not unescaped first,
+ * so this makes sense for plain string keys only.
+ */
+static inline gboolean
+teco_json_eq(sj_Value val, const char *str)
+{
+ return !strncmp(str, val.start, val.end - val.start);
+}
+
+static gsize
+teco_json_escape_len(const gchar *str, gsize len)
+{
+ gsize ret = 0;
+
+ while (len > 0) {
+ /*
+ * NOTE: Perhaps it would be more efficient to just escape
+ * everything with \u00XX.
+ * This overallocates in teco_json_escape(), but avoids
+ * redundancies with teco_lsp_send_escaped().
+ */
+ if (*str && strchr("\"\\\b\f\n\r\t", *str))
+ ret += 2;
+ else if (TECO_IS_CTL(*str))
+ ret += 6;
+ else
+ ret++;
+ str++;
+ len--;
+ }
+
+ return ret;
+}
+
+static gchar *
+teco_json_escape(const gchar *str, gsize len)
+{
+ gsize escaped_len = teco_json_escape_len(str, len);
+ gchar *escaped = g_malloc(escaped_len+1);
+ gchar *p = escaped;
+
+ while (len > 0) {
+ switch (*str) {
+ case '"':
+ case '\\':
+ *p++ = '\\';
+ *p++ = *str;
+ break;
+ case '\b':
+ *p++ = '\\';
+ *p++ = 'b';
+ break;
+ case '\f':
+ *p++ = '\\';
+ *p++ = 'f';
+ break;
+ case '\n':
+ *p++ = '\\';
+ *p++ = 'n';
+ break;
+ case '\r':
+ *p++ = '\\';
+ *p++ = 'r';
+ break;
+ case '\t':
+ *p++ = '\\';
+ *p++ = 't';
+ break;
+ default:
+ if (TECO_IS_CTL(*str))
+ p += sprintf(p, "\\u%04X", *str);
+ else
+ *p++ = *str;
+ }
+ str++;
+ len--;
+ }
+ *p = '\0';
+
+ return escaped;
+}
+
+static gchar *
+teco_json_unescape(sj_Value val)
+{
+ g_assert(val.type == SJ_STRING);
+
+ gchar *str = g_malloc(val.end - val.start + 1);
+ gchar *p = str;
+
+ while (val.start < val.end) {
+ if (*val.start == '\'' && *++val.start == 'u') {
+ val.start++;
+ gchar buf[4+1];
+ gsize len = MIN(val.end-val.start, 4);
+ strncpy(buf, val.start, len);
+ buf[len] = '\0';
+ // FIXME: validate?
+ gunichar c = strtoul(buf, NULL, 16);
+ /* there will be 6 bytes reserved in str (\uXXXX) */
+ p += g_unichar_to_utf8(c, p);
+ } else {
+ *p++ = *val.start++;
+ }
+ }
+ *p = '\0';
+
+ return str;
+}
+
+static teco_lsp_result_t *
+teco_lsp_result_new(const gchar *filename, guint line, guint column)
+{
+ teco_lsp_result_t *result = g_malloc(sizeof(teco_lsp_result_t) + strlen(filename) + 1);
+ strcpy(result->filename, filename);
+ result->line = line;
+ result->column = column;
+ return result;
+}
+
+static inline void
+teco_lsp_result_free(teco_lsp_result_t *result)
+{
+ g_free(result);
+}
+
+static inline void
+teco_lsp_list_clear(teco_stailq_head_t *list)
+{
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(list)))
+ teco_lsp_result_free((teco_lsp_result_t *)entry);
+}
+
+/*
+ * We better always shut down the LSP,
+ * even in optimized builds.
+ */
+static void __attribute__((destructor))
+teco_lsp_cleanup(void)
+{
+ teco_lsp_shutdown(NULL);
+
+ if (teco_lsp.stdin_chan)
+ g_io_channel_unref(teco_lsp.stdin_chan);
+ teco_lsp.stdin_chan = NULL;
+ if (teco_lsp.stdout_chan)
+ g_io_channel_unref(teco_lsp.stdout_chan);
+ teco_lsp.stdout_chan = NULL;
+
+ if (teco_lsp.pid >= 0) {
+ /*
+ * Sometimes, clangd will refuse to exit gracefully
+ * even after the shutdown procedure, so we kill it
+ * explicitly here.
+ * The process should be reaped automatically.
+ */
+#ifdef G_OS_UNIX
+ kill(teco_lsp.pid, SIGKILL);
+#elif defined(G_OS_WIN32)
+ TerminateProcess(teco_lsp.pid, 1);
+#endif
+ g_spawn_close_pid(teco_lsp.pid);
+ teco_lsp.pid = -1;
+ }
+
+ teco_lsp_list_clear(&teco_lsp.list);
+}
+
+static void
+teco_undo_restore_lsp_list_action(teco_stailq_head_t *ctx, gboolean run)
+{
+ if (run)
+ teco_lsp.list = *ctx;
+ else
+ teco_lsp_list_clear(ctx);
+}
+
+/**
+ * Restore teco_lsp_list on rubout.
+ * Ownership is passed to the undo token.
+ *
+ * @fixme Replace with TECO_DEFINE_UNDO_OBJECT_OWN()?
+ */
+static void
+teco_undo_restore_lsp_list(void)
+{
+ teco_stailq_head_t *ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_restore_lsp_list_action,
+ sizeof(teco_lsp.list));
+ if (ctx)
+ *ctx = teco_lsp.list;
+ else
+ teco_lsp_list_clear(&teco_lsp.list);
+}
+
+/**
+ * Send preformatted request to server.
+ *
+ * @todo What if the LSP hangs?
+ * Use non-blocking I/O and allow interruptions.
+ */
+static gboolean
+teco_lsp_send(const gchar *req, GError **error)
+{
+ gsize req_len = strlen(req);
+
+ gchar header[256];
+ gsize header_len = g_snprintf(header, sizeof(header), "Content-Length: %zu\r\n\r\n", req_len);
+
+ if (g_io_channel_write_chars(teco_lsp.stdin_chan, header, header_len,
+ NULL, error) == G_IO_STATUS_ERROR ||
+ g_io_channel_write_chars(teco_lsp.stdin_chan, req, req_len,
+ NULL, error) == G_IO_STATUS_ERROR ||
+ g_io_channel_flush(teco_lsp.stdin_chan, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+
+ return TRUE;
+}
+
+/**
+ * Escape and escape string to server
+ *
+ * Call teco_json_escape_len() to find out how much bytes this will write.
+ *
+ * @see teco_json_escape
+ */
+static gboolean
+teco_lsp_send_escaped(const gchar *str, gsize len, GError **error)
+{
+ while (len > 0) {
+ gchar buf[6+1] = {'\\', 0, 0};
+
+ switch (*str) {
+ case '"':
+ case '\\':
+ buf[1] = *str;
+ break;
+ case '\b':
+ buf[1] = 'b';
+ break;
+ case '\f':
+ buf[1] = 'f';
+ break;
+ case '\n':
+ buf[1] = 'n';
+ break;
+ case '\r':
+ buf[1] = 'r';
+ break;
+ case '\t':
+ buf[1] = 't';
+ break;
+ default:
+ if (TECO_IS_CTL(*str))
+ g_snprintf(buf+1, sizeof(buf)-1, "u%04X", *str);
+ else
+ buf[0] = *str;
+ }
+
+ if (g_io_channel_write_chars(teco_lsp.stdin_chan, buf, -1,
+ NULL, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+
+ str++;
+ len--;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Receive response from server.
+ * All notifications are ignored.
+ *
+ * @todo What if the LSP hangs?
+ * Use non-blocking I/O and allow interruptions.
+ * Currently, only on UNIX we can interrupt since SIGINT
+ * is passed down to the child processes.
+ */
+static gboolean
+teco_lsp_recv(teco_string_t *resp, GError **error)
+{
+ for (;;) {
+ memset(resp, 0, sizeof(*resp));
+
+ for (;;) {
+ g_autofree gchar *line = NULL;
+ gsize len;
+ if (g_io_channel_read_line(teco_lsp.stdout_chan, &line,
+ NULL, &len, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+ if (len == 0)
+ break;
+ sscanf(line, "Content-Length: %zu", &resp->len);
+ }
+
+ if (!resp->len) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Missing Content-Length field.");
+ return FALSE;
+ }
+
+ resp->data = g_malloc(resp->len+1);
+ gsize read_len;
+
+ if (g_io_channel_read_chars(teco_lsp.stdout_chan, resp->data,
+ resp->len, &read_len, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+ if (read_len != resp->len) {
+ /* can only mean end of LSP's stdout */
+ g_free(resp->data);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unexpected end of response (%zu bytes instead of %zu)",
+ read_len, resp->len);
+ return FALSE;
+ }
+
+ resp->data[resp->len] = '\0';
+
+ /*
+ * Check for notifications.
+ */
+ sj_Reader reader = sj_reader(resp->data, resp->len);
+ sj_Value obj = sj_read(&reader);
+ if (obj.type != SJ_OBJECT) {
+ g_free(resp->data);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected object in response (at byte %zu)",
+ obj.start - reader.data);
+ return FALSE;
+ }
+
+ sj_Value key, val;
+ while (sj_iter_object(&reader, obj, &key, &val))
+ if (teco_json_eq(key, "id"))
+ /* it's a proper response */
+ return TRUE;
+
+ /* it's a notification - ignore for the time being */
+ g_free(resp->data);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+teco_lsp_launch(GError **error)
+{
+ /*
+ * NOTE: With G_SPAWN_LEAVE_DESCRIPTORS_OPEN and without G_SPAWN_SEARCH_PATH_FROM_ENVP,
+ * Glib offers an "optimized codepath" on UNIX.
+ * G_SPAWN_SEARCH_PATH_FROM_ENVP does not appear to work on Windows, anyway.
+ * On the other hand, this means you cannot overwrite $PATH via Q-Registers.
+ */
+ static const GSpawnFlags flags = G_SPAWN_SEARCH_PATH |
+#ifdef G_OS_UNIX
+ G_SPAWN_LEAVE_DESCRIPTORS_OPEN |
+#endif
+ G_SPAWN_STDERR_TO_DEV_NULL;
+
+ static const gchar lsp_reg_name[] = "$SCITECO_LSP";
+ teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals,
+ lsp_reg_name, strlen(lsp_reg_name));
+ if (!reg) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register %s is undefined.", lsp_reg_name);
+ return FALSE;
+ }
+
+ g_auto(teco_string_t) command = {NULL, 0};
+ if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error))
+ return FALSE;
+ if (teco_string_contains(command, '\0')) {
+ teco_error_qregcontainsnull_set(error, lsp_reg_name, strlen(lsp_reg_name), FALSE);
+ return FALSE;
+ }
+
+ /*
+ * FIXME: This allows POSIX shell emulation.
+ * But how to do that only for launching the LSP?
+ */
+ g_auto(GStrv) argv = teco_parse_shell_command_line(command.data, error);
+ if (!argv)
+ return FALSE;
+
+ g_auto(GStrv) envp = teco_qreg_table_get_environ(&teco_qreg_table_globals, error);
+ if (!envp)
+ return FALSE;
+
+ gint stdin_fd, stdout_fd;
+
+ if (!g_spawn_async_with_pipes(NULL, argv, envp, flags, NULL, NULL, &teco_lsp.pid,
+ &stdin_fd, &stdout_fd, NULL, error))
+ return FALSE;
+
+#ifdef G_OS_WIN32
+ teco_lsp.stdin_chan = g_io_channel_win32_new_fd(stdin_fd);
+ teco_lsp.stdout_chan = g_io_channel_win32_new_fd(stdout_fd);
+#else
+ /* the UNIX constructors should work everywhere else */
+ teco_lsp.stdin_chan = g_io_channel_unix_new(stdin_fd);
+ teco_lsp.stdout_chan = g_io_channel_unix_new(stdout_fd);
+#endif
+
+ //g_io_channel_set_flags(teco_lsp.stdin_chan, G_IO_FLAG_NONBLOCK, NULL);
+ g_io_channel_set_encoding(teco_lsp.stdin_chan, NULL, NULL);
+ g_io_channel_set_buffered(teco_lsp.stdin_chan, TRUE);
+ //g_io_channel_set_flags(teco_lsp.stdout_chan, G_IO_FLAG_NONBLOCK, NULL);
+ g_io_channel_set_encoding(teco_lsp.stdout_chan, NULL, NULL);
+ g_io_channel_set_buffered(teco_lsp.stdout_chan, TRUE);
+
+ g_auto(teco_string_t) root = {NULL, 0};
+
+ static const gchar root_reg_name[] = "$SCITECO_LSP_ROOT";
+ reg = teco_qreg_table_find(&teco_qreg_table_globals,
+ root_reg_name, strlen(root_reg_name));
+ if (reg) {
+ if (!reg->vtable->get_string(reg, &root.data, &root.len, NULL, error))
+ return FALSE;
+ if (teco_string_contains(root, '\0')) {
+ teco_error_qregcontainsnull_set(error, root_reg_name, strlen(root_reg_name), FALSE);
+ return FALSE;
+ }
+ } else {
+ root.data = g_get_current_dir();
+ root.len = strlen(root.data);
+ }
+
+ g_autofree gchar *root_uri = g_filename_to_uri(root.data, NULL, error);
+ if (!root_uri)
+ return FALSE;
+ g_autofree gchar *root_uri_escaped = teco_json_escape(root_uri, strlen(root_uri));
+ g_autofree gchar *req = g_strdup_printf("{"
+ "\"jsonrpc\":\"2.0\","
+ "\"id\":1,"
+ "\"method\":\"initialize\","
+ "\"params\":{"
+ /*
+ * FIXME: Is there any advantage in passing the real pid?
+ */
+ "\"processId\":null,"
+ "\"clientInfo\":{"
+ "\"name\":\"%s\","
+ "\"version\":\"%s\""
+ "},"
+ "\"capabilities\":{"
+ "\"general\":{"
+ "\"positionEncodings\":[\"utf-8\"]"
+ "}"
+ "},"
+ "\"rootUri\":\"%s\""
+ "}"
+ "}", PACKAGE_NAME, PACKAGE_VERSION, root_uri_escaped);
+ if (!teco_lsp_send(req, error))
+ return FALSE;
+
+ g_auto(teco_string_t) resp = {NULL, 0};
+ if (!teco_lsp_recv(&resp, error))
+ 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;
+ while (sj_iter_object(&reader, obj, &key, &val)) {
+ if (teco_json_eq(key, "method") && !teco_json_eq(val, "initialized")) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "\"initialized\" method not found");
+ return FALSE;
+ }
+ }
+
+ static const gchar initialized[] = "{"
+ "\"jsonrpc\":\"2.0\","
+ "\"method\":\"initialized\","
+ "\"params\":{}"
+ "}";
+ if (!teco_lsp_send(initialized, error))
+ return FALSE;
+
+ /*
+ * Sends textDocument/didOpen for all buffers.
+ * This could also be moved here if we'd export teco_ring_head.
+ */
+ return teco_ring_sync_lsp(error);
+}
+
+gboolean
+teco_lsp_didopen(teco_buffer_t *buffer, GError **error)
+{
+ if (teco_lsp.pid < 0)
+ /* do nothing until the user queries something */
+ return TRUE;
+
+ /*
+ * FIXME: Can we somehow handle the unnamed buffer?
+ * clangd doesn't accept `untitled:` URI schemes.
+ */
+ if (!buffer->filename)
+ return TRUE;
+
+ /*
+ * FIXME: The Lexilla lexer names aren't always identical to the
+ * LSP lanuageIds.
+ * Without lexing, this will just pass the empty string.
+ */
+ gsize language_len = teco_view_ssm(buffer->view, SCI_GETLEXERLANGUAGE, 0, 0);
+ g_autofree gchar *language = g_malloc(language_len+1);
+ teco_view_ssm(buffer->view, SCI_GETLEXERLANGUAGE, 0, (sptr_t)language);
+ language[language_len] = '\0';
+ g_autofree gchar *language_escaped = teco_json_escape(language, language_len);
+
+ g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, error);
+ 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\","
+ "\"method\":\"textDocument/didOpen\","
+ "\"params\":{"
+ "\"textDocument\":{"
+ "\"languageId\":\"%s\","
+ "\"version\":%u,"
+ "\"uri\":\"%s\","
+ /* all text will be added with textDocument/didChange */
+ "\"text\":\"\""
+ "}"
+ "}"
+ "}", language_escaped, buffer->version, uri_escaped);
+ return teco_lsp_send(req, error);
+}
+
+gboolean
+teco_lsp_didchange_insert(teco_buffer_t *buffer, gsize pos, gsize len,
+ const gchar *text, GError **error)
+{
+ if (teco_lsp.pid < 0)
+ /* do nothing until the user queries something */
+ return TRUE;
+
+ /*
+ * FIXME: Can we somehow handle the unnamed buffer?
+ * clangd doesn't accept `untitled:` URI schemes.
+ */
+ if (!buffer->filename)
+ return TRUE;
+
+ guint line = teco_view_ssm(buffer->view, SCI_LINEFROMPOSITION, pos, 0);
+ guint column = pos - teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, line, 0);
+
+ g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, error);
+ if (!uri)
+ return FALSE;
+ g_autofree gchar *uri_escaped = teco_json_escape(uri, strlen(uri));
+ g_autofree gchar *req_prefix = g_strdup_printf("{"
+ "\"jsonrpc\":\"2.0\","
+ "\"method\":\"textDocument/didChange\","
+ "\"params\":{"
+ "\"textDocument\":{"
+ "\"version\":%u,"
+ "\"uri\":\"%s\""
+ "},"
+ "\"contentChanges\":[{"
+ "\"range\":{"
+ "\"start\":{\"line\":%u,\"character\":%u},"
+ "\"end\":{\"line\":%u,\"character\":%u}"
+ "},"
+ "\"text\":\"",
+ buffer->version+1, uri_escaped,
+ line, column, line, column);
+ static const gchar req_suffix[] = "\""
+ "}]"
+ "}"
+ "}";
+
+ /*
+ * Count the escaped size of the text buffer.
+ * We need to do this in advance to send a correct "Content-Length" header.
+ * We do this to avoid copying the entire buffer around several times as
+ * would be necessary when using teco_json_escape() and teco_lsp_send().
+ */
+ gsize req_prefix_len = strlen(req_prefix);
+ gsize req_len = req_prefix_len + teco_json_escape_len(text, len) + sizeof(req_suffix)-1;
+
+ gchar header[256];
+ gsize header_len = g_snprintf(header, sizeof(header), "Content-Length: %zu\r\n\r\n", req_len);
+
+ if (g_io_channel_write_chars(teco_lsp.stdin_chan, header, header_len,
+ NULL, error) == G_IO_STATUS_ERROR ||
+ g_io_channel_write_chars(teco_lsp.stdin_chan, req_prefix, req_prefix_len,
+ NULL, error) == G_IO_STATUS_ERROR ||
+ !teco_lsp_send_escaped(text, len, error) ||
+ g_io_channel_write_chars(teco_lsp.stdin_chan, req_suffix, sizeof(req_suffix)-1,
+ NULL, error) == G_IO_STATUS_ERROR ||
+ g_io_channel_flush(teco_lsp.stdin_chan, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+
+ buffer->version++;
+ return TRUE;
+}
+
+gboolean
+teco_lsp_didchange_delete(teco_buffer_t *buffer, gsize pos, gsize len, GError **error)
+{
+ if (teco_lsp.pid < 0)
+ /* do nothing until the user queries something */
+ return TRUE;
+
+ /*
+ * FIXME: Can we somehow handle the unnamed buffer?
+ * clangd doesn't accept `untitled:` URI schemes.
+ */
+ if (!buffer->filename)
+ return TRUE;
+
+ guint start_line = teco_view_ssm(buffer->view, SCI_LINEFROMPOSITION, pos, 0);
+ guint start_column = pos - teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, start_line, 0);
+ guint end_line = teco_view_ssm(buffer->view, SCI_LINEFROMPOSITION, pos+len, 0);
+ guint end_column = pos+len - teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, end_line, 0);
+
+ g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, error);
+ 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\","
+ "\"method\":\"textDocument/didChange\","
+ "\"params\":{"
+ "\"textDocument\":{"
+ "\"version\":%u,"
+ "\"uri\":\"%s\""
+ "},"
+ "\"contentChanges\":[{"
+ "\"range\":{"
+ "\"start\":{\"line\":%u,\"character\":%u},"
+ "\"end\":{\"line\":%u,\"character\":%u}"
+ "},"
+ "\"text\":\"\""
+ "}]"
+ "}"
+ "}", buffer->version+1, uri_escaped,
+ start_line, start_column, end_line, end_column);
+
+ if (!teco_lsp_send(req, error))
+ return FALSE;
+
+ buffer->version++;
+ return TRUE;
+}
+
+/**
+ * Send new file to LSP server.
+ *
+ * This only makes sense after launching the LSP server or
+ * when saving the unnamed buffer.
+ */
+gboolean
+teco_lsp_sync(teco_buffer_t *buffer, GError **error)
+{
+ g_assert(buffer->filename != NULL);
+
+ if (teco_lsp.pid < 0)
+ /* do nothing until the user queries something */
+ return TRUE;
+
+ if (!teco_lsp_didopen(buffer, error))
+ return FALSE;
+
+ /*
+ * After LSP startup, all document contents will be sent via
+ * teco_lsp_didchange_insert().
+ * Therefore teco_lsp_didopen() does not send document contents.
+ */
+ gsize gap = teco_view_ssm(buffer->view, SCI_GETGAPPOSITION, 0, 0);
+ if (gap) {
+ const gchar *pre_gap = (const gchar *)teco_view_ssm(buffer->view, SCI_GETRANGEPOINTER,
+ 0, gap);
+ if (!teco_lsp_didchange_insert(buffer, 0, gap, pre_gap, error))
+ return FALSE;
+ }
+ gsize post_gap_len = teco_view_ssm(buffer->view, SCI_GETLENGTH, 0, 0) - gap;
+ if (post_gap_len) {
+ const gchar *post_gap = (const gchar *)teco_view_ssm(buffer->view, SCI_GETRANGEPOINTER,
+ gap, post_gap_len);
+ if (!teco_lsp_didchange_insert(buffer, gap, post_gap_len, post_gap, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_lsp_didclose(teco_buffer_t *buffer, GError **error)
+{
+ if (teco_lsp.pid < 0)
+ /* do nothing until the user queries something */
+ return TRUE;
+
+ /*
+ * FIXME: Can we somehow handle the unnamed buffer?
+ * clangd doesn't accept `untitled:` URI schemes.
+ */
+ if (!buffer->filename)
+ return TRUE;
+
+ g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, error);
+ 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\","
+ "\"method\":\"textDocument/didClose\","
+ "\"params\":{"
+ "\"textDocument\":{"
+ "\"uri\":\"%s\""
+ "}"
+ "}"
+ "}", uri_escaped);
+
+ return teco_lsp_send(req, error);
+}
+
+static teco_lsp_result_t *
+teco_lsp_parse_location(sj_Reader reader, sj_Value obj, GError **error)
+{
+ if (obj.type != SJ_OBJECT) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected object in response (at byte %zu)",
+ obj.start - reader.data);
+ return NULL;
+ }
+
+ g_autofree gchar *uri = NULL;
+ gint line = -1, column = 0;
+
+ sj_Value key, val;
+ while (sj_iter_object(&reader, obj, &key, &val)) {
+ if (teco_json_eq(key, "uri")) {
+ uri = teco_json_unescape(val);
+ } else if (teco_json_eq(key, "range")) {
+ sj_Value obj = val;
+ if (obj.type != SJ_OBJECT) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected object in response (at byte %zu)",
+ obj.start - reader.data);
+ return NULL;
+ }
+
+ while (sj_iter_object(&reader, obj, &key, &val)) {
+ if (teco_json_eq(key, "start")) {
+ /* descend into object value */
+ obj = val;
+ if (obj.type != SJ_OBJECT) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected object in response (at byte %zu)",
+ obj.start - reader.data);
+ return NULL;
+ }
+ } else if (teco_json_eq(key, "line")) {
+ if (val.type != SJ_NUMBER) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected number in response (at byte %zu)",
+ val.start - reader.data);
+ return NULL;
+ }
+ line = atoi(val.start);
+ } else if (teco_json_eq(key, "character")) {
+ /*
+ * Column is optional.
+ * FIXME: Is it really in glyphs?
+ */
+ if (val.type != SJ_NUMBER) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected number in response (at byte %zu)",
+ val.start - reader.data);
+ return NULL;
+ }
+ column = atoi(val.start);
+ }
+ }
+ }
+ }
+
+ if (!uri || line < 0) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "URI and start position expected in result");
+ return NULL;
+ }
+
+ g_autofree gchar *hostname = NULL;
+ g_autofree gchar *filename = g_filename_from_uri(uri, &hostname, error);
+ if (!filename)
+ return NULL;
+ if (hostname) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Hostname \"%s\" unexpected in URI", hostname);
+ return NULL;
+ }
+
+ return teco_lsp_result_new(filename, line, column);
+}
+
+static gboolean
+teco_lsp_lookup_symbol(teco_string_t str, gboolean match_exact, GError **error)
+{
+ g_assert(teco_lsp.pid >= 0);
+
+ teco_undo_restore_lsp_list();
+ teco_lsp.list = TECO_STAILQ_HEAD_INITIALIZER(&teco_lsp.list);
+
+ g_autofree gchar *symbol_escaped = teco_json_escape(str.data, str.len);
+ g_autofree gchar *req = g_strdup_printf("{"
+ "\"jsonrpc\":\"2.0\","
+ "\"id\":1,"
+ "\"method\":\"workspace/symbol\","
+ "\"params\":{"
+ "\"query\":\"%s\""
+ "}"
+ "}", symbol_escaped);
+ if (!teco_lsp_send(req, error))
+ return FALSE;
+
+ g_auto(teco_string_t) resp = {NULL, 0};
+ if (!teco_lsp_recv(&resp, error))
+ 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"));
+ sj_Value arr = val;
+ if (arr.type != SJ_ARRAY) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected array in response (at byte %zu)",
+ arr.start - reader.data);
+ return FALSE;
+ }
+
+ guint results = 0;
+ while (sj_iter_array(&reader, arr, &obj)) {
+ if (obj.type != SJ_OBJECT) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected object in response (at byte %zu)",
+ obj.start - reader.data);
+ return FALSE;
+ }
+
+ gboolean skip_result = FALSE;
+ teco_lsp_result_t *result = NULL;
+ while (sj_iter_object(&reader, obj, &key, &val)) {
+ if (teco_json_eq(key, "name") && match_exact) {
+ g_autofree gchar *name = teco_json_unescape(val);
+ skip_result = strcmp(str.data, name) != 0;
+ if (skip_result)
+ break;
+ } else if (teco_json_eq(key, "location")) {
+ result = teco_lsp_parse_location(reader, val, error);
+ if (!result)
+ return FALSE;
+ }
+ }
+ if (skip_result) {
+ g_free(result);
+ continue;
+ }
+ if (!result) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "\"location\" missing in LSP response");
+ return FALSE;
+ }
+
+ teco_stailq_insert_tail(&teco_lsp.list, &result->entry);
+ results++;
+ }
+ if (results > 1)
+ teco_interface_msg(TECO_MSG_INFO, "Found %u references", results);
+
+ teco_undo_ptr(teco_lsp.current) = (teco_lsp_result_t *)teco_lsp.list.first;
+ return TRUE;
+}
+
+/**
+ * Auto-complete a workspace symbol.
+ *
+ * @param symbol The symbol to auto-complete or NULL.
+ * @param insert String to initialize with the completion.
+ * @return TRUE in case of an unambiguous completion.
+ */
+gboolean
+teco_lsp_symbol_auto_complete(const gchar *symbol, teco_string_t *insert)
+{
+ memset(insert, 0, sizeof(*insert));
+
+ /* server is started on demand */
+ if (G_UNLIKELY(teco_lsp.pid < 0) && !teco_lsp_launch(NULL))
+ return FALSE;
+
+ if (!symbol)
+ symbol = "";
+ gsize symbol_len = strlen(symbol);
+
+ g_autofree gchar *symbol_escaped = teco_json_escape(symbol, symbol_len);
+ g_autofree gchar *req = g_strdup_printf("{"
+ "\"jsonrpc\":\"2.0\","
+ "\"id\":1,"
+ "\"method\":\"workspace/symbol\","
+ "\"params\":{"
+ "\"query\":\"%s\""
+ "}"
+ "}", symbol_escaped);
+ 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"));
+ sj_Value arr = val;
+ if (arr.type != SJ_ARRAY)
+ return FALSE;
+
+ GSList *list = NULL;
+ guint list_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, "name"));
+
+ gchar *name = teco_json_unescape(val);
+ if (strncmp(name, symbol, symbol_len) != 0) {
+ g_free(name);
+ 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_casediff(list_str, (gchar *)name + symbol_len,
+ strlen(name) - symbol_len);
+ if (len < prefix_len)
+ prefix_len = len;
+ } else {
+ prefix_len = strlen(name) - symbol_len;
+ }
+
+ /* ownership of name is passed to the list */
+ list = g_slist_prepend(list, name);
+ list_len++;
+ }
+
+ if (prefix_len > 0) {
+ teco_string_init(insert, (gchar *)list->data + symbol_len, prefix_len);
+ } else if (list_len > 1) {
+ list = g_slist_sort(list, (GCompareFunc)strcmp);
+
+ for (GSList *entry = list; entry != NULL; entry = g_slist_next(entry))
+ teco_interface_popup_add(TECO_POPUP_PLAIN, entry->data,
+ strlen(entry->data), FALSE);
+
+ teco_interface_popup_show(symbol_len);
+ }
+
+ g_slist_free_full(list, g_free);
+ return list_len == 1;
+}
+
+static gboolean
+teco_lsp_lookup_definition(teco_buffer_t *buffer, teco_int_t pos, GError **error)
+{
+ g_assert(buffer->filename != NULL);
+ g_assert(teco_lsp.pid >= 0);
+
+ teco_undo_restore_lsp_list();
+ teco_lsp.list = TECO_STAILQ_HEAD_INITIALIZER(&teco_lsp.list);
+
+ gsize dot_bytes = teco_view_ssm(buffer->view, SCI_GETCURRENTPOS, 0, 0);
+ gssize pos_bytes = teco_view_glyphs2bytes_rel(buffer->view, buffer->dot, dot_bytes, pos - buffer->dot);
+ guint line = teco_view_ssm(buffer->view, SCI_LINEFROMPOSITION, pos_bytes, 0);
+ guint column = pos_bytes - teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, line, 0);
+
+ g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, error);
+ 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\":1,"
+ "\"method\":\"textDocument/definition\","
+ "\"params\":{"
+ "\"textDocument\":{"
+ "\"uri\":\"%s\""
+ "},"
+ "\"position\":{\"line\":%u,\"character\":%u}"
+ "}"
+ "}", uri_escaped, line, column);
+ if (!teco_lsp_send(req, error))
+ return FALSE;
+
+ g_auto(teco_string_t) resp = {NULL, 0};
+ if (!teco_lsp_recv(&resp, error))
+ 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) {
+ teco_lsp_result_t *result = teco_lsp_parse_location(reader, val, error);
+ if (!result)
+ return FALSE;
+ teco_stailq_insert_tail(&teco_lsp.list, &result->entry);
+ } else if (val.type != SJ_ARRAY) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected array in response (at byte %zu)",
+ val.start - reader.data);
+ return FALSE;
+ }
+ sj_Value arr = val;
+
+ guint results = 0;
+ while (sj_iter_array(&reader, arr, &obj)) {
+ teco_lsp_result_t *result = teco_lsp_parse_location(reader, obj, error);
+ if (!result)
+ return FALSE;
+ teco_stailq_insert_tail(&teco_lsp.list, &result->entry);
+ results++;
+ }
+ if (results > 1)
+ teco_interface_msg(TECO_MSG_INFO, "Found %u definitions", results);
+
+ teco_undo_ptr(teco_lsp.current) = (teco_lsp_result_t *)teco_lsp.list.first;
+ return TRUE;
+}
+
+static gboolean
+teco_lsp_lookup_references(teco_buffer_t *buffer, teco_int_t pos, GError **error)
+{
+ g_assert(buffer->filename != NULL);
+ g_assert(teco_lsp.pid >= 0);
+
+ teco_undo_restore_lsp_list();
+ teco_lsp.list = TECO_STAILQ_HEAD_INITIALIZER(&teco_lsp.list);
+
+ gsize dot_bytes = teco_view_ssm(buffer->view, SCI_GETCURRENTPOS, 0, 0);
+ gssize pos_bytes = teco_view_glyphs2bytes_rel(buffer->view, buffer->dot, dot_bytes, pos - buffer->dot);
+ guint line = teco_view_ssm(buffer->view, SCI_LINEFROMPOSITION, pos_bytes, 0);
+ guint column = pos_bytes - teco_view_ssm(buffer->view, SCI_POSITIONFROMLINE, line, 0);
+
+ g_autofree gchar *uri = g_filename_to_uri(buffer->filename, NULL, error);
+ 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\":1,"
+ "\"method\":\"textDocument/references\","
+ "\"params\":{"
+ "\"textDocument\":{"
+ "\"uri\":\"%s\""
+ "},"
+ "\"position\":{\"line\":%u,\"character\":%u},"
+ "\"context\":{"
+ "\"includeDeclaration\":true"
+ "}"
+ "}"
+ "}", uri_escaped, line, column);
+ if (!teco_lsp_send(req, error))
+ return FALSE;
+
+ g_auto(teco_string_t) resp = {NULL, 0};
+ if (!teco_lsp_recv(&resp, error))
+ 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"));
+ sj_Value arr = val;
+ if (arr.type != SJ_ARRAY) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Expected array in response (at byte %zu)",
+ arr.start - reader.data);
+ return FALSE;
+ }
+
+ guint results = 0;
+ while (sj_iter_array(&reader, arr, &obj)) {
+ teco_lsp_result_t *result = teco_lsp_parse_location(reader, obj, error);
+ if (!result)
+ return FALSE;
+ teco_stailq_insert_tail(&teco_lsp.list, &result->entry);
+ results++;
+ }
+ if (results > 1)
+ teco_interface_msg(TECO_MSG_INFO, "Found %u references", results);
+
+ teco_undo_ptr(teco_lsp.current) = (teco_lsp_result_t *)teco_lsp.list.first;
+ return TRUE;
+}
+
+static gboolean
+teco_lsp_shutdown(GError **error)
+{
+ if (teco_lsp.pid < 0)
+ return TRUE;
+
+ static const gchar req[] = "{"
+ "\"jsonrpc\":\"2.0\","
+ "\"id\":1,"
+ "\"method\":\"shutdown\","
+ "\"params\":null"
+ "}";
+ if (!teco_lsp_send(req, error))
+ return FALSE;
+
+ g_auto(teco_string_t) resp = {NULL, 0};
+ if (!teco_lsp_recv(&resp, error))
+ return FALSE;
+ /* FIXME: Do we need to check the response? */
+
+ static const gchar notification[] = "{"
+ "\"jsonrpc\":\"2.0\","
+ "\"method\":\"exit\","
+ "\"params\":null"
+ "}";
+ return teco_lsp_send(notification, error);
+}
+
+static teco_state_t *
+teco_state_lsp_lookup_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ gboolean have_colon = teco_machine_main_eval_colon(ctx) > 0;
+
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+
+ if (!teco_expressions_args()) {
+ /* look up symbol */
+ if (teco_num_sign < 0) {
+ /* terminate language server */
+ teco_lsp_cleanup();
+ return &teco_state_start;
+ }
+
+ /* server is started on demand */
+ if (G_UNLIKELY(teco_lsp.pid < 0) && !teco_lsp_launch(error))
+ return NULL;
+
+ if (str.len && !teco_lsp_lookup_symbol(str, !have_colon, error))
+ return NULL;
+ } else {
+ /* look up definition or references at <n> */
+ if (str.len) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "String argument must be empty when "
+ "looking up definitions or references");
+ return NULL;
+ }
+
+ teco_int_t v;
+ if (!teco_expressions_pop_num_calc(&v, 0, error))
+ return NULL;
+
+ if (v < 0) {
+ /* terminate language server */
+ teco_lsp_cleanup();
+ return &teco_state_start;
+ }
+
+ if (teco_qreg_current || !teco_ring_current->filename) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Registers and unnamed buffers not allowed");
+ return NULL;
+ }
+
+ /* server is started on demand */
+ if (G_UNLIKELY(teco_lsp.pid < 0) && !teco_lsp_launch(error))
+ return NULL;
+
+ gboolean rc = have_colon ? teco_lsp_lookup_references(teco_ring_current, v, error)
+ : teco_lsp_lookup_definition(teco_ring_current, v, error);
+ if (!rc)
+ return NULL;
+ }
+
+ if (!teco_lsp.current) {
+ /* mimics an unsuccessful search */
+ teco_interface_msg(TECO_MSG_ERROR, "No tags found");
+ return &teco_state_start;
+#if 0
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "No tags found");
+ return NULL;
+#endif
+ }
+
+ /*
+ * ED hooks with the default lexer framework
+ * will usually load the styling SciTECO script
+ * when editing the buffer for the first time.
+ */
+ if (!teco_current_doc_undo_edit(error) ||
+ !teco_ring_edit(teco_lsp.current->filename, error))
+ return NULL;
+
+ undo__teco_interface_ssm(SCI_GOTOPOS,
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0);
+
+ sptr_t pos = teco_interface_ssm(SCI_POSITIONFROMLINE, teco_lsp.current->line, 0) +
+ teco_lsp.current->column;
+ teco_current_doc_set_dot(teco_interface_bytes2glyphs_absdot(pos));
+ teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+
+ teco_undo_ptr(teco_lsp.current) = (teco_lsp_result_t *)teco_lsp.current->entry.next
+ ? : (teco_lsp_result_t *)teco_lsp.list.first;
+ return &teco_state_start;
+}
+
+/* in cmdline.c */
+gboolean teco_state_lsp_lookup_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
+ gunichar key, GError **error);
+gboolean teco_state_lsp_lookup_insert_completion(teco_machine_main_t *ctx, teco_string_t str,
+ GError **error);
+
+/*$ FT :FT LSP lookup definition
+ * FT[symbol]$ -- Look up symbol via language server
+ * :FT[symbol]$
+ * <n>FT$
+ * <n>:FT$
+ * FT$
+ * -FT$
+ *
+ * When called with a string argument, it looks up the given
+ * <symbol> in the language server's workspace and jumps to the
+ * corresponding position.
+ * If colon-modified (\(lq:FT\(rq) the symbol will be fuzzy-matched.
+ * A message is logged if there is more than one result.
+ * You can toggle through these results by calling \(lqFT\fB$\fP\(rq.
+ * Since all \*(ST buffers are automatically synchronized with the
+ * language server the matches should always be up to date.
+ * This may not be the case when modifying buffers between \(lqFT\fB$\fP\(rq
+ * calls.
+ * The symbol name can be auto-completed, but you may not be offered
+ * all possible symbols. I.e. it may be possible to find a <symbol>
+ * even it was not offered as an auto-completion.
+ *
+ * It is also possible to look up the definition of the construct
+ * at buffer position <n> (i.e. when providing a numeric argument).
+ * If colon-modified, the command will look up all references to the
+ * construct at buffer position <n> instead.
+ * So \(lq.FT\fB$\fP\(rq looks up the definition of the construct
+ * at dot. With the standard macros from \fBfnkeys.tes\fP you can also
+ * right click to insert a buffer position.
+ *
+ * The language server binary and arguments are configured via the \fB$SCITECO_LSP\fP
+ * environment variable (and corresponding Q-Register).
+ * The program from this register is spawned as a permanent subprocess \(em
+ * communication takes place using \fBstdin\fP and \fBstdout\fP.
+ * \fB$SCITECO_LSP_ROOT\fP can be used to point the language server
+ * to the root of the project, which may be necessary e.g. to find
+ * \fBcompile_commands.json\fP when using clangd.
+ * It will already be set by \fBsession.tes\fP when using a VCS.
+ * Language servers are launched on demand \(em only when first
+ * looking up a symbol.
+ * 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.
+ */
+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,
+ .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_lsp_lookup_insert_completion,
+ .expectstring.done_cb = teco_state_lsp_lookup_done
+);