mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-11-05 20:14:59 +01:00
`workspace/symbol` previously couldn't search for symbols by their qualified name (e.g. `String.description` or `Foo::bar`). The handler treated the query as a flat fuzzy match against bare symbol names, with no notion of containing types or namespaces. Add a small parser that splits queries on the trailing `.` or `::` separator into a parent chain + member, then in the handler: - Unqualified queries (no separator) take the existing flat-search path. - Qualified queries resolve the container named by the parent chain, then enumerate its members, keeping those whose name matches the member. An empty member (e.g. the query `Foo.`) lists all members of the container. Members declared in extensions are included by also enumerating the containers reached through the type's `extendedBy` relations. Both `.` and `::` are accepted and can be mixed in one query (`Foo::Bar.baz`). Multi-level chains (`Outer.Inner.method`) are matched by walking the `childOf` relation chain. Unlike the flat unqualified path, which filters out system symbols, qualified queries deliberately surface results from system SDK modules: naming an explicit container is a request for its members regardless of where they are defined, so `String.description` finds the standard library member. Such SDK/stdlib results point into the module's generated interface via a `sourcekit-lsp://generated-swift-interface` location (reusing the reference-document mechanism); only system *namespaces* (e.g. `std`) are skipped, to avoid enumerating an entire system scope. Qualified results are returned with the fully-qualified name in the symbol's `name` (e.g. `Foo.bar()`) and no `containerName`, so clients that filter workspace symbols by matching the query against the label (e.g. VS Code) keep them. No changes to `workspace/symbolNames` or `workspace/symbolInfo`, and no changes to the protocol or to IndexStoreDB.
97 lines
3.6 KiB
Swift
97 lines
3.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2026 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
@_spi(Testing) import SourceKitLSP
|
|
import XCTest
|
|
|
|
final class QualifiedWorkspaceSymbolQueryTests: XCTestCase {
|
|
func testUnqualifiedQueriesReturnNil() {
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery("description"))
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery("foo123_bar"))
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery(""))
|
|
}
|
|
|
|
func testDotSeparator() {
|
|
let q = QualifiedWorkspaceSymbolQuery("String.description")
|
|
XCTAssertEqual(q?.containerChain, ["String"])
|
|
XCTAssertEqual(q?.member, "description")
|
|
}
|
|
|
|
func testColonColonSeparator() {
|
|
let q = QualifiedWorkspaceSymbolQuery("Foo::bar")
|
|
XCTAssertEqual(q?.containerChain, ["Foo"])
|
|
XCTAssertEqual(q?.member, "bar")
|
|
}
|
|
|
|
func testMultiLevelDotChain() {
|
|
let q = QualifiedWorkspaceSymbolQuery("Outer.Inner.method")
|
|
XCTAssertEqual(q?.containerChain, ["Outer", "Inner"])
|
|
XCTAssertEqual(q?.member, "method")
|
|
}
|
|
|
|
func testMultiLevelColonChain() {
|
|
let q = QualifiedWorkspaceSymbolQuery("Outer::Inner::method")
|
|
XCTAssertEqual(q?.containerChain, ["Outer", "Inner"])
|
|
XCTAssertEqual(q?.member, "method")
|
|
}
|
|
|
|
func testMixedSeparators() {
|
|
let q = QualifiedWorkspaceSymbolQuery("Outer::Inner.method")
|
|
XCTAssertEqual(q?.containerChain, ["Outer", "Inner"])
|
|
XCTAssertEqual(q?.member, "method")
|
|
}
|
|
|
|
func testTrailingSeparatorListsAllMembers() {
|
|
// A trailing separator with an empty member is a valid qualified query that lists all members of
|
|
// the container.
|
|
let dot = QualifiedWorkspaceSymbolQuery("Foo.")
|
|
XCTAssertEqual(dot?.containerChain, ["Foo"])
|
|
XCTAssertEqual(dot?.member, "")
|
|
|
|
let colon = QualifiedWorkspaceSymbolQuery("Foo::")
|
|
XCTAssertEqual(colon?.containerChain, ["Foo"])
|
|
XCTAssertEqual(colon?.member, "")
|
|
|
|
let nested = QualifiedWorkspaceSymbolQuery("Outer.Inner.")
|
|
XCTAssertEqual(nested?.containerChain, ["Outer", "Inner"])
|
|
XCTAssertEqual(nested?.member, "")
|
|
}
|
|
|
|
func testLeadingSeparatorRejected() {
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery(".bar"))
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery("::bar"))
|
|
}
|
|
|
|
func testLoneColonRejected() {
|
|
// A single `:` isn't a qualifier separator; it's a literal character. `Foo:bar` therefore has no
|
|
// separator at all and is not a qualified query, so the parser returns `nil`.
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery("Foo:bar"))
|
|
}
|
|
|
|
func testArgumentLabelColonsArePartOfMember() {
|
|
// A lone `:` is a literal, so Swift argument labels survive in the member component.
|
|
let dotQuery = QualifiedWorkspaceSymbolQuery("Collection.append(contentsOf:)")
|
|
XCTAssertEqual(dotQuery?.containerChain, ["Collection"])
|
|
XCTAssertEqual(dotQuery?.member, "append(contentsOf:)")
|
|
|
|
// `::` still separates even when the member carries argument-label colons.
|
|
let colonQuery = QualifiedWorkspaceSymbolQuery("Foo::subscript(at:)")
|
|
XCTAssertEqual(colonQuery?.containerChain, ["Foo"])
|
|
XCTAssertEqual(colonQuery?.member, "subscript(at:)")
|
|
}
|
|
|
|
func testEmptyParentSegmentRejected() {
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery("Foo..bar"))
|
|
XCTAssertNil(QualifiedWorkspaceSymbolQuery("Foo.::bar"))
|
|
}
|
|
}
|