Using Autocomplete in Windows Console Applications

0
88

Introduction

AutoCompleteConsole is a solution that contains some tools for implementing auto complete functionality for a Windows Console Application, without losing key functions when using Console.Readkey().

Background

Recently, I was working on a project that needed a simple UI. To save time, I decided to use a Windows Console application. After implementing some simple commands, I thought it would be neat to allow the user to use autocomplete functionality (using the Tab key).

After doing some research, this turned out not to be as straightforward as I first thought. A lot of solutions offered for this problem included using the Console.ReadKey method. The problem with using this method is that it will disable a lot of other functions like using the arrow up/down keys to scroll through the history of typed commands. Functionality I wanted to keep.

After some more Googling, I didn’t find any solution to this problem and I decided to write my own. I want to share my solution with the community.

I’ve tried a lot of different approaches, even intercepting keys directly from the operating system. None had the desired effect.

Eventually, I decided to reverse engineer the console functionalities that are being lost by using the ReadKey method (on top of supporting auto complete). As I want this article to focus on the auto completion part of the problem, I’ve dedicated another article to the Console.ReadKey problem. That article can be found here.

This article explains how to implement either one of two types of autocompletion. It also explains how to combine the before mentioned solution with these implementations, as I suspect this is where the most people are having trouble. Of course, the autocompletion algorithms themselves can also be used in different circumstances. For this reason, it is implemented in a different project in the solution.

Using the Code

The ConsoleUtils library (also in attached solution) provides its own ConsoleExt.ReadKey method which is very similar to the Console.ReadLine method .NET provides. The difference is that the new method leaves most key functions intact (So the up and down arrows still scroll through previous commands, etc.). It also returns a KeyPressResult instead of a ConsoleKeyInfo entity. This object doesn’t only tell the programmer which key was pressed, but also contains information about the complete line and cursor position, both before and after the key press.

All properties on KeyPressResult:

  • ConsoleKeyInfo – The same struct that would be returned by Console.ReadKey()
  • Key – The ConsoleKey inside ConsoleKeyInfo
  • KeyChar – The key character inside ConsoleKeyInfo
  • Modifiers – The modifiers that were pressed when the input was given (e.g. Shift, Ctrl)
  • LineBeforeKeyPress – A LineState class containing the line information as it was before the key was pressed
  • LineAfterKeyPress – A LineState class containing the line information as it is after the key is pressed

All properties on LineState:

  • Line – The line
  • CursorPosition – The position of the console cursor
  • LineBeforeCursor – The part of the line that was before the CursorPosition
  • LineAfterCursor – The part of the line that was after the CursorPosition

An example of how to use ReadKey:

KeyPressResult result = ConsoleExt.ReadKey();
switch (result.Key)
{
 case ConsoleKey.Enter:
 
 break;
 case ConsoleKey.Tab:
 
 break;
}

Note: The example states that LineBeforeKeyPress.Line should be used to get the same result as Console.Readline. This is because after pressing the enter key, the newline is empty. So the LineAfterKeyPress.Line will be an empty string.

More information about the ConsoleUtils library can be found in this article.

Implementing Autocomplete

Even though the main problem most people are actually having (detecting a key (Tab) while retaining other console functionality) is solved with the implementation above. The article wouldn’t be complete if it didn’t at least provide you with the means to actually implement a basic form of autocompletion.

I found that there are two useful ways to implement autocompletion:

  • Complimentary autocomplete – Will look at the available commands, and gives the user an autocomplete that is shared by all commands.
  • Cycling autocomplete – Lets the user cycle through all options by repeatedly pressing the tab button (used in command window).

Complimentary Autocomplete

Implementing complimentary autocomplete is easiest, since it doesn’t require the program to have a state. To implement this, the AutoComplete.GetComplimentaryAutoComplete method can be used for automatically completing entire sentences.

Example:

var commands = new List<string>
{
 "Exit",
 "The green ball.",
 "The red ball.",
 "The red block.",
 "The round ball."
};

var running = true;
while (running)
{
 var result = ConsoleExt.ReadKey();
 switch (result.Key)
 {
 case ConsoleKey.Enter:
 
 break;
 case ConsoleKey.Tab:
 var autoCompletedLine = AutoComplete.GetComplimentaryAutoComplete(
 result.LineBeforeKeyPress.LineBeforeCursor, commands);
 ConsoleExt.SetLine(autoCompletedLine);
 break;
 }
}

Three things to note here:

  • commands – This variable is a list of Strings, containing the possible commands to autocomplete to.
  • result.LineBeforeKeyPress is used because we do not actually want the tab-character when looking for autocompletion.
  • LineBeforeCursor is used for autocompletion in this example. This means that if the user uses the left arrow to go back in his line, only the part before the cursor is used for autocompletion.
  • It is not necessary to use intercept. In the only case we get a character we don’t want (tab-character), we already use ConsoleExt.SetLine to overwrite the entire line, including the tab.

The gif shows the result (explanation below):

When the user pressed tab after typing t, the line is autocompleted to The . When the user then types re (making it The re), and pressed tab. The line turns into The red b. Only after providing the l can the system autocomplete to The red block..

In the example is also shown how the user decides to go back in the line to type a g. Because the code uses LineBeforeCursor, it is now only using The g for autocompletion. Turning the line into The green ball..

Cycling Autocomplete

For implementing cycling autocompletion, the CyclingAutoComplete class is provided.

Example:

var commands = new List<string>
{
 "Exit",
 "The green ball.",
 "The red ball.",
 "The red block.",
 "The round ball."
};

var running = true;
var cyclingAutoComplete = new CyclingAutoComplete();
while (running)
{
 var result = ConsoleExt.ReadKey();
 switch (result.Key)
 {
 case ConsoleKey.Enter:
 
 break;
 case ConsoleKey.Tab:
 var autoCompletedLine = cyclingAutoComplete.AutoComplete(
 result.LineBeforeKeyPress.LineBeforeCursor, commands);
 ConsoleExt.SetLine(autoCompletedLine);
 break;
 }
}

Result:

When the user pressed tab after typing T, the line is autocompleted to The green ball.. When the user then pressed tab again, the line turns into The red ball.. The lines will keep cycling every time tab is pressed.

When the user then moves the cursor back to after red, the cycle changes to only contain The red ball. and The red block.. Again because we used LineBeforeCursor here.

Cycling Both Directions

In most consoles, the user can cycle back using the combination Shift+Tab. The CyclingAutoComplete class already supports this using the CyclingDirections parameter. It can be easily achieved by altering the code a bit:

case ConsoleKey.Tab:
 var shiftPressed = (result.Modifiers & ConsoleModifiers.Shift) != 0;
 var cyclingDirection = shiftPressed ? CyclingDirections.Backward : CyclingDirections.Forward;
 var autoCompletedLine = cyclingAutoComplete.AutoComplete(
 result.LineBeforeKeyPress.LineBeforeCursor,
 commands, cyclingDirection);
 ConsoleExt.SetLine(autoCompletedLine);
 break;

All examples are present in the attached solution.

Points of Interest

Even though this article references this article because I think it is applicable to most people looking to implement auto completion in a console, the algorithms explained in this article can easily be used without it. It is for that reason that both implementations have their separate project in the attached solution.

I haven’t implemented all default console functionality. If you have any good additions, feel free to message me, and I might add them to the project.

All methods that can be used for autocompletion have an optional ignoreCase parameter. Default is true.

History

16-04-2017 – Version 1
17-04-2017 – Version 2
  • Extended InputResult with cursor position and modifiers
  • Added CyclingDirections to the library, allowing the user to cycle both directions
  • Simplified the examples
  • Uploaded gifs for illustration
27-04-2017 – Version 3
  • Made the entire library more generic. Where ConsoleExt used to have knowledge about autocompletion, it is now entirely separate from this logic. It is now also a separate library.
  • Added ReadLine, SimulateKeyPress, SetLine, ClearLine, StartNewLine and PrependLine to ConsoleExt
  • Changed InputResult in KeyPressResult and extracted LineState class
  • Made ConsoleExt thread safe
29-04-2017 – Version 3.1
  • Prevent normal use of tab key to cause undefined behaviour.
  • Solved bug in PrependLine when prepending multiline strings.
11-06-2017 – Version 4.0
  • Explained the separation between the ConsoleExt.ReadKey and Autocompletion algorithms clearer.
  • Refactored ConsoleExt to allow for a lot more unit testing and implemented those unit tests.

LEAVE A REPLY