From 97c957c104305e2eb2ab3ea22ea6017f80e2aae9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 14:33:23 +0000 Subject: [PATCH 1/5] Initial plan From a51bedea7b2bf19e89aedc1a97659ebabc047355 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 14:53:17 +0000 Subject: [PATCH 2/5] Add TextViewEditor wrapper that wraps gui-cs/Editor with a TextView-compatible API Introduces a new Terminal.Gui.TextViewEditor project that provides a TextViewEditor class wrapping the Terminal.Gui.Editor package's Editor view with an API compatible with TextView. This eases migration from TextView to gui-cs/Editor. Key features: - Text property (get/set full document content) - CurrentRow/CurrentColumn/InsertionPoint for cursor position - ReadOnly, TabWidth properties - IsSelecting, SelectedText, SelectedLength for selection - Load(string path) and Load(Stream) for file loading - SelectAll/ClearSelection methods - ContentsChanged, CaretChanged, SelectionChanged events - UnderlyingEditor property for advanced Editor access Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/597eda9c-4689-4846-830e-4c7f9d2d04af Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Directory.Packages.props | 1 + .../Terminal.Gui.TextViewEditor.csproj | 18 ++ Terminal.Gui.TextViewEditor/TextViewEditor.cs | 262 ++++++++++++++++++ Terminal.sln | 29 ++ .../TextViewEditorBasicTests.cs | 237 ++++++++++++++++ .../TextViewEditorTests.csproj | 27 ++ nuget.config | 1 + 7 files changed, 575 insertions(+) create mode 100644 Terminal.Gui.TextViewEditor/Terminal.Gui.TextViewEditor.csproj create mode 100644 Terminal.Gui.TextViewEditor/TextViewEditor.cs create mode 100644 Tests/TextViewEditorTests/TextViewEditorBasicTests.cs create mode 100644 Tests/TextViewEditorTests/TextViewEditorTests.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 511ec4d415..779a90a181 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,6 +47,7 @@ + diff --git a/Terminal.Gui.TextViewEditor/Terminal.Gui.TextViewEditor.csproj b/Terminal.Gui.TextViewEditor/Terminal.Gui.TextViewEditor.csproj new file mode 100644 index 0000000000..05e0715849 --- /dev/null +++ b/Terminal.Gui.TextViewEditor/Terminal.Gui.TextViewEditor.csproj @@ -0,0 +1,18 @@ + + + Terminal.Gui.TextViewEditor + net10.0 + 14 + enable + enable + Terminal.Gui.Views + Terminal.Gui.TextViewEditor + Wraps the Terminal.Gui.Editor view with a TextView-compatible API to ease porting. + true + + + + + + + diff --git a/Terminal.Gui.TextViewEditor/TextViewEditor.cs b/Terminal.Gui.TextViewEditor/TextViewEditor.cs new file mode 100644 index 0000000000..9835e02b03 --- /dev/null +++ b/Terminal.Gui.TextViewEditor/TextViewEditor.cs @@ -0,0 +1,262 @@ +using System.Drawing; +using Terminal.Gui.Document; +using Terminal.Gui.ViewBase; + +namespace Terminal.Gui.Views; + +/// +/// A that wraps the Terminal.Gui.Editor package's +/// view with an API that is compatible with +/// , easing migration from to the +/// gui-cs/Editor package. +/// +/// +/// +/// delegates all text editing to the underlying +/// while exposing properties and methods that match +/// the API. This enables existing code that uses +/// to migrate incrementally by swapping the type to with +/// minimal source changes. +/// +/// +/// Not all features are supported. Unsupported features will +/// throw or no-op as documented on each member. +/// +/// +public class TextViewEditor : View +{ + private readonly Terminal.Gui.Editor.Editor _editor = null!; + + /// + /// Initializes a new instance of . + /// + public TextViewEditor () + { + CanFocus = true; + + _editor = new Terminal.Gui.Editor.Editor + { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + _editor.CaretChanged += (_, _) => CaretChanged?.Invoke (this, EventArgs.Empty); + _editor.SelectionChanged += (_, _) => SelectionChanged?.Invoke (this, EventArgs.Empty); + + // Subscribe to document text changes + _editor.Document.Changed += (_, _) => OnContentsChanged (); + + Add (_editor); + } + + /// + /// Gets or sets the text content. This is the primary -compatible + /// text property. + /// + /// + /// Setting this property replaces the entire document content and resets the cursor + /// position to the beginning. + /// + public override string Text + { + get => _editor?.Document.Text ?? string.Empty; + set + { + if (_editor is null) + { + return; + } + + _editor.Document.Text = value ?? string.Empty; + _editor.CaretOffset = 0; + OnTextChanged (); + } + } + + /// + /// Gets the cursor column (zero-based offset within the current line). + /// + public int CurrentColumn + { + get + { + DocumentLine line = _editor.Document.GetLineByOffset (_editor.CaretOffset); + + return _editor.CaretOffset - line.Offset; + } + } + + /// + /// Gets the current cursor row (zero-based line number). + /// + public int CurrentRow + { + get + { + DocumentLine line = _editor.Document.GetLineByOffset (_editor.CaretOffset); + + return line.LineNumber - 1; // DocumentLine uses 1-based line numbers + } + } + + /// + /// Gets or sets the cursor position as a where + /// X is the column and Y is the row. + /// + public Point InsertionPoint + { + get => new (CurrentColumn, CurrentRow); + set + { + int row = Math.Max (0, Math.Min (value.Y, _editor.Document.LineCount - 1)); + DocumentLine line = _editor.Document.GetLineByNumber (row + 1); + int col = Math.Max (0, Math.Min (value.X, line.Length)); + _editor.CaretOffset = line.Offset + col; + } + } + + /// + /// Gets the number of lines in the document. + /// + public int Lines => _editor.Document.LineCount; + + /// + /// Gets or sets whether the editor is in read-only mode. + /// + public bool ReadOnly + { + get => _editor.ReadOnly; + set => _editor.ReadOnly = value; + } + + /// + /// Gets or sets the tab width (indentation size). Equivalent to . + /// + public int TabWidth + { + get => _editor.IndentationSize; + set => _editor.IndentationSize = Math.Max (value, 1); + } + + /// + /// Gets the selected text, or if no selection exists. + /// + public string SelectedText + { + get + { + if (!_editor.HasSelection) + { + return string.Empty; + } + + return _editor.SelectedText ?? string.Empty; + } + } + + /// + /// Gets whether the user currently has text selected. + /// + public bool IsSelecting => _editor.HasSelection; + + /// + /// Gets the length of the current selection. + /// + public int SelectedLength => _editor.SelectionLength; + + /// + /// Gets whether the text has been modified since last load or clear. + /// + public bool IsDirty => _editor.Document.UndoStack.CanUndo; + + /// + /// Raised when the caret (cursor) position changes. + /// + public event EventHandler? CaretChanged; + + /// + /// Raised when the selection changes. + /// + public event EventHandler? SelectionChanged; + + /// + /// Raised when the contents of the editor are changed. + /// + public event EventHandler? ContentsChanged; + + /// + /// Loads text from a file path. + /// + /// The file path to load. + /// if the file was loaded successfully. + public bool Load (string path) + { + if (!File.Exists (path)) + { + return false; + } + + string content = File.ReadAllText (path); + _editor.Document.Text = content; + _editor.CaretOffset = 0; + _editor.Document.UndoStack.MarkAsOriginalFile (); + OnTextChanged (); + + return true; + } + + /// + /// Loads text from a stream. + /// + /// The stream to load from. + public void Load (Stream stream) + { + using StreamReader reader = new (stream); + string content = reader.ReadToEnd (); + _editor.Document.Text = content; + _editor.CaretOffset = 0; + _editor.Document.UndoStack.MarkAsOriginalFile (); + OnTextChanged (); + } + + /// + /// Selects all text in the editor. + /// + public void SelectAll () + { + _editor.SelectAll (); + } + + /// + /// Clears the current selection. + /// + public void ClearSelection () + { + _editor.ClearSelection (); + } + + /// + /// Gets the underlying instance for advanced usage. + /// + /// + /// Use this property to access Editor-specific features not exposed by the + /// -compatible API, such as folding, syntax highlighting, + /// multi-caret editing, and the rendering pipeline. + /// + public Terminal.Gui.Editor.Editor UnderlyingEditor => _editor; + + /// + /// Gets the underlying for direct document manipulation. + /// + public TextDocument Document => _editor.Document; + + /// + /// Raises the event. + /// + protected virtual void OnContentsChanged () + { + ContentsChanged?.Invoke (this, EventArgs.Empty); + } +} diff --git a/Terminal.sln b/Terminal.sln index de4c68a853..95765706a6 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -158,6 +158,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers.Inte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerformanceTests", "Tests\PerformanceTests\PerformanceTests.csproj", "{6E98BACA-E6B6-47A0-B45C-B624F8E74EC2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.TextViewEditor", "Terminal.Gui.TextViewEditor\Terminal.Gui.TextViewEditor.csproj", "{048A9D63-B901-404A-8BF0-57DC66E9100F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextViewEditorTests", "Tests\TextViewEditorTests\TextViewEditorTests.csproj", "{9C89E569-2AB8-4970-9D68-793709940604}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -468,6 +472,30 @@ Global {6E98BACA-E6B6-47A0-B45C-B624F8E74EC2}.Release|x64.Build.0 = Release|Any CPU {6E98BACA-E6B6-47A0-B45C-B624F8E74EC2}.Release|x86.ActiveCfg = Release|Any CPU {6E98BACA-E6B6-47A0-B45C-B624F8E74EC2}.Release|x86.Build.0 = Release|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Debug|x64.ActiveCfg = Debug|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Debug|x64.Build.0 = Debug|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Debug|x86.ActiveCfg = Debug|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Debug|x86.Build.0 = Debug|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Release|Any CPU.Build.0 = Release|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Release|x64.ActiveCfg = Release|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Release|x64.Build.0 = Release|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Release|x86.ActiveCfg = Release|Any CPU + {048A9D63-B901-404A-8BF0-57DC66E9100F}.Release|x86.Build.0 = Release|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Debug|x64.Build.0 = Debug|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Debug|x86.Build.0 = Debug|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Release|Any CPU.Build.0 = Release|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Release|x64.ActiveCfg = Release|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Release|x64.Build.0 = Release|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Release|x86.ActiveCfg = Release|Any CPU + {9C89E569-2AB8-4970-9D68-793709940604}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -499,6 +527,7 @@ Global {70802F77-F259-44C6-9522-46FCE2FD754E} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} {3116547F-A8F2-4189-BC22-0B47C757164C} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} {6E98BACA-E6B6-47A0-B45C-B624F8E74EC2} = {A589126F-C71A-4FEE-B7EA-2DCA1ADF6A46} + {9C89E569-2AB8-4970-9D68-793709940604} = {A589126F-C71A-4FEE-B7EA-2DCA1ADF6A46} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F8F8A4D-7B8D-4C2A-AC5E-CD7117F74C03} diff --git a/Tests/TextViewEditorTests/TextViewEditorBasicTests.cs b/Tests/TextViewEditorTests/TextViewEditorBasicTests.cs new file mode 100644 index 0000000000..cbeae9dd06 --- /dev/null +++ b/Tests/TextViewEditorTests/TextViewEditorBasicTests.cs @@ -0,0 +1,237 @@ +// Copilot + +using Terminal.Gui.Views; + +namespace TextViewEditorTests; + +/// +/// Unit tests for . +/// +public class TextViewEditorBasicTests +{ + [Fact] + public void Constructor_Creates_Instance () + { + TextViewEditor editor = new (); + + Assert.NotNull (editor); + Assert.NotNull (editor.UnderlyingEditor); + Assert.NotNull (editor.Document); + } + + [Fact] + public void Text_SetAndGet_RoundTrips () + { + TextViewEditor editor = new (); + + editor.Text = "Hello, World!"; + + Assert.Equal ("Hello, World!", editor.Text); + } + + [Fact] + public void Text_SetNull_BecomesEmpty () + { + TextViewEditor editor = new (); + + editor.Text = null!; + + Assert.Equal (string.Empty, editor.Text); + } + + [Fact] + public void Lines_ReturnsCorrectCount () + { + TextViewEditor editor = new (); + + editor.Text = "Line1\nLine2\nLine3"; + + Assert.Equal (3, editor.Lines); + } + + [Fact] + public void CurrentRow_AfterSetText_IsZero () + { + TextViewEditor editor = new (); + + editor.Text = "Line1\nLine2"; + + Assert.Equal (0, editor.CurrentRow); + } + + [Fact] + public void CurrentColumn_AfterSetText_IsZero () + { + TextViewEditor editor = new (); + + editor.Text = "Hello"; + + Assert.Equal (0, editor.CurrentColumn); + } + + [Fact] + public void InsertionPoint_SetAndGet () + { + TextViewEditor editor = new (); + + editor.Text = "Line1\nLine2\nLine3"; + editor.InsertionPoint = new Point (3, 1); + + Assert.Equal (1, editor.CurrentRow); + Assert.Equal (3, editor.CurrentColumn); + Assert.Equal (new Point (3, 1), editor.InsertionPoint); + } + + [Fact] + public void InsertionPoint_ClampsToBounds () + { + TextViewEditor editor = new (); + + editor.Text = "AB\nCD"; + + // Set beyond line length + editor.InsertionPoint = new Point (100, 0); + Assert.Equal (2, editor.CurrentColumn); + + // Set beyond line count + editor.InsertionPoint = new Point (0, 100); + Assert.Equal (1, editor.CurrentRow); + } + + [Fact] + public void ReadOnly_DefaultFalse () + { + TextViewEditor editor = new (); + + Assert.False (editor.ReadOnly); + } + + [Fact] + public void ReadOnly_SetTrue () + { + TextViewEditor editor = new (); + + editor.ReadOnly = true; + + Assert.True (editor.ReadOnly); + } + + [Fact] + public void TabWidth_DefaultIs4 () + { + TextViewEditor editor = new (); + + Assert.Equal (4, editor.TabWidth); + } + + [Fact] + public void TabWidth_SetAndGet () + { + TextViewEditor editor = new (); + + editor.TabWidth = 8; + + Assert.Equal (8, editor.TabWidth); + } + + [Fact] + public void TabWidth_MinimumIsOne () + { + TextViewEditor editor = new (); + + editor.TabWidth = 0; + + Assert.Equal (1, editor.TabWidth); + } + + [Fact] + public void IsSelecting_DefaultFalse () + { + TextViewEditor editor = new (); + + editor.Text = "Hello"; + + Assert.False (editor.IsSelecting); + } + + [Fact] + public void SelectedText_WhenNoSelection_ReturnsEmpty () + { + TextViewEditor editor = new (); + + editor.Text = "Hello"; + + Assert.Equal (string.Empty, editor.SelectedText); + } + + [Fact] + public void SelectedLength_WhenNoSelection_ReturnsZero () + { + TextViewEditor editor = new (); + + editor.Text = "Hello"; + + Assert.Equal (0, editor.SelectedLength); + } + + [Fact] + public void Load_Stream_LoadsContent () + { + TextViewEditor editor = new (); + + using MemoryStream stream = new (System.Text.Encoding.UTF8.GetBytes ("Stream content")); + editor.Load (stream); + + Assert.Equal ("Stream content", editor.Text); + } + + [Fact] + public void Load_NonExistentFile_ReturnsFalse () + { + TextViewEditor editor = new (); + + bool result = editor.Load ("/nonexistent/path/file.txt"); + + Assert.False (result); + } + + [Fact] + public void ContentsChanged_RaisedOnTextChange () + { + TextViewEditor editor = new (); + bool raised = false; + editor.ContentsChanged += (_, _) => raised = true; + + editor.Text = "new content"; + + // ContentsChanged is raised via the document's Changed event + // When Text is set, it replaces the document content which fires Changed + Assert.True (raised); + } + + [Fact] + public void SelectAll_SelectsEntireDocument () + { + TextViewEditor editor = new (); + + editor.Text = "Hello"; + editor.SelectAll (); + + Assert.True (editor.IsSelecting); + Assert.Equal ("Hello", editor.SelectedText); + Assert.Equal (5, editor.SelectedLength); + } + + [Fact] + public void ClearSelection_RemovesSelection () + { + TextViewEditor editor = new (); + + editor.Text = "Hello"; + editor.SelectAll (); + editor.ClearSelection (); + + Assert.False (editor.IsSelecting); + Assert.Equal (string.Empty, editor.SelectedText); + } +} diff --git a/Tests/TextViewEditorTests/TextViewEditorTests.csproj b/Tests/TextViewEditorTests/TextViewEditorTests.csproj new file mode 100644 index 0000000000..9e9dcfaef4 --- /dev/null +++ b/Tests/TextViewEditorTests/TextViewEditorTests.csproj @@ -0,0 +1,27 @@ + + + Exe + true + true + enable + false + enable + true + true + + + + + + + + + + + + + + + + + diff --git a/nuget.config b/nuget.config index 4dcc41f23c..147177efe2 100644 --- a/nuget.config +++ b/nuget.config @@ -14,6 +14,7 @@ + From 19d19380bdbf1fdace54bc7b53c6069e688fd7a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 14:54:32 +0000 Subject: [PATCH 3/5] Add clarifying comment for null! field initialization pattern Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/597eda9c-4689-4846-830e-4c7f9d2d04af Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Terminal.Gui.TextViewEditor/TextViewEditor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Terminal.Gui.TextViewEditor/TextViewEditor.cs b/Terminal.Gui.TextViewEditor/TextViewEditor.cs index 9835e02b03..b8e3337bcd 100644 --- a/Terminal.Gui.TextViewEditor/TextViewEditor.cs +++ b/Terminal.Gui.TextViewEditor/TextViewEditor.cs @@ -25,6 +25,8 @@ namespace Terminal.Gui.Views; /// public class TextViewEditor : View { + // Initialized to null! because the base View constructor calls the virtual Text setter + // (via SetupText) before this constructor body runs. The null guard in Text protects against this. private readonly Terminal.Gui.Editor.Editor _editor = null!; /// From 90fd1aa098d63c92c621d9021cd7ab8cf4b9d0fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 15:10:52 +0000 Subject: [PATCH 4/5] Add NotepadEditor scenario porting Notepad to use TextViewEditor Demonstrates that TextViewEditor is a drop-in replacement for TextView in a real-world scenario. The NotepadEditor scenario mirrors the original Notepad scenario but uses TextViewEditor instead of TextView for all text editing functionality. Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/8d6fc3c8-1415-477d-8663-fefae033d836 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/UICatalog/Scenarios/NotepadEditor.cs | 476 ++++++++++++++++++ Examples/UICatalog/UICatalog.csproj | 1 + 2 files changed, 477 insertions(+) create mode 100644 Examples/UICatalog/Scenarios/NotepadEditor.cs diff --git a/Examples/UICatalog/Scenarios/NotepadEditor.cs b/Examples/UICatalog/Scenarios/NotepadEditor.cs new file mode 100644 index 0000000000..5333d17023 --- /dev/null +++ b/Examples/UICatalog/Scenarios/NotepadEditor.cs @@ -0,0 +1,476 @@ +// ReSharper disable AccessToDisposedClosure + +#nullable enable + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("NotepadEditor", "Multi-tab text editor using TextViewEditor (gui-cs/Editor wrapper).")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("Tabs")] +[ScenarioCategory ("TextViewEditor")] +public class NotepadEditor : Scenario +{ + private IApplication? _app; + private Tabs? _focusedTabs; + private string? _lastDirectory; + private int _numNewTabs = 1; + private Tabs? _tabs; + private Window? _topWindow; + public Shortcut? LenShortcut { get; private set; } + + public override void Main () + { + ConfigurationManager.Enable (ConfigLocations.All); + using IApplication app = Application.Create (); + app.Init (); + _app = app; + + // Set initial directory to docfx/docs relative to the repository root + string? repoRoot = FindRepoRoot (); + + if (repoRoot is { }) + { + string docsPath = Path.Combine (repoRoot, "docfx", "docs"); + + if (Directory.Exists (docsPath)) + { + _lastDirectory = docsPath; + } + } + + _topWindow = new Window { BorderStyle = LineStyle.None }; + + // MenuBar + MenuBar menu = new (); + + menu.Add (new MenuBarItem (Strings.menuFile, + [ + new MenuItem { Title = Strings.cmdNew, Key = Key.N.WithCtrl.WithAlt, Action = New }, + new MenuItem { Title = Strings.cmdOpen, Action = Open }, + new MenuItem { Title = Strings.cmdSave, Action = Save }, + new MenuItem { Title = "Save _As", Action = () => SaveAs () }, + new MenuItem { Title = Strings.cmdClose, Action = Close }, + new MenuItem { Title = Strings.cmdQuit, Action = Quit } + ])); + + menu.Add (new MenuBarItem ("_About", [new MenuItem { Title = "_About", Action = () => MessageBox.Query (app, "NotepadEditor", "About NotepadEditor (using TextViewEditor)...", "Ok") }])); + + _tabs = new Tabs { X = 0, Y = Pos.Bottom (menu), Width = Dim.Fill (), Height = Dim.Fill (1) }; + + LenShortcut = new Shortcut (Key.Empty, "Len: ", null); + + // StatusBar + StatusBar statusBar = + new ([ + new Shortcut (Application.GetDefaultKey (Command.Quit), "Quit", Quit), + new Shortcut (Key.F2, "Open", Open), + new Shortcut (Key.F1, "New", New), + new Shortcut (Key.F3, "Save", Save), + new Shortcut (Key.F6, "Close", Close), + LenShortcut + ]) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; + + _topWindow.Add (menu, _tabs, statusBar); + + _focusedTabs = _tabs; + _tabs.ValueChanged += Tabs_ValueChanged; + _tabs.HasFocusChanging += (_, _) => _focusedTabs = _tabs; + + _topWindow.IsModalChanged += (_, e) => + { + if (e.Value) + { + // Only create the initial tab the first time the window becomes modal. + // IsModalChanged fires again after every nested modal dialog closes, + // so we guard against creating duplicate tabs. + if (!_tabs!.TabCollection.Any ()) + { + New (); + } + + LenShortcut.Title = $"Len:{GetSelectedTextLength ()}"; + } + else + { + _tabs.ValueChanged -= Tabs_ValueChanged; + } + }; + + app.Run (_topWindow); + _topWindow.Dispose (); + } + + public void Save () + { + if (_focusedTabs?.Value is OpenedFile tab) + { + Save (_focusedTabs, tab); + } + } + + private void Save (Tabs tabsToSave, OpenedFile tabToSave) + { + if (tabToSave.File is null) + { + SaveAs (); + } + else + { + tabToSave.Save (); + } + + tabsToSave.SetNeedsDraw (); + } + + public bool SaveAs () + { + if (_focusedTabs?.Value is not OpenedFile tab) + { + return false; + } + + SaveDialog fd = new (); + + if (_lastDirectory is { }) + { + fd.Path = _lastDirectory; + } + + _app?.Run (fd); + + if (string.IsNullOrWhiteSpace (fd.Path) || fd.Canceled) + { + fd.Dispose (); + + return false; + } + + _lastDirectory = Path.GetDirectoryName (Path.GetFullPath (fd.Path)); + tab.File = new FileInfo (fd.Path); + tab.Title = fd.FileName ?? throw new InvalidOperationException (); + tab.Save (); + + fd.Dispose (); + + return true; + } + + private void Close () + { + if (_focusedTabs?.Value is OpenedFile tab) + { + Close (_focusedTabs, tab); + } + } + + private void Close (Tabs tabs, OpenedFile tabToClose) + { + _focusedTabs = tabs; + + if (tabToClose.UnsavedChanges) + { + int? result = MessageBox.Query (tabs.App!, "Save Changes", $"Save changes to {tabToClose.Title.TrimEnd ('*')}", "Yes", "No", "Cancel"); + + if (result is null or 2) + { + // user cancelled + return; + } + + if (result == 0) + { + if (tabToClose.File is null) + { + SaveAs (); + } + else + { + tabToClose.Save (); + } + } + } + + // close and dispose the tab + tabs.Remove (tabToClose); + tabToClose.Dispose (); + _focusedTabs = tabs; + + // If last tab is closed, open a new one + if (!tabs.TabCollection.Any ()) + { + New (); + } + } + + private void New () => Open (null!, $"new {_numNewTabs++}"); + + private void Open () + { + OpenDialog open = new () + { + Title = "Open", + AllowsMultipleSelection = true, + AllowedTypes = + [ + new AllowedType ("Markdown", ".md", ".markdown"), + new AllowedType ("Text", ".txt", ".csv", ".tsv"), + new AllowedType ("Code", ".c", ".h", ".js", ".cs", ".json", ".yml"), + new AllowedTypeAny () + ], + MustExist = true, + OpenMode = OpenMode.File + }; + + if (_lastDirectory is { }) + { + open.Path = _lastDirectory; + } + + _app?.Run (open); + + bool canceled = open.Canceled; + + if (!canceled) + { + foreach (string path in open.FilePaths) + { + if (string.IsNullOrEmpty (path) || !File.Exists (path)) + { + break; + } + + _lastDirectory = Path.GetDirectoryName (Path.GetFullPath (path)); + Open (new FileInfo (path), Path.GetFileName (path)); + } + } + + open.Dispose (); + } + + /// Creates a new tab with initial text, or reuses the current tab if it is virgin. + /// File that was read or null if a new blank document. + /// Display name for the tab. + private void Open (FileInfo? fileInfo, string tabName) + { + if (_focusedTabs is null) + { + return; + } + + // If the current tab is virgin (no file, no content), reuse it instead of creating a new one + if (fileInfo is { }) + { + if (_focusedTabs.Value is OpenedFile { IsPristine: true } currentTab) + { + currentTab.File = fileInfo; + currentTab.Title = tabName; + currentTab.LoadFile (fileInfo); + + return; + } + } + + OpenedFile tab = new (this) { Title = tabName, File = fileInfo }; + tab.CreateAndAddEditor (fileInfo); + tab.RegisterEditorEvents (); + + _focusedTabs.Add (tab); + _focusedTabs.Value = tab; + } + + private void Quit () + { + if (_tabs is { }) + { + foreach (OpenedFile tab in _tabs.TabCollection.OfType ()) + { + if (!tab.UnsavedChanges) + { + continue; + } + + int? result = MessageBox.Query (_app!, "Unsaved Changes", $"Save changes to {tab.Title.TrimEnd ('*')}?", "Yes", "No", "Cancel"); + + if (result is null or 2) + { + return; + } + + if (result != 0) + { + continue; + } + + _focusedTabs = _tabs; + _tabs.Value = tab; + + if (tab.File is null) + { + if (!SaveAs ()) + { + return; + } + } + else + { + tab.Save (); + } + } + } + + _topWindow?.RequestStop (); + } + + /// + /// Walks up the directory tree from the current directory looking for the repository root + /// (identified by Terminal.sln). + /// + private static string? FindRepoRoot () + { + DirectoryInfo? dir = new (Environment.CurrentDirectory); + + while (dir is { }) + { + if (File.Exists (Path.Combine (dir.FullName, "Terminal.sln"))) + { + return dir.FullName; + } + + dir = dir.Parent; + } + + return null; + } + + private int GetSelectedTextLength () + { + if (_focusedTabs?.Value is OpenedFile tab) + { + return tab.Editor?.Text.Length ?? 0; + } + + return 0; + } + + private void Tabs_ValueChanged (object? sender, ValueChangedEventArgs e) + { + if (LenShortcut is null) + { + return; + } + + var len = 0; + + if (e.NewValue is OpenedFile tab) + { + len = tab.Editor?.Text.Length ?? 0; + } + + LenShortcut.Title = $"Len:{len}"; + } + + private class OpenedFile (NotepadEditor notepad) : View + { + public FileInfo? File { get; set; } + + /// Gets whether this tab is a pristine new document — never opened to a file and has no content. + public bool IsPristine => File is null && string.IsNullOrEmpty (Editor?.Text); + + public TextViewEditor? Editor { get; private set; } + + /// The text of the tab the last time it was saved. + private string? _savedText; + + public bool UnsavedChanges => Editor is { } && !string.Equals (_savedText, Editor.Text); + + public void CreateAndAddEditor (FileInfo? file) + { + var initialText = string.Empty; + + if (file is { Exists: true }) + { + initialText = System.IO.File.ReadAllText (file.FullName); + } + + Editor = new TextViewEditor + { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (), + Text = initialText + }; + + _savedText = initialText; + + Add (Editor); + } + + /// Loads a file into an existing tab, replacing its content. + public void LoadFile (FileInfo file) + { + if (Editor is null) + { + return; + } + + var text = string.Empty; + + if (file.Exists) + { + text = System.IO.File.ReadAllText (file.FullName); + } + + // Set _savedText first so the ContentsChanged handler sees matching text (not dirty). + _savedText = text; + Editor.Text = text; + } + + public void RegisterEditorEvents () + { + if (Editor is null) + { + return; + } + + // when user makes changes rename tab to indicate unsaved + Editor.ContentsChanged += (_, _) => + { + // if current text doesn't match saved text + bool areDiff = UnsavedChanges; + + if (areDiff) + { + if (!Title.EndsWith ('*')) + { + Title = Title + "*"; + } + } + else + { + if (Title.EndsWith ('*')) + { + Title = Title.TrimEnd ('*'); + } + } + + notepad.LenShortcut!.Title = $"Len:{Editor.Text.Length}"; + }; + } + + internal void Save () + { + if (Editor is null || File is null || string.IsNullOrWhiteSpace (File.FullName)) + { + return; + } + + string newText = Editor.Text; + + System.IO.File.WriteAllText (File.FullName, newText); + _savedText = newText; + + Title = Title.TrimEnd ('*'); + } + } +} diff --git a/Examples/UICatalog/UICatalog.csproj b/Examples/UICatalog/UICatalog.csproj index 1dc8d9f303..1de677e941 100644 --- a/Examples/UICatalog/UICatalog.csproj +++ b/Examples/UICatalog/UICatalog.csproj @@ -35,6 +35,7 @@ + From 430b0de74f942554feb2aaca1e40505f3582eb4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 15:56:12 +0000 Subject: [PATCH 5/5] Remove NotepadEditor scenario; upgrade existing Notepad scenario to use TextViewEditor Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/5992c475-34d5-4941-8f1e-c499738e2721 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/UICatalog/Scenarios/Notepad.cs | 81 ++- Examples/UICatalog/Scenarios/NotepadEditor.cs | 476 ------------------ 2 files changed, 40 insertions(+), 517 deletions(-) delete mode 100644 Examples/UICatalog/Scenarios/NotepadEditor.cs diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 988e4764d5..94eaa57c64 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -7,7 +7,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Notepad", "Multi-tab text editor using the Tabs control.")] [ScenarioCategory ("Controls")] [ScenarioCategory ("Tabs")] -[ScenarioCategory ("TextView")] +[ScenarioCategory ("TextViewEditor")] public class Notepad : Scenario { private IApplication? _app; @@ -271,8 +271,8 @@ private void Open (FileInfo? fileInfo, string tabName) } OpenedFile tab = new (this) { Title = tabName, File = fileInfo }; - tab.CreateAndAddTextView (fileInfo); - tab.RegisterTextViewEvents (); + tab.CreateAndAddEditor (fileInfo); + tab.RegisterEditorEvents (); _focusedTabs.Add (tab); _focusedTabs.Value = tab; @@ -344,7 +344,7 @@ private int GetSelectedTextLength () { if (_focusedTabs?.Value is OpenedFile tab) { - return tab.TextView?.Text.Length ?? 0; + return tab.Editor?.Text.Length ?? 0; } return 0; @@ -360,7 +360,7 @@ private void Tabs_ValueChanged (object? sender, ValueChangedEventArgs e) if (e.NewValue is OpenedFile tab) { - len = tab.TextView?.Text.Length ?? 0; + len = tab.Editor?.Text.Length ?? 0; } LenShortcut.Title = $"Len:{len}"; @@ -376,16 +376,16 @@ private class OpenedFile (Notepad notepad) : View public FileInfo? File { get; set; } /// Gets whether this tab is a pristine new document — never opened to a file and has no content. - public bool IsPristine => File is null && string.IsNullOrEmpty (TextView?.Text); + public bool IsPristine => File is null && string.IsNullOrEmpty (Editor?.Text); - public TextView? TextView { get; private set; } + public TextViewEditor? Editor { get; private set; } /// The text of the tab the last time it was saved. private string? _savedText; - public bool UnsavedChanges => TextView is { } && !string.Equals (_savedText, TextView.Text); + public bool UnsavedChanges => Editor is { } && !string.Equals (_savedText, Editor.Text); - public void CreateAndAddTextView (FileInfo? file) + public void CreateAndAddEditor (FileInfo? file) { var initialText = string.Empty; @@ -394,25 +394,24 @@ public void CreateAndAddTextView (FileInfo? file) initialText = System.IO.File.ReadAllText (file.FullName); } - TextView = new TextView + Editor = new TextViewEditor { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), - Text = initialText, - TabKeyAddsTab = false + Text = initialText }; _savedText = initialText; - Add (TextView); + Add (Editor); } /// Loads a file into an existing tab, replacing its content. public void LoadFile (FileInfo file) { - if (TextView is null) + if (Editor is null) { return; } @@ -426,49 +425,49 @@ public void LoadFile (FileInfo file) // Set _savedText first so the ContentsChanged handler sees matching text (not dirty). _savedText = text; - TextView.Text = text; + Editor.Text = text; } - public void RegisterTextViewEvents () + public void RegisterEditorEvents () { - if (TextView is null) + if (Editor is null) { return; } // when user makes changes rename tab to indicate unsaved - TextView.ContentsChanged += (_, _) => - { - // if current text doesn't match saved text - bool areDiff = UnsavedChanges; - - if (areDiff) - { - if (!Title.EndsWith ('*')) - { - Title = Title + "*"; - } - } - else - { - if (Title.EndsWith ('*')) - { - Title = Title.TrimEnd ('*'); - } - } - - notepad.LenShortcut?.Title = $"Len:{TextView.Text.Length}"; - }; + Editor.ContentsChanged += (_, _) => + { + // if current text doesn't match saved text + bool areDiff = UnsavedChanges; + + if (areDiff) + { + if (!Title.EndsWith ('*')) + { + Title = Title + "*"; + } + } + else + { + if (Title.EndsWith ('*')) + { + Title = Title.TrimEnd ('*'); + } + } + + notepad.LenShortcut?.Title = $"Len:{Editor.Text.Length}"; + }; } internal void Save () { - if (TextView is null || File is null || string.IsNullOrWhiteSpace (File.FullName)) + if (Editor is null || File is null || string.IsNullOrWhiteSpace (File.FullName)) { return; } - string newText = TextView.Text; + string newText = Editor.Text; System.IO.File.WriteAllText (File.FullName, newText); _savedText = newText; diff --git a/Examples/UICatalog/Scenarios/NotepadEditor.cs b/Examples/UICatalog/Scenarios/NotepadEditor.cs deleted file mode 100644 index 5333d17023..0000000000 --- a/Examples/UICatalog/Scenarios/NotepadEditor.cs +++ /dev/null @@ -1,476 +0,0 @@ -// ReSharper disable AccessToDisposedClosure - -#nullable enable - -namespace UICatalog.Scenarios; - -[ScenarioMetadata ("NotepadEditor", "Multi-tab text editor using TextViewEditor (gui-cs/Editor wrapper).")] -[ScenarioCategory ("Controls")] -[ScenarioCategory ("Tabs")] -[ScenarioCategory ("TextViewEditor")] -public class NotepadEditor : Scenario -{ - private IApplication? _app; - private Tabs? _focusedTabs; - private string? _lastDirectory; - private int _numNewTabs = 1; - private Tabs? _tabs; - private Window? _topWindow; - public Shortcut? LenShortcut { get; private set; } - - public override void Main () - { - ConfigurationManager.Enable (ConfigLocations.All); - using IApplication app = Application.Create (); - app.Init (); - _app = app; - - // Set initial directory to docfx/docs relative to the repository root - string? repoRoot = FindRepoRoot (); - - if (repoRoot is { }) - { - string docsPath = Path.Combine (repoRoot, "docfx", "docs"); - - if (Directory.Exists (docsPath)) - { - _lastDirectory = docsPath; - } - } - - _topWindow = new Window { BorderStyle = LineStyle.None }; - - // MenuBar - MenuBar menu = new (); - - menu.Add (new MenuBarItem (Strings.menuFile, - [ - new MenuItem { Title = Strings.cmdNew, Key = Key.N.WithCtrl.WithAlt, Action = New }, - new MenuItem { Title = Strings.cmdOpen, Action = Open }, - new MenuItem { Title = Strings.cmdSave, Action = Save }, - new MenuItem { Title = "Save _As", Action = () => SaveAs () }, - new MenuItem { Title = Strings.cmdClose, Action = Close }, - new MenuItem { Title = Strings.cmdQuit, Action = Quit } - ])); - - menu.Add (new MenuBarItem ("_About", [new MenuItem { Title = "_About", Action = () => MessageBox.Query (app, "NotepadEditor", "About NotepadEditor (using TextViewEditor)...", "Ok") }])); - - _tabs = new Tabs { X = 0, Y = Pos.Bottom (menu), Width = Dim.Fill (), Height = Dim.Fill (1) }; - - LenShortcut = new Shortcut (Key.Empty, "Len: ", null); - - // StatusBar - StatusBar statusBar = - new ([ - new Shortcut (Application.GetDefaultKey (Command.Quit), "Quit", Quit), - new Shortcut (Key.F2, "Open", Open), - new Shortcut (Key.F1, "New", New), - new Shortcut (Key.F3, "Save", Save), - new Shortcut (Key.F6, "Close", Close), - LenShortcut - ]) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; - - _topWindow.Add (menu, _tabs, statusBar); - - _focusedTabs = _tabs; - _tabs.ValueChanged += Tabs_ValueChanged; - _tabs.HasFocusChanging += (_, _) => _focusedTabs = _tabs; - - _topWindow.IsModalChanged += (_, e) => - { - if (e.Value) - { - // Only create the initial tab the first time the window becomes modal. - // IsModalChanged fires again after every nested modal dialog closes, - // so we guard against creating duplicate tabs. - if (!_tabs!.TabCollection.Any ()) - { - New (); - } - - LenShortcut.Title = $"Len:{GetSelectedTextLength ()}"; - } - else - { - _tabs.ValueChanged -= Tabs_ValueChanged; - } - }; - - app.Run (_topWindow); - _topWindow.Dispose (); - } - - public void Save () - { - if (_focusedTabs?.Value is OpenedFile tab) - { - Save (_focusedTabs, tab); - } - } - - private void Save (Tabs tabsToSave, OpenedFile tabToSave) - { - if (tabToSave.File is null) - { - SaveAs (); - } - else - { - tabToSave.Save (); - } - - tabsToSave.SetNeedsDraw (); - } - - public bool SaveAs () - { - if (_focusedTabs?.Value is not OpenedFile tab) - { - return false; - } - - SaveDialog fd = new (); - - if (_lastDirectory is { }) - { - fd.Path = _lastDirectory; - } - - _app?.Run (fd); - - if (string.IsNullOrWhiteSpace (fd.Path) || fd.Canceled) - { - fd.Dispose (); - - return false; - } - - _lastDirectory = Path.GetDirectoryName (Path.GetFullPath (fd.Path)); - tab.File = new FileInfo (fd.Path); - tab.Title = fd.FileName ?? throw new InvalidOperationException (); - tab.Save (); - - fd.Dispose (); - - return true; - } - - private void Close () - { - if (_focusedTabs?.Value is OpenedFile tab) - { - Close (_focusedTabs, tab); - } - } - - private void Close (Tabs tabs, OpenedFile tabToClose) - { - _focusedTabs = tabs; - - if (tabToClose.UnsavedChanges) - { - int? result = MessageBox.Query (tabs.App!, "Save Changes", $"Save changes to {tabToClose.Title.TrimEnd ('*')}", "Yes", "No", "Cancel"); - - if (result is null or 2) - { - // user cancelled - return; - } - - if (result == 0) - { - if (tabToClose.File is null) - { - SaveAs (); - } - else - { - tabToClose.Save (); - } - } - } - - // close and dispose the tab - tabs.Remove (tabToClose); - tabToClose.Dispose (); - _focusedTabs = tabs; - - // If last tab is closed, open a new one - if (!tabs.TabCollection.Any ()) - { - New (); - } - } - - private void New () => Open (null!, $"new {_numNewTabs++}"); - - private void Open () - { - OpenDialog open = new () - { - Title = "Open", - AllowsMultipleSelection = true, - AllowedTypes = - [ - new AllowedType ("Markdown", ".md", ".markdown"), - new AllowedType ("Text", ".txt", ".csv", ".tsv"), - new AllowedType ("Code", ".c", ".h", ".js", ".cs", ".json", ".yml"), - new AllowedTypeAny () - ], - MustExist = true, - OpenMode = OpenMode.File - }; - - if (_lastDirectory is { }) - { - open.Path = _lastDirectory; - } - - _app?.Run (open); - - bool canceled = open.Canceled; - - if (!canceled) - { - foreach (string path in open.FilePaths) - { - if (string.IsNullOrEmpty (path) || !File.Exists (path)) - { - break; - } - - _lastDirectory = Path.GetDirectoryName (Path.GetFullPath (path)); - Open (new FileInfo (path), Path.GetFileName (path)); - } - } - - open.Dispose (); - } - - /// Creates a new tab with initial text, or reuses the current tab if it is virgin. - /// File that was read or null if a new blank document. - /// Display name for the tab. - private void Open (FileInfo? fileInfo, string tabName) - { - if (_focusedTabs is null) - { - return; - } - - // If the current tab is virgin (no file, no content), reuse it instead of creating a new one - if (fileInfo is { }) - { - if (_focusedTabs.Value is OpenedFile { IsPristine: true } currentTab) - { - currentTab.File = fileInfo; - currentTab.Title = tabName; - currentTab.LoadFile (fileInfo); - - return; - } - } - - OpenedFile tab = new (this) { Title = tabName, File = fileInfo }; - tab.CreateAndAddEditor (fileInfo); - tab.RegisterEditorEvents (); - - _focusedTabs.Add (tab); - _focusedTabs.Value = tab; - } - - private void Quit () - { - if (_tabs is { }) - { - foreach (OpenedFile tab in _tabs.TabCollection.OfType ()) - { - if (!tab.UnsavedChanges) - { - continue; - } - - int? result = MessageBox.Query (_app!, "Unsaved Changes", $"Save changes to {tab.Title.TrimEnd ('*')}?", "Yes", "No", "Cancel"); - - if (result is null or 2) - { - return; - } - - if (result != 0) - { - continue; - } - - _focusedTabs = _tabs; - _tabs.Value = tab; - - if (tab.File is null) - { - if (!SaveAs ()) - { - return; - } - } - else - { - tab.Save (); - } - } - } - - _topWindow?.RequestStop (); - } - - /// - /// Walks up the directory tree from the current directory looking for the repository root - /// (identified by Terminal.sln). - /// - private static string? FindRepoRoot () - { - DirectoryInfo? dir = new (Environment.CurrentDirectory); - - while (dir is { }) - { - if (File.Exists (Path.Combine (dir.FullName, "Terminal.sln"))) - { - return dir.FullName; - } - - dir = dir.Parent; - } - - return null; - } - - private int GetSelectedTextLength () - { - if (_focusedTabs?.Value is OpenedFile tab) - { - return tab.Editor?.Text.Length ?? 0; - } - - return 0; - } - - private void Tabs_ValueChanged (object? sender, ValueChangedEventArgs e) - { - if (LenShortcut is null) - { - return; - } - - var len = 0; - - if (e.NewValue is OpenedFile tab) - { - len = tab.Editor?.Text.Length ?? 0; - } - - LenShortcut.Title = $"Len:{len}"; - } - - private class OpenedFile (NotepadEditor notepad) : View - { - public FileInfo? File { get; set; } - - /// Gets whether this tab is a pristine new document — never opened to a file and has no content. - public bool IsPristine => File is null && string.IsNullOrEmpty (Editor?.Text); - - public TextViewEditor? Editor { get; private set; } - - /// The text of the tab the last time it was saved. - private string? _savedText; - - public bool UnsavedChanges => Editor is { } && !string.Equals (_savedText, Editor.Text); - - public void CreateAndAddEditor (FileInfo? file) - { - var initialText = string.Empty; - - if (file is { Exists: true }) - { - initialText = System.IO.File.ReadAllText (file.FullName); - } - - Editor = new TextViewEditor - { - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill (), - Text = initialText - }; - - _savedText = initialText; - - Add (Editor); - } - - /// Loads a file into an existing tab, replacing its content. - public void LoadFile (FileInfo file) - { - if (Editor is null) - { - return; - } - - var text = string.Empty; - - if (file.Exists) - { - text = System.IO.File.ReadAllText (file.FullName); - } - - // Set _savedText first so the ContentsChanged handler sees matching text (not dirty). - _savedText = text; - Editor.Text = text; - } - - public void RegisterEditorEvents () - { - if (Editor is null) - { - return; - } - - // when user makes changes rename tab to indicate unsaved - Editor.ContentsChanged += (_, _) => - { - // if current text doesn't match saved text - bool areDiff = UnsavedChanges; - - if (areDiff) - { - if (!Title.EndsWith ('*')) - { - Title = Title + "*"; - } - } - else - { - if (Title.EndsWith ('*')) - { - Title = Title.TrimEnd ('*'); - } - } - - notepad.LenShortcut!.Title = $"Len:{Editor.Text.Length}"; - }; - } - - internal void Save () - { - if (Editor is null || File is null || string.IsNullOrWhiteSpace (File.FullName)) - { - return; - } - - string newText = Editor.Text; - - System.IO.File.WriteAllText (File.FullName, newText); - _savedText = newText; - - Title = Title.TrimEnd ('*'); - } - } -}