// 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;
}
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;
// 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)
/// text
/// </remarks>
public AutoCompleteHandler AutoCompleteEvent;
-
+
static Handler [] handlers;
public LineEditor (string name) : this (name, 10) { }
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),
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 ();
}
}
+ 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;
}
}
}
- 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');
if (cursor == 0)
return;
+ bool completing = current_completion != null;
+ HideCompletions ();
+
text.Remove (--cursor, 1);
ComputeRendered ();
RenderAfter (cursor);
+ if (completing)
+ UpdateCompletionWindow ();
}
void CmdDeleteChar ()
}
+ 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);
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 ()
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;
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;
continue;
}
- if (cki.KeyChar != (char) 0)
+ if (cki.KeyChar != (char) 0){
HandleChar (cki.KeyChar);
+ }
}
}
edit_thread = Thread.CurrentThread;
searching = 0;
Console.CancelKeyPress += InterruptEdit;
-
+
done = false;
history.CursorToEnd ();
max_rendered = 0;
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){