TextModel.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. #nullable enable
  2. namespace Terminal.Gui.Views;
  3. /// <summary>
  4. /// Represents the underlying data model for managing and manipulating multi-line text in a <see cref="TextView"/>.
  5. /// </summary>
  6. /// <remarks>
  7. /// The <see cref="TextModel"/> class provides functionality for storing, retrieving, and modifying lines of text,
  8. /// as well as supporting operations like word navigation, text search, and file loading. It is used internally
  9. /// by text input controls such as <see cref="TextView"/> to manage text content.
  10. /// </remarks>
  11. internal class TextModel
  12. {
  13. private List<List<Cell>> _lines = new ();
  14. private (Point startPointToFind, Point currentPointToFind, bool found) _toFind;
  15. /// <summary>The number of text lines in the model</summary>
  16. public int Count => _lines.Count;
  17. public string? FilePath { get; set; }
  18. /// <summary>Adds a line to the model at the specified position.</summary>
  19. /// <param name="pos">Line number where the line will be inserted.</param>
  20. /// <param name="cells">The line of text and color, as a List of Cell.</param>
  21. public void AddLine (int pos, List<Cell> cells) { _lines.Insert (pos, cells); }
  22. public bool CloseFile ()
  23. {
  24. if (FilePath is null)
  25. {
  26. throw new ArgumentNullException (nameof (FilePath));
  27. }
  28. FilePath = null;
  29. _lines = new ();
  30. return true;
  31. }
  32. public List<List<Cell>> GetAllLines () { return _lines; }
  33. /// <summary>Returns the specified line as a List of Rune</summary>
  34. /// <returns>The line.</returns>
  35. /// <param name="line">Line number to retrieve.</param>
  36. public List<Cell> GetLine (int line)
  37. {
  38. if (_lines.Count > 0)
  39. {
  40. if (line < Count)
  41. {
  42. return _lines [line];
  43. }
  44. return _lines [Count - 1];
  45. }
  46. _lines.Add (new ());
  47. return _lines [0];
  48. }
  49. /// <summary>Returns the maximum line length of the visible lines.</summary>
  50. /// <param name="first">The first line.</param>
  51. /// <param name="last">The last line.</param>
  52. /// <param name="tabWidth">The tab width.</param>
  53. public int GetMaxVisibleLine (int first, int last, int tabWidth)
  54. {
  55. var maxLength = 0;
  56. last = last < _lines.Count ? last : _lines.Count;
  57. for (int i = first; i < last; i++)
  58. {
  59. List<Cell> line = GetLine (i);
  60. int tabSum = line.Sum (c => c.Rune.Value == '\t' ? Math.Max (tabWidth - 1, 0) : 0);
  61. int l = line.Count + tabSum;
  62. if (l > maxLength)
  63. {
  64. maxLength = l;
  65. }
  66. }
  67. return maxLength;
  68. }
  69. public event EventHandler? LinesLoaded;
  70. public bool LoadFile (string file)
  71. {
  72. FilePath = file ?? throw new ArgumentNullException (nameof (file));
  73. using (FileStream stream = File.OpenRead (file))
  74. {
  75. LoadStream (stream);
  76. return true;
  77. }
  78. }
  79. public void LoadListCells (List<List<Cell>> cellsList, Attribute? attribute)
  80. {
  81. _lines = cellsList;
  82. SetAttributes (attribute);
  83. OnLinesLoaded ();
  84. }
  85. public void LoadCells (List<Cell> cells, Attribute? attribute)
  86. {
  87. _lines = Cell.ToCells (cells);
  88. SetAttributes (attribute);
  89. OnLinesLoaded ();
  90. }
  91. public void LoadStream (Stream input)
  92. {
  93. if (input is null)
  94. {
  95. throw new ArgumentNullException (nameof (input));
  96. }
  97. _lines = new ();
  98. var buff = new BufferedStream (input);
  99. int v;
  100. List<byte> line = new ();
  101. var wasNewLine = false;
  102. while ((v = buff.ReadByte ()) != -1)
  103. {
  104. if (v == 13)
  105. {
  106. continue;
  107. }
  108. if (v == 10)
  109. {
  110. Append (line);
  111. line.Clear ();
  112. wasNewLine = true;
  113. continue;
  114. }
  115. line.Add ((byte)v);
  116. wasNewLine = false;
  117. }
  118. if (line.Count > 0 || wasNewLine)
  119. {
  120. Append (line);
  121. }
  122. buff.Dispose ();
  123. OnLinesLoaded ();
  124. }
  125. public void LoadString (string content)
  126. {
  127. _lines = Cell.StringToLinesOfCells (content);
  128. OnLinesLoaded ();
  129. }
  130. /// <summary>Removes the line at the specified position</summary>
  131. /// <param name="pos">Position.</param>
  132. public void RemoveLine (int pos)
  133. {
  134. if (_lines.Count > 0)
  135. {
  136. if (_lines.Count == 1 && _lines [0].Count == 0)
  137. {
  138. return;
  139. }
  140. _lines.RemoveAt (pos);
  141. }
  142. }
  143. public void ReplaceLine (int pos, List<Cell> runes)
  144. {
  145. if (_lines.Count > 0 && pos < _lines.Count)
  146. {
  147. _lines [pos] = [.. runes];
  148. }
  149. else if (_lines.Count == 0 || (_lines.Count > 0 && pos >= _lines.Count))
  150. {
  151. _lines.Add (runes);
  152. }
  153. }
  154. public override string ToString ()
  155. {
  156. var sb = new StringBuilder ();
  157. for (var i = 0; i < _lines.Count; i++)
  158. {
  159. sb.Append (Cell.ToString (_lines [i]));
  160. if (i + 1 < _lines.Count)
  161. {
  162. sb.AppendLine ();
  163. }
  164. }
  165. return sb.ToString ();
  166. }
  167. public (int col, int row)? WordBackward (int fromCol, int fromRow, bool useSameRuneType)
  168. {
  169. if (fromRow == 0 && fromCol == 0)
  170. {
  171. return null;
  172. }
  173. int col = Math.Max (fromCol - 1, 0);
  174. int row = fromRow;
  175. try
  176. {
  177. Cell? cell = RuneAt (col, row);
  178. Rune rune;
  179. if (cell is { })
  180. {
  181. rune = cell.Value.Rune;
  182. }
  183. else
  184. {
  185. if (col > 0)
  186. {
  187. return (col, row);
  188. }
  189. if (col == 0 && row > 0)
  190. {
  191. row--;
  192. List<Cell> line = GetLine (row);
  193. return (line.Count, row);
  194. }
  195. return null;
  196. }
  197. RuneType runeType = GetRuneType (rune);
  198. int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune))
  199. ? col
  200. : -1;
  201. void ProcMovePrev (ref int nCol, ref int nRow, Rune nRune)
  202. {
  203. if (Rune.IsWhiteSpace (nRune))
  204. {
  205. while (MovePrev (ref nCol, ref nRow, out nRune, useSameRuneType))
  206. {
  207. lastValidCol = nCol;
  208. if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))
  209. {
  210. rune = nRune;
  211. runeType = GetRuneType (nRune);
  212. }
  213. }
  214. if (lastValidCol > -1)
  215. {
  216. nCol = lastValidCol;
  217. nRow = fromRow;
  218. }
  219. if ((!Rune.IsWhiteSpace (nRune) && Rune.IsWhiteSpace (rune))
  220. || (Rune.IsWhiteSpace (nRune) && !Rune.IsWhiteSpace (rune)))
  221. {
  222. return;
  223. }
  224. if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)))
  225. {
  226. List<Cell> line = GetLine (nRow);
  227. if (lastValidCol > -1)
  228. {
  229. nCol = lastValidCol + Math.Max (lastValidCol, line.Count);
  230. }
  231. }
  232. }
  233. else
  234. {
  235. if (!MovePrev (ref nCol, ref nRow, out nRune, useSameRuneType))
  236. {
  237. if (lastValidCol > -1)
  238. {
  239. nCol = lastValidCol;
  240. nRow = fromRow;
  241. }
  242. return;
  243. }
  244. List<Cell> line = GetLine (nRow);
  245. if (nCol == 0
  246. && nRow == fromRow
  247. && (Rune.IsLetterOrDigit (line [0].Rune) || Rune.IsPunctuation (line [0].Rune) || Rune.IsSymbol (line [0].Rune)))
  248. {
  249. return;
  250. }
  251. lastValidCol =
  252. (IsSameRuneType (nRune, runeType, useSameRuneType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)
  253. ? nCol
  254. : lastValidCol;
  255. if (lastValidCol > -1 && Rune.IsWhiteSpace (nRune))
  256. {
  257. nCol = lastValidCol;
  258. return;
  259. }
  260. if (fromRow != nRow)
  261. {
  262. nCol = line.Count;
  263. return;
  264. }
  265. ProcMovePrev (ref nCol, ref nRow, nRune);
  266. }
  267. }
  268. ProcMovePrev (ref col, ref row, rune);
  269. if (fromCol != col || fromRow != row)
  270. {
  271. return (col, row);
  272. }
  273. if (fromCol == col && fromRow == row && row > 0)
  274. {
  275. row--;
  276. List<Cell> line = GetLine (row);
  277. col = line.Count;
  278. return (col, row);
  279. }
  280. return null;
  281. }
  282. catch (Exception)
  283. {
  284. return null;
  285. }
  286. }
  287. public (int col, int row)? WordForward (int fromCol, int fromRow, bool useSameRuneType)
  288. {
  289. if (fromRow == _lines.Count - 1 && fromCol == GetLine (_lines.Count - 1).Count)
  290. {
  291. return null;
  292. }
  293. int col = fromCol;
  294. int row = fromRow;
  295. try
  296. {
  297. Rune rune = _lines [row].Count > 0 ? RuneAt (col, row)!.Value.Rune : default (Rune);
  298. RuneType runeType = GetRuneType (rune);
  299. int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune))
  300. ? col
  301. : -1;
  302. void ProcMoveNext (ref int nCol, ref int nRow, Rune nRune)
  303. {
  304. if (Rune.IsWhiteSpace (nRune))
  305. {
  306. while (MoveNext (ref nCol, ref nRow, out nRune, useSameRuneType))
  307. {
  308. lastValidCol = nCol;
  309. if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))
  310. {
  311. return;
  312. }
  313. }
  314. lastValidCol = nCol;
  315. if (!Rune.IsWhiteSpace (nRune) && Rune.IsWhiteSpace (rune))
  316. {
  317. return;
  318. }
  319. if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)))
  320. {
  321. if (lastValidCol > -1)
  322. {
  323. nCol = lastValidCol;
  324. }
  325. return;
  326. }
  327. if (lastValidCol > -1)
  328. {
  329. nCol = lastValidCol;
  330. nRow = fromRow;
  331. }
  332. }
  333. else
  334. {
  335. if (!MoveNext (ref nCol, ref nRow, out nRune, useSameRuneType))
  336. {
  337. return;
  338. }
  339. lastValidCol = nCol;
  340. if (!IsSameRuneType (nRune, runeType, useSameRuneType) && !Rune.IsWhiteSpace (nRune))
  341. {
  342. return;
  343. }
  344. List<Cell> line = GetLine (nRow);
  345. if (nCol == line.Count
  346. && nRow == fromRow
  347. && (Rune.IsLetterOrDigit (line [0].Rune) || Rune.IsPunctuation (line [0].Rune) || Rune.IsSymbol (line [0].Rune)))
  348. {
  349. return;
  350. }
  351. if (fromRow != nRow)
  352. {
  353. nCol = 0;
  354. return;
  355. }
  356. ProcMoveNext (ref nCol, ref nRow, nRune);
  357. }
  358. }
  359. ProcMoveNext (ref col, ref row, rune);
  360. if (fromCol != col || fromRow != row)
  361. {
  362. return (col, row);
  363. }
  364. return null;
  365. }
  366. catch (Exception)
  367. {
  368. return null;
  369. }
  370. }
  371. public (int startCol, int col, int row)? ProcessDoubleClickSelection (int fromStartCol, int fromCol, int fromRow, bool useSameRuneType, bool selectWordOnly)
  372. {
  373. List<Cell> line = GetLine (fromRow);
  374. int startCol = fromStartCol;
  375. int col = fromCol;
  376. int row = fromRow;
  377. (int col, int row)? newPos = WordForward (col, row, useSameRuneType);
  378. if (newPos.HasValue)
  379. {
  380. col = row == newPos.Value.row ? newPos.Value.col : 0;
  381. }
  382. if (startCol > 0
  383. && StringExtensions.ToString (line.GetRange (startCol, col - startCol).Select (c => c.Rune).ToList ()).Trim () == ""
  384. && (col - startCol > 1 || (col - startCol > 0 && line [startCol - 1].Rune == (Rune)' ')))
  385. {
  386. while (startCol > 0 && line [startCol - 1].Rune == (Rune)' ')
  387. {
  388. startCol--;
  389. }
  390. }
  391. else
  392. {
  393. newPos = WordBackward (col, row, useSameRuneType);
  394. if (newPos is { })
  395. {
  396. startCol = row == newPos.Value.row ? newPos.Value.col : line.Count;
  397. }
  398. }
  399. if (selectWordOnly)
  400. {
  401. List<Rune> selRunes = line.GetRange (startCol, col - startCol).Select (c => c.Rune).ToList ();
  402. if (StringExtensions.ToString (selRunes).Trim () != "")
  403. {
  404. for (int i = selRunes.Count - 1; i > -1; i--)
  405. {
  406. if (selRunes [i] == (Rune)' ')
  407. {
  408. col--;
  409. }
  410. }
  411. }
  412. }
  413. if (fromStartCol != startCol || fromCol != col || fromRow != row)
  414. {
  415. return (startCol, col, row);
  416. }
  417. return null;
  418. }
  419. internal static int CalculateLeftColumn (List<Cell> t, int start, int end, int width, int tabWidth = 0)
  420. {
  421. List<Rune> runes = new ();
  422. foreach (Cell cell in t)
  423. {
  424. runes.Add (cell.Rune);
  425. }
  426. return CalculateLeftColumn (runes, start, end, width, tabWidth);
  427. }
  428. // Returns the left column in a range of the string.
  429. internal static int CalculateLeftColumn (List<Rune> t, int start, int end, int width, int tabWidth = 0)
  430. {
  431. if (t is null || t.Count == 0)
  432. {
  433. return 0;
  434. }
  435. var size = 0;
  436. int tcount = end > t.Count - 1 ? t.Count - 1 : end;
  437. var col = 0;
  438. for (int i = tcount; i >= 0; i--)
  439. {
  440. Rune rune = t [i];
  441. size += rune.GetColumns ();
  442. if (rune.Value == '\t')
  443. {
  444. size += tabWidth + 1;
  445. }
  446. if (size > width)
  447. {
  448. if (col + width == end)
  449. {
  450. col++;
  451. }
  452. break;
  453. }
  454. if ((end < t.Count && col > 0 && start < end && col == start) || end - col == width - 1)
  455. {
  456. break;
  457. }
  458. col = i;
  459. }
  460. return col;
  461. }
  462. internal static (int size, int length) DisplaySize (
  463. List<Cell> t,
  464. int start = -1,
  465. int end = -1,
  466. bool checkNextRune = true,
  467. int tabWidth = 0
  468. )
  469. {
  470. List<Rune> runes = new ();
  471. foreach (Cell cell in t)
  472. {
  473. runes.Add (cell.Rune);
  474. }
  475. return DisplaySize (runes, start, end, checkNextRune, tabWidth);
  476. }
  477. // Returns the size and length in a range of the string.
  478. internal static (int size, int length) DisplaySize (
  479. List<Rune> t,
  480. int start = -1,
  481. int end = -1,
  482. bool checkNextRune = true,
  483. int tabWidth = 0
  484. )
  485. {
  486. if (t is null || t.Count == 0)
  487. {
  488. return (0, 0);
  489. }
  490. var size = 0;
  491. var len = 0;
  492. int tcount = end == -1 ? t.Count :
  493. end > t.Count ? t.Count : end;
  494. int i = start == -1 ? 0 : start;
  495. for (; i < tcount; i++)
  496. {
  497. Rune rune = t [i];
  498. size += rune.GetColumns ();
  499. len += rune.GetEncodingLength (Encoding.Unicode);
  500. if (rune.Value == '\t')
  501. {
  502. size += tabWidth + 1;
  503. len += tabWidth - 1;
  504. }
  505. if (checkNextRune && i == tcount - 1 && t.Count > tcount && IsWideRune (t [i + 1], tabWidth, out int s, out int l))
  506. {
  507. size += s;
  508. len += l;
  509. }
  510. }
  511. bool IsWideRune (Rune r, int tWidth, out int s, out int l)
  512. {
  513. s = r.GetColumns ();
  514. l = r.GetEncodingLength ();
  515. if (r.Value == '\t')
  516. {
  517. s += tWidth + 1;
  518. l += tWidth - 1;
  519. }
  520. return s > 1;
  521. }
  522. return (size, len);
  523. }
  524. internal Size GetDisplaySize ()
  525. {
  526. var size = Size.Empty;
  527. return size;
  528. }
  529. internal (Point current, bool found) FindNextText (
  530. string text,
  531. out bool gaveFullTurn,
  532. bool matchCase = false,
  533. bool matchWholeWord = false
  534. )
  535. {
  536. if (text is null || _lines.Count == 0)
  537. {
  538. gaveFullTurn = false;
  539. return (Point.Empty, false);
  540. }
  541. if (_toFind.found)
  542. {
  543. _toFind.currentPointToFind.X++;
  544. }
  545. (Point current, bool found) foundPos = GetFoundNextTextPoint (
  546. text,
  547. _lines.Count,
  548. matchCase,
  549. matchWholeWord,
  550. _toFind.currentPointToFind
  551. );
  552. if (!foundPos.found && _toFind.currentPointToFind != _toFind.startPointToFind)
  553. {
  554. foundPos = GetFoundNextTextPoint (
  555. text,
  556. _toFind.startPointToFind.Y + 1,
  557. matchCase,
  558. matchWholeWord,
  559. Point.Empty
  560. );
  561. }
  562. gaveFullTurn = ApplyToFind (foundPos);
  563. return foundPos;
  564. }
  565. internal (Point current, bool found) FindPreviousText (
  566. string text,
  567. out bool gaveFullTurn,
  568. bool matchCase = false,
  569. bool matchWholeWord = false
  570. )
  571. {
  572. if (text is null || _lines.Count == 0)
  573. {
  574. gaveFullTurn = false;
  575. return (Point.Empty, false);
  576. }
  577. if (_toFind.found)
  578. {
  579. _toFind.currentPointToFind.X++;
  580. }
  581. int linesCount = _toFind.currentPointToFind.IsEmpty ? _lines.Count - 1 : _toFind.currentPointToFind.Y;
  582. (Point current, bool found) foundPos = GetFoundPreviousTextPoint (
  583. text,
  584. linesCount,
  585. matchCase,
  586. matchWholeWord,
  587. _toFind.currentPointToFind
  588. );
  589. if (!foundPos.found && _toFind.currentPointToFind != _toFind.startPointToFind)
  590. {
  591. foundPos = GetFoundPreviousTextPoint (
  592. text,
  593. _lines.Count - 1,
  594. matchCase,
  595. matchWholeWord,
  596. new (_lines [_lines.Count - 1].Count, _lines.Count)
  597. );
  598. }
  599. gaveFullTurn = ApplyToFind (foundPos);
  600. return foundPos;
  601. }
  602. internal static int GetColFromX (List<Cell> t, int start, int x, int tabWidth = 0)
  603. {
  604. List<Rune> runes = new ();
  605. foreach (Cell cell in t)
  606. {
  607. runes.Add (cell.Rune);
  608. }
  609. return GetColFromX (runes, start, x, tabWidth);
  610. }
  611. internal static int GetColFromX (List<Rune> t, int start, int x, int tabWidth = 0)
  612. {
  613. if (x < 0)
  614. {
  615. return x;
  616. }
  617. int size = start;
  618. int pX = x + start;
  619. for (int i = start; i < t.Count; i++)
  620. {
  621. Rune r = t [i];
  622. size += r.GetColumns ();
  623. if (r.Value == '\t')
  624. {
  625. size += tabWidth + 1;
  626. }
  627. if (i == pX || size > pX)
  628. {
  629. return i - start;
  630. }
  631. }
  632. return t.Count - start;
  633. }
  634. internal (Point current, bool found) ReplaceAllText (
  635. string text,
  636. bool matchCase = false,
  637. bool matchWholeWord = false,
  638. string? textToReplace = null
  639. )
  640. {
  641. var found = false;
  642. var pos = Point.Empty;
  643. for (var i = 0; i < _lines.Count; i++)
  644. {
  645. List<Cell> x = _lines [i];
  646. string txt = GetText (x);
  647. string matchText = !matchCase ? text.ToUpper () : text;
  648. int col = txt.IndexOf (matchText);
  649. while (col > -1)
  650. {
  651. if (matchWholeWord && !MatchWholeWord (txt, matchText, col))
  652. {
  653. if (col + 1 > txt.Length)
  654. {
  655. break;
  656. }
  657. col = txt.IndexOf (matchText, col + 1);
  658. continue;
  659. }
  660. if (col > -1)
  661. {
  662. if (!found)
  663. {
  664. found = true;
  665. }
  666. _lines [i] = Cell.ToCellList (ReplaceText (x, textToReplace!, matchText, col));
  667. x = _lines [i];
  668. txt = GetText (x);
  669. pos = new (col, i);
  670. col += textToReplace!.Length - matchText.Length;
  671. }
  672. if (col < 0 || col + 1 > txt.Length)
  673. {
  674. break;
  675. }
  676. col = txt.IndexOf (matchText, col + 1);
  677. }
  678. }
  679. string GetText (List<Cell> x)
  680. {
  681. var txt = Cell.ToString (x);
  682. if (!matchCase)
  683. {
  684. txt = txt.ToUpper ();
  685. }
  686. return txt;
  687. }
  688. return (pos, found);
  689. }
  690. /// <summary>Redefine column and line tracking.</summary>
  691. /// <param name="point">Contains the column and line.</param>
  692. internal void ResetContinuousFind (Point point)
  693. {
  694. _toFind.startPointToFind = _toFind.currentPointToFind = point;
  695. _toFind.found = false;
  696. }
  697. internal static bool SetCol (ref int col, int width, int cols)
  698. {
  699. if (col + cols <= width)
  700. {
  701. col += cols;
  702. return true;
  703. }
  704. return false;
  705. }
  706. private void Append (List<byte> line)
  707. {
  708. var str = StringExtensions.ToString (line.ToArray ());
  709. _lines.Add (Cell.StringToCells (str));
  710. }
  711. private bool ApplyToFind ((Point current, bool found) foundPos)
  712. {
  713. var gaveFullTurn = false;
  714. if (foundPos.found)
  715. {
  716. _toFind.currentPointToFind = foundPos.current;
  717. if (_toFind.found && _toFind.currentPointToFind == _toFind.startPointToFind)
  718. {
  719. gaveFullTurn = true;
  720. }
  721. if (!_toFind.found)
  722. {
  723. _toFind.startPointToFind = _toFind.currentPointToFind = foundPos.current;
  724. _toFind.found = foundPos.found;
  725. }
  726. }
  727. return gaveFullTurn;
  728. }
  729. private (Point current, bool found) GetFoundNextTextPoint (
  730. string text,
  731. int linesCount,
  732. bool matchCase,
  733. bool matchWholeWord,
  734. Point start
  735. )
  736. {
  737. for (int i = start.Y; i < linesCount; i++)
  738. {
  739. List<Cell> x = _lines [i];
  740. var txt = Cell.ToString (x);
  741. if (!matchCase)
  742. {
  743. txt = txt.ToUpper ();
  744. }
  745. string matchText = !matchCase ? text.ToUpper () : text;
  746. int col = txt.IndexOf (matchText, Math.Min (start.X, txt.Length));
  747. if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col))
  748. {
  749. continue;
  750. }
  751. if (col > -1 && ((i == start.Y && col >= start.X) || i > start.Y) && txt.Contains (matchText))
  752. {
  753. return (new (col, i), true);
  754. }
  755. if (col == -1 && start.X > 0)
  756. {
  757. start.X = 0;
  758. }
  759. }
  760. return (Point.Empty, false);
  761. }
  762. private (Point current, bool found) GetFoundPreviousTextPoint (
  763. string text,
  764. int linesCount,
  765. bool matchCase,
  766. bool matchWholeWord,
  767. Point start
  768. )
  769. {
  770. for (int i = linesCount; i >= 0; i--)
  771. {
  772. List<Cell> x = _lines [i];
  773. var txt = Cell.ToString (x);
  774. if (!matchCase)
  775. {
  776. txt = txt.ToUpper ();
  777. }
  778. if (start.Y != i)
  779. {
  780. start.X = Math.Max (x.Count - 1, 0);
  781. }
  782. string matchText = !matchCase ? text.ToUpper () : text;
  783. int col = txt.LastIndexOf (matchText, _toFind.found ? start.X - 1 : start.X);
  784. if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col))
  785. {
  786. continue;
  787. }
  788. if (col > -1 && ((i <= linesCount && col <= start.X) || i < start.Y) && txt.Contains (matchText))
  789. {
  790. return (new (col, i), true);
  791. }
  792. }
  793. return (Point.Empty, false);
  794. }
  795. private RuneType GetRuneType (Rune rune)
  796. {
  797. if (Rune.IsSymbol (rune))
  798. {
  799. return RuneType.IsSymbol;
  800. }
  801. if (Rune.IsWhiteSpace (rune))
  802. {
  803. return RuneType.IsWhiteSpace;
  804. }
  805. if (Rune.IsLetterOrDigit (rune))
  806. {
  807. return RuneType.IsLetterOrDigit;
  808. }
  809. if (Rune.IsPunctuation (rune))
  810. {
  811. return RuneType.IsPunctuation;
  812. }
  813. return RuneType.IsUnknown;
  814. }
  815. private bool IsSameRuneType (Rune newRune, RuneType runeType, bool useSameRuneType)
  816. {
  817. RuneType rt = GetRuneType (newRune);
  818. if (useSameRuneType)
  819. {
  820. return rt == runeType;
  821. }
  822. switch (runeType)
  823. {
  824. case RuneType.IsSymbol:
  825. case RuneType.IsPunctuation:
  826. return rt is RuneType.IsSymbol or RuneType.IsPunctuation;
  827. case RuneType.IsWhiteSpace:
  828. case RuneType.IsLetterOrDigit:
  829. case RuneType.IsUnknown:
  830. return rt == runeType;
  831. default:
  832. throw new ArgumentOutOfRangeException (nameof (runeType), runeType, null);
  833. }
  834. }
  835. private bool MatchWholeWord (string source, string matchText, int index = 0)
  836. {
  837. if (string.IsNullOrEmpty (source) || string.IsNullOrEmpty (matchText))
  838. {
  839. return false;
  840. }
  841. string txt = matchText.Trim ();
  842. int start = index > 0 ? index - 1 : 0;
  843. int end = index + txt.Length;
  844. if ((start == 0 || Rune.IsWhiteSpace ((Rune)source [start])) && (end == source.Length || Rune.IsWhiteSpace ((Rune)source [end])))
  845. {
  846. return true;
  847. }
  848. return false;
  849. }
  850. private bool MoveNext (ref int col, ref int row, out Rune rune, bool useSameRuneType)
  851. {
  852. List<Cell> line = GetLine (row);
  853. if (col + 1 < line.Count)
  854. {
  855. col++;
  856. rune = line [col].Rune;
  857. if (col + 1 == line.Count
  858. && !Rune.IsLetterOrDigit (rune)
  859. && !Rune.IsWhiteSpace (line [col - 1].Rune)
  860. && IsSameRuneType (line [col - 1].Rune, GetRuneType (rune), useSameRuneType))
  861. {
  862. col++;
  863. }
  864. if (!Rune.IsWhiteSpace (rune)
  865. && (Rune.IsWhiteSpace (line [col - 1].Rune) || !IsSameRuneType (line [col - 1].Rune, GetRuneType (rune), useSameRuneType)))
  866. {
  867. return false;
  868. }
  869. return true;
  870. }
  871. if (col + 1 == line.Count)
  872. {
  873. col++;
  874. rune = default (Rune);
  875. return false;
  876. }
  877. // End of line
  878. col = 0;
  879. row++;
  880. rune = default (Rune);
  881. return false;
  882. }
  883. private bool MovePrev (ref int col, ref int row, out Rune rune, bool useSameRuneType)
  884. {
  885. List<Cell> line = GetLine (row);
  886. if (col > 0)
  887. {
  888. col--;
  889. rune = line [col].Rune;
  890. if ((!Rune.IsWhiteSpace (rune)
  891. && !Rune.IsWhiteSpace (line [col + 1].Rune)
  892. && !IsSameRuneType (line [col + 1].Rune, GetRuneType (rune), useSameRuneType))
  893. || (Rune.IsWhiteSpace (rune) && !Rune.IsWhiteSpace (line [col + 1].Rune)))
  894. {
  895. return false;
  896. }
  897. return true;
  898. }
  899. rune = default (Rune);
  900. return false;
  901. }
  902. private void OnLinesLoaded () { LinesLoaded?.Invoke (this, EventArgs.Empty); }
  903. private string ReplaceText (List<Cell> source, string textToReplace, string matchText, int col)
  904. {
  905. var origTxt = Cell.ToString (source);
  906. (_, int len) = DisplaySize (source, 0, col, false);
  907. (_, int len2) = DisplaySize (source, col, col + matchText.Length, false);
  908. (_, int len3) = DisplaySize (source, col + matchText.Length, origTxt.GetRuneCount (), false);
  909. return origTxt [..len] + textToReplace + origTxt.Substring (len + len2, len3);
  910. }
  911. private Cell? RuneAt (int col, int row)
  912. {
  913. List<Cell> line = GetLine (row);
  914. if (line.Count > 0)
  915. {
  916. return line [col > line.Count - 1 ? line.Count - 1 : col];
  917. }
  918. return null;
  919. }
  920. private void SetAttributes (Attribute? attribute)
  921. {
  922. foreach (List<Cell> line in _lines)
  923. {
  924. for (var i = 0; i < line.Count; i++)
  925. {
  926. Cell cell = line [i];
  927. cell.Attribute ??= attribute;
  928. line [i] = cell;
  929. }
  930. }
  931. }
  932. private enum RuneType
  933. {
  934. IsSymbol,
  935. IsWhiteSpace,
  936. IsLetterOrDigit,
  937. IsPunctuation,
  938. IsUnknown
  939. }
  940. }