Files
RobustToolbox/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs
Pieter-Jan Briers 5057c91dcd Console command completions v1. (#2817)
* Console command completions v1.

I think it works™️

* Unify cvar commands

* Handle no-completions-at-all better.

* Don't crash if you tab complete while no completions available.

* Always show hints if available

* Properly null completion hint over the wire

* Unify help command, localize it.

* Clean up + localize cvar command.

* Remove debug logging

* List command unified & localized.

* Remove server completions debug logging

* Remote execute command.

Had to make everything async for this.

* Don't lower case enums or bools
Why

* GC commands converted and localized.

* Fix remote command completions.

Whoops

* Kick command completions

* lsasm unified & localized.

* Revert "Don't lower case enums or bools"

This reverts commit 2f825347c3.

* ToString gc_mode command enums instead of trying to fix Fluent.

Ah well.

* Unify szr_stats

* Unify log commands, completions

* Fix compile

* Improve completion with complex cases (quotes, escapes)

* Code cleanup, comments.

* Fix tab completion with empty arg ruining everything.

* Fix RegisteredCommand completions

* Add more complex completion options system.

* Refactor content directory entries into a proper resource manager API.

* Implement GetEntries for DirLoader

* Make type hint darker.

* Exec command autocomplete, pulled play global sound code out to engine.
2022-05-17 13:07:25 +10:00

350 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Input;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.Utility.Collections;
namespace Robust.Client.UserInterface.CustomControls;
public sealed partial class DebugConsole
{
private readonly DebugConsoleCompletion _compPopup;
// Last valid completion result we got.
private CompletionResult? _compCurResult;
// The parameter count for the above completion result.
// Used to immediately invalidate it if the amount changes.
private int _compParamCount;
// The filtered set of completions currently shown to the user.
private CompletionOption[]? _compFiltered;
// Which completion is currently selected, index into _compFiltered.
private int _compSelected;
// Vertical scroll offset of the completion list.
private int _compVerticalOffset;
// Used for sequencing to nicely handle out-of-order completion responses.
private int _compSeqSend;
private int _compSeqRecv;
private CancellationTokenSource _compCancel = new();
private void InitCompletions()
{
CommandBar.OnTextTyped += CommandBarOnOnTextTyped;
CommandBar.OnFocusExit += CommandBarOnOnFocusExit;
CommandBar.OnBackspace += CommandBarOnOnBackspace;
}
private void CommandBarOnOnFocusExit(LineEdit.LineEditEventArgs obj)
{
AbortActiveCompletions();
}
private void CommandBarOnOnTextTyped(GUITextEventArgs obj)
{
TypeUpdateCompletions(true);
}
private void CommandBarOnOnBackspace(LineEdit.LineEditBackspaceEventArgs eventArgs)
{
if (eventArgs.OldCursorPosition == 0 || eventArgs.OldSelectionStart != eventArgs.OldCursorPosition)
{
AbortActiveCompletions();
return;
}
if (CommandBar.CursorPosition == 0)
{
// Don't do completions if you have nothing typed.
AbortActiveCompletions();
return;
}
TypeUpdateCompletions(true);
}
private void AbortActiveCompletions()
{
_compCurResult = null;
_compFiltered = null;
_compSelected = 0;
_compVerticalOffset = 0;
_compPopup.Close();
_compCancel.Cancel();
_compCancel.Dispose();
_compCancel = new CancellationTokenSource();
}
private async void TypeUpdateCompletions(bool fullUpdate)
{
var (args, _, _) = CalcTypingArgs();
if (args.Count != _compParamCount)
{
_compParamCount = args.Count;
AbortActiveCompletions();
}
if (fullUpdate)
{
var seq = ++_compSeqSend;
var task = _consoleHost.GetCompletions(args, _compCancel.Token);
if (!task.IsCompleted)
{
// If we don't immediately get a result from the console (e.g. server command),
// we update the filtered immediately before asynchronously waiting on it.
UpdateFilteredCompletions();
// This means we only update completions once when running synchronously.
}
CompletionResult result;
try
{
result = await task;
}
catch (OperationCanceledException)
{
return;
}
if (seq < _compSeqRecv)
{
// Newer result already came before us.
return;
}
_compSeqRecv = seq;
_compCurResult = result;
}
UpdateFilteredCompletions();
}
private void UpdateFilteredCompletions()
{
if (_compCurResult == null)
return;
var (_, curTyping, _) = CalcTypingArgs();
var curSelected = _compFiltered?.Length > 0 ? _compFiltered[_compSelected] : default;
_compFiltered = FilterCompletions(_compCurResult.Options, curTyping);
if (curSelected == default)
{
_compSelected = 0;
}
else
{
var foundIdx = Array.IndexOf(_compFiltered, curSelected);
_compSelected = foundIdx > 0 ? foundIdx : 0;
}
FixScrollMargins();
// Logger.Debug($"Filtered completions: {string.Join(", ", _compFiltered)}");
UpdateCompletionsPopup();
}
private void UpdateCompletionsPopup()
{
if (_compFiltered == null)
return;
DebugTools.AssertNotNull(_compCurResult);
var (_, _, endRange) = CalcTypingArgs();
var offset = CommandBar.GetOffsetAtIndex(endRange.start);
// Logger.Debug($"Offset: {offset}");
_compPopup.Close();
_compPopup.Contents.RemoveAllChildren();
if (_compCurResult!.Hint != null)
{
var hint = _compCurResult.Hint;
_compPopup.Contents.AddChild(new Label
{
Text = hint,
FontColorOverride = Color.Gray
});
}
// Fill out list completions.
var maxCount = _cfg.GetCVar(CVars.ConCompletionCount);
var c = 0;
for (var i = _compVerticalOffset; i < _compFiltered.Length && c < maxCount; i++, c++)
{
var (value, hint, _) = _compFiltered[i];
var labelValue = new Label
{
Text = value,
FontColorOverride = i == _compSelected ? Color.White : Color.DarkGray
};
if (hint != null)
{
_compPopup.Contents.AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
labelValue,
new Label
{
Text = $" - {hint}",
FontColorOverride = Color.Gray
}
}
});
}
else
{
_compPopup.Contents.AddChild(labelValue);
}
}
if (_compPopup.Contents.ChildCount != 0)
{
_compPopup.Open(
UIBox2.FromDimensions(
offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
5, 5));
}
}
private (List<string> args, string curTyping, (int start, int end) lastRange) CalcTypingArgs()
{
var cursor = CommandBar.CursorPosition;
// Don't consider text after the cursor.
var text = CommandBar.Text.AsSpan(0, cursor);
var args = new List<string>();
var ranges = new ValueList<(int start, int end)>();
CommandParsing.ParseArguments(text, args, ref ranges);
if (args.Count == 0 || ranges[^1].end != text.Length)
args.Add("");
(int, int) lastRange;
if (ranges.Count == 0)
lastRange = default;
else if (ranges.Count == args.Count)
lastRange = ranges[^1];
else
lastRange = (cursor, cursor);
return (args, args[^1], lastRange);
}
private CompletionOption[] FilterCompletions(IEnumerable<CompletionOption> completions, string curTyping)
{
return completions
.Where(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase))
.ToArray();
}
private void CompletionKeyDown(GUIBoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.TextTabComplete)
{
if (_compFiltered != null && _compSelected < _compFiltered.Length)
{
// Figure out typing word so we know how much to replace.
var (completion, _, completionFlags) = _compFiltered[_compSelected];
var (_, _, lastRange) = CalcTypingArgs();
// Replace the full word from the start.
// This means that letter casing will match the completion suggestion.
CommandBar.CursorPosition = lastRange.end;
CommandBar.SelectionStart = lastRange.start;
var insertValue = CommandParsing.Escape(completion);
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
insertValue += " ";
CommandBar.InsertAtCursor(insertValue);
TypeUpdateCompletions(true);
args.Handle();
}
return;
}
if (args.Function == EngineKeyFunctions.TextCompleteNext)
{
if (_compFiltered == null)
return;
args.Handle();
var len = _compFiltered.Length;
var pos = (_compSelected + 1) % len;
_compSelected = pos;
FixScrollMargins();
UpdateCompletionsPopup();
return;
}
if (args.Function == EngineKeyFunctions.TextCompletePrev)
{
if (_compFiltered == null)
return;
args.Handle();
var len = _compFiltered.Length;
var pos = MathHelper.Mod(_compSelected - 1, len);
_compSelected = pos;
FixScrollMargins();
UpdateCompletionsPopup();
return;
}
}
private void FixScrollMargins()
{
if (_compFiltered == null)
return;
var maxCount = _cfg.GetCVar(CVars.ConCompletionCount);
var showCount = Math.Min(maxCount, _compFiltered.Length);
var margin = _cfg.GetCVar(CVars.ConCompletionMargin);
var posBottom = showCount + _compVerticalOffset - margin;
if (_compSelected >= posBottom)
_compVerticalOffset = Math.Min(_compFiltered.Length - showCount, _compSelected + 1 + margin - showCount);
if (_compSelected < _compVerticalOffset + margin)
_compVerticalOffset = Math.Max(0, _compSelected - margin);
}
private void CompletionCommandEntered()
{
AbortActiveCompletions();
}
}