mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
* 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.
350 lines
10 KiB
C#
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();
|
|
}
|
|
}
|