Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Sources/SourceKitLSP/LanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ package protocol LanguageService: AnyObject, Sendable {
manager: OnDiskDocumentManager
) async throws -> String

/// Check if the given position is on a literal value (string, integer, boolean, etc.).
///
/// Literal values should not support jump-to-definition since they don't have a "definition" location.
func isPositionOnLiteral(_ position: Position, in uri: DocumentURI) async -> Bool

/// Request a generated interface of a module to display in the IDE.
///
/// - Parameters:
Expand Down Expand Up @@ -395,6 +400,10 @@ package extension LanguageService {
throw ResponseError.requestNotImplemented(SymbolInfoRequest.self)
}

func isPositionOnLiteral(_ position: Position, in uri: DocumentURI) async -> Bool {
return false
}

func symbolGraph(
for snapshot: DocumentSnapshot,
at position: Position
Expand Down
5 changes: 5 additions & 0 deletions Sources/SourceKitLSP/SourceKitLSPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,11 @@ extension SourceKitLSPServer {
workspace: Workspace,
languageService: any LanguageService
) async throws -> [Location] {
// Check if the position is on a literal value (string, integer, etc.)
if await languageService.isPositionOnLiteral(req.position, in: req.textDocument.uri) {
return []
}
Comment on lines +2047 to +2050
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do this check in SwiftLanguageService.symbolInfo. I think we want to suppress the jump-to-definition kind of behavior as well for all the callers of symbolInfo (implementation, definition, call hierarchy, type hierarchy, find references).


let symbols = try await languageService.symbolInfo(
SymbolInfoRequest(
textDocument: req.textDocument,
Expand Down
60 changes: 60 additions & 0 deletions Sources/SwiftLanguageService/SwiftLanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1293,3 +1293,63 @@ extension sourcekitd_api_uid_t {
}
}
}

// MARK: - Literal Detection

extension SwiftLanguageService {
/// Check if the given position in the document is on a literal expression.
///
/// Literal expressions (string, integer, boolean, float, array, dictionary) should not support
/// jump-to-definition since they don't have a "definition" location - they are values typed
/// directly in the code.
///
package func isPositionOnLiteral(
_ position: Position,
in uri: DocumentURI
) async -> Bool {
guard let snapshot = try? await latestSnapshot(for: uri) else {
return false
}

let tree = await syntaxTreeManager.syntaxTree(for: snapshot)
let absolutePosition = snapshot.absolutePosition(of: position)

// Helper function to check if a token is part of a literal expression
func isTokenInLiteral(_ token: TokenSyntax) -> Bool {
var currentNode: Syntax? = Syntax(token)
while let node = currentNode {
// Check all literal expression types
if node.is(StringLiteralExprSyntax.self)
|| node.is(IntegerLiteralExprSyntax.self)
|| node.is(FloatLiteralExprSyntax.self)
|| node.is(BooleanLiteralExprSyntax.self)
|| node.is(ArrayExprSyntax.self)
|| node.is(DictionaryExprSyntax.self)
|| node.is(NilLiteralExprSyntax.self)
{
return true
}
currentNode = node.parent
}
return false
Comment on lines +1320 to +1334
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also prevent all jump-to-definition functionality for code inside literals, eg. jump-to-definition will no longer work inside literals, eg. you couldn’t jump to the definition of foo anymore in "Value: \(foo)" or [foo]. Instead, what I think we should do, is to check if the tokenKind is stringSegment, integerLiteral or floatLiteral. Maybe you can find other token kinds as well that we should suppress as well, I didn’t walk through all of them.

}

// Find the token at this position
if let token = tree.token(at: absolutePosition) {
if isTokenInLiteral(token) {
return true
}
}

// Also check the token before the cursor position, as users might click right after a literal
if absolutePosition.utf8Offset > 0,
let tokenBefore = tree.token(at: AbsolutePosition(utf8Offset: absolutePosition.utf8Offset - 1))
{
if isTokenInLiteral(tokenBefore) {
return true
}
}
Comment on lines +1344 to +1351
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that’s quite unfortunate. I would prefer if we could find this previous token using token.previousToken(viewMode: .sourceAccurate) because it will not need to do a tree traversal to find the token again and should thus be more efficient. It likely won’t matter much in practice but still.


return false
}
}
39 changes: 39 additions & 0 deletions Tests/SourceKitLSPTests/DefinitionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -689,4 +689,43 @@ class DefinitionTests: SourceKitLSPTestCase {
)
XCTAssertEqual(response?.locations?.map(\.uri), [try project.uri(for: "Test.h")])
}

func testDefinitionOnLiteralsShouldReturnEmpty() async throws {
let testClient = try await TestSourceKitLSPClient()
let uri = DocumentURI(for: .swift)

let positions = testClient.openDocument(
"""
let message = "Hello"1️⃣
let count = 422️⃣
let flag = true3️⃣
let items = [1, 2, 3]4️⃣
""",
uri: uri
)

// Test string literal
let stringResponse = try await testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
XCTAssertNil(stringResponse, "Jump-to-definition should not work on string literals")

// Test integer literal
let intResponse = try await testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
)
XCTAssertNil(intResponse, "Jump-to-definition should not work on integer literals")

// Test boolean literal
let boolResponse = try await testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["3️⃣"])
)
XCTAssertNil(boolResponse, "Jump-to-definition should not work on boolean literals")

// Test array literal
let arrayResponse = try await testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["4️⃣"])
)
XCTAssertNil(arrayResponse, "Jump-to-definition should not work on array literals")
}
}