From e163d47aefd0118b5d2965f2210ef77614ba8ba2 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:53:43 -0300 Subject: [PATCH 1/2] Removed thrown errors in favor of strings. Add tabbing --- Silkstring/Models/AliasEntry.cs | 13 +++--- Silkstring/Services/AliasValidator.cs | 6 +-- Silkstring/Services/CommandHandler.cs | 13 +----- Silkstring/Services/Conditions/Condition.cs | 11 +++++ .../Services/Conditions/ConditionEvaluator.cs | 6 +-- .../Services/Conditions/ConditionException.cs | 8 ---- Silkstring/Services/Conditions/Parser.cs | 46 ++++++++++--------- Silkstring/Services/Conditions/Tokenizer.cs | 12 ++--- Silkstring/UI/Panels/AliasEditPanel.cs | 2 + Silkstring/UI/SilkstringHighlighter.cs | 6 +-- Tests/ConditionEvaluatorTests.cs | 2 +- extern/DalamudTextEdit | 2 +- 12 files changed, 57 insertions(+), 70 deletions(-) create mode 100644 Silkstring/Services/Conditions/Condition.cs delete mode 100644 Silkstring/Services/Conditions/ConditionException.cs diff --git a/Silkstring/Models/AliasEntry.cs b/Silkstring/Models/AliasEntry.cs index ca6aa09..c83ed57 100644 --- a/Silkstring/Models/AliasEntry.cs +++ b/Silkstring/Models/AliasEntry.cs @@ -19,6 +19,8 @@ public class AliasEntry public string DisplayName = string.Empty; public string Name = string.Empty; + + [JsonIgnore] public string[] triggers => Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); [JsonIgnore] public string EffectiveName => string.IsNullOrWhiteSpace(DisplayName) ? Name : DisplayName; public bool Enabled = true; public List Output = new(); @@ -32,14 +34,13 @@ public AliasEntry() public bool IsValid() { - var names = Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (names.Length == 0) return false; + if (triggers.Length == 0) return false; - foreach (var name in names) + foreach (var trigger in triggers) { - if (Blacklist.Contains(name)) return false; - if (name.Contains(' ')) return false; - if (name.Contains('/')) return false; + if (Blacklist.Contains(trigger)) return false; + if (trigger.Contains(' ')) return false; + if (trigger.Contains('/')) return false; } return Output.Any(command => !string.IsNullOrWhiteSpace(command.Command)); } diff --git a/Silkstring/Services/AliasValidator.cs b/Silkstring/Services/AliasValidator.cs index 8b89311..9b7635f 100644 --- a/Silkstring/Services/AliasValidator.cs +++ b/Silkstring/Services/AliasValidator.cs @@ -26,8 +26,7 @@ public static List FindCycle(AliasEntry target, IEnumerable switch (kind) { case BlockKind.If: - try { new Parser(Tokenizer.Tokenize(expression)).Parse(); } - catch (ConditionException ex) { return $"Invalid condition: {ex.Message}"; } + if (!Condition.TryParse(expression, out _, out var error)) return $"Invalid condition: {error}"; elseSeen.Push(false); break; case BlockKind.Else: @@ -80,8 +79,7 @@ public static List FindCycle(AliasEntry target, IEnumerable if (kind != BlockKind.Until) continue; var (isUnsafe, condition) = BlockInterpreter.ParseUntil(expression); if (string.IsNullOrWhiteSpace(condition)) return ":until needs a condition"; - try { new Parser(Tokenizer.Tokenize(condition)).Parse(); } - catch (ConditionException ex) { return $"Invalid :until condition: {ex.Message}"; } + if (!Condition.TryParse(condition, out _, out var error)) return $"Invalid :until condition: {error}"; if (isUnsafe && !allowUnsafe) return "This :until uses -unsafe, but unsafe waits are off in settings"; } return null; diff --git a/Silkstring/Services/CommandHandler.cs b/Silkstring/Services/CommandHandler.cs index 51200e5..ea16af5 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -100,18 +100,7 @@ await _framework.RunOnFrameworkThread(() => } } - private bool EvaluateSafe(string expression, IReadOnlyList args) - { - try - { - return _conditions.Evaluate(expression, args); - } - catch (ConditionException ex) - { - Log.Warning(ex, "Invalid condition: {Expression}", expression); - return false; - } - } + private bool EvaluateSafe(string expression, IReadOnlyList args) => _conditions.Evaluate(expression, args); private async Task WaitUntilAsync(string condition, IReadOnlyList args, bool isUnsafe, int capMs, CancellationToken token) { diff --git a/Silkstring/Services/Conditions/Condition.cs b/Silkstring/Services/Conditions/Condition.cs new file mode 100644 index 0000000..58cbdb6 --- /dev/null +++ b/Silkstring/Services/Conditions/Condition.cs @@ -0,0 +1,11 @@ +namespace Silkstring.Services.Conditions; + +internal static class Condition +{ + public static bool TryParse(string expression, out ConditionNode? node, out string? error) + { + node = null; + if (!Tokenizer.TryTokenize(expression, out var tokens, out error)) return false; + return new Parser(tokens).TryParse(out node, out error); + } +} diff --git a/Silkstring/Services/Conditions/ConditionEvaluator.cs b/Silkstring/Services/Conditions/ConditionEvaluator.cs index 46c6988..7280c78 100644 --- a/Silkstring/Services/Conditions/ConditionEvaluator.cs +++ b/Silkstring/Services/Conditions/ConditionEvaluator.cs @@ -9,11 +9,7 @@ public sealed class ConditionEvaluator public ConditionEvaluator(Func, string> resolve) => _resolve = resolve; - public bool Evaluate(string expression, IReadOnlyList args) - { - var ast = new Parser(Tokenizer.Tokenize(expression)).Parse(); - return Eval(ast, args); - } + public bool Evaluate(string expression, IReadOnlyList args) => Condition.TryParse(expression, out var ast, out _) && Eval(ast!, args); private bool Eval(ConditionNode node, IReadOnlyList args) => node switch { diff --git a/Silkstring/Services/Conditions/ConditionException.cs b/Silkstring/Services/Conditions/ConditionException.cs deleted file mode 100644 index a0be9ce..0000000 --- a/Silkstring/Services/Conditions/ConditionException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Silkstring.Services.Conditions; - -public sealed class ConditionException : Exception -{ - public ConditionException(string message) : base(message) { } -} diff --git a/Silkstring/Services/Conditions/Parser.cs b/Silkstring/Services/Conditions/Parser.cs index 93d57ff..92bacd7 100644 --- a/Silkstring/Services/Conditions/Parser.cs +++ b/Silkstring/Services/Conditions/Parser.cs @@ -6,58 +6,60 @@ internal sealed class Parser { private readonly List _tokens; private int _pos; + private string? _error; + public Parser(List tokens) => _tokens = tokens; - public Parser(List tokens) => _tokens = tokens; + public bool TryParse(out ConditionNode? node, out string? error) + { + node = ParseOr(); + if (_error == null && Peek is { } leftover) _error = $"Unexpected '{leftover.Text}'"; + error = _error; + if (_error != null) node = null; + return _error == null; + } private Token? Peek => _pos < _tokens.Count ? _tokens[_pos] : null; private void Next() => _pos++; private bool IsOp(string s) => Peek is { Kind: TokenKind.Op } t && t.Text == s; private static bool IsComparison(string s) => s is "==" or "!=" or "<" or ">" or "<=" or ">="; - public ConditionNode Parse() - { - var n = ParseOr(); - if (Peek is { } leftover) throw new ConditionException($"Unexpected '{leftover.Text}'"); - return n; - } - - private ConditionNode ParseOr() + private ConditionNode? ParseOr() { var n = ParseAnd(); - while (IsOp("||")) { Next(); n = new OrNode(n, ParseAnd()); } + while (_error == null && IsOp("||")) { Next(); n = new OrNode(n!, ParseAnd()!); } return n; } - - private ConditionNode ParseAnd() + private ConditionNode? ParseAnd() { var n = ParseCmp(); - while (IsOp("&&")) { Next(); n = new AndNode(n, ParseCmp()); } + while (_error == null && IsOp("&&")) { Next(); n = new AndNode(n!, ParseCmp()!); } return n; } - - private ConditionNode ParseCmp() + private ConditionNode? ParseCmp() { if (Peek is { Kind: TokenKind.LParen }) { Next(); var n = ParseOr(); - if (Peek is not { Kind: TokenKind.RParen }) throw new ConditionException("Expected ')'"); + if (_error != null) return null; + if (Peek is not { Kind: TokenKind.RParen }) { _error = "Expected ')'"; return null; } Next(); return n; } - var left = ExpectOperand(); + if (_error != null) return null; if (Peek is { Kind: TokenKind.Op } op && IsComparison(op.Text)) { Next(); - return new CmpNode(left, op.Text, ExpectOperand()); + var right = ExpectOperand(); + if (_error != null) return null; + return new CmpNode(left!, op.Text, right!); } - return new BareNode(left); + return new BareNode(left!); } - - private string ExpectOperand() + private string? ExpectOperand() { - if (Peek is not { Kind: TokenKind.Operand } t) throw new ConditionException("Expected a value"); + if (Peek is not { Kind: TokenKind.Operand } t) { _error = "Expected a value"; return null; } Next(); return t.Text; } diff --git a/Silkstring/Services/Conditions/Tokenizer.cs b/Silkstring/Services/Conditions/Tokenizer.cs index 2377b03..d80945b 100644 --- a/Silkstring/Services/Conditions/Tokenizer.cs +++ b/Silkstring/Services/Conditions/Tokenizer.cs @@ -6,9 +6,10 @@ internal enum TokenKind { Operand, Op, LParen, RParen } internal readonly record struct Token(TokenKind Kind, string Text); internal static class Tokenizer { - public static List Tokenize(string expr) + public static bool TryTokenize(string expr, out List tokens, out string? error) { - var tokens = new List(); + tokens = new List(); + error = null; var i = 0; while (i < expr.Length) { @@ -24,12 +25,12 @@ public static List Tokenize(string expr) } if (c is '<' or '>') { tokens.Add(new(TokenKind.Op, c.ToString())); i++; continue; } - if (c is '&' or '|' or '=' or '!') throw new ConditionException($"Unexpected '{c}'"); + if (c is '&' or '|' or '=' or '!') { error = $"Unexpected '{c}'"; return false; } if (c == '"') { var end = expr.IndexOf('"', i + 1); - if (end < 0) throw new ConditionException("Unterminated Quote"); + if (end < 0) { error = "Unterminated quote"; return false; } tokens.Add(new(TokenKind.Operand, expr[(i + 1)..end])); i = end + 1; continue; @@ -39,7 +40,6 @@ public static List Tokenize(string expr) while (i < expr.Length && !char.IsWhiteSpace(expr[i]) && expr[i] is not ('(' or ')' or '"' or '&' or '|' or '=' or '!' or '<' or '>')) i++; tokens.Add(new(TokenKind.Operand, expr[start..i])); } - - return tokens; + return true; } } diff --git a/Silkstring/UI/Panels/AliasEditPanel.cs b/Silkstring/UI/Panels/AliasEditPanel.cs index 7b9eafa..89b775a 100644 --- a/Silkstring/UI/Panels/AliasEditPanel.cs +++ b/Silkstring/UI/Panels/AliasEditPanel.cs @@ -108,6 +108,8 @@ private void DrawMultilineView(AliasEntry alias) } _editor.Renderer.ShowLineNumbers = _configuration.ShowLineNumbers; + _editor.Renderer.IsShowingWhitespace = false; + SilkstringHighlighter.ApplyPalette(_editor); if (_editor.Render("###aliasEditor", new Vector2(-1, ImGui.GetContentRegionAvail().Y))) diff --git a/Silkstring/UI/SilkstringHighlighter.cs b/Silkstring/UI/SilkstringHighlighter.cs index 07799ed..edc15ce 100644 --- a/Silkstring/UI/SilkstringHighlighter.cs +++ b/Silkstring/UI/SilkstringHighlighter.cs @@ -99,11 +99,7 @@ public object Colorize(Span line, object? state) return Empty; } - private static bool TryParseCondition(string expression) - { - try { new Parser(Tokenizer.Tokenize(expression)).Parse(); return true; } - catch (ConditionException) { return false; } - } + private static bool TryParseCondition(string expression) => Condition.TryParse(expression, out _, out _); private static void PaintContent(Span line, string text, int from) { diff --git a/Tests/ConditionEvaluatorTests.cs b/Tests/ConditionEvaluatorTests.cs index 5af85e8..69c80eb 100644 --- a/Tests/ConditionEvaluatorTests.cs +++ b/Tests/ConditionEvaluatorTests.cs @@ -48,5 +48,5 @@ private static ConditionEvaluator Make() [InlineData("")] [InlineData("&&")] [InlineData("{a} {b}")] - public void ThrowsOnMalformed(string expr) => Assert.Throws(() => Make().Evaluate(expr, Array.Empty())); + public void ReturnsFalseOnMalformed(string expr) => Assert.False(Make().Evaluate(expr, Array.Empty())); } diff --git a/extern/DalamudTextEdit b/extern/DalamudTextEdit index f341119..5a5c2fa 160000 --- a/extern/DalamudTextEdit +++ b/extern/DalamudTextEdit @@ -1 +1 @@ -Subproject commit f341119fc9c0c4fe2e42e34b96424c236559da02 +Subproject commit 5a5c2fa23aae4ba5c8fccd34de8cad66ecf20967 From 6745e5f862829336dc69139e39d6aef44a554c9e Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:25:43 -0300 Subject: [PATCH 2/2] Add better validation hints --- Silkstring/Models/ThemeColors.cs | 1 + Silkstring/Services/AliasValidator.cs | 65 ++++++++++++++------------ Silkstring/Services/Diagnostic.cs | 5 ++ Silkstring/UI/Palette.cs | 2 + Silkstring/UI/Panels/AliasEditPanel.cs | 32 ++++++------- Tests/AliasValidatorTests.cs | 49 ++++++++++--------- extern/DalamudTextEdit | 2 +- 7 files changed, 85 insertions(+), 71 deletions(-) create mode 100644 Silkstring/Services/Diagnostic.cs diff --git a/Silkstring/Models/ThemeColors.cs b/Silkstring/Models/ThemeColors.cs index be69cc3..ccd6ae6 100644 --- a/Silkstring/Models/ThemeColors.cs +++ b/Silkstring/Models/ThemeColors.cs @@ -8,6 +8,7 @@ public class ThemeColors public Vector4 Folder = new(0.7f, 0.5f, 1.0f, 1.0f); public Vector4 LineNumber = new(0.5f, 0.5f, 0.5f, 1.0f); public Vector4 Error = new(1.0f, 0.4f, 0.4f, 1.0f); + public Vector4 Warning = new(0.90f, 0.70f, 0.20f, 1.0f); public Vector4 Success = new(0.4f, 1.0f, 0.4f, 1.0f); public Vector4 Variable = new(0.9f, 0.8f, 0.4f, 1.0f); public Vector4 Parameter = new(1.0f, 0.6f, 0.3f, 1.0f); diff --git a/Silkstring/Services/AliasValidator.cs b/Silkstring/Services/AliasValidator.cs index 9b7635f..0f34e3d 100644 --- a/Silkstring/Services/AliasValidator.cs +++ b/Silkstring/Services/AliasValidator.cs @@ -7,6 +7,18 @@ namespace Silkstring.Services; public static class AliasValidator { + public static List Validate(AliasEntry alias, ISet definedVariables, bool allowUnsafe, IEnumerable allAliases) + { + var diagnostics = new List(); + diagnostics.AddRange(ValidateBlocks(alias)); + diagnostics.AddRange(ValidateSets(alias, definedVariables)); + diagnostics.AddRange(ValidateWaits(alias)); + diagnostics.AddRange(ValidateUntils(alias, allowUnsafe)); + var cycle = FindCycle(alias, allAliases); + if (cycle.Count > 0) diagnostics.Add(new($"Cycle detected: {string.Join(" → ", cycle)}")); + return diagnostics; + } + public static List FindCycle(AliasEntry target, IEnumerable allAliases) { var lookup = BuildTriggerLookup(allAliases); @@ -15,74 +27,69 @@ public static List FindCycle(AliasEntry target, IEnumerable return Dfs(target, lookup, visited, path); } - public static string? ValidateBlocks(AliasEntry Alias) + public static IEnumerable ValidateBlocks(AliasEntry alias) { var elseSeen = new Stack(); - - foreach (var command in Alias.Output) + for (var i = 0; i < alias.Output.Count; i++) { - var (kind, expression) = BlockInterpreter.Classify((command.Command.Trim())); - + var (kind, expression) = BlockInterpreter.Classify(alias.Output[i].Command.Trim()); switch (kind) { case BlockKind.If: - if (!Condition.TryParse(expression, out _, out var error)) return $"Invalid condition: {error}"; + if (!Condition.TryParse(expression, out _, out var error)) { yield return new($"Invalid condition: {error}", i); yield break; } elseSeen.Push(false); break; case BlockKind.Else: - if (elseSeen.Count == 0) return ":else without a matching :if"; - if (elseSeen.Peek()) return "Duplicate :else in a block"; + if (elseSeen.Count == 0) { yield return new(":else without a matching :if", i); yield break; } + if (elseSeen.Peek()) { yield return new("Duplicate :else in a block", i); yield break; } elseSeen.Pop(); elseSeen.Push(true); break; case BlockKind.EndIf: - if (elseSeen.Count == 0) return ":endif without a matching :if"; + if (elseSeen.Count == 0) { yield return new(":endif without a matching :if", i); yield break; } elseSeen.Pop(); break; } } - return elseSeen.Count > 0 ? "Unclosed :if (missing :endif)" : null; + if (elseSeen.Count > 0) yield return new("Unclosed :if (missing :endif)"); } - public static string? ValidateSets(AliasEntry alias, ISet defined) + public static IEnumerable ValidateSets(AliasEntry alias, ISet defined) { - foreach (var command in alias.Output) + for (var i = 0; i < alias.Output.Count; i++) { - var (kind, expression) = BlockInterpreter.Classify(command.Command); + var (kind, expression) = BlockInterpreter.Classify(alias.Output[i].Command); if (kind != BlockKind.Set) continue; var (name, _) = BlockInterpreter.ParseSet(expression); - if (string.IsNullOrEmpty(name)) return ":set needs a variable name"; - if (!defined.Contains(name)) return $"Unknown variable in :set: {name}"; + if (string.IsNullOrEmpty(name)) { yield return new(":set needs a variable name", i); continue; } + if (!defined.Contains(name)) yield return new($"Unknown variable in :set: {name}", i); } - return null; } - public static string? ValidateWaits(AliasEntry alias) + public static IEnumerable ValidateWaits(AliasEntry alias) { - foreach (var command in alias.Output) + for (var i = 0; i < alias.Output.Count; i++) { - var (kind, expression) = BlockInterpreter.Classify(command.Command); + var (kind, expression) = BlockInterpreter.Classify(alias.Output[i].Command); if (kind != BlockKind.Wait) continue; var (value, _) = BlockInterpreter.ParseSet(expression); - if (string.IsNullOrEmpty(value)) return ":wait needs a duration"; + if (string.IsNullOrEmpty(value)) { yield return new(":wait needs a duration", i); continue; } if (expression.Contains('{')) continue; - if (!BlockInterpreter.TryParseDuration(expression, out _)) return $"Invalid :wait duration: {expression}"; + if (!BlockInterpreter.TryParseDuration(expression, out _)) yield return new($"Invalid :wait duration: {expression}", i); } - return null; } - public static string? ValidateUntils(AliasEntry alias, bool allowUnsafe) + public static IEnumerable ValidateUntils(AliasEntry alias, bool allowUnsafe) { - foreach (var command in alias.Output) + for (var i = 0; i < alias.Output.Count; i++) { - var (kind, expression) = BlockInterpreter.Classify(command.Command); + var (kind, expression) = BlockInterpreter.Classify(alias.Output[i].Command); if (kind != BlockKind.Until) continue; var (isUnsafe, condition) = BlockInterpreter.ParseUntil(expression); - if (string.IsNullOrWhiteSpace(condition)) return ":until needs a condition"; - if (!Condition.TryParse(condition, out _, out var error)) return $"Invalid :until condition: {error}"; - if (isUnsafe && !allowUnsafe) return "This :until uses -unsafe, but unsafe waits are off in settings"; + if (string.IsNullOrWhiteSpace(condition)) { yield return new(":until needs a condition", i); continue; } + if (!Condition.TryParse(condition, out _, out var error)) { yield return new($"Invalid :until condition: {error}", i); continue; } + if (isUnsafe && !allowUnsafe) yield return new("This :until uses -unsafe, but unsafe waits are off in settings", i, Severity.Warning); } - return null; } private static Dictionary BuildTriggerLookup(IEnumerable allAliases) diff --git a/Silkstring/Services/Diagnostic.cs b/Silkstring/Services/Diagnostic.cs new file mode 100644 index 0000000..2da2532 --- /dev/null +++ b/Silkstring/Services/Diagnostic.cs @@ -0,0 +1,5 @@ +namespace Silkstring.Services; + +public enum Severity { Error, Warning } + +public readonly record struct Diagnostic(string Message, int? Line = null, Severity Severity = Severity.Error); diff --git a/Silkstring/UI/Palette.cs b/Silkstring/UI/Palette.cs index 9ec635c..967967d 100644 --- a/Silkstring/UI/Palette.cs +++ b/Silkstring/UI/Palette.cs @@ -12,6 +12,7 @@ public static class Palette public static Vector4 Command; public static Vector4 Text; public static Vector4 Error; + public static Vector4 Warning; public static Vector4 Heading; public static Vector4 Folder; public static Vector4 Success; @@ -27,6 +28,7 @@ public static void Apply(ThemeColors t) Command = t.Command; Text = t.Text; Error = t.Error; + Warning = t.Warning; Heading = t.Heading; Folder = t.Folder; Success = t.Success; diff --git a/Silkstring/UI/Panels/AliasEditPanel.cs b/Silkstring/UI/Panels/AliasEditPanel.cs index 89b775a..5d84721 100644 --- a/Silkstring/UI/Panels/AliasEditPanel.cs +++ b/Silkstring/UI/Panels/AliasEditPanel.cs @@ -17,8 +17,7 @@ public class AliasEditPanel private readonly Configuration _configuration; private AliasEntry? _selectedAlias; - private List? _detectedCycle; - private string? _blockError; + private List _diagnostics = new(); private readonly TextEditor _editor; private int _editorAliasId = -1; @@ -34,8 +33,7 @@ public AliasEditPanel(Configuration configuration, MainWindow mainWindow) mainWindow.SelectionChanged += (alias, _) => { _selectedAlias = alias; - if (alias != null) RefreshCycleCheck(); - else { _detectedCycle = null; _blockError = null; } + RefreshDiagnostics(); }; } @@ -51,11 +49,13 @@ public void Draw() DrawAliasHeader(alias); ImGui.Separator(); - if (_blockError != null) + foreach (var d in _diagnostics) { - ImGui.TextColored(Palette.Error, _blockError); - ImGui.Spacing(); + var color = d.Severity == Severity.Error ? Palette.Error : Palette.Warning; + var prefix = d.Line is { } line ? $"Line {line + 1}: " : ""; + ImGui.TextColored(color, prefix + d.Message); } + if (_diagnostics.Count > 0) ImGui.Spacing(); DrawCommandList(alias); } @@ -79,12 +79,9 @@ private void DrawAliasHeader(AliasEntry alias) if (ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) { _configuration.MarkDirty(); - RefreshCycleCheck(); + RefreshDiagnostics(); } - var inputTooltip = _detectedCycle is { Count: > 0 } - ? $"Cycle detected: {string.Join(" → ", _detectedCycle)}" - : "Separate multiple aliases with | e.g. mew|meow|mreow"; - ImGuiUtil.Tooltip(inputTooltip); + ImGuiUtil.Tooltip("Separate multiple aliases with | e.g. mew|meow|mreow"); } private void DrawCommandList(AliasEntry alias) @@ -116,7 +113,7 @@ private void DrawMultilineView(AliasEntry alias) { ApplyMultiline(alias, _editor.AllText); _configuration.MarkDirty(); - RefreshCycleCheck(); + RefreshDiagnostics(); } } @@ -146,7 +143,7 @@ private void DrawCommandRow(CommandEntry command) if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) { _configuration.MarkDirty(); - RefreshCycleCheck(); + RefreshDiagnostics(); } ImGui.SameLine(); @@ -157,12 +154,11 @@ private void DrawCommandRow(CommandEntry command) ImGuiUtil.Tooltip("Hold Shift + Ctrl to delete", true); } - private void RefreshCycleCheck() + private void RefreshDiagnostics() { - if (_selectedAlias == null) return; - _detectedCycle = AliasValidator.FindCycle(_selectedAlias, _configuration.GetAliases()); + if (_selectedAlias == null) { _diagnostics.Clear(); return; } var defined = new HashSet(_configuration.UserVariables.Select(v => v.Name), StringComparer.OrdinalIgnoreCase); - _blockError = AliasValidator.ValidateBlocks(_selectedAlias) ?? AliasValidator.ValidateSets(_selectedAlias, defined) ?? AliasValidator.ValidateWaits(_selectedAlias) ?? AliasValidator.ValidateUntils(_selectedAlias, _configuration.AllowUnsafeWaits); + _diagnostics = AliasValidator.Validate(_selectedAlias, defined, _configuration.AllowUnsafeWaits, _configuration.GetAliases()); } private static void ApplyMultiline(AliasEntry alias, string text) diff --git a/Tests/AliasValidatorTests.cs b/Tests/AliasValidatorTests.cs index af6a3eb..6613c1f 100644 --- a/Tests/AliasValidatorTests.cs +++ b/Tests/AliasValidatorTests.cs @@ -46,29 +46,32 @@ public void NonSlashLineIsNotADependency() Assert.Empty(AliasValidator.FindCycle(a, new[] { a })); } - [Fact] public void ValidIf() => Assert.Null(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} < 50", "/echo", ":endif"))); - [Fact] public void ValidIfElse() => Assert.Null(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} < 50", "/a", ":else", "/b", ":endif"))); - [Fact] public void NestedValid() => Assert.Null(AliasValidator.ValidateBlocks(Alias("a", ":if {a} == 1", ":if {b} == 2", "/c", ":endif", ":endif"))); - [Fact] public void NoBlocks() => Assert.Null(AliasValidator.ValidateBlocks(Alias("a", "/say hi"))); - [Fact] public void Unclosed() => Assert.NotNull(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} < 50", "/a"))); - [Fact] public void OrphanElse() => Assert.NotNull(AliasValidator.ValidateBlocks(Alias("a", ":else"))); - [Fact] public void OrphanEndIf() => Assert.NotNull(AliasValidator.ValidateBlocks(Alias("a", ":endif"))); - [Fact] public void DuplicateElse() => Assert.NotNull(AliasValidator.ValidateBlocks(Alias("a", ":if {a} == 1", ":else", ":else", ":endif"))); - [Fact] public void BadExpression() => Assert.NotNull(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} <=", ":endif"))); + [Fact] public void ValidIf() => Assert.Empty(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} < 50", "/echo", ":endif"))); + [Fact] public void ValidIfElse() => Assert.Empty(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} < 50", "/a", ":else", "/b", ":endif"))); + [Fact] public void NestedValid() => Assert.Empty(AliasValidator.ValidateBlocks(Alias("a", ":if {a} == 1", ":if {b} == 2", "/c", ":endif", ":endif"))); + [Fact] public void NoBlocks() => Assert.Empty(AliasValidator.ValidateBlocks(Alias("a", "/say hi"))); + [Fact] public void Unclosed() => Assert.NotEmpty(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} < 50", "/a"))); + [Fact] public void OrphanElse() => Assert.NotEmpty(AliasValidator.ValidateBlocks(Alias("a", ":else"))); + [Fact] public void OrphanEndIf() => Assert.NotEmpty(AliasValidator.ValidateBlocks(Alias("a", ":endif"))); + [Fact] public void DuplicateElse() => Assert.NotEmpty(AliasValidator.ValidateBlocks(Alias("a", ":if {a} == 1", ":else", ":else", ":endif"))); + [Fact] public void BadExpression() => Assert.NotEmpty(AliasValidator.ValidateBlocks(Alias("a", ":if {hp} <=", ":endif"))); - [Fact] public void SetKnown() => Assert.Null(AliasValidator.ValidateSets(Alias("a", ":set foo bar"), Defined("foo"))); - [Fact] public void SetCaseInsensitive() => Assert.Null(AliasValidator.ValidateSets(Alias("a", ":set FOO bar"), Defined("foo"))); - [Fact] public void SetUnknown() => Assert.NotNull(AliasValidator.ValidateSets(Alias("a", ":set foo bar"), Defined())); - [Fact] public void SetNoName() => Assert.NotNull(AliasValidator.ValidateSets(Alias("a", ":set "), Defined("foo"))); - [Fact] public void SetOneBadAmongGood() => Assert.NotNull(AliasValidator.ValidateSets(Alias("a", ":set foo a", ":set bar b"), Defined("foo"))); - [Fact] public void SetNonSetLinesIgnored() => Assert.Null(AliasValidator.ValidateSets(Alias("a", "/say hi"), Defined())); + [Fact] public void SetKnown() => Assert.Empty(AliasValidator.ValidateSets(Alias("a", ":set foo bar"), Defined("foo"))); + [Fact] public void SetCaseInsensitive() => Assert.Empty(AliasValidator.ValidateSets(Alias("a", ":set FOO bar"), Defined("foo"))); + [Fact] public void SetUnknown() => Assert.NotEmpty(AliasValidator.ValidateSets(Alias("a", ":set foo bar"), Defined())); + [Fact] public void SetNoName() => Assert.NotEmpty(AliasValidator.ValidateSets(Alias("a", ":set "), Defined("foo"))); + [Fact] public void SetOneBadAmongGood() => Assert.NotEmpty(AliasValidator.ValidateSets(Alias("a", ":set foo a", ":set bar b"), Defined("foo"))); + [Fact] public void SetNonSetLinesIgnored() => Assert.Empty(AliasValidator.ValidateSets(Alias("a", "/say hi"), Defined())); - [Fact] public void WaitValid() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait 2"))); - [Fact] public void WaitDecimal() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait 1.5"))); - [Fact] public void WaitToken() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait {0}"))); - [Fact] public void WaitOverCapIsValid() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait 120"))); - [Fact] public void WaitInvalid() => Assert.NotNull(AliasValidator.ValidateWaits(Alias("a", ":wait potato"))); - [Fact] public void WaitNegative() => Assert.NotNull(AliasValidator.ValidateWaits(Alias("a", ":wait -1"))); - [Fact] public void WaitEmpty() => Assert.NotNull(AliasValidator.ValidateWaits(Alias("a", ":wait "))); - [Fact] public void WaitNonWaitLinesIgnored() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", "/say hi"))); + [Fact] public void WaitValid() => Assert.Empty(AliasValidator.ValidateWaits(Alias("a", ":wait 2"))); + [Fact] public void WaitDecimal() => Assert.Empty(AliasValidator.ValidateWaits(Alias("a", ":wait 1.5"))); + [Fact] public void WaitToken() => Assert.Empty(AliasValidator.ValidateWaits(Alias("a", ":wait {0}"))); + [Fact] public void WaitOverCapIsValid() => Assert.Empty(AliasValidator.ValidateWaits(Alias("a", ":wait 120"))); + [Fact] public void WaitInvalid() => Assert.NotEmpty(AliasValidator.ValidateWaits(Alias("a", ":wait potato"))); + [Fact] public void WaitNegative() => Assert.NotEmpty(AliasValidator.ValidateWaits(Alias("a", ":wait -1"))); + [Fact] public void WaitEmpty() => Assert.NotEmpty(AliasValidator.ValidateWaits(Alias("a", ":wait "))); + [Fact] public void WaitNonWaitLinesIgnored() => Assert.Empty(AliasValidator.ValidateWaits(Alias("a", "/say hi"))); + + [Fact] public void WaitReportsLine() => Assert.Equal(1, Assert.Single(AliasValidator.ValidateWaits(Alias("a", "/say hi", ":wait potato"))).Line); + [Fact] public void SetReportsEveryBadLine() => Assert.Equal(2, AliasValidator.ValidateSets(Alias("a", ":set foo a", ":set bar b", ":set baz c"), Defined("foo")).Count()); } diff --git a/extern/DalamudTextEdit b/extern/DalamudTextEdit index 5a5c2fa..80691f4 160000 --- a/extern/DalamudTextEdit +++ b/extern/DalamudTextEdit @@ -1 +1 @@ -Subproject commit 5a5c2fa23aae4ba5c8fccd34de8cad66ecf20967 +Subproject commit 80691f4a50ced89e733d245d264404edbeb0a206