diff --git a/po/POTFILES.in b/po/POTFILES.in index 8c566d8a7..31ec23b3e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -205,6 +205,7 @@ pretty-printer/src/PluginEntry.c pretty-printer/src/ConfigUI.c # ProjectOrganizer +projectorganizer/src/prjorg-goto-anywhere.c projectorganizer/src/prjorg-main.c projectorganizer/src/prjorg-menu.c projectorganizer/src/prjorg-project.c diff --git a/projectorganizer/README b/projectorganizer/README index 11329819f..4132f13b8 100644 --- a/projectorganizer/README +++ b/projectorganizer/README @@ -156,6 +156,12 @@ Project Organizer adds some extra entries under the Project menu: the properties, it opens a project file with the same base name (without extension) matching header patterns (and vice versa). If the files are already open, it just switches the document tabs. Nothing happens if no matching file is found. +* Go to anywhere, Go to document symbol, Go to workspace symbol, Go to line - + these items allow to perform jump to the entered destination. The popup window is + identical for all of these actions, the only difference is the pre-filled prefix + that determines the type of the go to. No prefix performs the search in open files, + @ performs the search in current document's symbols, # performs the search in + all workspace symbols, and : performs navigation to the specified line. Each of these entries can be assigned a key binding under Edit->Preferences->Keybindings. diff --git a/projectorganizer/src/Makefile.am b/projectorganizer/src/Makefile.am index cf0f2f9c0..91960801c 100644 --- a/projectorganizer/src/Makefile.am +++ b/projectorganizer/src/Makefile.am @@ -12,7 +12,11 @@ projectorganizer_la_SOURCES = \ prjorg-utils.h \ prjorg-utils.c \ prjorg-menu.h \ - prjorg-menu.c + prjorg-menu.c \ + prjorg-goto-panel.h \ + prjorg-goto-panel.c \ + prjorg-goto-anywhere.h \ + prjorg-goto-anywhere.c projectorganizer_la_CPPFLAGS = $(AM_CPPFLAGS) \ -DG_LOG_DOMAIN=\"ProjectOrganizer\" diff --git a/projectorganizer/src/prjorg-goto-anywhere.c b/projectorganizer/src/prjorg-goto-anywhere.c new file mode 100644 index 000000000..30244938f --- /dev/null +++ b/projectorganizer/src/prjorg-goto-anywhere.c @@ -0,0 +1,375 @@ +/* + * Copyright 2023 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 "prjorg-goto-anywhere.h" +#include "prjorg-goto-panel.h" +#include "prjorg-project.h" + +#include +#include + + +#define SSM(s, m, w, l) scintilla_send_message((s), (m), (w), (l)) + + +typedef struct +{ + GeanyDocument *doc; + gchar *query; +} DocQueryData; + + +extern GeanyData *geany_data; + + +static void goto_line(GeanyDocument *doc, const gchar *line_str) +{ + GPtrArray *arr = g_ptr_array_new_full(0, (GDestroyNotify)prjorg_goto_symbol_free); + gint lineno = atoi(line_str); + gint linenum = sci_get_line_count(doc->editor->sci); + guint i; + + for (i = 0; i < 4; i++) + { + PrjorgGotoSymbol *sym = g_new0(PrjorgGotoSymbol, 1); + + sym->file_name = utils_get_utf8_from_locale(doc->real_path); + sym->icon = TM_ICON_OTHER; + + switch (i) + { + case 0: + /* For translators: Item in a list which, when selected, navigates + * to the line typed in the entry above the list */ + sym->name = g_strdup(_("line typed above")); + if (lineno == 0) + sym->line = sci_get_current_line(doc->editor->sci) + 1; + else if (lineno > linenum) + sym->line = linenum; + else + sym->line = lineno; + break; + + case 1: + /* For translators: Item in a list which, when selected, navigates + * to the beginning of the current document */ + sym->name = g_strdup(_("beginning")); + sym->line = 1; + break; + + case 2: + /* For translators: Item in a list which, when selected, navigates + * to the middle of the current document */ + sym->name = g_strdup(_("middle")); + sym->line = linenum / 2; + break; + + case 3: + /* For translators: Item in a list which, when selected, navigates + * to the end of the current document */ + sym->name = g_strdup(_("end")); + sym->line = linenum; + break; + } + + g_ptr_array_add(arr, sym); + } + + prjorg_goto_panel_fill(arr); + + g_ptr_array_free(arr, TRUE); +} + + +static void goto_file(const gchar *file_str) +{ + GPtrArray *arr = g_ptr_array_new_full(0, (GDestroyNotify)prjorg_goto_symbol_free); + GHashTable *files_added = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + GPtrArray *filtered; + guint i; + + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + PrjorgGotoSymbol *sym; + + if (!doc->real_path) + continue; + + sym = g_new0(PrjorgGotoSymbol, 1); + sym->file_name = utils_get_utf8_from_locale(doc->real_path); + sym->name = g_path_get_basename(sym->file_name); + sym->icon = TM_ICON_OTHER; + g_ptr_array_add(arr, sym); + + g_hash_table_insert(files_added, g_strdup(sym->file_name), GINT_TO_POINTER(1)); + } + + if (prj_org && prj_org->roots) + { + PrjOrgRoot *root = prj_org->roots->data; + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, root->file_table); + while (g_hash_table_iter_next(&iter, &key, &value)) + { + if (!g_hash_table_lookup(files_added, key)) + { + PrjorgGotoSymbol *sym; + sym = g_new0(PrjorgGotoSymbol, 1); + sym->file_name = g_strdup(key); + sym->name = g_path_get_basename(key); + sym->icon = TM_ICON_NONE; + g_ptr_array_add(arr, sym); + } + } + } + + filtered = prjorg_goto_panel_filter(arr, file_str); + prjorg_goto_panel_fill(filtered); + + g_ptr_array_free(filtered, TRUE); + g_ptr_array_free(arr, TRUE); + g_hash_table_destroy(files_added); +} + + +/* symplified hard-coded icons because we don't have access to Geany icon mappings */ +static int get_icon(TMTagType type) +{ + switch (type) + { + case tm_tag_class_t: + return TM_ICON_CLASS; + case tm_tag_macro_t: + case tm_tag_macro_with_arg_t: + case tm_tag_undef_t: + return TM_ICON_MACRO; + case tm_tag_enum_t: + case tm_tag_struct_t: + case tm_tag_typedef_t: + case tm_tag_union_t: + return TM_ICON_STRUCT; + case tm_tag_enumerator_t: + case tm_tag_field_t: + case tm_tag_member_t: + return TM_ICON_MEMBER; + case tm_tag_method_t: + case tm_tag_function_t: + case tm_tag_prototype_t: + return TM_ICON_METHOD; + case tm_tag_interface_t: + case tm_tag_namespace_t: + case tm_tag_package_t: + return TM_ICON_NAMESPACE; + case tm_tag_variable_t: + case tm_tag_externvar_t: + case tm_tag_local_var_t: + case tm_tag_include_t: + return TM_ICON_VAR; + case tm_tag_other_t: + return TM_ICON_OTHER; + default: + return TM_ICON_NONE; + } +} + + +/* stolen from Geany */ +static gboolean langs_compatible(TMParserType lang, TMParserType other) +{ + if (lang == other) + return TRUE; + /* Accept CPP tags for C lang and vice versa - we don't have acces to + * Geany's TM_PARSER_ constants so let's hard-code 0 and 1 here (not too + * terrible things will happen if Geany changes these to something else) */ + else if (lang == 0 && other == 1) + return TRUE; + else if (lang == 1 && other == 0) + return TRUE; + + return FALSE; +} + + +static void goto_tm_symbol(const gchar *query, GPtrArray *tags, TMParserType lang) +{ + GPtrArray *converted = g_ptr_array_new_full(0, (GDestroyNotify)prjorg_goto_symbol_free); + GPtrArray *filtered; + TMTag *tag; + guint i; + + if (tags) + { + foreach_ptr_array(tag, i, tags) + { + if (tag->file && langs_compatible(tag->lang, lang) && + !(tag->type & (tm_tag_include_t | tm_tag_local_var_t))) + { + PrjorgGotoSymbol *sym = g_new0(PrjorgGotoSymbol, 1); + sym->name = g_strdup(tag->name); + sym->file_name = utils_get_utf8_from_locale(tag->file->file_name); + sym->line = tag->line; + sym->icon = get_icon(tag->type); + + g_ptr_array_add(converted, sym); + } + } + } + + filtered = prjorg_goto_panel_filter(converted, query); + prjorg_goto_panel_fill(filtered); + + g_ptr_array_free(filtered, TRUE); + g_ptr_array_free(converted, TRUE); +} + + +static void perform_lookup(const gchar *query) +{ + GeanyDocument *doc = document_get_current(); + const gchar *query_str = query ? query : ""; + + if (g_str_has_prefix(query_str, "#")) + { + if (doc) + { + // TODO: possibly improve performance by binary searching the start and the end point + goto_tm_symbol(query_str+1, geany_data->app->tm_workspace->tags_array, doc->file_type->lang); + } + } + else if (g_str_has_prefix(query_str, "@")) + { + if (doc) + { + GPtrArray *tags = doc->tm_file ? doc->tm_file->tags_array : g_ptr_array_new(); + goto_tm_symbol(query_str+1, tags, doc->file_type->lang); + if (!doc->tm_file) + g_ptr_array_free(tags, TRUE); + } + } + else if (g_str_has_prefix(query_str, ":")) + { + if (doc) + goto_line(doc, query_str+1); + } + else + goto_file(query_str); +} + + +static gchar *get_current_iden(GeanyDocument *doc, gint current_pos) +{ + //TODO: use configured wordchars (also change in Geany) + const gchar *wordchars = GEANY_WORDCHARS; + GeanyFiletypeID ft = doc->file_type->id; + ScintillaObject *sci = doc->editor->sci; + gint start_pos, end_pos, pos; + + if (ft == GEANY_FILETYPES_LATEX) + wordchars = GEANY_WORDCHARS"\\"; /* add \ to word chars if we are in a LaTeX file */ + else if (ft == GEANY_FILETYPES_CSS) + wordchars = GEANY_WORDCHARS"-"; /* add - because they are part of property names */ + + pos = current_pos; + while (TRUE) + { + gint new_pos = SSM(sci, SCI_POSITIONBEFORE, pos, 0); + if (new_pos == pos) + break; + if (pos - new_pos == 1) + { + gchar c = sci_get_char_at(sci, new_pos); + if (!strchr(wordchars, c)) + break; + } + pos = new_pos; + } + start_pos = pos; + + pos = current_pos; + while (TRUE) + { + gint new_pos = SSM(sci, SCI_POSITIONAFTER, pos, 0); + if (new_pos == pos) + break; + if (new_pos - pos == 1) + { + gchar c = sci_get_char_at(sci, pos); + if (!strchr(wordchars, c)) + break; + } + pos = new_pos; + } + end_pos = pos; + + if (start_pos == end_pos) + return NULL; + + return sci_get_contents_range(sci, start_pos, end_pos); +} + + +static void goto_panel_query(const gchar *query_type, gboolean prefill) +{ + GeanyDocument *doc = document_get_current(); + gchar *query = NULL; + gint pos; + + if (prefill && doc) + { + pos = sci_get_current_position(doc->editor->sci); + query = get_current_iden(doc, pos); + } + if (!query) + query = g_strdup(""); + SETPTR(query, g_strconcat(query_type, query, NULL)); + + prjorg_goto_panel_show(query, perform_lookup); + + g_free(query); +} + + +void prjorg_goto_anywhere_for_workspace(void) +{ + goto_panel_query("#", TRUE); +} + + +void prjorg_goto_anywhere_for_doc(void) +{ + goto_panel_query("@", TRUE); +} + + +void prjorg_goto_anywhere_for_line(void) +{ + goto_panel_query(":", FALSE); +} + + +void prjorg_goto_anywhere_for_file(void) +{ + goto_panel_query("", FALSE); +} diff --git a/projectorganizer/src/prjorg-goto-anywhere.h b/projectorganizer/src/prjorg-goto-anywhere.h new file mode 100644 index 000000000..c5551bcfe --- /dev/null +++ b/projectorganizer/src/prjorg-goto-anywhere.h @@ -0,0 +1,28 @@ +/* + * Copyright 2023 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 PRJORG_GOTO_ANYWHERE_H +#define PRJORG_GOTO_ANYWHERE_H 1 + + +void prjorg_goto_anywhere_for_workspace(void); +void prjorg_goto_anywhere_for_doc(void); +void prjorg_goto_anywhere_for_line(void); +void prjorg_goto_anywhere_for_file(void); + +#endif /* PRJORG_GOTO_ANYWHERE_H */ diff --git a/projectorganizer/src/prjorg-goto-panel.c b/projectorganizer/src/prjorg-goto-panel.c new file mode 100644 index 000000000..2252a1fc8 --- /dev/null +++ b/projectorganizer/src/prjorg-goto-panel.c @@ -0,0 +1,446 @@ +/* + * Copyright 2023 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. + */ + +/* This file contains mostly stolen code from the Colomban Wendling's Commander + * plugin. Thanks! */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "prjorg-goto-panel.h" + +#include +#include + + +enum { + COL_ICON, + COL_LABEL, + COL_PATH, + COL_LINENO, + COL_COUNT +}; + + +//TODO: free on plugin unload +struct { + GtkWidget *panel; + GtkWidget *entry; + GtkWidget *tree_view; + GtkListStore *store; +} panel_data = { + NULL, NULL, NULL, NULL +}; + + +static PrjorgGotoPanelLookupFunction lookup_function; + +extern GeanyData *geany_data; + + +void prjorg_goto_symbol_free(PrjorgGotoSymbol *symbol) +{ + g_free(symbol->name); + g_free(symbol->file_name); + g_free(symbol->scope); + g_free(symbol->tooltip); + g_free(symbol); +} + + +static void tree_view_set_cursor_from_iter(GtkTreeView *view, GtkTreeIter *iter) +{ + GtkTreePath *path; + + path = gtk_tree_model_get_path(gtk_tree_view_get_model(view), iter); + gtk_tree_view_set_cursor(view, path, NULL, FALSE); + gtk_tree_path_free(path); +} + + +static void tree_view_move_focus(GtkTreeView *view, GtkMovementStep step, gint amount) +{ + GtkTreeIter iter; + GtkTreePath *path; + GtkTreeModel *model = gtk_tree_view_get_model(view); + gboolean valid = FALSE; + + gtk_tree_view_get_cursor(view, &path, NULL); + if (!path) + valid = gtk_tree_model_get_iter_first(model, &iter); + else + { + switch (step) { + case GTK_MOVEMENT_BUFFER_ENDS: + valid = gtk_tree_model_get_iter_first(model, &iter); + if (valid && amount > 0) + { + GtkTreeIter prev; + + do { + prev = iter; + } while (gtk_tree_model_iter_next(model, &iter)); + iter = prev; + } + break; + + case GTK_MOVEMENT_PAGES: + /* FIXME: move by page */ + case GTK_MOVEMENT_DISPLAY_LINES: + gtk_tree_model_get_iter(model, &iter, path); + if (amount > 0) + { + while ((valid = gtk_tree_model_iter_next(model, &iter)) && --amount > 0) + ; + } + else if (amount < 0) + { + while ((valid = gtk_tree_path_prev(path)) && --amount > 0) + ; + + if (valid) + gtk_tree_model_get_iter(model, &iter, path); + } + break; + + default: + g_assert_not_reached(); + } + gtk_tree_path_free(path); + } + + if (valid) + tree_view_set_cursor_from_iter(view, &iter); + else + gtk_widget_error_bell(GTK_WIDGET(view)); +} + + +static void tree_view_activate_focused_row(GtkTreeView *view) +{ + GtkTreePath *path; + GtkTreeViewColumn *column; + + gtk_tree_view_get_cursor(view, &path, &column); + if (path) + { + gtk_tree_view_row_activated(view, path, column); + gtk_tree_path_free(path); + } +} + + +void prjorg_goto_panel_fill(GPtrArray *symbols) +{ + GtkTreeView *view = GTK_TREE_VIEW(panel_data.tree_view); + GtkTreeIter iter; + PrjorgGotoSymbol *sym; + guint i; + + gtk_list_store_clear(panel_data.store); + + foreach_ptr_array(sym, i, symbols) + { + gchar *label; + + if (!sym->file_name) + continue; + + if (sym->file_name && sym->line > 0) + label = g_markup_printf_escaped("%s\n%s:%d", + sym->name, sym->file_name, sym->line); + else if (sym->file_name) + label = g_markup_printf_escaped("%s\n%s", + sym->name, sym->file_name); + else + label = g_markup_printf_escaped("%s", sym->name); + + gtk_list_store_insert_with_values(panel_data.store, NULL, -1, + COL_ICON, symbols_get_icon_pixbuf(sym->icon), + COL_LABEL, label, + COL_PATH, sym->file_name, + COL_LINENO, sym->line, + -1); + + g_free(label); + } + + if (gtk_tree_model_get_iter_first(gtk_tree_view_get_model(view), &iter)) + tree_view_set_cursor_from_iter(GTK_TREE_VIEW(panel_data.tree_view), &iter); +} + + +static gboolean on_panel_key_press_event(GtkWidget *widget, GdkEventKey *event, + gpointer dummy) +{ + switch (event->keyval) { + case GDK_KEY_Escape: + gtk_widget_hide(widget); + return TRUE; + + case GDK_KEY_Tab: + /* avoid leaving the entry */ + return TRUE; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_ISO_Enter: + tree_view_activate_focused_row(GTK_TREE_VIEW(panel_data.tree_view)); + return TRUE; + + case GDK_KEY_Page_Up: + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Up: + case GDK_KEY_KP_Page_Down: + { + gboolean up = event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_KP_Page_Up; + tree_view_move_focus(GTK_TREE_VIEW(panel_data.tree_view), + GTK_MOVEMENT_PAGES, up ? -1 : 1); + return TRUE; + } + + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + { + gboolean up = event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up; + tree_view_move_focus(GTK_TREE_VIEW(panel_data.tree_view), + GTK_MOVEMENT_DISPLAY_LINES, up ? -1 : 1); + return TRUE; + } + } + + return FALSE; +} + + +static void on_entry_text_notify(GObject *object, GParamSpec *pspec, gpointer dummy) +{ + GtkTreeIter iter; + GtkTreeView *view = GTK_TREE_VIEW(panel_data.tree_view); + GtkTreeModel *model = gtk_tree_view_get_model(view); + const gchar *text = gtk_entry_get_text(GTK_ENTRY(panel_data.entry)); + + lookup_function(text); + + if (gtk_tree_model_get_iter_first(model, &iter)) + tree_view_set_cursor_from_iter(view, &iter); +} + + +static void on_entry_activate(GtkEntry *entry, gpointer dummy) +{ + tree_view_activate_focused_row(GTK_TREE_VIEW(panel_data.tree_view)); +} + + +static void on_panel_hide(GtkWidget *widget, gpointer dummy) +{ + gtk_list_store_clear(panel_data.store); +} + + +static void on_panel_show(GtkWidget *widget, gpointer dummy) +{ + const gchar *text = gtk_entry_get_text(GTK_ENTRY(panel_data.entry)); + gboolean select_first = TRUE; + + if (text && (text[0] == ':' || text[0] == '#' || text[0] == '@')) + select_first = FALSE; + + gtk_widget_grab_focus(panel_data.entry); + gtk_editable_select_region(GTK_EDITABLE(panel_data.entry), select_first ? 0 : 1, -1); +} + + +static void on_view_row_activated(GtkTreeView *view, GtkTreePath *path, + GtkTreeViewColumn *column, gpointer dummy) +{ + GtkTreeModel *model = gtk_tree_view_get_model(view); + GtkTreeIter iter; + + if (gtk_tree_model_get_iter(model, &iter, path)) + { + GeanyDocument *doc; + gchar *file_path; + gint line; + + gtk_tree_model_get(model, &iter, + COL_PATH, &file_path, + COL_LINENO, &line, + -1); + + SETPTR(file_path, utils_get_locale_from_utf8(file_path)); + doc = document_open_file(file_path, FALSE, NULL, NULL); + + if (doc && line > 0) + navqueue_goto_line(document_get_current(), doc, line); + + g_free(file_path); + } + + gtk_widget_hide(panel_data.panel); +} + + +static void create_panel(void) +{ + GtkWidget *frame, *box, *scroll; + GtkTreeViewColumn *col; + GtkCellRenderer *renderer; + + panel_data.panel = g_object_new(GTK_TYPE_WINDOW, + "decorated", FALSE, + "default-width", 500, + "default-height", 350, + "transient-for", geany_data->main_widgets->window, + "window-position", GTK_WIN_POS_CENTER_ON_PARENT, + "type-hint", GDK_WINDOW_TYPE_HINT_DIALOG, + "skip-taskbar-hint", TRUE, + "skip-pager-hint", TRUE, + NULL); + g_signal_connect(panel_data.panel, "focus-out-event", + G_CALLBACK(gtk_widget_hide), NULL); + g_signal_connect(panel_data.panel, "show", + G_CALLBACK(on_panel_show), NULL); + g_signal_connect(panel_data.panel, "hide", + G_CALLBACK(on_panel_hide), NULL); + g_signal_connect(panel_data.panel, "key-press-event", + G_CALLBACK(on_panel_key_press_event), NULL); + + frame = gtk_frame_new(NULL); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN); + gtk_container_add(GTK_CONTAINER(panel_data.panel), frame); + + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(frame), box); + + panel_data.entry = gtk_entry_new(); + gtk_box_pack_start(GTK_BOX(box), panel_data.entry, FALSE, TRUE, 0); + + scroll = g_object_new(GTK_TYPE_SCROLLED_WINDOW, + "hscrollbar-policy", GTK_POLICY_AUTOMATIC, + "vscrollbar-policy", GTK_POLICY_AUTOMATIC, + NULL); + gtk_box_pack_start(GTK_BOX(box), scroll, TRUE, TRUE, 0); + + panel_data.tree_view = gtk_tree_view_new(); + gtk_widget_set_can_focus(panel_data.tree_view, FALSE); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(panel_data.tree_view), FALSE); + + panel_data.store = gtk_list_store_new(COL_COUNT, + GDK_TYPE_PIXBUF, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_INT); + gtk_tree_view_set_model(GTK_TREE_VIEW(panel_data.tree_view), GTK_TREE_MODEL(panel_data.store)); + g_object_unref(panel_data.store); + + renderer = gtk_cell_renderer_pixbuf_new(); + col = gtk_tree_view_column_new(); + gtk_tree_view_column_pack_start(col, renderer, FALSE); + gtk_tree_view_column_set_attributes(col, renderer, "pixbuf", COL_ICON, NULL); + g_object_set(renderer, "xalign", 0.0, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(panel_data.tree_view), col); + + renderer = gtk_cell_renderer_text_new(); + g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + col = gtk_tree_view_column_new_with_attributes(NULL, renderer, + "markup", COL_LABEL, + NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(panel_data.tree_view), col); + + g_signal_connect(panel_data.tree_view, "row-activated", + G_CALLBACK(on_view_row_activated), NULL); + gtk_container_add(GTK_CONTAINER(scroll), panel_data.tree_view); + + /* connect entry signals after the view is created as they use it */ + g_signal_connect(panel_data.entry, "notify::text", + G_CALLBACK(on_entry_text_notify), NULL); + g_signal_connect(panel_data.entry, "activate", + G_CALLBACK(on_entry_activate), NULL); + + gtk_widget_show_all(frame); +} + + +void prjorg_goto_panel_show(const gchar *query, PrjorgGotoPanelLookupFunction func) +{ + if (!panel_data.panel) + create_panel(); + + lookup_function = func; + + gtk_entry_set_text(GTK_ENTRY(panel_data.entry), query); + gtk_list_store_clear(panel_data.store); + gtk_widget_show(panel_data.panel); + + lookup_function(query); +} + + +GPtrArray *prjorg_goto_panel_filter(GPtrArray *symbols, const gchar *filter) +{ + GPtrArray *ret = g_ptr_array_new(); + gchar *case_normalized_filter; + gchar **tf_strv; + guint i; + guint j = 0; + + if (!symbols) + return ret; + + case_normalized_filter = g_utf8_normalize(filter, -1, G_NORMALIZE_ALL); + SETPTR(case_normalized_filter, g_utf8_casefold(case_normalized_filter, -1)); + + tf_strv = g_strsplit_set(case_normalized_filter, " ", -1); + + for (i = 0; i < symbols->len && j < 20; i++) + { + PrjorgGotoSymbol *symbol = symbols->pdata[i]; + gboolean filtered = FALSE; + gchar *case_normalized_name; + gchar **val; + + case_normalized_name = g_utf8_normalize(symbol->name, -1, G_NORMALIZE_ALL); + SETPTR(case_normalized_name, g_utf8_casefold(case_normalized_name, -1)); + + foreach_strv(val, tf_strv) + { + if (case_normalized_name != NULL && *val != NULL) + filtered = strstr(case_normalized_name, *val) == NULL; + + if (filtered) + break; + } + if (!filtered) + { + g_ptr_array_add(ret, symbol); + j++; + } + + g_free(case_normalized_name); + } + + g_strfreev(tf_strv); + g_free(case_normalized_filter); + + return ret; +} diff --git a/projectorganizer/src/prjorg-goto-panel.h b/projectorganizer/src/prjorg-goto-panel.h new file mode 100644 index 000000000..589b2e68c --- /dev/null +++ b/projectorganizer/src/prjorg-goto-panel.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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 PRJORG_GOTO_PANEL_H +#define PRJORG_GOTO_PANEL_H 1 + +#include + + +typedef struct +{ + gchar *name; + gchar *file_name; + gchar *scope; + gchar *tooltip; + gint line; + TMIcon icon; +} PrjorgGotoSymbol; + +void prjorg_goto_symbol_free(PrjorgGotoSymbol *symbol); + + +typedef void (*PrjorgGotoPanelLookupFunction) (const char *); + +void prjorg_goto_panel_show(const gchar *query, PrjorgGotoPanelLookupFunction func); +void prjorg_goto_panel_fill(GPtrArray *symbols); +GPtrArray *prjorg_goto_panel_filter(GPtrArray *symbols, const gchar *filter); + +#endif /* PRJORG_GOTO_PANEL_H */ diff --git a/projectorganizer/src/prjorg-menu.c b/projectorganizer/src/prjorg-menu.c index 1ca28770b..13de670b4 100644 --- a/projectorganizer/src/prjorg-menu.c +++ b/projectorganizer/src/prjorg-menu.c @@ -29,6 +29,7 @@ #include "prjorg-project.h" #include "prjorg-utils.h" #include "prjorg-sidebar.h" +#include "prjorg-goto-anywhere.h" #include @@ -45,11 +46,17 @@ enum KB_FOCUS_SIDEBAR, KB_OPEN_FILE_MANAGER, KB_OPEN_TERMINAL, + KB_GOTO_ANYWHERE, + KB_GOTO_DOC_SYMBOL, + KB_GOTO_WORKSPACE_SYMBOL, + KB_GOTO_LINE, KB_COUNT }; -static GtkWidget *s_fif_item, *s_ff_item, *s_ft_item, *s_shs_item, *s_sep_item, *s_context_osf_item, *s_context_sep_item; +static GtkWidget *s_fif_item, *s_ff_item, *s_ft_item, *s_shs_item, *s_sep_item1, + *s_context_osf_item, *s_context_sep_item, + *s_sep_item2, *s_goto_any_item, *s_goto_doc_item, *s_goto_wks_item, *s_goto_line_item; static void on_swap_header_source(G_GNUC_UNUSED GtkMenuItem * menuitem, G_GNUC_UNUSED gpointer user_data) @@ -253,9 +260,9 @@ void prjorg_menu_init(void) { GeanyKeyGroup *key_group = plugin_set_key_group(geany_plugin, "ProjectOrganizer", KB_COUNT, kb_callback); - s_sep_item = gtk_separator_menu_item_new(); - gtk_widget_show(s_sep_item); - gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_sep_item); + s_sep_item1 = gtk_separator_menu_item_new(); + gtk_widget_show(s_sep_item1); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_sep_item1); s_fif_item = menu_item_new("edit-find", _("Find in Project Files...")); gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_fif_item); @@ -282,6 +289,38 @@ void prjorg_menu_init(void) keybindings_set_item(key_group, KB_SWAP_HEADER_SOURCE, NULL, 0, 0, "swap_header_source", _("Swap header/source"), s_shs_item); + s_sep_item2 = gtk_separator_menu_item_new(); + gtk_widget_show(s_sep_item2); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_sep_item2); + + s_goto_any_item = gtk_menu_item_new_with_mnemonic(_("Go to _Anywhere...")); + gtk_widget_show(s_goto_any_item); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_goto_any_item); + g_signal_connect(s_goto_any_item, "activate", G_CALLBACK(prjorg_goto_anywhere_for_file), NULL); + keybindings_set_item(key_group, KB_GOTO_ANYWHERE, NULL, 0, 0, "goto_anywhere", + _("Go to anywhere"), s_goto_any_item); + + s_goto_doc_item = gtk_menu_item_new_with_mnemonic(_("Go to _Document Symbol...")); + gtk_widget_show(s_goto_doc_item); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_goto_doc_item); + g_signal_connect(s_goto_doc_item, "activate", G_CALLBACK(prjorg_goto_anywhere_for_doc), NULL); + keybindings_set_item(key_group, KB_GOTO_DOC_SYMBOL, NULL, 0, 0, "goto_doc_symbol", + _("Go to document symbol"), s_goto_doc_item); + + s_goto_wks_item = gtk_menu_item_new_with_mnemonic(_("Go to _Workspace Symbol...")); + gtk_widget_show(s_goto_wks_item); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_goto_wks_item); + g_signal_connect(s_goto_wks_item, "activate", G_CALLBACK(prjorg_goto_anywhere_for_workspace), NULL); + keybindings_set_item(key_group, KB_GOTO_WORKSPACE_SYMBOL, NULL, 0, 0, "goto_workspace_symbol", + _("Go to workspace symbol"), s_goto_wks_item); + + s_goto_line_item = gtk_menu_item_new_with_mnemonic(_("Go to _Line...")); + gtk_widget_show(s_goto_line_item); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->project_menu), s_goto_line_item); + g_signal_connect(s_goto_line_item, "activate", G_CALLBACK(prjorg_goto_anywhere_for_line), NULL); + keybindings_set_item(key_group, KB_GOTO_LINE, NULL, 0, 0, "goto_line", + _("Go to line"), s_goto_line_item); + keybindings_set_item(key_group, KB_FOCUS_SIDEBAR, (GeanyKeyCallback)prjorg_sidebar_focus_project_tab, 0, 0, "focus_project_sidebar", _("Focus Project Sidebar"), NULL); @@ -319,7 +358,13 @@ void prjorg_menu_cleanup(void) gtk_widget_destroy(s_ff_item); gtk_widget_destroy(s_ft_item); gtk_widget_destroy(s_shs_item); - gtk_widget_destroy(s_sep_item); + gtk_widget_destroy(s_sep_item1); + + gtk_widget_destroy(s_goto_any_item); + gtk_widget_destroy(s_goto_doc_item); + gtk_widget_destroy(s_goto_wks_item); + gtk_widget_destroy(s_goto_line_item); + gtk_widget_destroy(s_sep_item2); gtk_widget_destroy(s_context_osf_item); gtk_widget_destroy(s_context_sep_item); diff --git a/projectorganizer/src/prjorg-project.c b/projectorganizer/src/prjorg-project.c index 537c8a8cf..f1a5a6c1f 100644 --- a/projectorganizer/src/prjorg-project.c +++ b/projectorganizer/src/prjorg-project.c @@ -70,7 +70,7 @@ static void collect_source_files(gchar *filename, TMSourceFile *sf, gpointer use } -/* path - absolute path in locale, returned list in locale */ +/* path - absolute path in locale, returned list in utf8 */ static GSList *get_file_list(const gchar *utf8_path, GSList *patterns, GSList *ignored_dirs_patterns, GSList *ignored_file_patterns, GHashTable *visited_paths) {