-
Notifications
You must be signed in to change notification settings - Fork 320
Fix module definition jumping in Swift interface files #2233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
* fixed second swiftinterface page won't response * fixed tap the same name of swiftinterface, it would open another swiftinterface with same content
@swift-ci Please test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR @aelam. The screen recording looks great. 🚀 I left a few comments mostly about the subtleties between generated interfaces and build settings inline.
Could you also add a test case for the new functionality? You should be able to follow the structure in SwiftInterfaceTests.testSemanticFunctionalityInGeneratedInterface
.
guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else { | ||
// Cache by module name and group name, not the full document data | ||
// This allows reuse across different buildSettingsFrom URIs | ||
guard let cachedIndex = openInterfaces.firstIndex(where: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think this is correct. Depending on the build settings used to retrieve the generated interface, the interface might look differently. I can’t come up with a great real-world example right now where the contents differ but a canonical example would be that we can’t load the generated interface if we are missing an include path to one of the module’s dependencies but we can generate the interface if all build settings are present.
But if we continue passing along the same buildSettingsFrom
file, I don’t think we need this change. Or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right
but about same buildSettingsFrom
file please check here #2233 (comment)
Sources/LanguageServerProtocolExtensions/Language+Inference.swift
Outdated
Show resolved
Hide resolved
@@ -21,10 +21,18 @@ extension SwiftLanguageService { | |||
groupName: String?, | |||
symbolUSR symbol: String? | |||
) async throws -> GeneratedInterfaceDetails? { | |||
// Generate a deterministic document name based on module name and group name | |||
// This ensures we reuse the same interface for the same module/group combination | |||
let documentName = if let groupName { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to my comment above, generated interfaces might look different depending on the build settings that were used to generate them, so module name and group name don’t uniquely identify its contents. That’s why we used the UUID-based filename below.
@@ -165,13 +165,14 @@ extension DocumentURI { | |||
return referenceDocument.buildSettingsFile | |||
} | |||
return self | |||
} /// Convert sourcekit-lsp:// URIs to actual file system paths when possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newlines before ///
?
} /// Convert sourcekit-lsp:// URIs to actual file system paths when possible. | ||
/// For generated Swift interfaces, this returns the path where the interface file | ||
/// would be stored in the GeneratedInterfaces directory. | ||
var actualFileSystemPath: DocumentURI { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think this is used anywhere, or am I missing something?
@@ -553,7 +553,7 @@ package actor SourceKitLSPServer { | |||
} | |||
|
|||
let toolchain = await workspace.buildServerManager.toolchain( | |||
for: uri, | |||
for: uri.buildSettingsFile, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change necessary? I just checked and it looks like toolchain(for:in:language:)
doesn’t even use uri
right now.
// Check if we're already in the target interface with the same module/group/symbol | ||
if let referenceDoc = try? ReferenceDocumentURL(from: originatorUri), | ||
case .generatedInterface(let interfaceData) = referenceDoc, | ||
interfaceData.moduleName == moduleName && interfaceData.groupName == groupName { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly to my other comments about the build settings, we need to check here that we are using build settings from the same original file. But I think as long as we pass along the same build settings file, this shouldn’t cause any issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ahoppen Thanks for the comment
without this change, here would happen
if I open defination of Foundation
in Foundation.swiftInterace
, it will open another one, repeat, it will open another new one
even if it's acceptable but it's quite slow (please check the video)
Cause:
originalUri is sourcekit-lsp://xxx?buildSettingsFrom=A
A is sourcekit-lsp://xxx?buildSettingsFrom=B
B is sourcekit-lsp://xxx?buildSettingsFrom=C
C is file://
this makes the logic quite complicated
- we have to recursively find the
buildSettingsFrom
- originalUri are different for same
swiftinterface
I think we could assume the interface is the same if the interface was originally opened from a same buildSettingsFrom
because interface opened from A, B should always be consistent?
then we could simplify the originalUri as
originalUri is sourcekit-lsp://xxx?buildSettingsFrom=C
the benefits are
- we don't need to resursively search
- caching/creating of
swiftinterface
could be simplified and easy to hit the cache - much faster
- the code change here is not needed any more.
Untitled.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, IMO when doing a jump to definition from within an interface, the buildSettingsFrom
should be the original source file rather than the interface you're currently in. IIUC this is what you're saying in:
then we could simplify the originalUri as
originalUri is sourcekit-lsp://xxx?buildSettingsFrom=C
? If so, then 👍
|
||
// If we have a specific symbol USR, try to find its position in the current interface | ||
if let symbolUSR = symbolUSR, | ||
let swiftLanguageService = languageService as? SwiftLanguageService { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m currently trying to unravel the dependency of the SourceKitLSP
module on SwiftLanguageService
in #2234, so getting the SwiftLanguageService
won’t work in the future.
What are the challenges of always going through openGeneratedInterface
? I can see the following but I might be missing some:
- Every time
openGeneratedInterface
is called, we get a newsourcekitdDocumentName
. We could fix that by instead of appending the group name and a hash ofdocument
(or something of that sort). - If the issue is that we always write a new file if the client doesn’t support reference documents, we could just not write out the file if the contents haven’t changed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adding a group might be a good idea
i imagine there could be different versions of swiftinterface which are used by different modules/files
i think the challenge is how to determine it in the beginning instead of a uuid
then the cache logic could be simplified
case .generatedInterface(let data): | ||
if let referenceDocumentURL = try? ReferenceDocumentURL(from: data.buildSettingsFrom) { | ||
return referenceDocumentURL.buildSettingsFile | ||
} | ||
return data.buildSettingsFrom | ||
} | ||
} | ||
|
||
var primaryFile: DocumentURI? { | ||
switch self { | ||
case .macroExpansion(let data): return data.primaryFile | ||
case .generatedInterface(let data): return data.buildSettingsFrom.primaryFile | ||
case .generatedInterface(let data): | ||
if let referenceDocumentURL = try? ReferenceDocumentURL(from: data.buildSettingsFrom) { | ||
return referenceDocumentURL.primaryFile | ||
} | ||
return data.buildSettingsFrom.primaryFile |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if let referenceDocumentURL = try? ReferenceDocumentURL(from: primaryFile), | ||
let fileURL = referenceDocumentURL.buildSettingsFile.fileURL { | ||
self.buildSettingsFrom = DocumentURI(fileURL) | ||
} else if let fileURL = primaryFile.fileURL { | ||
self.buildSettingsFrom = DocumentURI(fileURL) | ||
} else { | ||
self.buildSettingsFrom = primaryFile | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make sure buildSettingsFrom can be only an actual file otherwise
it could be
sourcekit-lsp://xxx?buildSettingsFrom=A
A=sourcekit-lsp://xxx?buildSettingsFrom=B
NOTE: this happens if we enable navigation inside swiftinterface in this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your continued work on this PR and adding the tests, @aelam. I left some comments, mostly on the tests. Also, addressing the comments in SourceKitLSPServer.swift would be important to make the next review round meaningful.
self.buildSettingsFrom = primaryFile | ||
if let referenceDocumentURL = try? ReferenceDocumentURL(from: primaryFile), | ||
let fileURL = referenceDocumentURL.buildSettingsFile.fileURL { | ||
self.buildSettingsFrom = DocumentURI(fileURL) | ||
} else if let fileURL = primaryFile.fileURL { | ||
self.buildSettingsFrom = DocumentURI(fileURL) | ||
} else { | ||
self.buildSettingsFrom = primaryFile | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have an accessor on DocumentURI
that should do exactly the same, so you should be able to simplify this to
self.buildSettingsFrom = primaryFile | |
if let referenceDocumentURL = try? ReferenceDocumentURL(from: primaryFile), | |
let fileURL = referenceDocumentURL.buildSettingsFile.fileURL { | |
self.buildSettingsFrom = DocumentURI(fileURL) | |
} else if let fileURL = primaryFile.fileURL { | |
self.buildSettingsFrom = DocumentURI(fileURL) | |
} else { | |
self.buildSettingsFrom = primaryFile | |
} | |
self.buildSettingsFrom = primaryFile.buildSettingsFile |
Sources/SKTestSupport/Utils.swift
Outdated
@@ -32,7 +32,7 @@ extension Language { | |||
case "cpp": self = .cpp | |||
case "m": self = .objective_c | |||
case "mm": self = .objective_cpp | |||
case "swift": self = .swift | |||
case "swift", "swiftinterface": self = .swift |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to my other comment about the .swiftinterface
extension, I don’t think we need this here, do we? This only applies to tests and I don’t think we open any .swiftinterface
files in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I deleted but please check the tests about swiftinterface
) | ||
|
||
// Find a symbol within the interface (e.g., "init" method in String) | ||
guard let initRange = interfaceContents.content.range(of: "public init()") else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we pick a function that’s unique within the Swift interface (eg. public func withUTF8Buffer
) to make this test less likely to change if new initializers get introduced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I deleted this tests, maybe it's duplicated
// 1. Return a position within the same interface (internal navigation) | ||
// 2. Return the same position if it's already at the definition | ||
// 3. Return nil if no definition is available |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test should deterministically return the same results, in which case only one of these checks should be true. Could we remove the other options?
} | ||
} | ||
|
||
func testFoundationImportNavigation() async throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anything in this test that isn’t already covered by testSwiftInterfaceAcrossModules
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleted
fixed format issues delete unnecessary unit tests
1c667dc
to
36a5f83
Compare
I went through all but maybe I missed any you may want to highlight? Sorry about too many unrelated changes, I just reverted a lot of unneccessary change and |
Fix module definition jumping for submodules and Swift interface files
Problem Description
1. Submodule Import Navigation Issues
NSArray
inimport Foundation.NSArray
fails to jump to definitionsysdir
inimport Darwin.sysdir
fails to jump to definition2. Swift Interface File Navigation Problems
.swiftinterface
filesSolution
1. Enhanced Module Symbol Processing
File:
Sources/SourceKitLSP/SourceKitLSPServer.swift
Module Name Parsing Logic
System Module Navigation
2. Swift Interface File Support
Generated Interface Manager Enhancement
File:
Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift
Language Service Mapping Fix
File:
Sources/SourceKitLSP/Workspace.swift
3. Interface File Recognition
File:
Sources/SourceKitLSP/Swift/OpenInterface.swift
Test Cases
1. Foundation Submodules
✅
import Foundation.NSArray
→ Correctly jumps to NSArray interface✅
import Foundation.NSURL
→ Correctly jumps to NSURL interface2. Darwin Submodules
✅
import Darwin.sysdir
→ Correctly jumps to sysdir interface✅
import Darwin.uuid
→ Correctly jumps to uuid interface✅
import Darwin.POSIX
→ Correctly jumps to POSIX interface3. Swift Interface Navigation
✅ Command+Click navigation works properly within generated
.swiftinterface
files✅ Avoids duplicate generation of interface files for the same module
✅ Symbols in interface files can correctly jump to definitions
Technical Details
Key Improvements
module.submodule
format import statementssystemModule
information for accurate module navigation.swiftinterface
file typesBackward Compatibility
Impact
Feature Improvements
Related Issues
Testing Recommendations
It is recommended to test in the following scenarios: