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.
25 // Typing before the program start causes the cursor position to be wrong
26 // This is caused by Console not reading all the available data
27 // before sending the report-position sequence and reading it back.
29 #if NET_2_0 || NET_1_1
33 // Only compile this code in the 2.0 profile, but not in the Moonlight one
34 #if (IN_MCS_BUILD && NET_2_0 && !SMCS_SOURCE) || !IN_MCS_BUILD
38 using System.Threading;
39 using System.Reflection;
41 namespace Mono.Terminal {
43 public class LineEditor {
45 public class Completion {
46 public string [] Result;
49 public Completion (string prefix, string [] result)
56 public delegate Completion AutoCompleteHandler (string text, int pos);
58 //static StreamWriter log;
60 // The text being edited.
63 // The text as it is rendered (replaces (char)1 with ^A on display for example).
64 StringBuilder rendered_text;
66 // The prompt specified, and the prompt shown to the user.
70 // The current cursor position, indexes into "text", for an index
71 // into rendered_text, use TextToRenderPos
74 // The row where we started displaying data.
77 // The maximum length that has been displayed on the screen
80 // If we are done editing, this breaks the interactive loop
83 // The thread where the Editing started taking place
86 // Our object that tracks history
89 // The contents of the kill buffer (cut/paste in Emacs parlance)
90 string kill_buffer = "";
92 // The string being searched for
96 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
99 // The position where we found the match.
102 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
103 KeyHandler last_handler;
105 delegate void KeyHandler ();
108 public ConsoleKeyInfo CKI;
109 public KeyHandler KeyHandler;
111 public Handler (ConsoleKey key, KeyHandler h)
113 CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
117 public Handler (char c, KeyHandler h)
120 // Use the "Zoom" as a flag that we only have a character.
121 CKI = new ConsoleKeyInfo (c, ConsoleKey.Zoom, false, false, false);
124 public Handler (ConsoleKeyInfo cki, KeyHandler h)
130 public static Handler Control (char c, KeyHandler h)
132 return new Handler ((char) (c - 'A' + 1), h);
135 public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
137 ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false);
138 return new Handler (cki, h);
143 /// Invoked when the user requests auto-completion using the tab character
146 /// The result is null for no values found, an array with a single
147 /// string, in that case the string should be the text to be inserted
148 /// for example if the word at pos is "T", the result for a completion
149 /// of "ToString" should be "oString", not "ToString".
151 /// When there are multiple results, the result should be the full
154 public AutoCompleteHandler AutoCompleteEvent;
156 static Handler [] handlers;
158 public LineEditor (string name) : this (name, 10) { }
160 public LineEditor (string name, int histsize)
162 handlers = new Handler [] {
163 new Handler (ConsoleKey.Home, CmdHome),
164 new Handler (ConsoleKey.End, CmdEnd),
165 new Handler (ConsoleKey.LeftArrow, CmdLeft),
166 new Handler (ConsoleKey.RightArrow, CmdRight),
167 new Handler (ConsoleKey.UpArrow, CmdHistoryPrev),
168 new Handler (ConsoleKey.DownArrow, CmdHistoryNext),
169 new Handler (ConsoleKey.Enter, CmdDone),
170 new Handler (ConsoleKey.Backspace, CmdBackspace),
171 new Handler (ConsoleKey.Delete, CmdDeleteChar),
172 new Handler (ConsoleKey.Tab, CmdTabOrComplete),
175 Handler.Control ('A', CmdHome),
176 Handler.Control ('E', CmdEnd),
177 Handler.Control ('B', CmdLeft),
178 Handler.Control ('F', CmdRight),
179 Handler.Control ('P', CmdHistoryPrev),
180 Handler.Control ('N', CmdHistoryNext),
181 Handler.Control ('K', CmdKillToEOF),
182 Handler.Control ('Y', CmdYank),
183 Handler.Control ('D', CmdDeleteChar),
184 Handler.Control ('L', CmdRefresh),
185 Handler.Control ('R', CmdReverseSearch),
186 Handler.Control ('G', delegate {} ),
187 Handler.Alt ('B', ConsoleKey.B, CmdBackwardWord),
188 Handler.Alt ('F', ConsoleKey.F, CmdForwardWord),
190 Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord),
191 Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword),
194 Handler.Control ('T', CmdDebug),
197 Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); })
200 rendered_text = new StringBuilder ();
201 text = new StringBuilder ();
203 history = new History (name, histsize);
205 //if (File.Exists ("log"))File.Delete ("log");
206 //log = File.CreateText ("log");
212 Console.WriteLine ();
218 Console.Write (shown_prompt);
219 Console.Write (rendered_text);
221 int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered);
223 for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++)
225 max_rendered = shown_prompt.Length + rendered_text.Length;
227 // Write one more to ensure that we always wrap around properly if we are at the
234 void UpdateHomeRow (int screenpos)
236 int lines = 1 + (screenpos / Console.WindowWidth);
238 home_row = Console.CursorTop - (lines - 1);
244 void RenderFrom (int pos)
246 int rpos = TextToRenderPos (pos);
249 for (i = rpos; i < rendered_text.Length; i++)
250 Console.Write (rendered_text [i]);
252 if ((shown_prompt.Length + rendered_text.Length) > max_rendered)
253 max_rendered = shown_prompt.Length + rendered_text.Length;
255 int max_extra = max_rendered - shown_prompt.Length;
256 for (; i < max_extra; i++)
261 void ComputeRendered ()
263 rendered_text.Length = 0;
265 for (int i = 0; i < text.Length; i++){
266 int c = (int) text [i];
269 rendered_text.Append (" ");
271 rendered_text.Append ('^');
272 rendered_text.Append ((char) (c + (int) 'A' - 1));
275 rendered_text.Append ((char)c);
279 int TextToRenderPos (int pos)
283 for (int i = 0; i < pos; i++){
300 int TextToScreenPos (int pos)
302 return shown_prompt.Length + TextToRenderPos (pos);
306 get { return prompt; }
307 set { prompt = value; }
312 return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth;
316 void ForceCursor (int newpos)
320 int actual_pos = shown_prompt.Length + TextToRenderPos (cursor);
321 int row = home_row + (actual_pos/Console.WindowWidth);
322 int col = actual_pos % Console.WindowWidth;
324 if (row >= Console.BufferHeight)
325 row = Console.BufferHeight-1;
326 Console.SetCursorPosition (col, row);
328 //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);
332 void UpdateCursor (int newpos)
334 if (cursor == newpos)
337 ForceCursor (newpos);
340 void InsertChar (char c)
342 int prev_lines = LineCount;
343 text = text.Insert (cursor, c);
345 if (prev_lines != LineCount){
347 Console.SetCursorPosition (0, home_row);
349 ForceCursor (++cursor);
352 ForceCursor (++cursor);
353 UpdateHomeRow (TextToScreenPos (cursor));
365 void CmdTabOrComplete ()
367 bool complete = false;
369 if (AutoCompleteEvent != null){
370 if (TabAtStartCompletes)
373 for (int i = 0; i < cursor; i++){
374 if (!Char.IsWhiteSpace (text [i])){
382 Completion completion = AutoCompleteEvent (text.ToString (), cursor);
383 string [] completions = completion.Result;
384 if (completions == null)
387 int ncompletions = completions.Length;
388 if (ncompletions == 0)
391 if (completions.Length == 1){
392 InsertTextAtCursor (completions [0]);
396 for (int p = 0; p < completions [0].Length; p++){
397 char c = completions [0][p];
400 for (int i = 1; i < ncompletions; i++){
401 if (completions [i].Length < p)
404 if (completions [i][p] != c){
412 InsertTextAtCursor (completions [0].Substring (0, last+1));
414 Console.WriteLine ();
415 foreach (string s in completions){
416 Console.Write (completion.Prefix);
420 Console.WriteLine ();
422 ForceCursor (cursor);
437 UpdateCursor (text.Length);
445 UpdateCursor (cursor-1);
448 void CmdBackwardWord ()
450 int p = WordBackward (cursor);
456 void CmdForwardWord ()
458 int p = WordForward (cursor);
466 if (cursor == text.Length)
469 UpdateCursor (cursor+1);
472 void RenderAfter (int p)
476 ForceCursor (cursor);
484 text.Remove (--cursor, 1);
486 RenderAfter (cursor);
489 void CmdDeleteChar ()
491 // If there is no input, this behaves like EOF
492 if (text.Length == 0){
495 Console.WriteLine ();
499 if (cursor == text.Length)
501 text.Remove (cursor, 1);
503 RenderAfter (cursor);
506 int WordForward (int p)
508 if (p >= text.Length)
512 if (Char.IsPunctuation (text [p]) || Char.IsWhiteSpace (text[p])){
513 for (; i < text.Length; i++){
514 if (Char.IsLetterOrDigit (text [i]))
517 for (; i < text.Length; i++){
518 if (!Char.IsLetterOrDigit (text [i]))
522 for (; i < text.Length; i++){
523 if (!Char.IsLetterOrDigit (text [i]))
532 int WordBackward (int p)
541 if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){
543 if (Char.IsLetterOrDigit (text [i]))
547 if (!Char.IsLetterOrDigit (text[i]))
552 if (!Char.IsLetterOrDigit (text [i]))
564 void CmdDeleteWord ()
566 int pos = WordForward (cursor);
571 string k = text.ToString (cursor, pos-cursor);
573 if (last_handler == CmdDeleteWord)
574 kill_buffer = kill_buffer + k;
578 text.Remove (cursor, pos-cursor);
580 RenderAfter (cursor);
583 void CmdDeleteBackword ()
585 int pos = WordBackward (cursor);
589 string k = text.ToString (pos, cursor-pos);
591 if (last_handler == CmdDeleteBackword)
592 kill_buffer = k + kill_buffer;
596 text.Remove (pos, cursor-pos);
602 // Adds the current line to the history if needed
604 void HistoryUpdateLine ()
606 history.Update (text.ToString ());
609 void CmdHistoryPrev ()
611 if (!history.PreviousAvailable ())
614 HistoryUpdateLine ();
616 SetText (history.Previous ());
619 void CmdHistoryNext ()
621 if (!history.NextAvailable())
624 history.Update (text.ToString ());
625 SetText (history.Next ());
631 kill_buffer = text.ToString (cursor, text.Length-cursor);
632 text.Length = cursor;
634 RenderAfter (cursor);
639 InsertTextAtCursor (kill_buffer);
642 void InsertTextAtCursor (string str)
644 int prev_lines = LineCount;
645 text.Insert (cursor, str);
647 if (prev_lines != LineCount){
648 Console.SetCursorPosition (0, home_row);
650 cursor += str.Length;
651 ForceCursor (cursor);
654 cursor += str.Length;
655 ForceCursor (cursor);
656 UpdateHomeRow (TextToScreenPos (cursor));
660 void SetSearchPrompt (string s)
662 SetPrompt ("(reverse-i-search)`" + s + "': ");
665 void ReverseSearch ()
669 if (cursor == text.Length){
670 // The cursor is at the end of the string
672 p = text.ToString ().LastIndexOf (search);
676 ForceCursor (cursor);
680 // The cursor is somewhere in the middle of the string
681 int start = (cursor == match_at) ? cursor - 1 : cursor;
683 p = text.ToString ().LastIndexOf (search, start);
687 ForceCursor (cursor);
693 // Need to search backwards in history
694 HistoryUpdateLine ();
695 string s = history.SearchBackward (search);
703 void CmdReverseSearch ()
707 last_search = search;
710 SetSearchPrompt ("");
713 if (last_search != "" && last_search != null){
714 search = last_search;
715 SetSearchPrompt (search);
725 void SearchAppend (char c)
728 SetSearchPrompt (search);
731 // If the new typed data still matches the current text, stay here
733 if (cursor < text.Length){
734 string r = text.ToString (cursor, text.Length - cursor);
735 if (r.StartsWith (search))
747 ForceCursor (cursor);
750 void InterruptEdit (object sender, ConsoleCancelEventArgs a)
752 // Do not abort our program:
755 // Interrupt the editor
759 void HandleChar (char c)
772 cki = Console.ReadKey (true);
774 bool handled = false;
775 foreach (Handler handler in handlers){
776 ConsoleKeyInfo t = handler.CKI;
778 if (t.Key == cki.Key && t.Modifiers == cki.Modifiers){
780 handler.KeyHandler ();
781 last_handler = handler.KeyHandler;
783 } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
785 handler.KeyHandler ();
786 last_handler = handler.KeyHandler;
792 if (last_handler != CmdReverseSearch){
800 if (cki.KeyChar != (char) 0)
801 HandleChar (cki.KeyChar);
805 void InitText (string initial)
807 text = new StringBuilder (initial);
809 cursor = text.Length;
811 ForceCursor (cursor);
814 void SetText (string newtext)
816 Console.SetCursorPosition (0, home_row);
820 void SetPrompt (string newprompt)
822 shown_prompt = newprompt;
823 Console.SetCursorPosition (0, home_row);
825 ForceCursor (cursor);
828 public string Edit (string prompt, string initial)
830 edit_thread = Thread.CurrentThread;
832 Console.CancelKeyPress += InterruptEdit;
835 history.CursorToEnd ();
839 shown_prompt = prompt;
841 history.Append (initial);
846 } catch (ThreadAbortException){
848 Thread.ResetAbort ();
849 Console.WriteLine ();
854 Console.WriteLine ();
856 Console.CancelKeyPress -= InterruptEdit;
863 string result = text.ToString ();
865 history.Accept (result);
867 history.RemoveLast ();
872 public bool TabAtStartCompletes { get; set; }
875 // Emulates the bash-like behavior, where edits done to the
876 // history are recorded
884 public History (string app, int size)
887 throw new ArgumentException ("size");
890 string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
891 //Console.WriteLine (dir);
892 if (!Directory.Exists (dir)){
894 Directory.CreateDirectory (dir);
900 histfile = Path.Combine (dir, app) + ".history";
903 history = new string [size];
904 head = tail = cursor = 0;
906 if (File.Exists (histfile)){
907 using (StreamReader sr = File.OpenText (histfile)){
910 while ((line = sr.ReadLine ()) != null){
920 if (histfile == null)
924 using (StreamWriter sw = File.CreateText (histfile)){
925 int start = (count == history.Length) ? head : tail;
926 for (int i = start; i < start+count; i++){
927 int p = i % history.Length;
928 sw.WriteLine (history [p]);
937 // Appends a value to the history
939 public void Append (string s)
941 //Console.WriteLine ("APPENDING {0} {1}", s, Environment.StackTrace);
943 head = (head+1) % history.Length;
945 tail = (tail+1 % history.Length);
946 if (count != history.Length)
951 // Updates the current cursor location with the string,
952 // to support editing of history items. For the current
953 // line to participate, an Append must be done before.
955 public void Update (string s)
957 history [cursor] = s;
960 public void RemoveLast ()
964 head = history.Length-1;
967 public void Accept (string s)
971 t = history.Length-1;
976 public bool PreviousAvailable ()
978 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
979 if (count == 0 || cursor == tail)
985 public bool NextAvailable ()
987 int next = (cursor + 1) % history.Length;
988 if (count == 0 || next > head)
996 // Returns: a string with the previous line contents, or
997 // nul if there is no data in the history to move to.
999 public string Previous ()
1001 if (!PreviousAvailable ())
1006 cursor = history.Length - 1;
1008 return history [cursor];
1011 public string Next ()
1013 if (!NextAvailable ())
1016 cursor = (cursor + 1) % history.Length;
1017 return history [cursor];
1020 public void CursorToEnd ()
1030 Console.WriteLine ("Head={0} Tail={1} Cursor={2}", head, tail, cursor);
1031 for (int i = 0; i < history.Length;i++){
1032 Console.WriteLine (" {0} {1}: {2}", i == cursor ? "==>" : " ", i, history[i]);
1037 public string SearchBackward (string term)
1039 for (int i = 1; i < count; i++){
1040 int slot = cursor-i;
1042 slot = history.Length-1;
1043 if (history [slot] != null && history [slot].IndexOf (term) != -1){
1045 return history [slot];
1048 // Will the next hit tail?
1051 slot = history.Length-1;
1066 LineEditor le = new LineEditor (null);
1069 while ((s = le.Edit ("shell> ", "")) != null){
1070 Console.WriteLine ("----> [{0}]", s);