Skip to content

Commit ad4f41c

Browse files
authored
Merge pull request #95 from gui-cs/feature/indentation-model
feat: lift IIndentationStrategy and wire auto-indent on Enter
2 parents d733bcb + 439dc20 commit ad4f41c

9 files changed

Lines changed: 380 additions & 2 deletions

File tree

examples/ted/TedApp.FileOperations.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ private SaveChangesChoice ShowDefaultSaveChangesDialog ()
151151
"Save changes?",
152152
"The document has unsaved changes. Save before quitting?",
153153
Strings.btnCancel,
154-
"Don't Save",
154+
"Do_n't Save",
155155
Strings.btnSave);
156156

157157
return result switch

examples/ted/TedApp.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ public TedApp (bool readOnly = false)
7373
Editor.ConvertTabsToSpaces = e.NewValue == CheckState.Checked;
7474
};
7575

76+
CheckBox autoIndentCheckBox = new ()
77+
{
78+
AllowCheckStateNone = false,
79+
CanFocus = false,
80+
Text = "_Auto Indent",
81+
Value = Editor.IndentationStrategy is not null ? CheckState.Checked : CheckState.UnChecked
82+
};
83+
84+
autoIndentCheckBox.ValueChanged += (_, e) =>
85+
{
86+
Editor.IndentationStrategy = e.NewValue == CheckState.Checked
87+
? new Terminal.Gui.Text.Indentation.DefaultIndentationStrategy ()
88+
: null;
89+
};
90+
7691
CheckBox useThemeBackgroundCheckBox = new ()
7792
{
7893
AllowCheckStateNone = false,
@@ -200,6 +215,11 @@ public TedApp (bool readOnly = false)
200215
HelpText = "Insert spaces when Tab is pressed"
201216
},
202217
new MenuItem
218+
{
219+
CommandView = autoIndentCheckBox,
220+
HelpText = "Copy indentation from the previous line on Enter"
221+
},
222+
new MenuItem
203223
{
204224
Action = () =>
205225
{

src/Terminal.Gui.Editor/Editor.Commands.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ private void CreateCommandsAndBindings ()
8181
});
8282

8383
// Editing — selection-aware
84-
AddCommand (Command.NewLine, () => InsertOrReplace ("\n"));
84+
AddCommand (Command.NewLine, InsertNewLineWithAutoIndent);
8585
AddCommand (Command.DeleteCharLeft, DeleteLeft);
8686
AddCommand (Command.DeleteCharRight, DeleteRight);
8787

@@ -170,6 +170,38 @@ private void CreateCommandsAndBindings ()
170170
return true;
171171
}
172172

173+
private bool? InsertNewLineWithAutoIndent ()
174+
{
175+
if (ReadOnly)
176+
{
177+
return true;
178+
}
179+
180+
// Wrap both the newline insertion and the auto-indent in a single undo group
181+
// so that one Ctrl+Z undoes the entire Enter operation.
182+
using (_document!.RunUpdate ())
183+
{
184+
if (HasSelection)
185+
{
186+
ReplaceSelection ("\n");
187+
}
188+
else
189+
{
190+
_document.Insert (CaretOffset, "\n");
191+
}
192+
193+
// After the newline is inserted the caret sits at the start of the new line.
194+
// Ask the indentation strategy to fill in leading whitespace.
195+
if (IndentationStrategy is { } strategy)
196+
{
197+
DocumentLine newLine = _document.GetLineByOffset (CaretOffset);
198+
strategy.IndentLine (_document, newLine);
199+
}
200+
}
201+
202+
return true;
203+
}
204+
173205
private bool? InsertOrReplace (string text)
174206
{
175207
if (ReadOnly)

src/Terminal.Gui.Editor/Editor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Drawing;
33
using Terminal.Gui.Document;
44
using Terminal.Gui.Drawing;
5+
using Terminal.Gui.Text.Indentation;
56
using Terminal.Gui.ViewBase;
67
using Terminal.Gui.Views.Rendering;
78
using Attribute = Terminal.Gui.Drawing.Attribute;
@@ -231,6 +232,15 @@ public int IndentationSize
231232
/// <summary>Whether pressing Tab inserts spaces instead of a tab character.</summary>
232233
public bool ConvertTabsToSpaces { get; set; }
233234

235+
/// <summary>
236+
/// Gets or sets the indentation strategy applied when Enter is pressed.
237+
/// When non-null, the strategy's <see cref="IIndentationStrategy.IndentLine" /> is called
238+
/// on the newly created line to copy (or compute) indentation from the previous line.
239+
/// Defaults to <see cref="DefaultIndentationStrategy" />.
240+
/// Set to <see langword="null" /> to disable auto-indent on Enter.
241+
/// </summary>
242+
public IIndentationStrategy? IndentationStrategy { get; set; } = new DefaultIndentationStrategy ();
243+
234244
/// <summary>
235245
/// When <see langword="true" /> (the default), syntax-highlighted tokens keep both their
236246
/// foreground and background from the syntax highlighting theme (e.g. Dark Plus's
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Adapted for Terminal.Gui from AvaloniaEdit d7a6b63
2+
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
5+
// software and associated documentation files (the "Software"), to deal in the Software
6+
// without restriction, including without limitation the rights to use, copy, modify, merge,
7+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
8+
// to whom the Software is furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all copies or
11+
// substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
14+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
15+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
16+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
17+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
18+
// DEALINGS IN THE SOFTWARE.
19+
20+
using Terminal.Gui.Document;
21+
22+
namespace Terminal.Gui.Text.Indentation;
23+
24+
/// <summary>
25+
/// Handles indentation by copying the indentation from the previous line.
26+
/// Does not support indenting multiple lines.
27+
/// </summary>
28+
public class DefaultIndentationStrategy : IIndentationStrategy
29+
{
30+
/// <inheritdoc />
31+
public virtual void IndentLine (TextDocument document, DocumentLine line)
32+
{
33+
ArgumentNullException.ThrowIfNull (document);
34+
ArgumentNullException.ThrowIfNull (line);
35+
36+
DocumentLine? previousLine = line.PreviousLine;
37+
38+
if (previousLine is null)
39+
{
40+
return;
41+
}
42+
43+
ISegment indentationSegment = TextUtilities.GetWhitespaceAfter (document, previousLine.Offset);
44+
var indentation = document.GetText (indentationSegment);
45+
46+
// Copy indentation to line
47+
indentationSegment = TextUtilities.GetWhitespaceAfter (document, line.Offset);
48+
49+
document.Replace (indentationSegment.Offset, indentationSegment.Length, indentation,
50+
OffsetChangeMappingType.RemoveAndInsert);
51+
// OffsetChangeMappingType.RemoveAndInsert guarantees the caret moves behind the new indentation.
52+
}
53+
54+
/// <summary>
55+
/// Does nothing: indenting multiple lines is useless without a smart indentation strategy.
56+
/// </summary>
57+
public virtual void IndentLines (TextDocument document, int beginLine, int endLine) { }
58+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Adapted for Terminal.Gui from AvaloniaEdit d7a6b63
2+
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
5+
// software and associated documentation files (the "Software"), to deal in the Software
6+
// without restriction, including without limitation the rights to use, copy, modify, merge,
7+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
8+
// to whom the Software is furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all copies or
11+
// substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
14+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
15+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
16+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
17+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
18+
// DEALINGS IN THE SOFTWARE.
19+
20+
using Terminal.Gui.Document;
21+
22+
namespace Terminal.Gui.Text.Indentation;
23+
24+
/// <summary>
25+
/// Strategy how the text editor handles indentation when new lines are inserted.
26+
/// </summary>
27+
public interface IIndentationStrategy
28+
{
29+
/// <summary>
30+
/// Sets the indentation for the specified line.
31+
/// Usually this is constructed from the indentation of the previous line.
32+
/// </summary>
33+
void IndentLine (TextDocument document, DocumentLine line);
34+
35+
/// <summary>
36+
/// Reindents a set of lines.
37+
/// </summary>
38+
void IndentLines (TextDocument document, int beginLine, int endLine);
39+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// CoPilot - claude-opus-4.6
2+
3+
using Terminal.Gui.Document;
4+
using Terminal.Gui.Text.Indentation;
5+
using Xunit;
6+
7+
namespace Terminal.Gui.Editor.Tests.Indentation;
8+
9+
/// <summary>
10+
/// Tests for <see cref="DefaultIndentationStrategy" /> — the AvaloniaEdit-derived strategy
11+
/// that copies leading whitespace from the previous line.
12+
/// </summary>
13+
public class DefaultIndentationStrategyTests
14+
{
15+
[Fact]
16+
public void IndentLine_Copies_Spaces_From_Previous_Line ()
17+
{
18+
TextDocument doc = new (" hello\n");
19+
DefaultIndentationStrategy strategy = new ();
20+
21+
DocumentLine line2 = doc.GetLineByNumber (2);
22+
strategy.IndentLine (doc, line2);
23+
24+
Assert.Equal (" hello\n ", doc.Text);
25+
}
26+
27+
[Fact]
28+
public void IndentLine_Copies_Tab_From_Previous_Line ()
29+
{
30+
TextDocument doc = new ("\thello\n");
31+
DefaultIndentationStrategy strategy = new ();
32+
33+
DocumentLine line2 = doc.GetLineByNumber (2);
34+
strategy.IndentLine (doc, line2);
35+
36+
Assert.Equal ("\thello\n\t", doc.Text);
37+
}
38+
39+
[Fact]
40+
public void IndentLine_Copies_Mixed_Whitespace_From_Previous_Line ()
41+
{
42+
TextDocument doc = new ("\t hello\n");
43+
DefaultIndentationStrategy strategy = new ();
44+
45+
DocumentLine line2 = doc.GetLineByNumber (2);
46+
strategy.IndentLine (doc, line2);
47+
48+
Assert.Equal ("\t hello\n\t ", doc.Text);
49+
}
50+
51+
[Fact]
52+
public void IndentLine_NoOp_On_First_Line ()
53+
{
54+
TextDocument doc = new ("hello");
55+
DefaultIndentationStrategy strategy = new ();
56+
57+
DocumentLine line1 = doc.GetLineByNumber (1);
58+
strategy.IndentLine (doc, line1);
59+
60+
Assert.Equal ("hello", doc.Text);
61+
}
62+
63+
[Fact]
64+
public void IndentLine_NoOp_When_Previous_Line_Has_No_Indentation ()
65+
{
66+
TextDocument doc = new ("hello\n");
67+
DefaultIndentationStrategy strategy = new ();
68+
69+
DocumentLine line2 = doc.GetLineByNumber (2);
70+
strategy.IndentLine (doc, line2);
71+
72+
Assert.Equal ("hello\n", doc.Text);
73+
}
74+
75+
[Fact]
76+
public void IndentLine_Replaces_Existing_Whitespace_On_Target_Line ()
77+
{
78+
TextDocument doc = new (" hello\n world");
79+
DefaultIndentationStrategy strategy = new ();
80+
81+
DocumentLine line2 = doc.GetLineByNumber (2);
82+
strategy.IndentLine (doc, line2);
83+
84+
Assert.Equal (" hello\n world", doc.Text);
85+
}
86+
87+
[Fact]
88+
public void IndentLines_Does_Nothing ()
89+
{
90+
TextDocument doc = new ("hello\nworld\n");
91+
DefaultIndentationStrategy strategy = new ();
92+
93+
// IndentLines is a no-op for DefaultIndentationStrategy
94+
strategy.IndentLines (doc, 1, 2);
95+
96+
Assert.Equal ("hello\nworld\n", doc.Text);
97+
}
98+
99+
[Fact]
100+
public void IndentLine_Throws_On_Null_Document ()
101+
{
102+
DefaultIndentationStrategy strategy = new ();
103+
104+
Assert.Throws<ArgumentNullException> (() => strategy.IndentLine (null!, null!));
105+
}
106+
107+
[Fact]
108+
public void IndentLine_Throws_On_Null_Line ()
109+
{
110+
TextDocument doc = new ("hello");
111+
DefaultIndentationStrategy strategy = new ();
112+
113+
Assert.Throws<ArgumentNullException> (() => strategy.IndentLine (doc, null!));
114+
}
115+
}

0 commit comments

Comments
 (0)