From d1f4e691918024b81363d0b4f25989105e276781 Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Wed, 1 Feb 2017 18:08:40 +0200 Subject: [PATCH] Add cmd --- vr21/Program.cs | 27 +- vr21/getline.cs | 1439 ++++++++++++++++++++++++++++++++++++++++++++++ vr21/vr21.csproj | 1 + 3 files changed, 1465 insertions(+), 2 deletions(-) create mode 100644 vr21/getline.cs diff --git a/vr21/Program.cs b/vr21/Program.cs index fad34a7..7b0b8f2 100644 --- a/vr21/Program.cs +++ b/vr21/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using System.Xml.Serialization; @@ -8,11 +9,14 @@ namespace vr21 public class Receipe { public string Name; + public string Description; public List Ingredients; + public List Steps; public Receipe() { Ingredients = new List(); + Steps = new List(); } } @@ -31,13 +35,32 @@ namespace vr21 Receipe r = new Receipe() {Name="Supp"}; r.Ingredients.Add(kartul); r.Ingredients.Add(kartul); + r.Steps.Add("Koori"); + r.Steps.Add("Keeda"); List l = new List(); l.Add(r); l.Add(r); XmlSerializer x = new XmlSerializer(l.GetType()); - x.Serialize(Console.Out, l); + // x.Serialize(Console.Out, l); //writeReceipesInformation(); - Console.ReadKey(); + + LineEditor le = new LineEditor("foo") {HeuristicsMode = "csharp"}; + string s; + + while ((s = le.Edit("shell> ", "")) != null) + { + var arguments = s.Split(' '); + var command = arguments[0].Trim(); + switch (command.ToLower()) + { + case "list": + Console.WriteLine("Tere"); + break; + default: + Console.WriteLine("No command"); + break; + } + } } static void writeReceipesInformation() diff --git a/vr21/getline.cs b/vr21/getline.cs new file mode 100644 index 0000000..4a7f3b5 --- /dev/null +++ b/vr21/getline.cs @@ -0,0 +1,1439 @@ +// +// getline.cs: A command line editor +// +// Authors: +// 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 +// +// USE -define:DEMO to build this as a standalone file and test it +// +// TODO: +// Enter an error (a = 1); Notice how the prompt is in the wrong line +// This is caused by Stderr not being tracked by System.Console. +// Completion support +// Why is Thread.Interrupt not working? Currently I resort to Abort which is too much. +// +// Limitations in System.Console: +// Console needs SIGWINCH support of some sort +// Console needs a way of updating its position after things have been written +// 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; +using System.Threading; +using System.Reflection; + +namespace vr21 { + + public class LineEditor { + + public class Completion { + public string [] Result; + public string Prefix; + + public Completion (string prefix, string [] result) + { + Prefix = prefix; + Result = result; + } + } + + 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; + + // The text being edited. + StringBuilder text; + + // The text as it is rendered (replaces (char)1 with ^A on display for example). + StringBuilder rendered_text; + + // The prompt specified, and the prompt shown to the user. + string prompt; + string shown_prompt; + + // The current cursor position, indexes into "text", for an index + // into rendered_text, use TextToRenderPos + int cursor; + + // The row where we started displaying data. + int home_row; + + // The maximum length that has been displayed on the screen + int max_rendered; + + // If we are done editing, this breaks the interactive loop + bool done = false; + + // The thread where the Editing started taking place + Thread edit_thread; + + // Our object that tracks history + History history; + + // The contents of the kill buffer (cut/paste in Emacs parlance) + string kill_buffer = ""; + + // The string being searched for + string search; + string last_search; + + // whether we are searching (-1= reverse; 0 = no; 1 = forward) + int searching; + + // The position where we found the match. + int match_at; + + // 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 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, 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, bool resetCompletion = true) + { + CKI = cki; + KeyHandler = h; + ResetCompletion = resetCompletion; + } + + public static Handler Control (char c, KeyHandler h, bool resetCompletion = true) + { + return new Handler ((char) (c - 'A' + 1), h, resetCompletion); + } + + public static Handler Alt (char c, ConsoleKey k, KeyHandler h) + { + ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false); + return new Handler (cki, h); + } + } + + /// + /// Invoked when the user requests auto-completion using the tab character + /// + /// + /// The result is null for no values found, an array with a single + /// string, in that case the string should be the text to be inserted + /// for example if the word at pos is "T", the result for a completion + /// of "ToString" should be "oString", not "ToString". + /// + /// When there are multiple results, the result should be the full + /// text + /// + public AutoCompleteHandler AutoCompleteEvent; + + static Handler [] handlers; + + public LineEditor (string name) : this (name, 10) { } + + public LineEditor (string name, int histsize) + { + handlers = new Handler [] { + new Handler (ConsoleKey.Home, CmdHome), + new Handler (ConsoleKey.End, CmdEnd), + new Handler (ConsoleKey.LeftArrow, CmdLeft), + new Handler (ConsoleKey.RightArrow, CmdRight), + 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, resetCompletion: false), + + // Emacs keys + Handler.Control ('A', CmdHome), + Handler.Control ('E', CmdEnd), + Handler.Control ('B', CmdLeft), + Handler.Control ('F', CmdRight), + Handler.Control ('P', CmdUp, resetCompletion: false), + Handler.Control ('N', CmdDown, resetCompletion: false), + Handler.Control ('K', CmdKillToEOF), + Handler.Control ('Y', CmdYank), + Handler.Control ('D', CmdDeleteChar), + Handler.Control ('L', CmdRefresh), + Handler.Control ('R', CmdReverseSearch), + Handler.Control ('G', delegate {} ), + Handler.Alt ('B', ConsoleKey.B, CmdBackwardWord), + Handler.Alt ('F', ConsoleKey.F, CmdForwardWord), + + Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord), + Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword), + + // DEBUG + //Handler.Control ('T', CmdDebug), + + // quote + Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); }) + }; + + rendered_text = new StringBuilder (); + 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 (); + Console.WriteLine (); + Render (); + } + + void Render () + { + Console.Write (shown_prompt); + Console.Write (rendered_text); + + int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered); + + for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++) + Console.Write (' '); + max_rendered = shown_prompt.Length + rendered_text.Length; + + // Write one more to ensure that we always wrap around properly if we are at the + // end of a line. + Console.Write (' '); + + UpdateHomeRow (max); + } + + void UpdateHomeRow (int screenpos) + { + int lines = 1 + (screenpos / Console.WindowWidth); + + home_row = Console.CursorTop - (lines - 1); + if (home_row < 0) + home_row = 0; + } + + + void RenderFrom (int pos) + { + int rpos = TextToRenderPos (pos); + int i; + + for (i = rpos; i < rendered_text.Length; i++) + Console.Write (rendered_text [i]); + + if ((shown_prompt.Length + rendered_text.Length) > max_rendered) + max_rendered = shown_prompt.Length + rendered_text.Length; + else { + int max_extra = max_rendered - shown_prompt.Length; + for (; i < max_extra; i++) + Console.Write (' '); + } + } + + void ComputeRendered () + { + rendered_text.Length = 0; + + for (int i = 0; i < text.Length; i++){ + int c = (int) text [i]; + if (c < 26){ + if (c == '\t') + rendered_text.Append (" "); + else { + rendered_text.Append ('^'); + rendered_text.Append ((char) (c + (int) 'A' - 1)); + } + } else + rendered_text.Append ((char)c); + } + } + + int TextToRenderPos (int pos) + { + int p = 0; + + for (int i = 0; i < pos; i++){ + int c; + + c = (int) text [i]; + + if (c < 26){ + if (c == 9) + p += 4; + else + p += 2; + } else + p++; + } + + return p; + } + + int TextToScreenPos (int pos) + { + return shown_prompt.Length + TextToRenderPos (pos); + } + + string Prompt { + get { return prompt; } + set { prompt = value; } + } + + int LineCount { + get { + return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth; + } + } + + void ForceCursor (int newpos) + { + cursor = newpos; + + int actual_pos = shown_prompt.Length + TextToRenderPos (cursor); + int row = home_row + (actual_pos/Console.WindowWidth); + int col = actual_pos % Console.WindowWidth; + + if (row >= Console.BufferHeight) + row = Console.BufferHeight-1; + Console.SetCursorPosition (col, row); + + //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); + //log.Flush (); + } + + void UpdateCursor (int newpos) + { + if (cursor == newpos) + return; + + ForceCursor (newpos); + } + + void InsertChar (char c) + { + int prev_lines = LineCount; + text = text.Insert (cursor, c); + ComputeRendered (); + if (prev_lines != LineCount){ + + Console.SetCursorPosition (0, home_row); + Render (); + ForceCursor (++cursor); + } else { + RenderFrom (cursor); + ForceCursor (++cursor); + UpdateHomeRow (TextToScreenPos (cursor)); + } + } + + 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; + } + + void CmdTabOrComplete () + { + bool complete = false; + + if (AutoCompleteEvent != null){ + if (TabAtStartCompletes) + complete = true; + else { + for (int i = 0; i < cursor; i++){ + if (!Char.IsWhiteSpace (text [i])){ + complete = true; + break; + } + } + } + + if (complete) + Complete (); + else + HandleChar ('\t'); + } else + HandleChar ('t'); + } + + void CmdHome () + { + UpdateCursor (0); + } + + void CmdEnd () + { + UpdateCursor (text.Length); + } + + void CmdLeft () + { + if (cursor == 0) + return; + + UpdateCursor (cursor-1); + } + + void CmdBackwardWord () + { + int p = WordBackward (cursor); + if (p == -1) + return; + UpdateCursor (p); + } + + void CmdForwardWord () + { + int p = WordForward (cursor); + if (p == -1) + return; + UpdateCursor (p); + } + + void CmdRight () + { + if (cursor == text.Length) + return; + + UpdateCursor (cursor+1); + } + + void RenderAfter (int p) + { + ForceCursor (p); + RenderFrom (p); + ForceCursor (cursor); + } + + void CmdBackspace () + { + if (cursor == 0) + return; + + bool completing = current_completion != null; + HideCompletions (); + + text.Remove (--cursor, 1); + ComputeRendered (); + RenderAfter (cursor); + if (completing) + UpdateCompletionWindow (); + } + + void CmdDeleteChar () + { + // If there is no input, this behaves like EOF + if (text.Length == 0){ + done = true; + text = null; + Console.WriteLine (); + return; + } + + if (cursor == text.Length) + return; + text.Remove (cursor, 1); + ComputeRendered (); + RenderAfter (cursor); + } + + int WordForward (int p) + { + if (p >= text.Length) + return -1; + + int i = p; + if (Char.IsPunctuation (text [p]) || Char.IsSymbol (text [p]) || Char.IsWhiteSpace (text[p])){ + for (; i < text.Length; i++){ + if (Char.IsLetterOrDigit (text [i])) + break; + } + for (; i < text.Length; i++){ + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } else { + for (; i < text.Length; i++){ + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } + if (i != p) + return i; + return -1; + } + + int WordBackward (int p) + { + if (p == 0) + return -1; + + int i = p-1; + if (i == 0) + return 0; + + if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){ + for (; i >= 0; i--){ + if (Char.IsLetterOrDigit (text [i])) + break; + } + for (; i >= 0; i--){ + if (!Char.IsLetterOrDigit (text[i])) + break; + } + } else { + for (; i >= 0; i--){ + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } + i++; + + if (i != p) + return i; + + return -1; + } + + void CmdDeleteWord () + { + int pos = WordForward (cursor); + + if (pos == -1) + return; + + string k = text.ToString (cursor, pos-cursor); + + if (last_handler == CmdDeleteWord) + kill_buffer = kill_buffer + k; + else + kill_buffer = k; + + text.Remove (cursor, pos-cursor); + ComputeRendered (); + RenderAfter (cursor); + } + + void CmdDeleteBackword () + { + int pos = WordBackward (cursor); + if (pos == -1) + return; + + string k = text.ToString (pos, cursor-pos); + + if (last_handler == CmdDeleteBackword) + kill_buffer = k + kill_buffer; + else + kill_buffer = k; + + text.Remove (pos, cursor-pos); + ComputeRendered (); + RenderAfter (pos); + } + + // + // Adds the current line to the history if needed + // + void HistoryUpdateLine () + { + history.Update (text.ToString ()); + } + + void CmdHistoryPrev () + { + if (!history.PreviousAvailable ()) + return; + + HistoryUpdateLine (); + + SetText (history.Previous ()); + } + + void CmdHistoryNext () + { + if (!history.NextAvailable()) + return; + + history.Update (text.ToString ()); + SetText (history.Next ()); + + } + + 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); + text.Length = cursor; + ComputeRendered (); + RenderAfter (cursor); + } + + void CmdYank () + { + InsertTextAtCursor (kill_buffer); + } + + void InsertTextAtCursor (string str) + { + int prev_lines = LineCount; + text.Insert (cursor, str); + ComputeRendered (); + if (prev_lines != LineCount){ + Console.SetCursorPosition (0, home_row); + Render (); + cursor += str.Length; + ForceCursor (cursor); + } else { + RenderFrom (cursor); + cursor += str.Length; + ForceCursor (cursor); + UpdateHomeRow (TextToScreenPos (cursor)); + } + } + + void SetSearchPrompt (string s) + { + SetPrompt ("(reverse-i-search)`" + s + "': "); + } + + void ReverseSearch () + { + int p; + + if (cursor == text.Length){ + // The cursor is at the end of the string + + p = text.ToString ().LastIndexOf (search); + if (p != -1){ + match_at = p; + cursor = p; + ForceCursor (cursor); + return; + } + } else { + // The cursor is somewhere in the middle of the string + int start = (cursor == match_at) ? cursor - 1 : cursor; + if (start != -1){ + p = text.ToString ().LastIndexOf (search, start); + if (p != -1){ + match_at = p; + cursor = p; + ForceCursor (cursor); + return; + } + } + } + + // Need to search backwards in history + HistoryUpdateLine (); + string s = history.SearchBackward (search); + if (s != null){ + match_at = -1; + SetText (s); + ReverseSearch (); + } + } + + void CmdReverseSearch () + { + if (searching == 0){ + match_at = -1; + last_search = search; + searching = -1; + search = ""; + SetSearchPrompt (""); + } else { + if (search == ""){ + if (last_search != "" && last_search != null){ + search = last_search; + SetSearchPrompt (search); + + ReverseSearch (); + } + return; + } + ReverseSearch (); + } + } + + void SearchAppend (char c) + { + search = search + c; + SetSearchPrompt (search); + + // + // If the new typed data still matches the current text, stay here + // + if (cursor < text.Length){ + string r = text.ToString (cursor, text.Length - cursor); + if (r.StartsWith (search)) + return; + } + + ReverseSearch (); + } + + void CmdRefresh () + { + Console.Clear (); + max_rendered = 0; + Render (); + ForceCursor (cursor); + } + + void InterruptEdit (object sender, ConsoleCancelEventArgs a) + { + // Do not abort our program: + a.Cancel = true; + + // Interrupt the editor + 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 { + bool completing = current_completion != null; + HideCompletions (); + + InsertChar (c); + if (HeuristicAutoComplete (completing, c)) + UpdateCompletionWindow (); + } + } + + void EditLoop () + { + ConsoleKeyInfo cki; + + while (!done){ + ConsoleModifiers mod; + + cki = Console.ReadKey (true); + if (cki.Key == ConsoleKey.Escape){ + if (current_completion != null){ + HideCompletions (); + continue; + } else { + cki = Console.ReadKey (true); + + mod = ConsoleModifiers.Alt; + } + } else + mod = cki.Modifiers; + + bool handled = false; + + foreach (Handler handler in handlers){ + ConsoleKeyInfo t = handler.CKI; + + 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; + } + } + if (handled){ + if (searching != 0){ + if (last_handler != CmdReverseSearch){ + searching = 0; + SetPrompt (prompt); + } + } + continue; + } + + if (cki.KeyChar != (char) 0){ + HandleChar (cki.KeyChar); + } + } + } + + void InitText (string initial) + { + text = new StringBuilder (initial); + ComputeRendered (); + cursor = text.Length; + Render (); + ForceCursor (cursor); + } + + void SetText (string newtext) + { + Console.SetCursorPosition (0, home_row); + InitText (newtext); + } + + void SetPrompt (string newprompt) + { + shown_prompt = newprompt; + Console.SetCursorPosition (0, home_row); + Render (); + ForceCursor (cursor); + } + + public string Edit (string prompt, string initial) + { + edit_thread = Thread.CurrentThread; + searching = 0; + Console.CancelKeyPress += InterruptEdit; + + done = false; + history.CursorToEnd (); + max_rendered = 0; + + Prompt = prompt; + shown_prompt = prompt; + InitText (initial); + history.Append (initial); + + do { + try { + EditLoop (); + } catch (ThreadAbortException){ + searching = 0; + Thread.ResetAbort (); + Console.WriteLine (); + SetPrompt (prompt); + SetText (""); + } + } while (!done); + Console.WriteLine (); + + Console.CancelKeyPress -= InterruptEdit; + + if (text == null){ + history.Close (); + return null; + } + + string result = text.ToString (); + if (result != "") + history.Accept (result); + else + history.RemoveLast (); + + return result; + } + + public void SaveHistory () + { + if (history != null) { + history.Close (); + } + } + + public bool TabAtStartCompletes { get; set; } + + // + // Emulates the bash-like behavior, where edits done to the + // history are recorded + // + class History { + string [] history; + int head, tail; + int cursor, count; + string histfile; + + public History (string app, int size) + { + if (size < 1) + throw new ArgumentException ("size"); + + if (app != null){ + string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); + //Console.WriteLine (dir); + if (!Directory.Exists (dir)){ + try { + Directory.CreateDirectory (dir); + } catch { + app = null; + } + } + if (app != null) + histfile = Path.Combine (dir, app) + ".history"; + } + + history = new string [size]; + head = tail = cursor = 0; + + if (File.Exists (histfile)){ + using (StreamReader sr = File.OpenText (histfile)){ + string line; + + while ((line = sr.ReadLine ()) != null){ + if (line != "") + Append (line); + } + } + } + } + + public void Close () + { + if (histfile == null) + return; + + try { + using (StreamWriter sw = File.CreateText (histfile)){ + int start = (count == history.Length) ? head : tail; + for (int i = start; i < start+count; i++){ + int p = i % history.Length; + sw.WriteLine (history [p]); + } + } + } catch { + // ignore + } + } + + // + // Appends a value to the history + // + public void Append (string s) + { + //Console.WriteLine ("APPENDING {0} head={1} tail={2}", s, head, tail); + history [head] = s; + head = (head+1) % history.Length; + if (head == tail) + tail = (tail+1 % history.Length); + if (count != history.Length) + count++; + //Console.WriteLine ("DONE: head={1} tail={2}", s, head, tail); + } + + // + // Updates the current cursor location with the string, + // to support editing of history items. For the current + // line to participate, an Append must be done before. + // + public void Update (string s) + { + history [cursor] = s; + } + + public void RemoveLast () + { + head = head-1; + if (head < 0) + head = history.Length-1; + } + + public void Accept (string s) + { + int t = head-1; + if (t < 0) + t = history.Length-1; + + history [t] = s; + } + + public bool PreviousAvailable () + { + //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor); + if (count == 0) + return false; + int next = cursor-1; + if (next < 0) + next = count-1; + + if (next == head) + return false; + + return true; + } + + public bool NextAvailable () + { + if (count == 0) + return false; + int next = (cursor + 1) % history.Length; + if (next == head) + return false; + return true; + } + + + // + // Returns: a string with the previous line contents, or + // nul if there is no data in the history to move to. + // + public string Previous () + { + if (!PreviousAvailable ()) + return null; + + cursor--; + if (cursor < 0) + cursor = history.Length - 1; + + return history [cursor]; + } + + public string Next () + { + if (!NextAvailable ()) + return null; + + cursor = (cursor + 1) % history.Length; + return history [cursor]; + } + + public void CursorToEnd () + { + if (head == tail) + return; + + cursor = head; + } + + public void Dump () + { + Console.WriteLine ("Head={0} Tail={1} Cursor={2} count={3}", head, tail, cursor, count); + for (int i = 0; i < history.Length;i++){ + Console.WriteLine (" {0} {1}: {2}", i == cursor ? "==>" : " ", i, history[i]); + } + //log.Flush (); + } + + public string SearchBackward (string term) + { + for (int i = 0; i < count; i++){ + int slot = cursor-i-1; + if (slot < 0) + slot = history.Length+slot; + if (slot >= history.Length) + slot = 0; + if (history [slot] != null && history [slot].IndexOf (term) != -1){ + cursor = slot; + return history [slot]; + } + } + + return null; + } + + } + } + +#if DEMO + class Demo { + static void Main () + { + 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){ + Console.WriteLine ("----> [{0}]", s); + } + } + } +#endif +} diff --git a/vr21/vr21.csproj b/vr21/vr21.csproj index 797356e..e1a66ac 100644 --- a/vr21/vr21.csproj +++ b/vr21/vr21.csproj @@ -43,6 +43,7 @@ +