Skip to content
Draft
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
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ var targets: [Target] = [
"SwiftBasicFormat",
"SwiftDiagnostics",
"SwiftIDEUtils",
"SwiftLexicalLookup",
"SwiftParser",
"SwiftParserDiagnostics",
"SwiftRefactor",
Expand Down Expand Up @@ -785,7 +786,8 @@ var dependencies: [Package.Dependency] {
.package(url: "https://github.com/swiftlang/swift-tools-protocols.git", exact: "0.0.9"),
.package(url: "https://github.com/swiftlang/swift-tools-support-core.git", branch: relatedDependenciesBranch),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch),
// .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch),
.package(path: "../swift-syntax"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
// Not a build dependency. Used so the "Format Source Code" command plugin can be used to format sourcekit-lsp
.package(url: "https://github.com/swiftlang/swift-format.git", branch: relatedDependenciesBranch),
Expand Down
70 changes: 70 additions & 0 deletions Sources/SwiftLanguageService/SwiftLanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ package import SwiftSyntax
package import ToolchainRegistry
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions

@_spi(Experimental) import SwiftLexicalLookup

#if os(Windows)
import WinSDK
#endif
Expand Down Expand Up @@ -875,6 +877,13 @@ extension SwiftLanguageService {

package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? {
let snapshot = try await self.latestSnapshot(for: req.textDocument.uri)

if let highlights = try await controlFlowExitKeywordHighlight(
at: req.position,
in: snapshot
) {
return highlights
}

let relatedIdentifiers = try await self.relatedIdentifiers(
at: req.position,
Expand All @@ -889,6 +898,67 @@ extension SwiftLanguageService {
}
}

private func controlFlowExitKeywordHighlight(
at position: Position,
in snapshot: DocumentSnapshot
) async throws -> [DocumentHighlight]? {
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)

guard let tokenSyntax = syntaxTree.token(at: snapshot.absolutePosition(of: position)) else {
return nil
}

guard let parent = Syntax(tokenSyntax).parent, let targetStructure = parent.lookupControlStructure() else {
return nil
}
class ControlFlowHighlighter: SyntaxVisitor {
var highlights: [DocumentHighlight] = []
let targetStructure: Syntax
let snapshot: DocumentSnapshot

init(targetStructure: Syntax, snapshot: DocumentSnapshot) {
self.targetStructure = targetStructure
self.snapshot = snapshot
super.init(viewMode: .sourceAccurate)
}

private func addHighlightIfMatches(_ node: some SyntaxProtocol) {
if node.lookupControlStructure() == targetStructure {
Copy link
Member

Choose a reason for hiding this comment

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

This could be a guard to avoid the nesting level.

highlights.append(
DocumentHighlight(
range: snapshot.absolutePositionRange(of: node.firstToken(viewMode: .sourceAccurate).map { $0.positionAfterSkippingLeadingTrivia..<$0.endPositionBeforeTrailingTrivia } ?? node.positionAfterSkippingLeadingTrivia..<node.endPositionBeforeTrailingTrivia),
Copy link
Member

Choose a reason for hiding this comment

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

Could you run swift-format on your changes?

Copy link
Member

Choose a reason for hiding this comment

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

While currently the return/break etc keyword is always the first token in the syntax node, this may change in the future. I’d prefer if we explicitly pass in the token to highlight.

Copy link
Member

Choose a reason for hiding this comment

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

Instead of $0.positionAfterSkippingLeadingTrivia..<$0.endPositionBeforeTrailingTrivia you should be able to do $0.trimmedRange or something like that.

kind: .read
)
)
}
}

override func visit(_ node: BreakStmtSyntax) -> SyntaxVisitorContinueKind {
Copy link
Member

Choose a reason for hiding this comment

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

You shouldn’t need to repeat the list of control flow nodes if this is a SyntaxAnyVisitor and you add a visitAny override.

addHighlightIfMatches(node)
return .skipChildren
}

override func visit(_ node: ContinueStmtSyntax) -> SyntaxVisitorContinueKind {
addHighlightIfMatches(node)
return .skipChildren
}

override func visit(_ node: ReturnStmtSyntax) -> SyntaxVisitorContinueKind {
addHighlightIfMatches(node)
return .skipChildren
}

override func visit(_ node: ThrowStmtSyntax) -> SyntaxVisitorContinueKind {
addHighlightIfMatches(node)
return .skipChildren
}
}

let highlighter = ControlFlowHighlighter(targetStructure: targetStructure, snapshot: snapshot)
highlighter.walk(targetStructure)
return highlighter.highlights.isEmpty ? nil : highlighter.highlights
}

package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? {
if (try? ReferenceDocumentURL(from: req.textDocument.uri)) != nil {
// Do not show code actions in reference documents
Expand Down