Files
RobustToolbox/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs
dcde669baa [Toolshed] Array parsing (#6864)
* Add parsing for arrays of values via ValueArray<T>.

* oops forgot command description

* add valarr to engine toolshed perms, fix stupidness, remove useless code in parser

* make optional not suck

* support for skipping optional parameters

* Switch to List<T> because I'm dumb and that just works™

* remove optional stuff to separate into its own PR

* Remove ValArr command

`valarr int [ 1, 2 ]` can just be replaced with ` val List<int> [ 1, 2 ]` or ` val int[] [ 1, 2 ]`

* Fix namespace

* Add support for array parsing

* Make ListTypeParser output nullable

* Pass ListLengthAttribute in the CommandArgument struct

The current method of trying to fetch the attribute for argument doesn't actually work. i.e., the argIndex calculation isn't always correct. This is still kinda janky and I hate it, but it should work better.

* Fix empty list parsing, add tests

* rename flag, and clarify description

* More tests

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2026-08-22 21:29:33 +00:00

407 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Console;
using Robust.Shared.Input;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
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.OnFocusExit += CommandBarOnOnFocusExit;
CommandBar.OnTextChanged += CommandBarOnTextChanged;
}
private void CommandBarOnOnFocusExit(LineEdit.LineEditEventArgs obj)
{
// Clicking a completion entry moves keyboard focus away from the command bar before the entry receives its
// click. Keep the displayed result alive long enough for that entry to insert it.
_compPopup.Close();
CancelActiveCompletionRequest();
}
private void CommandBarOnTextChanged(LineEdit.LineEditEventArgs args)
{
if (args.Text.Length == 0)
{
AbortActiveCompletions();
return;
}
TypeUpdateCompletions(true);
}
private void AbortActiveCompletions()
{
_compCurResult = null;
_compFiltered = null;
_compSelected = 0;
_compVerticalOffset = 0;
_compPopup.Close();
CancelActiveCompletionRequest();
}
private void CancelActiveCompletionRequest()
{
_compCancel.Cancel();
_compCancel.Dispose();
_compCancel = new CancellationTokenSource();
}
private async void TypeUpdateCompletions(bool fullUpdate)
{
var (args, _, _, str) = CalcTypingArgs();
if (args.Count != _compParamCount)
{
_compParamCount = args.Count;
AbortActiveCompletions();
}
if (fullUpdate)
{
var seq = ++_compSeqSend;
var task = _consoleHost.GetCompletions(args, str, _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 completionIndex = i;
var labelValue = new Label
{
Text = value,
FontColorOverride = i == _compSelected ? Color.White : Color.DarkGray
};
var entry = new ContainerButton
{
MuteSounds = true,
HorizontalExpand = true,
DefaultCursorShape = Control.CursorShape.Hand,
};
entry.OnButtonDown += _ =>
{
CommandBar.GrabKeyboardFocus();
if (InsertCompletion(completionIndex))
TypeUpdateCompletions(true);
};
Label? labelHint = null;
entry.OnMouseEntered += _ =>
{
labelValue.FontColorOverride = Color.White;
if (labelHint != null)
labelHint.FontColorOverride = Color.White;
};
entry.OnMouseExited += _ =>
{
labelValue.FontColorOverride = completionIndex == _compSelected ? Color.White : Color.DarkGray;
if (labelHint != null)
labelHint.FontColorOverride = Color.Gray;
};
var entryContents = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children = { labelValue },
};
if (hint != null)
{
labelHint = new Label
{
Text = $" - {hint}",
FontColorOverride = Color.Gray
};
entryContents.AddChild(labelHint);
}
entry.AddChild(entryContents);
_compPopup.Contents.AddChild(entry);
}
if (_compPopup.Contents.ChildCount != 0)
{
var box = UIBox2.FromDimensions(
offset - _compPopup.Contents.Margin.Left,
CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
5,
5);
var altPosUp = new Vector2(offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y);
_compPopup.Open(box, altPosUp: altPosUp);
}
}
private (List<string> args, string curTyping, (int start, int end) lastRange, string argStr) 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, text.ToString());
}
private CompletionOption[] FilterCompletions(IEnumerable<CompletionOption> completions, string curTyping)
{
return completions
.Where(c => c.Value.Contains(curTyping, StringComparison.CurrentCultureIgnoreCase) || (c.Flags & CompletionOptionFlags.NoFilter) != 0x0)
.OrderByDescending(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase))
.ToArray();
}
private void CompletionKeyDown(GUIBoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.TextTabComplete)
{
if (InsertCompletion(_compSelected))
args.Handle();
return;
}
if (args.Function == EngineKeyFunctions.TextCompleteNext)
{
if (_compFiltered == null || _compFiltered.Length == 0)
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 || _compFiltered.Length == 0)
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);
}
/// <summary>
/// Inserts a displayed completion using the same escaping and quoting rules for mouse and keyboard selection.
/// </summary>
private bool InsertCompletion(int index)
{
if (_compFiltered == null || index < 0 || index >= _compFiltered.Length)
return false;
// Figure out typing word so we know how much to replace.
var (completion, _, completionFlags) = _compFiltered[index];
var (_, _, lastRange, _) = CalcTypingArgs();
if ((completionFlags & CompletionOptionFlags.AppendOnly) != 0)
{
CommandBar.SelectionStart = CommandBar.CursorPosition;
}
else
{
// 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 = (completionFlags & CompletionOptionFlags.NoEscape) == 0
? CommandParsing.Escape(completion)
: completion;
// If the replacement contains a space, we must quote it to treat it as a single argument.
var mustQuote = (completionFlags & CompletionOptionFlags.NoQuote) == 0 && insertValue.Contains(' ');
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
{
if (mustQuote)
insertValue = $"\"{insertValue}\"";
insertValue += " ";
}
else if (mustQuote)
{
// If it's a partial completion, only quote the start.
insertValue = '"' + insertValue;
}
CommandBar.InsertAtCursor(insertValue);
return true;
}
private void CompletionCommandEntered()
{
AbortActiveCompletions();
}
}