Skip to content

Commit 9ebee17

Browse files
authored
Merge pull request #926 from ahoppen/ahoppen/document-symbols-syntax
Implement DocumentSymbolsRequest using SwiftSyntax
2 parents 87dd95e + 973c382 commit 9ebee17

File tree

4 files changed

+671
-483
lines changed

4 files changed

+671
-483
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ add_library(SourceKitLSP STATIC
1010
SourceKitServer+Options.swift
1111
SourceKitServer.swift
1212
ToolchainLanguageServer.swift
13-
Workspace.swift)
13+
Workspace.swift
14+
)
1415
target_sources(SourceKitLSP PRIVATE
1516
Clang/ClangLanguageServer.swift)
1617
target_sources(SourceKitLSP PRIVATE
@@ -19,6 +20,7 @@ target_sources(SourceKitLSP PRIVATE
1920
Swift/CommentXML.swift
2021
Swift/CursorInfo.swift
2122
Swift/Diagnostic.swift
23+
Swift/DocumentSymbols.swift
2224
Swift/EditorPlaceholder.swift
2325
Swift/OpenInterface.swift
2426
Swift/SemanticRefactorCommand.swift
@@ -30,7 +32,8 @@ target_sources(SourceKitLSP PRIVATE
3032
Swift/SyntaxHighlightingToken.swift
3133
Swift/SyntaxHighlightingTokenParser.swift
3234
Swift/SyntaxTreeManager.swift
33-
Swift/VariableTypeInfo.swift)
35+
Swift/VariableTypeInfo.swift
36+
)
3437
set_target_properties(SourceKitLSP PROPERTIES
3538
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
3639
# TODO(compnerd) reduce the exposure here, why is everything PUBLIC-ly linked?
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SwiftSyntax
15+
import LSPLogging
16+
17+
extension SwiftLanguageServer {
18+
public func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? {
19+
guard let snapshot = self.documentManager.latestSnapshot(req.textDocument.uri) else {
20+
logger.error("failed to find snapshot for url \(req.textDocument.uri.forLogging)")
21+
return nil
22+
}
23+
24+
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
25+
26+
try Task.checkCancellation()
27+
return .documentSymbols(DocumentSymbolsFinder.find(in: [Syntax(syntaxTree)], snapshot: snapshot))
28+
}
29+
}
30+
31+
// MARK: - DocumentSymbolsFinder
32+
33+
fileprivate final class DocumentSymbolsFinder: SyntaxAnyVisitor {
34+
/// The snapshot of the document for which we are getting document symbols.
35+
private let snapshot: DocumentSnapshot
36+
37+
/// Accumulating the result in here.
38+
private var result: [DocumentSymbol] = []
39+
40+
private init(snapshot: DocumentSnapshot) {
41+
self.snapshot = snapshot
42+
super.init(viewMode: .sourceAccurate)
43+
}
44+
45+
/// Designated entry point for `DocumentSymbolFinder`.
46+
static func find(in nodes: some Sequence<Syntax>, snapshot: DocumentSnapshot) -> [DocumentSymbol] {
47+
let visitor = Self(snapshot: snapshot)
48+
for node in nodes {
49+
visitor.walk(node)
50+
}
51+
return visitor.result
52+
}
53+
54+
/// Add a symbol with the given parameters to the `result` array.
55+
private func record(
56+
node: some SyntaxProtocol,
57+
name: String,
58+
symbolKind: SymbolKind,
59+
range: Range<AbsolutePosition>,
60+
selection: Range<AbsolutePosition>
61+
) -> SyntaxVisitorContinueKind {
62+
guard let rangeLowerBound = snapshot.position(of: range.lowerBound),
63+
let rangeUpperBound = snapshot.position(of: range.upperBound),
64+
let selectionLowerBound = snapshot.position(of: selection.lowerBound),
65+
let selectionUpperBound = snapshot.position(of: selection.upperBound)
66+
else {
67+
return .skipChildren
68+
}
69+
70+
let children = DocumentSymbolsFinder.find(in: node.children(viewMode: .sourceAccurate), snapshot: snapshot)
71+
72+
result.append(
73+
DocumentSymbol(
74+
name: name,
75+
kind: symbolKind,
76+
range: rangeLowerBound..<rangeUpperBound,
77+
selectionRange: selectionLowerBound..<selectionUpperBound,
78+
children: children
79+
)
80+
)
81+
return .skipChildren
82+
}
83+
84+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
85+
guard let node = node.asProtocol(NamedDeclSyntax.self) else {
86+
return .visitChildren
87+
}
88+
let symbolKind: SymbolKind? = switch node.kind {
89+
case .actorDecl: .class
90+
case .associatedTypeDecl: .typeParameter
91+
case .classDecl: .class
92+
case .enumDecl: .enum
93+
case .macroDecl: .function // LSP doesn't have a macro symbol kind. Function is the closest.
94+
case .operatorDecl: .operator
95+
case .precedenceGroupDecl: .operator // LSP doesn't have a precedence group symbol kind. Operator is the closest.
96+
case .protocolDecl: .interface
97+
case .structDecl: .struct
98+
case .typeAliasDecl: .typeParameter // LSP doesn't have a typealias symbol kind. Type parameter is the closest.
99+
default: nil
100+
}
101+
102+
guard let symbolKind else {
103+
return .visitChildren
104+
}
105+
return record(
106+
node: node,
107+
name: node.name.text,
108+
symbolKind: symbolKind,
109+
range: node.rangeWithoutTrivia,
110+
selection: node.name.rangeWithoutTrivia
111+
)
112+
}
113+
114+
override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
115+
let rangeEnd =
116+
if let parameterClause = node.parameterClause {
117+
parameterClause.endPositionBeforeTrailingTrivia
118+
} else {
119+
node.name.endPositionBeforeTrailingTrivia
120+
}
121+
122+
return record(
123+
node: node,
124+
name: node.qualifiedDeclName,
125+
symbolKind: .enumMember,
126+
range: node.name.positionAfterSkippingLeadingTrivia..<rangeEnd,
127+
selection: node.name.positionAfterSkippingLeadingTrivia..<rangeEnd
128+
)
129+
}
130+
131+
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
132+
return record(
133+
node: node,
134+
name: node.extendedType.trimmedDescription,
135+
symbolKind: .namespace,
136+
range: node.rangeWithoutTrivia,
137+
selection: node.extendedType.rangeWithoutTrivia
138+
)
139+
}
140+
141+
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
142+
let kind: SymbolKind = if node.name.tokenKind.isOperator {
143+
.operator
144+
} else if node.parent?.is(MemberBlockItemSyntax.self) ?? false {
145+
.method
146+
} else {
147+
.function
148+
}
149+
return record(
150+
node: node,
151+
name: node.qualifiedDeclName,
152+
symbolKind: kind,
153+
range: node.rangeWithoutTrivia,
154+
selection: node.name
155+
.positionAfterSkippingLeadingTrivia..<node.signature.parameterClause.endPositionBeforeTrailingTrivia
156+
)
157+
}
158+
159+
override func visit(_ node: GenericParameterSyntax) -> SyntaxVisitorContinueKind {
160+
return record(
161+
node: node,
162+
name: node.name.text,
163+
symbolKind: .typeParameter,
164+
range: node.rangeWithoutTrivia,
165+
selection: node.rangeWithoutTrivia
166+
)
167+
}
168+
169+
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
170+
return record(
171+
node: node,
172+
name: node.initKeyword.text,
173+
symbolKind: .constructor,
174+
range: node.rangeWithoutTrivia,
175+
selection: node.initKeyword
176+
.positionAfterSkippingLeadingTrivia..<node.signature.parameterClause.endPositionBeforeTrailingTrivia
177+
)
178+
}
179+
180+
override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind {
181+
// If there is only one pattern binding within the variable decl, consider the entire variable decl as the
182+
// referenced range. If there are multiple, consider each pattern binding separately since the `var` keyword doesn't
183+
// belong to any pattern binding in particular.
184+
guard let variableDecl = node.parent?.parent?.as(VariableDeclSyntax.self) else {
185+
return .visitChildren
186+
}
187+
let rangeNode: Syntax = variableDecl.bindings.count == 1 ? Syntax(variableDecl) : Syntax(node)
188+
189+
return record(
190+
node: node,
191+
name: node.pattern.trimmedDescription,
192+
symbolKind: variableDecl.parent?.is(MemberBlockItemSyntax.self) ?? false ? .property : .variable,
193+
range: rangeNode.rangeWithoutTrivia,
194+
selection: node.pattern.rangeWithoutTrivia
195+
)
196+
}
197+
}
198+
199+
// MARK: - Syntax Utilities
200+
201+
fileprivate extension EnumCaseElementSyntax {
202+
var qualifiedDeclName: String {
203+
var result = self.name.text
204+
if let parameterClause {
205+
result += "("
206+
for parameter in parameterClause.parameters {
207+
result += "\(parameter.firstName?.text ?? "_"):"
208+
}
209+
result += ")"
210+
}
211+
return result
212+
}
213+
}
214+
215+
fileprivate extension FunctionDeclSyntax {
216+
var qualifiedDeclName: String {
217+
var result = self.name.text
218+
result += "("
219+
for parameter in self.signature.parameterClause.parameters {
220+
result += "\(parameter.firstName.text):"
221+
}
222+
result += ")"
223+
return result
224+
}
225+
}
226+
227+
fileprivate extension SyntaxProtocol {
228+
/// The position range of this node without its leading and trailing trivia.
229+
var rangeWithoutTrivia: Range<AbsolutePosition> {
230+
return positionAfterSkippingLeadingTrivia..<endPositionBeforeTrailingTrivia
231+
}
232+
}
233+
234+
fileprivate extension TokenKind {
235+
var isOperator: Bool {
236+
switch self {
237+
case .prefixOperator, .binaryOperator, .postfixOperator: return true
238+
default: return false
239+
}
240+
}
241+
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -529,96 +529,6 @@ extension SwiftLanguageServer {
529529
return [cursorInfo.symbolInfo]
530530
}
531531

532-
public func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? {
533-
guard let snapshot = self.documentManager.latestSnapshot(req.textDocument.uri) else {
534-
throw ResponseError.unknown("failed to find snapshot for url \(req.textDocument.uri.forLogging)")
535-
}
536-
537-
let helperDocumentName = "DocumentSymbols:" + snapshot.uri.pseudoPath
538-
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
539-
skreq[keys.request] = self.requests.editor_open
540-
skreq[keys.name] = helperDocumentName
541-
skreq[keys.sourcetext] = snapshot.text
542-
skreq[keys.syntactic_only] = 1
543-
544-
let dict = try await self.sourcekitd.send(skreq)
545-
defer {
546-
let closeHelperReq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
547-
closeHelperReq[self.keys.request] = self.requests.editor_close
548-
closeHelperReq[self.keys.name] = helperDocumentName
549-
// FIXME: (async) We might receive two concurrent document symbol requests for the
550-
// same document, in which race to open/close a document with the same name in
551-
// sourcekitd. The solution is to either
552-
// - Not open the helper document and instead rely on the document that is already
553-
// open or
554-
// - Prefix the helper document with a UUID to make sure the two concurrent
555-
// requests operate on different documents as far as sourcekitd is concerned.
556-
Task {
557-
_ = try await self.sourcekitd.send(closeHelperReq)
558-
}
559-
}
560-
561-
guard let results: SKDResponseArray = dict[self.keys.substructure] else {
562-
return .documentSymbols([])
563-
}
564-
565-
func documentSymbol(value: SKDResponseDictionary) -> DocumentSymbol? {
566-
guard let name: String = value[self.keys.name],
567-
let uid: sourcekitd_uid_t = value[self.keys.kind],
568-
let kind: SymbolKind = uid.asSymbolKind(self.values),
569-
let offset: Int = value[self.keys.offset],
570-
let start: Position = snapshot.positionOf(utf8Offset: offset),
571-
let length: Int = value[self.keys.length],
572-
let end: Position = snapshot.positionOf(utf8Offset: offset + length)
573-
else {
574-
return nil
575-
}
576-
577-
let range = start..<end
578-
let selectionRange: Range<Position>
579-
if let nameOffset: Int = value[self.keys.nameoffset],
580-
let nameStart: Position = snapshot.positionOf(utf8Offset: nameOffset),
581-
let nameLength: Int = value[self.keys.namelength],
582-
let nameEnd: Position = snapshot.positionOf(utf8Offset: nameOffset + nameLength)
583-
{
584-
selectionRange = nameStart..<nameEnd
585-
} else {
586-
selectionRange = range
587-
}
588-
589-
let children: [DocumentSymbol]
590-
if let substructure: SKDResponseArray = value[self.keys.substructure] {
591-
children = documentSymbols(array: substructure)
592-
} else {
593-
children = []
594-
}
595-
return DocumentSymbol(
596-
name: name,
597-
detail: value[self.keys.typename] as String?,
598-
kind: kind,
599-
deprecated: nil,
600-
range: range,
601-
selectionRange: selectionRange,
602-
children: children
603-
)
604-
}
605-
606-
func documentSymbols(array: SKDResponseArray) -> [DocumentSymbol] {
607-
var result: [DocumentSymbol] = []
608-
array.forEach { (i: Int, value: SKDResponseDictionary) in
609-
if let documentSymbol = documentSymbol(value: value) {
610-
result.append(documentSymbol)
611-
} else if let substructure: SKDResponseArray = value[self.keys.substructure] {
612-
result += documentSymbols(array: substructure)
613-
}
614-
return true
615-
}
616-
return result
617-
}
618-
619-
return .documentSymbols(documentSymbols(array: results))
620-
}
621-
622532
public func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] {
623533
guard let snapshot = self.documentManager.latestSnapshot(req.textDocument.uri) else {
624534
logger.error("failed to find snapshot for url \(req.textDocument.uri.forLogging)")

0 commit comments

Comments
 (0)