TextModel.cs 32 KB

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