2 // getline.cs: A command line editor
5 // Miguel de Icaza (miguel@novell.com)
7 // Copyright 2008 Novell, Inc.
9 // Dual-licensed under the terms of the MIT X11 license or the
12 // USE -define:DEMO to build this as a standalone file and test it
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.
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.
28 // Only compile this code in the 2.0 profile, but not in the Moonlight one
29 #if (IN_MCS_BUILD && NET_2_0 && !SMCS_SOURCE) || !IN_MCS_BUILD
33 using System.Threading;
34 using System.Reflection;
36 namespace Mono.Terminal {
38 public class LineEditor {
40 public class Completion {
41 public string [] Result;
44 public Completion (string prefix, string [] result)
51 public delegate Completion AutoCompleteHandler (string text, int pos);
53 //static StreamWriter log;
55 // The text being edited.
58 // The text as it is rendered (replaces (char)1 with ^A on display for example).
59 StringBuilder rendered_text;
61 // The prompt specified, and the prompt shown to the user.
65 // The current cursor position, indexes into "text", for an index
66 // into rendered_text, use TextToRenderPos
69 // The row where we started displaying data.
72 // The maximum length that has been displayed on the screen
75 // If we are done editing, this breaks the interactive loop
78 // The thread where the Editing started taking place
81 // Our object that tracks history
84 // The contents of the kill buffer (cut/paste in Emacs parlance)
85 string kill_buffer = "";
87 // The string being searched for
91 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
94 // The position where we found the match.
97 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
98 KeyHandler last_handler;
100 delegate void KeyHandler ();
103 public ConsoleKeyInfo CKI;
104 public KeyHandler KeyHandler;
106 public Handler (ConsoleKey key, KeyHandler h)
108 CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
112 public Handler (char c, KeyHandler h)
115 // Use the "Zoom" as a flag that we only have a character.
116 CKI = new ConsoleKeyInfo (c, ConsoleKey.Zoom, false, false, false);
119 public Handler (ConsoleKeyInfo cki, KeyHandler h)
125 public static Handler Control (char c, KeyHandler h)
127 return new Handler ((char) (c - 'A' + 1), h);
130 public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
132 ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false);
133 return new Handler (cki, h);
138 /// Invoked when the user requests auto-completion using the tab character
141 /// The result is null for no values found, an array with a single
142 /// string, in that case the string should be the text to be inserted
143 /// for example if the word at pos is "T", the result for a completion
144 /// of "ToString" should be "oString", not "ToString".
146 /// When there are multiple results, the result should be the full
149 public AutoCompleteHandler AutoCompleteEvent;
151 static Handler [] handlers;
153 public LineEditor (string name) : this (name, 10) { }
155 public LineEditor (string name, int histsize)
157 handlers = new Handler [] {
158 new Handler (ConsoleKey.Home, CmdHome),
159 new Handler (ConsoleKey.End, CmdEnd),
160 new Handler (ConsoleKey.LeftArrow, CmdLeft),
161 new Handler (ConsoleKey.RightArrow, CmdRight),
162 new Handler (ConsoleKey.UpArrow, CmdHistoryPrev),
163 new Handler (ConsoleKey.DownArrow, CmdHistoryNext),
164 new Handler (ConsoleKey.Enter, CmdDone),
165 new Handler (ConsoleKey.Backspace, CmdBackspace),
166 new Handler (ConsoleKey.Delete, CmdDeleteChar),
167 new Handler (ConsoleKey.Tab, CmdTabOrComplete),
170 Handler.Control ('A', CmdHome),
171 Handler.Control ('E', CmdEnd),
172 Handler.Control ('B', CmdLeft),
173 Handler.Control ('F', CmdRight),
174 Handler.Control ('P', CmdHistoryPrev),
175 Handler.Control ('N', CmdHistoryNext),
176 Handler.Control ('K', CmdKillToEOF),
177 Handler.Control ('Y', CmdYank),
178 Handler.Control ('D', CmdDeleteChar),
179 Handler.Control ('L', CmdRefresh),
180 Handler.Control ('R', CmdReverseSearch),
181 Handler.Control ('G', delegate {} ),
182 Handler.Alt ('B', ConsoleKey.B, CmdBackwardWord),
183 Handler.Alt ('F', ConsoleKey.F, CmdForwardWord),
185 Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord),
186 Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword),
189 //Handler.Control ('T', CmdDebug),
192 Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); })
195 rendered_text = new StringBuilder ();
196 text = new StringBuilder ();
198 history = new History (name, histsize);
200 //if (File.Exists ("log"))File.Delete ("log");
201 //log = File.CreateText ("log");
207 Console.WriteLine ();
213 Console.Write (shown_prompt);
214 Console.Write (rendered_text);
216 int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered);
218 for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++)
220 max_rendered = shown_prompt.Length + rendered_text.Length;
222 // Write one more to ensure that we always wrap around properly if we are at the
229 void UpdateHomeRow (int screenpos)
231 int lines = 1 + (screenpos / Console.WindowWidth);
233 home_row = Console.CursorTop - (lines - 1);
239 void RenderFrom (int pos)
241 int rpos = TextToRenderPos (pos);
244 for (i = rpos; i < rendered_text.Length; i++)
245 Console.Write (rendered_text [i]);
247 if ((shown_prompt.Length + rendered_text.Length) > max_rendered)
248 max_rendered = shown_prompt.Length + rendered_text.Length;
250 int max_extra = max_rendered - shown_prompt.Length;
251 for (; i < max_extra; i++)
256 void ComputeRendered ()
258 rendered_text.Length = 0;
260 for (int i = 0; i < text.Length; i++){
261 int c = (int) text [i];
264 rendered_text.Append (" ");
266 rendered_text.Append ('^');
267 rendered_text.Append ((char) (c + (int) 'A' - 1));
270 rendered_text.Append ((char)c);
274 int TextToRenderPos (int pos)
278 for (int i = 0; i < pos; i++){
295 int TextToScreenPos (int pos)
297 return shown_prompt.Length + TextToRenderPos (pos);
301 get { return prompt; }
302 set { prompt = value; }
307 return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth;
311 void ForceCursor (int newpos)
315 int actual_pos = shown_prompt.Length + TextToRenderPos (cursor);
316 int row = home_row + (actual_pos/Console.WindowWidth);
317 int col = actual_pos % Console.WindowWidth;
319 if (row >= Console.BufferHeight)
320 row = Console.BufferHeight-1;
321 Console.SetCursorPosition (col, row);
323 //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);
327 void UpdateCursor (int newpos)
329 if (cursor == newpos)
332 ForceCursor (newpos);
335 void InsertChar (char c)
337 int prev_lines = LineCount;
338 text = text.Insert (cursor, c);
340 if (prev_lines != LineCount){
342 Console.SetCursorPosition (0, home_row);
344 ForceCursor (++cursor);
347 ForceCursor (++cursor);
348 UpdateHomeRow (TextToScreenPos (cursor));
360 void CmdTabOrComplete ()
362 bool complete = false;
364 if (AutoCompleteEvent != null){
365 if (TabAtStartCompletes)
368 for (int i = 0; i < cursor; i++){
369 if (!Char.IsWhiteSpace (text [i])){
377 Completion completion = AutoCompleteEvent (text.ToString (), cursor);
378 string [] completions = completion.Result;
379 if (completions == null)
382 int ncompletions = completions.Length;
383 if (ncompletions == 0)
386 if (completions.Length == 1){
387 InsertTextAtCursor (completions [0]);
391 for (int p = 0; p < completions [0].Length; p++){
392 char c = completions [0][p];
395 for (int i = 1; i < ncompletions; i++){
396 if (completions [i].Length < p)
399 if (completions [i][p] != c){
407 InsertTextAtCursor (completions [0].Substring (0, last+1));
409 Console.WriteLine ();
410 foreach (string s in completions){
411 Console.Write (completion.Prefix);
415 Console.WriteLine ();
417 ForceCursor (cursor);
432 UpdateCursor (text.Length);
440 UpdateCursor (cursor-1);
443 void CmdBackwardWord ()
445 int p = WordBackward (cursor);
451 void CmdForwardWord ()
453 int p = WordForward (cursor);
461 if (cursor == text.Length)
464 UpdateCursor (cursor+1);
467 void RenderAfter (int p)
471 ForceCursor (cursor);
479 text.Remove (--cursor, 1);
481 RenderAfter (cursor);
484 void CmdDeleteChar ()
486 // If there is no input, this behaves like EOF
487 if (text.Length == 0){
490 Console.WriteLine ();
494 if (cursor == text.Length)
496 text.Remove (cursor, 1);
498 RenderAfter (cursor);
501 int WordForward (int p)
503 if (p >= text.Length)
507 if (Char.IsPunctuation (text [p]) || Char.IsSymbol (text [p]) || Char.IsWhiteSpace (text[p])){
508 for (; i < text.Length; i++){
509 if (Char.IsLetterOrDigit (text [i]))
512 for (; i < text.Length; i++){
513 if (!Char.IsLetterOrDigit (text [i]))
517 for (; i < text.Length; i++){
518 if (!Char.IsLetterOrDigit (text [i]))
527 int WordBackward (int p)
536 if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){
538 if (Char.IsLetterOrDigit (text [i]))
542 if (!Char.IsLetterOrDigit (text[i]))
547 if (!Char.IsLetterOrDigit (text [i]))
559 void CmdDeleteWord ()
561 int pos = WordForward (cursor);
566 string k = text.ToString (cursor, pos-cursor);
568 if (last_handler == CmdDeleteWord)
569 kill_buffer = kill_buffer + k;
573 text.Remove (cursor, pos-cursor);
575 RenderAfter (cursor);
578 void CmdDeleteBackword ()
580 int pos = WordBackward (cursor);
584 string k = text.ToString (pos, cursor-pos);
586 if (last_handler == CmdDeleteBackword)
587 kill_buffer = k + kill_buffer;
591 text.Remove (pos, cursor-pos);
597 // Adds the current line to the history if needed
599 void HistoryUpdateLine ()
601 history.Update (text.ToString ());
604 void CmdHistoryPrev ()
606 if (!history.PreviousAvailable ())
609 HistoryUpdateLine ();
611 SetText (history.Previous ());
614 void CmdHistoryNext ()
616 if (!history.NextAvailable())
619 history.Update (text.ToString ());
620 SetText (history.Next ());
626 kill_buffer = text.ToString (cursor, text.Length-cursor);
627 text.Length = cursor;
629 RenderAfter (cursor);
634 InsertTextAtCursor (kill_buffer);
637 void InsertTextAtCursor (string str)
639 int prev_lines = LineCount;
640 text.Insert (cursor, str);
642 if (prev_lines != LineCount){
643 Console.SetCursorPosition (0, home_row);
645 cursor += str.Length;
646 ForceCursor (cursor);
649 cursor += str.Length;
650 ForceCursor (cursor);
651 UpdateHomeRow (TextToScreenPos (cursor));
655 void SetSearchPrompt (string s)
657 SetPrompt ("(reverse-i-search)`" + s + "': ");
660 void ReverseSearch ()
664 if (cursor == text.Length){
665 // The cursor is at the end of the string
667 p = text.ToString ().LastIndexOf (search);
671 ForceCursor (cursor);
675 // The cursor is somewhere in the middle of the string
676 int start = (cursor == match_at) ? cursor - 1 : cursor;
678 p = text.ToString ().LastIndexOf (search, start);
682 ForceCursor (cursor);
688 // Need to search backwards in history
689 HistoryUpdateLine ();
690 string s = history.SearchBackward (search);
698 void CmdReverseSearch ()
702 last_search = search;
705 SetSearchPrompt ("");
708 if (last_search != "" && last_search != null){
709 search = last_search;
710 SetSearchPrompt (search);
720 void SearchAppend (char c)
723 SetSearchPrompt (search);
726 // If the new typed data still matches the current text, stay here
728 if (cursor < text.Length){
729 string r = text.ToString (cursor, text.Length - cursor);
730 if (r.StartsWith (search))
742 ForceCursor (cursor);
745 void InterruptEdit (object sender, ConsoleCancelEventArgs a)
747 // Do not abort our program:
750 // Interrupt the editor
754 void HandleChar (char c)
767 ConsoleModifiers mod;
769 cki = Console.ReadKey (true);
770 if (cki.Key == ConsoleKey.Escape){
771 cki = Console.ReadKey (true);
773 mod = ConsoleModifiers.Alt;
777 bool handled = false;
779 foreach (Handler handler in handlers){
780 ConsoleKeyInfo t = handler.CKI;
782 if (t.Key == cki.Key && t.Modifiers == mod){
784 handler.KeyHandler ();
785 last_handler = handler.KeyHandler;
787 } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
789 handler.KeyHandler ();
790 last_handler = handler.KeyHandler;
796 if (last_handler != CmdReverseSearch){
804 if (cki.KeyChar != (char) 0)
805 HandleChar (cki.KeyChar);
809 void InitText (string initial)
811 text = new StringBuilder (initial);
813 cursor = text.Length;
815 ForceCursor (cursor);
818 void SetText (string newtext)
820 Console.SetCursorPosition (0, home_row);
824 void SetPrompt (string newprompt)
826 shown_prompt = newprompt;
827 Console.SetCursorPosition (0, home_row);
829 ForceCursor (cursor);
832 public string Edit (string prompt, string initial)
834 edit_thread = Thread.CurrentThread;
836 Console.CancelKeyPress += InterruptEdit;
839 history.CursorToEnd ();
843 shown_prompt = prompt;
845 history.Append (initial);
850 } catch (ThreadAbortException){
852 Thread.ResetAbort ();
853 Console.WriteLine ();
858 Console.WriteLine ();
860 Console.CancelKeyPress -= InterruptEdit;
867 string result = text.ToString ();
869 history.Accept (result);
871 history.RemoveLast ();
876 public bool TabAtStartCompletes { get; set; }
879 // Emulates the bash-like behavior, where edits done to the
880 // history are recorded
888 public History (string app, int size)
891 throw new ArgumentException ("size");
894 string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
895 //Console.WriteLine (dir);
896 if (!Directory.Exists (dir)){
898 Directory.CreateDirectory (dir);
904 histfile = Path.Combine (dir, app) + ".history";
907 history = new string [size];
908 head = tail = cursor = 0;
910 if (File.Exists (histfile)){
911 using (StreamReader sr = File.OpenText (histfile)){
914 while ((line = sr.ReadLine ()) != null){
924 if (histfile == null)
928 using (StreamWriter sw = File.CreateText (histfile)){
929 int start = (count == history.Length) ? head : tail;
930 for (int i = start; i < start+count; i++){
931 int p = i % history.Length;
932 sw.WriteLine (history [p]);
941 // Appends a value to the history
943 public void Append (string s)
945 //Console.WriteLine ("APPENDING {0} head={1} tail={2}", s, head, tail);
947 head = (head+1) % history.Length;
949 tail = (tail+1 % history.Length);
950 if (count != history.Length)
952 //Console.WriteLine ("DONE: head={1} tail={2}", s, head, tail);
956 // Updates the current cursor location with the string,
957 // to support editing of history items. For the current
958 // line to participate, an Append must be done before.
960 public void Update (string s)
962 history [cursor] = s;
965 public void RemoveLast ()
969 head = history.Length-1;
972 public void Accept (string s)
976 t = history.Length-1;
981 public bool PreviousAvailable ()
983 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
996 public bool NextAvailable ()
1000 int next = (cursor + 1) % history.Length;
1008 // Returns: a string with the previous line contents, or
1009 // nul if there is no data in the history to move to.
1011 public string Previous ()
1013 if (!PreviousAvailable ())
1018 cursor = history.Length - 1;
1020 return history [cursor];
1023 public string Next ()
1025 if (!NextAvailable ())
1028 cursor = (cursor + 1) % history.Length;
1029 return history [cursor];
1032 public void CursorToEnd ()
1042 Console.WriteLine ("Head={0} Tail={1} Cursor={2} count={3}", head, tail, cursor, count);
1043 for (int i = 0; i < history.Length;i++){
1044 Console.WriteLine (" {0} {1}: {2}", i == cursor ? "==>" : " ", i, history[i]);
1049 public string SearchBackward (string term)
1051 for (int i = 0; i < count; i++){
1052 int slot = cursor-i-1;
1054 slot = history.Length+slot;
1055 if (slot >= history.Length)
1057 if (history [slot] != null && history [slot].IndexOf (term) != -1){
1059 return history [slot];
1073 LineEditor le = new LineEditor ("foo");
1076 while ((s = le.Edit ("shell> ", "")) != null){
1077 Console.WriteLine ("----> [{0}]", s);