Skip to content

Commit eb8ea96

Browse files
Implement document symbol request
1 parent 6d6f899 commit eb8ea96

File tree

6 files changed

+218
-3
lines changed

6 files changed

+218
-3
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 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+
/// Request for symbols to display in the document outline.
14+
///
15+
/// This is used to provide list of all symbols in the document and display inside which
16+
/// type or function the cursor is currently in.
17+
///
18+
/// Servers that provide document highlights should set the `documentSymbolProvider` server
19+
/// capability.
20+
///
21+
/// - Parameters:
22+
/// - textDocument: The document in which to lookup the symbol location.
23+
///
24+
/// - Returns: An array of document symbols, if any.
25+
public struct DocumentSymbolRequest: TextDocumentRequest, Hashable {
26+
public static let method: String = "textDocument/documentSymbol"
27+
public typealias Response = [DocumentSymbol]?
28+
29+
/// The document in which to lookup the symbol location.
30+
public var textDocument: TextDocumentIdentifier
31+
32+
public init(textDocument: TextDocumentIdentifier) {
33+
self.textDocument = textDocument
34+
}
35+
}
36+
37+
/// Represents programming constructs like variables, classes, interfaces etc. that appear
38+
/// in a document. Document symbols can be hierarchical and they have two ranges: one that encloses
39+
/// its definition and one that points to its most interesting range, e.g. the range of an identifier.
40+
public struct DocumentSymbol: Hashable, Codable, ResponseType {
41+
42+
/// The name of this symbol. Will be displayed in the user interface and therefore must not be
43+
/// an empty string or a string only consisting of white spaces.
44+
var name: String
45+
46+
/// More detail for this symbol, e.g the signature of a function.
47+
var detail: String?
48+
49+
/// The kind of this symbol.
50+
var kind: SymbolKind
51+
52+
/// Indicates if this symbol is deprecated.
53+
var deprecated: Bool?
54+
55+
/// The range enclosing this symbol not including leading/trailing whitespace but everything else
56+
/// like comments. This information is typically used to determine if the clients cursor is
57+
/// inside the symbol to reveal in the symbol in the UI.
58+
var range: PositionRange
59+
60+
/// The range that should be selected and revealed when this symbol is being picked,
61+
/// e.g the name of a function.
62+
///
63+
/// Must be contained by the `range`.
64+
var selectionRange: PositionRange
65+
66+
/// Children of this symbol, e.g. properties of a class.
67+
var children: [DocumentSymbol]?
68+
69+
public init(
70+
name: String,
71+
detail: String?,
72+
kind: SymbolKind,
73+
deprecated: Bool?,
74+
range: PositionRange,
75+
selectionRange: PositionRange,
76+
children: [DocumentSymbol]?) {
77+
self.name = name
78+
self.detail = detail
79+
self.kind = kind
80+
self.deprecated = deprecated
81+
self.range = range
82+
self.selectionRange = selectionRange
83+
self.children = children
84+
}
85+
}

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public let builtinRequests: [_RequestType.Type] = [
3030
DocumentRangeFormatting.self,
3131
DocumentOnTypeFormatting.self,
3232
FoldingRangeRequest.self,
33+
DocumentSymbolRequest.self,
3334

3435
// MARK: LSP Extension Requests
3536

Sources/LanguageServerProtocol/ServerCapabilities.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public struct ServerCapabilities: Codable, Hashable {
4242
/// Whether the server provides "textDocument/foldingRange".
4343
public var foldingRangeProvider: Bool?
4444

45+
/// Whether the server provides "textDocument/documentSymbol"
46+
public var documentSymbolProvider: Bool?
47+
4548
// TODO: fill-in the rest.
4649

4750
public init(
@@ -54,7 +57,8 @@ public struct ServerCapabilities: Codable, Hashable {
5457
documentFormattingProvider: Bool? = nil,
5558
documentRangeFormattingProvider: Bool? = nil,
5659
documentOnTypeFormattingProvider: DocumentOnTypeFormattingOptions? = nil,
57-
foldingRangeProvider: Bool? = nil
60+
foldingRangeProvider: Bool? = nil,
61+
documentSymbolProvider: Bool? = nil
5862
)
5963
{
6064
self.textDocumentSync = textDocumentSync
@@ -67,6 +71,7 @@ public struct ServerCapabilities: Codable, Hashable {
6771
self.documentRangeFormattingProvider = documentRangeFormattingProvider
6872
self.documentOnTypeFormattingProvider = documentOnTypeFormattingProvider
6973
self.foldingRangeProvider = foldingRangeProvider
74+
self.documentSymbolProvider = documentSymbolProvider
7075
}
7176

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

7985
if let textDocumentSync = try? container.decode(TextDocumentSyncOptions.self, forKey: .textDocumentSync) {
8086
self.textDocumentSync = textDocumentSync

Sources/SourceKit/SourceKitServer.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public final class SourceKitServer: LanguageServer {
7676
registerWorkspaceRequest(SourceKitServer.documentSymbolHighlight)
7777
registerWorkspaceRequest(SourceKitServer.foldingRange)
7878
registerWorkspaceRequest(SourceKitServer.symbolInfo)
79+
registerWorkspaceRequest(SourceKitServer.documentSymbol)
7980
}
8081

8182
func registerWorkspaceRequest<R>(
@@ -257,7 +258,8 @@ extension SourceKitServer {
257258
definitionProvider: true,
258259
referencesProvider: true,
259260
documentHighlightProvider: true,
260-
foldingRangeProvider: true
261+
foldingRangeProvider: true,
262+
documentSymbolProvider: true
261263
)))
262264
}
263265

@@ -339,6 +341,10 @@ extension SourceKitServer {
339341
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
340342
}
341343

344+
func documentSymbol(_ req: Request<DocumentSymbolRequest>, workspace: Workspace) {
345+
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
346+
}
347+
342348
func definition(_ req: Request<DefinitionRequest>, workspace: Workspace) {
343349
// FIXME: sending yourself a request isn't very convenient
344350

Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public final class SwiftLanguageServer: LanguageServer {
6363
_register(SwiftLanguageServer.documentSymbolHighlight)
6464
_register(SwiftLanguageServer.foldingRange)
6565
_register(SwiftLanguageServer.symbolInfo)
66+
_register(SwiftLanguageServer.documentSymbol)
6667
}
6768

6869
func getDiagnostic(_ diag: SKResponseDictionary, for snapshot: DocumentSnapshot) -> Diagnostic? {
@@ -191,7 +192,8 @@ extension SwiftLanguageServer {
191192
definitionProvider: nil,
192193
referencesProvider: nil,
193194
documentHighlightProvider: true,
194-
foldingRangeProvider: true
195+
foldingRangeProvider: true,
196+
documentSymbolProvider: true
195197
)))
196198
}
197199

@@ -471,6 +473,117 @@ extension SwiftLanguageServer {
471473
}
472474
}
473475

476+
func documentSymbol(_ req: Request<DocumentSymbolRequest>) {
477+
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
478+
log("failed to find snapshot for url \(req.params.textDocument.url)")
479+
req.reply(nil)
480+
return
481+
}
482+
483+
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
484+
skreq[keys.request] = requests.editor_open
485+
skreq[keys.name] = snapshot.document.url.path
486+
skreq[keys.sourcefile] = snapshot.document.url.path
487+
let handle = sourcekitd.send(skreq) { [weak self] result in
488+
guard let self = self else { return }
489+
guard let dict = result.success else {
490+
req.reply(.failure(result.failure!))
491+
return
492+
}
493+
guard let results: SKResponseArray = dict[self.keys.substructure] else {
494+
return req.reply([])
495+
}
496+
497+
/// Translate sourcekit symbol kinds into LSP kinds.
498+
func symbolKind(sourcekitSymbolKind: sourcekitd_uid_t) -> SymbolKind? {
499+
switch sourcekitSymbolKind {
500+
case self.values.decl_class:
501+
return .class
502+
case self.values.decl_function_method_instance,
503+
self.values.decl_function_method_static:
504+
return .method
505+
case self.values.decl_var_instance:
506+
return .property
507+
case self.values.decl_enum:
508+
return .enum
509+
case self.values.decl_enumelement:
510+
return .enumMember
511+
case self.values.decl_protocol:
512+
return .interface
513+
case self.values.decl_function_free:
514+
return .function
515+
case self.values.decl_var_global:
516+
return .variable
517+
case self.values.decl_struct:
518+
return .struct
519+
case self.values.decl_generic_type_param:
520+
return .typeParameter
521+
case self.values.decl_extension:
522+
// There are no extensions in LSP, so I return something vaguely similar
523+
return .namespace
524+
default:
525+
return nil
526+
}
527+
}
528+
529+
func documentSymbol(value: SKResponseDictionary) -> DocumentSymbol? {
530+
guard let name: String = value[self.keys.name],
531+
let kind: SymbolKind = value[self.keys.kind].flatMap(symbolKind(sourcekitSymbolKind:)),
532+
let offset: Int = value[self.keys.offset],
533+
let start: Position = snapshot.positionOf(utf8Offset: offset),
534+
let length: Int = value[self.keys.length],
535+
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
536+
return nil
537+
}
538+
539+
let range = PositionRange(start..<end)
540+
let selectionRange: PositionRange
541+
if let nameOffset: Int = value[self.keys.nameoffset],
542+
let nameStart: Position = snapshot.positionOf(utf8Offset: nameOffset),
543+
let nameLength: Int = value[self.keys.namelength],
544+
let nameEnd: Position = snapshot.positionOf(utf8Offset: nameOffset + nameLength) {
545+
selectionRange = PositionRange(nameStart..<nameEnd)
546+
} else {
547+
selectionRange = range
548+
}
549+
550+
let children: [DocumentSymbol]?
551+
if let substructure: SKResponseArray = value[self.keys.substructure] {
552+
children = documentSymbols(array: substructure)
553+
} else {
554+
children = nil
555+
}
556+
return DocumentSymbol(
557+
name: name,
558+
detail: nil,
559+
kind: kind,
560+
deprecated: nil,
561+
range: range,
562+
selectionRange: selectionRange,
563+
children: children
564+
)
565+
}
566+
567+
func documentSymbols(array: SKResponseArray) -> [DocumentSymbol] {
568+
var result: [DocumentSymbol] = []
569+
array.forEach { (i: Int, value: SKResponseDictionary) in
570+
if let documentSymbol = documentSymbol(value: value) {
571+
result.append(documentSymbol)
572+
} else if let substructure: SKResponseArray = value[self.keys.substructure] {
573+
result += documentSymbols(array: substructure)
574+
}
575+
return true
576+
}
577+
return result
578+
}
579+
580+
req.reply(documentSymbols(array: results))
581+
}
582+
// FIXME: cancellation
583+
_ = handle
584+
return
585+
}
586+
474587
func documentSymbolHighlight(_ req: Request<DocumentHighlightRequest>) {
475588

476589
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {

Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ struct sourcekitd_keys {
214214
let bodyoffset: sourcekitd_uid_t
215215
let bodylength: sourcekitd_uid_t
216216
let syntaxmap: sourcekitd_uid_t
217+
let namelength: sourcekitd_uid_t
218+
let nameoffset: sourcekitd_uid_t
217219

218220
init(api: sourcekitd_functions_t) {
219221
request = api.uid_get_from_cstr("key.request")!
@@ -242,6 +244,8 @@ struct sourcekitd_keys {
242244
bodyoffset = api.uid_get_from_cstr("key.bodyoffset")!
243245
bodylength = api.uid_get_from_cstr("key.bodylength")!
244246
syntaxmap = api.uid_get_from_cstr("key.syntaxmap")!
247+
namelength = api.uid_get_from_cstr("key.namelength")!
248+
nameoffset = api.uid_get_from_cstr("key.nameoffset")!
245249
}
246250
}
247251

0 commit comments

Comments
 (0)