diff --git a/Tools/OutWit.Database.Studio.Tests/ViewModels/AParseErrorUnderlinesItsTokenTests.cs b/Tools/OutWit.Database.Studio.Tests/ViewModels/AParseErrorUnderlinesItsTokenTests.cs new file mode 100644 index 0000000..119f1f2 --- /dev/null +++ b/Tools/OutWit.Database.Studio.Tests/ViewModels/AParseErrorUnderlinesItsTokenTests.cs @@ -0,0 +1,124 @@ +using OutWit.Database.Studio.Tests.Helpers; +using OutWit.Database.Studio.ViewModels.Tabs; + +namespace OutWit.Database.Studio.Tests.ViewModels; + +/// +/// A parse error is underlined across the word it is about. +/// +/// +/// +/// Finding 20, and it took driving Studio to settle it. The report said a parse error is not +/// marked at all while a name error is. It IS marked - with a squiggle ONE CHARACTER wide, under the +/// first letter of the token - and beside Deliveries underlined across its whole ten letters, +/// one character reads as nothing. The mechanism was never broken; the length was. +/// +/// +/// The token comes out of the message. Every shape this parser produces names it in single +/// quotes - mismatched input 'Country', extraneous input 'x', +/// no viable alternative at input 'xy', missing ';' at 'SELECT' - and the alternative +/// was to add a field to the parser's error type, which is an engine change for a drawing detail. +/// When there is no quoted token the mark stays one character, which is what it always was. +/// +/// +[TestFixture] +public class AParseErrorUnderlinesItsTokenTests +{ + #region Fields + + private StudioFixture m_studio = null!; + private QueryTabViewModel m_tab = null!; + + #endregion + + #region Setup + + [SetUp] + public async Task SetUp() + { + m_studio = await StudioFixture.CreateAsync(); + + m_tab = m_studio.FirstQueryTab; + } + + [TearDown] + public async Task TearDown() + { + await m_studio.DisposeAsync(); + } + + #endregion + + #region Tests + + /// + /// The case from the report, with the length it was missing. + /// + [Test] + public async Task TheWordTheParserNamesIsTheWordUnderlinedTest() + { + m_tab.SqlText = "SELECT Id, Name" + Environment.NewLine + + "FROM Customers" + Environment.NewLine + + "WHER Country = 'Ireland'"; + + await m_tab.ExecuteSqlAsync(m_tab.SqlText); + + Assert.Multiple(() => + { + Assert.That(m_tab.ErrorMessage, Does.Contain("Country"), + "the parser names the token it could not use"); + + Assert.That(m_tab.UnderlineLine, Is.EqualTo(3), "on the line it was written on"); + + Assert.That(m_tab.UnderlineLength, Is.EqualTo("Country".Length), + "and the mark covers the word, not its first letter"); + }); + } + + /// + /// The path a person actually sees: the live syntax check, which wins over the executed one. + /// + /// + /// This is the case the screen taught. With the executed path fixed, driving Studio + /// still showed a one-letter mark - because whenever the text does not parse, the live check + /// has an answer and UpdateUnderline prefers it, and that branch hard-coded a length of + /// one. The ViewModel said seven and the window drew one. + /// + [Test] + public void TheLiveCheckMarksTheWordTooTest() + { + m_tab.SqlText = "SELECT Id, Name" + Environment.NewLine + + "FROM Customers" + Environment.NewLine + + "WHER Country = 'Ireland'"; + + m_tab.CheckSyntaxNow(); + + Assert.Multiple(() => + { + Assert.That(m_tab.UnderlineLine, Is.EqualTo(3)); + + Assert.That(m_tab.UnderlineLength, Is.EqualTo("Country".Length), + "the branch that wins draws the word as well"); + }); + } + /// + /// The control: a message with no token in it still marks something. + /// + [Test] + public void AMessageWithNoQuotedTokenKeepsTheOldMarkTest() + { + Assert.That(QueryTabViewModel.LengthOfTheOffendingToken("something went wrong"), Is.EqualTo(1), + "one character, which is what every parse error used to get"); + } + + [TestCase("mismatched input 'Country' expecting {')', ','}", 7)] + [TestCase("extraneous input 'x' expecting EOF", 1)] + [TestCase("no viable alternative at input 'SELECTFROM'", 10)] + [TestCase("missing ';' at 'SELECT'", 1)] + public void TheTokenIsTakenFromTheMessageTest(string message, int length) + { + Assert.That(QueryTabViewModel.LengthOfTheOffendingToken(message), Is.EqualTo(length)); + } + + #endregion +} diff --git a/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.Workspace.cs b/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.Workspace.cs index 9037742..fe107a3 100644 --- a/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.Workspace.cs +++ b/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.Workspace.cs @@ -169,7 +169,12 @@ private void UpdateUnderline() { UnderlineLine = SyntaxErrorLine; UnderlineColumn = SyntaxErrorColumn; - UnderlineLength = 1; + + // The word, like the executed path beside it. This branch WINS whenever the text does not + // parse, so it is the one a person sees for a syntax error - and it hard-coded a single + // character. Driving Studio on 2026-08-19 showed the mark still one letter wide after the + // other branch had been fixed: the ViewModel said seven and the window drew one. + UnderlineLength = LengthOfTheOffendingToken(SyntaxError?.Detail ?? SyntaxError?.Message); return; } diff --git a/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.cs b/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.cs index 7f7bc38..7c10cf9 100644 --- a/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.cs +++ b/Tools/OutWit.Database.Studio/ViewModels/Tabs/QueryTabViewModel.cs @@ -379,6 +379,35 @@ public async Task ExecuteSqlAsync(string sql, (int Line, int Column)? origin = n return (line, index - (lastBreak + 1)); } + /// + /// How many characters the parser's message says the error is about. + /// + /// + /// Every shape this parser produces names the offending text in single quotes - + /// mismatched input 'Country', extraneous input 'x', + /// no viable alternative at input 'xy', missing ';' at 'SELECT'. Reading it from + /// there keeps an engine package out of a question about drawing: the alternative was to add a + /// field to the parser's own error type. A message with no quoted token keeps the single character + /// the mark has always had. + /// + public static int LengthOfTheOffendingToken(string? message) + { + if (string.IsNullOrEmpty(message)) + return 1; + + var opening = message.IndexOf('\''); + + if (opening < 0) + return 1; + + var closing = message.IndexOf('\'', opening + 1); + + if (closing <= opening + 1) + return 1; + + return closing - opening - 1; + } + private void ReportError(SqlError error, (int Line, int Column) fragment, int number = 0, int total = 0) { // The fragment's own offset is added last, so a selection three screens down underlines the @@ -392,7 +421,11 @@ private void ReportError(SqlError error, (int Line, int Column) fragment, int nu ErrorMessage = where + error.Message; ErrorDetail = error.Detail; - ErrorLength = 1; + + // The WORD, not its first letter. Measured by driving Studio on 2026-08-19: a parse error was + // marked with a squiggle one character wide, beside a missing table underlined across its + // whole name - which is why the report said a parse error is not marked at all. + ErrorLength = LengthOfTheOffendingToken(error.Detail ?? error.Message); UpdateUnderline();