TextView.cs 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204
  1. //
  2. // TextView.cs: multi-line text editing
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. //
  8. // TODO:
  9. // In ReadOnly mode backspace/space behave like pageup/pagedown
  10. // Attributed text on spans
  11. // Replace insertion with Insert method
  12. // String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes
  13. // Alt-D, Alt-Backspace
  14. // API to set the cursor position
  15. // API to scroll to a particular place
  16. // keybindings to go to top/bottom
  17. // public API to insert, remove ranges
  18. // Add word forward/word backwards commands
  19. // Save buffer API
  20. // Mouse
  21. //
  22. // Desirable:
  23. // Move all the text manipulation into the TextModel
  24. using System;
  25. using System.Collections.Generic;
  26. using System.IO;
  27. using System.Linq;
  28. using System.Text;
  29. using NStack;
  30. namespace Terminal.Gui {
  31. class TextModel {
  32. List<List<Rune>> lines = new List<List<Rune>> ();
  33. public bool LoadFile (string file)
  34. {
  35. if (file == null)
  36. throw new ArgumentNullException (nameof (file));
  37. try {
  38. FilePath = file;
  39. var stream = File.OpenRead (file);
  40. } catch {
  41. return false;
  42. }
  43. LoadStream (File.OpenRead (file));
  44. return true;
  45. }
  46. public bool CloseFile ()
  47. {
  48. if (FilePath == null)
  49. throw new ArgumentNullException (nameof (FilePath));
  50. try {
  51. FilePath = null;
  52. lines = new List<List<Rune>> ();
  53. } catch {
  54. return false;
  55. }
  56. return true;
  57. }
  58. // Turns the ustring into runes, this does not split the
  59. // contents on a newline if it is present.
  60. internal static List<Rune> ToRunes (ustring str)
  61. {
  62. List<Rune> runes = new List<Rune> ();
  63. foreach (var x in str.ToRunes ()) {
  64. runes.Add (x);
  65. }
  66. return runes;
  67. }
  68. // Splits a string into a List that contains a List<Rune> for each line
  69. public static List<List<Rune>> StringToRunes (ustring content)
  70. {
  71. var lines = new List<List<Rune>> ();
  72. int start = 0, i = 0;
  73. for (; i < content.Length; i++) {
  74. if (content [i] == 10) {
  75. if (i - start > 0)
  76. lines.Add (ToRunes (content [start, i]));
  77. else
  78. lines.Add (ToRunes (ustring.Empty));
  79. start = i + 1;
  80. }
  81. }
  82. if (i - start >= 0)
  83. lines.Add (ToRunes (content [start, null]));
  84. return lines;
  85. }
  86. void Append (List<byte> line)
  87. {
  88. var str = ustring.Make (line.ToArray ());
  89. lines.Add (ToRunes (str));
  90. }
  91. public void LoadStream (Stream input)
  92. {
  93. if (input == null)
  94. throw new ArgumentNullException (nameof (input));
  95. lines = new List<List<Rune>> ();
  96. var buff = new BufferedStream (input);
  97. int v;
  98. var line = new List<byte> ();
  99. while ((v = buff.ReadByte ()) != -1) {
  100. if (v == 10) {
  101. Append (line);
  102. line.Clear ();
  103. continue;
  104. }
  105. line.Add ((byte)v);
  106. }
  107. if (line.Count > 0)
  108. Append (line);
  109. }
  110. public void LoadString (ustring content)
  111. {
  112. lines = StringToRunes (content);
  113. }
  114. public override string ToString ()
  115. {
  116. var sb = new StringBuilder ();
  117. foreach (var line in lines)
  118. {
  119. sb.Append (ustring.Make(line));
  120. sb.AppendLine ();
  121. }
  122. return sb.ToString ();
  123. }
  124. public string FilePath { get; set; }
  125. /// <summary>
  126. /// The number of text lines in the model
  127. /// </summary>
  128. public int Count => lines.Count;
  129. /// <summary>
  130. /// Returns the specified line as a List of Rune
  131. /// </summary>
  132. /// <returns>The line.</returns>
  133. /// <param name="line">Line number to retrieve.</param>
  134. public List<Rune> GetLine (int line) => line < Count ? lines [line]: lines[Count-1];
  135. /// <summary>
  136. /// Adds a line to the model at the specified position.
  137. /// </summary>
  138. /// <param name="pos">Line number where the line will be inserted.</param>
  139. /// <param name="runes">The line of text, as a List of Rune.</param>
  140. public void AddLine (int pos, List<Rune> runes)
  141. {
  142. lines.Insert (pos, runes);
  143. }
  144. /// <summary>
  145. /// Removes the line at the specified position
  146. /// </summary>
  147. /// <param name="pos">Position.</param>
  148. public void RemoveLine (int pos)
  149. {
  150. lines.RemoveAt (pos);
  151. }
  152. }
  153. /// <summary>
  154. /// Multi-line text editing view
  155. /// </summary>
  156. /// <remarks>
  157. /// <para>
  158. /// The text view provides a multi-line text view. Users interact
  159. /// with it with the standard Emacs commands for movement or the arrow
  160. /// keys.
  161. /// </para>
  162. /// <list type="table">
  163. /// <listheader>
  164. /// <term>Shortcut</term>
  165. /// <description>Action performed</description>
  166. /// </listheader>
  167. /// <item>
  168. /// <term>Left cursor, Control-b</term>
  169. /// <description>
  170. /// Moves the editing point left.
  171. /// </description>
  172. /// </item>
  173. /// <item>
  174. /// <term>Right cursor, Control-f</term>
  175. /// <description>
  176. /// Moves the editing point right.
  177. /// </description>
  178. /// </item>
  179. /// <item>
  180. /// <term>Alt-b</term>
  181. /// <description>
  182. /// Moves one word back.
  183. /// </description>
  184. /// </item>
  185. /// <item>
  186. /// <term>Alt-f</term>
  187. /// <description>
  188. /// Moves one word forward.
  189. /// </description>
  190. /// </item>
  191. /// <item>
  192. /// <term>Up cursor, Control-p</term>
  193. /// <description>
  194. /// Moves the editing point one line up.
  195. /// </description>
  196. /// </item>
  197. /// <item>
  198. /// <term>Down cursor, Control-n</term>
  199. /// <description>
  200. /// Moves the editing point one line down
  201. /// </description>
  202. /// </item>
  203. /// <item>
  204. /// <term>Home key, Control-a</term>
  205. /// <description>
  206. /// Moves the cursor to the beginning of the line.
  207. /// </description>
  208. /// </item>
  209. /// <item>
  210. /// <term>End key, Control-e</term>
  211. /// <description>
  212. /// Moves the cursor to the end of the line.
  213. /// </description>
  214. /// </item>
  215. /// <item>
  216. /// <term>Delete, Control-d</term>
  217. /// <description>
  218. /// Deletes the character in front of the cursor.
  219. /// </description>
  220. /// </item>
  221. /// <item>
  222. /// <term>Backspace</term>
  223. /// <description>
  224. /// Deletes the character behind the cursor.
  225. /// </description>
  226. /// </item>
  227. /// <item>
  228. /// <term>Control-k</term>
  229. /// <description>
  230. /// Deletes the text until the end of the line and replaces the kill buffer
  231. /// with the deleted text. You can paste this text in a different place by
  232. /// using Control-y.
  233. /// </description>
  234. /// </item>
  235. /// <item>
  236. /// <term>Control-y</term>
  237. /// <description>
  238. /// Pastes the content of the kill ring into the current position.
  239. /// </description>
  240. /// </item>
  241. /// <item>
  242. /// <term>Alt-d</term>
  243. /// <description>
  244. /// Deletes the word above the cursor and adds it to the kill ring. You
  245. /// can paste the contents of the kill ring with Control-y.
  246. /// </description>
  247. /// </item>
  248. /// <item>
  249. /// <term>Control-q</term>
  250. /// <description>
  251. /// Quotes the next input character, to prevent the normal processing of
  252. /// key handling to take place.
  253. /// </description>
  254. /// </item>
  255. /// </list>
  256. /// </remarks>
  257. public class TextView : View {
  258. TextModel model = new TextModel ();
  259. int topRow;
  260. int leftColumn;
  261. int currentRow;
  262. int currentColumn;
  263. int selectionStartColumn, selectionStartRow;
  264. bool selecting;
  265. //bool used;
  266. /// <summary>
  267. /// Raised when the Text of the TextView changes.
  268. /// </summary>
  269. public event EventHandler TextChanged;
  270. #if false
  271. /// <summary>
  272. /// Changed event, raised when the text has clicked.
  273. /// </summary>
  274. /// <remarks>
  275. /// Client code can hook up to this event, it is
  276. /// raised when the text in the entry changes.
  277. /// </remarks>
  278. public event EventHandler Changed;
  279. #endif
  280. /// <summary>
  281. /// Public constructor, creates a view on the specified area, with absolute position and size.
  282. /// </summary>
  283. /// <remarks>
  284. /// </remarks>
  285. public TextView (Rect frame) : base (frame)
  286. {
  287. CanFocus = true;
  288. }
  289. /// <summary>
  290. /// Public constructor, creates a view on the specified area, with dimensions controlled with the X, Y, Width and Height properties.
  291. /// </summary>
  292. public TextView () : base ()
  293. {
  294. CanFocus = true;
  295. }
  296. void ResetPosition ()
  297. {
  298. topRow = leftColumn = currentRow = currentColumn = 0;
  299. }
  300. /// <summary>
  301. /// Sets or gets the text in the entry.
  302. /// </summary>
  303. /// <remarks>
  304. /// </remarks>
  305. public ustring Text {
  306. get {
  307. return model.ToString ();
  308. }
  309. set {
  310. ResetPosition ();
  311. model.LoadString (value);
  312. TextChanged?.Invoke(this, new EventArgs());
  313. SetNeedsDisplay ();
  314. }
  315. }
  316. /// <summary>
  317. /// Loads the contents of the file into the TextView.
  318. /// </summary>
  319. /// <returns><c>true</c>, if file was loaded, <c>false</c> otherwise.</returns>
  320. /// <param name="path">Path to the file to load.</param>
  321. public bool LoadFile (string path)
  322. {
  323. if (path == null)
  324. throw new ArgumentNullException (nameof (path));
  325. ResetPosition ();
  326. var res = model.LoadFile (path);
  327. SetNeedsDisplay ();
  328. return res;
  329. }
  330. /// <summary>
  331. /// Loads the contents of the stream into the TextView.
  332. /// </summary>
  333. /// <returns><c>true</c>, if stream was loaded, <c>false</c> otherwise.</returns>
  334. /// <param name="stream">Stream to load the contents from.</param>
  335. public void LoadStream (Stream stream)
  336. {
  337. if (stream == null)
  338. throw new ArgumentNullException (nameof (stream));
  339. ResetPosition ();
  340. model.LoadStream(stream);
  341. SetNeedsDisplay ();
  342. }
  343. /// <summary>
  344. /// Closes the contents of the stream into the TextView.
  345. /// </summary>
  346. /// <returns><c>true</c>, if stream was closed, <c>false</c> otherwise.</returns>
  347. public bool CloseFile()
  348. {
  349. ResetPosition ();
  350. var res = model.CloseFile ();
  351. SetNeedsDisplay ();
  352. return res;
  353. }
  354. /// <summary>
  355. /// The current cursor row.
  356. /// </summary>
  357. public int CurrentRow => currentRow;
  358. /// <summary>
  359. /// Gets the cursor column.
  360. /// </summary>
  361. /// <value>The cursor column.</value>
  362. public int CurrentColumn => currentColumn;
  363. /// <summary>
  364. /// Positions the cursor on the current row and column
  365. /// </summary>
  366. public override void PositionCursor ()
  367. {
  368. if (selecting) {
  369. var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow)-topRow, 0), Frame.Height);
  370. var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Frame.Height);
  371. SetNeedsDisplay (new Rect (0, minRow, Frame.Width, maxRow));
  372. }
  373. Move (CurrentColumn - leftColumn, CurrentRow - topRow);
  374. }
  375. void ClearRegion (int left, int top, int right, int bottom)
  376. {
  377. for (int row = top; row < bottom; row++) {
  378. Move (left, row);
  379. for (int col = left; col < right; col++)
  380. AddRune (col, row, ' ');
  381. }
  382. }
  383. void ColorNormal ()
  384. {
  385. Driver.SetAttribute (ColorScheme.Normal);
  386. }
  387. void ColorSelection ()
  388. {
  389. if (HasFocus)
  390. Driver.SetAttribute (ColorScheme.Focus);
  391. else
  392. Driver.SetAttribute (ColorScheme.Normal);
  393. }
  394. bool isReadOnly = false;
  395. /// <summary>
  396. /// Indicates readonly attribute of TextView
  397. /// </summary>
  398. /// <value>Boolean value(Default false)</value>
  399. public bool ReadOnly {
  400. get => isReadOnly;
  401. set {
  402. isReadOnly = value;
  403. }
  404. }
  405. // Returns an encoded region start..end (top 32 bits are the row, low32 the column)
  406. void GetEncodedRegionBounds (out long start, out long end)
  407. {
  408. long selection = ((long)(uint)selectionStartRow << 32) | (uint)selectionStartColumn;
  409. long point = ((long)(uint)currentRow << 32) | (uint)currentColumn;
  410. if (selection > point) {
  411. start = point;
  412. end = selection;
  413. } else {
  414. start = selection;
  415. end = point;
  416. }
  417. }
  418. bool PointInSelection (int col, int row)
  419. {
  420. long start, end;
  421. GetEncodedRegionBounds (out start, out end);
  422. var q = ((long)(uint)row << 32) | (uint)col;
  423. return q >= start && q <= end;
  424. }
  425. //
  426. // Returns a ustring with the text in the selected
  427. // region.
  428. //
  429. ustring GetRegion ()
  430. {
  431. long start, end;
  432. GetEncodedRegionBounds (out start, out end);
  433. int startRow = (int)(start >> 32);
  434. var maxrow = ((int)(end >> 32));
  435. int startCol = (int)(start & 0xffffffff);
  436. var endCol = (int)(end & 0xffffffff);
  437. var line = model.GetLine (startRow);
  438. if (startRow == maxrow)
  439. return StringFromRunes (line.GetRange (startCol, endCol));
  440. ustring res = StringFromRunes (line.GetRange (startCol, line.Count - startCol));
  441. for (int row = startRow+1; row < maxrow; row++) {
  442. res = res + ustring.Make ((Rune)10) + StringFromRunes (model.GetLine (row));
  443. }
  444. line = model.GetLine (maxrow);
  445. res = res + ustring.Make ((Rune)10) + StringFromRunes (line.GetRange (0, endCol));
  446. return res;
  447. }
  448. //
  449. // Clears the contents of the selected region
  450. //
  451. void ClearRegion ()
  452. {
  453. long start, end;
  454. long currentEncoded = ((long)(uint)currentRow << 32) | (uint)currentColumn;
  455. GetEncodedRegionBounds (out start, out end);
  456. int startRow = (int)(start >> 32);
  457. var maxrow = ((int)(end >> 32));
  458. int startCol = (int)(start & 0xffffffff);
  459. var endCol = (int)(end & 0xffffffff);
  460. var line = model.GetLine (startRow);
  461. if (startRow == maxrow) {
  462. line.RemoveRange (startCol, endCol - startCol);
  463. currentColumn = startCol;
  464. SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1));
  465. return;
  466. }
  467. line.RemoveRange (startCol, line.Count - startCol);
  468. var line2 = model.GetLine (maxrow);
  469. line.AddRange (line2.Skip (endCol));
  470. for (int row = startRow + 1; row <= maxrow; row++) {
  471. model.RemoveLine (startRow+1);
  472. }
  473. if (currentEncoded == end) {
  474. currentRow -= maxrow - (startRow);
  475. }
  476. currentColumn = startCol;
  477. SetNeedsDisplay ();
  478. }
  479. /// <summary>
  480. /// Redraw the text editor region
  481. /// </summary>
  482. /// <param name="region">The region to redraw.</param>
  483. public override void Redraw (Rect region)
  484. {
  485. ColorNormal ();
  486. int bottom = region.Bottom;
  487. int right = region.Right;
  488. for (int row = region.Top; row < bottom; row++)
  489. {
  490. int textLine = topRow + row;
  491. if (textLine >= model.Count)
  492. {
  493. ColorNormal ();
  494. ClearRegion (region.Left, row, region.Right, row + 1);
  495. continue;
  496. }
  497. var line = model.GetLine (textLine);
  498. int lineRuneCount = line.Count;
  499. if (line.Count < region.Left)
  500. {
  501. ClearRegion (region.Left, row, region.Right, row + 1);
  502. continue;
  503. }
  504. Move (region.Left, row);
  505. for (int col = region.Left; col < right; col++)
  506. {
  507. var lineCol = leftColumn + col;
  508. var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol];
  509. if (selecting && PointInSelection (col, row))
  510. ColorSelection ();
  511. else
  512. ColorNormal ();
  513. AddRune (col, row, rune);
  514. }
  515. }
  516. PositionCursor ();
  517. }
  518. ///<inheritdoc cref="CanFocus"/>
  519. public override bool CanFocus {
  520. get => true;
  521. set { base.CanFocus = value; }
  522. }
  523. void SetClipboard (ustring text)
  524. {
  525. Clipboard.Contents = text;
  526. }
  527. void AppendClipboard (ustring text)
  528. {
  529. Clipboard.Contents = Clipboard.Contents + text;
  530. }
  531. void Insert (Rune rune)
  532. {
  533. var line = GetCurrentLine ();
  534. line.Insert (currentColumn, rune);
  535. var prow = currentRow - topRow;
  536. SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1));
  537. }
  538. ustring StringFromRunes (List<Rune> runes)
  539. {
  540. if (runes == null)
  541. throw new ArgumentNullException (nameof (runes));
  542. int size = 0;
  543. foreach (var rune in runes) {
  544. size += Utf8.RuneLen (rune);
  545. }
  546. var encoded = new byte [size];
  547. int offset = 0;
  548. foreach (var rune in runes) {
  549. offset += Utf8.EncodeRune (rune, encoded, offset);
  550. }
  551. return ustring.Make (encoded);
  552. }
  553. List<Rune> GetCurrentLine () => model.GetLine (currentRow);
  554. void InsertText (ustring text)
  555. {
  556. var lines = TextModel.StringToRunes (text);
  557. if (lines.Count == 0)
  558. return;
  559. var line = GetCurrentLine ();
  560. // Optmize single line
  561. if (lines.Count == 1) {
  562. line.InsertRange (currentColumn, lines [0]);
  563. currentColumn += lines [0].Count;
  564. if (currentColumn - leftColumn > Frame.Width)
  565. leftColumn = currentColumn - Frame.Width + 1;
  566. SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, currentRow - topRow + 1));
  567. return;
  568. }
  569. // Keep a copy of the rest of the line
  570. var restCount = line.Count - currentColumn;
  571. var rest = line.GetRange (currentColumn, restCount);
  572. line.RemoveRange (currentColumn, restCount);
  573. // First line is inserted at the current location, the rest is appended
  574. line.InsertRange (currentColumn, lines [0]);
  575. for (int i = 1; i < lines.Count; i++)
  576. model.AddLine (currentRow + i, lines [i]);
  577. var last = model.GetLine (currentRow + lines.Count-1);
  578. var lastp = last.Count;
  579. last.InsertRange (last.Count, rest);
  580. // Now adjjust column and row positions
  581. currentRow += lines.Count-1;
  582. currentColumn = lastp;
  583. if (currentRow - topRow > Frame.Height) {
  584. topRow = currentRow - Frame.Height + 1;
  585. if (topRow < 0)
  586. topRow = 0;
  587. }
  588. if (currentColumn < leftColumn)
  589. leftColumn = currentColumn;
  590. if (currentColumn-leftColumn >= Frame.Width)
  591. leftColumn = currentColumn - Frame.Width + 1;
  592. SetNeedsDisplay ();
  593. }
  594. // The column we are tracking, or -1 if we are not tracking any column
  595. int columnTrack = -1;
  596. // Tries to snap the cursor to the tracking column
  597. void TrackColumn ()
  598. {
  599. // Now track the column
  600. var line = GetCurrentLine ();
  601. if (line.Count < columnTrack)
  602. currentColumn = line.Count;
  603. else if (columnTrack != -1)
  604. currentColumn = columnTrack;
  605. else if (currentColumn > line.Count)
  606. currentColumn = line.Count;
  607. Adjust ();
  608. }
  609. void Adjust ()
  610. {
  611. bool need = false;
  612. if (currentColumn < leftColumn) {
  613. currentColumn = leftColumn;
  614. need = true;
  615. }
  616. if (currentColumn - leftColumn > Frame.Width) {
  617. leftColumn = currentColumn - Frame.Width + 1;
  618. need = true;
  619. }
  620. if (currentRow < topRow) {
  621. topRow = currentRow;
  622. need = true;
  623. }
  624. if (currentRow - topRow > Frame.Height) {
  625. topRow = currentRow - Frame.Height + 1;
  626. need = true;
  627. }
  628. if (need)
  629. SetNeedsDisplay ();
  630. else
  631. PositionCursor ();
  632. }
  633. /// <summary>
  634. /// Will scroll the view to display the specified row at the top
  635. /// </summary>
  636. /// <param name="row">Row that should be displayed at the top, if the value is negative it will be reset to zero</param>
  637. public void ScrollTo (int row)
  638. {
  639. if (row < 0)
  640. row = 0;
  641. topRow = row > model.Count ? model.Count - 1 : row;
  642. SetNeedsDisplay ();
  643. }
  644. bool lastWasKill;
  645. ///<inheritdoc cref="ProcessKey"/>
  646. public override bool ProcessKey (KeyEvent kb)
  647. {
  648. int restCount;
  649. List<Rune> rest;
  650. // Handle some state here - whether the last command was a kill
  651. // operation and the column tracking (up/down)
  652. switch (kb.Key) {
  653. case Key.ControlN:
  654. case Key.CursorDown:
  655. case Key.ControlP:
  656. case Key.CursorUp:
  657. lastWasKill = false;
  658. break;
  659. case Key.ControlK:
  660. break;
  661. default:
  662. lastWasKill = false;
  663. columnTrack = -1;
  664. break;
  665. }
  666. // Dispatch the command.
  667. switch (kb.Key) {
  668. case Key.PageDown:
  669. case Key.ControlV:
  670. int nPageDnShift = Frame.Height - 1;
  671. if (currentRow < model.Count) {
  672. if (columnTrack == -1)
  673. columnTrack = currentColumn;
  674. currentRow = (currentRow + nPageDnShift) > model.Count ? model.Count : currentRow + nPageDnShift;
  675. if (topRow < currentRow - nPageDnShift) {
  676. topRow = currentRow >= model.Count ? currentRow - nPageDnShift : topRow + nPageDnShift;
  677. SetNeedsDisplay ();
  678. }
  679. TrackColumn ();
  680. PositionCursor ();
  681. }
  682. break;
  683. case Key.PageUp:
  684. case ((int)'v' + Key.AltMask):
  685. int nPageUpShift = Frame.Height - 1;
  686. if (currentRow > 0) {
  687. if (columnTrack == -1)
  688. columnTrack = currentColumn;
  689. currentRow = currentRow - nPageUpShift < 0 ? 0 : currentRow - nPageUpShift;
  690. if (currentRow < topRow) {
  691. topRow = topRow - nPageUpShift < 0 ? 0 : topRow - nPageUpShift;
  692. SetNeedsDisplay ();
  693. }
  694. TrackColumn ();
  695. PositionCursor ();
  696. }
  697. break;
  698. case Key.ControlN:
  699. case Key.CursorDown:
  700. if (currentRow + 1 < model.Count) {
  701. if (columnTrack == -1)
  702. columnTrack = currentColumn;
  703. currentRow++;
  704. if (currentRow >= topRow + Frame.Height) {
  705. topRow++;
  706. SetNeedsDisplay ();
  707. }
  708. TrackColumn ();
  709. PositionCursor ();
  710. }
  711. break;
  712. case Key.ControlP:
  713. case Key.CursorUp:
  714. if (currentRow > 0) {
  715. if (columnTrack == -1)
  716. columnTrack = currentColumn;
  717. currentRow--;
  718. if (currentRow < topRow) {
  719. topRow--;
  720. SetNeedsDisplay ();
  721. }
  722. TrackColumn ();
  723. PositionCursor ();
  724. }
  725. break;
  726. case Key.ControlF:
  727. case Key.CursorRight:
  728. var currentLine = GetCurrentLine ();
  729. if (currentColumn < currentLine.Count) {
  730. currentColumn++;
  731. if (currentColumn >= leftColumn + Frame.Width) {
  732. leftColumn++;
  733. SetNeedsDisplay ();
  734. }
  735. PositionCursor ();
  736. } else {
  737. if (currentRow + 1 < model.Count) {
  738. currentRow++;
  739. currentColumn = 0;
  740. leftColumn = 0;
  741. if (currentRow >= topRow + Frame.Height) {
  742. topRow++;
  743. }
  744. SetNeedsDisplay ();
  745. PositionCursor ();
  746. }
  747. break;
  748. }
  749. break;
  750. case Key.ControlB:
  751. case Key.CursorLeft:
  752. if (currentColumn > 0) {
  753. currentColumn--;
  754. if (currentColumn < leftColumn) {
  755. leftColumn--;
  756. SetNeedsDisplay ();
  757. }
  758. PositionCursor ();
  759. } else {
  760. if (currentRow > 0) {
  761. currentRow--;
  762. if (currentRow < topRow) {
  763. topRow--;
  764. SetNeedsDisplay ();
  765. }
  766. currentLine = GetCurrentLine ();
  767. currentColumn = currentLine.Count;
  768. int prev = leftColumn;
  769. leftColumn = currentColumn - Frame.Width + 1;
  770. if (leftColumn < 0)
  771. leftColumn = 0;
  772. if (prev != leftColumn)
  773. SetNeedsDisplay ();
  774. PositionCursor ();
  775. }
  776. }
  777. break;
  778. case Key.Delete:
  779. case Key.Backspace:
  780. if (isReadOnly)
  781. break;
  782. if (currentColumn > 0) {
  783. // Delete backwards
  784. currentLine = GetCurrentLine ();
  785. currentLine.RemoveAt (currentColumn - 1);
  786. currentColumn--;
  787. if (currentColumn < leftColumn) {
  788. leftColumn--;
  789. SetNeedsDisplay ();
  790. } else
  791. SetNeedsDisplay (new Rect (0, currentRow - topRow, 1, Frame.Width));
  792. } else {
  793. // Merges the current line with the previous one.
  794. if (currentRow == 0)
  795. return true;
  796. var prowIdx = currentRow - 1;
  797. var prevRow = model.GetLine (prowIdx);
  798. var prevCount = prevRow.Count;
  799. model.GetLine (prowIdx).AddRange (GetCurrentLine ());
  800. model.RemoveLine (currentRow);
  801. currentRow--;
  802. currentColumn = prevCount;
  803. leftColumn = currentColumn - Frame.Width + 1;
  804. if (leftColumn < 0)
  805. leftColumn = 0;
  806. SetNeedsDisplay ();
  807. }
  808. break;
  809. // Home, C-A
  810. case Key.Home:
  811. case Key.ControlA:
  812. currentColumn = 0;
  813. if (currentColumn < leftColumn) {
  814. leftColumn = 0;
  815. SetNeedsDisplay ();
  816. } else
  817. PositionCursor ();
  818. break;
  819. case Key.DeleteChar:
  820. case Key.ControlD: // Delete
  821. if (isReadOnly)
  822. break;
  823. currentLine = GetCurrentLine ();
  824. if (currentColumn == currentLine.Count) {
  825. if (currentRow + 1 == model.Count)
  826. break;
  827. var nextLine = model.GetLine (currentRow + 1);
  828. currentLine.AddRange (nextLine);
  829. model.RemoveLine (currentRow + 1);
  830. var sr = currentRow - topRow;
  831. SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1));
  832. } else {
  833. currentLine.RemoveAt (currentColumn);
  834. var r = currentRow - topRow;
  835. SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1));
  836. }
  837. break;
  838. case Key.End:
  839. case Key.ControlE: // End
  840. currentLine = GetCurrentLine ();
  841. currentColumn = currentLine.Count;
  842. int pcol = leftColumn;
  843. leftColumn = currentColumn - Frame.Width + 1;
  844. if (leftColumn < 0)
  845. leftColumn = 0;
  846. if (pcol != leftColumn)
  847. SetNeedsDisplay ();
  848. PositionCursor ();
  849. break;
  850. case Key.ControlK: // kill-to-end
  851. if (isReadOnly)
  852. break;
  853. currentLine = GetCurrentLine ();
  854. if (currentLine.Count == 0) {
  855. model.RemoveLine (currentRow);
  856. var val = ustring.Make ((Rune)'\n');
  857. if (lastWasKill)
  858. AppendClipboard (val);
  859. else
  860. SetClipboard (val);
  861. } else {
  862. restCount = currentLine.Count - currentColumn;
  863. rest = currentLine.GetRange (currentColumn, restCount);
  864. var val = StringFromRunes (rest);
  865. if (lastWasKill)
  866. AppendClipboard (val);
  867. else
  868. SetClipboard (val);
  869. currentLine.RemoveRange (currentColumn, restCount);
  870. }
  871. SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height));
  872. lastWasKill = true;
  873. break;
  874. case Key.ControlY: // Control-y, yank
  875. if (isReadOnly)
  876. break;
  877. InsertText (Clipboard.Contents);
  878. selecting = false;
  879. break;
  880. case Key.ControlSpace:
  881. selecting = true;
  882. selectionStartColumn = currentColumn;
  883. selectionStartRow = currentRow;
  884. break;
  885. case ((int)'w' + Key.AltMask):
  886. SetClipboard (GetRegion ());
  887. selecting = false;
  888. break;
  889. case Key.ControlW:
  890. SetClipboard (GetRegion ());
  891. if (!isReadOnly)
  892. ClearRegion ();
  893. selecting = false;
  894. break;
  895. case (Key)((int)'b' + Key.AltMask):
  896. var newPos = WordBackward (currentColumn, currentRow);
  897. if (newPos.HasValue) {
  898. currentColumn = newPos.Value.col;
  899. currentRow = newPos.Value.row;
  900. }
  901. Adjust ();
  902. break;
  903. case (Key)((int)'f' + Key.AltMask):
  904. newPos = WordForward (currentColumn, currentRow);
  905. if (newPos.HasValue) {
  906. currentColumn = newPos.Value.col;
  907. currentRow = newPos.Value.row;
  908. }
  909. Adjust ();
  910. break;
  911. case Key.Enter:
  912. if (isReadOnly)
  913. break;
  914. var orow = currentRow;
  915. currentLine = GetCurrentLine ();
  916. restCount = currentLine.Count - currentColumn;
  917. rest = currentLine.GetRange (currentColumn, restCount);
  918. currentLine.RemoveRange (currentColumn, restCount);
  919. model.AddLine (currentRow + 1, rest);
  920. currentRow++;
  921. bool fullNeedsDisplay = false;
  922. if (currentRow >= topRow + Frame.Height) {
  923. topRow++;
  924. fullNeedsDisplay = true;
  925. }
  926. currentColumn = 0;
  927. if (currentColumn < leftColumn) {
  928. fullNeedsDisplay = true;
  929. leftColumn = 0;
  930. }
  931. if (fullNeedsDisplay)
  932. SetNeedsDisplay ();
  933. else
  934. SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height));
  935. break;
  936. default:
  937. // Ignore control characters and other special keys
  938. if (kb.Key < Key.Space || kb.Key > Key.CharMask)
  939. return false;
  940. //So that special keys like tab can be processed
  941. if (isReadOnly)
  942. return true;
  943. Insert ((uint)kb.Key);
  944. currentColumn++;
  945. if (currentColumn >= leftColumn + Frame.Width) {
  946. leftColumn++;
  947. SetNeedsDisplay ();
  948. }
  949. PositionCursor ();
  950. return true;
  951. }
  952. return true;
  953. }
  954. IEnumerable<(int col, int row, Rune rune)> ForwardIterator (int col, int row)
  955. {
  956. if (col < 0 || row < 0)
  957. yield break;
  958. if (row >= model.Count)
  959. yield break;
  960. var line = GetCurrentLine ();
  961. if (col >= line.Count)
  962. yield break;
  963. while (row < model.Count) {
  964. for (int c = col; c < line.Count; c++) {
  965. yield return (c, row, line [c]);
  966. }
  967. col = 0;
  968. row++;
  969. line = GetCurrentLine ();
  970. }
  971. }
  972. Rune RuneAt (int col, int row) => model.GetLine (row) [col];
  973. bool MoveNext (ref int col, ref int row, out Rune rune)
  974. {
  975. var line = model.GetLine (row);
  976. if (col + 1 < line.Count) {
  977. col++;
  978. rune = line [col];
  979. return true;
  980. }
  981. while (row + 1 < model.Count){
  982. col = 0;
  983. row++;
  984. line = model.GetLine (row);
  985. if (line.Count > 0) {
  986. rune = line [0];
  987. return true;
  988. }
  989. }
  990. rune = 0;
  991. return false;
  992. }
  993. bool MovePrev (ref int col, ref int row, out Rune rune)
  994. {
  995. var line = model.GetLine (row);
  996. if (col > 0) {
  997. col--;
  998. rune = line [col];
  999. return true;
  1000. }
  1001. if (row == 0) {
  1002. rune = 0;
  1003. return false;
  1004. }
  1005. while (row > 0) {
  1006. row--;
  1007. line = model.GetLine (row);
  1008. col = line.Count - 1;
  1009. if (col >= 0) {
  1010. rune = line [col];
  1011. return true;
  1012. }
  1013. }
  1014. rune = 0;
  1015. return false;
  1016. }
  1017. (int col, int row)? WordForward (int fromCol, int fromRow)
  1018. {
  1019. var col = fromCol;
  1020. var row = fromRow;
  1021. var line = GetCurrentLine ();
  1022. var rune = RuneAt (col, row);
  1023. var srow = row;
  1024. if (Rune.IsPunctuation (rune) || Rune.IsWhiteSpace (rune)) {
  1025. while (MoveNext (ref col, ref row, out rune)){
  1026. if (Rune.IsLetterOrDigit (rune))
  1027. break;
  1028. }
  1029. while (MoveNext (ref col, ref row, out rune)) {
  1030. if (!Rune.IsLetterOrDigit (rune))
  1031. break;
  1032. }
  1033. } else {
  1034. while (MoveNext (ref col, ref row, out rune)) {
  1035. if (!Rune.IsLetterOrDigit (rune))
  1036. break;
  1037. }
  1038. }
  1039. if (fromCol != col || fromRow != row)
  1040. return (col, row);
  1041. return null;
  1042. }
  1043. (int col, int row)? WordBackward (int fromCol, int fromRow)
  1044. {
  1045. if (fromRow == 0 && fromCol == 0)
  1046. return null;
  1047. var col = fromCol;
  1048. var row = fromRow;
  1049. var line = GetCurrentLine ();
  1050. var rune = RuneAt (col, row);
  1051. if (Rune.IsPunctuation (rune) || Rune.IsSymbol (rune) || Rune.IsWhiteSpace (rune)) {
  1052. while (MovePrev (ref col, ref row, out rune)){
  1053. if (Rune.IsLetterOrDigit (rune))
  1054. break;
  1055. }
  1056. while (MovePrev (ref col, ref row, out rune)){
  1057. if (!Rune.IsLetterOrDigit (rune))
  1058. break;
  1059. }
  1060. } else {
  1061. while (MovePrev (ref col, ref row, out rune)) {
  1062. if (!Rune.IsLetterOrDigit (rune))
  1063. break;
  1064. }
  1065. }
  1066. if (fromCol != col || fromRow != row)
  1067. return (col, row);
  1068. return null;
  1069. }
  1070. ///<inheritdoc cref="MouseEvent"/>
  1071. public override bool MouseEvent (MouseEvent ev)
  1072. {
  1073. if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  1074. return false;
  1075. }
  1076. if (!HasFocus)
  1077. SuperView.SetFocus (this);
  1078. if (model.Count > 0) {
  1079. var maxCursorPositionableLine = (model.Count - 1) - topRow;
  1080. if (ev.Y > maxCursorPositionableLine) {
  1081. currentRow = maxCursorPositionableLine;
  1082. } else {
  1083. currentRow = ev.Y + topRow;
  1084. }
  1085. var r = GetCurrentLine ();
  1086. if (ev.X - leftColumn >= r.Count)
  1087. currentColumn = r.Count - leftColumn;
  1088. else
  1089. currentColumn = ev.X - leftColumn;
  1090. }
  1091. PositionCursor ();
  1092. return true;
  1093. }
  1094. }
  1095. }