如何覆盖 'tab' 键行为以在控制台中完成输入?

How to overwrite 'tab' key behaviour to complete input in console?

我正在使用一个示例计算器项目,用户可以在其中输入他需要在控制台中执行的选项(操作类型)。目前我正在匹配用户提供的完整输入,以检查他输入的选项是否存在于我的选项列表中,然后执行操作。 目前选项列表如下(将进一步扩展更多新选项):

List<string> options = new List<string>()
{
   "add",
   "divide",
   "multiply",
   "subtract",
   "modulus"
};

用于指定操作的用户输入:

op add
op divide

'op'这里是一个特定的输入开始,指定further关键字将是一个操作选项。

目前我正在阅读整行,然后拆分输入并确定输入和选项的语法是否符合我的需要。

我在想是否可以包含当用户按下 tab 键时自动完成选项输入的行为。 (就像智能感知或 windows 'cd' 命令所发生的情况一样)。当前按 Tab 键会在输入行中添加空格。

例如:

op mul --> press 'tab' --> op multiply

我正在尝试使用 Console.ReadKey() 读取密钥,看看是否有任何可能,但到目前为止运气不好。

string input = Console.ReadLine();   //Currently fetching input string using this
//var key = Console.ReadKey();       //Not sure what to do here
//split inpput further and check if it matches correct syntax and option entered

您不能使用 ReadLine 执行此操作,因为在用户按下 ENTER 之前它会一直读取。相反,你应该只使用 ReadKey,像这样:

string opName = "";
while(true)
{
    var key = Console.ReadKey();
    if (key.Key == ConsoleKey.Enter)
       break;

    if (key.Key == ConsoleKey.Tab)
    {
        if (opName.StartsWith("op"))
        {
            var opString = opName.Split(" ")[1];
            var opName = "op" + //get operation that starts with opString
            break;
        }
    }
    else
    {
        opName += key.KeyChar;
    }
}

var operationName = var opString = opName.Split(" ")[1];

这样做比您想象的要复杂得多,因为您必须有效地编写自己的 Console.ReadLine() 版本,它必须处理光标移动、退格、删除等操作。当您按 TAB 键扩展关键字时,它还必须处理扩展控制台中的当前文本行。

但是,如果您可以使用只允许退格键的简化版本,您可以编写类似这样的代码(这只是帮助您入门的示例代码;正确的解决方案需要正确的错误处理):

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            var lookup = new Dictionary<string, string> // Expandable keywords
            {
                ["mul"] = "multiply ",  // Note trailing spaces. You may not want those.
                ["div"] = "divide " ,
                ["sub"] = "subtract ",
                ["mod"] = "modulus "
                // etc
            };

            while (true)
            {
                string result = ReadLineWithKeywordExpansion(lookup);
                Console.WriteLine(result);
            }
        }

        public static string ReadLineWithKeywordExpansion(Dictionary<string, string> lookup)
        {
            var sb = new StringBuilder();
            string blank = new string(' ', Console.WindowWidth - 1);

            while (true)
            {
                var k = Console.ReadKey();

                if (k.Key == ConsoleKey.Enter)
                {
                    Console.WriteLine();
                    return sb.ToString();
                }
                else if (k.Key == ConsoleKey.Backspace)
                {
                    if (sb.Length > 0)
                        --sb.Length;
                }
                else if (k.Key == ConsoleKey.Tab)
                {
                    if (lookup.TryGetValue(lastChars(sb, 3), out var rep))
                    {
                        sb.Length -= 3;
                        sb.Append(rep);
                    }
                }
                else if (k.KeyChar != '[=10=]') // Ignore special keys.
                {
                    sb.Append(k.KeyChar);
                }

                Console.Write("\r" + blank);
                Console.Write("\r" + sb.ToString());
            }
        }
        
        /// <summary>Returns the last 'n' chars of a StringBuilder. </summary>

        static string lastChars(StringBuilder sb, int n)
        {
            n = Math.Min(n, sb.Length);
            char[] chars = new char[n];

            for (int i = 0; i < n; ++i)
                chars[i] = sb[i + sb.Length - n];

            return new string(chars);
        }
    }
}

请注意,为简洁起见,我已将关键字长度硬编码为 3。您可能需要更复杂的查找,但适用上述一般原则。

这是对 Matthew Watson 作品的回馈。

此版本应将光标保持在调用时所在的位置,并且不会擦除光标开始处的整行。

它还会查找当前输入中的最后一个 space,并将其右侧的所有内容用作查找的“候选”:

static void Main(string[] args)
{
    List<string> commands = new List<string>()
    {
       "add",
       "divide",
       "multiply",
       "subtract",
       "modulus"
    };
    Console.Write("Command: ");
    var cmd = ReadLineWithKeywordExpansion(commands);
    Console.Write("Press Enter to Quit");
    Console.ReadLine();
}

public static string ReadLineWithKeywordExpansion(List<string> commands)
{
    int top = Console.CursorTop;
    int left = Console.CursorLeft;

    var sb = new StringBuilder();           
    while (true)
    {
        var k = Console.ReadKey(true);
        if (k.Key == ConsoleKey.Enter)
        {
            Console.WriteLine();
            return sb.ToString();
        }
        else if (k.Key == ConsoleKey.Backspace)
        {
            if (sb.Length > 0)
            {
                --sb.Length;
                Console.SetCursorPosition(left, top);
                Console.Write(sb.ToString() + " ");
                Console.SetCursorPosition(left + sb.Length, top);
            }
        }
        else if (k.Key == ConsoleKey.Tab)
        {
            int index = 0;
            string candidate = sb.ToString();
            for(int i=(sb.Length-1); i>=0; i--)
            {
                if (sb[i]==' ')
                {
                    index = i+1;
                    candidate = sb.ToString().Substring(index);
                    break;
                }
            }
            if (candidate != "")
            {
                string command = commands.FirstOrDefault(cmd => cmd.StartsWith(candidate));
                if (command != null)
                {
                    sb.Length = index;
                    sb.Append(command);
                    Console.SetCursorPosition(left, top);
                    Console.Write(sb.ToString());
                }
            }                    
        }
        else if (k.KeyChar != '[=10=]') // Ignore special keys.
        {
            sb.Append(k.KeyChar);
            Console.SetCursorPosition(left, top);
            Console.Write(sb.ToString());
        }
    }
}