Repository: mono/LineEditor Branch: main Commit: b727422e6c19 Files: 8 Total size: 51.9 KB Directory structure: gitextract_r8ju7kiq/ ├── .ci/ │ └── azure-pipelines.yml ├── .gitignore ├── CODE-OF-CONDUCT.md ├── LICENSE ├── LineEditor/ │ ├── LineEditor.csproj │ └── getline.cs ├── LineEditor.sln └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ci/azure-pipelines.yml ================================================ name: $(Build.SourceBranchName)-$(Build.SourceVersion)-$(Rev:r) trigger: - main - refs/tags/* pr: - main resources: repositories: - repository: internal-templates type: github name: xamarin/yaml-templates endpoint: xamarin - repository: components type: github name: xamarin/XamarinComponents endpoint: xamarin jobs: - template: .ci/build.yml@components parameters: publishJob: windows buildType: none areaPath: 'DevDiv\Xamarin SDK' linuxImage: '' # skip linux builds validPackagePrefixes: [ 'Mono' ] steps: - task: MSBuild@1 displayName: msbuild LineEditor.sln inputs: solution: LineEditor.sln configuration: Release msbuildArguments: /restore /t:Pack /p:PackageOutputPath=$(Build.SourcesDirectory)/output - ${{ if eq(variables['System.TeamProject'], 'devdiv') }}: - template: sign-artifacts/jobs/v1.yml@internal-templates parameters: dependsOn: [ 'build' ] realSign: true signTags: true signMain: true ================================================ FILE: .gitignore ================================================ bin obj *.userprefs ================================================ FILE: CODE-OF-CONDUCT.md ================================================ # Code of Conduct This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Mono Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LineEditor/LineEditor.csproj ================================================ netstandard2.0 Mono.Terminal 5.4.2 Microsoft © Microsoft Corporation. All rights reserved. https://github.com/mono/LineEditor/raw/main/icon_128x128.png https://github.com/mono/LineEditor/blob/main/LICENSE Xamarin https://github.com/mono/LineEditor Release 5.4.2 improves Windows support. True An interactive commmand line editor Mono.Terminal - LineEdit, GetLine Interactive Command Line Editor false $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb True snupkg bin\Debug\netstandard2.0\LineEditor.xml bin\Release\netstandard2.0\LineEditor.xml ================================================ FILE: LineEditor/getline.cs ================================================ // // getline.cs: A command line editor // // Authors: // Miguel de Icaza (miguel@microsoft.com) // // Copyright 2008 Novell, Inc. // Copyright 2016 Xamarin Inc // Copyright 2017 Microsoft // // 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; using System.Runtime.InteropServices; namespace Mono.Terminal { /// /// Interactive line editor. /// /// /// /// LineEditor is an interative line editor for .NET applications that provides /// editing capabilities for an input line with common editing capabilities and /// navigation expected in modern application as well as history, incremental /// search over the history, completion (both textual or visual) and various /// Emacs-like commands. /// /// /// When you create your line editor, you can pass the name of your application, /// which will be used to load and save the history of commands entered by the user /// for this particular application. /// /// /// /// /// /// The following example shows how you can instantiate a line editor that /// can provide code completion for some words when the user presses TAB /// and how the user can edit them. /// /// LineEditor le = new LineEditor ("myshell") { HeuristicsMode = "csharp" }; /// le.AutoCompleteEvent += delegate (string line, int point){ /// 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("You typed: [{0}]", s); } /// /// /// /// Users can use the cursor keys to navigate both the text on the current /// line, or move back and forward through the history of commands that have /// been entered. /// /// /// The interactive commands and keybindings are inspired by the GNU bash and /// GNU readline capabilities and follow the same concepts found there. /// /// /// Copy and pasting works like bash, deleted words or regions are added to /// the kill buffer. Repeated invocations of the same deleting operation will /// append to the kill buffer (for example, repeatedly deleting words) and to /// paste the results you would use the Control-y command (yank). /// /// /// The history search capability is triggered when you press /// Control-r to start a reverse interactive-search /// and start typing the text you are looking for, the edited line will /// be updated with matches. Typing control-r again will go to the next /// match in history and so on. /// /// /// /// Shortcut /// Action performed /// /// /// Left cursor, Control-b /// /// Moves the editing point left. /// /// /// /// Right cursor, Control-f /// /// Moves the editing point right. /// /// /// /// Alt-b /// /// Moves one word back. /// /// /// /// Alt-f /// /// Moves one word forward. /// /// /// /// Up cursor, Control-p /// /// Selects the previous item in the editing history. /// /// /// /// Down cursor, Control-n /// /// Selects the next item in the editing history. /// /// /// /// Home key, Control-a /// /// Moves the cursor to the beginning of the line. /// /// /// /// End key, Control-e /// /// Moves the cursor to the end of the line. /// /// /// /// Delete, Control-d /// /// Deletes the character in front of the cursor. /// /// /// /// Backspace /// /// Deletes the character behind the cursor. /// /// /// /// Tab /// /// Triggers the completion and invokes the AutoCompleteEvent which gets /// both the line contents and the position where the cursor is. /// /// /// /// Control-k /// /// Deletes the text until the end of the line and replaces the kill buffer /// with the deleted text. You can paste this text in a different place by /// using Control-y. /// /// /// /// Control-l refresh /// /// Clears the screen and forces a refresh of the line editor, useful when /// a background process writes to the console and garbles the contents of /// the screen. /// /// /// /// Control-r /// /// Initiates the reverse search in history. /// /// /// /// Alt-backspace /// /// Deletes the word behind the cursor and adds it to the kill ring. You /// can paste the contents of the kill ring with Control-y. /// /// /// /// Alt-d /// /// Deletes the word above the cursor and adds it to the kill ring. You /// can paste the contents of the kill ring with Control-y. /// /// /// /// Control-y /// /// Pastes the content of the kill ring into the current position. /// /// /// /// Control-q /// /// Quotes the next input character, to prevent the normal processing of /// key handling to take place. /// /// /// /// public class LineEditor { /// /// Completion results returned by the completion handler. /// /// /// You create an instance of this class to return the completion /// results for the text at the specific position. The prefix parameter /// indicates the common prefix in the results, and the results contain the /// results without the prefix. For example, when completing "ToString" and "ToDate" /// prefix would be "To" and the completions would be "String" and "Date". /// public class Completion { /// /// Array of results, with the stem removed. /// public string [] Result; /// /// Shared prefix for the completion results. /// public string Prefix; /// /// Initializes a new instance of the class. /// /// Common prefix for all results, an be null. /// Array of possible completions. public Completion (string prefix, string [] result) { Prefix = prefix; Result = result; } } /// /// Method signature for auto completion handlers. /// /// /// The completion handler receives the text as it is being edited as /// well as the position of the cursor in that line. The method /// must return an instance of Completion with the possible completions. /// public delegate Completion AutoCompleteHandler (string text, int pos); /// /// The heuristics mode used by code completion. /// /// /// /// This controls the heuristics style used to show the code /// completion popup as well as when to accept an entry. /// /// /// The default value is null which requires the user to explicitly /// use the TAB key to trigger a completion. /// /// /// Another possible value is "csharp" which will trigger auto-completion when a /// "." is entered. /// /// 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; private readonly bool isWindows; /// /// Initializes a new instance of the LineEditor, using the specified name for /// retrieving and storing the history. The history will default to 10 entries. /// /// Prefix for storing the editing history. public LineEditor (string name) : this (name, 10) { } /// /// Initializes a new instance of the LineEditor, using the specified name for /// retrieving and storing the history. /// /// Prefix for storing the editing history. /// Number of entries to store in the history file. 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); isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 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 // if (isWindows) 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 (!isWindows && Console.CursorTop > target_line){ 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); } /// /// Edit a line, and provides both a prompt and the initial contents to edit /// /// The edit. /// Prompt shown to edit the line. /// Initial contents, can be null. 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; } /// /// Triggers the history to be written at this point, usually not necessary, history is saved on each line edited. /// public void SaveHistory () { if (history != null) { history.Close (); } } /// /// Gets or sets a value indicating whether hitting the TAB key before any text exists triggers completion or inserts a "tab" character into the buffer. This is useful to allow users to copy/paste code that might contain whitespace at the start and you want to preserve it. /// /// true if tab at start completes; otherwise, false. 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 } ================================================ FILE: LineEditor.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LineEditor", "LineEditor\LineEditor.csproj", "{8E652215-21AE-483F-BCE5-C66A70CFE96C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8E652215-21AE-483F-BCE5-C66A70CFE96C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8E652215-21AE-483F-BCE5-C66A70CFE96C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E652215-21AE-483F-BCE5-C66A70CFE96C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E652215-21AE-483F-BCE5-C66A70CFE96C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal ================================================ FILE: README.md ================================================ # LineEditor LineEditor is an interative line editor for .NET applications that provides editing capabilities for an input line with common editing capabilities and navigation expected in modern application as well as history, incremental search over the history, completion (both textual or visual) and various Emacs-like commands. The code was originally developed for Mono's interactive C# shell: ![Screenshot of csharp with a completion window](https://github.com/mono/LineEditor/blob/main/LineEditor/csharp.png) When you create your line editor, you can pass the name of your application, which will be used to load and save the history of commands entered by the user for this particular application. # Installation Use NuGet or Paket to install the `Mono.Terminal` NuGet package. # Example The following example shows how you can instantiate a line editor that can provide code completion for some words when the user presses TAB and how the user can edit them. ```csharp using Mono.Terminal; using System; class Demo { static void Main () { // Creates a line editor, and sets the name of the editing session to // "foo". This is used to save the history of input entered LineEditor le = new LineEditor ("foo") { HeuristicsMode = "csharp" }; // Configures auto-completion, in this example, the result // is always to offer the numbers as completions le.AutoCompleteEvent += delegate (string text, 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; // Prompts the user for input while ((s = le.Edit ("shell> ", "")) != null){ Console.WriteLine ("Your Input: [{0}]", s); } } } ``` # Editing Users can use the cursor keys to navigate both the text on the current line, or move back and forward through the history of commands that have been entered. The interactive commands and keybindings are inspired by the GNU bash and GNU readline capabilities and follow the same concepts found there. ## Kill Buffer: Copy and Paste Copy and pasting works like bash, deleted words or regions are added to the kill buffer. Repeated invocations of the same deleting operation will append to the kill buffer (for example, repeatedly deleting words) and to paste the results you would use the Control-y command (yank). ## History and searching The history search capability is triggered when you press Control-r to start a reverse interactive-search and start typing the text you are looking for, the edited line will be updated with matches.Typing control-r again will go to the next match in history and so on. ## Shortcuts | Shortcut | Action performed | | ------------------------ | ----------------------------------------------- | |Left cursor, `Control`-`b`|Moves the editing point left | |`Alt`-`b` |Moves one word back. | |`Alt`-`f` |Moves one word forward. | |`↑`, `Control`-`p` |Selects the previous item in the editing history.| |`↓`, `Control`-`n` |Selects the next item in the editing history. | |`Home` key, `Control`-`a` |Moves the cursor to the beginning of the line. | |`End` key, `Control`-`e` |Moves the cursor to the end of the line. | |`Delete`, `Control`-`d` |Deletes the character in front of the cursor. | |`Backspace` |Deletes the character behind the cursor. | |`Tab` |Triggers the completion and invokes the AutoCompleteEvent which gets both the line contents and the position where the cursor is.| |`Control`-`k` |Deletes the text until the end of the line and replaces the kill buffer with the deleted text. You can paste this text in a different place by using Control-y.| |`Control`-`l` |Clears the screen and forces a refresh of the line editor, useful when a background process writes to the console and garbles the contents of the screen.| |`Control`-`r` |Initiates the reverse search in history. | |`Alt`-`backspace` |Deletes the word behind the cursor and adds it to the kill ring. You can paste the contents of the kill ring with Control-y.| |`Alt`-`d` |Deletes the word above the cursor and adds it to the kill ring. You can paste the contents of the kill ring with Control-y.| |`Control`-`q` |Quotes the next input character, to prevent the normal processing of keyhandling to take place| |`Control`-`c` |Interrupts editing |