Skip to content
Open
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ What follows is how TuiCode implements it today.
- **A tab is dirty only while saving would change the file (#550).** `EditorTab` keeps the text it last read or wrote and, after every edit, compares it with what `Save` would write, line endings and final newline included, so undo, redo or retyping back to it clears `●` and every prompt that reads `IsDirty`. `MatchesSaved` compares lengths first, then the lines in place, so typing in a large file builds no string. A file lacking a final newline therefore stays dirty after edit-and-undo while *Insert final newline* is on, since saving would add one.
- **Closing a dirty tab asks first (#409).** `Ctrl+W` / `cf` on an `EditorTab` with unsaved edits asks: `Ctrl+Enter Save`, `D` Don't save, `Esc Cancel`. Save goes through the same #267 check as `Ctrl+S`; over a changed file the conflict prompt appears and only **Overwrite** closes the tab. Diff and document tabs close without asking, and the command is enabled on any tab, document tabs included.
- **Close other tabs / Close all tabs (#518)** (`cot`/`cat`, File menu, no default key). Other closes the focused group's `EditorGroup.Others`, every tab but the active one and an active diff's file; the other group is untouched. All closes both groups. Both ask once, with *Quit*'s `AskToSave` dialog, about only the dirty tabs they'd close, and `Workbench` saves the workspace state afterwards, since closing tabs other than the active one changes no `ActiveTab`.
- **Undo close tab (#578)** (`Ctrl+Shift+T`, `uct`, File menu; the id is VS Code's `reopenClosedEditor`, so `reopen` finds it in the palette). `EditorGroups` pushes every file tab `EditorGroup.Close` closes onto `ClosedTabs` (newest last, capped at 20): its group, its index in the strip at that moment, its `ViewPosition` and `CodeEditor.History`. Indices taken as each tab goes are what make *Close other* / *Close all* come back in order one press at a time. A reopen skips, and drops, files open again or gone; it goes into the group it left, or last in the first once the second has gone. The undo steps are put back only when the freshly read lines equal those they were recorded against, so a file changed on disk, or closed dirty with *Don't save*, reopens with fresh history. `EditorGroups.CloseAll` (a folder switch) clears the list. TG's `Tabs` draws no close button, so every close already goes through `EditorGroup.Close`.
- **Reload from disk on demand (#272)** (`rd`, `Editor` scope, no default key) is `DiskChanges.Reload` with `evenIfUnchanged`, so a dirty tab over a file nobody touched still drops its edits. A clean tab just reloads; a dirty one confirms first (`Ctrl+Enter Reload`, Danger); a `Gone` file is refused, since the buffer is the only copy.
- **A watcher hears nothing about its own directory being removed**, so every directory *above* an open file is watched as well, all the way to the root, with `NotifyFilters.DirectoryName` in the filter, and `DiskWatcher` maps a directory event onto every file it follows beneath it. Without both halves, `rm -rf <dir>` around an open file is completely silent on macOS — no file event, no error, the FSEvents stream simply reports the directory and a non-recursive watcher rooted there filters it out. Whichever directory in the chain goes, the watcher one level above it is the one that says so, which is why stopping at the parent wasn't enough. A directory that holds no open file of its own asks for `DirectoryName` and nothing else, so passing through `$HOME` costs no file events.
- **An in-app delete leaves no marker**: `Workbench.Delete` closes the tabs through `EditorGroup.CloseUnder` first, so by the time the watcher's event is flushed the path is neither followed nor open, and both the flush filter and the tab lookup drop it.
Expand Down
1 change: 1 addition & 0 deletions src/TuiCode.Abstractions/CommandIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static class CommandIds
public const string CloseActiveEditor = "workbench.action.closeActiveEditor";
public const string CloseOtherTabs = "workbench.action.closeOtherEditors";
public const string CloseAllTabs = "workbench.action.closeAllEditors";
public const string ReopenClosedEditor = "workbench.action.reopenClosedEditor";
public const string NextEditor = "workbench.action.nextEditor";
public const string PreviousEditor = "workbench.action.previousEditor";
public const string MoveToOtherGroup = "workbench.action.moveEditorToOtherGroup";
Expand Down
1 change: 1 addition & 0 deletions src/TuiCode.Abstractions/CommandMnemonics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private static IReadOnlyDictionary<string, string> Build()
[CommandIds.CloseActiveEditor] = "cf",
[CommandIds.CloseOtherTabs] = "cot",
[CommandIds.CloseAllTabs] = "cat",
[CommandIds.ReopenClosedEditor] = "uct",
[CommandIds.ChangeGrammar] = "cg",
[CommandIds.CompareToSaved] = "cts",
[CommandIds.NextChange] = "nc",
Expand Down
36 changes: 36 additions & 0 deletions src/TuiCode.Editor/ClosedTabs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace TuiCode.Editor;

/// <summary>A file tab as it was when it closed: where it sat, where its cursor was, and its undo history (#578).</summary>
internal sealed record ClosedTab(IFileInfo File, int Group, int Index, ViewPosition Position, CodeEditor.UndoHistory History);

/// <summary>The file tabs closed this session, newest first, for Undo close tab (#578).</summary>
internal sealed class ClosedTabs
{
public const int Max = 20;

private readonly List<ClosedTab> _newestLast = [];

public int Count => _newestLast.Count;

public void Push(ClosedTab tab)
{
_newestLast.Add(tab);
if (_newestLast.Count > Max) _newestLast.RemoveAt(0);
}

public void Clear() => _newestLast.Clear();

public bool Any(Func<IFileInfo, bool> reopenable) => _newestLast.Any(t => reopenable(t.File));

/// <summary>Takes the newest tab <paramref name="reopenable"/> accepts, dropping the newer ones it doesn't.</summary>
public ClosedTab? Take(Func<IFileInfo, bool> reopenable)
{
while (_newestLast.Count > 0)
{
var tab = _newestLast[^1];
_newestLast.RemoveAt(_newestLast.Count - 1);
if (reopenable(tab.File)) return tab;
}
return null;
}
}
19 changes: 17 additions & 2 deletions src/TuiCode.Editor/CodeEditor.History.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ namespace TuiCode.Editor;
// Undo is ours, not Editor's, so a step restores every caret: see "Multiple cursors and undo" in AGENTS.md.
internal sealed partial class CodeEditor
{
private sealed record EditGroup(Change[] Changes, Caret[] Before, Caret[] After);
internal sealed record EditGroup(Change[] Changes, Caret[] Before, Caret[] After);

private readonly record struct Change(int OldStart, string[] Old, int NewStart, string[] New);
internal readonly record struct Change(int OldStart, string[] Old, int NewStart, string[] New);

private readonly List<EditGroup> _undo = [];
private int _undone;
Expand Down Expand Up @@ -78,6 +78,21 @@ private void Record(List<Hunk> hunks, Caret[] before)
Replay(changes, undo: false, applyToDocument: false);
}

/// <summary>The undo and redo steps, for a tab reopened later on the same lines (#578).</summary>
internal UndoHistory History => new([.. _undo], _undone, [.. _recorded]);

internal sealed class UndoHistory(EditGroup[] steps, int undone, string[] lines)
{
/// <summary>Puts the steps back on <paramref name="editor"/>, unless its lines aren't the ones they were recorded against.</summary>
internal void RestoreTo(CodeEditor editor)
{
if (!lines.SequenceEqual(editor._recorded, StringComparer.Ordinal)) return;
editor._undo.Clear();
editor._undo.AddRange(steps);
editor._undone = undone;
}
}

public bool Undo() => Step(undo: true);

public bool Redo() => Step(undo: false);
Expand Down
14 changes: 12 additions & 2 deletions src/TuiCode.Editor/EditorGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ private EditorTab Opening(EditorTab tab)
return tab;
}

/// <summary>Opens <paramref name="closed"/>'s file at <paramref name="index"/>, or last, with its cursor and, while the file is unchanged, its undo (#578).</summary>
internal EditorTab Reopen(ClosedTab closed, int? index)
{
var tab = Opening(new EditorTab(closed.File, _syntax));
closed.History.RestoreTo(tab.Code);
tab.RestoreViewPosition(closed.Position);
return Track(tab, index);
}

/// <summary>Opens a read-only Markdown document that isn't on disk (#185), like a PR's Overview.</summary>
public DocumentTab OpenDocument(IFileInfo file, string content, string? title = null)
{
Expand Down Expand Up @@ -149,7 +158,7 @@ public DocumentTab OpenDocument(IFileInfo file, string content, string? title =
return tab;
}

private EditorTab Track(EditorTab tab)
private EditorTab Track(EditorTab tab, int? index = null)
{
tab.GutterVisible = GutterVisible;
tab.ColumnSelect = ColumnSelect;
Expand All @@ -161,7 +170,8 @@ private EditorTab Track(EditorTab tab)
// see — and announce it before that, so the file is being watched by the time anything reacts to it (#269).
_byPath[tab.File.FullName] = tab;
TabsChanged?.Invoke(this, EventArgs.Empty);
Add(tab);
if (index is { } at) InsertTab(at, tab);
else Add(tab);
Value = tab;
return tab;
}
Expand Down
30 changes: 29 additions & 1 deletion src/TuiCode.Editor/EditorGroups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace TuiCode.Editor;
public sealed class EditorGroups
{
private readonly EditorGroup[] _all;
private readonly ClosedTabs _closed = new();

public event EventHandler<IFileInfo>? FileSaved;
public event EventHandler<CopyOutcome>? Copied;
Expand Down Expand Up @@ -40,7 +41,11 @@ public EditorGroups(EditorGroup first, EditorGroup second)
group.CursorMoved += (_, e) => CursorMoved?.Invoke(this, e);
group.PinnedLineClicked += (_, e) => PinnedLineClicked?.Invoke(this, e);
group.EditorOpening += (_, tab) => EditorOpening?.Invoke(this, tab);
group.EditorClosing += (_, tab) => EditorClosing?.Invoke(this, tab);
group.EditorClosing += (sender, tab) =>
{
Remember((EditorGroup)sender!, tab);
EditorClosing?.Invoke(this, tab);
};
group.ActiveTabChanged += (_, _) =>
{
// A tab opened into the second group lands after its TabsChanged, so the split shows up here.
Expand Down Expand Up @@ -180,6 +185,29 @@ public void CloseAll()
Second.CloseAll();
First.CloseAll();
Focused = First;
_closed.Clear();
}

/// <summary>Whether a closed file tab can come back: one that isn't open again and still exists.</summary>
public bool CanReopen => _closed.Any(Reopenable);

/// <summary>Reopens the newest closed file tab that can come back, in its place, or at the end of the first group once its own has gone; null when none can.</summary>
public EditorTab? ReopenClosed()
{
if (_closed.Take(Reopenable) is not { } closed) return null;
var stillThere = closed.Group == 0 || IsSplit;
Focused = stillThere ? _all[closed.Group] : First;
return Focused.Reopen(closed, stillThere ? closed.Index : null);
}

private bool Reopenable(IFileInfo file) =>
Holding(file.FullName) is null && file.FileSystem.File.Exists(file.FullName);

private void Remember(EditorGroup group, EditorTab tab)
{
var strip = group.TabCollection.ToList();
var index = strip.Take(strip.IndexOf(tab)).Count(t => t is not DiffTab diff || diff.Source != tab);
_closed.Push(new ClosedTab(tab.File, Array.IndexOf(_all, group), index, tab.ViewPosition, tab.History));
}

public void InferGrammars()
Expand Down
2 changes: 2 additions & 0 deletions src/TuiCode.Editor/EditorTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ public void MoveCursorToCharacter(int row, int character)

internal CodeEditor Code => _text;

internal CodeEditor.UndoHistory History => _text.History;

/// <summary>A screen row while wrapped.</summary>
public int TopRow => _text.Viewport.Y;

Expand Down
4 changes: 2 additions & 2 deletions src/TuiCode.Workbench/Menus/CommandMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ .. Enumerable.Range(1, 9).Select(CommandIds.FocusEditorByIndex),
{
[CommandIds.New] = 'n', [CommandIds.Open] = 'o', [CommandIds.OpenRecentFolder] = 'r', [CommandIds.OpenWorktree] = 'w',
[CommandIds.OpenFilePath] = 'f', [CommandIds.SaveActiveEditor] = 's', [CommandIds.SaveAll] = 'a',
[CommandIds.ReloadFromDisk] = 'l', [CommandIds.CloseActiveEditor] = 'c', [CommandIds.CloseOtherTabs] = 'e', [CommandIds.CloseAllTabs] = 'b',
[CommandIds.ReloadFromDisk] = 'l', [CommandIds.CloseActiveEditor] = 'c', [CommandIds.CloseOtherTabs] = 'e', [CommandIds.CloseAllTabs] = 'b', [CommandIds.ReopenClosedEditor] = 'u',
[CommandIds.RenameFile] = 'm', [CommandIds.CutFile] = 't', [CommandIds.PasteFile] = 'p', [CommandIds.DeleteFile] = 'd',
[CommandIds.DeletePermanently] = 'y',
[CommandIds.ShowDocumentInfo] = 'i', [CommandIds.OpenSettings] = 'g', [CommandIds.SuspendToShell] = 'h',
Expand Down Expand Up @@ -73,7 +73,7 @@ public static readonly (string Title, string[] Ids)[] Layout =
[
("_File",
[
CommandIds.New, CommandIds.Open, CommandIds.OpenRecentFolder, CommandIds.OpenWorktree, CommandIds.OpenFilePath, CommandIds.SaveActiveEditor, CommandIds.SaveAll, CommandIds.ReloadFromDisk, CommandIds.CloseActiveEditor, CommandIds.CloseOtherTabs, CommandIds.CloseAllTabs,
CommandIds.New, CommandIds.Open, CommandIds.OpenRecentFolder, CommandIds.OpenWorktree, CommandIds.OpenFilePath, CommandIds.SaveActiveEditor, CommandIds.SaveAll, CommandIds.ReloadFromDisk, CommandIds.CloseActiveEditor, CommandIds.CloseOtherTabs, CommandIds.CloseAllTabs, CommandIds.ReopenClosedEditor,
Separator,
CommandIds.RenameFile, CommandIds.CutFile, CommandIds.PasteFile, CommandIds.DeleteFile, CommandIds.DeletePermanently,
Separator,
Expand Down
8 changes: 8 additions & 0 deletions src/TuiCode.Workbench/Workbench.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ public void OpenFile(IFileInfo file)
FileOpened?.Invoke(this, EventArgs.Empty);
}

/// <summary>Reopens the most recently closed file tab where it was (#578).</summary>
public void ReopenClosedTab()
{
if (Editor.Groups.ReopenClosed() is not { } tab) return;
StatusBar.SetMessage(FileMessage(tab));
FileOpened?.Invoke(this, EventArgs.Empty);
}

/// <summary>Open a file with <paramref name="match"/> selected — where a search result lands.</summary>
public void OpenMatch(IFileInfo file, TextMatch match)
{
Expand Down
4 changes: 4 additions & 0 deletions src/TuiCode.Workbench/WorkbenchHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@

RegisterDefaultCommands();
_menu = new CommandMenu(_workbench.MenuBar, _commands, _keybindings, () => !_workbench.HasDialog,
id => IsAvailableFrom(id, _menu.IsOpen ? _menuOpenedFrom : _focus.Region));

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT osx-arm64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT osx-arm64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT linux-arm64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT win-arm64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT win-x64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT linux-x64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT linux-arm64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT win-x64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT win-arm64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / AOT linux-x64

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / build-test (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / build-test (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / build-test (windows-latest)

Dereference of a possibly null reference.

Check warning on line 202 in src/TuiCode.Workbench/WorkbenchHost.cs

View workflow job for this annotation

GitHub Actions / build-test (windows-latest)

Dereference of a possibly null reference.
_menu.Opened += (_, _) => OnMenuOpened();
_menu.Closed += (_, picked) => OnMenuClosed(picked);
ApplyKeybindings(_settings.KeybindingOverrides);
Expand Down Expand Up @@ -608,6 +608,8 @@
() => Group.Value is not null);
_commands.Register(CommandIds.CloseAllTabs, "Close all tabs", CloseAllTabs, CommandScope.Global,
() => Group.Value is not null);
_commands.Register(CommandIds.ReopenClosedEditor, "Undo close tab", _workbench.ReopenClosedTab, CommandScope.Global,
() => _workbench.Editor.Groups.CanReopen);
_commands.Register(CommandIds.NextEditor, "Next tab", () => CycleTabs(1));
_commands.Register(CommandIds.PreviousEditor, "Previous tab", () => CycleTabs(-1));
_commands.Register(CommandIds.MoveToOtherGroup, "Move to other group", MoveToOtherGroup, CommandScope.Global,
Expand Down Expand Up @@ -862,6 +864,7 @@
.. HelpRowOf(CommandIds.CloseActiveEditor, "Close tab"),
.. HelpRowOf(CommandIds.CloseOtherTabs, "Close other tabs"),
.. HelpRowOf(CommandIds.CloseAllTabs, "Close all tabs"),
.. HelpRowOf(CommandIds.ReopenClosedEditor, "Undo close tab"),
.. GroupHelp(),
]);

Expand Down Expand Up @@ -990,6 +993,7 @@
keybindings.Bind("Ctrl+S", CommandIds.SaveActiveEditor);
keybindings.Bind("Ctrl+Shift+S", CommandIds.SaveAll);
keybindings.Bind("Ctrl+W", CommandIds.CloseActiveEditor);
keybindings.Bind("Ctrl+Shift+T", CommandIds.ReopenClosedEditor);
// Not Ctrl+Tab, which Terminal.app and iTerm2 keep (#254), nor Alt+Tab, Windows' app switcher (#459).
keybindings.Bind("Ctrl+PageDown", CommandIds.NextEditor);
keybindings.Bind("Ctrl+PageUp", CommandIds.PreviousEditor);
Expand Down
60 changes: 60 additions & 0 deletions tests/TuiCode.Tests/ClosedTabsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using TuiCode.Editor;

namespace TuiCode.Tests;

// The list behind Undo close tab (#578).
public sealed class ClosedTabsTests
{
private readonly MockFileSystem _fs = new();
private readonly ClosedTabs _closed = new();

[Fact]
public void Take_gives_the_newest_first()
{
_closed.Push(Closed("a.txt"));
_closed.Push(Closed("b.txt"));

Assert.Equal("b.txt", _closed.Take(_ => true)!.File.Name);
Assert.Equal("a.txt", _closed.Take(_ => true)!.File.Name);
Assert.Null(_closed.Take(_ => true));
}

[Fact]
public void Push_forgets_the_oldest_past_twenty()
{
for (var i = 0; i <= ClosedTabs.Max; i++)
_closed.Push(Closed($"{i}.txt"));

Assert.Equal(ClosedTabs.Max, _closed.Count);
Assert.False(_closed.Any(f => f.Name == "0.txt"));
Assert.True(_closed.Any(f => f.Name == "1.txt"));
}

[Fact]
public void Take_skips_and_drops_tabs_that_cant_come_back()
{
_closed.Push(Closed("a.txt"));
_closed.Push(Closed("open.txt"));
_closed.Push(Closed("deleted.txt"));

Assert.Equal("a.txt", _closed.Take(f => f.Name == "a.txt")!.File.Name);
Assert.Equal(0, _closed.Count);
}

[Fact]
public void Any_says_whether_one_can_come_back()
{
Assert.False(_closed.Any(_ => true));
_closed.Push(Closed("a.txt"));

Assert.True(_closed.Any(_ => true));
Assert.False(_closed.Any(_ => false));
}

private ClosedTab Closed(string name)
{
_fs.AddFile($"/work/{name}", new MockFileData("one\n"));
using var tab = new EditorTab(_fs.FileInfo.New($"/work/{name}"));
return new ClosedTab(tab.File, 0, 0, tab.ViewPosition, tab.History);
}
}
17 changes: 17 additions & 0 deletions tests/TuiCode.Tests/HelpPlaceHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ public async Task F1_on_the_tab_strip_lists_the_tab_keys()
], help.Rows);
}

[Fact]
public async Task The_tabs_column_lists_undo_close_tab_once_a_tab_has_closed()
{
_fs.AddFile("/work/a.txt", new MockFileData("one"));
_fs.AddFile("/work/b.txt", new MockFileData("two"));
var help = await HelpFrom(() =>
{
Built.OpenFile(_fs.FileInfo.New("/work/a.txt"));
Built.OpenFile(_fs.FileInfo.New("/work/b.txt"));
_commands.TryExecute(CommandIds.CloseActiveEditor);
_commands.TryExecute(CommandIds.FocusEditorTabStrip);
}, "Tabs");

var close = help.Rows.ToList().IndexOf(new("Ctrl+W", "Close tab"));
Assert.Equal(new HelpRow("Ctrl+Shift+T", "Undo close tab"), help.Rows[close + 1]);
}

[Fact]
public async Task The_tabs_column_follows_a_rebind_of_next_tab()
{
Expand Down
2 changes: 1 addition & 1 deletion tests/TuiCode.Tests/MenuBarHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void The_File_menus_letters_match_the_pitch()
Assert.Equal(
[
"_New file or folder", "_Open file or folder", "Open _recent folder", "Open _worktree", "Open _file path",
"_Save active editor", "Save _all", "Re_load from disk", "_Close active editor", "Clos_e other tabs", "Close all ta_bs",
"_Save active editor", "Save _all", "Re_load from disk", "_Close active editor", "Clos_e other tabs", "Close all ta_bs", "_Undo close tab",
"_Move or rename file or folder", "Cu_t file or folder", "_Paste file or folder", "_Delete file or folder", "Delete permanentl_y",
"Show document _info", "Open settin_gs", "Suspend to s_hell", "_Quit",
], file.Select(i => i.Title));
Expand Down
Loading
Loading