2 // getline.cs: A command line editor
5 // Miguel de Icaza (miguel@novell.com)
7 // Copyright 2008 Novell, Inc.
8 // Copyright 2016 Xamarin Inc
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
23 // USE -define:DEMO to build this as a standalone file and test it
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.
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.
38 // About 8 lines missing, type "Con<TAB>" and not enough lines are inserted at the bottom.
44 using System
.Threading
;
45 using System
.Reflection
;
47 namespace Mono
.Terminal
{
49 public class LineEditor
{
51 public class Completion
{
52 public string [] Result
;
55 public Completion (string prefix
, string [] 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.
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.
79 // The current cursor position, indexes into "text", for an index
80 // into rendered_text, use TextToRenderPos
83 // The row where we started displaying data.
86 // The maximum length that has been displayed on the screen
89 // If we are done editing, this breaks the interactive loop
92 // The thread where the Editing started taking place
95 // Our object that tracks history
98 // The contents of the kill buffer (cut/paste in Emacs parlance)
99 string kill_buffer
= "";
101 // The string being searched for
105 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
108 // The position where we found the match.
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 ();
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);
134 ResetCompletion
= resetCompletion
;
137 public Handler (char c
, KeyHandler h
, bool resetCompletion
= true)
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)
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
);
165 /// Invoked when the user requests auto-completion using the tab character
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
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),
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
),
216 //Handler.Control ('T', CmdDebug),
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);
247 // Sole purpose of this call is to initialize the Terminfo driver
248 var x
= Console
.CursorLeft
;
251 var terminfo_driver
= Type
.GetType ("System.ConsoleDriver")?.GetField ("driver", BindingFlags
.Static
| BindingFlags
.NonPublic
)?.GetValue (null);
252 if (terminfo_driver
== null)
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
);
269 Console
.WriteLine ();
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
++)
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
291 void UpdateHomeRow (int screenpos
)
293 int lines
= 1 + (screenpos
/ Console
.WindowWidth
);
295 home_row
= Console
.CursorTop
- (lines
- 1);
301 void RenderFrom (int pos
)
303 int rpos
= TextToRenderPos (pos
);
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
;
312 int max_extra
= max_rendered
- shown_prompt
.Length
;
313 for (; i
< max_extra
; i
++)
318 void ComputeRendered ()
320 rendered_text
.Length
= 0;
322 for (int i
= 0; i
< text
.Length
; i
++){
323 int c
= (int) text
[i
];
326 rendered_text
.Append (" ");
328 rendered_text
.Append ('^');
329 rendered_text
.Append ((char) (c
+ (int) 'A' - 1));
332 rendered_text
.Append ((char)c
);
336 int TextToRenderPos (int pos
)
340 for (int i
= 0; i
< pos
; i
++){
357 int TextToScreenPos (int pos
)
359 return shown_prompt
.Length
+ TextToRenderPos (pos
);
363 get { return prompt; }
364 set { prompt = value; }
369 return (shown_prompt
.Length
+ rendered_text
.Length
)/Console
.WindowWidth
;
373 void ForceCursor (int 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);
389 void UpdateCursor (int newpos
)
391 if (cursor
== newpos
)
394 ForceCursor (newpos
);
397 void InsertChar (char c
)
399 int prev_lines
= LineCount
;
400 text
= text
.Insert (cursor
, c
);
402 if (prev_lines
!= LineCount
){
404 Console
.SetCursorPosition (0, home_row
);
406 ForceCursor (++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
;
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
);
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
)
447 throw new ArgumentException ("Cannot be less than zero" + Col
, "Col");
449 throw new ArgumentException ("Cannot be less than zero", "Row");
451 throw new ArgumentException ("Cannot be less than one", "Width");
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
++)
478 public string Current
{
480 return Completions
[selected_item
];
486 SaveExcursion (DrawSelection
);
489 public void SelectNext ()
491 if (selected_item
+1 < Completions
.Length
){
493 if (selected_item
- top_item
>= Height
)
495 SaveExcursion (DrawSelection
);
499 public void SelectPrevious ()
501 if (selected_item
> 0){
503 if (selected_item
< top_item
)
504 top_item
= selected_item
;
505 SaveExcursion (DrawSelection
);
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
++)
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;
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
) {
559 Completions
= completions
,
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)
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.
584 Completion completion
= AutoCompleteEvent (text
.ToString (), cursor
);
585 string [] completions
= completion
.Result
;
586 if (completions
== null){
591 int ncompletions
= completions
.Length
;
592 if (ncompletions
== 0){
597 if (completions
.Length
== 1){
598 InsertTextAtCursor (completions
[0]);
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
)
611 if (completions
[i
][p
] != c
){
618 var prefix
= completion
.Prefix
;
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
);
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
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)
648 int ncompletions
= completions
.Length
;
649 if (ncompletions
== 0)
652 ShowCompletions (completion
.Prefix
, completion
.Result
);
654 ForceCursor (cursor
);
663 if (current_completion
!= null){
664 InsertTextAtCursor (current_completion
.Current
);
671 void CmdTabOrComplete ()
673 bool complete
= false;
675 if (AutoCompleteEvent
!= null){
676 if (TabAtStartCompletes
)
679 for (int i
= 0; i
< cursor
; i
++){
680 if (!Char
.IsWhiteSpace (text
[i
])){
702 UpdateCursor (text
.Length
);
710 UpdateCursor (cursor
-1);
713 void CmdBackwardWord ()
715 int p
= WordBackward (cursor
);
721 void CmdForwardWord ()
723 int p
= WordForward (cursor
);
731 if (cursor
== text
.Length
)
734 UpdateCursor (cursor
+1);
737 void RenderAfter (int p
)
741 ForceCursor (cursor
);
749 bool completing
= current_completion
!= null;
752 text
.Remove (--cursor
, 1);
754 RenderAfter (cursor
);
756 UpdateCompletionWindow ();
759 void CmdDeleteChar ()
761 // If there is no input, this behaves like EOF
762 if (text
.Length
== 0){
765 Console
.WriteLine ();
769 if (cursor
== text
.Length
)
771 text
.Remove (cursor
, 1);
773 RenderAfter (cursor
);
776 int WordForward (int p
)
778 if (p
>= text
.Length
)
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
]))
787 for (; i
< text
.Length
; i
++){
788 if (!Char
.IsLetterOrDigit (text
[i
]))
792 for (; i
< text
.Length
; i
++){
793 if (!Char
.IsLetterOrDigit (text
[i
]))
802 int WordBackward (int p
)
811 if (Char
.IsPunctuation (text
[i
]) || Char
.IsSymbol (text
[i
]) || Char
.IsWhiteSpace (text
[i
])){
813 if (Char
.IsLetterOrDigit (text
[i
]))
817 if (!Char
.IsLetterOrDigit (text
[i
]))
822 if (!Char
.IsLetterOrDigit (text
[i
]))
834 void CmdDeleteWord ()
836 int pos
= WordForward (cursor
);
841 string k
= text
.ToString (cursor
, pos
-cursor
);
843 if (last_handler
== CmdDeleteWord
)
844 kill_buffer
= kill_buffer
+ k
;
848 text
.Remove (cursor
, pos
-cursor
);
850 RenderAfter (cursor
);
853 void CmdDeleteBackword ()
855 int pos
= WordBackward (cursor
);
859 string k
= text
.ToString (pos
, cursor
-pos
);
861 if (last_handler
== CmdDeleteBackword
)
862 kill_buffer
= k
+ kill_buffer
;
866 text
.Remove (pos
, cursor
-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 ())
884 HistoryUpdateLine ();
886 SetText (history
.Previous ());
889 void CmdHistoryNext ()
891 if (!history
.NextAvailable())
894 history
.Update (text
.ToString ());
895 SetText (history
.Next ());
901 if (current_completion
== null)
904 current_completion
.SelectPrevious ();
909 if (current_completion
== null)
912 current_completion
.SelectNext ();
917 kill_buffer
= text
.ToString (cursor
, text
.Length
-cursor
);
918 text
.Length
= cursor
;
920 RenderAfter (cursor
);
925 InsertTextAtCursor (kill_buffer
);
928 void InsertTextAtCursor (string str
)
930 int prev_lines
= LineCount
;
931 text
.Insert (cursor
, str
);
933 if (prev_lines
!= LineCount
){
934 Console
.SetCursorPosition (0, home_row
);
936 cursor
+= str
.Length
;
937 ForceCursor (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 ()
955 if (cursor
== text
.Length
){
956 // The cursor is at the end of the string
958 p
= text
.ToString ().LastIndexOf (search
);
962 ForceCursor (cursor
);
966 // The cursor is somewhere in the middle of the string
967 int start
= (cursor
== match_at
) ? cursor
- 1 : cursor
;
969 p
= text
.ToString ().LastIndexOf (search
, start
);
973 ForceCursor (cursor
);
979 // Need to search backwards in history
980 HistoryUpdateLine ();
981 string s
= history
.SearchBackward (search
);
989 void CmdReverseSearch ()
993 last_search
= search
;
996 SetSearchPrompt ("");
999 if (last_search
!= "" && last_search
!= null){
1000 search
= last_search
;
1001 SetSearchPrompt (search
);
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
))
1033 ForceCursor (cursor
);
1036 void InterruptEdit (object sender
, ConsoleCancelEventArgs a
)
1038 // Do not abort our program:
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
1053 if (insertedChar
== ' '){
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
--){
1064 if (Char
.IsDigit (c
))
1068 if (Char
.IsLetter (c
) || Char
.IsPunctuation (c
) || Char
.IsSymbol (c
) || Char
.IsControl (c
))
1079 void HandleChar (char c
)
1084 bool completing
= current_completion
!= null;
1088 if (HeuristicAutoComplete (completing
, c
))
1089 UpdateCompletionWindow ();
1098 ConsoleModifiers mod
;
1100 cki
= Console
.ReadKey (true);
1101 if (cki
.Key
== ConsoleKey
.Escape
){
1102 if (current_completion
!= null){
1106 cki
= Console
.ReadKey (true);
1108 mod
= ConsoleModifiers
.Alt
;
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
){
1120 if (handler
.ResetCompletion
)
1122 handler
.KeyHandler ();
1123 last_handler
= handler
.KeyHandler
;
1125 } else if (t
.KeyChar
== cki
.KeyChar
&& t
.Key
== ConsoleKey
.Zoom
){
1127 if (handler
.ResetCompletion
)
1130 handler
.KeyHandler ();
1131 last_handler
= handler
.KeyHandler
;
1136 if (searching
!= 0){
1137 if (last_handler
!= CmdReverseSearch
){
1145 if (cki
.KeyChar
!= (char) 0){
1146 HandleChar (cki
.KeyChar
);
1151 void InitText (string initial
)
1153 text
= new StringBuilder (initial
);
1155 cursor
= text
.Length
;
1157 ForceCursor (cursor
);
1160 void SetText (string newtext
)
1162 Console
.SetCursorPosition (0, home_row
);
1166 void SetPrompt (string newprompt
)
1168 shown_prompt
= newprompt
;
1169 Console
.SetCursorPosition (0, home_row
);
1171 ForceCursor (cursor
);
1174 public string Edit (string prompt
, string initial
)
1176 edit_thread
= Thread
.CurrentThread
;
1178 Console
.CancelKeyPress
+= InterruptEdit
;
1181 history
.CursorToEnd ();
1185 shown_prompt
= prompt
;
1187 history
.Append (initial
);
1192 } catch (ThreadAbortException
){
1194 Thread
.ResetAbort ();
1195 Console
.WriteLine ();
1200 Console
.WriteLine ();
1202 Console
.CancelKeyPress
-= InterruptEdit
;
1209 string result
= text
.ToString ();
1211 history
.Accept (result
);
1213 history
.RemoveLast ();
1218 public void SaveHistory ()
1220 if (history
!= null) {
1225 public bool TabAtStartCompletes { get; set; }
1228 // Emulates the bash-like behavior, where edits done to the
1229 // history are recorded
1237 public History (string app
, int size
)
1240 throw new ArgumentException ("size");
1243 string dir
= Environment
.GetFolderPath (Environment
.SpecialFolder
.ApplicationData
);
1244 //Console.WriteLine (dir);
1245 if (!Directory
.Exists (dir
)){
1247 Directory
.CreateDirectory (dir
);
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
)){
1263 while ((line
= sr
.ReadLine ()) != null){
1271 public void Close ()
1273 if (histfile
== null)
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
]);
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);
1296 head
= (head
+1) % history
.Length
;
1298 tail
= (tail
+1 % history
.Length
);
1299 if (count
!= history
.Length
)
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 ()
1318 head
= history
.Length
-1;
1321 public void Accept (string s
)
1325 t
= history
.Length
-1;
1330 public bool PreviousAvailable ()
1332 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
1335 int next
= cursor
-1;
1345 public bool NextAvailable ()
1349 int next
= (cursor
+ 1) % history
.Length
;
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 ())
1367 cursor
= history
.Length
- 1;
1369 return history
[cursor
];
1372 public string Next ()
1374 if (!NextAvailable ())
1377 cursor
= (cursor
+ 1) % history
.Length
;
1378 return history
[cursor
];
1381 public void CursorToEnd ()
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
]);
1398 public string SearchBackward (string term
)
1400 for (int i
= 0; i
< count
; i
++){
1401 int slot
= cursor
-i
-1;
1403 slot
= history
.Length
+slot
;
1404 if (slot
>= history
.Length
)
1406 if (history
[slot
] != null && history
[slot
].IndexOf (term
) != -1){
1408 return history
[slot
];
1422 LineEditor le
= new LineEditor ("foo") {
1423 HeuristicsMode
= "csharp"
1425 le
.AutoCompleteEvent
+= delegate (string a
, int pos
){
1427 var completions
= new string [] { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" }
;
1428 return new Mono
.Terminal
.LineEditor
.Completion (prefix
, completions
);
1433 while ((s
= le
.Edit ("shell> ", "")) != null){
1434 Console
.WriteLine ("----> [{0}]", s
);