Files
RobustToolbox/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.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

287 lines
8.4 KiB
C#

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.CustomControls
{
public interface IDebugConsoleView
{
/// <summary>
/// Write a line with a specific color to the console window.
/// </summary>
void AddLine(string text, Color color);
void AddLine(string text);
void AddFormattedLine(FormattedMessage message);
void Clear();
}
// Quick note on how thread safety works in here:
// Messages from other threads are not actually immediately drawn. They're stored in a queue.
// Every frame OR the next time a message on the main thread comes in, this queue is drained.
// This keeps thread safety while still making it so messages are ordered how they come in.
// And also if Update() stops firing due to an exception loop the console will still work.
// (At least from the main thread, which is what's throwing the exceptions..)
[GenerateTypedNameReferences]
public sealed partial class DebugConsole : Control, IDebugConsoleView
{
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private static readonly ResourcePath HistoryPath = new("/debug_console_history.json");
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
public DebugConsole()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
InitCompletions();
CommandBar.OnTextChanged += OnCommandChanged;
CommandBar.OnKeyBindDown += CommandBarOnOnKeyBindDown;
CommandBar.OnTextEntered += CommandEntered;
CommandBar.OnHistoryChanged += OnHistoryChanged;
_loadHistoryFromDisk();
_compPopup = new DebugConsoleCompletion();
}
protected override void EnteredTree()
{
base.EnteredTree();
_consoleHost.AddString += OnAddString;
_consoleHost.AddFormatted += OnAddFormatted;
_consoleHost.ClearText += OnClearText;
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
}
protected override void ExitedTree()
{
base.ExitedTree();
_consoleHost.AddString -= OnAddString;
_consoleHost.AddFormatted -= OnAddFormatted;
_consoleHost.ClearText -= OnClearText;
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
}
private void OnClearText(object? _, EventArgs args)
{
Clear();
}
private void OnAddFormatted(object? _, AddFormattedMessageArgs args)
{
AddFormattedLine(args.Message);
}
private void OnAddString(object? _, AddStringArgs args)
{
AddLine(args.Text, DetermineColor(args.Local, args.Error));
}
private Color DetermineColor(bool local, bool error)
{
return Color.White;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
_flushQueue();
}
private void CommandEntered(LineEdit.LineEditEventArgs args)
{
if (!string.IsNullOrWhiteSpace(args.Text))
{
_consoleHost.ExecuteCommand(args.Text);
CommandBar.Clear();
CompletionCommandEntered();
}
// commandChanged = true;
}
private void OnHistoryChanged()
{
_flushHistoryToDisk();
}
public void AddLine(string text, Color color)
{
var formatted = new FormattedMessage(3);
formatted.PushColor(color);
formatted.AddText(text);
formatted.Pop();
AddFormattedLine(formatted);
}
public void AddLine(string text)
{
AddLine(text, Color.White);
}
public void AddFormattedLine(FormattedMessage message)
{
_messageQueue.Enqueue(message);
}
public void Clear()
{
Output.Clear();
}
private void _addFormattedLineInternal(FormattedMessage message)
{
Output.AddMessage(message);
}
private void _flushQueue()
{
while (_messageQueue.TryDequeue(out var message))
{
_addFormattedLineInternal(message);
}
}
private void CommandBarOnOnKeyBindDown(GUIBoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.TextScrollToBottom)
{
Output.ScrollToBottom();
args.Handle();
return;
}
CompletionKeyDown(args);
}
private void OnCommandChanged(LineEdit.LineEditEventArgs args)
{
// commandChanged = true;
}
private async void _loadHistoryFromDisk()
{
var sawmill = Logger.GetSawmill("dbgconsole");
var data = await Task.Run(async () =>
{
Stream? stream = null;
for (var i = 0; i < 3; i++)
{
try
{
stream = _resourceManager.UserData.OpenRead(HistoryPath);
break;
}
catch (FileNotFoundException)
{
// Nada, nothing to load in that case.
return null;
}
catch (IOException)
{
// File locked probably??
await Task.Delay(10);
}
}
if (stream == null)
{
sawmill.Warning("Failed to load debug console history!");
return null;
}
try
{
return await JsonSerializer.DeserializeAsync<string[]>(stream);
}
catch (Exception e)
{
sawmill.Warning($"Failed to load debug console history due to exception!\n{e}");
return null;
}
finally
{
// ReSharper disable once MethodHasAsyncOverload
stream.Dispose();
}
});
if (data == null)
return;
CommandBar.ClearHistory();
CommandBar.History.AddRange(data);
CommandBar.HistoryIndex = CommandBar.History.Count;
}
private async void _flushHistoryToDisk()
{
CommandBar.HistoryIndex = CommandBar.History.Count;
var sawmill = Logger.GetSawmill("dbgconsole");
var newHistory = JsonSerializer.Serialize(CommandBar.History);
await Task.Run(async () =>
{
StreamWriter? writer = null;
for (var i = 0; i < 3; i++)
{
try
{
writer = _resourceManager.UserData.OpenWriteText(HistoryPath);
break;
}
catch (IOException)
{
// Probably locking.
await Task.Delay(10);
}
}
if (writer == null)
{
sawmill.Warning("Failed to save debug console history!");
return;
}
// ReSharper disable once UseAwaitUsing
using (writer)
{
// ReSharper disable once MethodHasAsyncOverload
writer.Write(newHistory);
}
});
}
}
}