X-Git-Url: http://wien.tomnetworks.com/gitweb/?a=blobdiff_plain;f=mcs%2Ftools%2Fcsharp%2Fgetline.cs;h=b60971e0df34eabe9d3954b6843f0f875063b41d;hb=ff49850dfc18f5991246a203184fa1e0b8a7c7ab;hp=b6dcf0759b66fdca59df0a2da54b94235f669be6;hpb=811674bc6331c98d33134e2a37a7c7dd66402227;p=mono.git diff --git a/mcs/tools/csharp/getline.cs b/mcs/tools/csharp/getline.cs index b6dcf0759b6..b60971e0df3 100644 --- a/mcs/tools/csharp/getline.cs +++ b/mcs/tools/csharp/getline.cs @@ -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 @@ -23,7 +34,10 @@ // 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" 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 /// 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){