Skip to content

Commit c71ab1a

Browse files
authored
Merge pull request #978 from ahoppen/ahoppen/workspace-tests
Add a request to list all the tests within a workspace or document
2 parents 9ed43df + 96905aa commit c71ab1a

File tree

10 files changed

+299
-37
lines changed

10 files changed

+299
-37
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ add_library(LanguageServerProtocol STATIC
5050
Requests/DocumentSemanticTokensRangeRequest.swift
5151
Requests/DocumentSemanticTokensRequest.swift
5252
Requests/DocumentSymbolRequest.swift
53+
Requests/DocumentTestsRequest.swift
5354
Requests/ExecuteCommandRequest.swift
5455
Requests/FoldingRangeRequest.swift
5556
Requests/FormattingRequests.swift
@@ -86,6 +87,7 @@ add_library(LanguageServerProtocol STATIC
8687
Requests/WorkspaceSemanticTokensRefreshRequest.swift
8788
Requests/WorkspaceSymbolResolveRequest.swift
8889
Requests/WorkspaceSymbolsRequest.swift
90+
Requests/WorkspaceTestsRequest.swift
8991

9092
SupportTypes/CallHierarchyItem.swift
9193
SupportTypes/ClientCapabilities.swift

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public let builtinRequests: [_RequestType.Type] = [
4545
DocumentSemanticTokensRangeRequest.self,
4646
DocumentSemanticTokensRequest.self,
4747
DocumentSymbolRequest.self,
48+
DocumentTestsRequest.self,
4849
ExecuteCommandRequest.self,
4950
FoldingRangeRequest.self,
5051
HoverRequest.self,
@@ -82,6 +83,7 @@ public let builtinRequests: [_RequestType.Type] = [
8283
WorkspaceSemanticTokensRefreshRequest.self,
8384
WorkspaceSymbolResolveRequest.self,
8485
WorkspaceSymbolsRequest.self,
86+
WorkspaceTestsRequest.self,
8587
]
8688

8789
/// The set of known notifications.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
/// A request that returns symbols for all the test classes and test methods within a file.
14+
///
15+
/// **(LSP Extension)**
16+
public struct DocumentTestsRequest: TextDocumentRequest, Hashable {
17+
public static let method: String = "document/tests"
18+
public typealias Response = [WorkspaceSymbolItem]?
19+
20+
public var textDocument: TextDocumentIdentifier
21+
22+
public init(textDocument: TextDocumentIdentifier) {
23+
self.textDocument = textDocument
24+
}
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/// A request that returns symbols for all the test classes and test methods within the current workspace.
14+
///
15+
/// **(LSP Extension)**
16+
public struct WorkspaceTestsRequest: RequestType, Hashable {
17+
public static let method: String = "workspace/tests"
18+
public typealias Response = [WorkspaceSymbolItem]?
19+
20+
public init() {}
21+
}

Sources/SKCore/BuildSystemManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ extension BuildSystemManager: MainFilesDelegate {
268268
/// For Swift or normal C files, this will be the file itself. For header
269269
/// files, we pick a main file that includes the header since header files
270270
/// don't have build settings by themselves.
271-
private func mainFile(for uri: DocumentURI, language: Language, useCache: Bool = true) async -> DocumentURI {
271+
public func mainFile(for uri: DocumentURI, language: Language, useCache: Bool = true) async -> DocumentURI {
272272
if language == .swift {
273273
// Swift doesn't have main files. Skip the main file provider query.
274274
return uri

Sources/SKTestSupport/SwiftPMTestWorkspace.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
4545
var filesByPath: [RelativeFileLocation: String] = [:]
4646
for (fileLocation, contents) in files {
4747
let directories =
48-
if fileLocation.directories.isEmpty {
48+
switch fileLocation.directories.first {
49+
case "Sources", "Tests":
50+
fileLocation.directories
51+
case nil:
4952
["Sources", "MyLibrary"]
50-
} else if fileLocation.directories.first != "Sources" {
53+
default:
5154
["Sources"] + fileLocation.directories
52-
} else {
53-
fileLocation.directories
5455
}
56+
5557
filesByPath[RelativeFileLocation(directories: directories, fileLocation.fileName)] = contents
5658
}
5759
filesByPath["Package.swift"] = manifest
@@ -77,6 +79,7 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
7779
swift.path,
7880
"build",
7981
"--package-path", path.path,
82+
"--build-tests",
8083
"-Xswiftc", "-index-ignore-system-modules",
8184
"-Xcc", "-index-ignore-system-symbols",
8285
]

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ add_library(SourceKitLSP STATIC
77
Sequence+AsyncMap.swift
88
SourceKitIndexDelegate.swift
99
SourceKitLSPCommandMetadata.swift
10-
SourceKitServer+Options.swift
1110
SourceKitServer.swift
11+
SourceKitServer+Options.swift
12+
TestDiscovery.swift
1213
ToolchainLanguageServer.swift
1314
Workspace.swift
1415
)

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ final actor WorkDoneProgressState {
163163
fileprivate enum TaskMetadata: DependencyTracker {
164164
/// A task that changes the global configuration of sourcekit-lsp in any way.
165165
///
166-
/// No other tasks must execute simulateneously with this task since they
166+
/// No other tasks must execute simultaneously with this task since they
167167
/// might be relying on this task to take effect.
168168
case globalConfigurationChange
169169

@@ -336,6 +336,8 @@ fileprivate enum TaskMetadata: DependencyTracker {
336336
self = .freestanding
337337
case is WorkspaceSymbolsRequest:
338338
self = .freestanding
339+
case is WorkspaceTestsRequest:
340+
self = .freestanding
339341
default:
340342
logger.error(
341343
"""
@@ -382,7 +384,7 @@ public actor SourceKitServer {
382384

383385
var languageServices: [LanguageServerType: [ToolchainLanguageServer]] = [:]
384386

385-
private let documentManager = DocumentManager()
387+
let documentManager = DocumentManager()
386388

387389
private var packageLoadingWorkDoneProgress = WorkDoneProgressState(
388390
"SourceKitLSP.SourceKitServer.reloadPackage",
@@ -398,7 +400,7 @@ public actor SourceKitServer {
398400
/// Must only be accessed from `queue`.
399401
private var uriToWorkspaceCache: [DocumentURI: WeakWorkspace] = [:]
400402

401-
private var workspaces: [Workspace] = [] {
403+
private(set) var workspaces: [Workspace] = [] {
402404
didSet {
403405
uriToWorkspaceCache = [:]
404406
}
@@ -837,6 +839,10 @@ extension SourceKitServer: MessageHandler {
837839
await request.reply { try await shutdown(request.params) }
838840
case let request as RequestAndReply<WorkspaceSymbolsRequest>:
839841
await request.reply { try await workspaceSymbols(request.params) }
842+
case let request as RequestAndReply<WorkspaceTestsRequest>:
843+
await request.reply { try await workspaceTests(request.params) }
844+
case let request as RequestAndReply<DocumentTestsRequest>:
845+
await self.handleRequest(for: request, requestHandler: self.documentTests)
840846
case let request as RequestAndReply<PollIndexRequest>:
841847
await request.reply { try await pollIndex(request.params) }
842848
case let request as RequestAndReply<BarrierRequest>:
@@ -1499,7 +1505,7 @@ extension SourceKitServer {
14991505
guard matching.count >= minWorkspaceSymbolPatternLength else {
15001506
return []
15011507
}
1502-
var symbolOccurenceResults: [SymbolOccurrence] = []
1508+
var symbolOccurrenceResults: [SymbolOccurrence] = []
15031509
for workspace in workspaces {
15041510
workspace.index?.forEachCanonicalSymbolOccurrence(
15051511
containing: matching,
@@ -1511,45 +1517,22 @@ extension SourceKitServer {
15111517
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
15121518
return true
15131519
}
1514-
symbolOccurenceResults.append(symbol)
1520+
symbolOccurrenceResults.append(symbol)
15151521
// FIXME: Once we have cancellation support, we should fetch all results and take the top
15161522
// `maxWorkspaceSymbolResults` symbols but bail if cancelled.
15171523
//
15181524
// Until then, take the first `maxWorkspaceSymbolResults` symbols to limit the impact of
15191525
// queries which match many symbols.
1520-
return symbolOccurenceResults.count < maxWorkspaceSymbolResults
1526+
return symbolOccurrenceResults.count < maxWorkspaceSymbolResults
15211527
}
15221528
}
1523-
return symbolOccurenceResults
1529+
return symbolOccurrenceResults
15241530
}
15251531

15261532
/// Handle a workspace/symbol request, returning the SymbolInformation.
15271533
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
15281534
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
1529-
let symbols = findWorkspaceSymbols(
1530-
matching: req.query
1531-
).map({ symbolOccurrence -> WorkspaceSymbolItem in
1532-
let symbolPosition = Position(
1533-
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
1534-
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
1535-
utf16index: symbolOccurrence.location.utf8Column - 1
1536-
)
1537-
1538-
let symbolLocation = Location(
1539-
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
1540-
range: Range(symbolPosition)
1541-
)
1542-
1543-
return .symbolInformation(
1544-
SymbolInformation(
1545-
name: symbolOccurrence.symbol.name,
1546-
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
1547-
deprecated: nil,
1548-
location: symbolLocation,
1549-
containerName: symbolOccurrence.getContainerName()
1550-
)
1551-
)
1552-
})
1535+
let symbols = findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
15531536
return symbols
15541537
}
15551538

@@ -2294,3 +2277,28 @@ fileprivate func transitiveSubtypeClosure(ofUsrs usrs: [String], index: IndexSto
22942277
}
22952278
return result
22962279
}
2280+
2281+
extension WorkspaceSymbolItem {
2282+
init(_ symbolOccurrence: SymbolOccurrence) {
2283+
let symbolPosition = Position(
2284+
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
2285+
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
2286+
utf16index: symbolOccurrence.location.utf8Column - 1
2287+
)
2288+
2289+
let symbolLocation = Location(
2290+
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
2291+
range: Range(symbolPosition)
2292+
)
2293+
2294+
self = .symbolInformation(
2295+
SymbolInformation(
2296+
name: symbolOccurrence.symbol.name,
2297+
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
2298+
deprecated: nil,
2299+
location: symbolLocation,
2300+
containerName: symbolOccurrence.getContainerName()
2301+
)
2302+
)
2303+
}
2304+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 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 IndexStoreDB
14+
import LanguageServerProtocol
15+
16+
fileprivate extension SymbolOccurrence {
17+
/// Assuming that this is a symbol occurrence returned by the index, return whether it can constitute the definition
18+
/// of a test case.
19+
///
20+
/// The primary intention for this is to filter out references to test cases and extension declarations of test cases.
21+
/// The latter is important to filter so we don't include extension declarations for the derived `DiscoveredTests`
22+
/// files on non-Darwin platforms.
23+
var canBeTestDefinition: Bool {
24+
guard roles.contains(.definition) else {
25+
return false
26+
}
27+
guard symbol.kind == .class || symbol.kind == .instanceMethod else {
28+
return false
29+
}
30+
return true
31+
}
32+
}
33+
34+
extension SourceKitServer {
35+
func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [WorkspaceSymbolItem]? {
36+
let testSymbols = workspaces.flatMap { (workspace) -> [SymbolOccurrence] in
37+
return workspace.index?.unitTests() ?? []
38+
}
39+
return
40+
testSymbols
41+
.filter { $0.canBeTestDefinition }
42+
.map(WorkspaceSymbolItem.init)
43+
}
44+
45+
func documentTests(
46+
_ req: DocumentTestsRequest,
47+
workspace: Workspace,
48+
languageService: ToolchainLanguageServer
49+
) async throws -> [WorkspaceSymbolItem]? {
50+
let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri)
51+
let mainFileUri = await workspace.buildSystemManager.mainFile(
52+
for: req.textDocument.uri,
53+
language: snapshot.language
54+
)
55+
let testSymbols = workspace.index?.unitTests(referencedByMainFiles: [mainFileUri.pseudoPath]) ?? []
56+
return
57+
testSymbols
58+
.filter { $0.canBeTestDefinition }
59+
.map(WorkspaceSymbolItem.init)
60+
}
61+
}

0 commit comments

Comments
 (0)