Skip to content

Commit 973c382

Browse files
committed
Implement DocumentSymbolsRequest using SwiftSyntax
1 parent 5a8ca10 commit 973c382

File tree

4 files changed

+259
-105
lines changed

4 files changed

+259
-105
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
@@ -516,96 +516,6 @@ extension SwiftLanguageServer {
516516
return [cursorInfo.symbolInfo]
517517
}
518518

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

0 commit comments

Comments
 (0)