Skip to content
Merged
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
98 changes: 98 additions & 0 deletions src/XTerm.NET.Tests/SelectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using XTerm.Options;

namespace XTerm.NET.Tests;

public class SelectionTests
{
[Fact]
public void SelectionText_RemainsAnchored_WhenViewportScrolls()
{
var terminal = new Terminal(new TerminalOptions { Rows = 5, Cols = 80, Scrollback = 100 });

for (int i = 0; i < 20; i++)
{
terminal.WriteLine($"Line{i:00}");
}

terminal.ScrollToTop();
terminal.Selection.StartSelection(4, 2);
terminal.Selection.UpdateSelection(5, 2);
terminal.Selection.EndSelection();

Assert.Equal("02", terminal.Selection.GetSelectionText());

terminal.ScrollLines(1);

Assert.Equal("02", terminal.Selection.GetSelectionText());
}

[Fact]
public void IsCellSelected_TracksBufferSelectionAcrossViewportScroll()
{
var terminal = new Terminal(new TerminalOptions { Rows = 5, Cols = 80, Scrollback = 100 });

for (int i = 0; i < 20; i++)
{
terminal.WriteLine($"Line{i:00}");
}

terminal.ScrollToTop();
terminal.Selection.StartSelection(4, 2);
terminal.Selection.UpdateSelection(5, 2);
terminal.Selection.EndSelection();

Assert.True(terminal.Selection.IsCellSelected(4, 2));
Assert.False(terminal.Selection.IsCellSelected(4, 1));

terminal.ScrollLines(1);

Assert.True(terminal.Selection.IsCellSelected(4, 1));
Assert.False(terminal.Selection.IsCellSelected(4, 2));
}

[Fact]
public void SelectAll_IncludesScrollback_NotJustViewport()
{
var terminal = new Terminal(new TerminalOptions { Rows = 3, Cols = 80, Scrollback = 20 });

for (int i = 0; i < 8; i++)
{
terminal.WriteLine($"Line{i}");
}

terminal.Selection.SelectAll();

var selectedText = terminal.Selection.GetSelectionText();

Assert.Contains("Line0", selectedText);
Assert.Contains("Line7", selectedText);
}

[Fact]
public void Selection_IsCleared_WhenTrimRemovesSelectedLines()
{
var terminal = new Terminal(new TerminalOptions { Rows = 3, Cols = 80, Scrollback = 2 });

for (int i = 0; i < 5; i++)
{
terminal.WriteLine($"Line{i}");
}

terminal.ScrollToTop();
var initialTopLine = terminal.GetVisibleLines()[0];
var expectedSelectedText = initialTopLine[4].ToString();
terminal.Selection.StartSelection(4, 0);
terminal.Selection.UpdateSelection(4, 0);
terminal.Selection.EndSelection();

Assert.Equal(expectedSelectedText, terminal.Selection.GetSelectionText());

for (int i = 5; i < 10; i++)
{
terminal.WriteLine($"Line{i}");
}

Assert.False(terminal.Selection.HasSelection);
Assert.Equal(string.Empty, terminal.Selection.GetSelectionText());
}
}
10 changes: 10 additions & 0 deletions src/XTerm.NET/Buffer/TerminalBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public int ViewportY

public CircularList<BufferLine> Lines => _lines;

/// <summary>
/// Fired when lines are trimmed from the start of the buffer.
/// </summary>
public event Action<int>? Trimmed;

/// <summary>
/// Saved cursor state for DECSC/DECRC.
/// </summary>
Expand Down Expand Up @@ -152,6 +157,11 @@ public void ScrollUp(int lines, bool isWrapped = false)
// Push the new line at the end (bottom of screen in buffer terms)
_lines.Push(newLine);

if (willBeRecycled)
{
Trimmed?.Invoke(1);
}

// Only increment yBase if the buffer didn't recycle
if (!willBeRecycled)
{
Expand Down
68 changes: 57 additions & 11 deletions src/XTerm.NET/Selection/SelectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public SelectionManager(Terminal terminal)
_terminal = terminal;
_isSelecting = false;
_selectionMode = SelectionMode.Normal;
_terminal.Buffer.Trimmed += HandleTrim;
}

/// <summary>
Expand All @@ -45,8 +46,9 @@ public void StartSelection(int x, int y, SelectionMode mode = SelectionMode.Norm
{
_isSelecting = true;
_selectionMode = mode;
_selectionStart = (x, y);
_selectionEnd = (x, y);
var absoluteY = ToAbsoluteY(y);
_selectionStart = (x, absoluteY);
_selectionEnd = (x, absoluteY);

// Adjust for word or line mode
if (mode == SelectionMode.Word)
Expand All @@ -69,7 +71,7 @@ public void UpdateSelection(int x, int y)
if (!_isSelecting || !_selectionStart.HasValue)
return;

_selectionEnd = (x, y);
_selectionEnd = (x, ToAbsoluteY(y));

// Adjust for selection mode
if (_selectionMode == SelectionMode.Word)
Expand Down Expand Up @@ -109,7 +111,7 @@ public void ClearSelection()
public void SelectAll()
{
_selectionStart = (0, 0);
_selectionEnd = (_terminal.Cols - 1, _terminal.Rows - 1);
_selectionEnd = (_terminal.Cols - 1, Math.Max(_terminal.Buffer.Lines.Length - 1, 0));
_isSelecting = false;
SelectionChanged?.Invoke();
}
Expand All @@ -136,7 +138,10 @@ public string GetSelectionText()

for (int y = start.y; y <= end.y; y++)
{
var line = buffer.Lines[buffer.YDisp + y];
if (y < 0 || y >= buffer.Lines.Length)
continue;

var line = buffer.Lines[y];
if (line == null)
continue;

Expand Down Expand Up @@ -164,6 +169,7 @@ public bool IsCellSelected(int x, int y)
if (!HasSelection)
return false;

var absoluteY = ToAbsoluteY(y);
var start = _selectionStart!.Value;
var end = _selectionEnd!.Value;

Expand All @@ -174,16 +180,16 @@ public bool IsCellSelected(int x, int y)
}

// Check if cell is in selection
if (y < start.y || y > end.y)
if (absoluteY < start.y || absoluteY > end.y)
return false;

if (y == start.y && y == end.y)
if (absoluteY == start.y && absoluteY == end.y)
return x >= start.x && x <= end.x;

if (y == start.y)
if (absoluteY == start.y)
return x >= start.x;

if (y == end.y)
if (absoluteY == end.y)
return x <= end.x;

return true;
Expand All @@ -202,7 +208,7 @@ private void ExpandSelectionToWord()
var end = _selectionEnd.Value;

// Expand start to word boundary
var startLine = buffer.Lines[buffer.YDisp + start.y];
var startLine = start.y >= 0 && start.y < buffer.Lines.Length ? buffer.Lines[start.y] : null;
if (startLine != null)
{
while (start.x > 0 && IsWordChar(startLine[start.x - 1].Content))
Expand All @@ -212,7 +218,7 @@ private void ExpandSelectionToWord()
}

// Expand end to word boundary
var endLine = buffer.Lines[buffer.YDisp + end.y];
var endLine = end.y >= 0 && end.y < buffer.Lines.Length ? buffer.Lines[end.y] : null;
if (endLine != null)
{
while (end.x < _terminal.Cols - 1 && IsWordChar(endLine[end.x + 1].Content))
Expand Down Expand Up @@ -261,6 +267,46 @@ private bool IsWordChar(string ch)
var c = ch[0];
return char.IsLetterOrDigit(c) || c == '_';
}

private int ToAbsoluteY(int viewportY)
{
return _terminal.Buffer.YDisp + viewportY;
}

private void HandleTrim(int amount)
{
if (amount <= 0)
return;

if (_selectionStart.HasValue)
{
_selectionStart = (_selectionStart.Value.x, _selectionStart.Value.y - amount);
}

if (_selectionEnd.HasValue)
{
_selectionEnd = (_selectionEnd.Value.x, _selectionEnd.Value.y - amount);
}

if (_selectionEnd.HasValue && _selectionEnd.Value.y < 0)
{
ClearSelection();
return;
}

if (_selectionStart.HasValue && _selectionStart.Value.y < 0)
{
_selectionStart = (0, 0);
}

if (_selectionEnd.HasValue)
{
var maxY = Math.Max(_terminal.Buffer.Lines.Length - 1, 0);
_selectionEnd = (_selectionEnd.Value.x, Math.Min(_selectionEnd.Value.y, maxY));
}

SelectionChanged?.Invoke();
}
}

/// <summary>
Expand Down
Loading