Skip to content

Implement document symbol request #98

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
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ SourceKit-LSP is still in early development, so you may run into rough edges wit
| Formatting | ❌ | |
| Folding | ✅ | |
| Syntax Highlighting | ❌ | Not currently part of LSP. |
| Document Symbols | | |
| Document Symbols | | |


### Caveats
Expand Down
5 changes: 4 additions & 1 deletion Sources/LanguageServerProtocol/ClientCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {
/// Whether the client supports dynamic registaration of this request.
public var dynamicRegistration: Bool? = nil

/// Capabilities specific to `SignatureInformation`.
/// Capabilities specific to `SymbolKind`.
public struct SymbolKind: Hashable, Codable {

/// The symbol kind values that the client can support.
Expand All @@ -201,6 +201,9 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {
}

public var symbolKind: SymbolKind? = nil

public init() {
}
}

/// Capabilities specific to the `textDocument/codeAction` request.
Expand Down
86 changes: 86 additions & 0 deletions Sources/LanguageServerProtocol/DocumentSymbol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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
//
//===----------------------------------------------------------------------===//

/// Request for symbols to display in the document outline.
///
/// This is used to provide list of all symbols in the document and display inside which
/// type or function the cursor is currently in.
///
/// Servers that provide document highlights should set the `documentSymbolProvider` server
/// capability.
///
/// - Parameters:
/// - textDocument: The document in which to lookup the symbol location.
///
/// - Returns: An array of document symbols, if any.
public struct DocumentSymbolRequest: TextDocumentRequest, Hashable {
public static let method: String = "textDocument/documentSymbol"
public typealias Response = [DocumentSymbol]?

/// The document in which to lookup the symbol location.
public var textDocument: TextDocumentIdentifier

public init(textDocument: TextDocumentIdentifier) {
self.textDocument = textDocument
}
}

/// Represents programming constructs like variables, classes, interfaces etc. that appear
/// in a document. Document symbols can be hierarchical and they have two ranges: one that encloses
/// its definition and one that points to its most interesting range, e.g. the range of an identifier.
public struct DocumentSymbol: Hashable, Codable, ResponseType {

/// The name of this symbol. Will be displayed in the user interface and therefore must not be
/// an empty string or a string only consisting of white spaces.
var name: String

/// More detail for this symbol, e.g the signature of a function.
var detail: String?

/// The kind of this symbol.
var kind: SymbolKind

/// Indicates if this symbol is deprecated.
var deprecated: Bool?

/// The range enclosing this symbol not including leading/trailing whitespace but everything else
/// like comments. This information is typically used to determine if the clients cursor is
/// inside the symbol to reveal in the symbol in the UI.
var range: PositionRange

/// The range that should be selected and revealed when this symbol is being picked,
/// e.g the name of a function.
///
/// Must be contained by the `range`.
var selectionRange: PositionRange

/// Children of this symbol, e.g. properties of a class.
var children: [DocumentSymbol]?

public init(
name: String,
detail: String?,
kind: SymbolKind,
deprecated: Bool?,
range: PositionRange,
selectionRange: PositionRange,
children: [DocumentSymbol]?)
{
self.name = name
self.detail = detail
self.kind = kind
self.deprecated = deprecated
self.range = range
self.selectionRange = selectionRange
self.children = children
}
}
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public let builtinRequests: [_RequestType.Type] = [
DocumentRangeFormatting.self,
DocumentOnTypeFormatting.self,
FoldingRangeRequest.self,
DocumentSymbolRequest.self,

// MARK: LSP Extension Requests

Expand Down
8 changes: 7 additions & 1 deletion Sources/LanguageServerProtocol/ServerCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public struct ServerCapabilities: Codable, Hashable {
/// Whether the server provides "textDocument/foldingRange".
public var foldingRangeProvider: Bool?

/// Whether the server provides "textDocument/documentSymbol"
public var documentSymbolProvider: Bool?

// TODO: fill-in the rest.

public init(
Expand All @@ -54,7 +57,8 @@ public struct ServerCapabilities: Codable, Hashable {
documentFormattingProvider: Bool? = nil,
documentRangeFormattingProvider: Bool? = nil,
documentOnTypeFormattingProvider: DocumentOnTypeFormattingOptions? = nil,
foldingRangeProvider: Bool? = nil
foldingRangeProvider: Bool? = nil,
documentSymbolProvider: Bool? = nil
)
{
self.textDocumentSync = textDocumentSync
Expand All @@ -67,6 +71,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.documentRangeFormattingProvider = documentRangeFormattingProvider
self.documentOnTypeFormattingProvider = documentOnTypeFormattingProvider
self.foldingRangeProvider = foldingRangeProvider
self.documentSymbolProvider = documentSymbolProvider
}

public init(from decoder: Decoder) throws {
Expand All @@ -75,6 +80,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.hoverProvider = try container.decodeIfPresent(Bool.self, forKey: .hoverProvider)
self.definitionProvider = try container.decodeIfPresent(Bool.self, forKey: .definitionProvider)
self.foldingRangeProvider = try container.decodeIfPresent(Bool.self, forKey: .foldingRangeProvider)
self.documentSymbolProvider = try container.decodeIfPresent(Bool.self, forKey: .documentSymbolProvider)

if let textDocumentSync = try? container.decode(TextDocumentSyncOptions.self, forKey: .textDocumentSync) {
self.textDocumentSync = textDocumentSync
Expand Down
8 changes: 7 additions & 1 deletion Sources/SourceKit/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public final class SourceKitServer: LanguageServer {
registerWorkspaceRequest(SourceKitServer.documentSymbolHighlight)
registerWorkspaceRequest(SourceKitServer.foldingRange)
registerWorkspaceRequest(SourceKitServer.symbolInfo)
registerWorkspaceRequest(SourceKitServer.documentSymbol)
}

func registerWorkspaceRequest<R>(
Expand Down Expand Up @@ -257,7 +258,8 @@ extension SourceKitServer {
definitionProvider: true,
referencesProvider: true,
documentHighlightProvider: true,
foldingRangeProvider: true
foldingRangeProvider: true,
documentSymbolProvider: true
)))
}

Expand Down Expand Up @@ -339,6 +341,10 @@ extension SourceKitServer {
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
}

func documentSymbol(_ req: Request<DocumentSymbolRequest>, workspace: Workspace) {
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
}

func definition(_ req: Request<DefinitionRequest>, workspace: Workspace) {
// FIXME: sending yourself a request isn't very convenient

Expand Down
119 changes: 118 additions & 1 deletion Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public final class SwiftLanguageServer: LanguageServer {
_register(SwiftLanguageServer.documentSymbolHighlight)
_register(SwiftLanguageServer.foldingRange)
_register(SwiftLanguageServer.symbolInfo)
_register(SwiftLanguageServer.documentSymbol)
}

func getDiagnostic(_ diag: SKResponseDictionary, for snapshot: DocumentSnapshot) -> Diagnostic? {
Expand Down Expand Up @@ -191,7 +192,8 @@ extension SwiftLanguageServer {
definitionProvider: nil,
referencesProvider: nil,
documentHighlightProvider: true,
foldingRangeProvider: true
foldingRangeProvider: true,
documentSymbolProvider: true
)))
}

Expand Down Expand Up @@ -471,6 +473,86 @@ extension SwiftLanguageServer {
}
}

func documentSymbol(_ req: Request<DocumentSymbolRequest>) {
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
log("failed to find snapshot for url \(req.params.textDocument.url)")
req.reply(nil)
return
}

let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
skreq[keys.request] = requests.editor_open
skreq[keys.name] = "DocumentSymbols:" + snapshot.document.url.path
skreq[keys.sourcetext] = snapshot.text
skreq[keys.syntactic_only] = 1

let handle = sourcekitd.send(skreq) { [weak self] result in
guard let self = self else { return }
guard let dict = result.success else {
req.reply(.failure(result.failure!))
return
}
guard let results: SKResponseArray = dict[self.keys.substructure] else {
return req.reply([])
}

func documentSymbol(value: SKResponseDictionary) -> 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 = PositionRange(start..<end)
let selectionRange: PositionRange
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 = PositionRange(nameStart..<nameEnd)
} else {
selectionRange = range
}

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

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

req.reply(documentSymbols(array: results))
}
// FIXME: cancellation
_ = handle
return
}

func documentSymbolHighlight(_ req: Request<DocumentHighlightRequest>) {

guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
Expand Down Expand Up @@ -758,4 +840,39 @@ extension sourcekitd_uid_t {
return nil
}
}

func asSymbolKind(_ vals: sourcekitd_values) -> SymbolKind? {
switch self {
case vals.decl_class:
return .class
case vals.decl_function_method_instance,
vals.decl_function_method_static,
vals.decl_function_method_class:
return .method
case vals.decl_var_instance,
vals.decl_var_static,
vals.decl_var_class:
return .property
case vals.decl_enum:
return .enum
case vals.decl_enumelement:
return .enumMember
case vals.decl_protocol:
return .interface
case vals.decl_function_free:
return .function
case vals.decl_var_global,
vals.decl_var_local:
return .variable
case vals.decl_struct:
return .struct
case vals.decl_generic_type_param:
return .typeParameter
case vals.decl_extension:
// There are no extensions in LSP, so I return something vaguely similar
return .namespace
default:
return nil
}
}
}
4 changes: 4 additions & 0 deletions Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ struct sourcekitd_keys {
let bodyoffset: sourcekitd_uid_t
let bodylength: sourcekitd_uid_t
let syntaxmap: sourcekitd_uid_t
let namelength: sourcekitd_uid_t
let nameoffset: sourcekitd_uid_t

init(api: sourcekitd_functions_t) {
request = api.uid_get_from_cstr("key.request")!
Expand Down Expand Up @@ -242,6 +244,8 @@ struct sourcekitd_keys {
bodyoffset = api.uid_get_from_cstr("key.bodyoffset")!
bodylength = api.uid_get_from_cstr("key.bodylength")!
syntaxmap = api.uid_get_from_cstr("key.syntaxmap")!
namelength = api.uid_get_from_cstr("key.namelength")!
nameoffset = api.uid_get_from_cstr("key.nameoffset")!
}
}

Expand Down
Loading