/*
This file is part of sImlac.
sImlac is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
sImlac is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with sImlac. If not, see .
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace imlac.Debugger
{
public class DebuggerPrompt
{
public DebuggerPrompt(DebuggerCommand root)
{
_commandTree = root;
_commandHistory = new List(64);
_historyIndex = 0;
}
///
/// Runs a nifty interactive debugger prompt.
///
public string Prompt()
{
DisplayPrompt();
ClearInput();
UpdateOrigin();
bool entryDone = false;
while (!entryDone)
{
UpdateDisplay();
// Read one keystroke from the console...
ConsoleKeyInfo key = Console.ReadKey(true);
//Parse special chars...
switch (key.Key)
{
case ConsoleKey.Escape: //Clear input
ClearInput();
break;
case ConsoleKey.Backspace: // Delete last char
DeleteCharAtCursor(true /* backspace */);
break;
case ConsoleKey.Delete: //Delete character at cursor
DeleteCharAtCursor(false /* delete */);
break;
case ConsoleKey.LeftArrow:
MoveLeft();
break;
case ConsoleKey.RightArrow:
MoveRight();
break;
case ConsoleKey.UpArrow:
HistoryPrev();
break;
case ConsoleKey.DownArrow:
HistoryNext();
break;
case ConsoleKey.Home:
MoveToBeginning();
break;
case ConsoleKey.End:
MoveToEnd();
break;
case ConsoleKey.Tab:
DoCompletion(false /* silent */);
break;
case ConsoleKey.Enter:
DoCompletion(true /* silent */);
UpdateDisplay();
CRLF();
entryDone = true;
break;
case ConsoleKey.Spacebar:
if (!_input.EndsWith(" ") &&
DoCompletion(true /* silent */))
{
UpdateDisplay();
}
else
{
InsertChar(key.KeyChar);
}
break;
default:
// Not a special key, just insert it if it's deemed printable.
if (char.IsLetterOrDigit(key.KeyChar) ||
char.IsPunctuation(key.KeyChar) ||
char.IsSymbol(key.KeyChar) ||
char.IsWhiteSpace(key.KeyChar))
{
InsertChar(key.KeyChar);
}
break;
}
}
// Done. Add to history if input is non-empty
if (_input != string.Empty)
{
_commandHistory.Add(_input);
HistoryIndex = _commandHistory.Count - 1;
}
return _input;
}
private void DeleteCharAtCursor(bool backspace)
{
if (_input.Length == 0)
{
//nothing to delete, bail.
return;
}
if (backspace)
{
if (TextPosition == 0)
{
// We's at the beginning of the input,
// can't backspace from here.
return;
}
else
{
// remove 1 char at the position before the cursor.
// and move the cursor back one char
_input = _input.Remove(TextPosition - 1, 1);
TextPosition--;
}
}
else
{
if (TextPosition == _input.Length)
{
// At the end of input, can't delete a char from here
return;
}
else
{
// remove one char at the current cursor pos.
// do not move the cursor
_input = _input.Remove(TextPosition, 1);
}
}
}
private void DisplayPrompt()
{
Console.Write(">");
}
private void UpdateDisplay()
{
//if the current input string is shorter than the last, then we need to erase a few chars at the end.
string clear = String.Empty;
if (_input.Length < _lastInputLength)
{
StringBuilder sb = new StringBuilder(_lastInputLength - _input.Length);
for (int i = 0; i < _lastInputLength - _input.Length; i++)
{
sb.Append(' ');
}
clear = sb.ToString();
}
// Default to 80 columns if BufferWidth happens to be zero.
int bufferWidth = Console.BufferWidth > 0 ? Console.BufferWidth : 80;
int column = ((_textPosition + _originColumn) % bufferWidth);
int row = ((_textPosition + _originColumn) / bufferWidth) + _originRow;
// Move cursor to origin to draw string
Console.CursorLeft = _originColumn;
Console.CursorTop = _originRow;
Console.Write(_input + clear);
// Move cursor to text position to draw cursor
Console.CursorLeft = column;
Console.CursorTop = row;
Console.CursorVisible = true;
_lastInputLength = _input.Length;
}
private void MoveLeft()
{
TextPosition--;
}
private void MoveRight()
{
TextPosition++;
}
private void HistoryPrev()
{
if (HistoryIndex < _commandHistory.Count)
{
_input = _commandHistory[HistoryIndex];
TextPosition = _input.Length;
HistoryIndex--;
}
}
private void HistoryNext()
{
if (HistoryIndex < _commandHistory.Count)
{
HistoryIndex++;
_input = _commandHistory[HistoryIndex];
TextPosition = _input.Length;
}
else
{
_input = String.Empty;
}
}
private void MoveToBeginning()
{
TextPosition = 0;
}
private void MoveToEnd()
{
TextPosition = _input.Length;
}
private void ClearInput()
{
_input = String.Empty;
HistoryIndex = _commandHistory.Count - 1;
TextPosition = 0;
}
private void InsertChar(char c)
{
_input = _input.Insert(TextPosition, c.ToString());
TextPosition++;
}
private void CRLF()
{
Console.WriteLine();
}
private bool DoCompletion(bool silent)
{
// This code should probably move to another class, but hey, I'm lazy.
// Take the current input and see if it matches anything in the command tree
string[] tokens = _input.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// Save off the current cursor row; this is an ugly-ish hack to detect whether the match process
// output anything. If the cursor row changes then we'll need to move the prompt
int oldRow = Console.CursorTop;
string matchString = FuzzyMatch(_commandTree, new List(tokens), silent);
bool changed = false;
if (matchString != String.Empty)
{
changed = _input.Trim().ToLower() != matchString.Trim().ToLower();
// Add a space if the output is different than the input (in which case a completion
// actually took place.
if (changed)
{
matchString += " ";
}
_input = matchString;
TextPosition = _input.Length;
}
if (!silent && oldRow != Console.CursorTop)
{
DisplayPrompt();
UpdateOrigin();
}
return changed;
}
private string FuzzyMatch(DebuggerCommand root, List tokens, bool silent)
{
if (tokens.Count == 0)
{
if (!silent)
{
// If there are no tokens, just show the completion for the root.
PrintCompletions(root.SubCommands);
}
return String.Empty;
}
DebuggerCommand match = null;
bool exactMatch = false;
// Search for exact matches. If we find one it's guaranteed to be unique
// so we can follow that node.
foreach (DebuggerCommand c in root.SubCommands)
{
if (c.Name.ToLower() == tokens[0].ToLower())
{
match = c;
exactMatch = true;
break;
}
}
if (match == null)
{
// No exact match. Try a substring match.
// If we have an unambiguous match then we can complete it automatically.
// If the match is ambiguous, display possible completions and return String.Empty.
List completions = new List();
foreach (DebuggerCommand c in root.SubCommands)
{
if (c.Name.StartsWith(tokens[0], StringComparison.InvariantCultureIgnoreCase))
{
completions.Add(c);
}
}
if (completions.Count == 1)
{
// unambiguous match. use it.
match = completions[0];
}
else if (completions.Count > 1)
{
// ambiguous match. display possible completions.
if (!silent)
{
PrintCompletions(completions);
}
}
}
if (match == null)
{
// If we reach this point then no matches are available. return the tokens we have...
StringBuilder sb = new StringBuilder();
for (int i = 0; i < tokens.Count; i++)
{
if (i < tokens.Count - 1)
{
sb.AppendFormat("{0} ", tokens[i]);
}
else
{
sb.AppendFormat("{0}", tokens[i]);
}
}
return sb.ToString();
}
else
{
// A match was found
tokens.RemoveAt(0);
string subMatch = String.Empty;
if (tokens.Count > 0)
{
subMatch = FuzzyMatch(match, tokens, silent);
}
else
{
if (!silent && match.SubCommands.Count > 1)
{
// More than one possible completion
// Just show the completions for this node.
PrintCompletions(match.SubCommands);
}
else if(!silent && match.SubCommands.Count == 1)
{
// Just one possible completion; fill it out.
DebuggerCommand next = match.SubCommands[0];
StringBuilder sb =
new StringBuilder(String.Format("{0} {1}", match.Name, next.Name));
while(next.SubCommands.Count > 0)
{
if (next.SubCommands.Count > 1)
{
break;
}
next = next.SubCommands[0];
sb.AppendFormat(" {0}", next.Name);
}
return sb.ToString();
}
else if (!silent &&
match.SubCommands.Count == 0 &&
exactMatch)
{
// No more completions; this was an exact match, so
// instead print the help for this command if any
// is available.
Console.WriteLine();
Console.WriteLine(match.Description);
if (!String.IsNullOrWhiteSpace(match.Usage))
{
Console.WriteLine("Parameters: {0}", match.Usage);
}
}
}
if (subMatch == String.Empty)
{
return String.Format("{0}", match.Name);
}
else
{
return String.Format("{0} {1}", match.Name, subMatch);
}
}
}
private void PrintCompletions(List completions)
{
// Just print all available completions at this node
Console.WriteLine();
Console.WriteLine("Possible completions are:");
foreach (DebuggerCommand c in completions)
{
Console.Write("{0}\t", c);
}
Console.WriteLine();
}
private void UpdateOrigin()
{
_originRow = Console.CursorTop;
_originColumn = Console.CursorLeft;
}
private int TextPosition
{
get
{
return _textPosition;
}
set
{
// Clip input between 0 and the length of input (+1, to allow adding text at end)
_textPosition = Math.Max(0, value);
_textPosition = Math.Min(_textPosition, _input.Length);
}
}
private int HistoryIndex
{
get
{
return _historyIndex;
}
set
{
_historyIndex = Math.Min(_commandHistory.Count - 1, value);
_historyIndex = Math.Max(0, _historyIndex);
}
}
private DebuggerCommand _commandTree;
private int _originRow;
private int _originColumn;
private string _input;
private int _textPosition;
private int _lastInputLength;
private List _commandHistory;
private int _historyIndex;
}
}