Skip to content

Implement DocumentSymbolsRequest using SwiftSyntax #926

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

Merged
merged 2 commits into from
Oct 26, 2023
Merged
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
7 changes: 5 additions & 2 deletions Sources/SourceKitLSP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ add_library(SourceKitLSP STATIC
SourceKitServer+Options.swift
SourceKitServer.swift
ToolchainLanguageServer.swift
Workspace.swift)
Workspace.swift
)
target_sources(SourceKitLSP PRIVATE
Clang/ClangLanguageServer.swift)
target_sources(SourceKitLSP PRIVATE
Expand All @@ -19,6 +20,7 @@ target_sources(SourceKitLSP PRIVATE
Swift/CommentXML.swift
Swift/CursorInfo.swift
Swift/Diagnostic.swift
Swift/DocumentSymbols.swift
Swift/EditorPlaceholder.swift
Swift/OpenInterface.swift
Swift/SemanticRefactorCommand.swift
Expand All @@ -30,7 +32,8 @@ target_sources(SourceKitLSP PRIVATE
Swift/SyntaxHighlightingToken.swift
Swift/SyntaxHighlightingTokenParser.swift
Swift/SyntaxTreeManager.swift
Swift/VariableTypeInfo.swift)
Swift/VariableTypeInfo.swift
)
set_target_properties(SourceKitLSP PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
# TODO(compnerd) reduce the exposure here, why is everything PUBLIC-ly linked?
Expand Down
241 changes: 241 additions & 0 deletions Sources/SourceKitLSP/Swift/DocumentSymbols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import LanguageServerProtocol
import SwiftSyntax
import LSPLogging

extension SwiftLanguageServer {
public func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? {
guard let snapshot = self.documentManager.latestSnapshot(req.textDocument.uri) else {
logger.error("failed to find snapshot for url \(req.textDocument.uri.forLogging)")
Copy link
Contributor

Choose a reason for hiding this comment

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

We have this in so many places. Is requesting a snapshot and not finding it ever not an error? ie. should we just always log?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea. Here we go: #943

return nil
}

let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)

try Task.checkCancellation()
return .documentSymbols(DocumentSymbolsFinder.find(in: [Syntax(syntaxTree)], snapshot: snapshot))
}
}

// MARK: - DocumentSymbolsFinder

fileprivate final class DocumentSymbolsFinder: SyntaxAnyVisitor {
/// The snapshot of the document for which we are getting document symbols.
private let snapshot: DocumentSnapshot

/// Accumulating the result in here.
private var result: [DocumentSymbol] = []

private init(snapshot: DocumentSnapshot) {
self.snapshot = snapshot
super.init(viewMode: .sourceAccurate)
}

/// Designated entry point for `DocumentSymbolFinder`.
static func find(in nodes: some Sequence<Syntax>, snapshot: DocumentSnapshot) -> [DocumentSymbol] {
let visitor = Self(snapshot: snapshot)
for node in nodes {
visitor.walk(node)
}
return visitor.result
}

/// Add a symbol with the given parameters to the `result` array.
private func record(
node: some SyntaxProtocol,
name: String,
symbolKind: SymbolKind,
range: Range<AbsolutePosition>,
selection: Range<AbsolutePosition>
) -> SyntaxVisitorContinueKind {
guard let rangeLowerBound = snapshot.position(of: range.lowerBound),
let rangeUpperBound = snapshot.position(of: range.upperBound),
let selectionLowerBound = snapshot.position(of: selection.lowerBound),
let selectionUpperBound = snapshot.position(of: selection.upperBound)
else {
return .skipChildren
}

let children = DocumentSymbolsFinder.find(in: node.children(viewMode: .sourceAccurate), snapshot: snapshot)

result.append(
DocumentSymbol(
name: name,
kind: symbolKind,
range: rangeLowerBound..<rangeUpperBound,
selectionRange: selectionLowerBound..<selectionUpperBound,
children: children
)
)
return .skipChildren
}

override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
guard let node = node.asProtocol(NamedDeclSyntax.self) else {
return .visitChildren
}
let symbolKind: SymbolKind? = switch node.kind {
case .actorDecl: .class
case .associatedTypeDecl: .typeParameter
case .classDecl: .class
case .enumDecl: .enum
case .macroDecl: .function // LSP doesn't have a macro symbol kind. Function is the closest.
case .operatorDecl: .operator
case .precedenceGroupDecl: .operator // LSP doesn't have a precedence group symbol kind. Operator is the closest.
case .protocolDecl: .interface
case .structDecl: .struct
case .typeAliasDecl: .typeParameter // LSP doesn't have a typealias symbol kind. Type parameter is the closest.
default: nil
}

guard let symbolKind else {
return .visitChildren
}
return record(
node: node,
name: node.name.text,
symbolKind: symbolKind,
range: node.rangeWithoutTrivia,
selection: node.name.rangeWithoutTrivia
)
}

override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
let rangeEnd =
if let parameterClause = node.parameterClause {
parameterClause.endPositionBeforeTrailingTrivia
} else {
node.name.endPositionBeforeTrailingTrivia
}

return record(
node: node,
name: node.qualifiedDeclName,
Copy link
Contributor

Choose a reason for hiding this comment

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

qualified to me suggests eg. Type.member rather than just the full name (including parentheses).

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yeah, qualified really isn’t the term here. Renaming to declName in #944

symbolKind: .enumMember,
range: node.name.positionAfterSkippingLeadingTrivia..<rangeEnd,
selection: node.name.positionAfterSkippingLeadingTrivia..<rangeEnd
)
}

override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
return record(
node: node,
name: node.extendedType.trimmedDescription,
symbolKind: .namespace,
range: node.rangeWithoutTrivia,
selection: node.extendedType.rangeWithoutTrivia
)
}

override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
let kind: SymbolKind = if node.name.tokenKind.isOperator {
.operator
} else if node.parent?.is(MemberBlockItemSyntax.self) ?? false {
.method
} else {
.function
}
return record(
node: node,
name: node.qualifiedDeclName,
symbolKind: kind,
range: node.rangeWithoutTrivia,
selection: node.name
.positionAfterSkippingLeadingTrivia..<node.signature.parameterClause.endPositionBeforeTrailingTrivia
)
}

override func visit(_ node: GenericParameterSyntax) -> SyntaxVisitorContinueKind {
return record(
node: node,
name: node.name.text,
symbolKind: .typeParameter,
range: node.rangeWithoutTrivia,
selection: node.rangeWithoutTrivia
)
}

override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
return record(
node: node,
name: node.initKeyword.text,
symbolKind: .constructor,
range: node.rangeWithoutTrivia,
selection: node.initKeyword
.positionAfterSkippingLeadingTrivia..<node.signature.parameterClause.endPositionBeforeTrailingTrivia
)
}

override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind {
// If there is only one pattern binding within the variable decl, consider the entire variable decl as the
// referenced range. If there are multiple, consider each pattern binding separately since the `var` keyword doesn't
// belong to any pattern binding in particular.
guard let variableDecl = node.parent?.parent?.as(VariableDeclSyntax.self) else {
return .visitChildren
}
let rangeNode: Syntax = variableDecl.bindings.count == 1 ? Syntax(variableDecl) : Syntax(node)

return record(
node: node,
name: node.pattern.trimmedDescription,
symbolKind: variableDecl.parent?.is(MemberBlockItemSyntax.self) ?? false ? .property : .variable,
range: rangeNode.rangeWithoutTrivia,
selection: node.pattern.rangeWithoutTrivia
)
}
}

// MARK: - Syntax Utilities

fileprivate extension EnumCaseElementSyntax {
var qualifiedDeclName: String {
var result = self.name.text
if let parameterClause {
result += "("
for parameter in parameterClause.parameters {
result += "\(parameter.firstName?.text ?? "_"):"
}
result += ")"
}
return result
}
}

fileprivate extension FunctionDeclSyntax {
var qualifiedDeclName: String {
var result = self.name.text
result += "("
for parameter in self.signature.parameterClause.parameters {
result += "\(parameter.firstName.text):"
}
result += ")"
return result
}
}

fileprivate extension SyntaxProtocol {
/// The position range of this node without its leading and trailing trivia.
var rangeWithoutTrivia: Range<AbsolutePosition> {
return positionAfterSkippingLeadingTrivia..<endPositionBeforeTrailingTrivia
}
}

fileprivate extension TokenKind {
var isOperator: Bool {
switch self {
case .prefixOperator, .binaryOperator, .postfixOperator: return true
default: return false
}
}
}
90 changes: 0 additions & 90 deletions Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -516,96 +516,6 @@ extension SwiftLanguageServer {
return [cursorInfo.symbolInfo]
}

public func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? {
guard let snapshot = self.documentManager.latestSnapshot(req.textDocument.uri) else {
throw ResponseError.unknown("failed to find snapshot for url \(req.textDocument.uri.forLogging)")
}

let helperDocumentName = "DocumentSymbols:" + snapshot.uri.pseudoPath
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
skreq[keys.request] = self.requests.editor_open
skreq[keys.name] = helperDocumentName
skreq[keys.sourcetext] = snapshot.text
skreq[keys.syntactic_only] = 1

let dict = try await self.sourcekitd.send(skreq)
defer {
let closeHelperReq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
closeHelperReq[self.keys.request] = self.requests.editor_close
closeHelperReq[self.keys.name] = helperDocumentName
// FIXME: (async) We might receive two concurrent document symbol requests for the
// same document, in which race to open/close a document with the same name in
// sourcekitd. The solution is to either
// - Not open the helper document and instead rely on the document that is already
// open or
// - Prefix the helper document with a UUID to make sure the two concurrent
// requests operate on different documents as far as sourcekitd is concerned.
Task {
_ = try await self.sourcekitd.send(closeHelperReq)
}
}

guard let results: SKDResponseArray = dict[self.keys.substructure] else {
return .documentSymbols([])
}

func documentSymbol(value: SKDResponseDictionary) -> DocumentSymbol? {
guard let name: String = value[self.keys.name],
let uid: sourcekitd_uid_t = value[self.keys.kind],
let kind: SymbolKind = uid.asSymbolKind(self.values),
let offset: Int = value[self.keys.offset],
let start: Position = snapshot.positionOf(utf8Offset: offset),
let length: Int = value[self.keys.length],
let end: Position = snapshot.positionOf(utf8Offset: offset + length)
else {
return nil
}

let range = start..<end
let selectionRange: Range<Position>
if let nameOffset: Int = value[self.keys.nameoffset],
let nameStart: Position = snapshot.positionOf(utf8Offset: nameOffset),
let nameLength: Int = value[self.keys.namelength],
let nameEnd: Position = snapshot.positionOf(utf8Offset: nameOffset + nameLength)
{
selectionRange = nameStart..<nameEnd
} else {
selectionRange = range
}

let children: [DocumentSymbol]
if let substructure: SKDResponseArray = value[self.keys.substructure] {
children = documentSymbols(array: substructure)
} else {
children = []
}
return DocumentSymbol(
name: name,
detail: value[self.keys.typename] as String?,
kind: kind,
deprecated: nil,
range: range,
selectionRange: selectionRange,
children: children
)
}

func documentSymbols(array: SKDResponseArray) -> [DocumentSymbol] {
var result: [DocumentSymbol] = []
array.forEach { (i: Int, value: SKDResponseDictionary) in
if let documentSymbol = documentSymbol(value: value) {
result.append(documentSymbol)
} else if let substructure: SKDResponseArray = value[self.keys.substructure] {
result += documentSymbols(array: substructure)
}
return true
}
return result
}

return .documentSymbols(documentSymbols(array: results))
}

public func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] {
let keys = self.keys

Expand Down
Loading