aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am4
-rw-r--r--src/cmdline.c57
-rw-r--r--src/core-commands.c3
-rw-r--r--src/help.c5
-rw-r--r--src/lsp.c1380
-rw-r--r--src/lsp.h35
-rw-r--r--src/ring.c41
-rw-r--r--src/ring.h8
-rw-r--r--src/spawn.c3
-rw-r--r--src/view.c64
-rw-r--r--src/view.h2
11 files changed, 1585 insertions, 17 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 8ac58c7..fa6c0e4 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -13,7 +13,8 @@ include $(top_srcdir)/contrib/scintilla.am
# FIXME: Common flags should be in configure.ac
AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value
-AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr -I$(top_srcdir)/contrib/terex
+AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr -I$(top_srcdir)/contrib/terex \
+ -I$(top_srcdir)/contrib/sj.h
AM_LDFLAGS =
if STATIC_EXECUTABLES
@@ -54,6 +55,7 @@ libsciteco_base_la_SOURCES = main.c sciteco.h list.h \
help.c help.h \
rb3str.c rb3str.h \
symbols.c symbols.h \
+ lsp.c lsp.h \
lexer.c lexer.h \
view.c view.h \
interface.c interface.h
diff --git a/src/cmdline.c b/src/cmdline.c
index 7fe044c..572b159 100644
--- a/src/cmdline.c
+++ b/src/cmdline.c
@@ -53,6 +53,7 @@
#include "error.h"
#include "qreg.h"
#include "glob.h"
+#include "lsp.h"
#include "cmdline.h"
#if defined(HAVE_MALLOC_TRIM) && !defined(HAVE_DECL_MALLOC_TRIM)
@@ -1506,6 +1507,62 @@ teco_state_help_insert_completion(teco_machine_main_t *ctx, teco_string_t str, G
return teco_cmdline_insert(str_escaped.data, str_escaped.len, error);
}
+gboolean
+teco_state_lsp_lookup_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
+ gunichar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete LSP symbol */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_scroll();
+ return TRUE;
+ }
+
+ if (teco_string_contains(ctx->expectstring.string, '\0'))
+ /* LSP symbol must not contain null-byte */
+ return TRUE;
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_lsp_symbol_auto_complete(ctx->expectstring.string.data, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous && ctx->expectstring.nesting == 1)
+ teco_string_append_wc(&new_chars_escaped,
+ ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_lsp_lookup_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+
+ g_auto(teco_string_t) str_escaped;
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
+ if (ctx->expectstring.nesting == 1)
+ teco_string_append_wc(&str_escaped,
+ ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
+ return teco_cmdline_insert(str_escaped.data, str_escaped.len, error);
+}
+
/*
* Command states
*/
diff --git a/src/core-commands.c b/src/core-commands.c
index 6c4ab58..db3ae9d 100644
--- a/src/core-commands.c
+++ b/src/core-commands.c
@@ -39,6 +39,7 @@
#include "spawn.h"
#include "glob.h"
#include "help.h"
+#include "lsp.h"
#include "cmdline.h"
#include "error.h"
#include "memory.h"
@@ -1068,6 +1069,8 @@ teco_state_fcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
.modifier_at = TRUE, .modifier_colon = 2},
['N'] = {&teco_state_replace_default_all,
.modifier_at = TRUE, .modifier_colon = 1},
+ ['T'] = {&teco_state_lsp_lookup,
+ .modifier_at = TRUE, .modifier_colon = 1},
['G'] = {&teco_state_changedir,
.modifier_at = TRUE},
diff --git a/src/help.c b/src/help.c
index c64538e..b7b5fd0 100644
--- a/src/help.c
+++ b/src/help.c
@@ -282,6 +282,11 @@ teco_state_help_done(teco_machine_main_t *ctx, teco_string_t str, GError **error
"Help topic must not contain null-byte");
return NULL;
}
+
+ /*
+ * FIXME: Perhaps ?$ could toggle through all topics with the
+ * same name.
+ */
const gchar *topic_name = str.data ? : "";
teco_help_topic_t *topic = teco_help_find(topic_name);
if (!topic) {
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
+);
diff --git a/src/lsp.h b/src/lsp.h
new file mode 100644
index 0000000..325c204
--- /dev/null
+++ b/src/lsp.h
@@ -0,0 +1,35 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include "parser.h"
+#include "ring.h"
+
+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);
+gboolean teco_lsp_didchange_delete(teco_buffer_t *buffer, gsize pos, gsize len,
+ GError **error);
+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);
+
+extern teco_state_t teco_state_lsp_lookup;
diff --git a/src/ring.c b/src/ring.c
index cda15c4..dc43ac5 100644
--- a/src/ring.c
+++ b/src/ring.c
@@ -37,6 +37,7 @@
#include "glob.h"
#include "error.h"
#include "list.h"
+#include "lsp.h"
#include "ring.h"
/** @private @static @memberof teco_buffer_t */
@@ -79,13 +80,20 @@ teco_buffer_undo_edit(teco_buffer_t *ctx)
static gboolean
teco_buffer_load(teco_buffer_t *ctx, const gchar *filename, GError **error)
{
+ teco_buffer_set_filename(ctx, filename);
+
+ if (!teco_lsp_didopen(ctx, error))
+ return FALSE;
+
+ /*
+ * Will also call teco_lsp_didchange_insert() following
+ * Scintilla notifications.
+ */
if (!teco_view_load(ctx->view, filename, TRUE, error))
return FALSE;
/* currently buffer cannot be dirty */
g_assert(ctx->state == TECO_BUFFER_CLEAN);
-
- teco_buffer_set_filename(ctx, filename);
return TRUE;
}
@@ -93,7 +101,9 @@ teco_buffer_load(teco_buffer_t *ctx, const gchar *filename, GError **error)
static gboolean
teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error)
{
- if (!filename && !ctx->filename) {
+ gboolean is_unnamed = !ctx->filename;
+
+ if (!filename && is_unnamed) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Cannot save the unnamed file "
"without providing a file name");
@@ -127,6 +137,13 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error)
teco_undo_cstring(ctx->filename);
teco_buffer_set_filename(ctx, filename ? : ctx->filename);
+ /*
+ * Unnamed buffers were not synced with the LSP server,
+ * so we do this now.
+ */
+ if (is_unnamed && !teco_lsp_sync(ctx, error))
+ return FALSE;
+
return TRUE;
}
@@ -400,6 +417,9 @@ teco_ring_edit_by_name(const gchar *filename, GError **error)
else
teco_interface_msg(TECO_MSG_INFO,
"Added new unnamed file to ring.");
+
+ if (!teco_lsp_didopen(buffer, error))
+ return FALSE;
}
return teco_ed_hook(TECO_ED_HOOK_ADD, error);
@@ -467,6 +487,9 @@ teco_ring_close(teco_buffer_t *buffer, GError **error)
teco_ring_remove_buffer(buffer);
}
+ if (!teco_lsp_didclose(buffer, error))
+ return FALSE;
+
/* transfer responsibility to the undo token object */
teco_undo_ring_reinsert(buffer);
@@ -489,6 +512,18 @@ teco_ring_set_scintilla_undo(gboolean state)
}
}
+gboolean
+teco_ring_sync_lsp(GError **error)
+{
+ for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
+ teco_buffer_t *buffer = (teco_buffer_t *)cur;
+ if (buffer->filename && !teco_lsp_sync(buffer, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
void
teco_ring_cleanup(void)
{
diff --git a/src/ring.h b/src/ring.h
index 91ce3c9..126f455 100644
--- a/src/ring.h
+++ b/src/ring.h
@@ -44,6 +44,12 @@ typedef struct teco_buffer_t {
gchar *filename;
/**
+ * Counter increasing with every change.
+ * It is maintained only if a language server (LSP) is running.
+ */
+ guint version;
+
+ /**
* A teco_buffer_state_t.
* This is still a guint, so you can call teco_undo_guint().
*/
@@ -117,6 +123,8 @@ void teco_ring_undo_close(void);
void teco_ring_set_scintilla_undo(gboolean state);
+gboolean teco_ring_sync_lsp(GError **error);
+
void teco_ring_cleanup(void);
/*
diff --git a/src/spawn.c b/src/spawn.c
index 507d1cd..6289830 100644
--- a/src/spawn.c
+++ b/src/spawn.c
@@ -176,6 +176,8 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error)
* As a workaround, only pass down absolute paths.
* This is not perfect: It will use $PATH from the environment,
* so you cannot overwrite it from within SciTECO.
+ * On the other hand we're not currently using
+ * G_SPAWN_SEARCH_PATH_FROM_ENVP anyway.
*/
gchar *program = g_find_program_in_path(argv[0]);
if (!program) {
@@ -270,6 +272,7 @@ teco_state_execute_done(teco_machine_main_t *ctx, teco_string_t str, GError **er
* 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_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH |
#ifdef G_OS_UNIX
diff --git a/src/view.c b/src/view.c
index 7a9e0cc..43cdd3f 100644
--- a/src/view.c
+++ b/src/view.c
@@ -47,13 +47,24 @@
#include "eol.h"
#include "memory.h"
#include "lexer.h"
+#include "ring.h"
+#include "lsp.h"
#include "view.h"
+//#define DEBUG
+
/** @memberof teco_view_t */
void
teco_view_setup(teco_view_t *ctx)
{
/*
+ * Avoid unnecessary notifications.
+ */
+ teco_view_ssm(ctx, SCI_SETCOMMANDEVENTS, FALSE, 0);
+ teco_view_ssm(ctx, SCI_SETMODEVENTMASK,
+ SC_MOD_INSERTTEXT | SC_MOD_BEFOREDELETE, 0);
+
+ /*
* Start with or without undo collection,
* depending on teco_undo_enabled.
*/
@@ -825,20 +836,49 @@ teco_view_get_character(teco_view_t *ctx, gsize pos, gsize len)
}
void
-teco_view_process_notify(teco_view_t *ctx, SCNotification *notify)
+teco_view_process_notify(teco_view_t *ctx, const SCNotification *notify)
{
#ifdef DEBUG
- g_printf("SCINTILLA NOTIFY: code=%d\n", notify->nmhdr.code);
+ g_printf("SCINTILLA NOTIFY: code=%u\n", notify->nmhdr.code);
#endif
- /*
- * Lexing in the container: only used for SciTECO.
- *
- * The "identifier" is abused to enable/disable lexing.
- * It could be extended later on for several internal lexers.
- * The alternative would be an ILexer5 wrapper, written in C++.
- */
- if (notify->nmhdr.code == SCN_STYLENEEDED &&
- teco_view_ssm(ctx, SCI_GETIDENTIFIER, 0, 0) != 0)
- teco_lexer_style(ctx, notify->position);
+ switch (notify->nmhdr.code) {
+ case SCN_STYLENEEDED:
+ /*
+ * Lexing in the container: only used for SciTECO.
+ *
+ * The "identifier" is abused to enable/disable lexing.
+ * It could be extended later on for several internal lexers.
+ * The alternative would be an ILexer5 wrapper, written in C++.
+ */
+ if (teco_view_ssm(ctx, SCI_GETIDENTIFIER, 0, 0) != 0)
+ teco_lexer_style(ctx, notify->position);
+ break;
+
+ case SCN_MODIFIED:
+#ifdef DEBUG
+ g_printf("SCINTILLA NOTIFY: modification=0x%02X\n", notify->modificationType);
+#endif
+
+ if (!teco_ring_current || ctx != teco_ring_current->view)
+ break;
+
+ if (notify->modificationType & SC_MOD_INSERTTEXT) {
+#ifdef DEBUG
+ g_printf("SCINTILLA NOTIFY: INSERT: pos=%ld, text=\"%.*s\"\n",
+ notify->position, (gint)notify->length, notify->text);
+#endif
+ teco_lsp_didchange_insert(teco_ring_current, notify->position,
+ notify->length, notify->text, NULL);
+ }
+ if (notify->modificationType & SC_MOD_BEFOREDELETE) {
+#ifdef DEBUG
+ g_printf("SCINTILLA NOTIFY: BEFORE DELETE: pos=%ld, len=%ld\n",
+ notify->position, notify->length);
+#endif
+ teco_lsp_didchange_delete(teco_ring_current, notify->position,
+ notify->length, NULL);
+ }
+ break;
+ }
}
diff --git a/src/view.h b/src/view.h
index 86cc3dc..887642a 100644
--- a/src/view.h
+++ b/src/view.h
@@ -92,4 +92,4 @@ teco_int_t teco_view_bytes2glyphs_rel(teco_view_t *ctx, teco_int_t pos_glyphs,
teco_int_t teco_view_get_character(teco_view_t *ctx, gsize pos, gsize len);
-void teco_view_process_notify(teco_view_t *ctx, SCNotification *notify);
+void teco_view_process_notify(teco_view_t *ctx, const SCNotification *notify);