diff --git a/lsp/README b/lsp/README index d3e42782a..21fedaa8d 100644 --- a/lsp/README +++ b/lsp/README @@ -205,6 +205,13 @@ When you click on a symbol in the document, this feature highlights all its occurrences in the document. You can customize the highlighting style to your preference by configuring it in the configuration file. +Smart selection expanding/shrinking +----------------------------------- + +This feature allows to expand the current text selection to contain the next +upper syntactic element such as a parent block in programming languages or a +parent tag in XML. Selection shrinking works in the opposite direction. + Document symbol renaming ------------------------ @@ -224,7 +231,20 @@ servers may not be completely reliable when performing the rename so be very cautious when using it. The plugin does not perform any additional checks and does not show any preview of the changes so it is best to use this feature only after committing all modified files so you can -easily revert to a working state if needed. +easily revert to a working state if needed. Since this is potentially a +dangerous operation, to prevent accidental renames, the "Rename" button in the +dialog is not selected by defalut and simply pressing enter just cancels the +dialog. + +Limitations +=========== + +By design, the plugin communicates over stdin/stdout only, is responsible +for launching and terminating the language server process, and supports only +a single language server per file type. + +All of these limitations are addressed by the LSP proxy project available at +https://github.com/techee/lsp-proxy and related issues should be directed there. License ======= diff --git a/lsp/data/lsp.conf b/lsp/data/lsp.conf index 67486d94b..a61a1703b 100644 --- a/lsp/data/lsp.conf +++ b/lsp/data/lsp.conf @@ -133,6 +133,11 @@ autocomplete_trigger_sequences= # 'begin' and 'end' which are typically followed by a newline where typing enter # after these words might select some unwanted word from the autocompletion list. autocomplete_hide_after_words= +# Whether to perform autocompletion inside strings +autocomplete_in_strings=false +# Show documentation (if available) of selected item in autocompletion popup +# in Geany status bar +autocomplete_show_documentation=true # Whether LSP should be used to display diagnostic messages. Typically these are # compiler errors or warnings @@ -237,12 +242,16 @@ format_on_save=false # when servers do not correctly terminate progress notifications. progress_bar_enable=true +# Enable non-standard clangd extension allowing to swap between C/C++ headers +# and sources. Only usable for clangd, it does not work with other servers. +swap_header_source_enable=false + # This is a dummy language server configuration describing the available # language-specific options. Most of the configuration options from the [all] # section can be used here as well. # For an extensive list of various servers and their configurations, check -# https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md +# https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md # While the configuration options names of neovim differ from Geany, the # general concepts are similar and applicable here. [DummyLanguage] @@ -281,6 +290,8 @@ lang_id_mappings=dummylanguage;*.dummy # first). The compile_commands.json file has to be manually regenerated when # the build is modified in any way, such as a file is added/removed. cmd=clangd +swap_header_source_enable=true +autocomplete_in_strings=true autocomplete_use_label=false semantic_tokens_enable=true #initialization_options={"compilationDatabasePath": "/home/some_user/my_project/my_builddir"} @@ -297,6 +308,28 @@ command_1_regex=Apply fix:.* use=C +[CSS] +cmd=vscode-css-language-server --stdio +extra_identifier_characters=- +send_did_change_configuration=true +autocomplete_use_snippets=true +use_without_project=true +use_outside_project_dir=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Dart] +cmd=dart language-server --protocol=lsp +# everything except ( which conflicts with signature help +autocomplete_trigger_sequences=.;=;$;";';{;/;: +semantic_tokens_enable=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + [Go] cmd=gopls autocomplete_apply_additional_edits=true @@ -324,6 +357,18 @@ semantic_tokens_enable=false #show_server_stderr=true +[HTML] +cmd=vscode-html-language-server --stdio +extra_identifier_characters=& +send_did_change_configuration=true +autocomplete_use_snippets=true +use_without_project=true +use_outside_project_dir=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + [Java] cmd=jdtls autocomplete_use_label=false @@ -391,8 +436,11 @@ extra_identifier_characters=$ [Python] # pip install pyright (or: pipx install pyright) cmd=pyright-langserver --stdio -# alternatively pylsp +cmd=pyright-langserver --stdio +# alternatively pylsp, jedi, ruff #cmd=pylsp +#cmd=jedi-language-server +#cmd=ruff server use_outside_project_dir=true use_without_project=true #rpc_log=stdout @@ -446,6 +494,7 @@ autocomplete_use_snippets=true diagnostics_statusbar_severity=4 use_without_project=true use_outside_project_dir=true +autocomplete_in_strings=true # see https://github.com/eclipse/lemminx/blob/main/docs/Configuration.md #initialization_options_file=/home/some_user/init_options.json #formatting_options={ "tabSize": 4, "insertSpaces": true } @@ -465,6 +514,15 @@ use_outside_project_dir=true #show_server_stderr=true +[Zig] +cmd=zls +semantic_tokens_enable=true +#autocomplete_use_snippets=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + # TODO: help needed! Only the above defined language servers have been tested # (lightly). If you know some other working language server or find a problem # with the settings above, please open an issue report or a pull request diff --git a/lsp/src/Makefile.am b/lsp/src/Makefile.am index e35efedc5..ffeb5ec6b 100644 --- a/lsp/src/Makefile.am +++ b/lsp/src/Makefile.am @@ -37,6 +37,8 @@ lsp_la_SOURCES = \ lsp-rpc.h \ lsp-semtokens.c \ lsp-semtokens.h \ + lsp-selection-range.c \ + lsp-selection-range.h \ lsp-server.c \ lsp-server.h \ lsp-signature.c \ diff --git a/lsp/src/lsp-autocomplete.c b/lsp/src/lsp-autocomplete.c index 322c41584..73899800c 100644 --- a/lsp/src/lsp-autocomplete.c +++ b/lsp/src/lsp-autocomplete.c @@ -36,11 +36,15 @@ typedef struct gchar *label; LspCompletionKind kind; gchar *sort_text; + gchar *filter_text; gchar *insert_text; gchar *detail; + gchar *documentation; LspTextEdit *text_edit; GPtrArray * additional_edits; gboolean is_snippet; + GVariant *raw_symbol; + gboolean resolved; } LspAutocompleteSymbol; @@ -60,10 +64,18 @@ typedef struct } SortData; +typedef struct +{ + LspAutocompleteSymbol *symbol; + GeanyDocument *doc; +} ResolveData; + + static GPtrArray *displayed_autocomplete_symbols = NULL; static gint sent_request_id = 0; static gint received_request_id = 0; static gint discard_up_to_request_id = 0; +static gboolean statusbar_modified = FALSE; void lsp_autocomplete_discard_pending_requests() @@ -85,11 +97,14 @@ static void free_autocomplete_symbol(gpointer data) LspAutocompleteSymbol *sym = data; g_free(sym->label); g_free(sym->sort_text); + g_free(sym->filter_text); g_free(sym->insert_text); g_free(sym->detail); + g_free(sym->documentation); lsp_utils_free_lsp_text_edit(sym->text_edit); if (sym->additional_edits) g_ptr_array_free(sym->additional_edits, TRUE); + g_variant_unref(sym->raw_symbol); g_free(sym); } @@ -210,12 +225,134 @@ void lsp_autocomplete_item_selected(LspServer *server, GeanyDocument *doc, guint } +void lsp_autocomplete_clear_statusbar(void) +{ + if (statusbar_modified) + ui_set_statusbar(FALSE, " "); + statusbar_modified = FALSE; +} + + +static void resolve_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + ResolveData *data = user_data; + LspServer *server = lsp_server_get_if_running(data->doc); + + if (!error && server && data->doc == document_get_current() && displayed_autocomplete_symbols && + g_ptr_array_find(displayed_autocomplete_symbols, data->symbol, NULL)) + { + const gchar *documentation = NULL; + + if (!JSONRPC_MESSAGE_PARSE(return_value, "documentation", JSONRPC_MESSAGE_GET_STRING(&documentation))) + { + JSONRPC_MESSAGE_PARSE(return_value, "documentation", "{", + "value", JSONRPC_MESSAGE_GET_STRING(&documentation), + "}"); + } + + if (documentation) + { + gint current_selection = SSM(data->doc->editor->sci, SCI_AUTOCGETCURRENT, 0, 0); + gchar *label; + + g_free(data->symbol->documentation); + data->symbol->documentation = g_strdup(documentation); + data->symbol->resolved = TRUE; + + if (current_selection < displayed_autocomplete_symbols->len) + { + LspAutocompleteSymbol *sym = displayed_autocomplete_symbols->pdata[current_selection]; + + label = get_symbol_label(server, sym); + if (sym == data->symbol) + lsp_autocomplete_selection_changed(data->doc, label); // reshow + g_free(label); + } + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + } + + g_free(data); +} + + +LspAutocompleteSymbol *find_symbol(GeanyDocument *doc, const gchar *text) +{ + LspServer *srv = lsp_server_get(doc); + LspAutocompleteSymbol *sym = NULL; + guint i; + + if (!srv || !displayed_autocomplete_symbols) + return NULL; + + foreach_ptr_array(sym, i, displayed_autocomplete_symbols) + { + gchar *label = get_symbol_label(srv, sym); + gboolean should_return = FALSE; + + if (g_strcmp0(label, text) == 0) + should_return = TRUE; + + g_free(label); + + if (should_return) + return sym; + } + + return NULL; +} + + +void lsp_autocomplete_selection_changed(GeanyDocument *doc, const gchar *text) +{ + LspAutocompleteSymbol *sym = find_symbol(doc, text); + LspServer *srv = lsp_server_get(doc); + + if (!sym || !srv || !srv->config.autocomplete_show_documentation) + return; + + if (!sym->resolved && srv->supports_completion_resolve) + { + ResolveData *data = g_new0(ResolveData, 1); + data->doc = doc; + data->symbol = sym; + lsp_rpc_call(srv, "completionItem/resolve", sym->raw_symbol, resolve_cb, data); + } + else if (!sym->documentation) + lsp_autocomplete_clear_statusbar(); + else + { + GString *str; + + g_strstrip(sym->documentation); + str = g_string_new(sym->documentation); + g_string_replace(str, "\n\n", " | ", -1); + g_string_replace(str, "\n", " ", -1); + g_string_replace(str, " ", " ", -1); + if (!EMPTY(str->str)) + { + ui_set_statusbar(FALSE, "%s", str->str); + statusbar_modified = TRUE; + } + else + lsp_autocomplete_clear_statusbar(); + + g_string_free(str, TRUE); + } +} + + void lsp_autocomplete_style_init(GeanyDocument *doc) { ScintillaObject *sci = doc->editor->sci; LspServer *srv = lsp_server_get_if_running(doc); - if (!srv) + // make sure to revert to default Geany behavior when autocompletion not + // available + SSM(sci, SCI_AUTOCSETAUTOHIDE, TRUE, 0); + + if (!srv || !srv->config.autocomplete_enable) return; SSM(sci, SCI_AUTOCSETORDER, SC_ORDER_CUSTOM, 0); @@ -238,6 +375,7 @@ static void show_tags_list(LspServer *server, GeanyDocument *doc, GPtrArray *sym gint pos = sci_get_current_position(sci); GString *words = g_string_sized_new(2000); gchar *label; + gchar *first_label = NULL; for (i = 0; i < symbols->len; i++) { @@ -252,6 +390,8 @@ static void show_tags_list(LspServer *server, GeanyDocument *doc, GPtrArray *sym g_string_append_c(words, '\n'); label = get_symbol_label(server, symbol); + if (!first_label) + first_label = g_strdup(label); g_string_append(words, label); sprintf(buf, "?%u", icon_id + 1); @@ -262,6 +402,11 @@ static void show_tags_list(LspServer *server, GeanyDocument *doc, GPtrArray *sym lsp_autocomplete_set_displayed_symbols(symbols); SSM(sci, SCI_AUTOCSHOW, get_ident_prefixlen(server->config.word_chars, doc, pos), (sptr_t) words->str); + if (first_label) + { + lsp_autocomplete_selection_changed(doc, first_label); + g_free(first_label); + } // TODO: remove eventually #ifndef SC_AUTOCOMPLETE_SELECT_FIRST_ITEM @@ -300,31 +445,72 @@ static gboolean has_identifier_chars(const gchar *s, const gchar *word_chars) } +static void get_letter_counts(gint *counts, const gchar *str) +{ + gint i; + + for (i = 0; str[i]; i++) + { + gchar c = str[i]; + if (c >= 'a' && c <= 'z') + counts[c-'a']++; + } +} + + +// fuzzy filtering - only require the same letters appear in name and prefix and +// that the first two letters of prefix appear as a substring in name. Most +// servers filter by themselves and this avoids filtering-out good suggestions +// when the typed string is just slightly misspelled. For servers that don't +// filter by themselves this filters the the strings that are totally out and +// together with sorting presents reasonable suggestions +static gboolean should_filter(const gchar *name, const gchar *prefix) +{ + gint name_letters['z'-'a'+1] = {0}; + gint prefix_letters['z'-'a'+1] = {0}; + gchar c; + + get_letter_counts(name_letters, name); + get_letter_counts(prefix_letters, prefix); + + for (c = 'a'; c <= 'z'; c++) + { + if (name_letters[c-'a'] < prefix_letters[c-'a']) + return TRUE; + } + + if (strlen(prefix) >= 2) + { + gchar pref[] = {prefix[0], prefix[1], '\0'}; + if (!strstr(name, pref)) + return TRUE; + } + + return FALSE; +} + + +static gboolean filter_autocomplete_symbols(LspAutocompleteSymbol *sym, const gchar *text, + gboolean use_label) +{ + const gchar *filter_text; + + if (EMPTY(text)) + return FALSE; + + filter_text = sym->filter_text ? sym->filter_text : get_label(sym, use_label); + + return GPOINTER_TO_INT(lsp_utils_lowercase_cmp((LspUtilsCmpFn)should_filter, filter_text, text)); +} + + static gint sort_autocomplete_symbols(gconstpointer a, gconstpointer b, gpointer user_data) { LspAutocompleteSymbol *sym1 = *((LspAutocompleteSymbol **)a); LspAutocompleteSymbol *sym2 = *((LspAutocompleteSymbol **)b); SortData *sort_data = user_data; - gchar *label1 = NULL; - gchar *label2 = NULL; - - if (sort_data->use_label && sym1->label) - label1 = sym1->label; - else if (sym1->text_edit && sym1->text_edit->new_text) - label1 = sym1->text_edit->new_text; - else if (sym1->insert_text) - label1 = sym1->insert_text; - else if (sym1->label) - label1 = sym1->label; - - if (sort_data->use_label && sym2->label) - label2 = sym2->label; - else if (sym2->text_edit && sym2->text_edit->new_text) - label2 = sym2->text_edit->new_text; - else if (sym2->insert_text) - label2 = sym2->insert_text; - else if (sym2->label) - label2 = sym2->label; + const gchar *label1 = get_label(sym1, sort_data->use_label); + const gchar *label2 = get_label(sym2, sort_data->use_label); if (sort_data->pass == 2 && label1 && label2 && sort_data->prefix) { @@ -393,13 +579,13 @@ static gboolean should_add(GPtrArray *symbols, const gchar *prefix) if (symbols->len > 1) return TRUE; - // don't single value with what's already typed unless it's a snippet + // don't add single value with what's already typed unless it's a snippet sym = symbols->pdata[0]; label = get_label(sym, FALSE); if (g_strcmp0(label, prefix) != 0) return TRUE; - return sym->is_snippet || sym->kind == LspCompletionKindSnippet; + return sym->is_snippet; } @@ -424,11 +610,14 @@ static void process_response(LspServer *server, GVariant *response, GeanyDocumen iter = g_variant_iter_new(response); if (!iter) + { + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); return; + } symbols = g_ptr_array_new_full(0, NULL); // not freeing symbols here - while (g_variant_iter_loop(iter, "v", &member)) + while (g_variant_iter_next(iter, "v", &member)) { LspAutocompleteSymbol *sym; GVariant *text_edit = NULL; @@ -436,14 +625,19 @@ static void process_response(LspServer *server, GVariant *response, GeanyDocumen const gchar *label = NULL; const gchar *insert_text = NULL; const gchar *sort_text = NULL; + const gchar *filter_text = NULL; const gchar *detail = NULL; + const gchar *documentation = NULL; gint64 kind = 0; gint64 format = 0; JSONRPC_MESSAGE_PARSE(member, "kind", JSONRPC_MESSAGE_GET_INT64(&kind)); if (kind == LspCompletionKindSnippet && !server->config.autocomplete_use_snippets) + { + g_variant_unref(member); continue; + } JSONRPC_MESSAGE_PARSE(member, "insertText", JSONRPC_MESSAGE_GET_STRING(&insert_text)); JSONRPC_MESSAGE_PARSE(member, "insertTextFormat", JSONRPC_MESSAGE_GET_INT64(&format)); @@ -452,24 +646,35 @@ static void process_response(LspServer *server, GVariant *response, GeanyDocumen // Lua server flags as snippet without actually being a snippet insert_text && strchr(insert_text, '$')) { + g_variant_unref(member); continue; } JSONRPC_MESSAGE_PARSE(member, "label", JSONRPC_MESSAGE_GET_STRING(&label)); JSONRPC_MESSAGE_PARSE(member, "sortText", JSONRPC_MESSAGE_GET_STRING(&sort_text)); + JSONRPC_MESSAGE_PARSE(member, "filterText", JSONRPC_MESSAGE_GET_STRING(&filter_text)); JSONRPC_MESSAGE_PARSE(member, "detail", JSONRPC_MESSAGE_GET_STRING(&detail)); JSONRPC_MESSAGE_PARSE(member, "textEdit", JSONRPC_MESSAGE_GET_VARIANT(&text_edit)); JSONRPC_MESSAGE_PARSE(member, "additionalTextEdits", JSONRPC_MESSAGE_GET_ITER(&additional_edits)); + if (!JSONRPC_MESSAGE_PARSE(member, "documentation", JSONRPC_MESSAGE_GET_STRING(&documentation))) + { + JSONRPC_MESSAGE_PARSE(member, "documentation", "{", + "value", JSONRPC_MESSAGE_GET_STRING(&documentation), + "}"); + } + sym = g_new0(LspAutocompleteSymbol, 1); sym->label = g_strdup(label); sym->insert_text = g_strdup(insert_text); sym->sort_text = g_strdup(sort_text); sym->detail = g_strdup(detail); + sym->documentation = g_strdup(documentation); sym->kind = kind; sym->text_edit = lsp_utils_parse_text_edit(text_edit); sym->additional_edits = lsp_utils_parse_text_edits(additional_edits); sym->is_snippet = (format == 2); + sym->raw_symbol = member; g_ptr_array_add(symbols, sym); @@ -486,13 +691,17 @@ static void process_response(LspServer *server, GVariant *response, GeanyDocumen symbols_filtered = g_ptr_array_new_full(symbols->len, free_autocomplete_symbol); entry_set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - /* remove duplicates */ + if (prefixlen > 0) + sort_data.prefix = sci_get_contents_range(sci, pos - prefixlen, pos); + + /* remove duplicates and items not matching filtering criteria */ for (i = 0; i < symbols->len; i++) { LspAutocompleteSymbol *sym = symbols->pdata[i]; gchar *display_label = get_symbol_label(server, sym); - if (g_hash_table_contains(entry_set, display_label)) + if (g_hash_table_contains(entry_set, display_label) || + filter_autocomplete_symbols(sym, sort_data.prefix, sort_data.use_label)) { free_autocomplete_symbol(sym); g_free(display_label); @@ -507,8 +716,6 @@ static void process_response(LspServer *server, GVariant *response, GeanyDocumen g_ptr_array_free(symbols, TRUE); symbols = symbols_filtered; - if (prefixlen > 0) - sort_data.prefix = sci_get_contents_range(sci, pos - prefixlen, pos); sort_data.pass = 2; /* sort with symbols matching the typed prefix first */ g_ptr_array_sort_with_data(symbols, sort_autocomplete_symbols, &sort_data); @@ -586,12 +793,27 @@ void lsp_autocomplete_completion(LspServer *server, GeanyDocument *doc, gboolean LspAutocompleteAsyncData *data; ScintillaObject *sci = doc->editor->sci; gint pos = sci_get_current_position(sci); + gint pos_before = SSM(sci, SCI_POSITIONBEFORE, pos, 0); LspPosition lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + gint lexer = sci_get_lexer(sci); + gint style = sci_get_style_at(sci, pos_before); + gint style_before = sci_get_style_at(sci, SSM(sci, SCI_POSITIONBEFORE, pos_before, 0)); gboolean is_trigger_char = FALSE; - gchar c = pos > 0 ? sci_get_char_at(sci, SSM(sci, SCI_POSITIONBEFORE, pos, 0)) : '\0'; + gchar c = pos > 0 ? sci_get_char_at(sci, pos_before) : '\0'; gchar c_str[2] = {c, '\0'}; gint prefixlen = get_ident_prefixlen(server->config.word_chars, doc, pos); + // also check position before the just typed characters (i.e. 2 positions + // before pos) - at least for Python comments typing at EOL probably doesn't + // have up-to-date styling information + if ((!server->config.autocomplete_in_strings && + (highlighting_is_string_style(lexer, style) || highlighting_is_string_style(lexer, style_before))) || + (highlighting_is_comment_style(lexer, style) || highlighting_is_comment_style(lexer, style_before))) + { + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); + return; + } + if (prefixlen == 0) { if (!EMPTY(server->config.autocomplete_trigger_sequences) && @@ -660,7 +882,7 @@ void lsp_autocomplete_completion(LspServer *server, GeanyDocument *doc, gboolean "}", "context", "{", "triggerKind", JSONRPC_MESSAGE_PUT_INT32(is_trigger_char ? 2 : 1), - "triggerCharacter", JSONRPC_MESSAGE_PUT_STRING(c_str), + "triggerCharacter", JSONRPC_MESSAGE_PUT_STRING(is_trigger_char ? c_str : NULL), "}" ); diff --git a/lsp/src/lsp-autocomplete.h b/lsp/src/lsp-autocomplete.h index b43d34475..3aca0810d 100644 --- a/lsp/src/lsp-autocomplete.h +++ b/lsp/src/lsp-autocomplete.h @@ -27,6 +27,8 @@ void lsp_autocomplete_completion(LspServer *server, GeanyDocument *doc, gboolean void lsp_autocomplete_set_displayed_symbols(GPtrArray *symbols); void lsp_autocomplete_item_selected(LspServer *server, GeanyDocument *doc, guint index); +void lsp_autocomplete_selection_changed(GeanyDocument *doc, const gchar *text); void lsp_autocomplete_discard_pending_requests(); +void lsp_autocomplete_clear_statusbar(void); #endif /* LSP_AUTOCOMPLETE_H */ diff --git a/lsp/src/lsp-command.c b/lsp/src/lsp-command.c index cc82b1be0..0bc37e6d3 100644 --- a/lsp/src/lsp-command.c +++ b/lsp/src/lsp-command.c @@ -32,6 +32,7 @@ typedef struct { LspCallback callback; gpointer user_data; + GeanyDocument *doc; } CommandData; @@ -50,10 +51,140 @@ void lsp_command_free(LspCommand *cmd) g_variant_unref(cmd->arguments); if (cmd->edit) g_variant_unref(cmd->edit); + if (cmd->data) + g_variant_unref(cmd->data); g_free(cmd); } +static LspCommand *parse_code_action(GVariant *code_action) +{ + const gchar *title = NULL; + const gchar *command = NULL; + GVariant *arguments = NULL; + GVariant *edit = NULL; + GVariant *data = NULL; + gboolean is_command; + LspCommand *cmd; + + // Can either be Command or CodeAction: + // Command {title: string; command: string; arguments?: LSPAny[];} + // CodeAction {title: string; edit?: WorkspaceEdit; command?: Command; data?: LSPAny[];} + + JSONRPC_MESSAGE_PARSE(code_action, + "title", JSONRPC_MESSAGE_GET_STRING(&title) + ); + + if (!title) + return NULL; + + is_command = JSONRPC_MESSAGE_PARSE(code_action, + "command", JSONRPC_MESSAGE_GET_STRING(&command) + ); + + if (is_command) + { + JSONRPC_MESSAGE_PARSE(code_action, + "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments) + ); + } + else + { + JSONRPC_MESSAGE_PARSE(code_action, + "command", "{", + "command", JSONRPC_MESSAGE_GET_STRING(&command), + "}" + ); + + JSONRPC_MESSAGE_PARSE(code_action, + "command", "{", + "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments), + "}" + ); + + JSONRPC_MESSAGE_PARSE(code_action, + "edit", JSONRPC_MESSAGE_GET_VARIANT(&edit) + ); + + JSONRPC_MESSAGE_PARSE(code_action, + "data", JSONRPC_MESSAGE_GET_VARIANT(&data) + ); + } + + cmd = g_new0(LspCommand, 1); + cmd->title = g_strdup(title); + cmd->command = g_strdup(command); + cmd->arguments = arguments; + cmd->edit = edit; + cmd->data = data; + + return cmd; +} + + +static void resolve_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + CommandData *data = user_data; + gboolean performed = FALSE; + + if (!error && data->doc == document_get_current()) + { + LspServer *server = lsp_server_get_if_running(data->doc); + + if (server) + { + LspCommand *cmd = parse_code_action(return_value); + + if (cmd && (cmd->command || cmd->edit)) + { + lsp_command_perform(server, cmd, data->callback, data->user_data); + performed = TRUE; + } + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + } + + if (!performed) + data->callback(data->user_data); + + g_free(data); +} + + +static void resolve_code_action(LspServer *server, LspCommand *cmd, LspCallback callback, gpointer user_data) +{ + GVariant *node; + CommandData *data; + + if (cmd->data) + { + GVariantDict dict; + + g_variant_dict_init(&dict, NULL); + g_variant_dict_insert_value(&dict, "title", g_variant_new_string(cmd->title)); + g_variant_dict_insert_value(&dict, "data", cmd->data); + node = g_variant_take_ref(g_variant_dict_end(&dict)); + } + else + { + node = JSONRPC_MESSAGE_NEW( + "title", JSONRPC_MESSAGE_PUT_STRING(cmd->title) + ); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + data = g_new0(CommandData, 1); + data->callback = callback; + data->user_data = user_data; + data->doc = document_get_current(); + lsp_rpc_call(server, "codeAction/resolve", node, resolve_cb, data); + + g_variant_unref(node); +} + + static void command_cb(GVariant *return_value, GError *error, gpointer user_data) { CommandData *data = user_data; @@ -66,6 +197,12 @@ static void command_cb(GVariant *return_value, GError *error, gpointer user_data void lsp_command_perform(LspServer *server, LspCommand *cmd, LspCallback callback, gpointer user_data) { + if (!cmd->command && !cmd->edit) + { + resolve_code_action(server, cmd, callback, user_data); + return; + } + if (cmd->edit) lsp_utils_apply_workspace_edit(cmd->edit); @@ -123,69 +260,10 @@ static void code_action_cb(GVariant *return_value, GError *error, gpointer user_ while (g_variant_iter_loop(&iter, "v", &code_action)) { - const gchar *title = NULL; - const gchar *command = NULL; - GVariant *edit = NULL; - LspCommand *cmd; - gboolean is_command; - - // Can either be Command or CodeAction: - // Command {title: string; command: string; arguments?: LSPAny[];} - // CodeAction {title: string; edit?: WorkspaceEdit; command?: Command;} - - JSONRPC_MESSAGE_PARSE(code_action, - "title", JSONRPC_MESSAGE_GET_STRING(&title) - ); - - is_command = JSONRPC_MESSAGE_PARSE(code_action, - "command", JSONRPC_MESSAGE_GET_STRING(&command) - ); - - if (!is_command) - { - JSONRPC_MESSAGE_PARSE(code_action, - "command", "{", - "command", JSONRPC_MESSAGE_GET_STRING(&command), - "}" - ); - - JSONRPC_MESSAGE_PARSE(code_action, - "edit", JSONRPC_MESSAGE_GET_VARIANT(&edit) - ); - } - - if (title && (command || edit)) - { - GVariant *arguments = NULL; - - if (is_command) - { - JSONRPC_MESSAGE_PARSE(code_action, - "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments) - ); - } - else - { - JSONRPC_MESSAGE_PARSE(code_action, - "command", "{", - "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments), - "}" - ); - } - - cmd = g_new0(LspCommand, 1); - cmd->title = g_strdup(title); - cmd->command = g_strdup(command); - cmd->arguments = arguments; - cmd->edit = edit; + LspCommand *cmd = parse_code_action(code_action); + if (cmd) g_ptr_array_add(code_actions, cmd); - } - else - { - if (edit) - g_variant_unref(edit); - } } } } diff --git a/lsp/src/lsp-command.h b/lsp/src/lsp-command.h index 7a834a333..e30c9f52d 100644 --- a/lsp/src/lsp-command.h +++ b/lsp/src/lsp-command.h @@ -30,6 +30,7 @@ typedef struct gchar *command; GVariant *arguments; GVariant *edit; + GVariant *data; } LspCommand; typedef gboolean (*CodeActionCallback) (GPtrArray *actions, gpointer user_data); diff --git a/lsp/src/lsp-goto.c b/lsp/src/lsp-goto.c index 125c4a002..236bc3cc3 100644 --- a/lsp/src/lsp-goto.c +++ b/lsp/src/lsp-goto.c @@ -143,8 +143,27 @@ static void goto_cb(GVariant *return_value, GError *error, gpointer user_data) msgwin_switch_tab(MSG_MESSAGE, TRUE); } + // single location + + /* check G_VARIANT_TYPE_DICTIONARY ("a{?*}") before + G_VARIANT_TYPE_ARRAY ("a*") as dictionary is apparently a + subset of array :-( */ + if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_DICTIONARY)) + { + LspLocation *loc = lsp_utils_parse_location(return_value); + + if (loc) + { + if (data->show_in_msgwin) + show_in_msgwin(loc, NULL); + else + goto_location(data->doc, loc); + } + + lsp_utils_free_lsp_location(loc); + } // array of locations - if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + else if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) { GPtrArray *locations = NULL; GVariantIter iter; @@ -207,21 +226,6 @@ static void goto_cb(GVariant *return_value, GError *error, gpointer user_data) g_ptr_array_free(locations, TRUE); } - //single location - else if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_DICTIONARY)) - { - LspLocation *loc = lsp_utils_parse_location(return_value); - - if (loc) - { - if (data->show_in_msgwin) - show_in_msgwin(loc, NULL); - else - goto_location(data->doc, loc); - } - - lsp_utils_free_lsp_location(loc); - } } //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); diff --git a/lsp/src/lsp-highlight.c b/lsp/src/lsp-highlight.c index 1e5440aea..961ee24de 100644 --- a/lsp/src/lsp-highlight.c +++ b/lsp/src/lsp-highlight.c @@ -26,6 +26,8 @@ #include +#define HIGHLIGHT_DIRTY "lsp_highlight_dirty" + typedef struct { GeanyDocument *doc; @@ -35,12 +37,17 @@ typedef struct { } LspHighlightData; +extern GeanyPlugin *geany_plugin; +extern GeanyData *geany_data; + static gint indicator; -static gboolean dirty; +static gint64 last_request_time; +static gint request_source; void lsp_highlight_clear(GeanyDocument *doc) { + gboolean dirty = GPOINTER_TO_UINT(plugin_get_document_data(geany_plugin, doc, HIGHLIGHT_DIRTY)); if (dirty) { ScintillaObject *sci = doc->editor->sci; @@ -48,7 +55,7 @@ void lsp_highlight_clear(GeanyDocument *doc) if (indicator > 0) sci_indicator_set(sci, indicator); sci_indicator_clear(sci, 0, sci_get_length(sci)); - dirty = FALSE; + plugin_set_document_data(geany_plugin, doc, HIGHLIGHT_DIRTY, GUINT_TO_POINTER(FALSE)); } } @@ -65,7 +72,7 @@ void lsp_highlight_style_init(GeanyDocument *doc) if (indicator > 0) { - dirty = TRUE; + plugin_set_document_data(geany_plugin, doc, HIGHLIGHT_DIRTY, GUINT_TO_POINTER(TRUE)); lsp_highlight_clear(doc); } indicator = lsp_utils_set_indicator_style(sci, srv->config.highlighting_style); @@ -82,7 +89,7 @@ static void highlight_range(GeanyDocument *doc, LspRange range) if (indicator > 0) editor_indicator_set_on_range(doc->editor, indicator, start_pos, end_pos); - dirty = TRUE; + plugin_set_document_data(geany_plugin, doc, HIGHLIGHT_DIRTY, GUINT_TO_POINTER(TRUE)); } @@ -193,6 +200,7 @@ static void send_request(LspServer *server, GeanyDocument *doc, gint pos, gboole data->highlight = highlight; lsp_rpc_call(server, "textDocument/documentHighlight", node, highlight_cb, data); + last_request_time = g_get_monotonic_time(); } else lsp_highlight_clear(doc); @@ -204,14 +212,56 @@ static void send_request(LspServer *server, GeanyDocument *doc, gint pos, gboole } -void lsp_highlight_send_request(LspServer *server, GeanyDocument *doc) +static gboolean request_idle(gpointer data) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv; + gint pos; + + request_source = 0; + + srv = lsp_server_get_if_running(doc); + if (!srv) + return G_SOURCE_REMOVE; + + pos = sci_get_current_position(doc->editor->sci); + + send_request(srv, doc, pos, TRUE); + + return G_SOURCE_REMOVE; +} + + +void lsp_highlight_schedule_request(GeanyDocument *doc) { gint pos = sci_get_current_position(doc->editor->sci); + LspServer *srv = lsp_server_get_if_running(doc); + gchar *iden; + + if (!srv) + return; - if (!doc || !doc->real_path) + iden = lsp_utils_get_current_iden(doc, pos, srv->config.word_chars); + if (!iden) + { + lsp_highlight_clear(doc); + // cancel request because we have an up-to-date information there's nothing + // to highlight + if (request_source != 0) + g_source_remove(request_source); + request_source = 0; return; + } + g_free(iden); + + if (request_source != 0) + g_source_remove(request_source); + request_source = 0; - send_request(server, doc, pos, TRUE); + if (last_request_time == 0 || g_get_monotonic_time() > last_request_time + 300000) + request_idle(NULL); + else + request_source = plugin_timeout_add(geany_plugin, 300, request_idle, NULL); } diff --git a/lsp/src/lsp-highlight.h b/lsp/src/lsp-highlight.h index dbd7fc1fd..37e7414ba 100644 --- a/lsp/src/lsp-highlight.h +++ b/lsp/src/lsp-highlight.h @@ -25,7 +25,7 @@ void lsp_highlight_style_init(GeanyDocument *doc); -void lsp_highlight_send_request(LspServer *server, GeanyDocument *doc); +void lsp_highlight_schedule_request(GeanyDocument *doc); void lsp_highlight_rename(gint pos); diff --git a/lsp/src/lsp-main.c b/lsp/src/lsp-main.c index e5ee166a4..b152c51df 100644 --- a/lsp/src/lsp-main.c +++ b/lsp/src/lsp-main.c @@ -40,6 +40,7 @@ #include "lsp-extension.h" #include "lsp-workspace-folders.h" #include "lsp-symbol-tree.h" +#include "lsp-selection-range.h" #include #include @@ -61,6 +62,7 @@ LspProjectConfigurationType project_configuration_type = UserConfigurationType; gchar *project_configuration_file; static gint last_click_pos; +static gboolean session_loaded; PLUGIN_VERSION_CHECK(248) @@ -76,32 +78,37 @@ PLUGIN_SET_TRANSLATABLE_INFO( #define CODE_ACTIONS_PERFORMED "lsp_code_actions_performed" enum { - KB_GOTO_DEFINITION, - KB_GOTO_DECLARATION, - KB_GOTO_TYPE_DEFINITION, + KB_GOTO_DEFINITION, + KB_GOTO_DECLARATION, + KB_GOTO_TYPE_DEFINITION, - KB_GOTO_ANYWHERE, - KB_GOTO_DOC_SYMBOL, - KB_GOTO_WORKSPACE_SYMBOL, - KB_GOTO_LINE, + KB_GOTO_ANYWHERE, + KB_GOTO_DOC_SYMBOL, + KB_GOTO_WORKSPACE_SYMBOL, + KB_GOTO_LINE, - KB_GOTO_NEXT_DIAG, - KB_GOTO_PREV_DIAG, - KB_SHOW_DIAG, + KB_GOTO_NEXT_DIAG, + KB_GOTO_PREV_DIAG, + KB_SHOW_DIAG, - KB_FIND_IMPLEMENTATIONS, - KB_FIND_REFERENCES, + KB_FIND_IMPLEMENTATIONS, + KB_FIND_REFERENCES, - KB_SHOW_HOVER_POPUP, - KB_SWAP_HEADER_SOURCE, + KB_EXPAND_SELECTION, + KB_SHRINK_SELECTION, - KB_RENAME_IN_FILE, - KB_RENAME_IN_PROJECT, - KB_FORMAT_CODE, + KB_SHOW_HOVER_POPUP, + KB_SHOW_CODE_ACTIONS, - KB_RESTART_SERVERS, + KB_SWAP_HEADER_SOURCE, - KB_COUNT + KB_RENAME_IN_FILE, + KB_RENAME_IN_PROJECT, + KB_FORMAT_CODE, + + KB_RESTART_SERVERS, + + KB_COUNT }; @@ -127,7 +134,12 @@ struct GtkWidget *rename_in_project; GtkWidget *format_code; + GtkWidget *expand_selection; + GtkWidget *shrink_selection; + GtkWidget *hover_popup; + GtkWidget *code_action_popup; + GtkWidget *header_source; } menu_items; @@ -271,6 +283,7 @@ static void update_menu(GeanyDocument *doc) { LspServer *srv = lsp_server_get_if_running(doc); gboolean goto_definition_enable = srv && srv->config.goto_definition_enable; + gboolean selection_range_enable = srv && srv->config.selection_range_enable; gboolean goto_references_enable = srv && srv->config.goto_references_enable; gboolean goto_type_definition_enable = srv && srv->config.goto_type_definition_enable; gboolean document_formatting_enable = srv && srv->config.document_formatting_enable; @@ -281,6 +294,8 @@ static void update_menu(GeanyDocument *doc) gboolean goto_implementation_enable = srv && srv->config.goto_implementation_enable; gboolean diagnostics_enable = srv && srv->config.diagnostics_enable; gboolean hover_popup_enable = srv && srv->config.hover_available; + gboolean code_action_enable = srv && (srv->config.code_action_enable || srv->config.code_lens_enable); + gboolean swap_header_source_enable = srv && srv->config.swap_header_source_enable; if (!menu_items.parent_item) return; @@ -300,7 +315,13 @@ static void update_menu(GeanyDocument *doc) gtk_widget_set_sensitive(menu_items.rename_in_project, rename_enable); gtk_widget_set_sensitive(menu_items.format_code, document_formatting_enable || range_formatting_enable); + gtk_widget_set_sensitive(menu_items.expand_selection, selection_range_enable); + gtk_widget_set_sensitive(menu_items.shrink_selection, selection_range_enable); + + gtk_widget_set_sensitive(menu_items.header_source, swap_header_source_enable); + gtk_widget_set_sensitive(menu_items.hover_popup, hover_popup_enable); + gtk_widget_set_sensitive(menu_items.code_action_popup, code_action_enable); } @@ -309,11 +330,11 @@ static gboolean on_update_idle(gpointer data) GeanyDocument *doc = data; LspServer *srv; + plugin_set_document_data(geany_plugin, doc, UPDATE_SOURCE_DOC_DATA, GUINT_TO_POINTER(0)); + if (!DOC_VALID(doc)) return G_SOURCE_REMOVE; - plugin_set_document_data(geany_plugin, doc, UPDATE_SOURCE_DOC_DATA, GUINT_TO_POINTER(0)); - srv = lsp_server_get_if_running(doc); if (!srv) return G_SOURCE_REMOVE; @@ -332,11 +353,16 @@ static void on_document_visible(GeanyDocument *doc) { LspServer *srv = lsp_server_get(doc); + session_loaded = TRUE; + update_menu(doc); // quick synchronous refresh with the last value without server request lsp_symbol_tree_refresh(); + // perform also without server - to revert to default Geany behavior + lsp_autocomplete_style_init(doc); + if (!srv) return; @@ -345,7 +371,8 @@ static void on_document_visible(GeanyDocument *doc) lsp_highlight_style_init(doc); lsp_semtokens_style_init(doc); lsp_code_lens_style_init(doc); - lsp_autocomplete_style_init(doc); + + lsp_selection_clear_selections(); // just in case we didn't get some callback from the server on_save_finish(doc); @@ -397,6 +424,7 @@ static void destroy_all(void) static void stop_and_init_all_servers(void) { lsp_server_stop_all(FALSE); + session_loaded = FALSE; lsp_server_init_all(); destroy_all(); @@ -585,7 +613,7 @@ static void on_document_filetype_set(G_GNUC_UNUSED GObject *obj, GeanyDocument * // called also when opening documents - without this it would start servers // unnecessarily - if (!lsp_sync_is_document_open(doc)) + if (!session_loaded) return; srv_old = lsp_server_get_for_ft(filetype_old); @@ -626,7 +654,7 @@ static void on_document_activate(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, static gboolean on_editor_notify(G_GNUC_UNUSED GObject *obj, GeanyEditor *editor, SCNotification *nt, G_GNUC_UNUSED gpointer user_data) { - static gboolean ignore_selection_change = FALSE; // static! + static gboolean perform_highlight = TRUE; // static! GeanyDocument *doc = editor->document; ScintillaObject *sci = editor->sci; @@ -642,7 +670,7 @@ static gboolean on_editor_notify(G_GNUC_UNUSED GObject *obj, GeanyEditor *editor return FALSE; // ignore cursor position change as a result of autocomplete (for highlighting) - ignore_selection_change = TRUE; + perform_highlight = FALSE; sci_start_undo_action(editor->sci); lsp_autocomplete_item_selected(srv, doc, SSM(sci, SCI_AUTOCGETCURRENT, 0, 0)); @@ -657,8 +685,14 @@ static gboolean on_editor_notify(G_GNUC_UNUSED GObject *obj, GeanyEditor *editor { lsp_autocomplete_set_displayed_symbols(NULL); lsp_autocomplete_discard_pending_requests(); + lsp_autocomplete_clear_statusbar(); return FALSE; } + else if (nt->nmhdr.code == SCN_AUTOCSELECTIONCHANGE && + plugin_extension_autocomplete_provided(doc, &extension)) + { + lsp_autocomplete_selection_changed(doc, nt->text); + } else if (nt->nmhdr.code == SCN_CALLTIPCLICK && plugin_extension_calltips_provided(doc, &extension)) { @@ -727,6 +761,12 @@ static gboolean on_editor_notify(G_GNUC_UNUSED GObject *obj, GeanyEditor *editor if (!srv || !doc->real_path) return FALSE; + if (nt->modificationType & (SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE)) + { + perform_highlight = FALSE; + lsp_highlight_clear(doc); + } + // BEFORE insert, BEFORE delete - send the original document if (!lsp_sync_is_document_open(doc) && nt->modificationType & (SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE)) @@ -802,20 +842,24 @@ static gboolean on_editor_notify(G_GNUC_UNUSED GObject *obj, GeanyEditor *editor lsp_diagnostics_hide_calltip(doc); SSM(sci, SCI_AUTOCCANCEL, 0, 0); + + if ((nt->updated & SC_UPDATE_SELECTION) && !sci_has_selection(sci)) + lsp_selection_clear_selections(); } - if (srv->config.highlighting_enable && !ignore_selection_change && + if (srv->config.highlighting_enable && perform_highlight && (nt->updated & SC_UPDATE_SELECTION)) { - lsp_highlight_send_request(srv, doc); + lsp_highlight_schedule_request(doc); } - ignore_selection_change = FALSE; + perform_highlight = TRUE; } else if (nt->nmhdr.code == SCN_CHARADDED) { // don't hightlight while typing lsp_highlight_clear(doc); - + lsp_hover_hide_calltip(doc); + lsp_diagnostics_hide_calltip(doc); } return FALSE; @@ -1062,6 +1106,43 @@ static gboolean update_command_menu_items(GPtrArray *code_action_commands, gpoin } +// stolen from Geany +static void show_menu_at_caret(GtkMenu* menu, ScintillaObject *sci) +{ + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(sci)); + gint pos = sci_get_current_position(sci); + gint line = sci_get_line_from_position(sci, pos); + gint line_height = SSM(sci, SCI_TEXTHEIGHT, line, 0); + gint x = SSM(sci, SCI_POINTXFROMPOSITION, 0, pos); + gint y = SSM(sci, SCI_POINTYFROMPOSITION, 0, pos); + gint pos_next = SSM(sci, SCI_POSITIONAFTER, pos, 0); + gint char_width = 0; + /* if next pos is on the same Y (same line and not after wrapping), diff the X */ + if (pos_next > pos && SSM(sci, SCI_POINTYFROMPOSITION, 0, pos_next) == y) + char_width = SSM(sci, SCI_POINTXFROMPOSITION, 0, pos_next) - x; + GdkRectangle rect = {x, y, char_width, line_height}; + gtk_menu_popup_at_rect(GTK_MENU(menu), window, &rect, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +} + + +static gboolean show_code_action_popup(GPtrArray *code_action_commands, gpointer user_data) +{ + GPtrArray *code_lens_commands = lsp_code_lens_get_commands(); + + if (code_action_commands->len > 0 || code_lens_commands->len > 0) + { + GeanyDocument *doc = user_data; + GtkWidget *menu; + + update_command_menu_items(code_action_commands, user_data); + menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(context_menu_items.command_item)); + show_menu_at_caret(GTK_MENU(menu), doc->editor->sci); + gtk_menu_shell_select_first(GTK_MENU_SHELL(menu), FALSE); + } + return FALSE; +} + + static gboolean on_update_editor_menu(G_GNUC_UNUSED GObject *obj, const gchar *word, gint pos, GeanyDocument *doc, gpointer user_data) { @@ -1317,6 +1398,13 @@ static void invoke_kb(guint key_id, gint pos) lsp_goto_implementations(pos); break; + case KB_EXPAND_SELECTION: + lsp_selection_range_expand(); + break; + case KB_SHRINK_SELECTION: + lsp_selection_range_shrink(); + break; + case KB_SHOW_HOVER_POPUP: show_hover_popup(); break; @@ -1341,6 +1429,10 @@ static void invoke_kb(guint key_id, gint pos) restart_all_servers(); break; + case KB_SHOW_CODE_ACTIONS: + lsp_command_send_code_action_request(doc, pos, show_code_action_popup, doc); + break; + default: break; } @@ -1505,6 +1597,29 @@ static void create_menu_items() keybindings_set_item(group, KB_SHOW_HOVER_POPUP, NULL, 0, 0, "show_hover_popup", _("Show hover popup"), menu_items.hover_popup); + menu_items.code_action_popup = gtk_menu_item_new_with_mnemonic(_("Show Code Action Popup")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.code_action_popup); + g_signal_connect(menu_items.code_action_popup, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_SHOW_CODE_ACTIONS)); + keybindings_set_item(group, KB_SHOW_CODE_ACTIONS, NULL, 0, 0, "show_code_action_popup", + _("Show code action popup"), menu_items.code_action_popup); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.expand_selection = gtk_menu_item_new_with_mnemonic(_("Expand Selection")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.expand_selection); + g_signal_connect(menu_items.expand_selection, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_EXPAND_SELECTION)); + keybindings_set_item(group, KB_EXPAND_SELECTION, NULL, 0, 0, "expand_selection", + _("Expand Selection"), menu_items.expand_selection); + + menu_items.shrink_selection = gtk_menu_item_new_with_mnemonic(_("Shrink Selection")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.shrink_selection); + g_signal_connect(menu_items.shrink_selection, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_SHRINK_SELECTION)); + keybindings_set_item(group, KB_SHRINK_SELECTION, NULL, 0, 0, "shrink_selection", + _("Shrink Selection"), menu_items.shrink_selection); + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); menu_items.header_source = gtk_menu_item_new_with_mnemonic(_("Swap Header/Source")); diff --git a/lsp/src/lsp-rpc.c b/lsp/src/lsp-rpc.c index c26d74bfe..82429696f 100644 --- a/lsp/src/lsp-rpc.c +++ b/lsp/src/lsp-rpc.c @@ -349,14 +349,29 @@ static gboolean handle_call(JsonrpcClient *client, gchar* method, GVariant *id, msg = apply_edit(srv, params); handled = TRUE; } - // we officially don't support this (not advertised in initialize's - // workspace/configuration) but some servers ask for it anyway so do our - // best in this case else if (g_strcmp0(method, "workspace/configuration") == 0) { + // we officially don't support this (not advertised in initialize's + // workspace/configuration) but some servers ask for it anyway so do our + // best in this case msg = workspace_configuration(srv, params); handled = TRUE; } + else if (g_strcmp0(method, "client/registerCapability") == 0) + { + // not supported at all, HTML/CSS servers sending the request despite + // no indication of support from the client. Just to suppress warnings + // from the servers + msg = NULL; + handled = TRUE; + } + else if (g_strcmp0(method, "workspace/semanticTokens/refresh") == 0) + { + // not supported at all - Kate seems to do the same as some servers + // require a successful reply + msg = NULL; + handled = TRUE; + } else if (g_strcmp0(method, "window/showDocument") == 0) { msg = show_document(srv, params); diff --git a/lsp/src/lsp-selection-range.c b/lsp/src/lsp-selection-range.c new file mode 100644 index 000000000..0314d253e --- /dev/null +++ b/lsp/src/lsp-selection-range.c @@ -0,0 +1,251 @@ +/* + * Copyright 2024 Jiri Techet + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-selection-range.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + +#include + + +extern GeanyData *geany_data; + +typedef struct { + GeanyDocument *doc; + gboolean expand; +} SelectionRangeData; + + +GPtrArray *selections = NULL; + + +static gboolean is_within_range(ScintillaObject *sci, LspRange parent, LspRange child) +{ + gint parent_start_pos = lsp_utils_lsp_pos_to_scintilla(sci, parent.start); + gint parent_end_pos = lsp_utils_lsp_pos_to_scintilla(sci, parent.end); + gint child_start_pos = lsp_utils_lsp_pos_to_scintilla(sci, child.start); + gint child_end_pos = lsp_utils_lsp_pos_to_scintilla(sci, child.end); + + return (parent_start_pos < child_start_pos && parent_end_pos >= child_end_pos) || + (parent_start_pos <= child_start_pos && parent_end_pos > child_end_pos); +} + + +static LspRange get_current_selection(ScintillaObject *sci) +{ + LspRange selection; + selection.start = lsp_utils_scintilla_pos_to_lsp(sci, sci_get_selection_start(sci)); + selection.end = lsp_utils_scintilla_pos_to_lsp(sci, sci_get_selection_end(sci)); + return selection; +} + + +static gboolean is_max_selection(ScintillaObject *sci) +{ + LspRange selection = get_current_selection(sci); + LspRange *max_selection; + + if (!selections || selections->len == 0) + return FALSE; + + max_selection = selections->pdata[selections->len-1]; + + return max_selection->start.character == selection.start.character && + max_selection->start.line == selection.start.line && + max_selection->end.character == selection.end.character && + max_selection->end.line == selection.end.line; +} + + +static void parse_selection(GVariant *val, ScintillaObject *sci, LspRange selection) +{ + GVariant *range_variant = NULL; + GVariant *parent = NULL; + + JSONRPC_MESSAGE_PARSE(val, + "range", JSONRPC_MESSAGE_GET_VARIANT(&range_variant)); + + if (range_variant) + { + LspRange parsed_range = lsp_utils_parse_range(range_variant); + + if (is_within_range(sci, parsed_range, selection)) + { + LspRange *range = g_new0(LspRange, 1); + *range = parsed_range; + g_ptr_array_add(selections, range); + } + + g_variant_unref(range_variant); + } + + JSONRPC_MESSAGE_PARSE(val, + "parent", JSONRPC_MESSAGE_GET_VARIANT(&parent)); + + if (parent) + { + parse_selection(parent, sci, selection); + g_variant_unref(parent); + } +} + + +static LspRange *find_selection_range(ScintillaObject *sci, gboolean expand) +{ + LspRange selection_range = get_current_selection(sci);; + LspRange *found_range = NULL; + LspRange *range; + gint i; + + // sorted from the smallest to the biggest + foreach_ptr_array(range, i, selections) + { + if (expand && is_within_range(sci, *range, selection_range)) + { + found_range = range; + break; + } + else if (!expand && is_within_range(sci, selection_range, *range)) + found_range = range; + } + + return found_range; +} + + +static void find_and_select(ScintillaObject *sci, gboolean expand) +{ + LspRange *found_range = find_selection_range(sci, expand); + + if (found_range) + { + gint start = lsp_utils_lsp_pos_to_scintilla(sci, found_range->start); + gint end = lsp_utils_lsp_pos_to_scintilla(sci, found_range->end); + SSM(sci, SCI_SETSELECTION, start, end); + } +} + + +static void goto_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + SelectionRangeData *data = user_data; + + if (!error) + { + GeanyDocument *doc = data->doc; + + if (DOC_VALID(doc) && g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + GVariant *val = NULL; + GVariantIter iter; + + g_variant_iter_init(&iter, return_value); + + while (g_variant_iter_loop(&iter, "v", &val)) + { + LspRange selection = get_current_selection(doc->editor->sci); + LspRange *existing_range = g_new0(LspRange, 1); + + *existing_range = selection; + g_ptr_array_add(selections, existing_range); + + parse_selection(val, doc->editor->sci, selection); + break; // for single query just a single result + } + + find_and_select(doc->editor->sci, data->expand); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + } + + g_free(user_data); +} + + +void lsp_selection_clear_selections(void) +{ + if (selections) + g_ptr_array_free(selections, TRUE); + selections = NULL; +} + + +static void selection_range_request(gboolean expand) +{ + GeanyDocument *doc = document_get_current(); + LspServer *server = lsp_server_get(doc); + SelectionRangeData *data; + LspPosition lsp_pos; + gchar *doc_uri; + GVariant *node; + gint pos; + + if (!server || !server->config.selection_range_enable) + return; + + if (expand && is_max_selection(doc->editor->sci)) + return; + else if (sci_has_selection(doc->editor->sci) && selections && + find_selection_range(doc->editor->sci, expand)) + { + find_and_select(doc->editor->sci, expand); + return; + } + + pos = sci_get_current_position(doc->editor->sci); + lsp_pos = lsp_utils_scintilla_pos_to_lsp(doc->editor->sci, pos); + doc_uri = lsp_utils_get_doc_uri(doc); + + lsp_selection_clear_selections(); + selections = g_ptr_array_new_full(1, g_free); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "positions", "[", "{", // TODO: support multiple ranges for multiple selections + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}", "]" + ); + + data = g_new0(SelectionRangeData, 1); + data->doc = doc; + data->expand = expand; + lsp_rpc_call(server, "textDocument/selectionRange", node, goto_cb, data); + + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_selection_range_expand(void) +{ + selection_range_request(TRUE); +} + + +void lsp_selection_range_shrink(void) +{ + selection_range_request(FALSE); +} diff --git a/lsp/src/lsp-selection-range.h b/lsp/src/lsp-selection-range.h new file mode 100644 index 000000000..e4e5266a9 --- /dev/null +++ b/lsp/src/lsp-selection-range.h @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Jiri Techet + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SELECTION_RANGE_H +#define LSP_SELECTION_RANGE_H 1 + +#include "lsp-server.h" + +#include + + +void lsp_selection_range_expand(void); +void lsp_selection_range_shrink(void); + +void lsp_selection_clear_selections(void); + + +#endif /* LSP_SELECTION_RANGE_H */ diff --git a/lsp/src/lsp-server.c b/lsp/src/lsp-server.c index 4e02397a8..be050f9e8 100644 --- a/lsp/src/lsp-server.c +++ b/lsp/src/lsp-server.c @@ -159,8 +159,12 @@ static gboolean kill_cb(gpointer user_data) if (g_ptr_array_find(servers_in_shutdown, srv, NULL)) { - msgwin_status_add(_("Killing LSP server %s"), srv->config.cmd); + msgwin_status_add(_("Force terminating LSP server %s"), srv->config.cmd); +#ifdef G_OS_WIN32 g_subprocess_force_exit(srv->process); +#else + g_subprocess_send_signal(srv->process, SIGTERM); +#endif } return G_SOURCE_REMOVE; @@ -206,7 +210,7 @@ static void stop_process(LspServer *s) lsp_rpc_call_startup_shutdown(s, "shutdown", NULL, shutdown_cb, s); // should not be performed if server behaves correctly - plugin_timeout_add(geany_plugin, 5000, kill_cb, s); + plugin_timeout_add(geany_plugin, 4000, kill_cb, s); } @@ -243,38 +247,6 @@ static gchar *get_autocomplete_trigger_chars(GVariant *node) } -static gboolean supports_full_semantic_tokens(GVariant *node) -{ - gboolean val = FALSE; - - JSONRPC_MESSAGE_PARSE(node, - "capabilities", "{", - "semanticTokensProvider", "{", - "full", JSONRPC_MESSAGE_GET_BOOLEAN(&val), - "}", - "}"); - - return val; -} - - -static gboolean supports_delta_semantic_tokens(GVariant *node) -{ - gboolean val = FALSE; - - JSONRPC_MESSAGE_PARSE(node, - "capabilities", "{", - "semanticTokensProvider", "{", - "full", "{", - "delta", JSONRPC_MESSAGE_GET_BOOLEAN(&val), - "}", - "}", - "}"); - - return val; -} - - static guint64 get_semantic_token_mask(LspServer *srv, GVariant *node) { guint64 mask = 0; @@ -319,6 +291,7 @@ static guint64 get_semantic_token_mask(LspServer *srv, GVariant *node) static gchar *get_signature_trigger_chars(GVariant *node) { GVariantIter *iter = NULL; + GVariantIter *iter2 = NULL; GString *str = g_string_new(""); JSONRPC_MESSAGE_PARSE(node, @@ -328,6 +301,13 @@ static gchar *get_signature_trigger_chars(GVariant *node) "}", "}"); + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "signatureHelpProvider", "{", + "retriggerCharacters", JSONRPC_MESSAGE_GET_ITER(&iter2), + "}", + "}"); + if (iter) { GVariant *val = NULL; @@ -336,6 +316,18 @@ static gchar *get_signature_trigger_chars(GVariant *node) g_variant_iter_free(iter); } + if (iter2) + { + GVariant *val = NULL; + while (g_variant_iter_loop(iter2, "v", &val)) + { + const gchar *chr = g_variant_get_string(val, NULL); + if (!strstr(str->str, chr)) + g_string_append(str, chr); + } + g_variant_iter_free(iter2); + } + return g_string_free(str, FALSE); } @@ -365,52 +357,6 @@ static gboolean use_incremental_sync(GVariant *node) } -static gboolean send_did_save(GVariant *node) -{ - gboolean val; - gboolean success = JSONRPC_MESSAGE_PARSE(node, - "capabilities", "{", - "textDocumentSync", "{", - "save", JSONRPC_MESSAGE_GET_BOOLEAN(&val), - "}", - "}"); - - if (!success) - { - GVariant *var = NULL; - - JSONRPC_MESSAGE_PARSE(node, - "capabilities", "{", - "textDocumentSync", "{", - "save", JSONRPC_MESSAGE_GET_VARIANT(&var), - "}", - "}"); - - success = val = var != NULL; - if (var) - g_variant_unref(var); - } - - return success && val; -} - - -static gboolean include_text_on_save(GVariant *node) -{ - gboolean val; - gboolean success = JSONRPC_MESSAGE_PARSE(node, - "capabilities", "{", - "textDocumentSync", "{", - "save", "{", - "includeText", JSONRPC_MESSAGE_GET_BOOLEAN(&val), - "}", - "}", - "}"); - - return success && val; -} - - static gboolean use_workspace_folders(GVariant *node) { gboolean change_notifications = FALSE; @@ -455,33 +401,74 @@ static gboolean use_workspace_folders(GVariant *node) } -static void update_config(GVariant *variant, gboolean *option, const gchar *key) +static gboolean has_capability(GVariant *variant, const gchar *key1, const gchar *key2, const gchar *key3) { gboolean val = FALSE; - gboolean success = JSONRPC_MESSAGE_PARSE(variant, - "capabilities", "{", - key, JSONRPC_MESSAGE_GET_BOOLEAN(&val), - "}"); + GVariant *var = NULL; + gboolean success; - if (success) // explicit TRUE, FALSE - { - if (!val) - *option = FALSE; - } - else // dict (possibly just empty) which also indicates TRUE - { - GVariant *var = NULL; + if (key2 && key3) + success = JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key1, "{", + key2, "{", + key3, JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}", + "}", + "}"); + else if (key2) + success = JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key1, "{", + key2, JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}", + "}"); + else + success = JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key1, JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}"); + // explicit TRUE, FALSE + if (success) + return val; + + // dict (possibly just empty) which also indicates TRUE + if (key2 && key3) + JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key1, "{", + key2, "{", + key3, JSONRPC_MESSAGE_GET_VARIANT(&var), + "}", + "}", + "}"); + else if (key2) + JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key1, "{", + key2, JSONRPC_MESSAGE_GET_VARIANT(&var), + "}", + "}"); + else JSONRPC_MESSAGE_PARSE(variant, "capabilities", "{", - key, JSONRPC_MESSAGE_GET_VARIANT(&var), + key1, JSONRPC_MESSAGE_GET_VARIANT(&var), "}"); - if (var) - g_variant_unref(var); - else - *option = FALSE; + if (var) + { + g_variant_unref(var); + return TRUE; } + + return FALSE; +} + + +static void update_config(GVariant *variant, gboolean *option, const gchar *key) +{ + *option = *option && has_capability(variant, key, NULL, NULL); } @@ -516,6 +503,7 @@ static void initialize_cb(GVariant *return_value, GError *error, gpointer user_d if (EMPTY(s->signature_trigger_chars)) s->config.signature_enable = FALSE; + update_config(return_value, &s->config.autocomplete_enable, "completionProvider"); update_config(return_value, &s->config.hover_enable, "hoverProvider"); update_config(return_value, &s->config.hover_available, "hoverProvider"); update_config(return_value, &s->config.goto_enable, "definitionProvider"); @@ -533,20 +521,24 @@ static void initialize_cb(GVariant *return_value, GError *error, gpointer user_d update_config(return_value, &s->config.execute_command_enable, "executeCommandProvider"); update_config(return_value, &s->config.code_action_enable, "codeActionProvider"); update_config(return_value, &s->config.rename_enable, "renameProvider"); + update_config(return_value, &s->config.selection_range_enable, "selectionRangeProvider"); + + s->supports_completion_resolve = has_capability(return_value, "completionProvider", "resolveProvider", NULL); s->supports_workspace_symbols = TRUE; update_config(return_value, &s->supports_workspace_symbols, "workspaceSymbolProvider"); s->use_incremental_sync = use_incremental_sync(return_value); - s->send_did_save = send_did_save(return_value); - s->include_text_on_save = include_text_on_save(return_value); + s->send_did_save = has_capability(return_value, "textDocumentSync", "save", NULL); + s->include_text_on_save = has_capability(return_value, "textDocumentSync", "save", "includeText"); s->use_workspace_folders = use_workspace_folders(return_value); s->initialize_response = lsp_utils_json_pretty_print(return_value); - s->config.semantic_tokens_supports_delta = supports_delta_semantic_tokens(return_value); - if (!supports_full_semantic_tokens(return_value) && !s->config.semantic_tokens_supports_delta) - s->config.semantic_tokens_enable = FALSE; + s->config.semantic_tokens_enable = s->config.semantic_tokens_enable && + has_capability(return_value, "semanticTokensProvider", "full", NULL); + s->config.semantic_tokens_supports_delta = has_capability(return_value, + "semanticTokensProvider", "full", "delta"); s->semantic_token_mask = get_semantic_token_mask(s, return_value); msgwin_status_add(_("LSP server %s initialized"), s->config.cmd); @@ -572,12 +564,16 @@ static void initialize_cb(GVariant *return_value, GError *error, gpointer user_d } else { - msgwin_status_add(_("LSP initialize request failed for LSP server, killing %s"), s->config.cmd); + msgwin_status_add(_("LSP initialize request failed for LSP server, terminating %s"), s->config.cmd); // force exit the server - since the handshake didn't perform, the // server may be in some strange state and normal "exit" may not work // (happens with haskell server) +#ifdef G_OS_WIN32 g_subprocess_force_exit(s->process); +#else + g_subprocess_send_signal(s->process, SIGTERM); +#endif } } @@ -616,7 +612,7 @@ static void perform_initialize(LspServer *server) LSP_COMPLETION_KINDS, "]", "}", - "contxtSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "contextSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), "}", "hover", "{", "contentFormat", "[", @@ -631,6 +627,31 @@ static void perform_initialize(LspServer *server) "}", "hierarchicalDocumentSymbolSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), "}", + "publishDiagnostics", "{", // zls requires this to publish diagnostics + "}", + "codeAction", "{", + "resolveSupport", "{", + "properties", "[", + "edit", "command", + "]", + "}", + "dataSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "codeActionLiteralSupport", "{", + "codeActionKind", "{", + "valueSet", "[", + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + "source.fixAll", + "]", + "}", + "}", + "}", "semanticTokens", "{", "requests", "{", "full", "{", @@ -883,6 +904,8 @@ static void load_config(GKeyFile *kf, const gchar *section, LspServer *s) get_bool(&s->config.autocomplete_apply_additional_edits, kf, section, "autocomplete_apply_additional_edits"); get_bool(&s->config.diagnostics_enable, kf, section, "diagnostics_enable"); get_bool(&s->config.autocomplete_use_snippets, kf, section, "autocomplete_use_snippets"); + get_bool(&s->config.autocomplete_in_strings, kf, section, "autocomplete_in_strings"); + get_bool(&s->config.autocomplete_show_documentation, kf, section, "autocomplete_show_documentation"); get_int(&s->config.diagnostics_statusbar_severity, kf, section, "diagnostics_statusbar_severity"); get_str(&s->config.diagnostics_disable_for, kf, section, "diagnostics_disable_for"); @@ -919,6 +942,7 @@ static void load_config(GKeyFile *kf, const gchar *section, LspServer *s) get_str(&s->config.command_on_save_regex, kf, section, "command_on_save_regex"); get_bool(&s->config.progress_bar_enable, kf, section, "progress_bar_enable"); + get_bool(&s->config.swap_header_source_enable, kf, section, "swap_header_source_enable"); // create for the first time, then just update if (!s->config.command_regexes) @@ -947,6 +971,7 @@ static void load_config(GKeyFile *kf, const gchar *section, LspServer *s) s->config.execute_command_enable = TRUE; s->config.code_action_enable = TRUE; s->config.rename_enable = TRUE; + s->config.selection_range_enable = TRUE; s->config.hover_available = TRUE; s->config.document_symbols_available = TRUE; @@ -966,9 +991,20 @@ static void load_all_section_only_config(GKeyFile *kf, const gchar *section, Lsp static void load_filetype_only_config(GKeyFile *kf, const gchar *section, LspServer *s) { - get_str(&s->config.cmd, kf, section, "cmd"); + gchar *cmd = NULL; + gchar *use = NULL; + + get_str(&cmd, kf, section, "cmd"); + get_str(&use, kf, section, "use"); + if (!EMPTY(cmd) || !EMPTY(use)) + { + // make sure 'use' from global config file gets overridden by 'cmd' from user config file + // and that not both of them are set + SETPTR(s->config.cmd, cmd); + SETPTR(s->config.ref_lang, use); + } + get_strv(&s->config.env, kf, section, "env"); - get_str(&s->config.ref_lang, kf, section, "use"); get_str(&s->config.rpc_log, kf, section, "rpc_log"); get_str(&s->config.initialization_options_file, kf, section, "initialization_options_file"); get_str(&s->config.initialization_options, kf, section, "initialization_options"); diff --git a/lsp/src/lsp-server.h b/lsp/src/lsp-server.h index e6b15e897..cc4978403 100644 --- a/lsp/src/lsp-server.h +++ b/lsp/src/lsp-server.h @@ -57,6 +57,8 @@ typedef struct LspServerConfig gint autocomplete_window_max_width; gboolean autocomplete_use_snippets; gchar *autocomplete_hide_after_words; + gboolean autocomplete_in_strings; + gboolean autocomplete_show_documentation; gboolean diagnostics_enable; gint diagnostics_statusbar_severity; @@ -109,6 +111,8 @@ typedef struct LspServerConfig gboolean execute_command_enable; gboolean code_action_enable; + gboolean selection_range_enable; + gboolean swap_header_source_enable; gchar *command_on_save_regex; gint command_keybinding_num; GPtrArray *command_regexes; @@ -150,6 +154,7 @@ typedef struct LspServer gboolean include_text_on_save; gboolean use_workspace_folders; gboolean supports_workspace_symbols; + gboolean supports_completion_resolve; guint64 semantic_token_mask; } LspServer; diff --git a/lsp/src/lsp-signature.c b/lsp/src/lsp-signature.c index f8e012a79..b830c490c 100644 --- a/lsp/src/lsp-signature.c +++ b/lsp/src/lsp-signature.c @@ -75,44 +75,52 @@ static void signature_cb(GVariant *return_value, GError *error, gpointer user_da //printf("%s\n", lsp_utils_json_pretty_print(return_value)); - if (current_doc == data->doc && - sci_get_current_position(current_doc->editor->sci) < data->pos + 10 && - (data->force || (!data->force && !SSM(current_doc->editor->sci, SCI_AUTOCACTIVE, 0, 0)))) + if (current_doc == data->doc) { - GVariantIter *iter = NULL; - gint64 active = 1; - - JSONRPC_MESSAGE_PARSE(return_value, "signatures", JSONRPC_MESSAGE_GET_ITER(&iter)); - JSONRPC_MESSAGE_PARSE(return_value, "activeSignature", JSONRPC_MESSAGE_GET_INT64(&active)); + if (!g_variant_is_of_type(return_value, G_VARIANT_TYPE_DICTIONARY) && + lsp_signature_showing_calltip(current_doc)) + { + // null response + lsp_signature_hide_calltip(current_doc); + } + else if (sci_get_current_position(current_doc->editor->sci) < data->pos + 10 && + (data->force || (!data->force && !SSM(current_doc->editor->sci, SCI_AUTOCACTIVE, 0, 0)))) + { + GVariantIter *iter = NULL; + gint64 active = 1; - if (signatures) - g_ptr_array_free(signatures, TRUE); - signatures = g_ptr_array_new_full(1, g_free); + JSONRPC_MESSAGE_PARSE(return_value, "signatures", JSONRPC_MESSAGE_GET_ITER(&iter)); + JSONRPC_MESSAGE_PARSE(return_value, "activeSignature", JSONRPC_MESSAGE_GET_INT64(&active)); - if (iter) - { - GVariant *member = NULL; + if (signatures) + g_ptr_array_free(signatures, TRUE); + signatures = g_ptr_array_new_full(1, g_free); - while (g_variant_iter_loop(iter, "v", &member)) + if (iter) { - const gchar *label = NULL; + GVariant *member = NULL; + + while (g_variant_iter_loop(iter, "v", &member)) + { + const gchar *label = NULL; - JSONRPC_MESSAGE_PARSE(member, "label", JSONRPC_MESSAGE_GET_STRING(&label)); + JSONRPC_MESSAGE_PARSE(member, "label", JSONRPC_MESSAGE_GET_STRING(&label)); - if (label) - g_ptr_array_add(signatures, g_strdup(label)); + if (label) + g_ptr_array_add(signatures, g_strdup(label)); + } } - } - displayed_signature = CLAMP(active, 1, signatures->len) - 1; + displayed_signature = CLAMP(active, 1, signatures->len) - 1; - if (signatures->len == 0) - SSM(current_doc->editor->sci, SCI_CALLTIPCANCEL, 0, 0); - else - show_signature(current_doc->editor->sci); + if (signatures->len == 0) + SSM(current_doc->editor->sci, SCI_CALLTIPCANCEL, 0, 0); + else + show_signature(current_doc->editor->sci); - if (iter) - g_variant_iter_free(iter); + if (iter) + g_variant_iter_free(iter); + } } }