diff --git a/data/io.elementary.code.gschema.xml b/data/io.elementary.code.gschema.xml index 400fe64e48..397b293ed1 100644 --- a/data/io.elementary.code.gschema.xml +++ b/data/io.elementary.code.gschema.xml @@ -152,6 +152,11 @@ Remember the last focused document. Restore the focused document from a previous session when opening Code. + + '' + The active project path. + The path to the folder containing the active project. + '' The default build directory's relative path. diff --git a/plugins/highlight-word-selection/highlight-word-selection.vala b/plugins/highlight-word-selection/highlight-word-selection.vala index 5896b5d43e..4c989c36c4 100644 --- a/plugins/highlight-word-selection/highlight-word-selection.vala +++ b/plugins/highlight-word-selection/highlight-word-selection.vala @@ -23,9 +23,7 @@ public class Scratch.Plugins.HighlightSelectedWords : Peas.ExtensionBase, Peas.A Scratch.MainWindow? main_window = null; Gtk.SourceSearchContext? current_search_context = null; - // Consts - // Pneumonoultramicroscopicsilicovolcanoconiosis longest word in a major dictionary @ 45 - private const uint SELECTION_HIGHLIGHT_MAX_CHARS = 45; + private const uint SELECTION_HIGHLIGHT_MAX_CHARS = 255; Scratch.Services.Interface plugins; public Object object { owned get; construct; } @@ -36,12 +34,10 @@ public class Scratch.Plugins.HighlightSelectedWords : Peas.ExtensionBase, Peas.A plugins = (Scratch.Services.Interface) object; plugins.hook_document.connect ((doc) => { if (current_source != null) { - current_source.deselected.disconnect (on_deselection); current_source.selection_changed.disconnect (on_selection_changed); } current_source = doc.source_view; - current_source.deselected.connect (on_deselection); current_source.selection_changed.connect (on_selection_changed); }); @@ -50,13 +46,13 @@ public class Scratch.Plugins.HighlightSelectedWords : Peas.ExtensionBase, Peas.A }); } - public void on_selection_changed (ref Gtk.TextIter start, ref Gtk.TextIter end) { + public void on_selection_changed (string selected_text, ref Gtk.TextIter start, ref Gtk.TextIter end) { var window_search_context = main_window != null ? main_window.search_bar.search_context : null; - - if (window_search_context == null || + if (selected_text != "" && + (window_search_context == null || window_search_context.settings.search_text == "" || - window_search_context.get_occurrences_count () == 0) { - // Perform plugin selection when there is no ongoing and successful search + window_search_context.get_occurrences_count () == 0)) { + // Perform plugin selection when there is no ongoing and successful search current_search_context = new Gtk.SourceSearchContext ( (Gtk.SourceBuffer)current_source.buffer, null @@ -118,13 +114,13 @@ public class Scratch.Plugins.HighlightSelectedWords : Peas.ExtensionBase, Peas.A ); // Ensure no leading or trailing space - var selected_text = start.get_text (end).strip (); + var stripped_selected_words = start.get_text (end).strip (); - if (selected_text.char_count () > SELECTION_HIGHLIGHT_MAX_CHARS) { + if (stripped_selected_words.char_count () > SELECTION_HIGHLIGHT_MAX_CHARS) { return; } - current_search_context.settings.search_text = selected_text; + current_search_context.settings.search_text = stripped_selected_words; current_search_context.set_highlight (true); } else if (current_search_context != null) { // Cancel existing search @@ -133,16 +129,8 @@ public class Scratch.Plugins.HighlightSelectedWords : Peas.ExtensionBase, Peas.A } } - public void on_deselection () { - if (current_search_context != null) { - current_search_context.set_highlight (false); - current_search_context = null; - } - } - public void deactivate () { if (current_source != null) { - current_source.deselected.disconnect (on_deselection); current_source.selection_changed.disconnect (on_selection_changed); } } diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index d9abbc7b77..fad3ffe6bd 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -1,4 +1,5 @@ /* + * Copyright 2024 elementary, Inc. * Copyright (c) 2013 Mario Guerriero * * This is a free software; you can redistribute it and/or @@ -18,50 +19,68 @@ * */ -public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, Object { - public string name; - public int priority; +public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, GLib.Object { + private const int MAX_COMPLETIONS = 10; + public string name { get; construct; } + public int priority { get; construct; } + public int interactive_delay { get; construct; } + public Gtk.SourceCompletionActivation activation { get; construct; } public const string COMPLETION_END_MARK_NAME = "ScratchWordCompletionEnd"; public const string COMPLETION_START_MARK_NAME = "ScratchWordCompletionStart"; - private Gtk.TextView? view; - private Gtk.TextBuffer? buffer; - private Euclide.Completion.Parser parser; + public Gtk.TextView? view { get; construct; } + public Euclide.Completion.Parser parser { get; construct; } + + private unowned Gtk.TextBuffer buffer { + get { + return view.buffer; + } + } + private Gtk.TextMark completion_end_mark; private Gtk.TextMark completion_start_mark; + private string current_text_to_find = ""; public signal void can_propose (bool b); - public CompletionProvider (Scratch.Plugins.Completion completion) { - this.view = completion.current_view as Gtk.TextView; - this.buffer = completion.current_view.buffer; - this.parser = completion.parser; + public CompletionProvider ( + Euclide.Completion.Parser _parser, + Scratch.Services.Document _doc + ) { + + Object ( + parser: _parser, + view: _doc.source_view, + name: _("%s - Word Completion").printf (_doc.get_basename ()) + ); + } + + construct { + interactive_delay = (int) Completion.INTERACTIVE_DELAY; + activation = INTERACTIVE | USER_REQUESTED; Gtk.TextIter iter; - buffer.get_iter_at_offset (out iter, 0); + view.buffer.get_iter_at_offset (out iter, 0); completion_end_mark = buffer.create_mark (COMPLETION_END_MARK_NAME, iter, false); completion_start_mark = buffer.create_mark (COMPLETION_START_MARK_NAME, iter, false); } - public string get_name () { - return this.name; - } + public override bool match (Gtk.SourceCompletionContext context) { + var iter = context.iter; + var start_iter = iter; + var end_iter = iter; + bool found = false; - public int get_priority () { - return this.priority; - } - - public bool match (Gtk.SourceCompletionContext context) { - Gtk.TextIter start, end; - buffer.get_iter_at_offset (out end, buffer.cursor_position); - start = end.copy (); - Euclide.Completion.Parser.back_to_word_start (ref start); - string text = buffer.get_text (start, end, true); + var preceding_word = parser.get_word_immediately_before (iter); + if (preceding_word != "") { + found = parser.match (preceding_word); + current_text_to_find = found ? preceding_word : ""; + } - return parser.match (text); + return found; } - public void populate (Gtk.SourceCompletionContext context) { + public override void populate (Gtk.SourceCompletionContext context) { /*Store current insertion point for use in activate_proposal */ GLib.List? file_props; bool no_minimum = (context.get_activation () == Gtk.SourceCompletionActivation.USER_REQUESTED); @@ -69,43 +88,40 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, context.add_proposals (this, file_props, true); } - public bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { + public override bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { Gtk.TextIter start; - Gtk.TextIter end; + Gtk.TextIter end_iter; Gtk.TextMark mark; mark = buffer.get_mark (COMPLETION_END_MARK_NAME); - buffer.get_iter_at_mark (out end, mark); + buffer.get_iter_at_mark (out end_iter, mark); + var end_pos = end_iter.get_offset (); + // If inserting in middle of word then completion overwrites end of word + var following_word = parser.get_word_immediately_after (end_iter); + if (following_word != "") { + buffer.get_iter_at_offset (out end_iter, end_pos + following_word.length); + } mark = buffer.get_mark (COMPLETION_START_MARK_NAME); buffer.get_iter_at_mark (out start, mark); - buffer.@delete (ref start, ref end); + buffer.@delete (ref start, ref end_iter); buffer.insert (ref start, proposal.get_text (), proposal.get_text ().length); return true; } - public Gtk.SourceCompletionActivation get_activation () { - return Gtk.SourceCompletionActivation.INTERACTIVE | - Gtk.SourceCompletionActivation.USER_REQUESTED; - } - - public int get_interactive_delay () { - return 0; - } - - public bool get_start_iter (Gtk.SourceCompletionContext context, + public override bool get_start_iter (Gtk.SourceCompletionContext context, Gtk.SourceCompletionProposal proposal, out Gtk.TextIter iter) { - var mark = buffer.get_insert (); - Gtk.TextIter cursor_iter; - buffer.get_iter_at_mark (out cursor_iter, mark); - iter = cursor_iter; - Euclide.Completion.Parser.back_to_word_start (ref iter); + var word_start = buffer.cursor_position; + buffer.get_iter_at_offset (out iter, word_start); return true; } + public override string get_name () { + return name; + } private bool get_proposals (out GLib.List? props, bool no_minimum) { string to_find = ""; @@ -114,36 +130,38 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, Gtk.TextIter start, end; buffer.get_selection_bounds (out start, out end); - to_find = temp_buffer.get_text (start, end, true); if (to_find.length == 0) { - temp_buffer.get_iter_at_offset (out end, buffer.cursor_position); - - start = end; - Euclide.Completion.Parser.back_to_word_start (ref start); - - to_find = buffer.get_text (start, end, false); + to_find = current_text_to_find; } buffer.move_mark_by_name (COMPLETION_END_MARK_NAME, end); buffer.move_mark_by_name (COMPLETION_START_MARK_NAME, start); /* There is no minimum length of word to find if the user requested a completion */ - if (no_minimum || to_find.length >= Euclide.Completion.Parser.MINIMUM_WORD_LENGTH) { + if (no_minimum || to_find.length >= Euclide.Completion.Parser.MINIMUM_PREFIX_LENGTH) { /* Get proposals, if any */ - List prop_word_list; - if (parser.get_for_word (to_find, out prop_word_list)) { - foreach (var word in prop_word_list) { - var item = new Gtk.SourceCompletionItem (); - item.label = word; - item.text = word; - props.append (item); + var completions = parser.get_current_completions (to_find); + if (completions.length () > 0) { + var index = 0; + foreach (var completion in completions) { + if (completion.length > 0) { + var item = new Gtk.SourceCompletionItem (); + var word = to_find + completion; + item.label = word; + item.text = completion; + props.append (item); + if (++index > MAX_COMPLETIONS) { + break; + } + } } return true; } } + return false; } } diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index eff236042f..8efdbf5799 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -1,6 +1,7 @@ /* - * Copyright (c) 2011 Lucas Baudin - * + * Copyright 2024 elementary, Inc. + * 2011 Lucas Baudin + * * * This is a 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 @@ -19,83 +20,214 @@ */ public class Euclide.Completion.Parser : GLib.Object { - public const int MINIMUM_WORD_LENGTH = 1; - public const int MAX_TOKENS = 1000000; + // DELIMITERS used for word completion are not necessarily the same Pango word breaks + // Therefore, we reimplement some iter functions to move between words here below + public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`\"\'"; + public const int MINIMUM_WORD_LENGTH = 3; + public const int MAXIMUM_WORD_LENGTH = 50; + public const int MINIMUM_PREFIX_LENGTH = 1; + public const int MAX_TOKENS = 100000; + public static bool is_delimiter (unichar? uc) { + return uc == null || DELIMITERS.index_of_char (uc) > -1; + } - private Scratch.Plugins.PrefixTree prefix_tree; + private Scratch.Plugins.PrefixTree? current_tree = null; + public Gee.HashMap text_view_words; + public bool parsing_cancelled = false; - public const string DELIMITERS = " .,;:?{}[]()0123456789+=&|<>*\\/\r\n\t\'\"`"; - public static bool is_delimiter (unichar c) { - return DELIMITERS.index_of_char (c) >= 0; + public Parser () { + text_view_words = new Gee.HashMap (); } - public static void back_to_word_start (ref Gtk.TextIter iter) { - iter.backward_find_char (is_delimiter, null); - iter.forward_char (); + public bool select_current_tree (Gtk.TextView view) { + bool pre_existing = true; + if (!text_view_words.has_key (view)) { + var new_treemap = new Scratch.Plugins.PrefixTree (); + text_view_words.@set (view, new_treemap); + pre_existing = false; + } + + lock (current_tree) { + current_tree = text_view_words.@get (view); + parsing_cancelled = false; + } + + return pre_existing && get_initial_parsing_completed (); } - public Gee.HashMap text_view_words; - public bool parsing_cancelled = false; + public void set_initial_parsing_completed (bool completed) requires (current_tree != null) { + lock (current_tree) { + current_tree.completed = completed; + } + } - public Parser () { - text_view_words = new Gee.HashMap (); - prefix_tree = new Scratch.Plugins.PrefixTree (); + public bool get_initial_parsing_completed () requires (current_tree != null) { + return current_tree.completed; } - public bool match (string to_find) { - return prefix_tree.find_prefix (to_find); + // This gets called from a thread + public void initial_parse_buffer_text (string buffer_text) { + parsing_cancelled = false; + clear (); + if (buffer_text.length > 0) { + var parsed = parse_text_and_add (buffer_text); + set_initial_parsing_completed (parsed); + } else { + // Assume any buffer text would have been loaded when this is called + // so definitely no initial parse needed + set_initial_parsing_completed (true); + } + + debug ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); } - public bool get_for_word (string to_find, out List list) { - list = prefix_tree.get_all_matches (to_find); - return list.first () != null; + // Returns true if text was completely parsed + public bool parse_text_and_add (string text) { + int index = 0; + string[] words = text.split_set (DELIMITERS); + uint n_words = words.length; + while (!parsing_cancelled && index < n_words) { + add_word (words[index++]); // only valid words will be added + } + + return index == n_words; } - public void rebuild_word_list (Gtk.TextView view) { - prefix_tree.clear (); - parse_text_view (view); + public void parse_text_and_remove (string text) { + if (text.length < MINIMUM_WORD_LENGTH) { + return; + } + + int index = 0; + string[] words = text.split_set (DELIMITERS); + uint n_words = words.length; + while (index < n_words) { + remove_word (words[index++]); + } + + return; } - public void parse_text_view (Gtk.TextView view) { - /* If this view has already been parsed, restore the word list */ - lock (prefix_tree) { - if (text_view_words.has_key (view)) { - prefix_tree = text_view_words.@get (view); - } else { - /* Else create a new word list and parse the buffer text */ - prefix_tree = new Scratch.Plugins.PrefixTree (); - } + public string get_word_immediately_before (Gtk.TextIter iter) { + int end_pos; + var text = get_sentence_at_iter (iter, out end_pos); + var pos = end_pos; + unichar uc; + text.get_prev_char (ref pos, out uc); + if (is_delimiter (uc)) { + return ""; } - if (view.buffer.text.length > 0) { - parse_string (view.buffer.text); - text_view_words.@set (view, prefix_tree); + pos = (end_pos - MAXIMUM_WORD_LENGTH - 1).clamp (0, end_pos); + if (pos >= end_pos) { + critical ("pos after end_pos"); + return ""; } + + var sliced_text = text.slice (pos, end_pos); + var words = sliced_text.split_set (DELIMITERS); + var previous_word = words[words.length - 1]; // Maybe "" + return previous_word; } - public void add_word (string word) { - if (word.length < MINIMUM_WORD_LENGTH) - return; + public string get_word_immediately_after (Gtk.TextIter iter) { + int start_pos; + var text = get_sentence_at_iter (iter, out start_pos); + var pos = start_pos; + unichar uc; + text.get_next_char (ref pos, out uc); + if (is_delimiter (uc)) { + return ""; + } + + // Find end of search range + pos = (start_pos + MAXIMUM_WORD_LENGTH + 1).clamp (start_pos, text.length); + if (start_pos >= pos) { + critical ("start pos after pos"); + return ""; + } + + // Find first word in range + var words = text.slice (start_pos, pos).split_set (DELIMITERS, 2); + var next_word = words[0]; // Maybe "" + return next_word; + } + + private string get_sentence_at_iter (Gtk.TextIter iter, out int iter_sentence_offset) { + var start_iter = iter; + var end_iter = iter; + start_iter.backward_sentence_start (); + end_iter.forward_sentence_end (); + var text = start_iter.get_text (end_iter); + iter_sentence_offset = iter.get_offset () - start_iter.get_offset (); + return text; + } + + public void clear () requires (current_tree != null) { + cancel_parsing (); + lock (current_tree) { + current_tree.clear (); // Sets completed false + set_initial_parsing_completed (false); - lock (prefix_tree) { - prefix_tree.insert (word); } + + parsing_cancelled = false; } public void cancel_parsing () { + // Do not need to cancel reaping - this continues when prefix_tree is not current parsing_cancelled = true; } - private bool parse_string (string text) { - parsing_cancelled = false; - string [] word_array = text.split_set (DELIMITERS, MAX_TOKENS); - foreach (var current_word in word_array ) { - if (parsing_cancelled) { - debug ("Cancelling parse"); - return false; + private List current_completions; + private string current_prefix; + public bool match (string prefix) requires (current_tree != null) { + lock (current_tree) { + current_completions = current_tree.get_all_completions (prefix); + current_prefix = prefix; + } + + return current_completions != null && current_completions.first ().data != null; + } + + public List get_current_completions (string prefix) requires (current_tree != null) { + // Assume always preceded by match and current_completions up to date + if (current_prefix != prefix) { + critical ("current prefix does not match"); + match (prefix); + } + + return (owned)current_completions; + } + + public void add_word (string word_to_add) requires (current_tree != null) { + if (is_valid_word (word_to_add)) { + // warning ("ADD WORD %s", word_to_add); + lock (current_tree) { + current_tree.add_word (word_to_add); + } + } + } + + public void remove_word (string word_to_remove) requires (current_tree != null) { + if (is_valid_word (word_to_remove)) { + // warning ("REMOVE WORD %s", word_to_remove); + lock (current_tree) { + current_tree.remove_word (word_to_remove); } - add_word (current_word); } + } + + private bool is_valid_word (string word) { + if (word.strip ().length < MINIMUM_WORD_LENGTH) { + return false; + } + + // Exclude words beginning with digit + if (word.get_char (0).isdigit ()) { + return false; + } + return true; } } diff --git a/plugins/word-completion/meson.build b/plugins/word-completion/meson.build index 247b0e60ad..23a75eee23 100644 --- a/plugins/word-completion/meson.build +++ b/plugins/word-completion/meson.build @@ -2,6 +2,7 @@ module_name = 'word-completion' module_files = [ 'prefix-tree.vala', + 'prefix-tree-node.vala', 'completion-provider.vala', 'engine.vala', 'plugin.vala' diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index d227d12a6a..69c97b6d63 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -19,16 +19,10 @@ */ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { - public Object object { owned get; construct; } - - private List text_view_list = new List (); - public Euclide.Completion.Parser parser {get; private set;} - public Gtk.SourceView? current_view {get; private set;} - public Scratch.Services.Document current_document {get; private set;} - private MainWindow main_window; - private Scratch.Services.Interface plugins; - private bool completion_in_progress = false; + public const int MAX_TOKENS = 1000000; + public const uint INTERACTIVE_DELAY = 500; + public const int INITIAL_PARSE_DELAY_MSEC = 1000; private const uint [] ACTIVATE_KEYS = { Gdk.Key.Return, @@ -41,6 +35,19 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private const uint REFRESH_SHORTCUT = Gdk.Key.bar; //"|" in combination with will cause refresh + + public Object object { owned get; construct; } + + private List text_view_list = new List (); + private Euclide.Completion.Parser parser; + private Gtk.SourceView? current_view; + private Gtk.SourceCompletion? current_completion; + private Scratch.Plugins.CompletionProvider current_provider; + private Scratch.Services.Document current_document {get; private set;} + private MainWindow main_window; + private Scratch.Services.Interface plugins; + private bool completion_in_progress = false; + private uint timeout_id = 0; public void activate () { @@ -62,124 +69,157 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } public void on_new_source_view (Scratch.Services.Document doc) { + debug ("new source_view %s", doc != null ? doc.title : "null"); if (current_view != null) { - if (current_view == doc.source_view) + if (current_view == doc.source_view) { return; + } parser.cancel_parsing (); - - if (timeout_id > 0) - GLib.Source.remove (timeout_id); - - cleanup (current_view); + cleanup (); } current_document = doc; current_view = doc.source_view; - current_view.key_press_event.connect (on_key_press); - current_view.completion.show.connect (() => { - completion_in_progress = true; - }); - current_view.completion.hide.connect (() => { - completion_in_progress = false; - }); - + current_completion = current_view.completion; - if (text_view_list.find (current_view) == null) + if (text_view_list.find (current_view) == null) { text_view_list.append (current_view); + } - var comp_provider = new Scratch.Plugins.CompletionProvider (this); - comp_provider.priority = 1; - comp_provider.name = provider_name_from_document (doc); + current_provider = new Scratch.Plugins.CompletionProvider (parser, doc); try { - current_view.completion.add_provider (comp_provider); - current_view.completion.show_headers = true; - current_view.completion.show_icons = true; - /* Wait a bit to allow text to load then run parser*/ - timeout_id = Timeout.add (1000, on_timeout_update); - + current_completion.add_provider (current_provider); + current_completion.show_headers = true; + current_completion.show_icons = true; + current_completion.accelerators = 9; + current_completion.select_on_show = true; } catch (Error e) { - warning (e.message); + critical ( + "Could not add completion provider to %s. %s\n", + current_document.title, + e.message + ); + cleanup (); + return; } - } - private bool on_timeout_update () { - try { - new Thread.try ("word-completion-thread", () => { - if (current_view != null) - parser.parse_text_view (current_view as Gtk.TextView); + // Check whether there is already a parsed tree + if (!parser.select_current_tree (current_view)) { + // If not, start initial parsing after timeout to ensure text loaded + var view_to_parse = current_view; + timeout_id = Timeout.add (INITIAL_PARSE_DELAY_MSEC, () => { + timeout_id = 0; + // Check view has not been switched + if (view_to_parse == current_view) { + try { + new Thread.try ("word-completion-thread", () => { + // The initial parse gets cancelled if view switched before complete + parser.initial_parse_buffer_text (view_to_parse.buffer.text); + return null; + }); + } catch (Error e) { + warning (e.message); + } + } - return null; + return Source.REMOVE; }); - } catch (Error e) { - warning (e.message); } - timeout_id = 0; - return false; + // Always connect signals - they are disconnected in cleanup + connect_signals (); } - private bool on_key_press (Gtk.Widget view, Gdk.EventKey event) { - var kv = event.keyval; - /* Pass through any modified keypress except Shift or Capslock */ - Gdk.ModifierType mods = event.state & Gdk.ModifierType.MODIFIER_MASK - & ~Gdk.ModifierType.SHIFT_MASK - & ~Gdk.ModifierType.LOCK_MASK; - if (mods > 0 ) { - /* Default key for USER_REQUESTED completion is ControlSpace - * but this is trapped elsewhere. Control + USER_REQUESTED_KEY acts as an - * alternative and also purges spelling mistakes and unused words from the list. - * If used when a word or part of a word is selected, the selection will be - * used as the word to find. */ - - if ((mods & Gdk.ModifierType.CONTROL_MASK) > 0 && - (kv == REFRESH_SHORTCUT)) { - - parser.rebuild_word_list (current_view); - current_view.show_completion (); - return true; - } + // Runs before default handler so buffer text not yet modified. @pos must not be invalidated + private void on_insert_text (Gtk.TextIter iter, string new_text, int new_text_length) { + if (!parser.get_initial_parsing_completed ()) { + return; } + // Determine whether insertion point ends and/or starts a word + var word_before = parser.get_word_immediately_before (iter); + var word_after = parser.get_word_immediately_after (iter); + var text_to_add = (word_before + new_text + word_after); + var text_to_remove = (word_before + word_after); + // Only update if words have changed + debug ("insert text - add '%s' + '%s' + '%s'", word_before, new_text, word_after); + if (text_to_add != text_to_remove) { + parser.parse_text_and_add (text_to_add); + parser.remove_word (text_to_remove); + } + } - var uc = (unichar)(Gdk.keyval_to_unicode (kv)); - if (!completion_in_progress && Euclide.Completion.Parser.is_delimiter (uc) && - (uc.isprint () || uc.isspace ())) { - - var buffer = current_view.buffer; - var mark = buffer.get_insert (); - Gtk.TextIter cursor_iter; - buffer.get_iter_at_mark (out cursor_iter, mark); - - var word_start = cursor_iter; - Euclide.Completion.Parser.back_to_word_start (ref word_start); - - string word = buffer.get_text (word_start, cursor_iter, false); - parser.add_word (word); + private void on_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + if (!parser.get_initial_parsing_completed ()) { + return; } - return false; + var del_text = del_start_iter.get_text (del_end_iter); + var word_before = parser.get_word_immediately_before (del_start_iter); + var word_after = parser.get_word_immediately_after (del_end_iter); + var to_remove = word_before + del_text + word_after; + var to_add = word_before + word_after; + + // More than one word could be deleted so parse. + debug ("delete range - remove '%s' + '%s' + '%s'", word_before, del_text, word_after); + parser.parse_text_and_remove (to_remove); + // Only one at most new words + parser.add_word (to_add); + + // Completions not usually shown after deletions so trigger it ourselves + if (del_text.length == 1) { + schedule_completion (); + } } - private string provider_name_from_document (Scratch.Services.Document doc) { - return _("%s - Word Completion").printf (doc.get_basename ()); + uint completion_timeout_id = 0; + bool wait = true; + // Wait until after buffer has finished being amended then trigger completion + private void schedule_completion () { + if (completion_timeout_id == 0) { + completion_timeout_id = Timeout.add (current_provider.interactive_delay, () => { + if (wait) { + wait = false; + return Source.CONTINUE; + } else { + completion_timeout_id = 0; + wait = true; + current_view.show_completion (); + return Source.REMOVE; + } + }); + } else { + wait = true; + } } - private void cleanup (Gtk.SourceView view) { - current_view.key_press_event.disconnect (on_key_press); + private void cleanup () { + if (timeout_id > 0) { + GLib.Source.remove (timeout_id); + } + + disconnect_signals (); - current_view.completion.get_providers ().foreach ((p) => { + current_completion.get_providers ().foreach ((p) => { try { /* Only remove provider added by this plug in */ - if (p.get_name () == provider_name_from_document (current_document)) { - debug ("removing provider %s", p.get_name ()); - current_view.completion.remove_provider (p); - } + current_completion.remove_provider (current_provider); } catch (Error e) { warning (e.message); } }); } + + private void connect_signals () { + current_view.buffer.insert_text.connect (on_insert_text); + current_view.buffer.delete_range.connect (on_delete_range); + } + + private void disconnect_signals () { + current_view.buffer.insert_text.disconnect (on_insert_text); + current_view.buffer.delete_range.disconnect (on_delete_range); + } } [ModuleInit] diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala new file mode 100644 index 0000000000..34c4b7d6c6 --- /dev/null +++ b/plugins/word-completion/prefix-tree-node.vala @@ -0,0 +1,242 @@ +/* + * Copyright 2024 elementary, Inc. + * 2011 Lucas Baudin + * * + * This is a 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 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +public class Scratch.Plugins.PrefixNode : Object { + private const unichar WORD_END_CHAR = '\0'; + private enum NodeType { + ROOT, + CHAR, + WORD_END + } + + private Gee.ArrayList children; + private unichar? uc = null; + private NodeType type = ROOT; + public uint occurrences { get; set construct; default = 0; } + public PrefixNode? parent { get; construct; default = null; } + + protected bool is_word_end { + get { + return type == WORD_END; + } + } + + protected string char_s { + owned get { + if (uc != null) { + return uc.to_string (); + } else { + return ""; + } + } + } + + protected bool has_children { + get { + return type != WORD_END && children.size > 0; + } + } + + public PrefixNode.from_unichar (unichar c, PrefixNode? _parent) requires (c != WORD_END_CHAR) { + Object ( + parent: _parent, + occurrences: 1 + ); + + uc = c; + type = CHAR; + } + + public PrefixNode.root () { + Object ( + parent: null, + occurrences: 0 + ); + + type = ROOT; + } + + public PrefixNode.word_end (PrefixNode _parent) { + Object ( + parent: _parent, + occurrences: 1 + ); + + uc = WORD_END_CHAR; + type = WORD_END; + } + + construct { + children = new Gee.ArrayList (); + } + + private bool has_char (unichar c) { + return uc == c; + } + + private void increment () requires (type == WORD_END) { + lock (occurrences) { + occurrences++; + } + } + + // Returns true if word still occurs + public bool decrement () requires (type == WORD_END) { + if (occurrences == 0) { + warning ("decrementing non-occurring node"); + return false; + } + + lock (occurrences) { + occurrences--; + } + + return occurrences > 0; + } + + public bool occurs () requires (type == WORD_END) { + return occurrences > 0; + } + + // Only called to add a complete word to the tree + public void insert_word (string text) requires (type == ROOT) { + debug ("rootnode: insert word %s", text); + int index = 0; + insert_word_internal (text, ref index); + } + + public void remove_child (PrefixNode child) requires (type != WORD_END) { + lock (children) { + children.remove (child); + debug ("removed child '%s'", child.char_s); + if (children.is_empty && type != ROOT) { + debug ("remove this from parent"); + parent.remove_child (this); + } + } + } + + // We return any end_node for @text even if occurences == 0 + // because we may be re-adding it before it is reaped + public PrefixNode? find_end_node_for (string text) { + debug ("find_end_node_for %s", text); + var last_node = find_last_node_for (text); + if (last_node != null) { + var end_node = last_node.find_or_append_char_child (WORD_END_CHAR, false); + return end_node; + } + + return null; + } + + // Returns node corresponding to last char in @text (or null if not in tree) + public PrefixNode? find_last_node_for (string text) { + int index = 0; + return find_last_node_for_internal (text, ref index); + } + + //PROTECTED METHODS + + protected void insert_word_internal (string text, ref int index) { + unichar? uc = null; + if (text.get_next_char (ref index, out uc)) { + var child = find_or_append_char_child (uc, true); // Appends if not found + child.insert_word_internal (text, ref index); + } else { + append_or_increment_word_end (); + } + } + + protected PrefixNode? find_last_node_for_internal (string text, ref int index) requires (type != WORD_END) { + unichar? uc = null; + if (text.get_next_char (ref index, out uc)) { + debug ("find char_child '%s'", uc.to_string ()); + var child = find_or_append_char_child (uc, false); + if (child == null ) { + debug ("child not found"); + return null; + } else { + return child.find_last_node_for_internal (text, ref index); + } + } else { + debug ("end of text - current node type %s", this.type.to_string ()); + return this; + } + } + //PRIVATE METHODS + + private void append_or_increment_word_end () requires (type != WORD_END && type != ROOT) { + foreach (var child in children) { + if (child.type == WORD_END) { + debug ("incrementing end node occurrence"); + child.increment (); + return; + } + } + + var new_child = new PrefixNode.word_end (this); + debug ("append new word end"); + append_child (new_child); + } + + private PrefixNode? find_or_append_char_child (unichar c, bool append_if_not_found = false) + requires (type != WORD_END) { + + foreach (var child in children) { + if (child.has_char (c)) { + return child; + } + } + + if (append_if_not_found) { + var new_child = new PrefixNode.from_unichar (c, this); + append_child (new_child); + return new_child; + } else { + return null; + } + } + + // Children are only appended if they do not already exist + private void append_child (owned PrefixNode child) requires (type != WORD_END) { + lock (children) { + children.add (child); + } + } + + public void get_all_completions (ref List completions, ref StringBuilder sb) { + if (type == WORD_END) { + return; + } + + var initial_sb_str = sb.str; + foreach (var child in children) { + if (child.type == WORD_END && child.occurs ()) { + completions.prepend (sb.str); + } else { + sb.append (child.char_s); + child.get_all_completions (ref completions, ref sb); + } + + sb.assign (initial_sb_str); + } + } +} diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index ea5ca2d414..4cd34852e6 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -1,115 +1,114 @@ - -namespace Scratch.Plugins { - private class PrefixNode : Object { - public GLib.List children; - public unichar value { get; set; } - - construct { - children = new List (); - } +/* + * Copyright 2024 elementary, Inc. + * 2011 Lucas Baudin + * * + * This is a 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 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + public class Scratch.Plugins.PrefixTree : Object { + private PrefixNode? root = null; + private GLib.StringBuilder sb; + private uint reaper_timeout_id = 0; + private bool delay_reaping = false; + // Reaping need not occur before completions shown since non-occurring words are + // not shown anyway. + private const uint REAPING_THROTTLE_MS = Completion.INTERACTIVE_DELAY * 2; + private bool reaping_cancelled = false; + + public bool completed { get; set; default = false; } + // We just store the end_nodes corresponding to words to be removed + public Gee.LinkedList words_to_remove { get; construct; } + + construct { + clear (); + sb = new GLib.StringBuilder (""); + words_to_remove = new Gee.LinkedList (); } - public class PrefixTree : Object { - private PrefixNode root; - - construct { - clear (); - } - - public void clear () { - root = new PrefixNode () { - value = '\0' - }; - } - - public void insert (string word) { - if (word.length == 0) { - return; - } - - this.insert_at (word, this.root); - } - - private void insert_at (string word, PrefixNode node, int i = 0) { - unichar curr = '\0'; - - bool has_next_character = false; - do { - has_next_character = word.get_next_char (ref i, out curr); - } while (has_next_character && Euclide.Completion.Parser.is_delimiter (curr)); - - foreach (var child in node.children) { - if (child.value == curr) { - if (curr != '\0') { - insert_at (word, child, i); - } - return; - } - } + public void clear () { + root = new PrefixNode.root (); + completed = false; + } - var new_child = new PrefixNode () { - value = curr - }; - node.children.insert_sorted (new_child, (c1, c2) => { - if (c1.value > c2.value) { - return 1; - } else if (c1.value == c2.value) { - return 0; - } - return -1; - }); - if (curr != '\0') { - insert_at (word, new_child, i); - } - } + public void add_word (string word) requires (word.length > 0) { + debug ("add '%s' to root", word); + root.insert_word (word); + } - public bool find_prefix (string prefix) { - return find_prefix_at (prefix, root) != null? true : false; + public List get_all_completions (string prefix) { + var list = new List (); + var last_node = root.find_last_node_for (prefix); + if (last_node != null) { + sb.erase (); + last_node.get_all_completions (ref list, ref sb); } - private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { - unichar curr; - - prefix.get_next_char (ref i, out curr); - if (curr == '\0') { - return node; - } - - foreach (var child in node.children) { - if (child.value == curr) { - return find_prefix_at (prefix, child, i); - } - } - - return null; - } + return (owned)list; + } - public List get_all_matches (string prefix) { - var list = new List (); - var node = find_prefix_at (prefix, root, 0); - if (node != null) { - var sb = new StringBuilder (prefix); - get_all_matches_rec (node, ref sb, ref list); + public void remove_word (string word_to_remove) { + debug ("prefix tree: remove word %s", word_to_remove); + var end_node = root.find_end_node_for (word_to_remove); + if (end_node != null) { + if (!end_node.decrement ()) { + debug ("schedule remove %s", word_to_remove); + words_to_remove.add (end_node); + schedule_reaping (); + } else { + debug ("not removing %s - still occurs", word_to_remove); } - - return list; + } else { + debug ("%s end node not found in tree", word_to_remove); } + } - private void get_all_matches_rec ( - PrefixNode node, - ref StringBuilder sbuilder, - ref List matches) { - - foreach (var child in node.children) { - if (child.value == '\0') { - matches.append (sbuilder.str); + private void schedule_reaping () { + reaping_cancelled = false; + if (reaper_timeout_id > 0) { + delay_reaping = true; + return; + } else { + reaper_timeout_id = Timeout.add (REAPING_THROTTLE_MS, () => { + if (delay_reaping) { + delay_reaping = false; + return Source.CONTINUE; } else { - sbuilder.append_unichar (child.value); - get_all_matches_rec (child, ref sbuilder, ref matches); - var length = child.value.to_string ().length; - sbuilder.erase (sbuilder.len - length, -1); + reaper_timeout_id = 0; + debug ("reaping"); + words_to_remove.foreach ((end_node) => { + if (reaping_cancelled) { + debug ("reaping was cancelled"); + return false; + } + + if (!end_node.occurs ()) { + end_node.parent.remove_child (end_node); + } else { + debug ("still occurs when reaping"); + } + + return true; + }); + + // Cannot clear list while inside @foreach loop so do it now + words_to_remove.clear (); + return Source.REMOVE; } - } + }); } } } diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index c9efd85bbf..ef747756b6 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -35,6 +35,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane public const string ACTION_CHANGE_BRANCH = "change-branch"; public const string ACTION_CLOSE_FOLDER = "close-folder"; public const string ACTION_CLOSE_OTHER_FOLDERS = "close-other-folders"; + public const string ACTION_SET_ACTIVE_PROJECT = "set-active-project"; private const ActionEntry[] ACTION_ENTRIES = { { ACTION_LAUNCH_APP_WITH_FILE_PATH, action_launch_app_with_file_path, "as" }, @@ -46,7 +47,8 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane { ACTION_NEW_FILE, add_new_file, "s" }, { ACTION_NEW_FOLDER, add_new_folder, "s"}, { ACTION_CLOSE_FOLDER, action_close_folder, "s"}, - { ACTION_CLOSE_OTHER_FOLDERS, action_close_other_folders, "s"} + { ACTION_CLOSE_OTHER_FOLDERS, action_close_other_folders, "s"}, + { ACTION_SET_ACTIVE_PROJECT, action_set_active_project, "s"} }; private GLib.Settings settings; @@ -60,11 +62,6 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane public ActionGroup toplevel_action_group { get; private set; } public string icon_name { get; set; } public string title { get; set; } - public string active_project_path { - get { - return git_manager.active_project_path; - } - } public FileView (Scratch.Services.PluginsManager plugins_manager) { plugins = plugins_manager; @@ -119,10 +116,30 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane if (project_folder_item != folder_root) { toplevel_action_group.activate_action (MainWindow.ACTION_CLOSE_PROJECT_DOCS, new Variant.string (project_folder_item.path)); root.remove (project_folder_item); - Scratch.Services.GitManager.get_instance ().remove_project (project_folder_item); + git_manager.remove_project (project_folder_item); } } + //Make remaining project the active one + git_manager.active_project_path = path; + + write_settings (); + } + + private void action_set_active_project (SimpleAction action, GLib.Variant? parameter) { + warning ("set active project"); + var path = parameter.get_string (); + if (path == null || path == "") { + return; + } + + var folder_root = find_path (root, path) as ProjectFolderItem; + if (folder_root == null) { + return; + } + + git_manager.active_project_path = path; + write_settings (); } @@ -176,14 +193,9 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane selected = null; } - public void collapse_other_projects (string? keep_open_path = null) { + public void collapse_other_projects () { unowned string path; - if (keep_open_path == null) { - path = git_manager.active_project_path; - } else { - path = keep_open_path; - git_manager.active_project_path = path; - } + path = git_manager.active_project_path; foreach (var child in root.children) { var project_folder = ((ProjectFolderItem) child); @@ -528,10 +540,13 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane rename_items_with_same_name (child_folder); } } - Scratch.Services.GitManager.get_instance ().remove_project (folder_root); + + git_manager.remove_project (folder_root); write_settings (); }); + // Do not assume this is the active folder + write_settings (); } diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 9998db6be3..1e0c8434a9 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -134,16 +134,28 @@ namespace Scratch.FolderManager { warning (e.message); } - var open_in_terminal_pane_item = new GLib.MenuItem ( - _("Open in Terminal Pane"), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN_IN_TERMINAL, - new Variant.string ( - Services.GitManager.get_instance ().get_default_build_dir (path) + MenuItem set_active_folder_item; + if (is_git_repo) { + set_active_folder_item = new GLib.MenuItem ( + _("Set as Active Project"), + GLib.Action.print_detailed_name ( + FileView.ACTION_PREFIX + FileView.ACTION_SET_ACTIVE_PROJECT, + new Variant.string (file.path) ) - ) - ); - open_in_terminal_pane_item.set_attribute_value ( + ); + } else { + set_active_folder_item = new GLib.MenuItem ( + _("Open in Terminal Pane"), + GLib.Action.print_detailed_name ( + MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN_IN_TERMINAL, + new Variant.string ( + Services.GitManager.get_instance ().get_default_build_dir (path) + ) + ) + ); + } + + set_active_folder_item.set_attribute_value ( "accel", Utils.get_accel_for_action ( GLib.Action.print_detailed_name ( @@ -154,7 +166,7 @@ namespace Scratch.FolderManager { ); var external_actions_section = new GLib.Menu (); - external_actions_section.append_item (open_in_terminal_pane_item); + external_actions_section.append_item (set_active_folder_item); external_actions_section.append_item (create_submenu_for_open_in (file_type)); var folder_actions_section = new GLib.Menu (); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 34fe7d93db..ece315229a 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -27,6 +27,17 @@ namespace Scratch { public Scratch.Application app { get; private set; } public bool restore_docs { get; construct; } public RestoreOverride restore_override { get; construct set; } + public string default_globalsearch_path { + owned get { + if (document_view.current_document != null) { + if (document_view.current_document.project_path != "") { + return document_view.current_document.project_path; + } + } + + return git_manager.active_project_path; + } + } public Scratch.Widgets.DocumentView document_view; @@ -618,7 +629,11 @@ namespace Scratch { title = _("%s - %s").printf (doc.get_basename (), base_title); toolbar.set_document_focus (doc); - git_manager.active_project_path = doc.source_view.project.path; + + if (doc.source_view.project != null) { + git_manager.active_project_path = doc.source_view.project.path; + } + folder_manager_view.select_path (doc.file.get_path ()); // Must follow setting focus document for editorconfig plug @@ -1200,8 +1215,10 @@ namespace Scratch { } private void action_find_global (SimpleAction action, Variant? param) { + var selected_text = ""; + var search_path = ""; + var current_doc = get_current_document (); - string selected_text = ""; if (current_doc != null) { selected_text = current_doc.get_selected_text (false); } @@ -1221,7 +1238,19 @@ namespace Scratch { term = search_bar.search_entry.text; } - folder_manager_view.search_global (get_target_path_for_actions (param), term); + if (param != null && param.get_string () != "") { + search_path = param.get_string (); + } else { + search_path = default_globalsearch_path; + } + + if (search_path != "") { + folder_manager_view.search_global (search_path, term); + } else { + // Fallback to standard search + warning ("Unable to perform global search - search document instead"); + action_fetch (action, param); + } } private void update_find_actions () { @@ -1232,9 +1261,9 @@ namespace Scratch { Utils.action_from_group (ACTION_SHOW_FIND, actions).set_enabled (is_current_doc); Utils.action_from_group (ACTION_FIND_NEXT, actions).set_enabled (is_current_doc); Utils.action_from_group (ACTION_FIND_PREVIOUS, actions).set_enabled (is_current_doc); + var can_global_search = is_current_doc || git_manager.active_project_path != null; + Utils.action_from_group (ACTION_FIND_GLOBAL, actions).set_enabled (can_global_search); - var is_active_project = git_manager.active_project_path != ""; - Utils.action_from_group (ACTION_FIND_GLOBAL, actions).set_enabled (is_active_project); return Source.REMOVE; }); } diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 2505959e16..02752e0dfd 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -65,6 +65,16 @@ namespace Scratch.Services { } } + public string project_path { + owned get { + if (source_view.project != null) { + return source_view.project.path; + } else { + return ""; + } + } + } + private string _title = ""; public string title { get { return _title; } diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 6001473cf4..3b43bb88aa 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -44,9 +44,10 @@ namespace Scratch.Services { return instance; } - private GitManager () { + construct { // Used to populate the ChooseProject popover in sorted order project_liststore = new ListStore (typeof (FolderManager.ProjectFolderItem)); + settings.bind ("active-project-path", this, "active-project-path", DEFAULT); } public MonitoredRepository? add_project (FolderManager.ProjectFolderItem root_folder) { @@ -72,8 +73,7 @@ namespace Scratch.Services { ); } - //Ensure active_project_path always set - active_project_path = root_path; + // No longer need to set default project (restored from settings or left unset) return project_gitrepo_map.@get (root_path); } diff --git a/src/SymbolPane/C/CtagsSymbolOutline.vala b/src/SymbolPane/C/CtagsSymbolOutline.vala index bcaf86e00a..97c06f0d97 100644 --- a/src/SymbolPane/C/CtagsSymbolOutline.vala +++ b/src/SymbolPane/C/CtagsSymbolOutline.vala @@ -42,7 +42,11 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin } construct { store.item_selected.connect ((selected) => { + if (selected == null) { + return; + } doc.goto (((CtagsSymbol)selected).line); + store.selected = null; }); } diff --git a/src/SymbolPane/Vala/ValaSymbolOutline.vala b/src/SymbolPane/Vala/ValaSymbolOutline.vala index f33520a509..1131d4317f 100644 --- a/src/SymbolPane/Vala/ValaSymbolOutline.vala +++ b/src/SymbolPane/Vala/ValaSymbolOutline.vala @@ -48,7 +48,12 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline resolver = new Code.Plugins.ValaSymbolResolver (); store.item_selected.connect ((selected) => { + if (selected == null) { + return; + } + doc.goto (((ValaSymbolItem)selected).symbol.source_reference.begin.line); + store.selected = null; }); doc.doc_closed.connect (doc_closed); diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 79f597435f..a16ec37ee6 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -18,18 +18,9 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { private const string NO_PROJECT_SELECTED = N_("No Project Selected"); + private const string PROJECT_TOOLTIP = N_("Active Git Project: %s"); private Gtk.Label label_widget; private Gtk.ListBox project_listbox; - public unowned Gtk.RadioButton? group_source { - get { - var first_row = project_listbox.get_row_at_index (0); - if (first_row != null) { - return ((ProjectRow)first_row).project_radio; - } else { - return null; - } - } - } public signal void project_chosen (); @@ -44,8 +35,6 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { xalign = 0.0f }; - tooltip_text = _("Active Git project: %s").printf (_(NO_PROJECT_SELECTED)); - var grid = new Gtk.Grid () { halign = Gtk.Align.START }; @@ -97,6 +86,7 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { popover = project_popover; var git_manager = Scratch.Services.GitManager.get_instance (); + git_manager.project_liststore.items_changed.connect ((src, pos, n_removed, n_added) => { var rows = project_listbox.get_children (); for (int index = (int)pos; index < pos + n_removed; index++) { @@ -111,89 +101,81 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { project_listbox.insert (row, index); } } - - set_active_path (git_manager.active_project_path); - }); - - git_manager.notify["active-project-path"].connect (() => { - set_active_path (git_manager.active_project_path); }); project_listbox.remove.connect ((row) => { var project_row = row as ProjectRow; var current_project = Scratch.Services.GitManager.get_instance ().active_project_path; if (project_row.project_path == current_project) { - label_widget.label = _(NO_PROJECT_SELECTED); - label_widget.tooltip_text = _("Active Git project: %s").printf (_(NO_PROJECT_SELECTED)); Scratch.Services.GitManager.get_instance ().active_project_path = ""; + // Label and active_path will be updated automatically } }); project_listbox.row_activated.connect ((row) => { var project_entry = ((ProjectRow) row); - label_widget.label = project_entry.project_name; - var tooltip_text = Scratch.Utils.replace_home_with_tilde (project_entry.project_path); - label_widget.tooltip_text = _("Active Git project: %s").printf (tooltip_text); - Scratch.Services.GitManager.get_instance ().active_project_path = project_entry.project_path; - project_entry.active = true; + git_manager.active_project_path = project_entry.project_path; project_chosen (); }); + + realize.connect (() => { + set_active_path (git_manager.active_project_path); + git_manager.notify["active-project-path"].connect (() => { + // Sync menubutton states + set_active_path (git_manager.active_project_path); + // Signal window to update as required (e.g. terminal) + project_chosen (); + }); + }); } + // Set appearance (only) of project chooser button and list according to active path private void set_active_path (string active_path) { foreach (var child in project_listbox.get_children ()) { - var project_entry = ((ProjectRow) child); - if (active_path.has_prefix (project_entry.project_path + Path.DIR_SEPARATOR_S)) { - project_listbox.row_activated (project_entry); - break; - } + var project_row = ((ProjectRow) child); + // All paths must not end in directory separator so can be compared directly + project_row.active = active_path == project_row.project_path; + } + + if (active_path != "") { + label_widget.label = Path.get_basename (active_path); + tooltip_text = _(PROJECT_TOOLTIP).printf (Scratch.Utils.replace_home_with_tilde (active_path)); + } else { + label_widget.label = Path.get_basename (_(NO_PROJECT_SELECTED)); + tooltip_text = _(PROJECT_TOOLTIP).printf (_(NO_PROJECT_SELECTED)); } } private Gtk.Widget create_project_row (Scratch.FolderManager.ProjectFolderItem project_folder) { - var project_row = new ProjectRow (project_folder.file.file.get_path (), group_source); - // Handle renaming of project; - project_folder.bind_property ("name", project_row.project_radio, "label", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE, - (binding, srcval, ref targetval) => { - var label = srcval.get_string (); - targetval.set_string (label); - if (project_row.active) { - label_widget.label = label; - } - - return true; - } - ); + var project_path = project_folder.file.file.get_path (); + var project_row = new ProjectRow (project_path); + // Project folder items cannot be renamed in UI, no need to handle return project_row; } public class ProjectRow : Gtk.ListBoxRow { + private Gtk.CheckButton check_button; public bool active { get { - return project_radio.active; + return check_button.active; } set { - if (value) { - project_radio.active = true; - } + check_button.active = value; } } public string project_path { get; construct; } public string project_name { get { - return project_radio.label; + return check_button.label; } } - public Gtk.RadioButton project_radio { get; construct; } - - public ProjectRow (string project_path, Gtk.RadioButton? group_source ) { + public ProjectRow (string project_path) { Object ( - project_path: project_path, - project_radio: new Gtk.RadioButton.with_label_from_widget (group_source, Path.get_basename (project_path)) + project_path: project_path ); } @@ -202,8 +184,9 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { } construct { - add (project_radio); - project_radio.button_release_event.connect (() => { + check_button = new Gtk.CheckButton.with_label (Path.get_basename (project_path)); + add (check_button); + check_button.button_release_event.connect (() => { activate (); return Gdk.EVENT_PROPAGATE; }); diff --git a/src/Widgets/SourceList/SourceList.vala b/src/Widgets/SourceList/SourceList.vala index 0ecff6daea..06fdcc783b 100644 --- a/src/Widgets/SourceList/SourceList.vala +++ b/src/Widgets/SourceList/SourceList.vala @@ -870,7 +870,11 @@ public class SourceList : Gtk.ScrolledWindow { return items.has_key (item); } - public void update_item (Item item) requires (has_item (item)) { + public void update_item (Item item) { + if (!has_item (item)) { + return; + } + assert (root != null); // Emitting row_changed() for this item's row in the child model causes the filter diff --git a/src/Widgets/SourceView.vala b/src/Widgets/SourceView.vala index 2759b98bfe..88a772792c 100644 --- a/src/Widgets/SourceView.vala +++ b/src/Widgets/SourceView.vala @@ -31,21 +31,20 @@ namespace Scratch.Widgets { public FolderManager.ProjectFolderItem project { get; set; default = null; } private string font; - private uint selection_changed_timer = 0; private uint size_allocate_timer = 0; private Gtk.TextIter last_select_start_iter; private Gtk.TextIter last_select_end_iter; - private string selected_text = ""; + private string prev_selected_text = ""; private SourceGutterRenderer git_diff_gutter_renderer; - private const uint THROTTLE_MS = 400; + private const uint SIZE_ALLOCATION_THROTTLE_MS = 400; + private const uint SELECTION_CHANGE_THROTTLE_MS = 100; private double total_delta = 0; private const double SCROLL_THRESHOLD = 1.0; public signal void style_changed (Gtk.SourceStyleScheme style); // "selection_changed" signal now only emitted when the selected text changes (position ignored). Listened to by searchbar and highlight word selection plugin - public signal void selection_changed (Gtk.TextIter start_iter, Gtk.TextIter end_iter); - public signal void deselected (); + public signal void selection_changed (string selected_text, Gtk.TextIter start_iter, Gtk.TextIter end_iter); //lang can be null, in the case of *No highlight style* aka Normal text public Gtk.SourceLanguage? language { @@ -99,7 +98,9 @@ namespace Scratch.Widgets { var source_buffer = new Gtk.SourceBuffer (null); set_buffer (source_buffer); source_buffer.highlight_syntax = true; - source_buffer.mark_set.connect (on_mark_set); + source_buffer.mark_set.connect (schedule_selection_changed_event); + // Need to handle this signal else not all deselections are detected + source_buffer.mark_deleted.connect (schedule_selection_changed_event); highlight_current_line = true; var draw_spaces_tag = new Gtk.SourceTag ("draw_spaces"); @@ -189,7 +190,7 @@ namespace Scratch.Widgets { size_allocate.connect ((allocation) => { // Throttle for performance if (size_allocate_timer == 0) { - size_allocate_timer = Timeout.add (THROTTLE_MS, () => { + size_allocate_timer = Timeout.add (SIZE_ALLOCATION_THROTTLE_MS, () => { size_allocate_timer = 0; bottom_margin = calculate_bottom_margin (allocation.height); return GLib.Source.REMOVE; @@ -539,14 +540,12 @@ namespace Scratch.Widgets { /* Draw spaces in selection the same way if drawn at all */ if (selection && draw_spaces_state in (ScratchDrawSpacesState.FOR_SELECTION | ScratchDrawSpacesState.CURRENT | ScratchDrawSpacesState.ALWAYS)) { - buffer.apply_tag_by_name ("draw_spaces", start, end); return; } if (draw_spaces_state == ScratchDrawSpacesState.CURRENT && get_current_line (out start, out end)) { - buffer.apply_tag_by_name ("draw_spaces", start, end); } } @@ -597,49 +596,37 @@ namespace Scratch.Widgets { return (int) (height_in_px - (LINES_TO_KEEP * px_per_line)); } - void on_mark_set (Gtk.TextIter loc, Gtk.TextMark mar) { - // Weed out user movement for text selection changes - Gtk.TextIter start, end; - buffer.get_selection_bounds (out start, out end); - - if (start.equal (last_select_start_iter) && end.equal (last_select_end_iter)) { - return; - } - - last_select_start_iter.assign (start); - last_select_end_iter.assign (end); + private bool continue_selection_timer = false; + private uint selection_changed_timer = 0; + private void schedule_selection_changed_event () { + // Update spaces immediately to maintain previous behaviour update_draw_spaces (); if (selection_changed_timer != 0) { - Source.remove (selection_changed_timer); - selection_changed_timer = 0; + continue_selection_timer = true; + return; } - // Fire deselected immediately - if (start.equal (end)) { - deselected (); - // Don't fire signal till we think select movement is done - } else { - selection_changed_timer = Timeout.add (THROTTLE_MS, selection_changed_event); - } + selection_changed_timer = Timeout.add (SELECTION_CHANGE_THROTTLE_MS, () => { + if (continue_selection_timer) { + continue_selection_timer = false; + return Source.CONTINUE; + } - } + selection_changed_timer = 0; + Gtk.TextIter start, end; + var selected_text = ""; + if (buffer.get_selection_bounds (out start, out end)) { + selected_text = buffer.get_text (start, end, true); + } - bool selection_changed_event () { - Gtk.TextIter start, end; - bool selected = buffer.get_selection_bounds (out start, out end); - if (selected) { - var prev_selected_text = selected_text; - selected_text = buffer.get_text (start, end, true); if (selected_text != prev_selected_text) { - selection_changed (start, end); + selection_changed (selected_text, start, end); } - } else { - deselected (); - } - selection_changed_timer = 0; - return false; + prev_selected_text = selected_text; + return Source.REMOVE; + }); } uint refresh_diff_timeout_id = 0;