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.
26 #if NET_2_0 || NET_1_1
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
35 using System
.Threading
;
36 using System
.Reflection
;
38 namespace Mono
.Terminal
{
40 public class LineEditor
{
42 public class Completion
{
43 public string [] Result
;
46 public Completion (string prefix
, string [] result
)
53 public delegate Completion
AutoCompleteHandler (string text
, int pos
);
55 //static StreamWriter log;
57 // The text being edited.
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.
67 // The current cursor position, indexes into "text", for an index
68 // into rendered_text, use TextToRenderPos
71 // The row where we started displaying data.
74 // The maximum length that has been displayed on the screen
77 // If we are done editing, this breaks the interactive loop
80 // The thread where the Editing started taking place
83 // Our object that tracks history
86 // The contents of the kill buffer (cut/paste in Emacs parlance)
87 string kill_buffer
= "";
89 // The string being searched for
93 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
96 // The position where we found the match.
99 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
100 KeyHandler last_handler
;
102 delegate void KeyHandler ();
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);
114 public Handler (char c
, 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
)
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
);
140 /// Invoked when the user requests auto-completion using the tab character
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
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
),
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
),
191 //Handler.Control ('T', CmdDebug),
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");
209 Console
.WriteLine ();
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
++)
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
231 void UpdateHomeRow (int screenpos
)
233 int lines
= 1 + (screenpos
/ Console
.WindowWidth
);
235 home_row
= Console
.CursorTop
- (lines
- 1);
241 void RenderFrom (int pos
)
243 int rpos
= TextToRenderPos (pos
);
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
;
252 int max_extra
= max_rendered
- shown_prompt
.Length
;
253 for (; i
< max_extra
; i
++)
258 void ComputeRendered ()
260 rendered_text
.Length
= 0;
262 for (int i
= 0; i
< text
.Length
; i
++){
263 int c
= (int) text
[i
];
266 rendered_text
.Append (" ");
268 rendered_text
.Append ('^');
269 rendered_text
.Append ((char) (c
+ (int) 'A' - 1));
272 rendered_text
.Append ((char)c
);
276 int TextToRenderPos (int pos
)
280 for (int i
= 0; i
< pos
; i
++){
297 int TextToScreenPos (int pos
)
299 return shown_prompt
.Length
+ TextToRenderPos (pos
);
303 get { return prompt; }
304 set { prompt = value; }
309 return (shown_prompt
.Length
+ rendered_text
.Length
)/Console
.WindowWidth
;
313 void ForceCursor (int 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);
329 void UpdateCursor (int newpos
)
331 if (cursor
== newpos
)
334 ForceCursor (newpos
);
337 void InsertChar (char c
)
339 int prev_lines
= LineCount
;
340 text
= text
.Insert (cursor
, c
);
342 if (prev_lines
!= LineCount
){
344 Console
.SetCursorPosition (0, home_row
);
346 ForceCursor (++cursor
);
349 ForceCursor (++cursor
);
350 UpdateHomeRow (TextToScreenPos (cursor
));
362 void CmdTabOrComplete ()
364 bool complete
= false;
366 if (AutoCompleteEvent
!= null){
367 if (TabAtStartCompletes
)
370 for (int i
= 0; i
< cursor
; i
++){
371 if (!Char
.IsWhiteSpace (text
[i
])){
379 Completion completion
= AutoCompleteEvent (text
.ToString (), cursor
);
380 string [] completions
= completion
.Result
;
381 if (completions
== null)
384 int ncompletions
= completions
.Length
;
385 if (ncompletions
== 0)
388 if (completions
.Length
== 1){
389 InsertTextAtCursor (completions
[0]);
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
)
401 if (completions
[i
][p
] != c
){
409 InsertTextAtCursor (completions
[0].Substring (0, last
+1));
411 Console
.WriteLine ();
412 foreach (string s
in completions
){
413 Console
.Write (completion
.Prefix
);
417 Console
.WriteLine ();
419 ForceCursor (cursor
);
434 UpdateCursor (text
.Length
);
442 UpdateCursor (cursor
-1);
445 void CmdBackwardWord ()
447 int p
= WordBackward (cursor
);
453 void CmdForwardWord ()
455 int p
= WordForward (cursor
);
463 if (cursor
== text
.Length
)
466 UpdateCursor (cursor
+1);
469 void RenderAfter (int p
)
473 ForceCursor (cursor
);
481 text
.Remove (--cursor
, 1);
483 RenderAfter (cursor
);
486 void CmdDeleteChar ()
488 // If there is no input, this behaves like EOF
489 if (text
.Length
== 0){
492 Console
.WriteLine ();
496 if (cursor
== text
.Length
)
498 text
.Remove (cursor
, 1);
500 RenderAfter (cursor
);
503 int WordForward (int p
)
505 if (p
>= text
.Length
)
509 if (Char
.IsPunctuation (text
[p
]) || Char
.IsWhiteSpace (text
[p
])){
510 for (; i
< text
.Length
; i
++){
511 if (Char
.IsLetterOrDigit (text
[i
]))
514 for (; i
< text
.Length
; i
++){
515 if (!Char
.IsLetterOrDigit (text
[i
]))
519 for (; i
< text
.Length
; i
++){
520 if (!Char
.IsLetterOrDigit (text
[i
]))
529 int WordBackward (int p
)
538 if (Char
.IsPunctuation (text
[i
]) || Char
.IsSymbol (text
[i
]) || Char
.IsWhiteSpace (text
[i
])){
540 if (Char
.IsLetterOrDigit (text
[i
]))
544 if (!Char
.IsLetterOrDigit (text
[i
]))
549 if (!Char
.IsLetterOrDigit (text
[i
]))
561 void CmdDeleteWord ()
563 int pos
= WordForward (cursor
);
568 string k
= text
.ToString (cursor
, pos
-cursor
);
570 if (last_handler
== CmdDeleteWord
)
571 kill_buffer
= kill_buffer
+ k
;
575 text
.Remove (cursor
, pos
-cursor
);
577 RenderAfter (cursor
);
580 void CmdDeleteBackword ()
582 int pos
= WordBackward (cursor
);
586 string k
= text
.ToString (pos
, cursor
-pos
);
588 if (last_handler
== CmdDeleteBackword
)
589 kill_buffer
= k
+ kill_buffer
;
593 text
.Remove (pos
, cursor
-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 ())
611 HistoryUpdateLine ();
613 SetText (history
.Previous ());
616 void CmdHistoryNext ()
618 if (!history
.NextAvailable())
621 history
.Update (text
.ToString ());
622 SetText (history
.Next ());
628 kill_buffer
= text
.ToString (cursor
, text
.Length
-cursor
);
629 text
.Length
= cursor
;
631 RenderAfter (cursor
);
636 InsertTextAtCursor (kill_buffer
);
639 void InsertTextAtCursor (string str
)
641 int prev_lines
= LineCount
;
642 text
.Insert (cursor
, str
);
644 if (prev_lines
!= LineCount
){
645 Console
.SetCursorPosition (0, home_row
);
647 cursor
+= str
.Length
;
648 ForceCursor (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 ()
666 if (cursor
== text
.Length
){
667 // The cursor is at the end of the string
669 p
= text
.ToString ().LastIndexOf (search
);
673 ForceCursor (cursor
);
677 // The cursor is somewhere in the middle of the string
678 int start
= (cursor
== match_at
) ? cursor
- 1 : cursor
;
680 p
= text
.ToString ().LastIndexOf (search
, start
);
684 ForceCursor (cursor
);
690 // Need to search backwards in history
691 HistoryUpdateLine ();
692 string s
= history
.SearchBackward (search
);
700 void CmdReverseSearch ()
704 last_search
= search
;
707 SetSearchPrompt ("");
710 if (last_search
!= "" && last_search
!= null){
711 search
= last_search
;
712 SetSearchPrompt (search
);
722 void SearchAppend (char 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
))
744 ForceCursor (cursor
);
747 void InterruptEdit (object sender
, ConsoleCancelEventArgs a
)
749 // Do not abort our program:
752 // Interrupt the editor
756 void HandleChar (char c
)
769 ConsoleModifiers mod
;
771 cki
= Console
.ReadKey (true);
772 if (cki
.Key
== ConsoleKey
.Escape
){
773 cki
= Console
.ReadKey (true);
775 mod
= ConsoleModifiers
.Alt
;
779 bool handled
= false;
781 foreach (Handler handler
in handlers
){
782 ConsoleKeyInfo t
= handler
.CKI
;
784 if (t
.Key
== cki
.Key
&& t
.Modifiers
== mod
){
786 handler
.KeyHandler ();
787 last_handler
= handler
.KeyHandler
;
789 } else if (t
.KeyChar
== cki
.KeyChar
&& t
.Key
== ConsoleKey
.Zoom
){
791 handler
.KeyHandler ();
792 last_handler
= handler
.KeyHandler
;
798 if (last_handler
!= CmdReverseSearch
){
806 if (cki
.KeyChar
!= (char) 0)
807 HandleChar (cki
.KeyChar
);
811 void InitText (string initial
)
813 text
= new StringBuilder (initial
);
815 cursor
= text
.Length
;
817 ForceCursor (cursor
);
820 void SetText (string newtext
)
822 Console
.SetCursorPosition (0, home_row
);
826 void SetPrompt (string newprompt
)
828 shown_prompt
= newprompt
;
829 Console
.SetCursorPosition (0, home_row
);
831 ForceCursor (cursor
);
834 public string Edit (string prompt
, string initial
)
836 edit_thread
= Thread
.CurrentThread
;
838 Console
.CancelKeyPress
+= InterruptEdit
;
841 history
.CursorToEnd ();
845 shown_prompt
= prompt
;
847 history
.Append (initial
);
852 } catch (ThreadAbortException
){
854 Thread
.ResetAbort ();
855 Console
.WriteLine ();
860 Console
.WriteLine ();
862 Console
.CancelKeyPress
-= InterruptEdit
;
869 string result
= text
.ToString ();
871 history
.Accept (result
);
873 history
.RemoveLast ();
878 public bool TabAtStartCompletes { get; set; }
881 // Emulates the bash-like behavior, where edits done to the
882 // history are recorded
890 public History (string app
, int size
)
893 throw new ArgumentException ("size");
896 string dir
= Environment
.GetFolderPath (Environment
.SpecialFolder
.ApplicationData
);
897 //Console.WriteLine (dir);
898 if (!Directory
.Exists (dir
)){
900 Directory
.CreateDirectory (dir
);
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
)){
916 while ((line
= sr
.ReadLine ()) != null){
926 if (histfile
== null)
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
]);
943 // Appends a value to the history
945 public void Append (string s
)
947 //Console.WriteLine ("APPENDING {0} {1}", s, Environment.StackTrace);
949 head
= (head
+1) % history
.Length
;
951 tail
= (tail
+1 % history
.Length
);
952 if (count
!= history
.Length
)
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 ()
970 head
= history
.Length
-1;
973 public void Accept (string s
)
977 t
= history
.Length
-1;
982 public bool PreviousAvailable ()
984 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
985 if (count
== 0 || cursor
== tail
)
991 public bool NextAvailable ()
993 int next
= (cursor
+ 1) % history
.Length
;
994 if (count
== 0 || next
>= head
)
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 ())
1012 cursor
= history
.Length
- 1;
1014 return history
[cursor
];
1017 public string Next ()
1019 if (!NextAvailable ())
1022 cursor
= (cursor
+ 1) % history
.Length
;
1023 return history
[cursor
];
1026 public void CursorToEnd ()
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
]);
1043 public string SearchBackward (string term
)
1045 for (int i
= 1; i
< count
; i
++){
1046 int slot
= cursor
-i
;
1048 slot
= history
.Length
-1;
1049 if (history
[slot
] != null && history
[slot
].IndexOf (term
) != -1){
1051 return history
[slot
];
1054 // Will the next hit tail?
1057 slot
= history
.Length
-1;
1072 LineEditor le
= new LineEditor (null);
1075 while ((s
= le
.Edit ("shell> ", "")) != null){
1076 Console
.WriteLine ("----> [{0}]", s
);