diff --git a/lsp/data/lsp.conf b/lsp/data/lsp.conf index f125618d9..f6c27b010 100644 --- a/lsp/data/lsp.conf +++ b/lsp/data/lsp.conf @@ -46,11 +46,26 @@ enable_by_default=false # Defines whether the server should be used when no project is open. Servers # may not work correctly without a project because most of them need to know # the path to the project directory which corresponds to the path defined under -# Project->Properties->Base path +# Project->Properties->Base path. This option can be partially overridden +# by project_root_marker_patterns, see below use_without_project=false # Defines whether the server should be used for files whose path is not within -# the project directory +# the project directory. This option can be partially overridden by +# project_root_marker_patterns, see below use_outside_project_dir=false +# A semicolon-separated list of glob patterns of files that are typically stored +# inside the root directory of the project. Language servers supporting +# changeNotifications of workspaceFolders (these two values should appear inside +# the server initialize response) can use these marker files to detect root +# project directories of open files. Starting from the open file directory, +# the plugin goes up in the directory structure and tests whether a file +# matching project_root_marker_patterns exists - if it does, such a directory +# is considered to be the root directory of the project. This allows to +# detect projects without any Geany project open and allows the plugin to work +# on multiple projects simultaneously. Typically, the pattern contains +# files/directories such as .git, configure.ac, go.mod, etc. If a pattern is +# found, enable_by_default and use_without_project are ignored +project_root_marker_patterns= # The number of keybindings that can be assigned to LSP code action commands. # This option is valid only within the [all] section and changing the value @@ -186,6 +201,10 @@ format_on_save=false # This is a dummy language server configuration describing the available # language-specific options +# For an extensive list of various servers and their configurations, check +# https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md +# While the configuration options names of neovim differ from Geany, the +# general concepts are similar and applicable here [DummyLanguage] # The command (including parameters and possibly also the full path) used to # start the LSP server. Instead of starting a new server, it is also possible to diff --git a/lsp/src/Makefile.am b/lsp/src/Makefile.am index d19372529..97a8cd5c6 100644 --- a/lsp/src/Makefile.am +++ b/lsp/src/Makefile.am @@ -50,7 +50,9 @@ lsp_la_SOURCES = \ lsp-sync.c \ lsp-sync.h \ lsp-utils.c \ - lsp-utils.h + lsp-utils.h \ + lsp-workspace-folders.c \ + lsp-workspace-folders.h lsp_la_CPPFLAGS = $(AM_CPPFLAGS) \ -DG_LOG_DOMAIN=\"LSP\" \ diff --git a/lsp/src/lsp-main.c b/lsp/src/lsp-main.c index 726ebe1d6..30b0f6378 100644 --- a/lsp/src/lsp-main.c +++ b/lsp/src/lsp-main.c @@ -38,6 +38,7 @@ #include "lsp-code-lens.h" #include "lsp-symbol.h" #include "lsp-extension.h" +#include "lsp-workspace-folders.h" #include #include @@ -459,6 +460,8 @@ static void on_document_visible(GeanyDocument *doc) if (srv && !lsp_sync_is_document_open(doc)) lsp_sync_text_document_did_open(srv, doc); + lsp_workspace_folders_doc_open(doc); + on_update_idle(doc); #ifndef HAVE_GEANY_PLUGIN_EXTENSION @@ -504,6 +507,7 @@ static void on_document_close(G_GNUC_UNUSED GObject * obj, GeanyDocument *doc, lsp_diagnostics_clear(doc); lsp_semtokens_clear(doc); lsp_sync_text_document_did_close(srv, doc); + lsp_workspace_folders_doc_closed(doc); } @@ -512,6 +516,7 @@ static void destroy_all(void) lsp_diagnostics_destroy(); lsp_semtokens_destroy(); lsp_symbols_destroy(); + lsp_workspace_folders_destroy(); } @@ -524,6 +529,7 @@ static void stop_and_init_all_servers(void) lsp_sync_init(); lsp_diagnostics_init(); + lsp_workspace_folders_init(); } diff --git a/lsp/src/lsp-rpc.c b/lsp/src/lsp-rpc.c index 6eac88022..cc1e3f794 100644 --- a/lsp/src/lsp-rpc.c +++ b/lsp/src/lsp-rpc.c @@ -26,6 +26,7 @@ #include "lsp-progress.h" #include "lsp-log.h" #include "lsp-utils.h" +#include "lsp-workspace-folders.h" #include #include @@ -47,6 +48,8 @@ struct LspRpc }; +extern GeanyData *geany_data; + GHashTable *client_table; @@ -189,6 +192,53 @@ static gboolean handle_call(JsonrpcClient *client, gchar* method, GVariant *id, g_variant_unref(edit); return TRUE; } + else if (g_strcmp0(method, "workspace/workspaceFolders") == 0) + { + GPtrArray *folders = lsp_workspace_folders_get(); + guint num = 0; + + foreach_document(num) + { + if (num > 1) + break; + } + + if (num > 1) // non-single-open document variant + { + GPtrArray *arr = g_ptr_array_new_full(1, (GDestroyNotify) g_variant_unref); + GVariant *folders_variant; + gchar *folder; + guint i; + + foreach_ptr_array(folder, i, folders) + { + gchar *uri = g_filename_to_uri(folder, NULL, NULL); + GVariant *folder_variant; + + folder_variant = JSONRPC_MESSAGE_NEW( + "uri", JSONRPC_MESSAGE_PUT_STRING(uri), + "name", JSONRPC_MESSAGE_PUT_STRING(folder) + ); + g_ptr_array_add(arr, folder_variant); + + g_free(uri); + } + + folders_variant = g_variant_take_ref(g_variant_new_array(G_VARIANT_TYPE_VARDICT, + (GVariant **)arr->pdata, arr->len)); + + reply_async(srv, method, client, id, folders_variant); + + g_variant_unref(folders_variant); + g_ptr_array_free(arr, TRUE); + } + else + reply_async(srv, method, client, id, NULL); + + g_ptr_array_free(folders, TRUE); + + return TRUE; + } else { GVariant *variant; diff --git a/lsp/src/lsp-server.c b/lsp/src/lsp-server.c index a3fafdc43..210fe54f9 100644 --- a/lsp/src/lsp-server.c +++ b/lsp/src/lsp-server.c @@ -71,6 +71,7 @@ static void free_config(LspServerConfig *cfg) g_free(cfg->initialization_options); g_strfreev(cfg->lang_id_mappings); g_strfreev(cfg->command_regexes); + g_strfreev(cfg->project_root_marker_patterns); } @@ -359,6 +360,50 @@ static gboolean use_incremental_sync(GVariant *node) } +static gboolean use_workspace_folders(GVariant *node) +{ + gboolean change_notifications = FALSE; + const gchar *notif_id = NULL; + gboolean supported = FALSE; + gboolean success; + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "workspace", "{", + "workspaceFolders", "{", + "supported", JSONRPC_MESSAGE_GET_BOOLEAN(&supported), + "}", + "}", + "}"); + + if (!supported) + return FALSE; + + success = JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "workspace", "{", + "workspaceFolders", "{", + "changeNotifications", JSONRPC_MESSAGE_GET_BOOLEAN(&change_notifications), + "}", + "}", + "}"); + + if (!success) // can also be string + { + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "workspace", "{", + "workspaceFolders", "{", + "changeNotifications", JSONRPC_MESSAGE_GET_STRING(¬if_id), + "}", + "}", + "}"); + } + + return change_notifications || notif_id; +} + + static void update_config(GVariant *variant, gboolean *option, const gchar *key) { gboolean val = FALSE; @@ -427,6 +472,7 @@ static void initialize_cb(GVariant *return_value, GError *error, gpointer user_d update_config(return_value, &s->supports_workspace_symbols, "workspaceSymbolProvider"); s->use_incremental_sync = use_incremental_sync(return_value); + s->use_workspace_folders = use_workspace_folders(return_value); s->initialize_response = lsp_utils_json_pretty_print(return_value); @@ -752,6 +798,7 @@ static void load_config(GKeyFile *kf, const gchar *section, LspServer *s) get_bool(&s->config.autocomplete_enable, kf, section, "autocomplete_enable"); get_strv(&s->config.autocomplete_trigger_sequences, kf, section, "autocomplete_trigger_sequences"); + get_strv(&s->config.project_root_marker_patterns, kf, section, "project_root_marker_patterns"); get_int(&s->config.autocomplete_window_max_entries, kf, section, "autocomplete_window_max_entries"); get_int(&s->config.autocomplete_window_max_displayed, kf, section, "autocomplete_window_max_displayed"); @@ -917,10 +964,21 @@ static gboolean is_lsp_valid_for_doc(LspServerConfig *cfg, GeanyDocument *doc) gchar *base_path, *real_path, *rel_path; gboolean inside_project; - if (!cfg->use_without_project && !geany_data->app->project) + if (!doc || !doc->real_path) return FALSE; - if (!doc || !doc->real_path) + if (cfg->project_root_marker_patterns) + { + gchar *project_root = lsp_utils_find_project_root(doc, cfg); + if (project_root) + { + g_free(project_root); + return TRUE; + } + g_free(project_root); + } + + if (!cfg->use_without_project && !geany_data->app->project) return FALSE; if (cfg->use_outside_project_dir || !geany_data->app->project) @@ -1002,19 +1060,14 @@ GeanyFiletype *lsp_server_get_ft(GeanyDocument *doc, gchar **lsp_lang_id) static LspServer *server_get_for_doc(GeanyDocument *doc, gboolean launch_server) { + LspServerConfig *cfg = lsp_server_get_config(doc); GeanyFiletype *ft; - LspServer *srv; - if (!doc) + if (!doc || !cfg) return NULL; ft = lsp_server_get_ft(doc, NULL); - srv = server_get_or_start_for_ft(ft, launch_server); - - if (!srv || !is_lsp_valid_for_doc(&srv->config, doc)) - return NULL; - - return srv; + return server_get_or_start_for_ft(ft, launch_server); } diff --git a/lsp/src/lsp-server.h b/lsp/src/lsp-server.h index 46f4ff059..008cfba91 100644 --- a/lsp/src/lsp-server.h +++ b/lsp/src/lsp-server.h @@ -29,7 +29,7 @@ struct LspRpc; typedef struct LspRpc LspRpc; -typedef struct +typedef struct LspServerConfig { gchar *cmd; gchar **env; @@ -41,6 +41,7 @@ typedef struct gboolean rpc_log_full; gchar *initialization_options_file; gchar *initialization_options; + gchar **project_root_marker_patterns; gboolean enable_by_default; gboolean use_outside_project_dir; gboolean use_without_project; @@ -137,6 +138,7 @@ typedef struct LspServer gchar *signature_trigger_chars; gchar *initialize_response; gboolean use_incremental_sync; + gboolean use_workspace_folders; gboolean supports_workspace_symbols; guint64 semantic_token_mask; diff --git a/lsp/src/lsp-utils.c b/lsp/src/lsp-utils.c index c86b856c4..6a0345939 100644 --- a/lsp/src/lsp-utils.c +++ b/lsp/src/lsp-utils.c @@ -183,14 +183,25 @@ gchar *lsp_utils_get_real_path_from_uri_utf8(const gchar *uri) } +static gchar *utf8_real_path_from_utf8(const gchar *utf8_path) +{ + gchar *ret = utils_get_locale_from_utf8(utf8_path); + SETPTR(ret, utils_get_real_path(ret)); + SETPTR(ret, utils_get_utf8_from_locale(ret)); + return ret; +} + + /* utf8 */ gchar *lsp_utils_get_project_base_path(void) { + gchar *ret = NULL; + GeanyProject *project = geany_data->app->project; if (project && !EMPTY(project->base_path)) { if (g_path_is_absolute(project->base_path)) - return g_strdup(project->base_path); + return utf8_real_path_from_utf8(project->base_path); else { /* build base_path out of project file name's dir and base_path */ gchar *path; @@ -200,11 +211,12 @@ gchar *lsp_utils_get_project_base_path(void) return dir; path = g_build_filename(dir, project->base_path, NULL); + SETPTR(path, utf8_real_path_from_utf8(path)); g_free(dir); return path; } } - return NULL; + return ret; } @@ -916,3 +928,63 @@ gboolean lsp_utils_doc_ft_has_tags(GeanyDocument *doc) return found; } + + +static gboolean content_matches_pattern(const gchar *dirname, gchar **patterns) +{ + gboolean success = FALSE; + const gchar *filename; + GDir *dir; + + dir = g_dir_open(dirname, 0, NULL); + if (!dir) + return FALSE; + + while ((filename = g_dir_read_name(dir)) && !success) + { + gchar **pattern; + + foreach_strv(pattern, patterns) + { + if (g_pattern_match_simple(*pattern, filename)) + { + success = TRUE; + break; + } + } + } + + g_dir_close(dir); + + return success; +} + + +gchar *lsp_utils_find_project_root(GeanyDocument *doc, LspServerConfig *cfg) +{ + gchar *dirname; + + if (!cfg || !cfg->project_root_marker_patterns || !doc->real_path) + return NULL; + + dirname = g_path_get_dirname(doc->real_path); + + while (dirname) + { + gchar *new_dirname; + + if (content_matches_pattern(dirname, cfg->project_root_marker_patterns)) + break; + + new_dirname = g_path_get_dirname(dirname); + if (strlen(new_dirname) >= strlen(dirname)) + { + g_free(new_dirname); + new_dirname = NULL; + } + g_free(dirname); + dirname = new_dirname; + } + + return dirname; +} diff --git a/lsp/src/lsp-utils.h b/lsp/src/lsp-utils.h index 2fc7bc278..9e631503f 100644 --- a/lsp/src/lsp-utils.h +++ b/lsp/src/lsp-utils.h @@ -23,6 +23,8 @@ #define SSM(s, m, w, l) scintilla_send_message(s, m, w, l) +struct LspServerConfig; +typedef struct LspServerConfig LspServerConfig; typedef enum { @@ -119,4 +121,6 @@ void lsp_utils_save_all_docs(void); gboolean lsp_utils_doc_ft_has_tags(GeanyDocument *doc); +gchar *lsp_utils_find_project_root(GeanyDocument *doc, LspServerConfig *cfg); + #endif /* LSP_UTILS_H */ diff --git a/lsp/src/lsp-workspace-folders.c b/lsp/src/lsp-workspace-folders.c new file mode 100644 index 000000000..5314860cd --- /dev/null +++ b/lsp/src/lsp-workspace-folders.c @@ -0,0 +1,179 @@ +/* + * 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 + +#include "lsp-workspace-folders.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + + +extern GeanyData *geany_data; + +static GHashTable *folder_table = NULL; + + +void lsp_workspace_folders_init(void) +{ + if (!folder_table) + folder_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_remove_all(folder_table); +} + + +void lsp_workspace_folders_destroy(void) +{ + if (folder_table) + g_hash_table_destroy(folder_table); + folder_table = NULL; +} + + +static void noitfy_root_change(LspServer *srv, const gchar *root, gboolean added) +{ + gchar *root_uri = g_filename_to_uri(root, NULL, NULL); + GVariant *node; + + if (added) + { + node = JSONRPC_MESSAGE_NEW ( + "event", "{", + "added", "[", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(root_uri), + "name", JSONRPC_MESSAGE_PUT_STRING(root), + "}", "]", + "}" + ); + } + else + { + node = JSONRPC_MESSAGE_NEW ( + "event", "{", + "removed", "[", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(root_uri), + "name", JSONRPC_MESSAGE_PUT_STRING(root), + "}", "]", + "}" + ); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_notify(srv, "workspace/didChangeWorkspaceFolders", node, NULL, NULL); + + g_free(root_uri); +} + + +void lsp_workspace_folders_doc_open(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + gchar *project_root; + gchar *project_base; + + if (!doc->real_path || !srv || !srv->use_workspace_folders) + return; + + project_base = lsp_utils_get_project_base_path(); + if (g_str_has_prefix(doc->real_path, project_base)) // already added during initialize + { + g_free(project_base); + return; + } + g_free(project_base); + + project_root = lsp_utils_find_project_root(doc, &srv->config); + if (!project_root) + return; + + if (!g_hash_table_contains(folder_table, project_root)) + { + g_hash_table_insert(folder_table, project_root, GINT_TO_POINTER(0)); + + noitfy_root_change(srv, project_root, TRUE); + } + else + g_free(project_root); +} + + +void lsp_workspace_folders_doc_closed(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + GList *roots = g_hash_table_get_keys(folder_table); + GList *root; + + if (!srv || !srv->use_workspace_folders) + return; + + foreach_list(root, roots) + { + gboolean root_used = FALSE; + guint i; + + foreach_document(i) + { + GeanyDocument *document = documents[i]; + + if (doc == document) + continue; + + if (g_str_has_prefix(document->real_path, root->data)) + { + root_used = TRUE; + break; + } + } + + if (!root_used) + { + noitfy_root_change(srv, root->data, FALSE); + + g_hash_table_remove(folder_table, root->data); + } + } + + g_list_free(roots); +} + + +GPtrArray *lsp_workspace_folders_get(void) +{ + GPtrArray *arr = g_ptr_array_new_full(1, g_free); + gchar *project_base; + GList *node, *lst; + + if (!folder_table) + lsp_workspace_folders_init(); + + project_base = lsp_utils_get_project_base_path(); + if (project_base) + g_ptr_array_add(arr, project_base); + g_free(project_base); + + lst = g_hash_table_get_keys(folder_table); + foreach_list(node, lst) + g_ptr_array_add(arr, g_strdup(node->data)); + g_list_free(lst); + + return arr; +} diff --git a/lsp/src/lsp-workspace-folders.h b/lsp/src/lsp-workspace-folders.h new file mode 100644 index 000000000..2090913c0 --- /dev/null +++ b/lsp/src/lsp-workspace-folders.h @@ -0,0 +1,32 @@ +/* + * 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_WORKSPACE_FOLDERS_H +#define LSP_WORKSPACE_FOLDERS_H 1 + +#include "lsp-server.h" + +void lsp_workspace_folders_init(void); +void lsp_workspace_folders_destroy(void); + +void lsp_workspace_folders_doc_open(GeanyDocument *doc); +void lsp_workspace_folders_doc_closed(GeanyDocument *doc); + +GPtrArray *lsp_workspace_folders_get(void); + +#endif /* LSP_WORKSPACE_FOLDERS */ \ No newline at end of file