Skip to content

Commit a2e0b2f

Browse files
committed
fix: navigate to C# source definitions
1 parent 58796fe commit a2e0b2f

7 files changed

Lines changed: 259 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ All notable changes to this project will be documented in this file.
1616
### Known issues
1717
- _None yet._
1818

19+
## 0.1.18
20+
21+
### Fixed
22+
- Go to Definition from VB.NET into source-loaded C# `netstandard2.0` project references now maps metadata symbols back to their C# source declarations, resolving the "No definition found" result for `Utils.GetMessage()` in the issue #15 console sample.
23+
24+
### Known issues
25+
- C# projects are still loaded for cross-project symbols only; the extension continues to serve VB.NET documents in the current phase.
26+
1927
## 0.1.17
2028

2129
### Fixed

‎src/VbNet.LanguageServer.Vb/Core/LanguageServer.vb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Namespace Core
8282
''' <summary>
8383
''' Server version reported in initialize response.
8484
''' </summary>
85-
Public Const ServerVersion As String = "0.1.17"
85+
Public Const ServerVersion As String = "0.1.18"
8686

8787
Public Sub New(transport As ITransport, loggerFactory As ILoggerFactory)
8888
If transport Is Nothing Then

‎src/VbNet.LanguageServer.Vb/Services/DefinitionService.vb‎

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,26 +140,73 @@ Namespace Services
140140

141141
Dim definitionSymbol = If(symbol.OriginalDefinition, symbol)
142142

143+
Await AddLocationsForSymbolAsync(definitionSymbol, locations, cancellationToken).ConfigureAwait(False)
144+
145+
If locations.Count = 0 AndAlso definitionSymbol.Locations.Any(Function(l) l.IsInMetadata) Then
146+
Dim sourceSymbol = Await SymbolFinder.FindSourceDefinitionAsync(definitionSymbol, solution, cancellationToken).ConfigureAwait(False)
147+
If sourceSymbol Is Nothing Then
148+
sourceSymbol = Await FindMatchingSourceSymbolAsync(definitionSymbol, solution, cancellationToken).ConfigureAwait(False)
149+
End If
150+
151+
If sourceSymbol IsNot Nothing Then
152+
Await AddLocationsForSymbolAsync(sourceSymbol, locations, cancellationToken).ConfigureAwait(False)
153+
End If
154+
End If
155+
156+
If locations.Count = 0 AndAlso definitionSymbol.Locations.Any(Function(l) l.IsInMetadata) Then
157+
_logger.LogTrace("Symbol {Symbol} is defined in metadata, no source location available", definitionSymbol.Name)
158+
End If
159+
160+
Return locations.ToArray()
161+
End Function
162+
163+
Private Async Function AddLocationsForSymbolAsync(symbol As ISymbol, locations As IList(Of Protocol.Location), cancellationToken As CancellationToken) As Task
164+
Dim definitionSymbol = If(symbol.OriginalDefinition, symbol)
165+
143166
For Each syntaxRef In definitionSymbol.DeclaringSyntaxReferences
144167
cancellationToken.ThrowIfCancellationRequested()
145168

146-
Dim location = Await CreateLocationFromSyntaxReferenceAsync(syntaxRef, solution, cancellationToken).ConfigureAwait(False)
169+
Dim location = Await CreateLocationFromSyntaxReferenceAsync(syntaxRef, cancellationToken).ConfigureAwait(False)
147170
If location IsNot Nothing Then
148171
locations.Add(location)
149172
End If
150173
Next
174+
End Function
151175

152-
If locations.Count = 0 AndAlso definitionSymbol.Locations.Any(Function(l) l.IsInMetadata) Then
153-
_logger.LogTrace("Symbol {Symbol} is defined in metadata, no source location available", definitionSymbol.Name)
176+
Private Shared Async Function FindMatchingSourceSymbolAsync(symbol As ISymbol, solution As Solution, cancellationToken As CancellationToken) As Task(Of ISymbol)
177+
Dim documentationId = DocumentationCommentId.CreateDeclarationId(symbol)
178+
If String.IsNullOrWhiteSpace(documentationId) Then
179+
Return Nothing
154180
End If
155181

156-
Return locations.ToArray()
182+
Dim containingAssemblyName = symbol.ContainingAssembly?.Name
183+
184+
For Each project In solution.Projects
185+
cancellationToken.ThrowIfCancellationRequested()
186+
187+
If Not String.IsNullOrWhiteSpace(containingAssemblyName) AndAlso
188+
Not String.Equals(project.AssemblyName, containingAssemblyName, StringComparison.OrdinalIgnoreCase) Then
189+
Continue For
190+
End If
191+
192+
Dim compilation = Await project.GetCompilationAsync(cancellationToken).ConfigureAwait(False)
193+
If compilation Is Nothing Then
194+
Continue For
195+
End If
196+
197+
Dim sourceSymbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(documentationId, compilation)
198+
If sourceSymbol IsNot Nothing AndAlso sourceSymbol.DeclaringSyntaxReferences.Length > 0 Then
199+
Return sourceSymbol
200+
End If
201+
Next
202+
203+
Return Nothing
157204
End Function
158205

159206
''' <summary>
160207
''' Creates an LSP Location from a Roslyn SyntaxReference.
161208
''' </summary>
162-
Private Async Function CreateLocationFromSyntaxReferenceAsync(syntaxRef As SyntaxReference, solution As Solution, cancellationToken As CancellationToken) As Task(Of Protocol.Location)
209+
Private Async Function CreateLocationFromSyntaxReferenceAsync(syntaxRef As SyntaxReference, cancellationToken As CancellationToken) As Task(Of Protocol.Location)
163210
Dim syntaxTree = syntaxRef.SyntaxTree
164211
Dim filePath = syntaxTree.FilePath
165212

@@ -190,19 +237,24 @@ Namespace Services
190237
''' </summary>
191238
Private Shared Function GetIdentifierSpan(node As SyntaxNode) As TextSpan?
192239
For Each child In node.ChildTokens()
193-
If child.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.IdentifierToken) Then
240+
If IsIdentifierToken(child) Then
194241
Return child.Span
195242
End If
196243
Next
197244

198245
Dim firstToken = node.GetFirstToken()
199-
If firstToken.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.IdentifierToken) Then
246+
If IsIdentifierToken(firstToken) Then
200247
Return firstToken.Span
201248
End If
202249

203250
Return Nothing
204251
End Function
205252

253+
Private Shared Function IsIdentifierToken(token As SyntaxToken) As Boolean
254+
Return token.RawKind = CInt(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.IdentifierToken) OrElse
255+
token.RawKind = CInt(Microsoft.CodeAnalysis.CSharp.SyntaxKind.IdentifierToken)
256+
End Function
257+
206258
''' <summary>
207259
''' Converts an LSP Position to a Roslyn offset.
208260
''' </summary>

‎src/extension/CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ All notable changes to this project will be documented in this file.
1616
### Known issues
1717
- _None yet._
1818

19+
## 0.1.18
20+
21+
### Fixed
22+
- Go to Definition from VB.NET into source-loaded C# `netstandard2.0` project references now maps metadata symbols back to their C# source declarations, resolving the "No definition found" result for `Utils.GetMessage()` in the issue #15 console sample.
23+
24+
### Known issues
25+
- C# projects are still loaded for cross-project symbols only; the extension continues to serve VB.NET documents in the current phase.
26+
1927
## 0.1.17
2028

2129
### Fixed

‎src/extension/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/extension/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vbnet-language-support",
33
"publisher": "dnakode",
4-
"version": "0.1.17",
4+
"version": "0.1.18",
55
"description": "First-class VB.NET language support for VS Code",
66
"displayName": "VB.NET Language Support",
77
"icon": "images/vbnet-icon.png",

‎test/VbNet.LanguageServer.Tests.Vb/Integration/DefinitionIntegrationTests.vb‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Imports System
2+
Imports System.Diagnostics
23
Imports System.IO
34
Imports System.Threading
45
Imports System.Threading.Tasks
@@ -178,6 +179,57 @@ Namespace VbNet.LanguageServer.Tests.Integration
178179
End If
179180
End Function
180181

182+
<Fact>
183+
Public Async Function GetDefinitionAsync_SdkVbConsoleWithCSharpNetStandardReference_NavigatesToCSharpSource() As Task
184+
Dim fixture = CreateSdkVbConsoleCSharpReferenceFixture()
185+
186+
Try
187+
Await RestoreProjectAsync(fixture.SolutionPath).ConfigureAwait(False)
188+
189+
Dim loaded = Await _workspaceManager.LoadSolutionAsync(fixture.SolutionPath).ConfigureAwait(False)
190+
Assert.True(loaded)
191+
192+
Dim programUri = New Uri(fixture.ProgramPath).ToString()
193+
Dim text = Await File.ReadAllTextAsync(fixture.ProgramPath).ConfigureAwait(False)
194+
195+
_documentManager.HandleDidOpen(New DidOpenTextDocumentParams With {
196+
.TextDocument = New TextDocumentItem With {
197+
.Uri = programUri,
198+
.LanguageId = "vb",
199+
.Version = 1,
200+
.Text = text
201+
}
202+
})
203+
204+
Dim lines = text.Split(ControlChars.Lf)
205+
Dim lineIndex = Array.FindIndex(lines, Function(line) line.Contains("Utils.GetMessage", StringComparison.Ordinal))
206+
Assert.True(lineIndex >= 0)
207+
208+
Dim getMessageIndex = lines(lineIndex).IndexOf("GetMessage", StringComparison.Ordinal)
209+
Dim request = New DefinitionParams With {
210+
.TextDocument = New TextDocumentIdentifier With {.Uri = programUri},
211+
.Position = New Position With {.Line = lineIndex, .Character = getMessageIndex + 2}
212+
}
213+
214+
Dim result = Await _definitionService.GetDefinitionAsync(request, CancellationToken.None).ConfigureAwait(False)
215+
216+
Assert.NotEmpty(result)
217+
Dim sourceLocation = result.FirstOrDefault(Function(location) Uri.UnescapeDataString(New Uri(location.Uri).LocalPath).EndsWith("Utils.cs", StringComparison.OrdinalIgnoreCase))
218+
Assert.NotNull(sourceLocation)
219+
220+
Dim utilsLines = (Await File.ReadAllTextAsync(fixture.UtilsPath).ConfigureAwait(False)).Split(ControlChars.Lf)
221+
Dim expectedLine = Array.FindIndex(utilsLines, Function(line) line.Contains("GetMessage()", StringComparison.Ordinal))
222+
Dim expectedCharacter = utilsLines(expectedLine).IndexOf("GetMessage", StringComparison.Ordinal)
223+
224+
Assert.Equal(expectedLine, sourceLocation.Range.Start.Line)
225+
Assert.Equal(expectedCharacter, sourceLocation.Range.Start.Character)
226+
Finally
227+
If Directory.Exists(fixture.RootPath) Then
228+
Directory.Delete(fixture.RootPath, recursive:=True)
229+
End If
230+
End Try
231+
End Function
232+
181233
<Fact>
182234
Public Async Function GetDefinitionAsync_ReturnsValidRange() As Task
183235
Dim projectPath = Path.Combine(TestProjectsRoot, "SmallProject", "SmallProject.vbproj")
@@ -224,6 +276,134 @@ Namespace VbNet.LanguageServer.Tests.Integration
224276
Assert.True(location.Range.End.Line >= location.Range.Start.Line)
225277
End If
226278
End Function
279+
280+
Private Shared Function CreateSdkVbConsoleCSharpReferenceFixture() As SdkVbConsoleCSharpReferenceFixture
281+
Dim root = Path.Combine(Path.GetTempPath(), "vbnet-lsp-tests", Guid.NewGuid().ToString("N"))
282+
283+
Try
284+
Dim consoleDir = Path.Combine(root, "Console", "CoreConsole2")
285+
Dim libDir = Path.Combine(root, "lib", "MyLib")
286+
Directory.CreateDirectory(consoleDir)
287+
Directory.CreateDirectory(libDir)
288+
289+
Dim programPath = Path.Combine(consoleDir, "Program.vb")
290+
Dim utilsPath = Path.Combine(libDir, "Utils.cs")
291+
Dim solutionPath = Path.Combine(consoleDir, "CoreConsole2.slnx")
292+
293+
File.WriteAllText(
294+
programPath,
295+
"Imports System" & Environment.NewLine &
296+
"Imports MyLib" & Environment.NewLine &
297+
Environment.NewLine &
298+
"Module Program" & Environment.NewLine &
299+
" Sub Main(args As String())" & Environment.NewLine &
300+
" Console.WriteLine(Utils.GetMessage())" & Environment.NewLine &
301+
" End Sub" & Environment.NewLine &
302+
"End Module")
303+
File.WriteAllText(
304+
Path.Combine(consoleDir, "CoreConsole2.vbproj"),
305+
"<Project Sdk=""Microsoft.NET.Sdk"">" & Environment.NewLine &
306+
" <PropertyGroup>" & Environment.NewLine &
307+
" <OutputType>Exe</OutputType>" & Environment.NewLine &
308+
" <RootNamespace>CoreConsole2</RootNamespace>" & Environment.NewLine &
309+
" <TargetFramework>net10.0</TargetFramework>" & Environment.NewLine &
310+
" </PropertyGroup>" & Environment.NewLine &
311+
" <ItemGroup>" & Environment.NewLine &
312+
" <ProjectReference Include=""..\..\lib\MyLib\MyLib.csproj"" />" & Environment.NewLine &
313+
" </ItemGroup>" & Environment.NewLine &
314+
"</Project>")
315+
File.WriteAllText(
316+
utilsPath,
317+
"using System;" & Environment.NewLine &
318+
Environment.NewLine &
319+
"namespace MyLib" & Environment.NewLine &
320+
"{" & Environment.NewLine &
321+
" public static class Utils" & Environment.NewLine &
322+
" {" & Environment.NewLine &
323+
" public static string GetMessage()" & Environment.NewLine &
324+
" {" & Environment.NewLine &
325+
" return ""Hello from MyLib (netstandard)"";" & Environment.NewLine &
326+
" }" & Environment.NewLine &
327+
" }" & Environment.NewLine &
328+
"}")
329+
File.WriteAllText(
330+
Path.Combine(libDir, "MyLib.csproj"),
331+
"<Project Sdk=""Microsoft.NET.Sdk"">" & Environment.NewLine &
332+
" <PropertyGroup>" & Environment.NewLine &
333+
" <TargetFramework>netstandard2.0</TargetFramework>" & Environment.NewLine &
334+
" <GenerateAssemblyInfo>false</GenerateAssemblyInfo>" & Environment.NewLine &
335+
" <RootNamespace>MyLib</RootNamespace>" & Environment.NewLine &
336+
" <AssemblyName>MyLib</AssemblyName>" & Environment.NewLine &
337+
" </PropertyGroup>" & Environment.NewLine &
338+
"</Project>")
339+
File.WriteAllText(
340+
solutionPath,
341+
"<Solution>" & Environment.NewLine &
342+
" <Project Path=""..\..\lib\MyLib\MyLib.csproj"" />" & Environment.NewLine &
343+
" <Project Path=""CoreConsole2.vbproj"" />" & Environment.NewLine &
344+
"</Solution>")
345+
346+
Return New SdkVbConsoleCSharpReferenceFixture With {
347+
.RootPath = root,
348+
.ProgramPath = programPath,
349+
.UtilsPath = utilsPath,
350+
.SolutionPath = solutionPath
351+
}
352+
Catch
353+
If Directory.Exists(root) Then
354+
Directory.Delete(root, recursive:=True)
355+
End If
356+
357+
Throw
358+
End Try
359+
End Function
360+
361+
Private NotInheritable Class SdkVbConsoleCSharpReferenceFixture
362+
Public Property RootPath As String
363+
Public Property ProgramPath As String
364+
Public Property UtilsPath As String
365+
Public Property SolutionPath As String
366+
End Class
367+
368+
Private Shared Async Function RestoreProjectAsync(projectOrSolutionPath As String) As Task
369+
Dim startInfo As New ProcessStartInfo With {
370+
.FileName = "dotnet",
371+
.RedirectStandardError = True,
372+
.RedirectStandardOutput = True,
373+
.UseShellExecute = False
374+
}
375+
startInfo.ArgumentList.Add("restore")
376+
startInfo.ArgumentList.Add(projectOrSolutionPath)
377+
378+
Using restoreProcess = Process.Start(startInfo)
379+
Assert.NotNull(restoreProcess)
380+
381+
Dim standardOutput = restoreProcess.StandardOutput.ReadToEndAsync()
382+
Dim standardError = restoreProcess.StandardError.ReadToEndAsync()
383+
384+
Using timeout As New CancellationTokenSource(TimeSpan.FromMinutes(2))
385+
Try
386+
Await restoreProcess.WaitForExitAsync(timeout.Token).ConfigureAwait(False)
387+
Catch ex As OperationCanceledException
388+
Try
389+
If Not restoreProcess.HasExited Then
390+
restoreProcess.Kill(entireProcessTree:=True)
391+
End If
392+
Catch
393+
End Try
394+
395+
Assert.True(False, "dotnet restore timed out for test fixture: " & projectOrSolutionPath)
396+
End Try
397+
End Using
398+
399+
Dim output = Await standardOutput.ConfigureAwait(False)
400+
Dim [error] = Await standardError.ConfigureAwait(False)
401+
402+
Assert.True(
403+
restoreProcess.ExitCode = 0,
404+
"dotnet restore failed for test fixture:" & Environment.NewLine & output & Environment.NewLine & [error])
405+
End Using
406+
End Function
227407
End Class
228408

229409
End Namespace

0 commit comments

Comments
 (0)