Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AGENTS.md

All AI coding agents (GitHub Copilot, Codex, etc.) must read and follow the rules in [CLAUDE.md](CLAUDE.md).
1 change: 1 addition & 0 deletions Terminal.Gui.Editor.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Solution>
<Folder Name="/Docs/">
<File Path="AGENTS.md" />
<File Path="CLAUDE.md" />
<File Path="LICENSE" />
<File Path="README.md" />
Expand Down
1 change: 1 addition & 0 deletions examples/ted/TedApp.FileOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ internal void SetDocument (string text, string? filePath)
LanguageShortcut.Title = def?.Name ?? "Plain Text";

UpdateFileNameShortcut ();
InstallFolding ();
Editor.SetNeedsDraw ();
}

Expand Down
71 changes: 68 additions & 3 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Terminal.Gui.App;
using Terminal.Gui.Configuration;
using Terminal.Gui.Document;
using Terminal.Gui.Document.Folding;
using Terminal.Gui.Drawing;
using Terminal.Gui.Highlighting;
using Terminal.Gui.Input;
Expand All @@ -17,6 +18,7 @@ namespace Ted;
/// </summary>
public sealed partial class TedApp : Window
{
private readonly BraceFoldingStrategy _braceFoldingStrategy;
private readonly Shortcut _fileNameShortcut;

/// <summary>Initializes a new <see cref="TedApp" />.</summary>
Expand All @@ -29,7 +31,7 @@ public TedApp (bool readOnly = false)
// Editor's KeyBindings (any commands the editor doesn't claim fall back to Application).
Editor = new Editor
{
ShowLineNumbers = true,
GutterOptions = GutterOptions.LineNumbers | GutterOptions.Folding,
ConvertTabsToSpaces = true,
ReadOnly = readOnly,

Expand All @@ -39,6 +41,10 @@ public TedApp (bool readOnly = false)
HighlightingDefinition = HighlightingManager.Instance.GetDefinition ("C#")
};

// Enable brace-based folding. The strategy re-scans on each document change.
_braceFoldingStrategy = new BraceFoldingStrategy ();
InstallFolding ();

ShowOpenDialog = ShowDefaultOpenDialog;
ShowSaveDialog = ShowDefaultSaveDialog;
ShowSaveChangesDialog = ShowDefaultSaveChangesDialog;
Expand All @@ -53,7 +59,15 @@ public TedApp (bool readOnly = false)
AllowCheckStateNone = false,
CanFocus = false,
Text = "_Line Numbers",
Value = Editor.ShowLineNumbers ? CheckState.Checked : CheckState.UnChecked
Value = Editor.GutterOptions.HasFlag (GutterOptions.LineNumbers) ? CheckState.Checked : CheckState.UnChecked
};

CheckBox foldIndicatorsCheckBox = new ()
{
AllowCheckStateNone = false,
CanFocus = false,
Text = "_Fold Indicators",
Value = Editor.GutterOptions.HasFlag (GutterOptions.Folding) ? CheckState.Checked : CheckState.UnChecked
};

CheckBox convertTabsToSpacesCheckBox = new ()
Expand Down Expand Up @@ -167,13 +181,39 @@ public TedApp (bool readOnly = false)
{
Action = () =>
{
Editor.ShowLineNumbers = lineNumbersCheckBox.Value == CheckState.Checked;
if (lineNumbersCheckBox.Value == CheckState.Checked)
{
Editor.GutterOptions |= GutterOptions.LineNumbers;
}
else
{
Editor.GutterOptions &= ~GutterOptions.LineNumbers;
}

Editor.SetNeedsDraw ();
},
CommandView = lineNumbersCheckBox,
HelpText = "Show line numbers"
},
new MenuItem
{
Action = () =>
{
if (foldIndicatorsCheckBox.Value == CheckState.Checked)
{
Editor.GutterOptions |= GutterOptions.Folding;
}
else
{
Editor.GutterOptions &= ~GutterOptions.Folding;
}

Editor.SetNeedsDraw ();
},
CommandView = foldIndicatorsCheckBox,
HelpText = "Show fold indicators in the gutter"
},
new MenuItem
{
CommandView = convertTabsToSpacesCheckBox,
HelpText = "Insert spaces when Tab is pressed"
Expand Down Expand Up @@ -318,4 +358,29 @@ private static string FormatLoc (int line, int column)
{
return $"Ln: {line}, Ch: {column}";
}

/// <summary>
/// Creates a <see cref="FoldingManager" /> for the current document and wires up
/// automatic fold updates on document changes.
/// </summary>
private void InstallFolding ()
{
if (Editor.Document is null)
{
return;
}

FoldingManager fm = new (Editor.Document);
Editor.FoldingManager = fm;
_braceFoldingStrategy.UpdateFoldings (fm, Editor.Document);
Editor.Document.Changed += (_, _) => UpdateFoldings ();
}

private void UpdateFoldings ()
{
if (Editor.FoldingManager is not null && Editor.Document is not null)
{
_braceFoldingStrategy.UpdateFoldings (Editor.FoldingManager, Editor.Document);
}
}
}
42 changes: 41 additions & 1 deletion src/Terminal.Gui.Editor/Editor.Commands.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Terminal.Gui.Document;
using Terminal.Gui.Document.Folding;
using Terminal.Gui.Input;
using Terminal.Gui.ViewBase;

Expand All @@ -25,7 +26,8 @@ public partial class Editor
[Command.DeleteCharLeft] = Bind.All (Key.Backspace),
[Command.DeleteCharRight] = Bind.All (Key.Delete),
[Command.Undo] = Bind.All (Key.Z.WithCtrl),
[Command.Redo] = Bind.All (Key.Y.WithCtrl, Key.Z.WithCtrl.WithShift)
[Command.Redo] = Bind.All (Key.Y.WithCtrl, Key.Z.WithCtrl.WithShift),
[Command.Collapse] = Bind.All (Key.M.WithCtrl)
Comment thread
tig marked this conversation as resolved.
};

private void CreateCommandsAndBindings ()
Expand Down Expand Up @@ -112,6 +114,9 @@ private void CreateCommandsAndBindings ()
return true;
});

// Folding
AddCommand (Command.Collapse, ToggleFoldUnderCaret);

ApplyKeyBindings (View.DefaultKeyBindings, DefaultKeyBindings);

// Reclaim Tab before the framework consumes it; the editor handles Tab / Shift+Tab
Expand All @@ -123,6 +128,9 @@ private void CreateCommandsAndBindings ()
MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft);
MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight);

// Allow scroll commands from gutter subviews (hosted in Padding) to bubble up to this Editor.
CommandsToBubbleUp = [Command.ScrollUp, Command.ScrollDown, Command.ScrollLeft, Command.ScrollRight];
}

private bool? ExtendCommand (Action extend)
Expand Down Expand Up @@ -285,4 +293,36 @@ private void CreateCommandsAndBindings ()

return true;
}

private bool? ToggleFoldUnderCaret ()
{
if (FoldingManager is not { } fm || _document is null)
{
return true;
}

var caretOffset = CaretOffset;
DocumentLine caretLine = _document.GetLineByOffset (caretOffset);

// First, try to find a fold starting on this line.
FoldingSection? fold = fm.GetFoldingAtLine (caretLine.LineNumber);

// If none, try folds containing the caret.
if (fold is null)
{
foreach (FoldingSection fs in fm.GetFoldingsContaining (caretOffset))
{
fold = fs;

break;
}
}

if (fold is not null)
{
fold.IsFolded = !fold.IsFolded;
Comment thread
tig marked this conversation as resolved.
}

return true;
}
}
89 changes: 85 additions & 4 deletions src/Terminal.Gui.Editor/Editor.Drawing.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Drawing;
using Terminal.Gui.Document;
using Terminal.Gui.Document.Folding;
using Terminal.Gui.Drawing;
using Terminal.Gui.Drivers;
using Terminal.Gui.Highlighting;
Expand All @@ -12,6 +13,9 @@ namespace Terminal.Gui.Views;

public partial class Editor
{
/// <summary>Cached visible-line mapping; cleared when folds change or the document changes.</summary>
private List<int>? _cachedVisibleLineNumbers;

/// <inheritdoc />
protected override bool OnDrawingContent (DrawContext? context)
{
Expand Down Expand Up @@ -73,21 +77,98 @@ private void DrawVisibleLines (Rectangle viewport, Attribute normal, Attribute s
var visibleStart = viewport.X;
var visibleEnd = viewport.X + viewport.Width;

// Build a mapping from viewport row → document line number (1-based),
// skipping lines hidden by collapsed folds.
List<int> visibleLineNumbers = GetVisibleLineNumbers ();

for (var row = 0; row < viewport.Height; row++)
{
var lineIndex = viewport.Y + row;
var visibleIndex = viewport.Y + row;

Comment thread
tig marked this conversation as resolved.
if (lineIndex < 0 || lineIndex >= _document!.LineCount)
if (visibleIndex < 0 || visibleIndex >= visibleLineNumbers.Count)
{
break;
}

DocumentLine line = _document.GetLineByNumber (lineIndex + 1);
var lineNumber = visibleLineNumbers[visibleIndex];
DocumentLine line = _document!.GetLineByNumber (lineNumber);

DrawVisualLine (row, line, visibleStart, visibleEnd, null, normal, selected, selStart, selEnd);
}
}

/// <summary>
/// Returns a list of 1-based document line numbers that are visible (not hidden by folds),
/// in order. Cached until folds change.
/// </summary>
internal List<int> GetVisibleLineNumbers ()
{
if (_cachedVisibleLineNumbers is not null)
{
return _cachedVisibleLineNumbers;
Comment thread
tig marked this conversation as resolved.
}

List<int> result = new ();

if (_document is null)
{
_cachedVisibleLineNumbers = result;

return result;
}

FoldingManager? fm = FoldingManager;
var lineNumber = 1;

while (lineNumber <= _document.LineCount)
{
result.Add (lineNumber);

if (fm is not null)
{
// If there are folded sections starting on this line, skip past the deepest one.
var maxEndLine = lineNumber;

foreach (FoldingSection fs in fm.AllFoldings)
{
if (!fs.IsFolded)
{
continue;
}

DocumentLine startLine =
fm.Document.GetLineByOffset (Math.Clamp (fs.StartOffset, 0, fm.Document.TextLength));

if (startLine.LineNumber != lineNumber)
{
continue;
}

DocumentLine endLine =
fm.Document.GetLineByOffset (Math.Clamp (fs.EndOffset, 0, fm.Document.TextLength));

if (endLine.LineNumber > maxEndLine)
{
maxEndLine = endLine.LineNumber;
}
}

if (maxEndLine > lineNumber)
{
lineNumber = maxEndLine + 1;

continue;
}
}

lineNumber++;
}

_cachedVisibleLineNumbers = result;

return result;
}

/// <summary>
/// Rebuilds the <see cref="HighlightingColorizer" /> if the editor's normal attribute
/// has changed (e.g. after a scheme swap or focus change). Keeps the same underlying
Expand Down Expand Up @@ -170,7 +251,7 @@ private void UpdateCursor ()
}

Rectangle viewport = Viewport;
var caretLine = GetCaretLineIndex ();
var caretLine = GetCaretVisibleLineIndex ();
var caretCol = GetCaretColumn ();
var row = caretLine - viewport.Y;
var col = caretCol - viewport.X;
Expand Down
7 changes: 5 additions & 2 deletions src/Terminal.Gui.Editor/Editor.Mouse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ private int MousePositionToOffset (Point viewPos)
return 0;
}

var lineIndex = Math.Clamp (Viewport.Y + viewPos.Y, 0, _document.LineCount - 1);
DocumentLine line = _document.GetLineByNumber (lineIndex + 1);
// Map viewport row to document line via visible-line list (respects folding).
List<int> visibleLines = GetVisibleLineNumbers ();
var visibleIndex = Math.Clamp (Viewport.Y + viewPos.Y, 0, visibleLines.Count - 1);
var lineNumber = visibleLines[visibleIndex];
DocumentLine line = _document.GetLineByNumber (lineNumber);
var col = Math.Max (0, Viewport.X + viewPos.X);
var colInLine = GetOrBuildDefaultVisualLine (line).GetRelativeOffset (col);

Expand Down
17 changes: 15 additions & 2 deletions src/Terminal.Gui.Editor/Editor.Selection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ internal void SelectLineAtViewRow (int row)
return;
}

var lineNumber = Math.Clamp (Viewport.Y + row + 1, 1, _document.LineCount);
var lineNumber = ViewRowToLineNumber (row);
SelectLines (lineNumber, lineNumber);
}

Expand All @@ -268,7 +268,20 @@ internal int ViewRowToLineNumber (int row)
return 1;
}

return Math.Clamp (Viewport.Y + row + 1, 1, _document.LineCount);
List<int> visibleLines = GetVisibleLineNumbers ();
var visibleIndex = Viewport.Y + row;

if (visibleIndex < 0 || visibleLines.Count == 0)
{
return 1;
}

if (visibleIndex >= visibleLines.Count)
{
return visibleLines[^1];
}

return visibleLines[visibleIndex];
}

private bool IsIdentifierWordCharAt (int offset)
Expand Down
Loading
Loading