Merge pull request #2720 from mono/fix-39325
[mono.git] / mcs / tools / csharp / getline.cs
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 // Copyright 2016 Xamarin Inc
9 //
10 // Completion wanted:
11 //
12 //   * Enable bash-like completion window the window as an option for non-GUI people?
13 //
14 //   * Continue completing when Backspace is used?
15 //
16 //   * Should we keep the auto-complete on "."?
17 //
18 //   * Completion produces an error if the value is not resolvable, we should hide those errors
19 //
20 // Dual-licensed under the terms of the MIT X11 license or the
21 // Apache License 2.0
22 //
23 // USE -define:DEMO to build this as a standalone file and test it
24 //
25 // TODO:
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.
28 //    Completion support
29 //    Why is Thread.Interrupt not working?   Currently I resort to Abort which is too much.
30 //
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.
36 //
37 // Bug:
38 //   About 8 lines missing, type "Con<TAB>" and not enough lines are inserted at the bottom.
39 // 
40 //
41 using System;
42 using System.Text;
43 using System.IO;
44 using System.Threading;
45 using System.Reflection;
46
47 namespace Mono.Terminal {
48
49         public class LineEditor {
50
51                 public class Completion {
52                         public string [] Result;
53                         public string Prefix;
54
55                         public Completion (string prefix, string [] result)
56                         {
57                                 Prefix = prefix;
58                                 Result = result;
59                         }
60                 }
61                 
62                 public delegate Completion AutoCompleteHandler (string text, int pos);
63
64                 // null does nothing, "csharp" uses some heuristics that make sense for C#
65                 public string HeuristicsMode;
66                 
67                 //static StreamWriter log;
68                 
69                 // The text being edited.
70                 StringBuilder text;
71
72                 // The text as it is rendered (replaces (char)1 with ^A on display for example).
73                 StringBuilder rendered_text;
74
75                 // The prompt specified, and the prompt shown to the user.
76                 string prompt;
77                 string shown_prompt;
78                 
79                 // The current cursor position, indexes into "text", for an index
80                 // into rendered_text, use TextToRenderPos
81                 int cursor;
82
83                 // The row where we started displaying data.
84                 int home_row;
85
86                 // The maximum length that has been displayed on the screen
87                 int max_rendered;
88
89                 // If we are done editing, this breaks the interactive loop
90                 bool done = false;
91
92                 // The thread where the Editing started taking place
93                 Thread edit_thread;
94
95                 // Our object that tracks history
96                 History history;
97
98                 // The contents of the kill buffer (cut/paste in Emacs parlance)
99                 string kill_buffer = "";
100
101                 // The string being searched for
102                 string search;
103                 string last_search;
104
105                 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
106                 int searching;
107
108                 // The position where we found the match.
109                 int match_at;
110                 
111                 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
112                 KeyHandler last_handler;
113
114                 // If we have a popup completion, this is not null and holds the state.
115                 CompletionState current_completion;
116
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;
119
120                 // This contains a raw stream pointing to stdout, used to bypass the TermInfoDriver
121                 static Stream unix_raw_output;
122                 
123                 delegate void KeyHandler ();
124                 
125                 struct Handler {
126                         public ConsoleKeyInfo CKI;
127                         public KeyHandler KeyHandler;
128                         public bool ResetCompletion;
129                         
130                         public Handler (ConsoleKey key, KeyHandler h, bool resetCompletion = true)
131                         {
132                                 CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
133                                 KeyHandler = h;
134                                 ResetCompletion = resetCompletion;
135                         }
136
137                         public Handler (char c, KeyHandler h, bool resetCompletion = true)
138                         {
139                                 KeyHandler = h;
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;
143                         }
144
145                         public Handler (ConsoleKeyInfo cki, KeyHandler h, bool resetCompletion = true)
146                         {
147                                 CKI = cki;
148                                 KeyHandler = h;
149                                 ResetCompletion = resetCompletion;
150                         }
151                         
152                         public static Handler Control (char c, KeyHandler h, bool resetCompletion = true)
153                         {
154                                 return new Handler ((char) (c - 'A' + 1), h, resetCompletion);
155                         }
156
157                         public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
158                         {
159                                 ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false);
160                                 return new Handler (cki, h);
161                         }
162                 }
163
164                 /// <summary>
165                 ///   Invoked when the user requests auto-completion using the tab character
166                 /// </summary>
167                 /// <remarks>
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".
172                 ///
173                 ///    When there are multiple results, the result should be the full
174                 ///    text
175                 /// </remarks>
176                 public AutoCompleteHandler AutoCompleteEvent;
177
178                 static Handler [] handlers;
179
180                 public LineEditor (string name) : this (name, 10) { }
181                 
182                 public LineEditor (string name, int histsize)
183                 {
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),
195                                 
196                                 // Emacs keys
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),
211                                 
212                                 Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord),
213                                 Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword),
214                                 
215                                 // DEBUG
216                                 //Handler.Control ('T', CmdDebug),
217
218                                 // quote
219                                 Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); })
220                         };
221
222                         rendered_text = new StringBuilder ();
223                         text = new StringBuilder ();
224
225                         history = new History (name, histsize);
226
227                         GetUnixConsoleReset ();
228                         //if (File.Exists ("log"))File.Delete ("log");
229                         //log = File.CreateText ("log"); 
230                 }
231
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
236
237                 void GetUnixConsoleReset ()
238                 {
239                         //
240                         // On Unix, we want to be able to reset the color for the pop-up completion
241                         //
242                         int p = (int) Environment.OSVersion.Platform;
243                         var is_unix = (p == 4) || (p == 128);
244                         if (!is_unix)
245                                 return;
246
247                         // Sole purpose of this call is to initialize the Terminfo driver
248                         var x = Console.CursorLeft;
249                         
250                         try {
251                                 var terminfo_driver = Type.GetType ("System.ConsoleDriver")?.GetField ("driver", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue (null);
252                                 if (terminfo_driver == null)
253                                         return;
254
255                                 var unix_reset_colors_str = (terminfo_driver?.GetType ()?.GetField ("origPair", BindingFlags.Instance | BindingFlags.NonPublic))?.GetValue (terminfo_driver) as string;
256                                 
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);
262                         }
263                 }
264                 
265
266                 void CmdDebug ()
267                 {
268                         history.Dump ();
269                         Console.WriteLine ();
270                         Render ();
271                 }
272
273                 void Render ()
274                 {
275                         Console.Write (shown_prompt);
276                         Console.Write (rendered_text);
277
278                         int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered);
279                         
280                         for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++)
281                                 Console.Write (' ');
282                         max_rendered = shown_prompt.Length + rendered_text.Length;
283
284                         // Write one more to ensure that we always wrap around properly if we are at the
285                         // end of a line.
286                         Console.Write (' ');
287
288                         UpdateHomeRow (max);
289                 }
290
291                 void UpdateHomeRow (int screenpos)
292                 {
293                         int lines = 1 + (screenpos / Console.WindowWidth);
294
295                         home_row = Console.CursorTop - (lines - 1);
296                         if (home_row < 0)
297                                 home_row = 0;
298                 }
299                 
300
301                 void RenderFrom (int pos)
302                 {
303                         int rpos = TextToRenderPos (pos);
304                         int i;
305                         
306                         for (i = rpos; i < rendered_text.Length; i++)
307                                 Console.Write (rendered_text [i]);
308
309                         if ((shown_prompt.Length + rendered_text.Length) > max_rendered)
310                                 max_rendered = shown_prompt.Length + rendered_text.Length;
311                         else {
312                                 int max_extra = max_rendered - shown_prompt.Length;
313                                 for (; i < max_extra; i++)
314                                         Console.Write (' ');
315                         }
316                 }
317
318                 void ComputeRendered ()
319                 {
320                         rendered_text.Length = 0;
321
322                         for (int i = 0; i < text.Length; i++){
323                                 int c = (int) text [i];
324                                 if (c < 26){
325                                         if (c == '\t')
326                                                 rendered_text.Append ("    ");
327                                         else {
328                                                 rendered_text.Append ('^');
329                                                 rendered_text.Append ((char) (c + (int) 'A' - 1));
330                                         }
331                                 } else
332                                         rendered_text.Append ((char)c);
333                         }
334                 }
335
336                 int TextToRenderPos (int pos)
337                 {
338                         int p = 0;
339
340                         for (int i = 0; i < pos; i++){
341                                 int c;
342
343                                 c = (int) text [i];
344                                 
345                                 if (c < 26){
346                                         if (c == 9)
347                                                 p += 4;
348                                         else
349                                                 p += 2;
350                                 } else
351                                         p++;
352                         }
353
354                         return p;
355                 }
356
357                 int TextToScreenPos (int pos)
358                 {
359                         return shown_prompt.Length + TextToRenderPos (pos);
360                 }
361                 
362                 string Prompt {
363                         get { return prompt; }
364                         set { prompt = value; }
365                 }
366
367                 int LineCount {
368                         get {
369                                 return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth;
370                         }
371                 }
372                 
373                 void ForceCursor (int newpos)
374                 {
375                         cursor = newpos;
376
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;
380
381                         if (row >= Console.BufferHeight)
382                                 row = Console.BufferHeight-1;
383                         Console.SetCursorPosition (col, row);
384                         
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);
386                         //log.Flush ();
387                 }
388
389                 void UpdateCursor (int newpos)
390                 {
391                         if (cursor == newpos)
392                                 return;
393
394                         ForceCursor (newpos);
395                 }
396
397                 void InsertChar (char c)
398                 {
399                         int prev_lines = LineCount;
400                         text = text.Insert (cursor, c);
401                         ComputeRendered ();
402                         if (prev_lines != LineCount){
403
404                                 Console.SetCursorPosition (0, home_row);
405                                 Render ();
406                                 ForceCursor (++cursor);
407                         } else {
408                                 RenderFrom (cursor);
409                                 ForceCursor (++cursor);
410                                 UpdateHomeRow (TextToScreenPos (cursor));
411                         }
412                 }
413
414                 static void SaveExcursion (Action code)
415                 {
416                         var saved_col = Console.CursorLeft;
417                         var saved_row = Console.CursorTop;
418                         var saved_fore = Console.ForegroundColor;
419                         var saved_back = Console.BackgroundColor;
420                         
421                         code ();
422                         
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);
427                         } else {
428                                 Console.ForegroundColor = saved_fore;
429                                 Console.BackgroundColor = saved_back;
430                         }
431                 }
432                 
433                 class CompletionState {
434                         public string Prefix;
435                         public string [] Completions;
436                         public int Col, Row, Width, Height;
437                         int selected_item, top_item;
438
439                         public CompletionState (int col, int row, int width, int height)
440                         {
441                                 Col = col;
442                                 Row = row;
443                                 Width = width;
444                                 Height = height;
445
446                                 if (Col < 0)
447                                         throw new ArgumentException ("Cannot be less than zero" + Col, "Col");
448                                 if (Row < 0)
449                                         throw new ArgumentException ("Cannot be less than zero", "Row");
450                                 if (Width < 1)
451                                         throw new ArgumentException ("Cannot be less than one", "Width");
452                                 if (Height < 1)
453                                         throw new ArgumentException ("Cannot be less than one", "Height");
454                                 
455                         }
456                         
457                         void DrawSelection ()
458                         {
459                                 for (int r = 0; r < Height; r++){
460                                         int item_idx = top_item + r;
461                                         bool selected = (item_idx == selected_item);
462                                         
463                                         Console.ForegroundColor = selected ? ConsoleColor.Black : ConsoleColor.Gray;
464                                         Console.BackgroundColor = selected ? ConsoleColor.Cyan : ConsoleColor.Blue;
465
466                                         var item = Prefix + Completions [item_idx];
467                                         if (item.Length > Width)
468                                                 item = item.Substring (0, Width);
469
470                                         Console.CursorLeft = Col;
471                                         Console.CursorTop = Row + r;
472                                         Console.Write (item);
473                                         for (int space = item.Length; space <= Width; space++)
474                                                 Console.Write (" ");
475                                 }
476                         }
477
478                         public string Current {
479                                 get {
480                                         return Completions [selected_item];
481                                 }
482                         }
483                         
484                         public void Show ()
485                         {
486                                 SaveExcursion (DrawSelection);
487                         }
488
489                         public void SelectNext ()
490                         {
491                                 if (selected_item+1 < Completions.Length){
492                                         selected_item++;
493                                         if (selected_item - top_item >= Height)
494                                                 top_item++;
495                                         SaveExcursion (DrawSelection);
496                                 }
497                         }
498
499                         public void SelectPrevious ()
500                         {
501                                 if (selected_item > 0){
502                                         selected_item--;
503                                         if (selected_item < top_item)
504                                                 top_item = selected_item;
505                                         SaveExcursion (DrawSelection);
506                                 }
507                         }
508
509                         void Clear ()
510                         {
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++)
515                                                 Console.Write (" ");
516                                 }
517                         }
518                         
519                         public void Remove ()
520                         {
521                                 SaveExcursion (Clear);
522                         }
523                 }
524
525                 void ShowCompletions (string prefix, string [] completions)
526                 {
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);
538                                 }
539                                 Console.CursorTop = target_line;
540                                 Console.CursorLeft = 0;
541                                 Render ();
542                         }
543
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);
550
551                         if (current_completion == null){
552                                 int left = Console.CursorLeft-prefix.Length;
553                                 
554                                 if (left + window_width + 1 >= Console.WindowWidth)
555                                         left = Console.WindowWidth-window_width-1;
556                                 
557                                 current_completion = new CompletionState (left, Console.CursorTop+1, window_width, window_height) {
558                                         Prefix = prefix,
559                                         Completions = completions,
560                                 };
561                         } else {
562                                 current_completion.Prefix = prefix;
563                                 current_completion.Completions = completions;
564                         }
565                         current_completion.Show ();
566                         Console.CursorLeft = 0;
567                 }
568
569                 void HideCompletions ()
570                 {
571                         if (current_completion == null)
572                                 return;
573                         current_completion.Remove ();
574                         current_completion = null;
575                 }
576
577                 //
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.
581                 //
582                 void Complete ()
583                 {
584                         Completion completion = AutoCompleteEvent (text.ToString (), cursor);
585                         string [] completions = completion.Result;
586                         if (completions == null){
587                                 HideCompletions ();
588                                 return;
589                         }
590                                         
591                         int ncompletions = completions.Length;
592                         if (ncompletions == 0){
593                                 HideCompletions ();
594                                 return;
595                         }
596                                         
597                         if (completions.Length == 1){
598                                 InsertTextAtCursor (completions [0]);
599                                 HideCompletions ();
600                         } else {
601                                 int last = -1;
602
603                                 for (int p = 0; p < completions [0].Length; p++){
604                                         char c = completions [0][p];
605
606
607                                         for (int i = 1; i < ncompletions; i++){
608                                                 if (completions [i].Length < p)
609                                                         goto mismatch;
610                                                         
611                                                 if (completions [i][p] != c){
612                                                         goto mismatch;
613                                                 }
614                                         }
615                                         last = p;
616                                 }
617                         mismatch:
618                                 var prefix = completion.Prefix;
619                                 if (last != -1){
620                                         InsertTextAtCursor (completions [0].Substring (0, last+1));
621
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);
626                                 }
627                                 ShowCompletions (prefix, completions);
628                                 Render ();
629                                 ForceCursor (cursor);
630                         }
631                 }
632
633                 //
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
636                 // point
637                 // 
638                 void UpdateCompletionWindow ()
639                 {
640                         if (current_completion != null)
641                                 throw new Exception ("This method should only be called if the window has been hidden");
642                         
643                         Completion completion = AutoCompleteEvent (text.ToString (), cursor);
644                         string [] completions = completion.Result;
645                         if (completions == null)
646                                 return;
647                                         
648                         int ncompletions = completions.Length;
649                         if (ncompletions == 0)
650                                 return;
651                         
652                         ShowCompletions (completion.Prefix, completion.Result);
653                         Render ();
654                         ForceCursor (cursor);
655                 }
656                 
657                 
658                 //
659                 // Commands
660                 //
661                 void CmdDone ()
662                 {
663                         if (current_completion != null){
664                                 InsertTextAtCursor (current_completion.Current);
665                                 HideCompletions ();
666                                 return;
667                         }
668                         done = true;
669                 }
670
671                 void CmdTabOrComplete ()
672                 {
673                         bool complete = false;
674
675                         if (AutoCompleteEvent != null){
676                                 if (TabAtStartCompletes)
677                                         complete = true;
678                                 else {
679                                         for (int i = 0; i < cursor; i++){
680                                                 if (!Char.IsWhiteSpace (text [i])){
681                                                         complete = true;
682                                                         break;
683                                                 }
684                                         }
685                                 }
686
687                                 if (complete)
688                                         Complete ();
689                                 else
690                                         HandleChar ('\t');
691                         } else
692                                 HandleChar ('t');
693                 }
694                 
695                 void CmdHome ()
696                 {
697                         UpdateCursor (0);
698                 }
699
700                 void CmdEnd ()
701                 {
702                         UpdateCursor (text.Length);
703                 }
704                 
705                 void CmdLeft ()
706                 {
707                         if (cursor == 0)
708                                 return;
709
710                         UpdateCursor (cursor-1);
711                 }
712
713                 void CmdBackwardWord ()
714                 {
715                         int p = WordBackward (cursor);
716                         if (p == -1)
717                                 return;
718                         UpdateCursor (p);
719                 }
720
721                 void CmdForwardWord ()
722                 {
723                         int p = WordForward (cursor);
724                         if (p == -1)
725                                 return;
726                         UpdateCursor (p);
727                 }
728
729                 void CmdRight ()
730                 {
731                         if (cursor == text.Length)
732                                 return;
733
734                         UpdateCursor (cursor+1);
735                 }
736
737                 void RenderAfter (int p)
738                 {
739                         ForceCursor (p);
740                         RenderFrom (p);
741                         ForceCursor (cursor);
742                 }
743                 
744                 void CmdBackspace ()
745                 {
746                         if (cursor == 0)
747                                 return;
748
749                         bool completing = current_completion != null;
750                         HideCompletions ();
751                         
752                         text.Remove (--cursor, 1);
753                         ComputeRendered ();
754                         RenderAfter (cursor);
755                         if (completing)
756                                 UpdateCompletionWindow ();
757                 }
758
759                 void CmdDeleteChar ()
760                 {
761                         // If there is no input, this behaves like EOF
762                         if (text.Length == 0){
763                                 done = true;
764                                 text = null;
765                                 Console.WriteLine ();
766                                 return;
767                         }
768                         
769                         if (cursor == text.Length)
770                                 return;
771                         text.Remove (cursor, 1);
772                         ComputeRendered ();
773                         RenderAfter (cursor);
774                 }
775
776                 int WordForward (int p)
777                 {
778                         if (p >= text.Length)
779                                 return -1;
780
781                         int i = p;
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]))
785                                             break;
786                                 }
787                                 for (; i < text.Length; i++){
788                                         if (!Char.IsLetterOrDigit (text [i]))
789                                             break;
790                                 }
791                         } else {
792                                 for (; i < text.Length; i++){
793                                         if (!Char.IsLetterOrDigit (text [i]))
794                                             break;
795                                 }
796                         }
797                         if (i != p)
798                                 return i;
799                         return -1;
800                 }
801
802                 int WordBackward (int p)
803                 {
804                         if (p == 0)
805                                 return -1;
806
807                         int i = p-1;
808                         if (i == 0)
809                                 return 0;
810                         
811                         if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){
812                                 for (; i >= 0; i--){
813                                         if (Char.IsLetterOrDigit (text [i]))
814                                                 break;
815                                 }
816                                 for (; i >= 0; i--){
817                                         if (!Char.IsLetterOrDigit (text[i]))
818                                                 break;
819                                 }
820                         } else {
821                                 for (; i >= 0; i--){
822                                         if (!Char.IsLetterOrDigit (text [i]))
823                                                 break;
824                                 }
825                         }
826                         i++;
827                         
828                         if (i != p)
829                                 return i;
830
831                         return -1;
832                 }
833                 
834                 void CmdDeleteWord ()
835                 {
836                         int pos = WordForward (cursor);
837
838                         if (pos == -1)
839                                 return;
840
841                         string k = text.ToString (cursor, pos-cursor);
842                         
843                         if (last_handler == CmdDeleteWord)
844                                 kill_buffer = kill_buffer + k;
845                         else
846                                 kill_buffer = k;
847                         
848                         text.Remove (cursor, pos-cursor);
849                         ComputeRendered ();
850                         RenderAfter (cursor);
851                 }
852                 
853                 void CmdDeleteBackword ()
854                 {
855                         int pos = WordBackward (cursor);
856                         if (pos == -1)
857                                 return;
858
859                         string k = text.ToString (pos, cursor-pos);
860                         
861                         if (last_handler == CmdDeleteBackword)
862                                 kill_buffer = k + kill_buffer;
863                         else
864                                 kill_buffer = k;
865                         
866                         text.Remove (pos, cursor-pos);
867                         ComputeRendered ();
868                         RenderAfter (pos);
869                 }
870                 
871                 //
872                 // Adds the current line to the history if needed
873                 //
874                 void HistoryUpdateLine ()
875                 {
876                         history.Update (text.ToString ());
877                 }
878                 
879                 void CmdHistoryPrev ()
880                 {
881                         if (!history.PreviousAvailable ())
882                                 return;
883
884                         HistoryUpdateLine ();
885                         
886                         SetText (history.Previous ());
887                 }
888
889                 void CmdHistoryNext ()
890                 {
891                         if (!history.NextAvailable())
892                                 return;
893
894                         history.Update (text.ToString ());
895                         SetText (history.Next ());
896                         
897                 }
898
899                 void CmdUp ()
900                 {
901                         if (current_completion == null)
902                                 CmdHistoryPrev ();
903                         else
904                                 current_completion.SelectPrevious ();
905                 }
906
907                 void CmdDown ()
908                 {
909                         if (current_completion == null)
910                                 CmdHistoryNext ();
911                         else
912                                 current_completion.SelectNext ();
913                 }
914                 
915                 void CmdKillToEOF ()
916                 {
917                         kill_buffer = text.ToString (cursor, text.Length-cursor);
918                         text.Length = cursor;
919                         ComputeRendered ();
920                         RenderAfter (cursor);
921                 }
922
923                 void CmdYank ()
924                 {
925                         InsertTextAtCursor (kill_buffer);
926                 }
927
928                 void InsertTextAtCursor (string str)
929                 {
930                         int prev_lines = LineCount;
931                         text.Insert (cursor, str);
932                         ComputeRendered ();
933                         if (prev_lines != LineCount){
934                                 Console.SetCursorPosition (0, home_row);
935                                 Render ();
936                                 cursor += str.Length;
937                                 ForceCursor (cursor);
938                         } else {
939                                 RenderFrom (cursor);
940                                 cursor += str.Length;
941                                 ForceCursor (cursor);
942                                 UpdateHomeRow (TextToScreenPos (cursor));
943                         }
944                 }
945                 
946                 void SetSearchPrompt (string s)
947                 {
948                         SetPrompt ("(reverse-i-search)`" + s + "': ");
949                 }
950
951                 void ReverseSearch ()
952                 {
953                         int p;
954
955                         if (cursor == text.Length){
956                                 // The cursor is at the end of the string
957                                 
958                                 p = text.ToString ().LastIndexOf (search);
959                                 if (p != -1){
960                                         match_at = p;
961                                         cursor = p;
962                                         ForceCursor (cursor);
963                                         return;
964                                 }
965                         } else {
966                                 // The cursor is somewhere in the middle of the string
967                                 int start = (cursor == match_at) ? cursor - 1 : cursor;
968                                 if (start != -1){
969                                         p = text.ToString ().LastIndexOf (search, start);
970                                         if (p != -1){
971                                                 match_at = p;
972                                                 cursor = p;
973                                                 ForceCursor (cursor);
974                                                 return;
975                                         }
976                                 }
977                         }
978
979                         // Need to search backwards in history
980                         HistoryUpdateLine ();
981                         string s = history.SearchBackward (search);
982                         if (s != null){
983                                 match_at = -1;
984                                 SetText (s);
985                                 ReverseSearch ();
986                         }
987                 }
988                 
989                 void CmdReverseSearch ()
990                 {
991                         if (searching == 0){
992                                 match_at = -1;
993                                 last_search = search;
994                                 searching = -1;
995                                 search = "";
996                                 SetSearchPrompt ("");
997                         } else {
998                                 if (search == ""){
999                                         if (last_search != "" && last_search != null){
1000                                                 search = last_search;
1001                                                 SetSearchPrompt (search);
1002
1003                                                 ReverseSearch ();
1004                                         }
1005                                         return;
1006                                 }
1007                                 ReverseSearch ();
1008                         } 
1009                 }
1010
1011                 void SearchAppend (char c)
1012                 {
1013                         search = search + c;
1014                         SetSearchPrompt (search);
1015
1016                         //
1017                         // If the new typed data still matches the current text, stay here
1018                         //
1019                         if (cursor < text.Length){
1020                                 string r = text.ToString (cursor, text.Length - cursor);
1021                                 if (r.StartsWith (search))
1022                                         return;
1023                         }
1024
1025                         ReverseSearch ();
1026                 }
1027                 
1028                 void CmdRefresh ()
1029                 {
1030                         Console.Clear ();
1031                         max_rendered = 0;
1032                         Render ();
1033                         ForceCursor (cursor);
1034                 }
1035
1036                 void InterruptEdit (object sender, ConsoleCancelEventArgs a)
1037                 {
1038                         // Do not abort our program:
1039                         a.Cancel = true;
1040
1041                         // Interrupt the editor
1042                         edit_thread.Abort();
1043                 }
1044
1045                 //
1046                 // Implements heuristics to show the completion window based on the mode
1047                 //
1048                 bool HeuristicAutoComplete (bool wasCompleting, char insertedChar)
1049                 {
1050                         if (HeuristicsMode == "csharp"){
1051                                 // csharp heuristics
1052                                 if (wasCompleting){
1053                                         if (insertedChar == ' '){
1054                                                 return false;
1055                                         }
1056                                         return true;
1057                                 } 
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--){
1063                                                         char c = text[p];
1064                                                         if (Char.IsDigit (c))
1065                                                                 continue;
1066                                                         if (c == '_')
1067                                                                 return true;
1068                                                         if (Char.IsLetter (c) || Char.IsPunctuation (c) || Char.IsSymbol (c) || Char.IsControl (c))
1069                                                                 return true;
1070                                                 }
1071                                                 return false;
1072                                         }
1073                                         return true;
1074                                 }
1075                         }
1076                         return false;
1077                 }
1078                 
1079                 void HandleChar (char c)
1080                 {
1081                         if (searching != 0)
1082                                 SearchAppend (c);
1083                         else {
1084                                 bool completing = current_completion != null;
1085                                 HideCompletions ();
1086
1087                                 InsertChar (c);
1088                                 if (HeuristicAutoComplete (completing, c))
1089                                         UpdateCompletionWindow ();
1090                         }
1091                 }
1092
1093                 void EditLoop ()
1094                 {
1095                         ConsoleKeyInfo cki;
1096
1097                         while (!done){
1098                                 ConsoleModifiers mod;
1099                                 
1100                                 cki = Console.ReadKey (true);
1101                                 if (cki.Key == ConsoleKey.Escape){
1102                                         if (current_completion != null){
1103                                                 HideCompletions ();
1104                                                 continue;
1105                                         } else {
1106                                                 cki = Console.ReadKey (true);
1107                                                 
1108                                                 mod = ConsoleModifiers.Alt;
1109                                         }
1110                                 } else
1111                                         mod = cki.Modifiers;
1112                                 
1113                                 bool handled = false;
1114
1115                                 foreach (Handler handler in handlers){
1116                                         ConsoleKeyInfo t = handler.CKI;
1117
1118                                         if (t.Key == cki.Key && t.Modifiers == mod){
1119                                                 handled = true;
1120                                                 if (handler.ResetCompletion)
1121                                                         HideCompletions ();
1122                                                 handler.KeyHandler ();
1123                                                 last_handler = handler.KeyHandler;
1124                                                 break;
1125                                         } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
1126                                                 handled = true;
1127                                                 if (handler.ResetCompletion)
1128                                                         HideCompletions ();
1129
1130                                                 handler.KeyHandler ();
1131                                                 last_handler = handler.KeyHandler;
1132                                                 break;
1133                                         }
1134                                 }
1135                                 if (handled){
1136                                         if (searching != 0){
1137                                                 if (last_handler != CmdReverseSearch){
1138                                                         searching = 0;
1139                                                         SetPrompt (prompt);
1140                                                 }
1141                                         }
1142                                         continue;
1143                                 }
1144
1145                                 if (cki.KeyChar != (char) 0){
1146                                         HandleChar (cki.KeyChar);
1147                                 }
1148                         } 
1149                 }
1150
1151                 void InitText (string initial)
1152                 {
1153                         text = new StringBuilder (initial);
1154                         ComputeRendered ();
1155                         cursor = text.Length;
1156                         Render ();
1157                         ForceCursor (cursor);
1158                 }
1159
1160                 void SetText (string newtext)
1161                 {
1162                         Console.SetCursorPosition (0, home_row);
1163                         InitText (newtext);
1164                 }
1165
1166                 void SetPrompt (string newprompt)
1167                 {
1168                         shown_prompt = newprompt;
1169                         Console.SetCursorPosition (0, home_row);
1170                         Render ();
1171                         ForceCursor (cursor);
1172                 }
1173                 
1174                 public string Edit (string prompt, string initial)
1175                 {
1176                         edit_thread = Thread.CurrentThread;
1177                         searching = 0;
1178                         Console.CancelKeyPress += InterruptEdit;
1179                                                 
1180                         done = false;
1181                         history.CursorToEnd ();
1182                         max_rendered = 0;
1183                         
1184                         Prompt = prompt;
1185                         shown_prompt = prompt;
1186                         InitText (initial);
1187                         history.Append (initial);
1188
1189                         do {
1190                                 try {
1191                                         EditLoop ();
1192                                 } catch (ThreadAbortException){
1193                                         searching = 0;
1194                                         Thread.ResetAbort ();
1195                                         Console.WriteLine ();
1196                                         SetPrompt (prompt);
1197                                         SetText ("");
1198                                 }
1199                         } while (!done);
1200                         Console.WriteLine ();
1201                         
1202                         Console.CancelKeyPress -= InterruptEdit;
1203
1204                         if (text == null){
1205                                 history.Close ();
1206                                 return null;
1207                         }
1208
1209                         string result = text.ToString ();
1210                         if (result != "")
1211                                 history.Accept (result);
1212                         else
1213                                 history.RemoveLast ();
1214
1215                         return result;
1216                 }
1217                 
1218                 public void SaveHistory ()
1219                 {
1220                         if (history != null) {
1221                                 history.Close ();
1222                         }
1223                 }
1224
1225                 public bool TabAtStartCompletes { get; set; }
1226                         
1227                 //
1228                 // Emulates the bash-like behavior, where edits done to the
1229                 // history are recorded
1230                 //
1231                 class History {
1232                         string [] history;
1233                         int head, tail;
1234                         int cursor, count;
1235                         string histfile;
1236                         
1237                         public History (string app, int size)
1238                         {
1239                                 if (size < 1)
1240                                         throw new ArgumentException ("size");
1241
1242                                 if (app != null){
1243                                         string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
1244                                         //Console.WriteLine (dir);
1245                                         if (!Directory.Exists (dir)){
1246                                                 try {
1247                                                         Directory.CreateDirectory (dir);
1248                                                 } catch {
1249                                                         app = null;
1250                                                 }
1251                                         }
1252                                         if (app != null)
1253                                                 histfile = Path.Combine (dir, app) + ".history";
1254                                 }
1255                                 
1256                                 history = new string [size];
1257                                 head = tail = cursor = 0;
1258
1259                                 if (File.Exists (histfile)){
1260                                         using (StreamReader sr = File.OpenText (histfile)){
1261                                                 string line;
1262                                                 
1263                                                 while ((line = sr.ReadLine ()) != null){
1264                                                         if (line != "")
1265                                                                 Append (line);
1266                                                 }
1267                                         }
1268                                 }
1269                         }
1270
1271                         public void Close ()
1272                         {
1273                                 if (histfile == null)
1274                                         return;
1275
1276                                 try {
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]);
1282                                                 }
1283                                         }
1284                                 } catch {
1285                                         // ignore
1286                                 }
1287                         }
1288                         
1289                         //
1290                         // Appends a value to the history
1291                         //
1292                         public void Append (string s)
1293                         {
1294                                 //Console.WriteLine ("APPENDING {0} head={1} tail={2}", s, head, tail);
1295                                 history [head] = s;
1296                                 head = (head+1) % history.Length;
1297                                 if (head == tail)
1298                                         tail = (tail+1 % history.Length);
1299                                 if (count != history.Length)
1300                                         count++;
1301                                 //Console.WriteLine ("DONE: head={1} tail={2}", s, head, tail);
1302                         }
1303
1304                         //
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.
1308                         //
1309                         public void Update (string s)
1310                         {
1311                                 history [cursor] = s;
1312                         }
1313
1314                         public void RemoveLast ()
1315                         {
1316                                 head = head-1;
1317                                 if (head < 0)
1318                                         head = history.Length-1;
1319                         }
1320                         
1321                         public void Accept (string s)
1322                         {
1323                                 int t = head-1;
1324                                 if (t < 0)
1325                                         t = history.Length-1;
1326                                 
1327                                 history [t] = s;
1328                         }
1329                         
1330                         public bool PreviousAvailable ()
1331                         {
1332                                 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
1333                                 if (count == 0)
1334                                         return false;
1335                                 int next = cursor-1;
1336                                 if (next < 0)
1337                                         next = count-1;
1338
1339                                 if (next == head)
1340                                         return false;
1341
1342                                 return true;
1343                         }
1344
1345                         public bool NextAvailable ()
1346                         {
1347                                 if (count == 0)
1348                                         return false;
1349                                 int next = (cursor + 1) % history.Length;
1350                                 if (next == head)
1351                                         return false;
1352                                 return true;
1353                         }
1354                         
1355                         
1356                         //
1357                         // Returns: a string with the previous line contents, or
1358                         // nul if there is no data in the history to move to.
1359                         //
1360                         public string Previous ()
1361                         {
1362                                 if (!PreviousAvailable ())
1363                                         return null;
1364
1365                                 cursor--;
1366                                 if (cursor < 0)
1367                                         cursor = history.Length - 1;
1368
1369                                 return history [cursor];
1370                         }
1371
1372                         public string Next ()
1373                         {
1374                                 if (!NextAvailable ())
1375                                         return null;
1376
1377                                 cursor = (cursor + 1) % history.Length;
1378                                 return history [cursor];
1379                         }
1380
1381                         public void CursorToEnd ()
1382                         {
1383                                 if (head == tail)
1384                                         return;
1385
1386                                 cursor = head;
1387                         }
1388
1389                         public void Dump ()
1390                         {
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]);
1394                                 }
1395                                 //log.Flush ();
1396                         }
1397
1398                         public string SearchBackward (string term)
1399                         {
1400                                 for (int i = 0; i < count; i++){
1401                                         int slot = cursor-i-1;
1402                                         if (slot < 0)
1403                                                 slot = history.Length+slot;
1404                                         if (slot >= history.Length)
1405                                                 slot = 0;
1406                                         if (history [slot] != null && history [slot].IndexOf (term) != -1){
1407                                                 cursor = slot;
1408                                                 return history [slot];
1409                                         }
1410                                 }
1411
1412                                 return null;
1413                         }
1414                         
1415                 }
1416         }
1417
1418 #if DEMO
1419         class Demo {
1420                 static void Main ()
1421                 {
1422                         LineEditor le = new LineEditor ("foo") {
1423                                 HeuristicsMode = "csharp"
1424                         };
1425                         le.AutoCompleteEvent += delegate (string a, int pos){
1426                                 string prefix = "";
1427                                 var completions = new string [] { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" };
1428                                 return new Mono.Terminal.LineEditor.Completion (prefix, completions);
1429                         };
1430                         
1431                         string s;
1432                         
1433                         while ((s = le.Edit ("shell> ", "")) != null){
1434                                 Console.WriteLine ("----> [{0}]", s);
1435                         }
1436                 }
1437         }
1438 #endif
1439 }