diff --git a/src/Application.vala b/src/Application.vala index 2c02f652e..8473dd563 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -32,11 +32,13 @@ namespace Scratch { private static string _data_home_folder_unsaved; private static bool create_new_tab = false; private static bool create_new_window = false; + private LocationJumpManager location_jump_manager; const OptionEntry[] ENTRIES = { { "new-tab", 't', 0, OptionArg.NONE, null, N_("New Tab"), null }, { "new-window", 'n', 0, OptionArg.NONE, null, N_("New Window"), null }, { "version", 'v', 0, OptionArg.NONE, null, N_("Print version info and exit"), null }, + { "go-to", 'g', 0, OptionArg.STRING, null, "Open file at specified selection range", "" }, { GLib.OPTION_REMAINING, 0, 0, OptionArg.FILENAME_ARRAY, null, null, N_("[FILEā€¦]") }, { null } }; @@ -46,6 +48,7 @@ namespace Scratch { _data_home_folder_unsaved = Path.build_filename ( Environment.get_user_data_dir (), Constants.PROJECT_NAME, "unsaved" ); + } construct { @@ -66,6 +69,7 @@ namespace Scratch { service_settings = new GLib.Settings (Constants.PROJECT_NAME + ".services"); privacy_settings = new GLib.Settings ("org.gnome.desktop.privacy"); + location_jump_manager = new LocationJumpManager (); Environment.set_variable ("GTK_USE_PORTAL", "1", true); GLib.Intl.setlocale (LocaleCategory.ALL, ""); @@ -94,6 +98,7 @@ namespace Scratch { }; var options = command_line.get_options_dict (); + location_jump_manager.clear (); if (options.contains ("new-tab")) { create_new_tab = true; @@ -103,6 +108,25 @@ namespace Scratch { create_new_window = true; } + if (options.contains ("go-to")) { + var go_to_string_variant = options.lookup_value ("go-to", GLib.VariantType.STRING); + string selection_range_string = (string) go_to_string_variant.get_string (); + location_jump_manager.parse_selection_range_string (selection_range_string); + debug ("go-to arg value: %s", selection_range_string); + } + + if (location_jump_manager.has_selection_range () && options.contains (GLib.OPTION_REMAINING)) { + (unowned string)[] file_list = options.lookup_value ( + GLib.OPTION_REMAINING, + VariantType.BYTESTRING_ARRAY + ).get_bytestring_array (); + + if (file_list.length == 1) { + unowned string selection_range_file_path = file_list[0]; + location_jump_manager.file = command_line.create_file_for_arg (selection_range_file_path); + } + } + activate (); if (options.contains (GLib.OPTION_REMAINING)) { @@ -126,7 +150,12 @@ namespace Scratch { protected override void activate () { if (active_window == null) { - add_window (new MainWindow (true)); // Will restore documents if required + if (location_jump_manager.has_selection_range () && location_jump_manager.has_override_target ()) { + RestoreOverride restore_override = location_jump_manager.create_restore_override (); + add_window (new MainWindow.with_restore_override (true, restore_override)); + } else { + add_window (new MainWindow (true)); // Will restore documents if required + } } else if (create_new_window) { create_new_window = false; add_window (new MainWindow (false)); // Will NOT restore documents in additional windows @@ -143,15 +172,19 @@ namespace Scratch { protected override void open (File[] files, string hint) { var window = get_last_window (); - foreach (var file in files) { bool is_folder; if (Scratch.Services.FileHandler.can_open_file (file, out is_folder)) { if (is_folder) { window.open_folder (file); } else { + debug ("Files length: %d\n", files.length); var doc = new Scratch.Services.Document (window.actions, file); - window.open_document (doc); + if (location_jump_manager.has_selection_range != null && files.length == 1) { + window.open_document_at_selected_range (doc, true, location_jump_manager.range); + } else { + window.open_document (doc); + } } } } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 42b79f833..21ae93280 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -26,6 +26,7 @@ namespace Scratch { public Scratch.Application app { get; private set; } public bool restore_docs { get; construct; } + public RestoreOverride restore_override { get; construct set; } public Scratch.Widgets.DocumentView document_view; @@ -158,6 +159,14 @@ namespace Scratch { ); } + public MainWindow.with_restore_override (bool restore_docs, RestoreOverride restore_override) { + Object ( + icon_name: Constants.PROJECT_NAME, + restore_docs: restore_docs, + restore_override: restore_override + ); + } + static construct { action_accelerators.set (ACTION_FIND + "::", "f"); action_accelerators.set (ACTION_FIND_NEXT, "g"); @@ -594,6 +603,7 @@ namespace Scratch { string focused_document = settings.get_string ("focused-document"); string uri; int pos; + bool was_restore_overriden = false; while (doc_info_iter.next ("(si)", out uri, out pos)) { if (uri != "") { GLib.File file; @@ -610,7 +620,12 @@ namespace Scratch { var doc = new Scratch.Services.Document (actions, file); bool is_focused = file.get_uri () == focused_document; if (doc.exists () || !doc.is_file_temporary) { - open_document (doc, is_focused, pos); + if (restore_override != null && (file.get_path () == restore_override.file.get_path ())) { + open_document_at_selected_range (doc, true, restore_override.range, true); + was_restore_overriden = true; + } else { + open_document (doc, was_restore_overriden ? false : is_focused, pos); + } } if (is_focused) { //Maybe expand to show all opened documents? @@ -623,6 +638,7 @@ namespace Scratch { Idle.add (() => { document_view.request_placeholder_if_empty (); + restore_override = null; return Source.REMOVE; }); } @@ -686,6 +702,19 @@ namespace Scratch { document_view.open_document (doc, focus, cursor_position); } + public void open_document_at_selected_range (Scratch.Services.Document doc, + bool focus = true, + SelectionRange range = SelectionRange.EMPTY, + bool is_override = false) { + if (restore_override != null && is_override == false) { + return; + } + + FolderManager.ProjectFolderItem? project = folder_manager_view.get_project_for_file (doc.file); + doc.source_view.project = project; + document_view.open_document (doc, focus, 0, range); + } + // Close a document public void close_document (Scratch.Services.Document doc) { document_view.close_document (doc); diff --git a/src/Services/LocationJumpManager.vala b/src/Services/LocationJumpManager.vala new file mode 100644 index 000000000..d4423210f --- /dev/null +++ b/src/Services/LocationJumpManager.vala @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Colin Kiama + */ + +namespace Scratch { + public class LocationJumpManager : GLib.Object { + public GLib.File file { get; set; } + public SelectionRange range { get; set; } + + public bool has_override_target () { + if (file == null) { + return false; + } + + bool is_override_target = false; + + if (privacy_settings.get_boolean ("remember-recent-files")) { + var doc_infos = settings.get_value ("opened-files"); + var doc_info_iter = new VariantIter (doc_infos); + + string uri; + int pos; + while (doc_info_iter.next ("(si)", out uri, out pos)) { + if (uri != "") { + GLib.File file_to_restore; + if (Uri.parse_scheme (uri) != null) { + file_to_restore = File.new_for_uri (uri); + } else { + file_to_restore = File.new_for_commandline_arg (uri); + } + + if (file_to_restore.query_exists () && file_to_restore.get_path () == file.get_path ()) { + is_override_target = true; + break; + } + } + } + } + + return is_override_target; + } + + public RestoreOverride create_restore_override () { + return new RestoreOverride (file, range); + } + + public void clear () { + range = SelectionRange.EMPTY; + file = null; + } + + public bool has_selection_range () { + return range != SelectionRange.EMPTY; + } + + public bool parse_selection_range_string (string selection_range_string) { + Regex go_to_line_regex = /^(?[0-9]+)+(?:\.(?[0-9]+)+)?(?:-(?:(?[0-9]+)+(?:\.(?[0-9]+)+)?))?$/; // vala-lint=space-before-paren, line-length + MatchInfo match_info; + if (go_to_line_regex.match (selection_range_string, 0, out match_info)) { + range = parse_go_to_range_from_match_info (match_info); + debug ("Selection Range - start_line: %d", range.start_line); + debug ("Selection Range - start_column: %d", range.start_column); + debug ("Selection Range - end_line: %d", range.end_line); + debug ("Selection Range - end_column: %d", range.end_column); + } + + return true; + } + + private static SelectionRange parse_go_to_range_from_match_info (GLib.MatchInfo match_info) { + return SelectionRange () { + start_line = parse_num_from_match_info (match_info, "start_line"), + end_line = parse_num_from_match_info (match_info, "end_line"), + start_column = parse_num_from_match_info (match_info, "start_column"), + end_column = parse_num_from_match_info (match_info, "end_column"), + }; + } + + private static int parse_num_from_match_info (MatchInfo match_info, string match_name) { + var str = match_info.fetch_named (match_name); + int num = 0; + + if (str != null) { + int.try_parse (str, out num); + } + + return num; + } + } +} diff --git a/src/Services/RestoreOverride.vala b/src/Services/RestoreOverride.vala new file mode 100644 index 000000000..cd1f21ad4 --- /dev/null +++ b/src/Services/RestoreOverride.vala @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Colin Kiama + */ + +public class RestoreOverride : GLib.Object { + public GLib.File file { get; construct; } + public SelectionRange range { get; construct; } + + public RestoreOverride (GLib.File file, SelectionRange range) { + Object ( + file: file, + range: range + ); + } +} diff --git a/src/Structs/SelectionRange.vala b/src/Structs/SelectionRange.vala new file mode 100644 index 000000000..81e33cddd --- /dev/null +++ b/src/Structs/SelectionRange.vala @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Colin Kiama + */ + +public struct SelectionRange { + public int start_line; + public int start_column; + public int end_line; + public int end_column; + + public const SelectionRange EMPTY = {0, 0, 0, 0}; +} diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index f76831259..a6e51bc24 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -213,7 +213,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } } - public void open_document (Services.Document doc, bool focus = true, int cursor_position = 0) { + public void open_document (Services.Document doc, bool focus = true, int cursor_position = 0, SelectionRange range = SelectionRange.EMPTY) { for (int n = 0; n <= docs.length (); n++) { var nth_doc = docs.nth_data (n); if (nth_doc == null) { @@ -226,6 +226,15 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } debug ("This Document was already opened! Not opening a duplicate!"); + if (range != SelectionRange.EMPTY) { + Idle.add_full (GLib.Priority.LOW, () => { // This helps ensures new tab is drawn before opening document. + current_document.source_view.select_range (range); + save_opened_files (); + + return false; + }); + } + return; } } @@ -242,9 +251,12 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { doc.focus (); } - if (cursor_position > 0) { + if (range != SelectionRange.EMPTY) { + doc.source_view.select_range (range); + } else if (cursor_position > 0) { doc.source_view.cursor_position = cursor_position; } + save_opened_files (); }); diff --git a/src/Widgets/SourceView.vala b/src/Widgets/SourceView.vala index bb8bab160..5589de04f 100644 --- a/src/Widgets/SourceView.vala +++ b/src/Widgets/SourceView.vala @@ -476,6 +476,35 @@ namespace Scratch.Widgets { buffer.end_user_action (); } + public void select_range (SelectionRange range) { + if (range.start_line < 0) { + return; + } + + Gtk.TextIter start_iter; + buffer.get_start_iter (out start_iter); + start_iter.set_line (range.start_line - 1); + + if (range.start_column > 0) { + start_iter.set_visible_line_offset (range.start_column - 1); + } + + Gtk.TextIter end_iter = start_iter.copy (); + if (range.end_line > 0) { + end_iter.set_line (range.end_line - 1); + + if (range.end_column > 0) { + end_iter.set_visible_line_offset (range.end_column - 1); + } + } + + buffer.select_range (start_iter, end_iter); + Idle.add (() => { + scroll_to_iter (end_iter, 0.25, false, 0, 0); + return Source.REMOVE; + }); + } + public void set_text (string text, bool opening = true) { var source_buffer = (Gtk.SourceBuffer) buffer; if (opening) { diff --git a/src/meson.build b/src/meson.build index ff454644d..bdafbfe28 100644 --- a/src/meson.build +++ b/src/meson.build @@ -34,8 +34,10 @@ code_files = files( 'Services/DocumentManager.vala', 'Services/FileHandler.vala', 'Services/GitManager.vala', + 'Services/LocationJumpManager.vala', 'Services/MonitoredRepository.vala', 'Services/PluginManager.vala', + 'Services/RestoreOverride.vala', 'Services/Settings.vala', 'Services/TemplateManager.vala', 'Widgets/ChooseProjectButton.vala', @@ -57,7 +59,7 @@ code_files = files( 'SymbolPane/C/CtagsSymbol.vala', 'SymbolPane/C/CtagsSymbolIter.vala', 'SymbolPane/C/CtagsSymbolOutline.vala', - + 'Structs/SelectionRange.vala' ) executable(