TextView.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. //
  2. // TextView.cs: multi-line text editing
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. //
  8. // TODO:
  9. // Attributed text on spans
  10. // Render selection
  11. // Mark/Delete/Cut commands
  12. // Replace insertion with Insert method
  13. // String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes
  14. //
  15. using System;
  16. using System.Collections.Generic;
  17. using System.IO;
  18. using System.Linq;
  19. using System.Text;
  20. using NStack;
  21. namespace Terminal.Gui {
  22. class TextModel {
  23. List<List<Rune>> lines;
  24. public bool LoadFile (string file)
  25. {
  26. if (file == null)
  27. throw new ArgumentNullException (nameof (file));
  28. try {
  29. var stream = File.OpenRead (file);
  30. if (stream == null)
  31. return false;
  32. } catch {
  33. return false;
  34. }
  35. LoadStream (File.OpenRead (file));
  36. return true;
  37. }
  38. // Turns the ustring into runes, this does not split the
  39. // contents on a newline if it is present.
  40. static List<Rune> ToRunes (ustring str)
  41. {
  42. List<Rune> runes = new List<Rune> ();
  43. foreach (var x in str.ToRunes ()) {
  44. runes.Add (x);
  45. }
  46. return runes;
  47. }
  48. // Splits a string into a List that contains a List<Rune> for each line
  49. public static List<List<Rune>> StringToRunes (ustring content)
  50. {
  51. var lines = new List<List<Rune>> ();
  52. int start = 0, i = 0;
  53. for (; i < content.Length; i++) {
  54. if (content [i] == 10) {
  55. if (i - start > 0)
  56. lines.Add (ToRunes (content [start, i]));
  57. else
  58. lines.Add (ToRunes (ustring.Empty));
  59. start = i + 1;
  60. }
  61. }
  62. if (i - start > 0)
  63. lines.Add (ToRunes (content [start, null]));
  64. return lines;
  65. }
  66. void Append (List<byte> line)
  67. {
  68. var str = ustring.Make (line.ToArray ());
  69. lines.Add (ToRunes (str));
  70. }
  71. public void LoadStream (Stream input)
  72. {
  73. if (input == null)
  74. throw new ArgumentNullException (nameof (input));
  75. lines = new List<List<Rune>> ();
  76. var buff = new BufferedStream (input);
  77. int v;
  78. var line = new List<byte> ();
  79. while ((v = buff.ReadByte ()) != -1) {
  80. if (v == 10) {
  81. Append (line);
  82. line.Clear ();
  83. continue;
  84. }
  85. line.Add ((byte)v);
  86. }
  87. if (line.Count > 0)
  88. Append (line);
  89. }
  90. public void LoadString (ustring content)
  91. {
  92. lines = StringToRunes (content);
  93. }
  94. public override string ToString ()
  95. {
  96. var sb = new StringBuilder ();
  97. foreach (var line in lines) {
  98. sb.Append (line);
  99. sb.AppendLine ();
  100. }
  101. return sb.ToString ();
  102. }
  103. public int Count => lines.Count;
  104. public List<Rune> GetLine (int line) => lines [line];
  105. public void AddLine (int pos, List<Rune> runes)
  106. {
  107. lines.Insert (pos, runes);
  108. }
  109. public void RemoveLine (int pos)
  110. {
  111. lines.RemoveAt (pos);
  112. }
  113. }
  114. /// <summary>
  115. /// Text data entry widget
  116. /// </summary>
  117. /// <remarks>
  118. /// The Entry widget provides Emacs-like editing
  119. /// functionality, and mouse support.
  120. /// </remarks>
  121. public class TextView : View {
  122. TextModel model = new TextModel ();
  123. int topRow;
  124. int leftColumn;
  125. int currentRow;
  126. int currentColumn;
  127. bool used;
  128. /// <summary>
  129. /// Changed event, raised when the text has clicked.
  130. /// </summary>
  131. /// <remarks>
  132. /// Client code can hook up to this event, it is
  133. /// raised when the text in the entry changes.
  134. /// </remarks>
  135. public event EventHandler Changed;
  136. /// <summary>
  137. /// Public constructor.
  138. /// </summary>
  139. /// <remarks>
  140. /// </remarks>
  141. public TextView (Rect frame) : base (frame)
  142. {
  143. CanFocus = true;
  144. }
  145. void ResetPosition ()
  146. {
  147. topRow = leftColumn = currentRow = currentColumn = 0;
  148. }
  149. /// <summary>
  150. /// Sets or gets the text in the entry.
  151. /// </summary>
  152. /// <remarks>
  153. /// </remarks>
  154. public ustring Text {
  155. get {
  156. return model.ToString ();
  157. }
  158. set {
  159. ResetPosition ();
  160. model.LoadString (value);
  161. SetNeedsDisplay ();
  162. }
  163. }
  164. /// <summary>
  165. /// The current cursor row.
  166. /// </summary>
  167. public int CurrentRow => currentRow;
  168. /// <summary>
  169. /// Gets the cursor column.
  170. /// </summary>
  171. /// <value>The cursor column.</value>
  172. public int CurrentColumn => currentColumn;
  173. /// <summary>
  174. /// Sets the cursor position.
  175. /// </summary>
  176. public override void PositionCursor ()
  177. {
  178. Move (CurrentColumn - leftColumn, CurrentRow - topRow);
  179. }
  180. void ClearRegion (int left, int top, int right, int bottom)
  181. {
  182. for (int row = top; row < bottom; row++) {
  183. Move (left, row);
  184. for (int col = left; col < right; col++)
  185. AddRune (col, row, ' ');
  186. }
  187. }
  188. /// <summary>
  189. /// Redraw the text editor region
  190. /// </summary>
  191. /// <param name="region">The region to redraw.</param>
  192. public override void Redraw (Rect region)
  193. {
  194. Driver.SetAttribute (ColorScheme.Focus);
  195. Move (0, 0);
  196. int bottom = region.Bottom;
  197. int right = region.Right;
  198. for (int row = region.Top; row < bottom; row++) {
  199. int textLine = topRow + row;
  200. if (textLine >= model.Count) {
  201. ClearRegion (region.Left, row, region.Right, row + 1);
  202. continue;
  203. }
  204. var line = model.GetLine (textLine);
  205. int lineRuneCount = line.Count;
  206. if (line.Count < region.Left){
  207. ClearRegion (region.Left, row, region.Right, row + 1);
  208. continue;
  209. }
  210. Move (region.Left, row);
  211. for (int col = region.Left; col < right; col++) {
  212. var lineCol = leftColumn + col;
  213. var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol];
  214. AddRune (col, row, rune);
  215. }
  216. }
  217. PositionCursor ();
  218. }
  219. public override bool CanFocus {
  220. get => true;
  221. set { base.CanFocus = value; }
  222. }
  223. void SetClipboard (ustring text)
  224. {
  225. Clipboard.Contents = text;
  226. }
  227. void AppendClipboard (ustring text)
  228. {
  229. Clipboard.Contents = Clipboard.Contents + text;
  230. }
  231. void Insert (Rune rune)
  232. {
  233. var line = GetCurrentLine ();
  234. line.Insert (currentColumn, rune);
  235. var prow = currentRow - topRow;
  236. SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1));
  237. }
  238. ustring StringFromRunes (List<Rune> runes)
  239. {
  240. if (runes == null)
  241. throw new ArgumentNullException (nameof (runes));
  242. int size = 0;
  243. foreach (var rune in runes) {
  244. size += Utf8.RuneLen (rune);
  245. }
  246. var encoded = new byte [size];
  247. int offset = 0;
  248. foreach (var rune in runes) {
  249. offset += Utf8.EncodeRune (rune, encoded, offset);
  250. }
  251. return ustring.Make (encoded);
  252. }
  253. List<Rune> GetCurrentLine () => model.GetLine (currentRow);
  254. void InsertText (ustring text)
  255. {
  256. var lines = TextModel.StringToRunes (text);
  257. if (lines.Count == 0)
  258. return;
  259. var line = GetCurrentLine ();
  260. // Optmize single line
  261. if (lines.Count == 1) {
  262. line.InsertRange (currentColumn, lines [0]);
  263. currentColumn += lines [0].Count;
  264. if (currentColumn - leftColumn > Frame.Width)
  265. leftColumn = currentColumn - Frame.Width + 1;
  266. SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, currentRow - topRow + 1));
  267. return;
  268. }
  269. // Keep a copy of the rest of the line
  270. var restCount = line.Count - currentColumn;
  271. var rest = line.GetRange (currentColumn, restCount);
  272. line.RemoveRange (currentColumn, restCount);
  273. // First line is inserted at the current location, the rest is appended
  274. line.InsertRange (currentColumn, lines [0]);
  275. for (int i = 1; i < lines.Count; i++)
  276. model.AddLine (currentRow + i, lines [i]);
  277. var last = model.GetLine (currentRow + lines.Count-1);
  278. var lastp = last.Count;
  279. last.InsertRange (last.Count, rest);
  280. // Now adjjust column and row positions
  281. currentRow += lines.Count-1;
  282. currentColumn = lastp;
  283. if (currentRow - topRow > Frame.Height) {
  284. topRow = currentRow - Frame.Height + 1;
  285. if (topRow < 0)
  286. topRow = 0;
  287. }
  288. if (currentColumn < leftColumn)
  289. leftColumn = currentColumn;
  290. if (currentColumn-leftColumn >= Frame.Width)
  291. leftColumn = currentColumn - Frame.Width + 1;
  292. SetNeedsDisplay ();
  293. }
  294. // The column we are tracking, or -1 if we are not tracking any column
  295. int columnTrack = -1;
  296. // Tries to snap the cursor to the tracking column
  297. void TrackColumn ()
  298. {
  299. // Now track the column
  300. var line = GetCurrentLine ();
  301. if (line.Count < columnTrack)
  302. currentColumn = line.Count;
  303. else if (columnTrack != -1)
  304. currentColumn = columnTrack;
  305. else if (currentColumn > line.Count)
  306. currentColumn = line.Count;
  307. if (currentColumn < leftColumn) {
  308. leftColumn = currentColumn;
  309. SetNeedsDisplay ();
  310. }
  311. if (currentColumn - leftColumn > Frame.Width) {
  312. leftColumn = currentColumn - Frame.Width + 1;
  313. SetNeedsDisplay ();
  314. }
  315. }
  316. bool lastWasKill;
  317. public override bool ProcessKey (KeyEvent kb)
  318. {
  319. int restCount;
  320. List<Rune> rest;
  321. switch (kb.Key) {
  322. case Key.ControlN:
  323. case Key.CursorDown:
  324. case Key.ControlP:
  325. case Key.CursorUp:
  326. lastWasKill = false;
  327. break;
  328. case Key.ControlK:
  329. break;
  330. default:
  331. lastWasKill = false;
  332. columnTrack = -1;
  333. break;
  334. }
  335. switch (kb.Key) {
  336. case Key.ControlN:
  337. case Key.CursorDown:
  338. if (currentRow + 1 < model.Count) {
  339. if (columnTrack == -1)
  340. columnTrack = currentColumn;
  341. currentRow++;
  342. if (currentRow >= topRow + Frame.Height) {
  343. topRow++;
  344. SetNeedsDisplay ();
  345. }
  346. TrackColumn ();
  347. PositionCursor ();
  348. }
  349. break;
  350. case Key.ControlP:
  351. case Key.CursorUp:
  352. if (currentRow > 0) {
  353. if (columnTrack == -1)
  354. columnTrack = currentColumn;
  355. currentRow--;
  356. if (currentRow < topRow) {
  357. topRow--;
  358. SetNeedsDisplay ();
  359. }
  360. TrackColumn ();
  361. PositionCursor ();
  362. }
  363. break;
  364. case Key.ControlF:
  365. case Key.CursorRight:
  366. var currentLine = GetCurrentLine ();
  367. if (currentColumn < currentLine.Count) {
  368. currentColumn++;
  369. if (currentColumn >= leftColumn + Frame.Width) {
  370. leftColumn++;
  371. SetNeedsDisplay ();
  372. }
  373. PositionCursor ();
  374. } else {
  375. if (currentRow + 1 < model.Count) {
  376. currentRow++;
  377. currentColumn = 0;
  378. leftColumn = 0;
  379. if (currentRow >= topRow + Frame.Height) {
  380. topRow++;
  381. }
  382. SetNeedsDisplay ();
  383. PositionCursor ();
  384. }
  385. break;
  386. }
  387. break;
  388. case Key.ControlB:
  389. case Key.CursorLeft:
  390. if (currentColumn > 0) {
  391. currentColumn--;
  392. if (currentColumn < leftColumn) {
  393. leftColumn--;
  394. SetNeedsDisplay ();
  395. }
  396. PositionCursor ();
  397. } else {
  398. if (currentRow > 0) {
  399. currentRow--;
  400. if (currentRow < topRow) {
  401. topRow--;
  402. }
  403. currentLine = GetCurrentLine ();
  404. currentColumn = currentLine.Count;
  405. int prev = leftColumn;
  406. leftColumn = currentColumn - Frame.Width + 1;
  407. if (leftColumn < 0)
  408. leftColumn = 0;
  409. if (prev != leftColumn)
  410. SetNeedsDisplay ();
  411. PositionCursor ();
  412. }
  413. }
  414. break;
  415. case Key.Delete:
  416. case Key.Backspace:
  417. if (currentColumn > 0) {
  418. // Delete backwards
  419. currentLine = GetCurrentLine ();
  420. currentLine.RemoveAt (currentColumn - 1);
  421. currentColumn--;
  422. if (currentColumn < leftColumn) {
  423. leftColumn--;
  424. SetNeedsDisplay ();
  425. } else
  426. SetNeedsDisplay (new Rect (0, currentRow - topRow, 1, Frame.Width));
  427. } else {
  428. // Merges the current line with the previous one.
  429. if (currentRow == 0)
  430. return true;
  431. var prowIdx = currentRow - 1;
  432. var prevRow = model.GetLine (prowIdx);
  433. var prevCount = prevRow.Count;
  434. model.GetLine (prowIdx).AddRange (GetCurrentLine ());
  435. currentRow--;
  436. currentColumn = prevCount;
  437. leftColumn = currentColumn - Frame.Width + 1;
  438. if (leftColumn < 0)
  439. leftColumn = 0;
  440. SetNeedsDisplay ();
  441. }
  442. break;
  443. // Home, C-A
  444. case Key.Home:
  445. case Key.ControlA:
  446. currentColumn = 0;
  447. if (currentColumn < leftColumn) {
  448. leftColumn = 0;
  449. SetNeedsDisplay ();
  450. } else
  451. PositionCursor ();
  452. break;
  453. case Key.ControlD: // Delete
  454. currentLine = GetCurrentLine ();
  455. if (currentColumn == currentLine.Count) {
  456. if (currentRow + 1 == model.Count)
  457. break;
  458. var nextLine = model.GetLine (currentRow + 1);
  459. currentLine.AddRange (nextLine);
  460. model.RemoveLine (currentRow + 1);
  461. var sr = currentRow - topRow;
  462. SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1));
  463. } else {
  464. currentLine.RemoveAt (currentColumn);
  465. var r = currentRow - topRow;
  466. SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1));
  467. }
  468. break;
  469. case Key.ControlE: // End
  470. currentLine = GetCurrentLine ();
  471. currentColumn = currentLine.Count;
  472. int pcol = leftColumn;
  473. leftColumn = currentColumn - Frame.Width + 1;
  474. if (leftColumn < 0)
  475. leftColumn = 0;
  476. if (pcol != leftColumn)
  477. SetNeedsDisplay ();
  478. PositionCursor ();
  479. break;
  480. case Key.ControlK: // kill-to-end
  481. currentLine = GetCurrentLine ();
  482. if (currentLine.Count == 0) {
  483. model.RemoveLine (currentRow);
  484. var val = ustring.Make ('\n');
  485. if (lastWasKill)
  486. AppendClipboard (val);
  487. else
  488. SetClipboard (val);
  489. } else {
  490. restCount = currentLine.Count - currentColumn;
  491. rest = currentLine.GetRange (currentColumn, restCount);
  492. var val = StringFromRunes (rest);
  493. if (lastWasKill)
  494. AppendClipboard (val);
  495. else
  496. SetClipboard (val);
  497. currentLine.RemoveRange (currentColumn, restCount);
  498. }
  499. SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height));
  500. lastWasKill = true;
  501. break;
  502. case Key.ControlY: // Control-y, yank
  503. InsertText (Clipboard.Contents);
  504. break;
  505. case (Key)((int)'b' + Key.AltMask):
  506. break;
  507. case (Key)((int)'f' + Key.AltMask):
  508. break;
  509. case Key.Enter:
  510. var orow = currentRow;
  511. currentLine = GetCurrentLine ();
  512. restCount = currentLine.Count - currentColumn;
  513. rest = currentLine.GetRange (currentColumn, restCount);
  514. currentLine.RemoveRange (currentColumn, restCount);
  515. model.AddLine (currentRow + 1, rest);
  516. currentRow++;
  517. bool fullNeedsDisplay = false;
  518. if (currentRow >= topRow + Frame.Height) {
  519. topRow++;
  520. fullNeedsDisplay = true;
  521. }
  522. currentColumn = 0;
  523. if (currentColumn < leftColumn) {
  524. fullNeedsDisplay = true;
  525. leftColumn = 0;
  526. }
  527. if (fullNeedsDisplay)
  528. SetNeedsDisplay ();
  529. else
  530. SetNeedsDisplay (new Rect (0, currentRow - topRow, 0, Frame.Height));
  531. break;
  532. default:
  533. // Ignore control characters and other special keys
  534. if (kb.Key < Key.Space || kb.Key > Key.CharMask)
  535. return false;
  536. Insert ((uint)kb.Key);
  537. currentColumn++;
  538. if (currentColumn >= leftColumn + Frame.Width) {
  539. leftColumn++;
  540. SetNeedsDisplay ();
  541. }
  542. PositionCursor ();
  543. return true;
  544. }
  545. return true;
  546. }
  547. #if false
  548. int WordForward (int p)
  549. {
  550. if (p >= text.Length)
  551. return -1;
  552. int i = p;
  553. if (Rune.IsPunctuation (text [p]) || Rune.IsWhiteSpace (text [p])) {
  554. for (; i < text.Length; i++) {
  555. var r = text [i];
  556. if (Rune.IsLetterOrDigit (r))
  557. break;
  558. }
  559. for (; i < text.Length; i++) {
  560. var r = text [i];
  561. if (!Rune.IsLetterOrDigit (r))
  562. break;
  563. }
  564. } else {
  565. for (; i < text.Length; i++) {
  566. var r = text [i];
  567. if (!Rune.IsLetterOrDigit (r))
  568. break;
  569. }
  570. }
  571. if (i != p)
  572. return i;
  573. return -1;
  574. }
  575. int WordBackward (int p)
  576. {
  577. if (p == 0)
  578. return -1;
  579. int i = p - 1;
  580. if (i == 0)
  581. return 0;
  582. var ti = text [i];
  583. if (Rune.IsPunctuation (ti) || Rune.IsSymbol (ti) || Rune.IsWhiteSpace (ti)) {
  584. for (; i >= 0; i--) {
  585. if (Rune.IsLetterOrDigit (text [i]))
  586. break;
  587. }
  588. for (; i >= 0; i--) {
  589. if (!Rune.IsLetterOrDigit (text [i]))
  590. break;
  591. }
  592. } else {
  593. for (; i >= 0; i--) {
  594. if (!Rune.IsLetterOrDigit (text [i]))
  595. break;
  596. }
  597. }
  598. i++;
  599. if (i != p)
  600. return i;
  601. return -1;
  602. }
  603. public override bool MouseEvent (MouseEvent ev)
  604. {
  605. if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked))
  606. return false;
  607. if (!HasFocus)
  608. SuperView.SetFocus (this);
  609. // We could also set the cursor position.
  610. point = first + ev.X;
  611. if (point > text.Length)
  612. point = text.Length;
  613. if (point < first)
  614. point = 0;
  615. SetNeedsDisplay ();
  616. return true;
  617. }
  618. #endif
  619. }
  620. }