Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions data/io.elementary.code.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,19 @@
<description>Opened folders that should be restored in startup.</description>
</key>
</schema>

<!--Relocatable schema for per project info-->
<schema id="io.elementary.code.Projects">
<key name="expanded" type="b">
<default>false</default>
<summary>Project Folder expanded</summary>
<description>Whether the project folder in the sidebar should be expanded on loading</description>
</key>
<key name="open-files" type="a(si)">
<default>[]</default>
<summary>Open files in project with cursor position</summary>
<description>Array of paths to files currently being edited in project with the current cursor position (char offset)</description>
</key>
</schema>

</schemalist>
10 changes: 10 additions & 0 deletions src/FolderManager/FileView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane
foreach (unowned string path in settings.get_strv ("opened-folders")) {
yield add_folder (new File (path), false);
}

set_project_active (Scratch.Services.GitManager.get_instance ().active_project_path);
}

public void open_folder (File folder) {
Expand Down Expand Up @@ -577,7 +579,10 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane

folder_root.expanded = expand;
folder_root.closed.connect (() => {
ProjectInfoManager.prepare_close_project (folder_root);

toplevel_action_group.activate_action (MainWindow.ACTION_CLOSE_PROJECT_DOCS, new Variant.string (folder_root.path));

root.remove (folder_root);
foreach (var child in root.children) {
var child_folder = (ProjectFolderItem) child;
Expand All @@ -589,6 +594,11 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane
write_settings ();
});

//Make all docs restorable - those of the active project will be loaded later
ProjectInfoManager.get_project_info (folder_root).get_open_file_infos ((uri, pos) => {
//TODO Make cursor pos restorable
Scratch.Services.DocumentManager.get_instance ().make_uri_restorable (folder_root.path, uri);
});
write_settings ();
add_folder.callback ();
return Source.REMOVE;
Expand Down
36 changes: 19 additions & 17 deletions src/FolderManager/ProjectFolderItem.vala
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,6 @@ namespace Scratch.FolderManager {
modified_icon = new ThemedIcon ("emblem-git-modified-symbolic");
}

private void branch_or_name_changed () {
if (monitored_repo != null) {
//As SourceList items are not widgets we have to use markup to change appearance of text.
if (monitored_repo.head_is_branch) {
markup = "%s\n<span size='small' weight='normal'>%s</span>".printf (
name, monitored_repo.branch_name
);
} else { //Distinguish detached heads visually
markup = "%s\n <span size='small' weight='normal' style='italic'>%s</span>".printf (
name, monitored_repo.branch_name
);
}

checkout_local_branch_action.set_state (monitored_repo.branch_name);
}
}

construct {
monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this);
notify["name"].connect (branch_or_name_changed);
Expand All @@ -95,6 +78,25 @@ namespace Scratch.FolderManager {
checkout_local_branch_action.activate.connect (handle_checkout_local_branch_action);
checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action);
}

ProjectInfoManager.get_project_info (this);
}

private void branch_or_name_changed () {
if (monitored_repo != null) {
//As SourceList items are not widgets we have to use markup to change appearance of text.
if (monitored_repo.head_is_branch) {
markup = "%s\n<span size='small' weight='normal'>%s</span>".printf (
name, monitored_repo.branch_name
);
} else { //Distinguish detached heads visually
markup = "%s\n <span size='small' weight='normal' style='italic'>%s</span>".printf (
name, monitored_repo.branch_name
);
}

checkout_local_branch_action.set_state (monitored_repo.branch_name);
}
}

protected override void on_changed (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) {
Expand Down
139 changes: 139 additions & 0 deletions src/FolderManager/ProjectInfoManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2026 elementary, Inc. <https://elementary.io>
*
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
*/

// Using static methods unless we find we require singleton
// We keep a separate list of project infos for future use in e.g. recently closed project list
// and for re-opening projects without recreating info object
public class Scratch.FolderManager.ProjectInfoManager : Object {
public class ProjectInfo : Object {
const string PROJECT_INFO_SCHEMA_ID = "io.elementary.code.Projects";
const string PROJECT_INFO_SCHEMA_PATH_PREFIX = "/io/elementary/code/Projects/";
public string path {
owned get {
return project.path;
}
}

public ProjectFolderItem project { get; construct; }
private Settings project_settings;

public ProjectInfo (ProjectFolderItem project) {
Object (
project: project
);
}

construct {
var settings_path = PROJECT_INFO_SCHEMA_PATH_PREFIX +
schema_name_from_path (path) +
Path.DIR_SEPARATOR_S;

project_settings = new Settings.with_path (
PROJECT_INFO_SCHEMA_ID,
settings_path
);

settings.bind ("expanded", project, "expanded", DEFAULT);
}

public void save_doc_info () {
var doc_manager = Scratch.Services.DocumentManager.get_instance ();
var vb = new VariantBuilder (new VariantType ("a(si)"));
// This will erase any existing open-files setting if history is off
if (privacy_settings.get_boolean ("remember-recent-files")) {
//NOTE `foreach (var x in y) {}` syntax does not work here!
doc_manager.get_open_paths (path).@foreach ((path) => {
//Need to save cursor position in Document Manager
//Default to 0 for now
//Assume path exists for now (do we need check - it might disappear later anyway)
vb.add ("(si)", path, 0);
return true;
});

doc_manager.take_restorable_paths (path).@foreach ((path) => {
//Need to save cursor position in Document Manager
//Default to 0 for now
//Assume path exists for now (do we need check - it might disappear later anyway)
vb.add ("(si)", path, 0);
return true;
});
}

project_settings.set_value ("open-files", vb.end ());
}

//Combine basename and parent folder name and convert to camelcase
public delegate void OpenFileCallback (string uri, uint pos);
public void get_open_file_infos (OpenFileCallback cb) {
if (privacy_settings.get_boolean ("remember-recent-files")) {
var doc_infos = project_settings.get_value ("open-files");
var doc_info_iter = new VariantIter (doc_infos);
//TODO Restore focused doc per project
string uri;
int pos;
while (doc_info_iter.next ("(si)", out uri, out pos)) {
cb (uri, pos);
}
}
}

//Combine basename and parent folder name and convert to camelcase
private string schema_name_from_path (string path) {
var dir = Path.get_basename (Path.get_dirname (path)).normalize ();
var basename = Path.get_basename (path).normalize ();
var name = dir.substring (0, 1).up () +
dir.substring (1, -1).down () +
basename.substring (0, 1).up () +
basename.substring (1, -1).down ();

return name;
}
}

private static Gee.HashMap<string, ProjectInfo>? map = null;
private static Gee.HashMap<string, ProjectInfo> project_info_map {
get {
if (map == null) {
map = new Gee.HashMap<string, ProjectInfo> ();
}

return map;
}
}

//Called when folder created
public static ProjectInfo get_project_info (ProjectFolderItem project_folder) {
//TODO Should we only store info for code (git) projects?
var info = project_info_map[project_folder.path];
if (info == null) {
info = new ProjectInfo (project_folder);
project_info_map[project_folder.path] = info;
}

return info;
}

//Called when a project closed
//We do not bind open doc info so update settings here
//TODO closed doc info too?
public static void prepare_close_project (ProjectFolderItem project_folder) {
var info = project_info_map[project_folder.path];
if (info != null) {
info.save_doc_info ();
}

//We keep info in case project re-opened
}

public static void prepare_to_quit () {
foreach (var info in project_info_map.values) {
info.save_doc_info ();
}

//Assume destructor will take care of map when app closes?
}
}
57 changes: 4 additions & 53 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -649,54 +649,6 @@ namespace Scratch {
}
}

private async void restore_opened_documents () {
File? focused_file = null;
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 focused_uri = 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;
if (Uri.parse_scheme (uri) != null) {
file = File.new_for_uri (uri);
} else {
file = File.new_for_commandline_arg (uri);
}
/* Leave it to doc to handle problematic files properly
But for files that do not exist we need to make sure that doc won't create a new file
*/
if (file.query_exists ()) {
var is_focused = uri == focused_uri;
if (is_focused) {
focused_file = file;
}
//TODO Check files valid (settings could have been manually altered)
var doc = new Scratch.Services.Document (actions, file);
if (doc.exists () || !doc.is_file_temporary) {
if (restore_override != null && (file.get_path () == restore_override.file.get_path ())) {
yield open_document_at_selected_range (doc, true, restore_override.range, true);
was_restore_overriden = true;
} else {
yield open_document (doc, was_restore_overriden ? false : is_focused, pos);
}
}
}
}
}
}

document_view.request_placeholder_if_empty ();
document_view.update_outline_visible ();
restore_override = null;
if (focused_file != null) {
folder_manager_view.expand_to_path (focused_file.get_path ());
}
}

private bool on_key_pressed (uint keyval, uint keycode, Gdk.ModifierType state) {
switch (Gdk.keyval_name (keyval)) {
case "Escape":
Expand Down Expand Up @@ -818,12 +770,10 @@ namespace Scratch {
// Plugin panes size
hp1.set_position (Scratch.saved_state.get_int ("hp1-size"));
vp.set_position (Scratch.saved_state.get_int ("vp-size"));
// Ensure foldermanager finishes loading projects before start opening documents
folder_manager_view.restore_saved_state.begin ((obj, res) => {
folder_manager_view.restore_saved_state.end (res);
if (restore_docs) {
restore_opened_documents.begin ();
}
document_view.request_placeholder_if_empty ();
document_view.update_outline_visible ();
});
}

Expand Down Expand Up @@ -870,6 +820,7 @@ namespace Scratch {
// For exit cleanup
private void handle_quit () {
document_view.save_opened_files ();
Scratch.FolderManager.ProjectInfoManager.prepare_to_quit ();
update_saved_state ();
}

Expand Down Expand Up @@ -1456,7 +1407,7 @@ namespace Scratch {
var project_path = param.get_string ();
if (folder_manager_view.project_is_open (project_path)) {
git_manager.active_project_path = project_path;
folder_manager_view.collapse_other_projects ();
folder_manager_view.collapse_other_projects (); // triggers restoring project doc action
//The opened folders are not changed so no need to update "opened-folders" setting
} else {
warning ("Attempt to set folder path %s which is not opened as active project ignored", project_path);
Expand Down
11 changes: 10 additions & 1 deletion src/Services/DocumentManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
}

public void make_restorable (Document doc) {
project_restorable_docs_map.@set (doc.source_view.project.path, doc.file.get_path ());
make_uri_restorable (doc.source_view.project.path, doc.file.get_path ());
}

public void make_uri_restorable (string project_path, string uri) {
project_restorable_docs_map.@set (project_path, uri);
}

public void add_open_document (Document doc) {
Expand Down Expand Up @@ -66,6 +70,11 @@
return docs;
}

public Gee.Collection<string> get_open_paths (string project_path) {
var docs = project_open_docs_map.@get (project_path);
return docs;
}

public uint restorable_for_project (string project_path) {
return project_restorable_docs_map.@get (project_path).size;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Services/LocationJumpManager.vala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2023 elementary, Inc. <https://elementary.io>
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2023 elementary, Inc. <https://elementary.io>
*
* Authored by: Colin Kiama <colinkiama@gmail.com>
*/
Expand Down
Loading