Merge pull request #2698 from esdrubal/iosxmlarray
[mono.git] / mcs / tools / csharp / getline.cs
index b6dcf0759b66fdca59df0a2da54b94235f669be6..b60971e0df34eabe9d3954b6843f0f875063b41d 100644 (file)
@@ -5,6 +5,17 @@
 //   Miguel de Icaza (miguel@novell.com)
 //
 // Copyright 2008 Novell, Inc.
+// Copyright 2016 Xamarin Inc
+//
+// Completion wanted:
+//
+//   * Enable bash-like completion window the window as an option for non-GUI people?
+//
+//   * Continue completing when Backspace is used?
+//
+//   * Should we keep the auto-complete on "."?
+//
+//   * Completion produces an error if the value is not resolvable, we should hide those errors
 //
 // Dual-licensed under the terms of the MIT X11 license or the
 // Apache License 2.0
 //    behind its back (P/Invoke puts for example).
 //    System.Console needs to get the DELETE character, and report accordingly.
 //
-
+// Bug:
+//   About 8 lines missing, type "Con<TAB>" and not enough lines are inserted at the bottom.
+// 
+//
 using System;
 using System.Text;
 using System.IO;
@@ -46,6 +60,9 @@ namespace Mono.Terminal {
                }
                
                public delegate Completion AutoCompleteHandler (string text, int pos);
+
+               // null does nothing, "csharp" uses some heuristics that make sense for C#
+               public string HeuristicsMode;
                
                //static StreamWriter log;
                
@@ -93,35 +110,48 @@ namespace Mono.Terminal {
                
                // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
                KeyHandler last_handler;
+
+               // If we have a popup completion, this is not null and holds the state.
+               CompletionState current_completion;
+
+               // If this is set, it contains an escape sequence to reset the Unix colors to the ones that were used on startup
+               static byte [] unix_reset_colors;
+
+               // This contains a raw stream pointing to stdout, used to bypass the TermInfoDriver
+               static Stream unix_raw_output;
                
                delegate void KeyHandler ();
                
                struct Handler {
                        public ConsoleKeyInfo CKI;
                        public KeyHandler KeyHandler;
-
-                       public Handler (ConsoleKey key, KeyHandler h)
+                       public bool ResetCompletion;
+                       
+                       public Handler (ConsoleKey key, KeyHandler h, bool resetCompletion = true)
                        {
                                CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
                                KeyHandler = h;
+                               ResetCompletion = resetCompletion;
                        }
 
-                       public Handler (char c, KeyHandler h)
+                       public Handler (char c, KeyHandler h, bool resetCompletion = true)
                        {
                                KeyHandler = h;
                                // Use the "Zoom" as a flag that we only have a character.
                                CKI = new ConsoleKeyInfo (c, ConsoleKey.Zoom, false, false, false);
+                               ResetCompletion = resetCompletion;
                        }
 
-                       public Handler (ConsoleKeyInfo cki, KeyHandler h)
+                       public Handler (ConsoleKeyInfo cki, KeyHandler h, bool resetCompletion = true)
                        {
                                CKI = cki;
                                KeyHandler = h;
+                               ResetCompletion = resetCompletion;
                        }
                        
-                       public static Handler Control (char c, KeyHandler h)
+                       public static Handler Control (char c, KeyHandler h, bool resetCompletion = true)
                        {
-                               return new Handler ((char) (c - 'A' + 1), h);
+                               return new Handler ((char) (c - 'A' + 1), h, resetCompletion);
                        }
 
                        public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
@@ -144,7 +174,7 @@ namespace Mono.Terminal {
                ///    text
                /// </remarks>
                public AutoCompleteHandler AutoCompleteEvent;
-               
+
                static Handler [] handlers;
 
                public LineEditor (string name) : this (name, 10) { }
@@ -156,20 +186,20 @@ namespace Mono.Terminal {
                                new Handler (ConsoleKey.End,        CmdEnd),
                                new Handler (ConsoleKey.LeftArrow,  CmdLeft),
                                new Handler (ConsoleKey.RightArrow, CmdRight),
-                               new Handler (ConsoleKey.UpArrow,    CmdHistoryPrev),
-                               new Handler (ConsoleKey.DownArrow,  CmdHistoryNext),
-                               new Handler (ConsoleKey.Enter,      CmdDone),
-                               new Handler (ConsoleKey.Backspace,  CmdBackspace),
+                               new Handler (ConsoleKey.UpArrow,    CmdUp, resetCompletion: false),
+                               new Handler (ConsoleKey.DownArrow,  CmdDown, resetCompletion: false),
+                               new Handler (ConsoleKey.Enter,      CmdDone, resetCompletion: false),
+                               new Handler (ConsoleKey.Backspace,  CmdBackspace, resetCompletion: false),
                                new Handler (ConsoleKey.Delete,     CmdDeleteChar),
-                               new Handler (ConsoleKey.Tab,        CmdTabOrComplete),
+                               new Handler (ConsoleKey.Tab,        CmdTabOrComplete, resetCompletion: false),
                                
                                // Emacs keys
                                Handler.Control ('A', CmdHome),
                                Handler.Control ('E', CmdEnd),
                                Handler.Control ('B', CmdLeft),
                                Handler.Control ('F', CmdRight),
-                               Handler.Control ('P', CmdHistoryPrev),
-                               Handler.Control ('N', CmdHistoryNext),
+                               Handler.Control ('P', CmdUp, resetCompletion: false),
+                               Handler.Control ('N', CmdDown, resetCompletion: false),
                                Handler.Control ('K', CmdKillToEOF),
                                Handler.Control ('Y', CmdYank),
                                Handler.Control ('D', CmdDeleteChar),
@@ -193,11 +223,46 @@ namespace Mono.Terminal {
                        text = new StringBuilder ();
 
                        history = new History (name, histsize);
-                       
+
+                       GetUnixConsoleReset ();
                        //if (File.Exists ("log"))File.Delete ("log");
                        //log = File.CreateText ("log"); 
                }
 
+               // On Unix, there is a "default" color which is not represented by any colors in
+               // ConsoleColor and it is not possible to set is by setting the ForegroundColor or
+               // BackgroundColor properties, so we have to use the terminfo driver in Mono to
+               // fetch these values
+
+               void GetUnixConsoleReset ()
+               {
+                       //
+                       // On Unix, we want to be able to reset the color for the pop-up completion
+                       //
+                       int p = (int) Environment.OSVersion.Platform;
+                       var is_unix = (p == 4) || (p == 128);
+                       if (!is_unix)
+                               return;
+
+                       // Sole purpose of this call is to initialize the Terminfo driver
+                       var x = Console.CursorLeft;
+                       
+                       try {
+                               var terminfo_driver = Type.GetType ("System.ConsoleDriver")?.GetField ("driver", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue (null);
+                               if (terminfo_driver == null)
+                                       return;
+
+                               var unix_reset_colors_str = (terminfo_driver?.GetType ()?.GetField ("origPair", BindingFlags.Instance | BindingFlags.NonPublic))?.GetValue (terminfo_driver) as string;
+                               
+                               if (unix_reset_colors_str != null)
+                                       unix_reset_colors = Encoding.UTF8.GetBytes ((string)unix_reset_colors_str);
+                               unix_raw_output = Console.OpenStandardOutput ();
+                       } catch (Exception e){
+                               Console.WriteLine ("Error: " + e);
+                       }
+               }
+               
+
                void CmdDebug ()
                {
                        history.Dump ();
@@ -346,11 +411,260 @@ namespace Mono.Terminal {
                        }
                }
 
+               static void SaveExcursion (Action code)
+               {
+                       var saved_col = Console.CursorLeft;
+                       var saved_row = Console.CursorTop;
+                       var saved_fore = Console.ForegroundColor;
+                       var saved_back = Console.BackgroundColor;
+                       
+                       code ();
+                       
+                       Console.CursorLeft = saved_col;
+                       Console.CursorTop = saved_row;
+                       if (unix_reset_colors != null){
+                               unix_raw_output.Write (unix_reset_colors, 0, unix_reset_colors.Length);
+                       } else {
+                               Console.ForegroundColor = saved_fore;
+                               Console.BackgroundColor = saved_back;
+                       }
+               }
+               
+               class CompletionState {
+                       public string Prefix;
+                       public string [] Completions;
+                       public int Col, Row, Width, Height;
+                       int selected_item, top_item;
+
+                       public CompletionState (int col, int row, int width, int height)
+                       {
+                               Col = col;
+                               Row = row;
+                               Width = width;
+                               Height = height;
+
+                               if (Col < 0)
+                                       throw new ArgumentException ("Cannot be less than zero" + Col, "Col");
+                               if (Row < 0)
+                                       throw new ArgumentException ("Cannot be less than zero", "Row");
+                               if (Width < 1)
+                                       throw new ArgumentException ("Cannot be less than one", "Width");
+                               if (Height < 1)
+                                       throw new ArgumentException ("Cannot be less than one", "Height");
+                               
+                       }
+                       
+                       void DrawSelection ()
+                       {
+                               for (int r = 0; r < Height; r++){
+                                       int item_idx = top_item + r;
+                                       bool selected = (item_idx == selected_item);
+                                       
+                                       Console.ForegroundColor = selected ? ConsoleColor.Black : ConsoleColor.Gray;
+                                       Console.BackgroundColor = selected ? ConsoleColor.Cyan : ConsoleColor.Blue;
+
+                                       var item = Prefix + Completions [item_idx];
+                                       if (item.Length > Width)
+                                               item = item.Substring (0, Width);
+
+                                       Console.CursorLeft = Col;
+                                       Console.CursorTop = Row + r;
+                                       Console.Write (item);
+                                       for (int space = item.Length; space <= Width; space++)
+                                               Console.Write (" ");
+                               }
+                       }
+
+                       public string Current {
+                               get {
+                                       return Completions [selected_item];
+                               }
+                       }
+                       
+                       public void Show ()
+                       {
+                               SaveExcursion (DrawSelection);
+                       }
+
+                       public void SelectNext ()
+                       {
+                               if (selected_item+1 < Completions.Length){
+                                       selected_item++;
+                                       if (selected_item - top_item >= Height)
+                                               top_item++;
+                                       SaveExcursion (DrawSelection);
+                               }
+                       }
+
+                       public void SelectPrevious ()
+                       {
+                               if (selected_item > 0){
+                                       selected_item--;
+                                       if (selected_item < top_item)
+                                               top_item = selected_item;
+                                       SaveExcursion (DrawSelection);
+                               }
+                       }
+
+                       void Clear ()
+                       {
+                               for (int r = 0; r < Height; r++){
+                                       Console.CursorLeft = Col;
+                                       Console.CursorTop = Row + r;
+                                       for (int space = 0; space <= Width; space++)
+                                               Console.Write (" ");
+                               }
+                       }
+                       
+                       public void Remove ()
+                       {
+                               SaveExcursion (Clear);
+                       }
+               }
+
+               void ShowCompletions (string prefix, string [] completions)
+               {
+                       // Ensure we have space, determine window size
+                       int window_height = System.Math.Min (completions.Length, Console.WindowHeight/5);
+                       int target_line = Console.WindowHeight-window_height-1;
+                       if (Console.CursorTop > target_line){
+                               var saved_left = Console.CursorLeft;
+                               var delta = Console.CursorTop-target_line;
+                               Console.CursorLeft = 0;
+                               Console.CursorTop = Console.WindowHeight-1;
+                               for (int i = 0; i < delta+1; i++){
+                                       for (int c = Console.WindowWidth; c > 0; c--)
+                                               Console.Write (" "); // To debug use ("{0}", i%10);
+                               }
+                               Console.CursorTop = target_line;
+                               Console.CursorLeft = 0;
+                               Render ();
+                       }
+
+                       const int MaxWidth = 50;
+                       int window_width = 12;
+                       int plen = prefix.Length;
+                       foreach (var s in completions)
+                               window_width = System.Math.Max (plen + s.Length, window_width);
+                       window_width = System.Math.Min (window_width, MaxWidth);
+
+                       if (current_completion == null){
+                               int left = Console.CursorLeft-prefix.Length;
+                               
+                               if (left + window_width + 1 >= Console.WindowWidth)
+                                       left = Console.WindowWidth-window_width-1;
+                               
+                               current_completion = new CompletionState (left, Console.CursorTop+1, window_width, window_height) {
+                                       Prefix = prefix,
+                                       Completions = completions,
+                               };
+                       } else {
+                               current_completion.Prefix = prefix;
+                               current_completion.Completions = completions;
+                       }
+                       current_completion.Show ();
+                       Console.CursorLeft = 0;
+               }
+
+               void HideCompletions ()
+               {
+                       if (current_completion == null)
+                               return;
+                       current_completion.Remove ();
+                       current_completion = null;
+               }
+
+               //
+               // Triggers the completion engine, if insertBestMatch is true, then this will
+               // insert the best match found, this behaves like the shell "tab" which will
+               // complete as much as possible given the options.
+               //
+               void Complete ()
+               {
+                       Completion completion = AutoCompleteEvent (text.ToString (), cursor);
+                       string [] completions = completion.Result;
+                       if (completions == null){
+                               HideCompletions ();
+                               return;
+                       }
+                                       
+                       int ncompletions = completions.Length;
+                       if (ncompletions == 0){
+                               HideCompletions ();
+                               return;
+                       }
+                                       
+                       if (completions.Length == 1){
+                               InsertTextAtCursor (completions [0]);
+                               HideCompletions ();
+                       } else {
+                               int last = -1;
+
+                               for (int p = 0; p < completions [0].Length; p++){
+                                       char c = completions [0][p];
+
+
+                                       for (int i = 1; i < ncompletions; i++){
+                                               if (completions [i].Length < p)
+                                                       goto mismatch;
+                                                       
+                                               if (completions [i][p] != c){
+                                                       goto mismatch;
+                                               }
+                                       }
+                                       last = p;
+                               }
+                       mismatch:
+                               var prefix = completion.Prefix;
+                               if (last != -1){
+                                       InsertTextAtCursor (completions [0].Substring (0, last+1));
+
+                                       // Adjust the completions to skip the common prefix
+                                       prefix += completions [0].Substring (0, last+1);
+                                       for (int i = 0; i < completions.Length; i++)
+                                               completions [i] = completions [i].Substring (last+1);
+                               }
+                               ShowCompletions (prefix, completions);
+                               Render ();
+                               ForceCursor (cursor);
+                       }
+               }
+
+               //
+               // When the user has triggered a completion window, this will try to update
+               // the contents of it.   The completion window is assumed to be hidden at this
+               // point
+               // 
+               void UpdateCompletionWindow ()
+               {
+                       if (current_completion != null)
+                               throw new Exception ("This method should only be called if the window has been hidden");
+                       
+                       Completion completion = AutoCompleteEvent (text.ToString (), cursor);
+                       string [] completions = completion.Result;
+                       if (completions == null)
+                               return;
+                                       
+                       int ncompletions = completions.Length;
+                       if (ncompletions == 0)
+                               return;
+                       
+                       ShowCompletions (completion.Prefix, completion.Result);
+                       Render ();
+                       ForceCursor (cursor);
+               }
+               
+               
                //
                // Commands
                //
                void CmdDone ()
                {
+                       if (current_completion != null){
+                               InsertTextAtCursor (current_completion.Current);
+                               HideCompletions ();
+                               return;
+                       }
                        done = true;
                }
 
@@ -370,50 +684,9 @@ namespace Mono.Terminal {
                                        }
                                }
 
-                               if (complete){
-                                       Completion completion = AutoCompleteEvent (text.ToString (), cursor);
-                                       string [] completions = completion.Result;
-                                       if (completions == null)
-                                               return;
-                                       
-                                       int ncompletions = completions.Length;
-                                       if (ncompletions == 0)
-                                               return;
-                                       
-                                       if (completions.Length == 1){
-                                               InsertTextAtCursor (completions [0]);
-                                       } else {
-                                               int last = -1;
-                                               
-                                               for (int p = 0; p < completions [0].Length; p++){
-                                                       char c = completions [0][p];
-
-
-                                                       for (int i = 1; i < ncompletions; i++){
-                                                               if (completions [i].Length < p)
-                                                                       goto mismatch;
-                                                       
-                                                               if (completions [i][p] != c){
-                                                                       goto mismatch;
-                                                               }
-                                                       }
-                                                       last = p;
-                                               }
-                                       mismatch:
-                                               if (last != -1){
-                                                       InsertTextAtCursor (completions [0].Substring (0, last+1));
-                                               }
-                                               Console.WriteLine ();
-                                               foreach (string s in completions){
-                                                       Console.Write (completion.Prefix);
-                                                       Console.Write (s);
-                                                       Console.Write (' ');
-                                               }
-                                               Console.WriteLine ();
-                                               Render ();
-                                               ForceCursor (cursor);
-                                       }
-                               } else
+                               if (complete)
+                                       Complete ();
+                               else
                                        HandleChar ('\t');
                        } else
                                HandleChar ('t');
@@ -473,9 +746,14 @@ namespace Mono.Terminal {
                        if (cursor == 0)
                                return;
 
+                       bool completing = current_completion != null;
+                       HideCompletions ();
+                       
                        text.Remove (--cursor, 1);
                        ComputeRendered ();
                        RenderAfter (cursor);
+                       if (completing)
+                               UpdateCompletionWindow ();
                }
 
                void CmdDeleteChar ()
@@ -618,6 +896,22 @@ namespace Mono.Terminal {
                        
                }
 
+               void CmdUp ()
+               {
+                       if (current_completion == null)
+                               CmdHistoryPrev ();
+                       else
+                               current_completion.SelectPrevious ();
+               }
+
+               void CmdDown ()
+               {
+                       if (current_completion == null)
+                               CmdHistoryNext ();
+                       else
+                               current_completion.SelectNext ();
+               }
+               
                void CmdKillToEOF ()
                {
                        kill_buffer = text.ToString (cursor, text.Length-cursor);
@@ -748,12 +1042,52 @@ namespace Mono.Terminal {
                        edit_thread.Abort();
                }
 
+               //
+               // Implements heuristics to show the completion window based on the mode
+               //
+               bool HeuristicAutoComplete (bool wasCompleting, char insertedChar)
+               {
+                       if (HeuristicsMode == "csharp"){
+                               // csharp heuristics
+                               if (wasCompleting){
+                                       if (insertedChar == ' '){
+                                               return false;
+                                       }
+                                       return true;
+                               } 
+                               // If we were not completing, determine if we want to now
+                               if (insertedChar == '.'){
+                                       // Avoid completing for numbers "1.2" for example
+                                       if (cursor > 1 && Char.IsDigit (text[cursor-2])){
+                                               for (int p = cursor-3; p >= 0; p--){
+                                                       char c = text[p];
+                                                       if (Char.IsDigit (c))
+                                                               continue;
+                                                       if (c == '_')
+                                                               return true;
+                                                       if (Char.IsLetter (c) || Char.IsPunctuation (c) || Char.IsSymbol (c) || Char.IsControl (c))
+                                                               return true;
+                                               }
+                                               return false;
+                                       }
+                                       return true;
+                               }
+                       }
+                       return false;
+               }
+               
                void HandleChar (char c)
                {
                        if (searching != 0)
                                SearchAppend (c);
-                       else
+                       else {
+                               bool completing = current_completion != null;
+                               HideCompletions ();
+
                                InsertChar (c);
+                               if (HeuristicAutoComplete (completing, c))
+                                       UpdateCompletionWindow ();
+                       }
                }
 
                void EditLoop ()
@@ -765,9 +1099,14 @@ namespace Mono.Terminal {
                                
                                cki = Console.ReadKey (true);
                                if (cki.Key == ConsoleKey.Escape){
-                                       cki = Console.ReadKey (true);
-
-                                       mod = ConsoleModifiers.Alt;
+                                       if (current_completion != null){
+                                               HideCompletions ();
+                                               continue;
+                                       } else {
+                                               cki = Console.ReadKey (true);
+                                               
+                                               mod = ConsoleModifiers.Alt;
+                                       }
                                } else
                                        mod = cki.Modifiers;
                                
@@ -778,11 +1117,16 @@ namespace Mono.Terminal {
 
                                        if (t.Key == cki.Key && t.Modifiers == mod){
                                                handled = true;
+                                               if (handler.ResetCompletion)
+                                                       HideCompletions ();
                                                handler.KeyHandler ();
                                                last_handler = handler.KeyHandler;
                                                break;
                                        } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
                                                handled = true;
+                                               if (handler.ResetCompletion)
+                                                       HideCompletions ();
+
                                                handler.KeyHandler ();
                                                last_handler = handler.KeyHandler;
                                                break;
@@ -798,8 +1142,9 @@ namespace Mono.Terminal {
                                        continue;
                                }
 
-                               if (cki.KeyChar != (char) 0)
+                               if (cki.KeyChar != (char) 0){
                                        HandleChar (cki.KeyChar);
+                               }
                        } 
                }
 
@@ -831,7 +1176,7 @@ namespace Mono.Terminal {
                        edit_thread = Thread.CurrentThread;
                        searching = 0;
                        Console.CancelKeyPress += InterruptEdit;
-                       
+                                               
                        done = false;
                        history.CursorToEnd ();
                        max_rendered = 0;
@@ -1074,7 +1419,15 @@ namespace Mono.Terminal {
        class Demo {
                static void Main ()
                {
-                       LineEditor le = new LineEditor ("foo");
+                       LineEditor le = new LineEditor ("foo") {
+                               HeuristicsMode = "csharp"
+                       };
+                       le.AutoCompleteEvent += delegate (string a, int pos){
+                               string prefix = "";
+                               var completions = new string [] { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" };
+                               return new Mono.Terminal.LineEditor.Completion (prefix, completions);
+                       };
+                       
                        string s;
                        
                        while ((s = le.Edit ("shell> ", "")) != null){