Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebirth of Auto-complete Support #962

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/deploy-local-smapi.targets
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This assumes `find-game-folder.targets` has already been imported and validated.
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Pintail.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\ConsoleWrapperLib.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />

<!-- Harmony + dependencies -->
Expand Down
30 changes: 22 additions & 8 deletions src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,15 @@ public void WriteLine(string message, ConsoleLogLevel level)
{
if (level == ConsoleLogLevel.Critical)
{
Console.BackgroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(message);
Console.ResetColor();
this.WriteLineImpl(message, ConsoleColor.White, ConsoleColor.Red);
}
else
{
Console.ForegroundColor = this.Colors[level];
Console.WriteLine(message);
Console.ResetColor();
this.WriteLineImpl(message, this.Colors[level], null);
}
}
else
Console.WriteLine(message);
this.WriteLineImpl(message, null, null);
}

/// <summary>Get the default color scheme config for cases where it's not configurable (e.g. the installer).</summary>
Expand Down Expand Up @@ -103,6 +98,25 @@ public static ColorSchemeConfig GetDefaultColorSchemeConfig(MonitorColorScheme u
}


/*********
** Private methods
*********/
/// <summary>
/// Implementation of writing a line to the console, virtual to allow for other console implementations.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="foregroundColor">The foreground color to override the default with, if any.</param>
/// <param name="backgroundColor">The background color to override the default with, if any.</param>
protected virtual void WriteLineImpl(string message, ConsoleColor? foregroundColor, ConsoleColor? backgroundColor)
{
if (backgroundColor.HasValue)
Console.BackgroundColor = backgroundColor.Value;
if (foregroundColor.HasValue)
Console.ForegroundColor = foregroundColor.Value;
Console.WriteLine(message);
Console.ResetColor();
}

/*********
** Private methods
*********/
Expand Down
7 changes: 6 additions & 1 deletion src/SMAPI/Framework/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ internal class Command
/// <summary>The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</summary>
public Action<string, string[]> Callback { get; }

/// <summary>The method to invoke for auto-complete handling. This method is passed the command name and current input, and should return the potential matches.</summary>
public Func<string, string, string[]>? AutoCompleteHandler { get; }


/*********
** Public methods
Expand All @@ -29,12 +32,14 @@ internal class Command
/// <param name="name">The command name, which the user must type to trigger it.</param>
/// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
/// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
public Command(IModMetadata? mod, string name, string documentation, Action<string, string[]> callback)
/// <param name="autoCompleteHandler">The method to invoke for auto-complete handling. This method is passed the command name and current input, and should return the potential matches.</param>
public Command(IModMetadata? mod, string name, string documentation, Action<string, string[]> callback, Func<string, string, string[]>? autoCompleteHandler)
{
this.Mod = mod;
this.Name = name;
this.Documentation = documentation;
this.Callback = callback;
this.AutoCompleteHandler = autoCompleteHandler;
}
}
}
48 changes: 46 additions & 2 deletions src/SMAPI/Framework/CommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ public CommandManager(IMonitor monitor)
/// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
/// <exception cref="ArgumentException">There's already a command with that name.</exception>
public CommandManager Add(IModMetadata? mod, string name, string documentation, Action<string, string[]> callback)
{
return this.Add(mod, name, documentation, callback, null);
}

/// <summary>Add a console command.</summary>
/// <param name="mod">The mod adding the command (or <c>null</c> for a SMAPI command).</param>
/// <param name="name">The command name, which the user must type to trigger it.</param>
/// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
/// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
/// <param name="autoCompleteHandler">The method to invoke for auto-complete handling. This method is passed the command name and current input, and should return the potential matches. The matches should all start with the last space-separated string of input.</param>
/// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
/// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
/// <exception cref="ArgumentException">There's already a command with that name.</exception>
public CommandManager Add(IModMetadata? mod, string name, string documentation, Action<string, string[]> callback, Func<string, string, string[]>? autoCompleteHandler)
{
name = this.GetNormalizedName(name)!; // null-checked below

Expand All @@ -55,7 +69,7 @@ public CommandManager Add(IModMetadata? mod, string name, string documentation,
throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name.");

// add command
this.Commands.Add(name, new Command(mod, name, documentation, callback));
this.Commands.Add(name, new Command(mod, name, documentation, callback, autoCompleteHandler));
return this;
}

Expand All @@ -65,7 +79,7 @@ public CommandManager Add(IModMetadata? mod, string name, string documentation,
/// <exception cref="ArgumentException">There's already a command with that name.</exception>
public CommandManager Add(IInternalCommand command, IMonitor monitor)
{
return this.Add(null, command.Name, command.Description, (_, args) => command.HandleCommand(args, monitor));
return this.Add(null, command.Name, command.Description, (_, args) => command.HandleCommand(args, monitor), (_, input) => command.HandleAutocomplete(input, monitor));
}

/// <summary>Get a command by its unique name.</summary>
Expand Down Expand Up @@ -138,6 +152,36 @@ public bool TryParse(string? input, [NotNullWhen(true)] out string? name, [NotNu
return this.Commands.TryGetValue(name, out command);
}

/// <summary>
/// Handle autocompletion results.
/// </summary>
/// <param name="input">The input string to autocomplete for.</param>
/// <returns>An array of matches for the input.</returns>
public string[] HandleAutocomplete(string input)
{
int space = input.IndexOf(' ');
if (space == -1)
{
List<string> matches = new();
foreach (string cmd in this.Commands.Keys)
{
if (cmd.StartsWith(input))
matches.Add(cmd);
}
return matches.ToArray();
}
else
{
string currCmd = input.Substring(0, space);
if (!this.Commands.TryGetValue(currCmd, out Command? cmd) || cmd.AutoCompleteHandler == null)
{
return Array.Empty<string>();
}

return cmd.AutoCompleteHandler(currCmd, input.Substring(space + 1));
}
}


/*********
** Private methods
Expand Down
21 changes: 21 additions & 0 deletions src/SMAPI/Framework/Commands/HelpCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace StardewModdingAPI.Framework.Commands
Expand Down Expand Up @@ -72,5 +74,24 @@ public void HandleCommand(string[] args, IMonitor monitor)
monitor.Log(message, LogLevel.Info);
}
}

/// <summary>Handle the console command auto-complete when requested by the user..</summary>
/// <param name="input">The current input.</param>
/// <param name="monitor">Writes messages to the console.</param>
public string[] HandleAutocomplete(string input, IMonitor monitor)
{
if (input.Contains(' '))
return Array.Empty<string>();

var allCommandNames = this.CommandManager.GetAll().Select(cmd => cmd.Name);

List<string> ret = new();
foreach (string name in allCommandNames)
{
if (name.StartsWith(input))
ret.Add(name);
}
return ret.ToArray();
}
}
}
11 changes: 11 additions & 0 deletions src/SMAPI/Framework/Commands/IInternalCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace StardewModdingAPI.Framework.Commands
{
/// <summary>A core SMAPI console command.</summary>
Expand All @@ -20,5 +22,14 @@ interface IInternalCommand
/// <param name="args">The command arguments.</param>
/// <param name="monitor">Writes messages to the console.</param>
void HandleCommand(string[] args, IMonitor monitor);

/// <summary>Handle the console command auto-complete when requested by the user..</summary>
/// <param name="input">The current input.</param>
/// <param name="monitor">Writes messages to the console.</param>
string[] HandleAutocomplete(string input, IMonitor monitor)
{
// Default implementation for if a command doesn't support it.
return Array.Empty<string>();
}
}
}
32 changes: 32 additions & 0 deletions src/SMAPI/Framework/ConsoleWrapperConsoleLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using ConsoleWrapperLib;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewModdingAPI.Internal.ConsoleWriting
{
/// <summary>Writes color-coded text to a ConsoleWrapper object.</summary>
internal class ConsoleWrapperConsoleWriter : ColorfulConsoleWriter
{
/// <summary>The console wrapper object to use, if avaiable.</summary>
public ConsoleWrapper? ConsoleWrapper { get; set; }

/// <summary>Construct an instance.</summary>
/// <param name="platform">The target platform.</param>
/// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
public ConsoleWrapperConsoleWriter(Platform platform, ColorSchemeConfig colorConfig)
: base(platform, colorConfig)
{
}

/// <inheritdoc/>
protected override void WriteLineImpl(string message, ConsoleColor? foregroundColor, ConsoleColor? backgroundColor)
{
if (this.ConsoleWrapper != null)
this.ConsoleWrapper.WriteLine(message, foregroundColor ?? this.ConsoleWrapper.DefaultForeground, backgroundColor ?? this.ConsoleWrapper.DefaultBackground);
else
base.WriteLineImpl(message, foregroundColor, backgroundColor);
}
}
}
39 changes: 36 additions & 3 deletions src/SMAPI/Framework/Logging/LogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Text;
using System.Threading;
using ConsoleWrapperLib;
using StardewModdingAPI.Framework.Commands;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModLoading;
Expand All @@ -25,6 +26,16 @@ internal class LogManager : IDisposable
/// <summary>The log file to which to write messages.</summary>
private readonly LogFileManager LogFile;

/// <summary>If we're in legacy mode or not.</summary>
[MemberNotNullWhen(false, nameof(ConsoleWrapper))]
private bool LegacyMode { get; }

/// <summary>The console wrapper object.</summary>
private ConsoleWrapper? ConsoleWrapper;

/// <summary>The console writer object.</summary>
private IConsoleWriter ConsoleWriter;

/// <summary>Create a monitor instance given the ID and name.</summary>
private readonly Func<string, string, Monitor> GetMonitorImpl;

Expand All @@ -51,14 +62,28 @@ internal class LogManager : IDisposable
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
/// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param>
/// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
/// <param name="legacyMode">Whether to use legacy mode or not, which enables auto completion and moves user input to always be at the bottom of the console.</param>
/// <param name="getScreenIdForLog">Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</param>
public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, HashSet<string> verboseLogging, bool isDeveloperMode, Func<int?> getScreenIdForLog)
public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, HashSet<string> verboseLogging, bool isDeveloperMode, bool legacyMode, Func<int?> getScreenIdForLog)
{
// init log file
this.LogFile = new LogFileManager(logPath);

// save legacy mode value
this.LegacyMode = legacyMode;

// init console
if (!this.LegacyMode)
{
this.ConsoleWriter = new ConsoleWrapperConsoleWriter(Constants.Platform, colorConfig);
}
else
{
this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig);
}

// init monitor
this.GetMonitorImpl = (id, name) => new Monitor(name, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog)
this.GetMonitorImpl = (id, name) => new Monitor(name, this.LogFile, this.ConsoleWriter, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog)
{
WriteToConsole = writeToConsole,
ShowTraceInConsole = isDeveloperMode,
Expand Down Expand Up @@ -104,13 +129,21 @@ public void RunConsoleInputLoop(CommandManager commandManager, Action reloadTran
.Add(new HarmonySummaryCommand(), this.Monitor)
.Add(new ReloadI18nCommand(reloadTranslations), this.Monitor);

if (!this.LegacyMode)
{
this.ConsoleWrapper = new ConsoleWrapper();
this.ConsoleWrapper.AutoCompleteHandler = commandManager.HandleAutocomplete;
if (this.ConsoleWriter is ConsoleWrapperConsoleWriter consoleWrapperWriter)
consoleWrapperWriter.ConsoleWrapper = this.ConsoleWrapper;
}

// start handling command line input
Thread inputThread = new(() =>
{
while (true)
{
// get input
string? input = Console.ReadLine();
string? input = (this.ConsoleWrapper != null) ? this.ConsoleWrapper.ReadLine() : Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
continue;

Expand Down
6 changes: 6 additions & 0 deletions src/SMAPI/Framework/ModHelpers/CommandHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,11 @@ public ICommandHelper Add(string name, string documentation, Action<string, stri
this.CommandManager.Add(this.Mod, name, documentation, callback);
return this;
}
/// <inheritdoc />
public ICommandHelper Add(string name, string documentation, Action<string, string[]> callback, Func<string, string, string[]> autoCompleteHandler)
{
this.CommandManager.Add(this.Mod, name, documentation, callback, autoCompleteHandler);
return this;
}
}
}
Loading