Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 examples/ted/TedApp.FileOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ internal void SetDocument (string text, string? filePath)
Editor.CaretOffset = 0;
CurrentFilePath = filePath;
UpdateFileNameShortcut ();
InstallFolding ();
Editor.SetNeedsDraw ();
}

Expand Down
33 changes: 32 additions & 1 deletion 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.Input;
using Terminal.Gui.Resources;
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 @@ -43,6 +45,10 @@ public TedApp (bool readOnly = false)
#pragma warning disable CS0618 // Type or member is obsolete
Editor.SyntaxHighlighter = new TextMateSyntaxHighlighter ();
#pragma warning restore CS0618 // Type or member is obsolete

// Enable brace-based folding. The strategy re-scans on each document change.
_braceFoldingStrategy = new BraceFoldingStrategy ();
InstallFolding ();
ShowOpenDialog = ShowDefaultOpenDialog;
ShowSaveDialog = ShowDefaultSaveDialog;
ShowSaveChangesDialog = ShowDefaultSaveChangesDialog;
Expand Down Expand Up @@ -262,7 +268,7 @@ private Key KeyFor (Command command)
private void ShowAboutDialog ()
{
Dialog dialog = new ()
{ Title = "About ted", Buttons = [new Button { Title = Strings.btnOk, IsDefault = true }] };
{ Title = "About ted", Buttons = [new Button { Title = Strings.btnOk, IsDefault = true }] };

dialog.Border.Settings &= ~BorderSettings.Title;

Expand Down Expand Up @@ -331,4 +337,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);
}
}
}
39 changes: 38 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 Down Expand Up @@ -253,4 +258,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;
}
}
69 changes: 66 additions & 3 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.ViewBase;
Expand All @@ -11,6 +12,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;

private ISyntaxHighlighter? _highlighterPreparedInstance;

// Syntax highlighter state optimization: tracks how far we've prepared so incremental
Expand Down Expand Up @@ -84,18 +88,23 @@ 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 ();

PrepareSyntaxHighlighter (syntaxHighlighter, viewport.Y);
Comment thread
tig marked this conversation as resolved.
Outdated

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);
#pragma warning disable CS0618 // Type or member is obsolete — see PrepareSyntaxHighlighter.
IReadOnlyList<StyledSegment>? segments =
syntaxHighlighter?.Highlight (_document.GetText (line), SyntaxLanguage);
Expand All @@ -105,6 +114,60 @@ private void DrawVisibleLines (Rectangle viewport, Attribute normal, Attribute s
}
}

/// <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's a folded section starting on this line, skip its hidden lines.
FoldingSection? fold = fm.GetFoldingAtLine (lineNumber);

if (fold is { IsFolded: true })
{
Comment thread
tig marked this conversation as resolved.
Outdated
DocumentLine endLine =
fm.Document.GetLineByOffset (Math.Clamp (fold.EndOffset, 0, fm.Document.TextLength));

if (endLine.LineNumber > lineNumber)
{
lineNumber = endLine.LineNumber + 1;

continue;
}
}
}

lineNumber++;
}

_cachedVisibleLineNumbers = result;

return result;
}

private void PrepareSyntaxHighlighter (ISyntaxHighlighter? syntaxHighlighter, int firstVisibleLineIndex)
{
if (syntaxHighlighter is null || _document is null)
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
88 changes: 86 additions & 2 deletions src/Terminal.Gui.Editor/Editor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Drawing;
using Terminal.Gui.Document;
using Terminal.Gui.Document.Folding;
using Terminal.Gui.Drawing;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views.Rendering;
Expand Down Expand Up @@ -277,6 +278,80 @@ public bool ShowTabs
/// <summary>Background renderers drawn before visual-line elements.</summary>
public IList<IBackgroundRenderer> BackgroundRenderers { get; } = [];

/// <summary>
/// Gets or sets the <see cref="Document.Folding.FoldingManager" /> that tracks collapsible regions.
/// Setting this installs a <see cref="FoldingTransformer" /> and subscribes to fold change events.
/// </summary>
public FoldingManager? FoldingManager
{
get;
set
{
if (field == value)
{
return;
}

if (field is not null)
{
field.FoldingChanged -= OnFoldingChanged;

// Remove the folding transformer installed by the previous manager.
for (var i = LineTransformers.Count - 1; i >= 0; i--)
{
if (LineTransformers[i] is FoldingTransformer)
{
LineTransformers.RemoveAt (i);
}
}
}

field = value;

if (field is not null)
{
field.FoldingChanged += OnFoldingChanged;
Comment thread
tig marked this conversation as resolved.
LineTransformers.Insert (0, new FoldingTransformer (field));
}

ClearVisualLineCaches ();
UpdateContentSize ();
SetNeedsDraw ();
Comment thread
tig marked this conversation as resolved.
Comment thread
tig marked this conversation as resolved.
}
}

private void OnFoldingChanged (object? sender, EventArgs e)
{
ClearVisualLineCaches ();
_cachedVisibleLineNumbers = null;
_maxWidthDirty = true;
UpdateContentSize ();
EnsureCaretNotInFold ();
Comment thread
tig marked this conversation as resolved.
Outdated
SetNeedsDraw ();
_gutter?.SetNeedsDraw ();
}

/// <summary>
/// If the caret is inside a collapsed fold, expand it so the caret stays visible.
/// </summary>
private void EnsureCaretNotInFold ()
{
if (FoldingManager is not { } fm)
{
return;
}

var caretOffset = CaretOffset;

foreach (FoldingSection fs in fm.GetFoldingsContaining (caretOffset))
{
if (fs.IsFolded && fs.StartOffset < caretOffset && caretOffset < fs.EndOffset)
{
fs.IsFolded = false;
}
}
}

/// <summary>Raised whenever <see cref="CaretOffset" /> changes.</summary>
public event EventHandler? CaretChanged;

Expand Down Expand Up @@ -333,6 +408,7 @@ private void OnDocumentChanged (object? sender, DocumentChangeEventArgs e)
// Cheap: usually one or a handful of entries; correctness > saving a few cache hits.
InvalidateVisualLineCaches (e);
InvalidateHighlighterState (e);
_cachedVisibleLineNumbers = null;
UpdateMaxWidthIncremental (e);
UpdateContentSize ();
UpdateLineNumberPadding ();
Expand Down Expand Up @@ -364,7 +440,8 @@ private void UpdateContentSize ()
}

// +1 column lets the caret sit just past the end-of-line.
SetContentSize (new Size (_maxVisualWidth + 1, _document.LineCount));
var visibleLines = _document.LineCount - (FoldingManager?.GetHiddenLineCount () ?? 0);
SetContentSize (new Size (_maxVisualWidth + 1, Math.Max (1, visibleLines)));
}

/// <summary>Full O(N) recompute — only called on Document swap, IndentationSize change, etc.</summary>
Expand Down Expand Up @@ -629,8 +706,15 @@ private void SyncGutter (int left)
private int GetLineNumberPaddingWidth ()
{
var lineCount = Math.Max (1, _document?.LineCount ?? 1);
var digitWidth = lineCount.ToString ().Length + 1;

// Add 2 columns for fold indicator when folding is active.
if (FoldingManager is not null)
{
digitWidth += 2;
}

return lineCount.ToString ().Length + 1;
return digitWidth;
}

private int GetCaretColumn ()
Expand Down
Loading
Loading