2010-05-19 Jb Evain <jbevain@novell.com>
[mcs.git] / tools / csharp / getline.cs
blob63ff480118bb61e82f6f46a87196ffe2d361da06
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 //
9 // Dual-licensed under the terms of the MIT X11 license or the
10 // Apache License 2.0
12 // USE -define:DEMO to build this as a standalone file and test it
14 // TODO:
15 // Enter an error (a = 1); Notice how the prompt is in the wrong line
16 // This is caused by Stderr not being tracked by System.Console.
17 // Completion support
18 // Why is Thread.Interrupt not working? Currently I resort to Abort which is too much.
20 // Limitations in System.Console:
21 // Console needs SIGWINCH support of some sort
22 // Console needs a way of updating its position after things have been written
23 // behind its back (P/Invoke puts for example).
24 // System.Console needs to get the DELETE character, and report accordingly.
26 #if NET_2_0 || NET_1_1
27 #define IN_MCS_BUILD
28 #endif
30 // Only compile this code in the 2.0 profile, but not in the Moonlight one
31 #if (IN_MCS_BUILD && NET_2_0 && !SMCS_SOURCE) || !IN_MCS_BUILD
32 using System;
33 using System.Text;
34 using System.IO;
35 using System.Threading;
36 using System.Reflection;
38 namespace Mono.Terminal {
40 public class LineEditor {
42 public class Completion {
43 public string [] Result;
44 public string Prefix;
46 public Completion (string prefix, string [] result)
48 Prefix = prefix;
49 Result = result;
53 public delegate Completion AutoCompleteHandler (string text, int pos);
55 //static StreamWriter log;
57 // The text being edited.
58 StringBuilder text;
60 // The text as it is rendered (replaces (char)1 with ^A on display for example).
61 StringBuilder rendered_text;
63 // The prompt specified, and the prompt shown to the user.
64 string prompt;
65 string shown_prompt;
67 // The current cursor position, indexes into "text", for an index
68 // into rendered_text, use TextToRenderPos
69 int cursor;
71 // The row where we started displaying data.
72 int home_row;
74 // The maximum length that has been displayed on the screen
75 int max_rendered;
77 // If we are done editing, this breaks the interactive loop
78 bool done = false;
80 // The thread where the Editing started taking place
81 Thread edit_thread;
83 // Our object that tracks history
84 History history;
86 // The contents of the kill buffer (cut/paste in Emacs parlance)
87 string kill_buffer = "";
89 // The string being searched for
90 string search;
91 string last_search;
93 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
94 int searching;
96 // The position where we found the match.
97 int match_at;
99 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
100 KeyHandler last_handler;
102 delegate void KeyHandler ();
104 struct Handler {
105 public ConsoleKeyInfo CKI;
106 public KeyHandler KeyHandler;
108 public Handler (ConsoleKey key, KeyHandler h)
110 CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
111 KeyHandler = h;
114 public Handler (char c, KeyHandler h)
116 KeyHandler = h;
117 // Use the "Zoom" as a flag that we only have a character.
118 CKI = new ConsoleKeyInfo (c, ConsoleKey.Zoom, false, false, false);
121 public Handler (ConsoleKeyInfo cki, KeyHandler h)
123 CKI = cki;
124 KeyHandler = h;
127 public static Handler Control (char c, KeyHandler h)
129 return new Handler ((char) (c - 'A' + 1), h);
132 public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
134 ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false);
135 return new Handler (cki, h);
139 /// <summary>
140 /// Invoked when the user requests auto-completion using the tab character
141 /// </summary>
142 /// <remarks>
143 /// The result is null for no values found, an array with a single
144 /// string, in that case the string should be the text to be inserted
145 /// for example if the word at pos is "T", the result for a completion
146 /// of "ToString" should be "oString", not "ToString".
148 /// When there are multiple results, the result should be the full
149 /// text
150 /// </remarks>
151 public AutoCompleteHandler AutoCompleteEvent;
153 static Handler [] handlers;
155 public LineEditor (string name) : this (name, 10) { }
157 public LineEditor (string name, int histsize)
159 handlers = new Handler [] {
160 new Handler (ConsoleKey.Home, CmdHome),
161 new Handler (ConsoleKey.End, CmdEnd),
162 new Handler (ConsoleKey.LeftArrow, CmdLeft),
163 new Handler (ConsoleKey.RightArrow, CmdRight),
164 new Handler (ConsoleKey.UpArrow, CmdHistoryPrev),
165 new Handler (ConsoleKey.DownArrow, CmdHistoryNext),
166 new Handler (ConsoleKey.Enter, CmdDone),
167 new Handler (ConsoleKey.Backspace, CmdBackspace),
168 new Handler (ConsoleKey.Delete, CmdDeleteChar),
169 new Handler (ConsoleKey.Tab, CmdTabOrComplete),
171 // Emacs keys
172 Handler.Control ('A', CmdHome),
173 Handler.Control ('E', CmdEnd),
174 Handler.Control ('B', CmdLeft),
175 Handler.Control ('F', CmdRight),
176 Handler.Control ('P', CmdHistoryPrev),
177 Handler.Control ('N', CmdHistoryNext),
178 Handler.Control ('K', CmdKillToEOF),
179 Handler.Control ('Y', CmdYank),
180 Handler.Control ('D', CmdDeleteChar),
181 Handler.Control ('L', CmdRefresh),
182 Handler.Control ('R', CmdReverseSearch),
183 Handler.Control ('G', delegate {} ),
184 Handler.Alt ('B', ConsoleKey.B, CmdBackwardWord),
185 Handler.Alt ('F', ConsoleKey.F, CmdForwardWord),
187 Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord),
188 Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword),
190 // DEBUG
191 //Handler.Control ('T', CmdDebug),
193 // quote
194 Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); })
197 rendered_text = new StringBuilder ();
198 text = new StringBuilder ();
200 history = new History (name, histsize);
202 //if (File.Exists ("log"))File.Delete ("log");
203 //log = File.CreateText ("log");
206 void CmdDebug ()
208 history.Dump ();
209 Console.WriteLine ();
210 Render ();
213 void Render ()
215 Console.Write (shown_prompt);
216 Console.Write (rendered_text);
218 int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered);
220 for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++)
221 Console.Write (' ');
222 max_rendered = shown_prompt.Length + rendered_text.Length;
224 // Write one more to ensure that we always wrap around properly if we are at the
225 // end of a line.
226 Console.Write (' ');
228 UpdateHomeRow (max);
231 void UpdateHomeRow (int screenpos)
233 int lines = 1 + (screenpos / Console.WindowWidth);
235 home_row = Console.CursorTop - (lines - 1);
236 if (home_row < 0)
237 home_row = 0;
241 void RenderFrom (int pos)
243 int rpos = TextToRenderPos (pos);
244 int i;
246 for (i = rpos; i < rendered_text.Length; i++)
247 Console.Write (rendered_text [i]);
249 if ((shown_prompt.Length + rendered_text.Length) > max_rendered)
250 max_rendered = shown_prompt.Length + rendered_text.Length;
251 else {
252 int max_extra = max_rendered - shown_prompt.Length;
253 for (; i < max_extra; i++)
254 Console.Write (' ');
258 void ComputeRendered ()
260 rendered_text.Length = 0;
262 for (int i = 0; i < text.Length; i++){
263 int c = (int) text [i];
264 if (c < 26){
265 if (c == '\t')
266 rendered_text.Append (" ");
267 else {
268 rendered_text.Append ('^');
269 rendered_text.Append ((char) (c + (int) 'A' - 1));
271 } else
272 rendered_text.Append ((char)c);
276 int TextToRenderPos (int pos)
278 int p = 0;
280 for (int i = 0; i < pos; i++){
281 int c;
283 c = (int) text [i];
285 if (c < 26){
286 if (c == 9)
287 p += 4;
288 else
289 p += 2;
290 } else
291 p++;
294 return p;
297 int TextToScreenPos (int pos)
299 return shown_prompt.Length + TextToRenderPos (pos);
302 string Prompt {
303 get { return prompt; }
304 set { prompt = value; }
307 int LineCount {
308 get {
309 return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth;
313 void ForceCursor (int newpos)
315 cursor = newpos;
317 int actual_pos = shown_prompt.Length + TextToRenderPos (cursor);
318 int row = home_row + (actual_pos/Console.WindowWidth);
319 int col = actual_pos % Console.WindowWidth;
321 if (row >= Console.BufferHeight)
322 row = Console.BufferHeight-1;
323 Console.SetCursorPosition (col, row);
325 //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);
326 //log.Flush ();
329 void UpdateCursor (int newpos)
331 if (cursor == newpos)
332 return;
334 ForceCursor (newpos);
337 void InsertChar (char c)
339 int prev_lines = LineCount;
340 text = text.Insert (cursor, c);
341 ComputeRendered ();
342 if (prev_lines != LineCount){
344 Console.SetCursorPosition (0, home_row);
345 Render ();
346 ForceCursor (++cursor);
347 } else {
348 RenderFrom (cursor);
349 ForceCursor (++cursor);
350 UpdateHomeRow (TextToScreenPos (cursor));
355 // Commands
357 void CmdDone ()
359 done = true;
362 void CmdTabOrComplete ()
364 bool complete = false;
366 if (AutoCompleteEvent != null){
367 if (TabAtStartCompletes)
368 complete = true;
369 else {
370 for (int i = 0; i < cursor; i++){
371 if (!Char.IsWhiteSpace (text [i])){
372 complete = true;
373 break;
378 if (complete){
379 Completion completion = AutoCompleteEvent (text.ToString (), cursor);
380 string [] completions = completion.Result;
381 if (completions == null)
382 return;
384 int ncompletions = completions.Length;
385 if (ncompletions == 0)
386 return;
388 if (completions.Length == 1){
389 InsertTextAtCursor (completions [0]);
390 } else {
391 int last = -1;
393 for (int p = 0; p < completions [0].Length; p++){
394 char c = completions [0][p];
397 for (int i = 1; i < ncompletions; i++){
398 if (completions [i].Length < p)
399 goto mismatch;
401 if (completions [i][p] != c){
402 goto mismatch;
405 last = p;
407 mismatch:
408 if (last != -1){
409 InsertTextAtCursor (completions [0].Substring (0, last+1));
411 Console.WriteLine ();
412 foreach (string s in completions){
413 Console.Write (completion.Prefix);
414 Console.Write (s);
415 Console.Write (' ');
417 Console.WriteLine ();
418 Render ();
419 ForceCursor (cursor);
421 } else
422 HandleChar ('\t');
423 } else
424 HandleChar ('t');
427 void CmdHome ()
429 UpdateCursor (0);
432 void CmdEnd ()
434 UpdateCursor (text.Length);
437 void CmdLeft ()
439 if (cursor == 0)
440 return;
442 UpdateCursor (cursor-1);
445 void CmdBackwardWord ()
447 int p = WordBackward (cursor);
448 if (p == -1)
449 return;
450 UpdateCursor (p);
453 void CmdForwardWord ()
455 int p = WordForward (cursor);
456 if (p == -1)
457 return;
458 UpdateCursor (p);
461 void CmdRight ()
463 if (cursor == text.Length)
464 return;
466 UpdateCursor (cursor+1);
469 void RenderAfter (int p)
471 ForceCursor (p);
472 RenderFrom (p);
473 ForceCursor (cursor);
476 void CmdBackspace ()
478 if (cursor == 0)
479 return;
481 text.Remove (--cursor, 1);
482 ComputeRendered ();
483 RenderAfter (cursor);
486 void CmdDeleteChar ()
488 // If there is no input, this behaves like EOF
489 if (text.Length == 0){
490 done = true;
491 text = null;
492 Console.WriteLine ();
493 return;
496 if (cursor == text.Length)
497 return;
498 text.Remove (cursor, 1);
499 ComputeRendered ();
500 RenderAfter (cursor);
503 int WordForward (int p)
505 if (p >= text.Length)
506 return -1;
508 int i = p;
509 if (Char.IsPunctuation (text [p]) || Char.IsWhiteSpace (text[p])){
510 for (; i < text.Length; i++){
511 if (Char.IsLetterOrDigit (text [i]))
512 break;
514 for (; i < text.Length; i++){
515 if (!Char.IsLetterOrDigit (text [i]))
516 break;
518 } else {
519 for (; i < text.Length; i++){
520 if (!Char.IsLetterOrDigit (text [i]))
521 break;
524 if (i != p)
525 return i;
526 return -1;
529 int WordBackward (int p)
531 if (p == 0)
532 return -1;
534 int i = p-1;
535 if (i == 0)
536 return 0;
538 if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){
539 for (; i >= 0; i--){
540 if (Char.IsLetterOrDigit (text [i]))
541 break;
543 for (; i >= 0; i--){
544 if (!Char.IsLetterOrDigit (text[i]))
545 break;
547 } else {
548 for (; i >= 0; i--){
549 if (!Char.IsLetterOrDigit (text [i]))
550 break;
553 i++;
555 if (i != p)
556 return i;
558 return -1;
561 void CmdDeleteWord ()
563 int pos = WordForward (cursor);
565 if (pos == -1)
566 return;
568 string k = text.ToString (cursor, pos-cursor);
570 if (last_handler == CmdDeleteWord)
571 kill_buffer = kill_buffer + k;
572 else
573 kill_buffer = k;
575 text.Remove (cursor, pos-cursor);
576 ComputeRendered ();
577 RenderAfter (cursor);
580 void CmdDeleteBackword ()
582 int pos = WordBackward (cursor);
583 if (pos == -1)
584 return;
586 string k = text.ToString (pos, cursor-pos);
588 if (last_handler == CmdDeleteBackword)
589 kill_buffer = k + kill_buffer;
590 else
591 kill_buffer = k;
593 text.Remove (pos, cursor-pos);
594 ComputeRendered ();
595 RenderAfter (pos);
599 // Adds the current line to the history if needed
601 void HistoryUpdateLine ()
603 history.Update (text.ToString ());
606 void CmdHistoryPrev ()
608 if (!history.PreviousAvailable ())
609 return;
611 HistoryUpdateLine ();
613 SetText (history.Previous ());
616 void CmdHistoryNext ()
618 if (!history.NextAvailable())
619 return;
621 history.Update (text.ToString ());
622 SetText (history.Next ());
626 void CmdKillToEOF ()
628 kill_buffer = text.ToString (cursor, text.Length-cursor);
629 text.Length = cursor;
630 ComputeRendered ();
631 RenderAfter (cursor);
634 void CmdYank ()
636 InsertTextAtCursor (kill_buffer);
639 void InsertTextAtCursor (string str)
641 int prev_lines = LineCount;
642 text.Insert (cursor, str);
643 ComputeRendered ();
644 if (prev_lines != LineCount){
645 Console.SetCursorPosition (0, home_row);
646 Render ();
647 cursor += str.Length;
648 ForceCursor (cursor);
649 } else {
650 RenderFrom (cursor);
651 cursor += str.Length;
652 ForceCursor (cursor);
653 UpdateHomeRow (TextToScreenPos (cursor));
657 void SetSearchPrompt (string s)
659 SetPrompt ("(reverse-i-search)`" + s + "': ");
662 void ReverseSearch ()
664 int p;
666 if (cursor == text.Length){
667 // The cursor is at the end of the string
669 p = text.ToString ().LastIndexOf (search);
670 if (p != -1){
671 match_at = p;
672 cursor = p;
673 ForceCursor (cursor);
674 return;
676 } else {
677 // The cursor is somewhere in the middle of the string
678 int start = (cursor == match_at) ? cursor - 1 : cursor;
679 if (start != -1){
680 p = text.ToString ().LastIndexOf (search, start);
681 if (p != -1){
682 match_at = p;
683 cursor = p;
684 ForceCursor (cursor);
685 return;
690 // Need to search backwards in history
691 HistoryUpdateLine ();
692 string s = history.SearchBackward (search);
693 if (s != null){
694 match_at = -1;
695 SetText (s);
696 ReverseSearch ();
700 void CmdReverseSearch ()
702 if (searching == 0){
703 match_at = -1;
704 last_search = search;
705 searching = -1;
706 search = "";
707 SetSearchPrompt ("");
708 } else {
709 if (search == ""){
710 if (last_search != "" && last_search != null){
711 search = last_search;
712 SetSearchPrompt (search);
714 ReverseSearch ();
716 return;
718 ReverseSearch ();
722 void SearchAppend (char c)
724 search = search + c;
725 SetSearchPrompt (search);
728 // If the new typed data still matches the current text, stay here
730 if (cursor < text.Length){
731 string r = text.ToString (cursor, text.Length - cursor);
732 if (r.StartsWith (search))
733 return;
736 ReverseSearch ();
739 void CmdRefresh ()
741 Console.Clear ();
742 max_rendered = 0;
743 Render ();
744 ForceCursor (cursor);
747 void InterruptEdit (object sender, ConsoleCancelEventArgs a)
749 // Do not abort our program:
750 a.Cancel = true;
752 // Interrupt the editor
753 edit_thread.Abort();
756 void HandleChar (char c)
758 if (searching != 0)
759 SearchAppend (c);
760 else
761 InsertChar (c);
764 void EditLoop ()
766 ConsoleKeyInfo cki;
768 while (!done){
769 ConsoleModifiers mod;
771 cki = Console.ReadKey (true);
772 if (cki.Key == ConsoleKey.Escape){
773 cki = Console.ReadKey (true);
775 mod = ConsoleModifiers.Alt;
776 } else
777 mod = cki.Modifiers;
779 bool handled = false;
781 foreach (Handler handler in handlers){
782 ConsoleKeyInfo t = handler.CKI;
784 if (t.Key == cki.Key && t.Modifiers == mod){
785 handled = true;
786 handler.KeyHandler ();
787 last_handler = handler.KeyHandler;
788 break;
789 } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
790 handled = true;
791 handler.KeyHandler ();
792 last_handler = handler.KeyHandler;
793 break;
796 if (handled){
797 if (searching != 0){
798 if (last_handler != CmdReverseSearch){
799 searching = 0;
800 SetPrompt (prompt);
803 continue;
806 if (cki.KeyChar != (char) 0)
807 HandleChar (cki.KeyChar);
811 void InitText (string initial)
813 text = new StringBuilder (initial);
814 ComputeRendered ();
815 cursor = text.Length;
816 Render ();
817 ForceCursor (cursor);
820 void SetText (string newtext)
822 Console.SetCursorPosition (0, home_row);
823 InitText (newtext);
826 void SetPrompt (string newprompt)
828 shown_prompt = newprompt;
829 Console.SetCursorPosition (0, home_row);
830 Render ();
831 ForceCursor (cursor);
834 public string Edit (string prompt, string initial)
836 edit_thread = Thread.CurrentThread;
837 searching = 0;
838 Console.CancelKeyPress += InterruptEdit;
840 done = false;
841 history.CursorToEnd ();
842 max_rendered = 0;
844 Prompt = prompt;
845 shown_prompt = prompt;
846 InitText (initial);
847 history.Append (initial);
849 do {
850 try {
851 EditLoop ();
852 } catch (ThreadAbortException){
853 searching = 0;
854 Thread.ResetAbort ();
855 Console.WriteLine ();
856 SetPrompt (prompt);
857 SetText ("");
859 } while (!done);
860 Console.WriteLine ();
862 Console.CancelKeyPress -= InterruptEdit;
864 if (text == null){
865 history.Close ();
866 return null;
869 string result = text.ToString ();
870 if (result != "")
871 history.Accept (result);
872 else
873 history.RemoveLast ();
875 return result;
878 public bool TabAtStartCompletes { get; set; }
881 // Emulates the bash-like behavior, where edits done to the
882 // history are recorded
884 class History {
885 string [] history;
886 int head, tail;
887 int cursor, count;
888 string histfile;
890 public History (string app, int size)
892 if (size < 1)
893 throw new ArgumentException ("size");
895 if (app != null){
896 string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
897 //Console.WriteLine (dir);
898 if (!Directory.Exists (dir)){
899 try {
900 Directory.CreateDirectory (dir);
901 } catch {
902 app = null;
905 if (app != null)
906 histfile = Path.Combine (dir, app) + ".history";
909 history = new string [size];
910 head = tail = cursor = 0;
912 if (File.Exists (histfile)){
913 using (StreamReader sr = File.OpenText (histfile)){
914 string line;
916 while ((line = sr.ReadLine ()) != null){
917 if (line != "")
918 Append (line);
924 public void Close ()
926 if (histfile == null)
927 return;
929 try {
930 using (StreamWriter sw = File.CreateText (histfile)){
931 int start = (count == history.Length) ? head : tail;
932 for (int i = start; i < start+count; i++){
933 int p = i % history.Length;
934 sw.WriteLine (history [p]);
937 } catch {
938 // ignore
943 // Appends a value to the history
945 public void Append (string s)
947 //Console.WriteLine ("APPENDING {0} {1}", s, Environment.StackTrace);
948 history [head] = s;
949 head = (head+1) % history.Length;
950 if (head == tail)
951 tail = (tail+1 % history.Length);
952 if (count != history.Length)
953 count++;
957 // Updates the current cursor location with the string,
958 // to support editing of history items. For the current
959 // line to participate, an Append must be done before.
961 public void Update (string s)
963 history [cursor] = s;
966 public void RemoveLast ()
968 head = head-1;
969 if (head < 0)
970 head = history.Length-1;
973 public void Accept (string s)
975 int t = head-1;
976 if (t < 0)
977 t = history.Length-1;
979 history [t] = s;
982 public bool PreviousAvailable ()
984 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
985 if (count == 0 || cursor == tail)
986 return false;
988 return true;
991 public bool NextAvailable ()
993 int next = (cursor + 1) % history.Length;
994 if (count == 0 || next >= head)
995 return false;
997 return true;
1002 // Returns: a string with the previous line contents, or
1003 // nul if there is no data in the history to move to.
1005 public string Previous ()
1007 if (!PreviousAvailable ())
1008 return null;
1010 cursor--;
1011 if (cursor < 0)
1012 cursor = history.Length - 1;
1014 return history [cursor];
1017 public string Next ()
1019 if (!NextAvailable ())
1020 return null;
1022 cursor = (cursor + 1) % history.Length;
1023 return history [cursor];
1026 public void CursorToEnd ()
1028 if (head == tail)
1029 return;
1031 cursor = head;
1034 public void Dump ()
1036 Console.WriteLine ("Head={0} Tail={1} Cursor={2}", head, tail, cursor);
1037 for (int i = 0; i < history.Length;i++){
1038 Console.WriteLine (" {0} {1}: {2}", i == cursor ? "==>" : " ", i, history[i]);
1040 //log.Flush ();
1043 public string SearchBackward (string term)
1045 for (int i = 1; i < count; i++){
1046 int slot = cursor-i;
1047 if (slot < 0)
1048 slot = history.Length-1;
1049 if (history [slot] != null && history [slot].IndexOf (term) != -1){
1050 cursor = slot;
1051 return history [slot];
1054 // Will the next hit tail?
1055 slot--;
1056 if (slot < 0)
1057 slot = history.Length-1;
1058 if (slot == tail)
1059 break;
1062 return null;
1068 #if DEMO
1069 class Demo {
1070 static void Main ()
1072 LineEditor le = new LineEditor (null);
1073 string s;
1075 while ((s = le.Edit ("shell> ", "")) != null){
1076 Console.WriteLine ("----> [{0}]", s);
1080 #endif
1082 #endif