Update dependencies from https://github.com/dotnet/arcade build 20200118.1 (#18514)
[mono-project.git] / mcs / tools / csharp / getline.cs
blobb60971e0df34eabe9d3954b6843f0f875063b41d
1 //
2 // getline.cs: A command line editor
3 //
4 // Authors:
5 // Miguel de Icaza (miguel@novell.com)
6 //
7 // Copyright 2008 Novell, Inc.
8 // Copyright 2016 Xamarin Inc
9 //
10 // Completion wanted:
12 // * Enable bash-like completion window the window as an option for non-GUI people?
14 // * Continue completing when Backspace is used?
16 // * Should we keep the auto-complete on "."?
18 // * Completion produces an error if the value is not resolvable, we should hide those errors
20 // Dual-licensed under the terms of the MIT X11 license or the
21 // Apache License 2.0
23 // USE -define:DEMO to build this as a standalone file and test it
25 // TODO:
26 // Enter an error (a = 1); Notice how the prompt is in the wrong line
27 // This is caused by Stderr not being tracked by System.Console.
28 // Completion support
29 // Why is Thread.Interrupt not working? Currently I resort to Abort which is too much.
31 // Limitations in System.Console:
32 // Console needs SIGWINCH support of some sort
33 // Console needs a way of updating its position after things have been written
34 // behind its back (P/Invoke puts for example).
35 // System.Console needs to get the DELETE character, and report accordingly.
37 // Bug:
38 // About 8 lines missing, type "Con<TAB>" and not enough lines are inserted at the bottom.
39 //
41 using System;
42 using System.Text;
43 using System.IO;
44 using System.Threading;
45 using System.Reflection;
47 namespace Mono.Terminal {
49 public class LineEditor {
51 public class Completion {
52 public string [] Result;
53 public string Prefix;
55 public Completion (string prefix, string [] result)
57 Prefix = prefix;
58 Result = result;
62 public delegate Completion AutoCompleteHandler (string text, int pos);
64 // null does nothing, "csharp" uses some heuristics that make sense for C#
65 public string HeuristicsMode;
67 //static StreamWriter log;
69 // The text being edited.
70 StringBuilder text;
72 // The text as it is rendered (replaces (char)1 with ^A on display for example).
73 StringBuilder rendered_text;
75 // The prompt specified, and the prompt shown to the user.
76 string prompt;
77 string shown_prompt;
79 // The current cursor position, indexes into "text", for an index
80 // into rendered_text, use TextToRenderPos
81 int cursor;
83 // The row where we started displaying data.
84 int home_row;
86 // The maximum length that has been displayed on the screen
87 int max_rendered;
89 // If we are done editing, this breaks the interactive loop
90 bool done = false;
92 // The thread where the Editing started taking place
93 Thread edit_thread;
95 // Our object that tracks history
96 History history;
98 // The contents of the kill buffer (cut/paste in Emacs parlance)
99 string kill_buffer = "";
101 // The string being searched for
102 string search;
103 string last_search;
105 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
106 int searching;
108 // The position where we found the match.
109 int match_at;
111 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
112 KeyHandler last_handler;
114 // If we have a popup completion, this is not null and holds the state.
115 CompletionState current_completion;
117 // If this is set, it contains an escape sequence to reset the Unix colors to the ones that were used on startup
118 static byte [] unix_reset_colors;
120 // This contains a raw stream pointing to stdout, used to bypass the TermInfoDriver
121 static Stream unix_raw_output;
123 delegate void KeyHandler ();
125 struct Handler {
126 public ConsoleKeyInfo CKI;
127 public KeyHandler KeyHandler;
128 public bool ResetCompletion;
130 public Handler (ConsoleKey key, KeyHandler h, bool resetCompletion = true)
132 CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
133 KeyHandler = h;
134 ResetCompletion = resetCompletion;
137 public Handler (char c, KeyHandler h, bool resetCompletion = true)
139 KeyHandler = h;
140 // Use the "Zoom" as a flag that we only have a character.
141 CKI = new ConsoleKeyInfo (c, ConsoleKey.Zoom, false, false, false);
142 ResetCompletion = resetCompletion;
145 public Handler (ConsoleKeyInfo cki, KeyHandler h, bool resetCompletion = true)
147 CKI = cki;
148 KeyHandler = h;
149 ResetCompletion = resetCompletion;
152 public static Handler Control (char c, KeyHandler h, bool resetCompletion = true)
154 return new Handler ((char) (c - 'A' + 1), h, resetCompletion);
157 public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
159 ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false);
160 return new Handler (cki, h);
164 /// <summary>
165 /// Invoked when the user requests auto-completion using the tab character
166 /// </summary>
167 /// <remarks>
168 /// The result is null for no values found, an array with a single
169 /// string, in that case the string should be the text to be inserted
170 /// for example if the word at pos is "T", the result for a completion
171 /// of "ToString" should be "oString", not "ToString".
173 /// When there are multiple results, the result should be the full
174 /// text
175 /// </remarks>
176 public AutoCompleteHandler AutoCompleteEvent;
178 static Handler [] handlers;
180 public LineEditor (string name) : this (name, 10) { }
182 public LineEditor (string name, int histsize)
184 handlers = new Handler [] {
185 new Handler (ConsoleKey.Home, CmdHome),
186 new Handler (ConsoleKey.End, CmdEnd),
187 new Handler (ConsoleKey.LeftArrow, CmdLeft),
188 new Handler (ConsoleKey.RightArrow, CmdRight),
189 new Handler (ConsoleKey.UpArrow, CmdUp, resetCompletion: false),
190 new Handler (ConsoleKey.DownArrow, CmdDown, resetCompletion: false),
191 new Handler (ConsoleKey.Enter, CmdDone, resetCompletion: false),
192 new Handler (ConsoleKey.Backspace, CmdBackspace, resetCompletion: false),
193 new Handler (ConsoleKey.Delete, CmdDeleteChar),
194 new Handler (ConsoleKey.Tab, CmdTabOrComplete, resetCompletion: false),
196 // Emacs keys
197 Handler.Control ('A', CmdHome),
198 Handler.Control ('E', CmdEnd),
199 Handler.Control ('B', CmdLeft),
200 Handler.Control ('F', CmdRight),
201 Handler.Control ('P', CmdUp, resetCompletion: false),
202 Handler.Control ('N', CmdDown, resetCompletion: false),
203 Handler.Control ('K', CmdKillToEOF),
204 Handler.Control ('Y', CmdYank),
205 Handler.Control ('D', CmdDeleteChar),
206 Handler.Control ('L', CmdRefresh),
207 Handler.Control ('R', CmdReverseSearch),
208 Handler.Control ('G', delegate {} ),
209 Handler.Alt ('B', ConsoleKey.B, CmdBackwardWord),
210 Handler.Alt ('F', ConsoleKey.F, CmdForwardWord),
212 Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord),
213 Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword),
215 // DEBUG
216 //Handler.Control ('T', CmdDebug),
218 // quote
219 Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); })
222 rendered_text = new StringBuilder ();
223 text = new StringBuilder ();
225 history = new History (name, histsize);
227 GetUnixConsoleReset ();
228 //if (File.Exists ("log"))File.Delete ("log");
229 //log = File.CreateText ("log");
232 // On Unix, there is a "default" color which is not represented by any colors in
233 // ConsoleColor and it is not possible to set is by setting the ForegroundColor or
234 // BackgroundColor properties, so we have to use the terminfo driver in Mono to
235 // fetch these values
237 void GetUnixConsoleReset ()
240 // On Unix, we want to be able to reset the color for the pop-up completion
242 int p = (int) Environment.OSVersion.Platform;
243 var is_unix = (p == 4) || (p == 128);
244 if (!is_unix)
245 return;
247 // Sole purpose of this call is to initialize the Terminfo driver
248 var x = Console.CursorLeft;
250 try {
251 var terminfo_driver = Type.GetType ("System.ConsoleDriver")?.GetField ("driver", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue (null);
252 if (terminfo_driver == null)
253 return;
255 var unix_reset_colors_str = (terminfo_driver?.GetType ()?.GetField ("origPair", BindingFlags.Instance | BindingFlags.NonPublic))?.GetValue (terminfo_driver) as string;
257 if (unix_reset_colors_str != null)
258 unix_reset_colors = Encoding.UTF8.GetBytes ((string)unix_reset_colors_str);
259 unix_raw_output = Console.OpenStandardOutput ();
260 } catch (Exception e){
261 Console.WriteLine ("Error: " + e);
266 void CmdDebug ()
268 history.Dump ();
269 Console.WriteLine ();
270 Render ();
273 void Render ()
275 Console.Write (shown_prompt);
276 Console.Write (rendered_text);
278 int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered);
280 for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++)
281 Console.Write (' ');
282 max_rendered = shown_prompt.Length + rendered_text.Length;
284 // Write one more to ensure that we always wrap around properly if we are at the
285 // end of a line.
286 Console.Write (' ');
288 UpdateHomeRow (max);
291 void UpdateHomeRow (int screenpos)
293 int lines = 1 + (screenpos / Console.WindowWidth);
295 home_row = Console.CursorTop - (lines - 1);
296 if (home_row < 0)
297 home_row = 0;
301 void RenderFrom (int pos)
303 int rpos = TextToRenderPos (pos);
304 int i;
306 for (i = rpos; i < rendered_text.Length; i++)
307 Console.Write (rendered_text [i]);
309 if ((shown_prompt.Length + rendered_text.Length) > max_rendered)
310 max_rendered = shown_prompt.Length + rendered_text.Length;
311 else {
312 int max_extra = max_rendered - shown_prompt.Length;
313 for (; i < max_extra; i++)
314 Console.Write (' ');
318 void ComputeRendered ()
320 rendered_text.Length = 0;
322 for (int i = 0; i < text.Length; i++){
323 int c = (int) text [i];
324 if (c < 26){
325 if (c == '\t')
326 rendered_text.Append (" ");
327 else {
328 rendered_text.Append ('^');
329 rendered_text.Append ((char) (c + (int) 'A' - 1));
331 } else
332 rendered_text.Append ((char)c);
336 int TextToRenderPos (int pos)
338 int p = 0;
340 for (int i = 0; i < pos; i++){
341 int c;
343 c = (int) text [i];
345 if (c < 26){
346 if (c == 9)
347 p += 4;
348 else
349 p += 2;
350 } else
351 p++;
354 return p;
357 int TextToScreenPos (int pos)
359 return shown_prompt.Length + TextToRenderPos (pos);
362 string Prompt {
363 get { return prompt; }
364 set { prompt = value; }
367 int LineCount {
368 get {
369 return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth;
373 void ForceCursor (int newpos)
375 cursor = newpos;
377 int actual_pos = shown_prompt.Length + TextToRenderPos (cursor);
378 int row = home_row + (actual_pos/Console.WindowWidth);
379 int col = actual_pos % Console.WindowWidth;
381 if (row >= Console.BufferHeight)
382 row = Console.BufferHeight-1;
383 Console.SetCursorPosition (col, row);
385 //log.WriteLine ("Going to cursor={0} row={1} col={2} actual={3} prompt={4} ttr={5} old={6}", newpos, row, col, actual_pos, prompt.Length, TextToRenderPos (cursor), cursor);
386 //log.Flush ();
389 void UpdateCursor (int newpos)
391 if (cursor == newpos)
392 return;
394 ForceCursor (newpos);
397 void InsertChar (char c)
399 int prev_lines = LineCount;
400 text = text.Insert (cursor, c);
401 ComputeRendered ();
402 if (prev_lines != LineCount){
404 Console.SetCursorPosition (0, home_row);
405 Render ();
406 ForceCursor (++cursor);
407 } else {
408 RenderFrom (cursor);
409 ForceCursor (++cursor);
410 UpdateHomeRow (TextToScreenPos (cursor));
414 static void SaveExcursion (Action code)
416 var saved_col = Console.CursorLeft;
417 var saved_row = Console.CursorTop;
418 var saved_fore = Console.ForegroundColor;
419 var saved_back = Console.BackgroundColor;
421 code ();
423 Console.CursorLeft = saved_col;
424 Console.CursorTop = saved_row;
425 if (unix_reset_colors != null){
426 unix_raw_output.Write (unix_reset_colors, 0, unix_reset_colors.Length);
427 } else {
428 Console.ForegroundColor = saved_fore;
429 Console.BackgroundColor = saved_back;
433 class CompletionState {
434 public string Prefix;
435 public string [] Completions;
436 public int Col, Row, Width, Height;
437 int selected_item, top_item;
439 public CompletionState (int col, int row, int width, int height)
441 Col = col;
442 Row = row;
443 Width = width;
444 Height = height;
446 if (Col < 0)
447 throw new ArgumentException ("Cannot be less than zero" + Col, "Col");
448 if (Row < 0)
449 throw new ArgumentException ("Cannot be less than zero", "Row");
450 if (Width < 1)
451 throw new ArgumentException ("Cannot be less than one", "Width");
452 if (Height < 1)
453 throw new ArgumentException ("Cannot be less than one", "Height");
457 void DrawSelection ()
459 for (int r = 0; r < Height; r++){
460 int item_idx = top_item + r;
461 bool selected = (item_idx == selected_item);
463 Console.ForegroundColor = selected ? ConsoleColor.Black : ConsoleColor.Gray;
464 Console.BackgroundColor = selected ? ConsoleColor.Cyan : ConsoleColor.Blue;
466 var item = Prefix + Completions [item_idx];
467 if (item.Length > Width)
468 item = item.Substring (0, Width);
470 Console.CursorLeft = Col;
471 Console.CursorTop = Row + r;
472 Console.Write (item);
473 for (int space = item.Length; space <= Width; space++)
474 Console.Write (" ");
478 public string Current {
479 get {
480 return Completions [selected_item];
484 public void Show ()
486 SaveExcursion (DrawSelection);
489 public void SelectNext ()
491 if (selected_item+1 < Completions.Length){
492 selected_item++;
493 if (selected_item - top_item >= Height)
494 top_item++;
495 SaveExcursion (DrawSelection);
499 public void SelectPrevious ()
501 if (selected_item > 0){
502 selected_item--;
503 if (selected_item < top_item)
504 top_item = selected_item;
505 SaveExcursion (DrawSelection);
509 void Clear ()
511 for (int r = 0; r < Height; r++){
512 Console.CursorLeft = Col;
513 Console.CursorTop = Row + r;
514 for (int space = 0; space <= Width; space++)
515 Console.Write (" ");
519 public void Remove ()
521 SaveExcursion (Clear);
525 void ShowCompletions (string prefix, string [] completions)
527 // Ensure we have space, determine window size
528 int window_height = System.Math.Min (completions.Length, Console.WindowHeight/5);
529 int target_line = Console.WindowHeight-window_height-1;
530 if (Console.CursorTop > target_line){
531 var saved_left = Console.CursorLeft;
532 var delta = Console.CursorTop-target_line;
533 Console.CursorLeft = 0;
534 Console.CursorTop = Console.WindowHeight-1;
535 for (int i = 0; i < delta+1; i++){
536 for (int c = Console.WindowWidth; c > 0; c--)
537 Console.Write (" "); // To debug use ("{0}", i%10);
539 Console.CursorTop = target_line;
540 Console.CursorLeft = 0;
541 Render ();
544 const int MaxWidth = 50;
545 int window_width = 12;
546 int plen = prefix.Length;
547 foreach (var s in completions)
548 window_width = System.Math.Max (plen + s.Length, window_width);
549 window_width = System.Math.Min (window_width, MaxWidth);
551 if (current_completion == null){
552 int left = Console.CursorLeft-prefix.Length;
554 if (left + window_width + 1 >= Console.WindowWidth)
555 left = Console.WindowWidth-window_width-1;
557 current_completion = new CompletionState (left, Console.CursorTop+1, window_width, window_height) {
558 Prefix = prefix,
559 Completions = completions,
561 } else {
562 current_completion.Prefix = prefix;
563 current_completion.Completions = completions;
565 current_completion.Show ();
566 Console.CursorLeft = 0;
569 void HideCompletions ()
571 if (current_completion == null)
572 return;
573 current_completion.Remove ();
574 current_completion = null;
578 // Triggers the completion engine, if insertBestMatch is true, then this will
579 // insert the best match found, this behaves like the shell "tab" which will
580 // complete as much as possible given the options.
582 void Complete ()
584 Completion completion = AutoCompleteEvent (text.ToString (), cursor);
585 string [] completions = completion.Result;
586 if (completions == null){
587 HideCompletions ();
588 return;
591 int ncompletions = completions.Length;
592 if (ncompletions == 0){
593 HideCompletions ();
594 return;
597 if (completions.Length == 1){
598 InsertTextAtCursor (completions [0]);
599 HideCompletions ();
600 } else {
601 int last = -1;
603 for (int p = 0; p < completions [0].Length; p++){
604 char c = completions [0][p];
607 for (int i = 1; i < ncompletions; i++){
608 if (completions [i].Length < p)
609 goto mismatch;
611 if (completions [i][p] != c){
612 goto mismatch;
615 last = p;
617 mismatch:
618 var prefix = completion.Prefix;
619 if (last != -1){
620 InsertTextAtCursor (completions [0].Substring (0, last+1));
622 // Adjust the completions to skip the common prefix
623 prefix += completions [0].Substring (0, last+1);
624 for (int i = 0; i < completions.Length; i++)
625 completions [i] = completions [i].Substring (last+1);
627 ShowCompletions (prefix, completions);
628 Render ();
629 ForceCursor (cursor);
634 // When the user has triggered a completion window, this will try to update
635 // the contents of it. The completion window is assumed to be hidden at this
636 // point
638 void UpdateCompletionWindow ()
640 if (current_completion != null)
641 throw new Exception ("This method should only be called if the window has been hidden");
643 Completion completion = AutoCompleteEvent (text.ToString (), cursor);
644 string [] completions = completion.Result;
645 if (completions == null)
646 return;
648 int ncompletions = completions.Length;
649 if (ncompletions == 0)
650 return;
652 ShowCompletions (completion.Prefix, completion.Result);
653 Render ();
654 ForceCursor (cursor);
659 // Commands
661 void CmdDone ()
663 if (current_completion != null){
664 InsertTextAtCursor (current_completion.Current);
665 HideCompletions ();
666 return;
668 done = true;
671 void CmdTabOrComplete ()
673 bool complete = false;
675 if (AutoCompleteEvent != null){
676 if (TabAtStartCompletes)
677 complete = true;
678 else {
679 for (int i = 0; i < cursor; i++){
680 if (!Char.IsWhiteSpace (text [i])){
681 complete = true;
682 break;
687 if (complete)
688 Complete ();
689 else
690 HandleChar ('\t');
691 } else
692 HandleChar ('t');
695 void CmdHome ()
697 UpdateCursor (0);
700 void CmdEnd ()
702 UpdateCursor (text.Length);
705 void CmdLeft ()
707 if (cursor == 0)
708 return;
710 UpdateCursor (cursor-1);
713 void CmdBackwardWord ()
715 int p = WordBackward (cursor);
716 if (p == -1)
717 return;
718 UpdateCursor (p);
721 void CmdForwardWord ()
723 int p = WordForward (cursor);
724 if (p == -1)
725 return;
726 UpdateCursor (p);
729 void CmdRight ()
731 if (cursor == text.Length)
732 return;
734 UpdateCursor (cursor+1);
737 void RenderAfter (int p)
739 ForceCursor (p);
740 RenderFrom (p);
741 ForceCursor (cursor);
744 void CmdBackspace ()
746 if (cursor == 0)
747 return;
749 bool completing = current_completion != null;
750 HideCompletions ();
752 text.Remove (--cursor, 1);
753 ComputeRendered ();
754 RenderAfter (cursor);
755 if (completing)
756 UpdateCompletionWindow ();
759 void CmdDeleteChar ()
761 // If there is no input, this behaves like EOF
762 if (text.Length == 0){
763 done = true;
764 text = null;
765 Console.WriteLine ();
766 return;
769 if (cursor == text.Length)
770 return;
771 text.Remove (cursor, 1);
772 ComputeRendered ();
773 RenderAfter (cursor);
776 int WordForward (int p)
778 if (p >= text.Length)
779 return -1;
781 int i = p;
782 if (Char.IsPunctuation (text [p]) || Char.IsSymbol (text [p]) || Char.IsWhiteSpace (text[p])){
783 for (; i < text.Length; i++){
784 if (Char.IsLetterOrDigit (text [i]))
785 break;
787 for (; i < text.Length; i++){
788 if (!Char.IsLetterOrDigit (text [i]))
789 break;
791 } else {
792 for (; i < text.Length; i++){
793 if (!Char.IsLetterOrDigit (text [i]))
794 break;
797 if (i != p)
798 return i;
799 return -1;
802 int WordBackward (int p)
804 if (p == 0)
805 return -1;
807 int i = p-1;
808 if (i == 0)
809 return 0;
811 if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){
812 for (; i >= 0; i--){
813 if (Char.IsLetterOrDigit (text [i]))
814 break;
816 for (; i >= 0; i--){
817 if (!Char.IsLetterOrDigit (text[i]))
818 break;
820 } else {
821 for (; i >= 0; i--){
822 if (!Char.IsLetterOrDigit (text [i]))
823 break;
826 i++;
828 if (i != p)
829 return i;
831 return -1;
834 void CmdDeleteWord ()
836 int pos = WordForward (cursor);
838 if (pos == -1)
839 return;
841 string k = text.ToString (cursor, pos-cursor);
843 if (last_handler == CmdDeleteWord)
844 kill_buffer = kill_buffer + k;
845 else
846 kill_buffer = k;
848 text.Remove (cursor, pos-cursor);
849 ComputeRendered ();
850 RenderAfter (cursor);
853 void CmdDeleteBackword ()
855 int pos = WordBackward (cursor);
856 if (pos == -1)
857 return;
859 string k = text.ToString (pos, cursor-pos);
861 if (last_handler == CmdDeleteBackword)
862 kill_buffer = k + kill_buffer;
863 else
864 kill_buffer = k;
866 text.Remove (pos, cursor-pos);
867 ComputeRendered ();
868 RenderAfter (pos);
872 // Adds the current line to the history if needed
874 void HistoryUpdateLine ()
876 history.Update (text.ToString ());
879 void CmdHistoryPrev ()
881 if (!history.PreviousAvailable ())
882 return;
884 HistoryUpdateLine ();
886 SetText (history.Previous ());
889 void CmdHistoryNext ()
891 if (!history.NextAvailable())
892 return;
894 history.Update (text.ToString ());
895 SetText (history.Next ());
899 void CmdUp ()
901 if (current_completion == null)
902 CmdHistoryPrev ();
903 else
904 current_completion.SelectPrevious ();
907 void CmdDown ()
909 if (current_completion == null)
910 CmdHistoryNext ();
911 else
912 current_completion.SelectNext ();
915 void CmdKillToEOF ()
917 kill_buffer = text.ToString (cursor, text.Length-cursor);
918 text.Length = cursor;
919 ComputeRendered ();
920 RenderAfter (cursor);
923 void CmdYank ()
925 InsertTextAtCursor (kill_buffer);
928 void InsertTextAtCursor (string str)
930 int prev_lines = LineCount;
931 text.Insert (cursor, str);
932 ComputeRendered ();
933 if (prev_lines != LineCount){
934 Console.SetCursorPosition (0, home_row);
935 Render ();
936 cursor += str.Length;
937 ForceCursor (cursor);
938 } else {
939 RenderFrom (cursor);
940 cursor += str.Length;
941 ForceCursor (cursor);
942 UpdateHomeRow (TextToScreenPos (cursor));
946 void SetSearchPrompt (string s)
948 SetPrompt ("(reverse-i-search)`" + s + "': ");
951 void ReverseSearch ()
953 int p;
955 if (cursor == text.Length){
956 // The cursor is at the end of the string
958 p = text.ToString ().LastIndexOf (search);
959 if (p != -1){
960 match_at = p;
961 cursor = p;
962 ForceCursor (cursor);
963 return;
965 } else {
966 // The cursor is somewhere in the middle of the string
967 int start = (cursor == match_at) ? cursor - 1 : cursor;
968 if (start != -1){
969 p = text.ToString ().LastIndexOf (search, start);
970 if (p != -1){
971 match_at = p;
972 cursor = p;
973 ForceCursor (cursor);
974 return;
979 // Need to search backwards in history
980 HistoryUpdateLine ();
981 string s = history.SearchBackward (search);
982 if (s != null){
983 match_at = -1;
984 SetText (s);
985 ReverseSearch ();
989 void CmdReverseSearch ()
991 if (searching == 0){
992 match_at = -1;
993 last_search = search;
994 searching = -1;
995 search = "";
996 SetSearchPrompt ("");
997 } else {
998 if (search == ""){
999 if (last_search != "" && last_search != null){
1000 search = last_search;
1001 SetSearchPrompt (search);
1003 ReverseSearch ();
1005 return;
1007 ReverseSearch ();
1011 void SearchAppend (char c)
1013 search = search + c;
1014 SetSearchPrompt (search);
1017 // If the new typed data still matches the current text, stay here
1019 if (cursor < text.Length){
1020 string r = text.ToString (cursor, text.Length - cursor);
1021 if (r.StartsWith (search))
1022 return;
1025 ReverseSearch ();
1028 void CmdRefresh ()
1030 Console.Clear ();
1031 max_rendered = 0;
1032 Render ();
1033 ForceCursor (cursor);
1036 void InterruptEdit (object sender, ConsoleCancelEventArgs a)
1038 // Do not abort our program:
1039 a.Cancel = true;
1041 // Interrupt the editor
1042 edit_thread.Abort();
1046 // Implements heuristics to show the completion window based on the mode
1048 bool HeuristicAutoComplete (bool wasCompleting, char insertedChar)
1050 if (HeuristicsMode == "csharp"){
1051 // csharp heuristics
1052 if (wasCompleting){
1053 if (insertedChar == ' '){
1054 return false;
1056 return true;
1058 // If we were not completing, determine if we want to now
1059 if (insertedChar == '.'){
1060 // Avoid completing for numbers "1.2" for example
1061 if (cursor > 1 && Char.IsDigit (text[cursor-2])){
1062 for (int p = cursor-3; p >= 0; p--){
1063 char c = text[p];
1064 if (Char.IsDigit (c))
1065 continue;
1066 if (c == '_')
1067 return true;
1068 if (Char.IsLetter (c) || Char.IsPunctuation (c) || Char.IsSymbol (c) || Char.IsControl (c))
1069 return true;
1071 return false;
1073 return true;
1076 return false;
1079 void HandleChar (char c)
1081 if (searching != 0)
1082 SearchAppend (c);
1083 else {
1084 bool completing = current_completion != null;
1085 HideCompletions ();
1087 InsertChar (c);
1088 if (HeuristicAutoComplete (completing, c))
1089 UpdateCompletionWindow ();
1093 void EditLoop ()
1095 ConsoleKeyInfo cki;
1097 while (!done){
1098 ConsoleModifiers mod;
1100 cki = Console.ReadKey (true);
1101 if (cki.Key == ConsoleKey.Escape){
1102 if (current_completion != null){
1103 HideCompletions ();
1104 continue;
1105 } else {
1106 cki = Console.ReadKey (true);
1108 mod = ConsoleModifiers.Alt;
1110 } else
1111 mod = cki.Modifiers;
1113 bool handled = false;
1115 foreach (Handler handler in handlers){
1116 ConsoleKeyInfo t = handler.CKI;
1118 if (t.Key == cki.Key && t.Modifiers == mod){
1119 handled = true;
1120 if (handler.ResetCompletion)
1121 HideCompletions ();
1122 handler.KeyHandler ();
1123 last_handler = handler.KeyHandler;
1124 break;
1125 } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
1126 handled = true;
1127 if (handler.ResetCompletion)
1128 HideCompletions ();
1130 handler.KeyHandler ();
1131 last_handler = handler.KeyHandler;
1132 break;
1135 if (handled){
1136 if (searching != 0){
1137 if (last_handler != CmdReverseSearch){
1138 searching = 0;
1139 SetPrompt (prompt);
1142 continue;
1145 if (cki.KeyChar != (char) 0){
1146 HandleChar (cki.KeyChar);
1151 void InitText (string initial)
1153 text = new StringBuilder (initial);
1154 ComputeRendered ();
1155 cursor = text.Length;
1156 Render ();
1157 ForceCursor (cursor);
1160 void SetText (string newtext)
1162 Console.SetCursorPosition (0, home_row);
1163 InitText (newtext);
1166 void SetPrompt (string newprompt)
1168 shown_prompt = newprompt;
1169 Console.SetCursorPosition (0, home_row);
1170 Render ();
1171 ForceCursor (cursor);
1174 public string Edit (string prompt, string initial)
1176 edit_thread = Thread.CurrentThread;
1177 searching = 0;
1178 Console.CancelKeyPress += InterruptEdit;
1180 done = false;
1181 history.CursorToEnd ();
1182 max_rendered = 0;
1184 Prompt = prompt;
1185 shown_prompt = prompt;
1186 InitText (initial);
1187 history.Append (initial);
1189 do {
1190 try {
1191 EditLoop ();
1192 } catch (ThreadAbortException){
1193 searching = 0;
1194 Thread.ResetAbort ();
1195 Console.WriteLine ();
1196 SetPrompt (prompt);
1197 SetText ("");
1199 } while (!done);
1200 Console.WriteLine ();
1202 Console.CancelKeyPress -= InterruptEdit;
1204 if (text == null){
1205 history.Close ();
1206 return null;
1209 string result = text.ToString ();
1210 if (result != "")
1211 history.Accept (result);
1212 else
1213 history.RemoveLast ();
1215 return result;
1218 public void SaveHistory ()
1220 if (history != null) {
1221 history.Close ();
1225 public bool TabAtStartCompletes { get; set; }
1228 // Emulates the bash-like behavior, where edits done to the
1229 // history are recorded
1231 class History {
1232 string [] history;
1233 int head, tail;
1234 int cursor, count;
1235 string histfile;
1237 public History (string app, int size)
1239 if (size < 1)
1240 throw new ArgumentException ("size");
1242 if (app != null){
1243 string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
1244 //Console.WriteLine (dir);
1245 if (!Directory.Exists (dir)){
1246 try {
1247 Directory.CreateDirectory (dir);
1248 } catch {
1249 app = null;
1252 if (app != null)
1253 histfile = Path.Combine (dir, app) + ".history";
1256 history = new string [size];
1257 head = tail = cursor = 0;
1259 if (File.Exists (histfile)){
1260 using (StreamReader sr = File.OpenText (histfile)){
1261 string line;
1263 while ((line = sr.ReadLine ()) != null){
1264 if (line != "")
1265 Append (line);
1271 public void Close ()
1273 if (histfile == null)
1274 return;
1276 try {
1277 using (StreamWriter sw = File.CreateText (histfile)){
1278 int start = (count == history.Length) ? head : tail;
1279 for (int i = start; i < start+count; i++){
1280 int p = i % history.Length;
1281 sw.WriteLine (history [p]);
1284 } catch {
1285 // ignore
1290 // Appends a value to the history
1292 public void Append (string s)
1294 //Console.WriteLine ("APPENDING {0} head={1} tail={2}", s, head, tail);
1295 history [head] = s;
1296 head = (head+1) % history.Length;
1297 if (head == tail)
1298 tail = (tail+1 % history.Length);
1299 if (count != history.Length)
1300 count++;
1301 //Console.WriteLine ("DONE: head={1} tail={2}", s, head, tail);
1305 // Updates the current cursor location with the string,
1306 // to support editing of history items. For the current
1307 // line to participate, an Append must be done before.
1309 public void Update (string s)
1311 history [cursor] = s;
1314 public void RemoveLast ()
1316 head = head-1;
1317 if (head < 0)
1318 head = history.Length-1;
1321 public void Accept (string s)
1323 int t = head-1;
1324 if (t < 0)
1325 t = history.Length-1;
1327 history [t] = s;
1330 public bool PreviousAvailable ()
1332 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
1333 if (count == 0)
1334 return false;
1335 int next = cursor-1;
1336 if (next < 0)
1337 next = count-1;
1339 if (next == head)
1340 return false;
1342 return true;
1345 public bool NextAvailable ()
1347 if (count == 0)
1348 return false;
1349 int next = (cursor + 1) % history.Length;
1350 if (next == head)
1351 return false;
1352 return true;
1357 // Returns: a string with the previous line contents, or
1358 // nul if there is no data in the history to move to.
1360 public string Previous ()
1362 if (!PreviousAvailable ())
1363 return null;
1365 cursor--;
1366 if (cursor < 0)
1367 cursor = history.Length - 1;
1369 return history [cursor];
1372 public string Next ()
1374 if (!NextAvailable ())
1375 return null;
1377 cursor = (cursor + 1) % history.Length;
1378 return history [cursor];
1381 public void CursorToEnd ()
1383 if (head == tail)
1384 return;
1386 cursor = head;
1389 public void Dump ()
1391 Console.WriteLine ("Head={0} Tail={1} Cursor={2} count={3}", head, tail, cursor, count);
1392 for (int i = 0; i < history.Length;i++){
1393 Console.WriteLine (" {0} {1}: {2}", i == cursor ? "==>" : " ", i, history[i]);
1395 //log.Flush ();
1398 public string SearchBackward (string term)
1400 for (int i = 0; i < count; i++){
1401 int slot = cursor-i-1;
1402 if (slot < 0)
1403 slot = history.Length+slot;
1404 if (slot >= history.Length)
1405 slot = 0;
1406 if (history [slot] != null && history [slot].IndexOf (term) != -1){
1407 cursor = slot;
1408 return history [slot];
1412 return null;
1418 #if DEMO
1419 class Demo {
1420 static void Main ()
1422 LineEditor le = new LineEditor ("foo") {
1423 HeuristicsMode = "csharp"
1425 le.AutoCompleteEvent += delegate (string a, int pos){
1426 string prefix = "";
1427 var completions = new string [] { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" };
1428 return new Mono.Terminal.LineEditor.Completion (prefix, completions);
1431 string s;
1433 while ((s = le.Edit ("shell> ", "")) != null){
1434 Console.WriteLine ("----> [{0}]", s);
1438 #endif