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();