private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true);
private void MenuCompleteImpl(Menu menu, CommandCompletion completions) { var menuStack = new Stack <Menu>(10); menuStack.Push(null); // Used to help detect excess backspaces RemoveEditsAfterUndo(); var undoPoint = _edits.Count; bool undo = false; int savedUserMark = _mark; _visualSelectionCommandCount++; // Get the unambiguous prefix, possibly removing the first quote var userCompletionText = GetUnambiguousPrefix(menu.MenuItems, out var ambiguous); if (userCompletionText.Length > 0) { var c = userCompletionText[0]; if (IsSingleQuote(c) || IsDoubleQuote(c)) { userCompletionText = userCompletionText.Substring(1); } } var userInitialCompletionLength = userCompletionText.Length; completions.CurrentMatchIndex = 0; menu.DrawMenu(null); bool processingKeys = true; int previousSelection = -1; while (processingKeys) { if (menu.CurrentSelection != previousSelection) { var currentMenuItem = menu.CurrentMenuItem; int curPos = FindUserCompletionTextPosition(currentMenuItem, userCompletionText); if (userCompletionText.Length == 0 && (IsSingleQuote(currentMenuItem.CompletionText[0]) || IsDoubleQuote(currentMenuItem.CompletionText[0]))) { curPos++; } // set mark to the end of UserCompletion but in real completion (because of .\ and so on) _mark = completions.ReplacementIndex + curPos + userCompletionText.Length; DoReplacementForCompletion(currentMenuItem, completions); ExchangePointAndMark(); // After replacement, the menu might be misplaced from the command line // getting shorter or longer. var endOfCommandLine = ConvertOffsetToPoint(_buffer.Length); var topAdjustment = (endOfCommandLine.Y + 1) - menu.Top; if (topAdjustment != 0) { menu.Top += topAdjustment; menu.DrawMenu(null); } if (topAdjustment > 0) { // Render did not clear the rest of the command line which flowed // into the menu, so we must do that here. _console.SaveCursor(); _console.SetCursorPosition(endOfCommandLine.X, endOfCommandLine.Y); _console.Write(Spaces(_console.BufferWidth - endOfCommandLine.X)); _console.RestoreCursor(); } if (previousSelection != -1) { if (menu.ToolTipLines > 0) { // Erase previous tooltip, taking into account if the menu moved up/down. WriteBlankLines(menu.Top + menu.Rows, -topAdjustment + menu.ToolTipLines); } menu.UpdateMenuSelection(previousSelection, /*select*/ false, /*showToolTips*/ false, VTColorUtils.AsEscapeSequence(Options.EmphasisColor)); } menu.UpdateMenuSelection(menu.CurrentSelection, /*select*/ true, Options.ShowToolTips, VTColorUtils.AsEscapeSequence(Options.EmphasisColor)); previousSelection = menu.CurrentSelection; } var nextKey = ReadKey(); if (nextKey.EqualsNormalized(Keys.RightArrow)) { menu.MoveRight(); } else if (nextKey.EqualsNormalized(Keys.LeftArrow)) { menu.MoveLeft(); } else if (nextKey.EqualsNormalized(Keys.DownArrow)) { menu.MoveDown(); } else if (nextKey.EqualsNormalized(Keys.UpArrow)) { menu.MoveUp(); } else if (nextKey.EqualsNormalized(Keys.PageDown)) { menu.MovePageDown(); } else if (nextKey.EqualsNormalized(Keys.PageUp)) { menu.MovePageUp(); } else if (nextKey.EqualsNormalized(Keys.Tab)) { // Search for possible unambiguous common prefix. string unAmbiguousText = GetUnambiguousPrefix(menu.MenuItems, out ambiguous); int userComplPos = unAmbiguousText.IndexOf(userCompletionText, StringComparison.OrdinalIgnoreCase); // ... If found - advance IncrementalCompletion ... if (unAmbiguousText.Length > 0 && userComplPos >= 0 && unAmbiguousText.Length > (userComplPos + userCompletionText.Length)) { userCompletionText = unAmbiguousText.Substring(userComplPos); _current = completions.ReplacementIndex + FindUserCompletionTextPosition(menu.MenuItems[menu.CurrentSelection], userCompletionText) + userCompletionText.Length; Render(); Ding(); } // ... if no - usual Tab behaviour else { menu.MoveN(1); } } else if (nextKey.EqualsNormalized(Keys.ShiftTab)) { menu.MoveN(-1); } else if (nextKey.EqualsNormalized(Keys.CtrlG) || nextKey.EqualsNormalized(Keys.Escape)) { undo = true; processingKeys = false; _visualSelectionCommandCount = 0; _mark = savedUserMark; } else if (nextKey.EqualsNormalized(Keys.Backspace)) { // TODO: Shift + Backspace does not fail here? if (menuStack.Count > 1) { var newMenu = menuStack.Pop(); newMenu.DrawMenu(menu); previousSelection = -1; menu = newMenu; userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); } else if (menuStack.Count == 1) { Ding(); Debug.Assert(menuStack.Peek() == null, "sentinel value expected"); // Pop so the next backspace sends us to the else block and out of the loop. menuStack.Pop(); } else { processingKeys = false; undo = true; _visualSelectionCommandCount = 0; _mark = savedUserMark; PrependQueuedKeys(nextKey); } } else { bool prependNextKey = false; int cursorAdjustment = 0; bool truncateCurrentCompletion = false; bool keepSelection = false; var currentMenuItem = menu.CurrentMenuItem; if (IsDoneWithCompletions(currentMenuItem, nextKey)) { processingKeys = false; ExchangePointAndMark(); // cursor to the end of Completion if (!nextKey.EqualsNormalized(Keys.Enter)) { if (currentMenuItem.ResultType == CompletionResultType.ProviderContainer) { userCompletionText = GetUnquotedText( GetReplacementTextForDirectory(currentMenuItem.CompletionText, ref cursorAdjustment), consistentQuoting: false); } else { userCompletionText = GetUnquotedText(currentMenuItem, consistentQuoting: false); } // do not append the same char as last char in CompletionText (works for for '(', '\') prependNextKey = userCompletionText[userCompletionText.Length - 1] != nextKey.KeyChar; } } else if (nextKey.KeyChar > 0 && !char.IsControl(nextKey.KeyChar)) { userCompletionText += nextKey.KeyChar; // filter out matches and redraw menu var newMatches = FilterCompletions(completions, userCompletionText); if (newMatches.Count > 0) { var newMenu = CreateCompletionMenu(newMatches); newMenu.DrawMenu(menu); previousSelection = -1; // Remember the current menu for when we see Backspace. menu.ToolTipLines = 0; if (menuStack.Count == 0) { // The user hit backspace before there were any items on the stack // and we removed the sentinel - so put it back now. menuStack.Push(null); } menuStack.Push(menu); menu = newMenu; } else { processingKeys = false; prependNextKey = true; // we exit loop with current completion up to cursor truncateCurrentCompletion = true; if (userInitialCompletionLength == 0) { undo = true; } } } else // exit with any other Key chord { processingKeys = false; prependNextKey = true; // without this branch experience doesnt look naturally if (_dispatchTable.TryGetValue(nextKey, out var handler) && ( handler.Action == CopyOrCancelLine || handler.Action == Cut || handler.Action == DeleteChar || handler.Action == Paste ) ) { keepSelection = true; } } if (!processingKeys) // time to exit loop { if (truncateCurrentCompletion && !undo) { CompletionResult r = new CompletionResult(currentMenuItem .CompletionText.Substring(0, _current - completions.ReplacementIndex)); DoReplacementForCompletion(r, completions); } if (keepSelection) { _visualSelectionCommandCount = 1; } else { _visualSelectionCommandCount = 0; // if mark was set after cursor, it restored in uninspected position, because text before mark now longer // should we correct it ? I think not, beause any other text insertion does not correct it _mark = savedUserMark; } // without render all key chords that just move cursor leave selection visible, but it can be wrong if (!undo && !keepSelection) { Render(); } if (prependNextKey) { _current -= cursorAdjustment; PrependQueuedKeys(nextKey); } } } } menu.Clear(); var lastInsert = ((GroupedEdit)_edits[_edits.Count - 1])._groupedEditItems[1]; Debug.Assert(lastInsert is EditItemInsertString, "The only edits possible here are pairs of Delete/Insert"); var firstDelete = ((GroupedEdit)_edits[undoPoint])._groupedEditItems[0]; Debug.Assert(firstDelete is EditItemDelete, "The only edits possible here are pairs of Delete/Insert"); var groupEditCount = _edits.Count - undoPoint; _edits.RemoveRange(undoPoint, groupEditCount); _undoEditIndex = undoPoint; if (undo) { // Pretend it never happened. lastInsert.Undo(); firstDelete.Undo(); Render(); } else { // Leave one edit instead of possibly many to undo SaveEditItem(GroupedEdit.Create(new List <EditItem> { firstDelete, lastInsert })); } }