namespace Terminal.Gui.Views;
///
/// Represents the underlying data model for managing and manipulating multi-line text in a .
///
///
/// The class provides functionality for storing, retrieving, and modifying lines of text,
/// as well as supporting operations like word navigation, text search, and file loading. It is used internally
/// by text input controls such as to manage text content.
///
internal class TextModel
{
private List> _lines = new ();
private (Point startPointToFind, Point currentPointToFind, bool found) _toFind;
/// The number of text lines in the model
public int Count => _lines.Count;
public string? FilePath { get; set; }
/// Adds a line to the model at the specified position.
/// Line number where the line will be inserted.
/// The line of text and color, as a List of Cell.
public void AddLine (int pos, List cells) { _lines.Insert (pos, cells); }
public bool CloseFile ()
{
if (FilePath is null)
{
throw new ArgumentNullException (nameof (FilePath));
}
FilePath = null;
_lines = new ();
return true;
}
public List> GetAllLines () { return _lines; }
/// Returns the specified line as a List of Rune
/// The line.
/// Line number to retrieve.
public List GetLine (int line)
{
if (_lines.Count > 0)
{
if (line < Count)
{
return _lines [line];
}
return _lines [Count - 1];
}
_lines.Add (new ());
return _lines [0];
}
/// Returns the maximum line length of the visible lines.
/// The first line.
/// The last line.
/// The tab width.
public int GetMaxVisibleLine (int first, int last, int tabWidth)
{
var maxLength = 0;
last = last < _lines.Count ? last : _lines.Count;
for (int i = first; i < last; i++)
{
List line = GetLine (i);
int tabSum = line.Sum (c => c.Grapheme == "\t" ? Math.Max (tabWidth - 1, 0) : 0);
int l = line.Count + tabSum;
if (l > maxLength)
{
maxLength = l;
}
}
return maxLength;
}
public event EventHandler? LinesLoaded;
public bool LoadFile (string file)
{
FilePath = file ?? throw new ArgumentNullException (nameof (file));
using (FileStream stream = File.OpenRead (file))
{
LoadStream (stream);
return true;
}
}
public void LoadListCells (List> cellsList, Attribute? attribute)
{
_lines = cellsList;
SetAttributes (attribute);
OnLinesLoaded ();
}
public void LoadCells (List cells, Attribute? attribute)
{
_lines = Cell.ToCells (cells);
SetAttributes (attribute);
OnLinesLoaded ();
}
public void LoadStream (Stream input)
{
if (input is null)
{
throw new ArgumentNullException (nameof (input));
}
_lines = new ();
var buff = new BufferedStream (input);
int v;
List line = new ();
var wasNewLine = false;
while ((v = buff.ReadByte ()) != -1)
{
if (v == 13)
{
continue;
}
if (v == 10)
{
Append (line);
line.Clear ();
wasNewLine = true;
continue;
}
line.Add ((byte)v);
wasNewLine = false;
}
if (line.Count > 0 || wasNewLine)
{
Append (line);
}
buff.Dispose ();
OnLinesLoaded ();
}
public void LoadString (string content)
{
_lines = Cell.StringToLinesOfCells (content);
OnLinesLoaded ();
}
/// Removes the line at the specified position
/// Position.
public void RemoveLine (int pos)
{
if (_lines.Count > 0)
{
if (_lines.Count == 1 && _lines [0].Count == 0)
{
return;
}
_lines.RemoveAt (pos);
}
}
public void ReplaceLine (int pos, List runes)
{
if (_lines.Count > 0 && pos < _lines.Count)
{
_lines [pos] = [.. runes];
}
else if (_lines.Count == 0 || (_lines.Count > 0 && pos >= _lines.Count))
{
_lines.Add (runes);
}
}
public override string ToString ()
{
var sb = new StringBuilder ();
for (var i = 0; i < _lines.Count; i++)
{
sb.Append (Cell.ToString (_lines [i]));
if (i + 1 < _lines.Count)
{
sb.AppendLine ();
}
}
return sb.ToString ();
}
public (int col, int row)? WordBackward (int fromCol, int fromRow, bool useSameRuneType)
{
if (fromRow == 0 && fromCol == 0)
{
return null;
}
int col = Math.Max (fromCol - 1, 0);
int row = fromRow;
try
{
Cell? cell = RuneAt (col, row);
Rune rune;
if (cell is { })
{
rune = Rune.GetRuneAt (cell.Value.Grapheme, 0);
}
else
{
if (col > 0)
{
return (col, row);
}
if (col == 0 && row > 0)
{
row--;
List line = GetLine (row);
return (line.Count, row);
}
return null;
}
RuneType runeType = GetRuneType (rune);
int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune))
? col
: -1;
void ProcMovePrev (ref int nCol, ref int nRow, Rune nRune)
{
if (Rune.IsWhiteSpace (nRune))
{
while (MovePrev (ref nCol, ref nRow, out nRune, useSameRuneType))
{
lastValidCol = nCol;
if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))
{
rune = nRune;
runeType = GetRuneType (nRune);
}
}
if (lastValidCol > -1)
{
nCol = lastValidCol;
nRow = fromRow;
}
if ((!Rune.IsWhiteSpace (nRune) && Rune.IsWhiteSpace (rune))
|| (Rune.IsWhiteSpace (nRune) && !Rune.IsWhiteSpace (rune)))
{
return;
}
if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)))
{
List line = GetLine (nRow);
if (lastValidCol > -1)
{
nCol = lastValidCol + Math.Max (lastValidCol, line.Count);
}
}
}
else
{
if (!MovePrev (ref nCol, ref nRow, out nRune, useSameRuneType))
{
if (lastValidCol > -1)
{
nCol = lastValidCol;
nRow = fromRow;
}
return;
}
List line = GetLine (nRow);
Rune firstRune = Rune.GetRuneAt (line [0].Grapheme, 0);
if (nCol == 0
&& nRow == fromRow
&& (Rune.IsLetterOrDigit (firstRune) || Rune.IsPunctuation (firstRune) || Rune.IsSymbol (firstRune)))
{
return;
}
lastValidCol =
(IsSameRuneType (nRune, runeType, useSameRuneType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)
? nCol
: lastValidCol;
if (lastValidCol > -1 && Rune.IsWhiteSpace (nRune))
{
nCol = lastValidCol;
return;
}
if (fromRow != nRow)
{
nCol = line.Count;
return;
}
ProcMovePrev (ref nCol, ref nRow, nRune);
}
}
ProcMovePrev (ref col, ref row, rune);
if (fromCol != col || fromRow != row)
{
return (col, row);
}
if (fromCol == col && fromRow == row && row > 0)
{
row--;
List line = GetLine (row);
col = line.Count;
return (col, row);
}
return null;
}
catch (Exception)
{
return null;
}
}
public (int col, int row)? WordForward (int fromCol, int fromRow, bool useSameRuneType)
{
if (fromRow == _lines.Count - 1 && fromCol == GetLine (_lines.Count - 1).Count)
{
return null;
}
int col = fromCol;
int row = fromRow;
try
{
Rune rune = _lines [row].Count > 0 ? Rune.GetRuneAt (RuneAt (col, row)!.Value.Grapheme, 0) : default (Rune);
RuneType runeType = GetRuneType (rune);
int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune))
? col
: -1;
void ProcMoveNext (ref int nCol, ref int nRow, Rune nRune)
{
if (Rune.IsWhiteSpace (nRune))
{
while (MoveNext (ref nCol, ref nRow, out nRune, useSameRuneType))
{
lastValidCol = nCol;
if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))
{
return;
}
}
lastValidCol = nCol;
if (!Rune.IsWhiteSpace (nRune) && Rune.IsWhiteSpace (rune))
{
return;
}
if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)))
{
if (lastValidCol > -1)
{
nCol = lastValidCol;
}
return;
}
if (lastValidCol > -1)
{
nCol = lastValidCol;
nRow = fromRow;
}
}
else
{
if (!MoveNext (ref nCol, ref nRow, out nRune, useSameRuneType))
{
return;
}
lastValidCol = nCol;
if (!IsSameRuneType (nRune, runeType, useSameRuneType) && !Rune.IsWhiteSpace (nRune))
{
return;
}
List line = GetLine (nRow);
Rune firstRune = Rune.GetRuneAt (line [0].Grapheme, 0);
if (nCol == line.Count
&& nRow == fromRow
&& (Rune.IsLetterOrDigit (firstRune) || Rune.IsPunctuation (firstRune) || Rune.IsSymbol (firstRune)))
{
return;
}
if (fromRow != nRow)
{
nCol = 0;
return;
}
ProcMoveNext (ref nCol, ref nRow, nRune);
}
}
ProcMoveNext (ref col, ref row, rune);
if (fromCol != col || fromRow != row)
{
return (col, row);
}
return null;
}
catch (Exception)
{
return null;
}
}
public (int startCol, int col, int row)? ProcessDoubleClickSelection (int fromStartCol, int fromCol, int fromRow, bool useSameRuneType, bool selectWordOnly)
{
List line = GetLine (fromRow);
int startCol = fromStartCol;
int col = fromCol;
int row = fromRow;
(int col, int row)? newPos = WordForward (col, row, useSameRuneType);
if (newPos.HasValue)
{
col = row == newPos.Value.row ? newPos.Value.col : 0;
}
if (startCol > 0
&& StringExtensions.ToString (line.GetRange (startCol, col - startCol).Select (c => c.Grapheme).ToList ()).Trim () == ""
&& (col - startCol > 1 || (col - startCol > 0 && line [startCol - 1].Grapheme == " ")))
{
while (startCol > 0 && line [startCol - 1].Grapheme == " ")
{
startCol--;
}
}
else
{
newPos = WordBackward (col, row, useSameRuneType);
if (newPos is { })
{
startCol = row == newPos.Value.row ? newPos.Value.col : line.Count;
}
}
if (selectWordOnly)
{
List selText = line.GetRange (startCol, col - startCol).Select (c => c.Grapheme).ToList ();
if (StringExtensions.ToString (selText).Trim () != "")
{
for (int i = selText.Count - 1; i > -1; i--)
{
if (selText [i] == " ")
{
col--;
}
}
}
}
if (fromStartCol != startCol || fromCol != col || fromRow != row)
{
return (startCol, col, row);
}
return null;
}
internal static int CalculateLeftColumn (List t, int start, int end, int width, int tabWidth = 0)
{
List strings = new ();
foreach (Cell cell in t)
{
strings.Add (cell.Grapheme);
}
return CalculateLeftColumn (strings, start, end, width, tabWidth);
}
// Returns the left column in a range of the string.
internal static int CalculateLeftColumn (List t, int start, int end, int width, int tabWidth = 0)
{
if (t is null || t.Count == 0)
{
return 0;
}
var size = 0;
int tCount = end > t.Count - 1 ? t.Count - 1 : end;
var col = 0;
for (int i = tCount; i >= 0; i--)
{
string text = t [i];
size += text.GetColumns (false);
if (text == "\t")
{
size += tabWidth + 1;
}
if (size > width)
{
if (col + width == end)
{
col++;
}
break;
}
if ((end < t.Count && col > 0 && start < end && col == start) || end - col == width - 1)
{
break;
}
col = i;
}
return col;
}
internal static (int size, int length) DisplaySize (
List t,
int start = -1,
int end = -1,
bool checkNextText = true,
int tabWidth = 0
)
{
List strings = new ();
foreach (Cell cell in t)
{
strings.Add (cell.Grapheme);
}
return DisplaySize (strings, start, end, checkNextText, tabWidth);
}
// Returns the size and length in a range of the string.
internal static (int size, int length) DisplaySize (
List t,
int start = -1,
int end = -1,
bool checkNextRune = true,
int tabWidth = 0
)
{
if (t is null || t.Count == 0)
{
return (0, 0);
}
var size = 0;
var len = 0;
int tCount = end == -1 ? t.Count :
end > t.Count ? t.Count : end;
int i = start == -1 ? 0 : start;
for (; i < tCount; i++)
{
string text = t [i];
size += text.GetColumns (false);
len += text.Length;
if (text == "\t")
{
size += tabWidth + 1;
len += tabWidth - 1;
}
if (checkNextRune && i == tCount - 1 && t.Count > tCount && IsWideText (t [i + 1], tabWidth, out int s, out int l))
{
size += s;
len += l;
}
}
bool IsWideText (string s1, int tWidth, out int s, out int l)
{
s = s1.GetColumns ();
l = Encoding.Unicode.GetByteCount (s1);
if (s1 == "\t")
{
s += tWidth + 1;
l += tWidth - 1;
}
return s > 1;
}
return (size, len);
}
internal Size GetDisplaySize ()
{
var size = Size.Empty;
return size;
}
internal (Point current, bool found) FindNextText (
string text,
out bool gaveFullTurn,
bool matchCase = false,
bool matchWholeWord = false
)
{
if (text is null || _lines.Count == 0)
{
gaveFullTurn = false;
return (Point.Empty, false);
}
if (_toFind.found)
{
_toFind.currentPointToFind.X++;
}
(Point current, bool found) foundPos = GetFoundNextTextPoint (
text,
_lines.Count,
matchCase,
matchWholeWord,
_toFind.currentPointToFind
);
if (!foundPos.found && _toFind.currentPointToFind != _toFind.startPointToFind)
{
foundPos = GetFoundNextTextPoint (
text,
_toFind.startPointToFind.Y + 1,
matchCase,
matchWholeWord,
Point.Empty
);
}
gaveFullTurn = ApplyToFind (foundPos);
return foundPos;
}
internal (Point current, bool found) FindPreviousText (
string text,
out bool gaveFullTurn,
bool matchCase = false,
bool matchWholeWord = false
)
{
if (text is null || _lines.Count == 0)
{
gaveFullTurn = false;
return (Point.Empty, false);
}
if (_toFind.found)
{
_toFind.currentPointToFind.X++;
}
int linesCount = _toFind.currentPointToFind.IsEmpty ? _lines.Count - 1 : _toFind.currentPointToFind.Y;
(Point current, bool found) foundPos = GetFoundPreviousTextPoint (
text,
linesCount,
matchCase,
matchWholeWord,
_toFind.currentPointToFind
);
if (!foundPos.found && _toFind.currentPointToFind != _toFind.startPointToFind)
{
foundPos = GetFoundPreviousTextPoint (
text,
_lines.Count - 1,
matchCase,
matchWholeWord,
new (_lines [_lines.Count - 1].Count, _lines.Count)
);
}
gaveFullTurn = ApplyToFind (foundPos);
return foundPos;
}
internal static int GetColFromX (List t, int start, int x, int tabWidth = 0)
{
List strings = new ();
foreach (Cell cell in t)
{
strings.Add (cell.Grapheme);
}
return GetColFromX (strings, start, x, tabWidth);
}
internal static int GetColFromX (List t, int start, int x, int tabWidth = 0)
{
if (x < 0)
{
return x;
}
int size = start;
int pX = x + start;
for (int i = start; i < t.Count; i++)
{
string s = t [i];
size += s.GetColumns ();
if (s == "\t")
{
size += tabWidth + 1;
}
if (i == pX || size > pX)
{
return i - start;
}
}
return t.Count - start;
}
internal (Point current, bool found) ReplaceAllText (
string text,
bool matchCase = false,
bool matchWholeWord = false,
string? textToReplace = null
)
{
var found = false;
var pos = Point.Empty;
for (var i = 0; i < _lines.Count; i++)
{
List x = _lines [i];
string txt = GetText (x);
string matchText = !matchCase ? text.ToUpper () : text;
int col = txt.IndexOf (matchText);
while (col > -1)
{
if (matchWholeWord && !MatchWholeWord (txt, matchText, col))
{
if (col + 1 > txt.Length)
{
break;
}
col = txt.IndexOf (matchText, col + 1);
continue;
}
if (col > -1)
{
if (!found)
{
found = true;
}
_lines [i] = Cell.ToCellList (ReplaceText (x, textToReplace!, matchText, col));
x = _lines [i];
txt = GetText (x);
pos = new (col, i);
col += textToReplace!.Length - matchText.Length;
}
if (col < 0 || col + 1 > txt.Length)
{
break;
}
col = txt.IndexOf (matchText, col + 1);
}
}
string GetText (List x)
{
var txt = Cell.ToString (x);
if (!matchCase)
{
txt = txt.ToUpper ();
}
return txt;
}
return (pos, found);
}
/// Redefine column and line tracking.
/// Contains the column and line.
internal void ResetContinuousFind (Point point)
{
_toFind.startPointToFind = _toFind.currentPointToFind = point;
_toFind.found = false;
}
internal static bool SetCol (ref int col, int width, int cols)
{
if (col + cols <= width)
{
col += cols;
return true;
}
return false;
}
private void Append (List line)
{
var str = StringExtensions.ToString (line.ToArray ());
_lines.Add (Cell.StringToCells (str));
}
private bool ApplyToFind ((Point current, bool found) foundPos)
{
var gaveFullTurn = false;
if (foundPos.found)
{
_toFind.currentPointToFind = foundPos.current;
if (_toFind.found && _toFind.currentPointToFind == _toFind.startPointToFind)
{
gaveFullTurn = true;
}
if (!_toFind.found)
{
_toFind.startPointToFind = _toFind.currentPointToFind = foundPos.current;
_toFind.found = foundPos.found;
}
}
return gaveFullTurn;
}
private (Point current, bool found) GetFoundNextTextPoint (
string text,
int linesCount,
bool matchCase,
bool matchWholeWord,
Point start
)
{
for (int i = start.Y; i < linesCount; i++)
{
List x = _lines [i];
var txt = Cell.ToString (x);
if (!matchCase)
{
txt = txt.ToUpper ();
}
string matchText = !matchCase ? text.ToUpper () : text;
int col = txt.IndexOf (matchText, Math.Min (start.X, txt.Length));
if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col))
{
continue;
}
if (col > -1 && ((i == start.Y && col >= start.X) || i > start.Y) && txt.Contains (matchText))
{
return (new (col, i), true);
}
if (col == -1 && start.X > 0)
{
start.X = 0;
}
}
return (Point.Empty, false);
}
private (Point current, bool found) GetFoundPreviousTextPoint (
string text,
int linesCount,
bool matchCase,
bool matchWholeWord,
Point start
)
{
for (int i = linesCount; i >= 0; i--)
{
List x = _lines [i];
var txt = Cell.ToString (x);
if (!matchCase)
{
txt = txt.ToUpper ();
}
if (start.Y != i)
{
start.X = Math.Max (x.Count - 1, 0);
}
string matchText = !matchCase ? text.ToUpper () : text;
int col = txt.LastIndexOf (matchText, _toFind.found ? start.X - 1 : start.X);
if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col))
{
continue;
}
if (col > -1 && ((i <= linesCount && col <= start.X) || i < start.Y) && txt.Contains (matchText))
{
return (new (col, i), true);
}
}
return (Point.Empty, false);
}
private RuneType GetRuneType (Rune rune)
{
if (Rune.IsSymbol (rune))
{
return RuneType.IsSymbol;
}
if (Rune.IsWhiteSpace (rune))
{
return RuneType.IsWhiteSpace;
}
if (Rune.IsLetterOrDigit (rune))
{
return RuneType.IsLetterOrDigit;
}
if (Rune.IsPunctuation (rune))
{
return RuneType.IsPunctuation;
}
return RuneType.IsUnknown;
}
private bool IsSameRuneType (Rune newRune, RuneType runeType, bool useSameRuneType)
{
RuneType rt = GetRuneType (newRune);
if (useSameRuneType)
{
return rt == runeType;
}
switch (runeType)
{
case RuneType.IsSymbol:
case RuneType.IsPunctuation:
return rt is RuneType.IsSymbol or RuneType.IsPunctuation;
case RuneType.IsWhiteSpace:
case RuneType.IsLetterOrDigit:
case RuneType.IsUnknown:
return rt == runeType;
default:
throw new ArgumentOutOfRangeException (nameof (runeType), runeType, null);
}
}
private bool MatchWholeWord (string source, string matchText, int index = 0)
{
if (string.IsNullOrEmpty (source) || string.IsNullOrEmpty (matchText))
{
return false;
}
string txt = matchText.Trim ();
int start = index > 0 ? index - 1 : 0;
int end = index + txt.Length;
if ((start == 0 || Rune.IsWhiteSpace ((Rune)source [start])) && (end == source.Length || Rune.IsWhiteSpace ((Rune)source [end])))
{
return true;
}
return false;
}
private bool MoveNext (ref int col, ref int row, out Rune rune, bool useSameRuneType)
{
List line = GetLine (row);
if (col + 1 < line.Count)
{
col++;
rune = Rune.GetRuneAt (line [col].Grapheme, 0);
Rune prevRune = Rune.GetRuneAt (line [col - 1].Grapheme, 0);
if (col + 1 == line.Count
&& !Rune.IsLetterOrDigit (rune)
&& !Rune.IsWhiteSpace (prevRune)
&& IsSameRuneType (prevRune, GetRuneType (rune), useSameRuneType))
{
col++;
}
prevRune = Rune.GetRuneAt (line [col - 1].Grapheme, 0);
if (!Rune.IsWhiteSpace (rune)
&& (Rune.IsWhiteSpace (prevRune) || !IsSameRuneType (prevRune, GetRuneType (rune), useSameRuneType)))
{
return false;
}
return true;
}
if (col + 1 == line.Count)
{
col++;
rune = default (Rune);
return false;
}
// End of line
col = 0;
row++;
rune = default (Rune);
return false;
}
private bool MovePrev (ref int col, ref int row, out Rune rune, bool useSameRuneType)
{
List line = GetLine (row);
if (col > 0)
{
col--;
rune = Rune.GetRuneAt (line [col].Grapheme, 0);
Rune nextRune = Rune.GetRuneAt (line [col + 1].Grapheme, 0);
if ((!Rune.IsWhiteSpace (rune)
&& !Rune.IsWhiteSpace (nextRune)
&& !IsSameRuneType (nextRune, GetRuneType (rune), useSameRuneType))
|| (Rune.IsWhiteSpace (rune) && !Rune.IsWhiteSpace (nextRune)))
{
return false;
}
return true;
}
rune = default (Rune);
return false;
}
private void OnLinesLoaded () { LinesLoaded?.Invoke (this, EventArgs.Empty); }
private string ReplaceText (List source, string textToReplace, string matchText, int col)
{
var origTxt = Cell.ToString (source);
(_, int len) = DisplaySize (source, 0, col, false);
(_, int len2) = DisplaySize (source, col, col + matchText.Length, false);
(_, int len3) = DisplaySize (source, col + matchText.Length, origTxt.GetRuneCount (), false);
return origTxt [..len] + textToReplace + origTxt.Substring (len + len2, len3);
}
private Cell? RuneAt (int col, int row)
{
List line = GetLine (row);
if (line.Count > 0)
{
return line [col > line.Count - 1 ? line.Count - 1 : col];
}
return null;
}
private void SetAttributes (Attribute? attribute)
{
foreach (List| line in _lines)
{
for (var i = 0; i < line.Count; i++)
{
Cell cell = line [i];
cell.Attribute ??= attribute;
line [i] = cell;
}
}
}
private enum RuneType
{
IsSymbol,
IsWhiteSpace,
IsLetterOrDigit,
IsPunctuation,
IsUnknown
}
}
| | | | | | | | | | | | | | | | | | | | | | |