Skip to content

Commit 2be987b

Browse files
committed
fix: Extend multi-line template search range for early property navigation
- Fixed ReconstructMultiLineTemplate search range from currentLine + 5 to Math.Max(currentLine + 5, serilogCallLine + 20) - Resolves navigation failure for early properties in long multi-line verbatim strings - Added v0.6.2 changelog entry documenting the fix
1 parent a27a0f0 commit 2be987b

File tree

3 files changed

+160
-14
lines changed

3 files changed

+160
-14
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.6.2] - 2025-09-09
11+
12+
### Fixed
13+
- Multi-line template navigation for early properties in verbatim strings
14+
- Navigation from early template properties (like `{AppName}`, `{Version}`, `{Environment}`) now works correctly in multi-line verbatim strings
15+
- Fixed ReconstructMultiLineTemplate search range to properly find string termination beyond initial 5-line limit
16+
- Extended search range from `currentLine + 5` to `Math.Max(currentLine + 5, serilogCallLine + 20)` for better coverage
17+
- Resolves issue where navigation worked for later properties but failed for early ones in long multi-line templates
18+
1019
## [0.6.1] - 2025-09-09
1120

1221
### Fixed

SerilogSyntax.Tests/Navigation/SerilogNavigationProviderTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,4 +440,94 @@ public void GetSuggestedActions_RawStringLiteralMultiLine_ShouldProvideNavigatio
440440

441441
Assert.NotEmpty(actions); // This should fail - no navigation appears
442442
}
443+
444+
[Fact]
445+
public void GetSuggestedActions_VerbatimStringMultiLine_EarlyProperties_ShouldProvideNavigation()
446+
{
447+
// Test the specific scenario where {AppName}, {Version}, {Environment} don't get navigation
448+
var multiLineCode =
449+
"logger.LogInformation(@\"\r\n" +
450+
"===============================================\r\n" +
451+
"Application: {AppName}\r\n" +
452+
"Version: {Version}\r\n" +
453+
"Environment: {Environment}\r\n" +
454+
"===============================================\r\n" +
455+
"User: {UserName} (ID: {UserId})\r\n" +
456+
"Session: {SessionId}\r\n" +
457+
"Timestamp: {Timestamp:yyyy-MM-dd HH:mm:ss}\r\n" +
458+
"===============================================\r\n" +
459+
"\", appName, version, env, userName, userId, sessionId, DateTime.Now);";
460+
461+
var mockBuffer = new MockTextBuffer(multiLineCode);
462+
var mockSnapshot = new MockTextSnapshot(multiLineCode, mockBuffer, 1);
463+
var provider = new SerilogSuggestedActionsSource(null);
464+
465+
// Test navigation for {AppName} - this reportedly fails
466+
var appNameStart = multiLineCode.IndexOf("{AppName}");
467+
var appNameRange = new SnapshotSpan(mockSnapshot, appNameStart + 1, 7); // Inside "AppName"
468+
var appNameActions = provider.GetSuggestedActions(null, appNameRange, CancellationToken.None);
469+
470+
// Test navigation for {Version} - this reportedly fails
471+
var versionStart = multiLineCode.IndexOf("{Version}");
472+
var versionRange = new SnapshotSpan(mockSnapshot, versionStart + 1, 7); // Inside "Version"
473+
var versionActions = provider.GetSuggestedActions(null, versionRange, CancellationToken.None);
474+
475+
// Test navigation for {Environment} - this reportedly fails
476+
var environmentStart = multiLineCode.IndexOf("{Environment}");
477+
var environmentRange = new SnapshotSpan(mockSnapshot, environmentStart + 1, 11); // Inside "Environment"
478+
var environmentActions = provider.GetSuggestedActions(null, environmentRange, CancellationToken.None);
479+
480+
// Test navigation for {UserName} - this reportedly works
481+
var userNameStart = multiLineCode.IndexOf("{UserName}");
482+
var userNameRange = new SnapshotSpan(mockSnapshot, userNameStart + 1, 8); // Inside "UserName"
483+
var userNameActions = provider.GetSuggestedActions(null, userNameRange, CancellationToken.None);
484+
485+
// All should work, but currently only later ones do
486+
Assert.NotEmpty(appNameActions);
487+
Assert.NotEmpty(versionActions);
488+
Assert.NotEmpty(environmentActions);
489+
Assert.NotEmpty(userNameActions);
490+
}
491+
492+
[Fact]
493+
public void GetSuggestedActions_RawStringMultiLine_EarlyProperties_ShouldProvideNavigation()
494+
{
495+
// Test the same issue with raw string literals
496+
var multiLineCode =
497+
"logger.LogInformation(\"\"\"\r\n" +
498+
" ===============================================\r\n" +
499+
" Application: {AppName}\r\n" +
500+
" Version: {Version}\r\n" +
501+
" Environment: {Environment}\r\n" +
502+
" ===============================================\r\n" +
503+
" User: {UserName} (ID: {UserId})\r\n" +
504+
" Session: {SessionId}\r\n" +
505+
" Timestamp: {Timestamp:yyyy-MM-dd HH:mm:ss}\r\n" +
506+
" ===============================================\r\n" +
507+
" \"\"\", appName, version, environment, userName, userId, sessionId, DateTime.Now);";
508+
509+
var mockBuffer = new MockTextBuffer(multiLineCode);
510+
var mockSnapshot = new MockTextSnapshot(multiLineCode, mockBuffer, 1);
511+
var provider = new SerilogSuggestedActionsSource(null);
512+
513+
// Test navigation for {AppName} - this reportedly fails
514+
var appNameStart = multiLineCode.IndexOf("{AppName}");
515+
var appNameRange = new SnapshotSpan(mockSnapshot, appNameStart + 1, 7); // Inside "AppName"
516+
var appNameActions = provider.GetSuggestedActions(null, appNameRange, CancellationToken.None);
517+
518+
// Test navigation for {Version} - this reportedly fails
519+
var versionStart = multiLineCode.IndexOf("{Version}");
520+
var versionRange = new SnapshotSpan(mockSnapshot, versionStart + 1, 7); // Inside "Version"
521+
var versionActions = provider.GetSuggestedActions(null, versionRange, CancellationToken.None);
522+
523+
// Test navigation for {Environment} - this reportedly fails
524+
var environmentStart = multiLineCode.IndexOf("{Environment}");
525+
var environmentRange = new SnapshotSpan(mockSnapshot, environmentStart + 1, 11); // Inside "Environment"
526+
var environmentActions = provider.GetSuggestedActions(null, environmentRange, CancellationToken.None);
527+
528+
// All should work, but currently only later ones do
529+
Assert.NotEmpty(appNameActions);
530+
Assert.NotEmpty(versionActions);
531+
Assert.NotEmpty(environmentActions);
532+
}
443533
}

SerilogSyntax/Navigation/SerilogNavigationProvider.cs

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public async Task<bool> HasSuggestedActionsAsync(
8080

8181
// Check if we're in a Serilog call
8282
var serilogMatch = SerilogCallDetector.FindSerilogCall(lineText);
83+
ITextSnapshotLine serilogCallLine = line;
84+
8385
DiagnosticLogger.Log($"Serilog match on current line: {(serilogMatch != null ? $"Found at {serilogMatch.Index}" : "Not found")}");
8486
if (serilogMatch == null)
8587
{
@@ -91,26 +93,70 @@ public async Task<bool> HasSuggestedActionsAsync(
9193
DiagnosticLogger.Log("No Serilog call found - returning false");
9294
return false;
9395
}
96+
97+
serilogMatch = multiLineResult.Value.Match;
98+
serilogCallLine = multiLineResult.Value.Line;
9499
}
95100

96-
// Find the template string
97-
var templateMatch = FindTemplateString(lineText, serilogMatch.Index + serilogMatch.Length);
98-
if (!templateMatch.HasValue)
99-
return false;
100-
101-
var (templateStart, templateEnd) = templateMatch.Value;
102-
var template = lineText.Substring(templateStart, templateEnd - templateStart);
101+
// Find the template string - handle both single-line and multi-line scenarios
102+
string template;
103+
int templateStartPosition;
104+
int templateEndPosition;
103105

104-
// Check if cursor is within template
105-
var positionInLine = triggerPoint.Position - lineStart;
106-
if (positionInLine < templateStart || positionInLine > templateEnd)
107-
return false;
106+
if (serilogCallLine == line)
107+
{
108+
// Same-line scenario: template starts on the same line as the Serilog call
109+
var templateMatch = FindTemplateString(lineText, serilogMatch.Index + serilogMatch.Length);
110+
if (!templateMatch.HasValue)
111+
{
112+
// No complete template found on this line - check if it's a multi-line template starting here
113+
var multiLineTemplate = ReconstructMultiLineTemplate(range.Snapshot, serilogCallLine, line);
114+
if (multiLineTemplate == null)
115+
return false;
116+
117+
template = multiLineTemplate.Value.Template;
118+
templateStartPosition = multiLineTemplate.Value.StartPosition;
119+
templateEndPosition = multiLineTemplate.Value.EndPosition;
120+
121+
// Check if cursor is within the multi-line template bounds
122+
if (triggerPoint.Position < templateStartPosition || triggerPoint.Position > templateEndPosition)
123+
return false;
124+
}
125+
else
126+
{
127+
// Complete single-line template found
128+
var (templateStart, templateEnd) = templateMatch.Value;
129+
template = lineText.Substring(templateStart, templateEnd - templateStart);
130+
templateStartPosition = lineStart + templateStart;
131+
templateEndPosition = lineStart + templateEnd;
132+
133+
// Check if cursor is within template
134+
var positionInLine = triggerPoint.Position - lineStart;
135+
if (positionInLine < templateStart || positionInLine > templateEnd)
136+
return false;
137+
}
138+
}
139+
else
140+
{
141+
// Multi-line scenario: reconstruct the full template from multiple lines
142+
var multiLineTemplate = ReconstructMultiLineTemplate(range.Snapshot, serilogCallLine, line);
143+
if (multiLineTemplate == null)
144+
return false;
145+
146+
template = multiLineTemplate.Value.Template;
147+
templateStartPosition = multiLineTemplate.Value.StartPosition;
148+
templateEndPosition = multiLineTemplate.Value.EndPosition;
149+
150+
// Check if cursor is within the multi-line template bounds
151+
if (triggerPoint.Position < templateStartPosition || triggerPoint.Position > templateEndPosition)
152+
return false;
153+
}
108154

109155
// Parse template to find properties
110156
var properties = _parser.Parse(template).ToList();
111157

112158
// Find which property the cursor is on
113-
var cursorPosInTemplate = positionInLine - templateStart;
159+
var cursorPosInTemplate = triggerPoint.Position - templateStartPosition;
114160
var property = properties.FirstOrDefault(p =>
115161
cursorPosInTemplate >= p.BraceStartIndex &&
116162
cursorPosInTemplate <= p.BraceEndIndex);
@@ -549,7 +595,7 @@ private bool IsInsideMultiLineTemplate(ITextSnapshot snapshot, ITextSnapshotLine
549595
bool inRawString = false;
550596
int rawStringQuoteCount = 0;
551597

552-
for (int lineNum = serilogCallLine.LineNumber; lineNum <= currentLine.LineNumber + 5 && lineNum < snapshot.LineCount; lineNum++)
598+
for (int lineNum = serilogCallLine.LineNumber; lineNum <= Math.Max(currentLine.LineNumber + 5, serilogCallLine.LineNumber + 20) && lineNum < snapshot.LineCount; lineNum++)
553599
{
554600
var line = snapshot.GetLineFromLineNumber(lineNum);
555601
var lineText = line.GetText();
@@ -656,7 +702,8 @@ private bool IsInsideMultiLineTemplate(ITextSnapshot snapshot, ITextSnapshotLine
656702
if (foundTemplateStart && (inVerbatimString || inRawString) && lineNum < snapshot.LineCount - 1)
657703
{
658704
var nextLine = snapshot.GetLineFromLineNumber(lineNum + 1);
659-
if (nextLine.LineNumber <= currentLine.LineNumber + 5)
705+
// Continue processing lines until we find the end of the template or reasonable limit
706+
if (lineNum + 1 <= Math.Min(snapshot.LineCount - 1, serilogCallLine.LineNumber + 20))
660707
{
661708
// Get the actual line break text from the snapshot
662709
var lineBreakStart = line.End.Position;

0 commit comments

Comments
 (0)