Skip to content
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

Added ability to search workspace by symbol name #1348

Merged
merged 4 commits into from
Jan 28, 2025
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
62 changes: 56 additions & 6 deletions src/FsAutoComplete/LspHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,8 @@ module Conversions =
(uri: DocumentUri)
(glyphToSymbolKind: FSharpGlyph -> SymbolKind option)
(topLevel: NavigationTopLevelDeclaration)
(symbolFilter: SymbolInformation -> bool)
: SymbolInformation[] =
let inner (container: string option) (decl: NavigationItem) : SymbolInformation option =
let inner (container: string option) (decl: NavigationItem) : SymbolInformation =
// We should nearly always have a kind, if the client doesn't send weird capabilities,
// if we don't why not assume module...
let kind = defaultArg (glyphToSymbolKind decl.Glyph) SymbolKind.Module
Expand All @@ -158,25 +157,76 @@ module Conversions =
Tags = None
Deprecated = None }

sym

[| yield inner None topLevel.Declaration
yield! topLevel.Nested |> Array.map (inner (Some topLevel.Declaration.LogicalName)) |]

let getDocumentSymbols
(glyphToSymbolKind: FSharpGlyph -> SymbolKind option)
(topLevel: NavigationTopLevelDeclaration)
: DocumentSymbol[] =
let inner (decl: NavigationItem) : DocumentSymbol =
// We should nearly always have a kind, if the client doesn't send weird capabilities,
// if we don't why not assume module...
let kind = defaultArg (glyphToSymbolKind decl.Glyph) SymbolKind.Module

let sym: DocumentSymbol =
{ Name = decl.LogicalName
Kind = kind
Tags = None
Deprecated = None
Children = None
Range = fcsRangeToLsp decl.Range
Detail = None
SelectionRange = fcsRangeToLsp decl.Range }

sym

[| yield inner topLevel.Declaration
yield! topLevel.Nested |> Array.map inner |]

let getWorkspaceSymbols
(uri: DocumentUri)
(glyphToSymbolKind: FSharpGlyph -> SymbolKind option)
(topLevel: NavigationTopLevelDeclaration)
(symbolFilter: WorkspaceSymbol -> bool)
: WorkspaceSymbol[] =
let inner (container: string option) (decl: NavigationItem) : WorkspaceSymbol option =
// We should nearly always have a kind, if the client doesn't send weird capabilities,
// if we don't why not assume module...
let kind = defaultArg (glyphToSymbolKind decl.Glyph) SymbolKind.Module

let location =
{ Uri = uri
Range = fcsRangeToLsp decl.Range }

let sym: WorkspaceSymbol =
{ ContainerName = container
Data = None
Name = decl.LogicalName
Kind = kind
Location = U2.C1 location
Tags = None }

if symbolFilter sym then Some sym else None

[| yield! inner None topLevel.Declaration |> Option.toArray
yield! topLevel.Nested |> Array.choose (inner (Some topLevel.Declaration.LogicalName)) |]

let applyQuery (query: string) (info: SymbolInformation) =
let inline applyQuery (query: string) (info: #IBaseSymbolInformation) =
match query.Split([| '.' |], StringSplitOptions.RemoveEmptyEntries) with
| [||] -> false
| [| fullName |] -> info.Name.StartsWith(fullName, StringComparison.Ordinal)
| [| moduleName; fieldName |] ->
info.Name.StartsWith(fieldName, StringComparison.Ordinal)
&& info.ContainerName = Some moduleName
| parts ->
let containerName = parts.[0 .. (parts.Length - 2)] |> String.concat "."

let cName = parts.[0 .. (parts.Length - 2)] |> String.concat "."
let fieldName = Array.last parts

info.Name.StartsWith(fieldName, StringComparison.Ordinal)
&& info.ContainerName = Some containerName
&& info.ContainerName = Some cName

let getCodeLensInformation (uri: DocumentUri) (typ: string) (topLevel: NavigationTopLevelDeclaration) : CodeLens[] =
let map (decl: NavigationItem) : CodeLens =
Expand Down
16 changes: 14 additions & 2 deletions src/FsAutoComplete/LspHelpers.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,22 @@ module Conversions =
uri: DocumentUri ->
glyphToSymbolKind: (FSharpGlyph -> SymbolKind option) ->
topLevel: NavigationTopLevelDeclaration ->
symbolFilter: (SymbolInformation -> bool) ->
SymbolInformation array

val applyQuery: query: string -> info: SymbolInformation -> bool
val getDocumentSymbols:
glyphToSymbolKind: (FSharpGlyph -> SymbolKind option) ->
topLevel: NavigationTopLevelDeclaration ->
DocumentSymbol array

val getWorkspaceSymbols:
uri: DocumentUri ->
glyphToSymbolKind: (FSharpGlyph -> SymbolKind option) ->
topLevel: NavigationTopLevelDeclaration ->
symbolFilter: (WorkspaceSymbol -> bool) ->
WorkspaceSymbol array


val inline applyQuery: query: string -> info: #IBaseSymbolInformation -> bool

val getCodeLensInformation:
uri: DocumentUri -> typ: string -> topLevel: NavigationTopLevelDeclaration -> CodeLens array
Expand Down
12 changes: 5 additions & 7 deletions src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1304,9 +1304,8 @@ type AdaptiveFSharpLspServer

return
decls
|> Array.collect (fun top ->
getSymbolInformations p.TextDocument.Uri state.GlyphToSymbolKind top (fun _s -> true))
|> U2.C1
|> Array.collect (getDocumentSymbols state.GlyphToSymbolKind)
|> U2.C2
|> Some
with e ->
trace |> Tracing.recordException e
Expand Down Expand Up @@ -1341,9 +1340,8 @@ type AdaptiveFSharpLspServer
let uri = Path.LocalPathToUri p

ns
|> Array.collect (fun n ->
getSymbolInformations uri glyphToSymbolKind n (applyQuery symbolRequest.Query)))
|> U2.C1
|> Array.collect (fun n -> getWorkspaceSymbols uri glyphToSymbolKind n (applyQuery symbolRequest.Query)))
|> U2.C2
|> Some

return res
Expand Down Expand Up @@ -2251,7 +2249,7 @@ type AdaptiveFSharpLspServer

override x.WorkspaceDiagnostic p = x.logUnimplementedRequest p

override x.WorkspaceSymbolResolve p = x.logUnimplementedRequest p
override x.WorkspaceSymbolResolve p = AsyncLspResult.success p

//unsupported -- end

Expand Down
166 changes: 146 additions & 20 deletions test/FsAutoComplete.Tests.Lsp/CoreTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,21 @@ let initTests createServer =
| Result.Error _e -> failtest "Initialization failed"
})

let validateSymbolExists msgType symbolInfos predicate =
Expect.exists
symbolInfos
predicate
$"{msgType}s do not contain the expected symbol"

let allSymbolInfosExist (infos: SymbolInformation seq) predicates =
predicates |> List.iter (validateSymbolExists (nameof SymbolInformation) infos)

let allWorkspaceSymbolsExist (infos: WorkspaceSymbol seq) predicates =
predicates |> List.iter (validateSymbolExists (nameof WorkspaceSymbol) infos)

let allDocumentSymbolsExist (infos: DocumentSymbol seq) predicates =
predicates |> List.iter (validateSymbolExists (nameof DocumentSymbol) infos)

///Tests for getting document symbols
let documentSymbolTest state =
let server =
Expand All @@ -155,30 +170,141 @@ let documentSymbolTest state =

testList
"Document Symbols Tests"
[ testCaseAsync
"Get Document Symbols"
(async {
let! server, path = server
[ testCaseAsync "Get Document Symbols"
<| async {
let! server, path = server

let p: DocumentSymbolParams =
{ TextDocument = { Uri = Path.FilePathToUri path }
WorkDoneToken = None
PartialResultToken = None }
let p: DocumentSymbolParams =
{ TextDocument = { Uri = Path.FilePathToUri path }
WorkDoneToken = None
PartialResultToken = None }

let! res = server.TextDocumentDocumentSymbol p
let! res = server.TextDocumentDocumentSymbol p

match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Result.Ok None -> failtest "Request none"
| Result.Ok(Some(U2.C1 res)) ->
Expect.equal res.Length 15 "Document Symbol has all symbols"
match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Ok None -> failtest "Request none"
| Ok(Some(U2.C1 symbolInformations)) ->
Expect.equal symbolInformations.Length 15 "Document Symbol has all symbols"

allSymbolInfosExist
symbolInformations
[fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class]

| Ok(Some(U2.C2 documentSymbols)) ->
Expect.equal documentSymbols.Length 15 "Document Symbol has all symbols"

allDocumentSymbolsExist
documentSymbols
[fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class]
} ]

let workspaceSymbolTest state =
let server =
async {
let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "WorkspaceSymbolTest")
let! (server, _event) = serverInitialize path defaultConfigDto state
let path = Path.Combine(path, "Script.fsx")
let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path }
do! server.TextDocumentDidOpen tdop
return (server, path)
}
|> Async.Cache

testList
"Workspace Symbols Tests"
[
testCaseAsync "Get Workspace Symbols Using Filename of Script File as Query"
<| async {
let! server, _path = server

let p: WorkspaceSymbolParams =
{ Query = "Script"
WorkDoneToken = None
PartialResultToken = None }

let! res = server.WorkspaceSymbol p

match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Ok None -> failtest "Request none"
| Ok(Some(U2.C1 symbolInfos)) ->
Expect.equal symbolInfos.Length 1 "Workspace did not find all the expected symbols"

allSymbolInfosExist
symbolInfos
[fun n -> n.Name = "Script" && n.Kind = SymbolKind.Module]

| Ok(Some(U2.C2 workspaceSymbols)) ->
Expect.equal workspaceSymbols.Length 1 "Workspace did not find all the expected symbols"

allWorkspaceSymbolsExist
workspaceSymbols
[fun n -> n.Name = "Script" && n.Kind = SymbolKind.Module]
}

testCaseAsync "Get Workspace Symbols Using Query w/ Text"
<| async {
let! server, _path = server

let p: WorkspaceSymbolParams =
{ Query = "X"
WorkDoneToken = None
PartialResultToken = None }

let! res = server.WorkspaceSymbol p

match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Ok None -> failtest "Request none"
| Ok(Some(U2.C1 symbolInfos)) ->
Expect.equal symbolInfos.Length 5 "Workspace did not find all the expected symbols"

allSymbolInfosExist
symbolInfos
[
fun n -> n.Name = "X" && n.Kind = SymbolKind.Class
fun n -> n.Name = "X" && n.Kind = SymbolKind.Class
fun n -> n.Name = "X.X" && n.Kind = SymbolKind.Module
fun n -> n.Name = "X.Y" && n.Kind = SymbolKind.Module
fun n -> n.Name = "X.Z" && n.Kind = SymbolKind.Class
]

| Ok(Some(U2.C2 workspaceSymbols)) ->
Expect.equal workspaceSymbols.Length 5 "Workspace did not find all the expected symbols"

allWorkspaceSymbolsExist
workspaceSymbols
[
fun n -> n.Name = "X" && n.Kind = SymbolKind.Class
fun n -> n.Name = "X" && n.Kind = SymbolKind.Class
fun n -> n.Name = "X.X" && n.Kind = SymbolKind.Module
fun n -> n.Name = "X.Y" && n.Kind = SymbolKind.Module
fun n -> n.Name = "X.Z" && n.Kind = SymbolKind.Class
]
}

testCaseAsync "Get Workspace Symbols Using Query w/o Text"
<| async {
let! server, _path = server

let p: WorkspaceSymbolParams =
{ Query = String.Empty
WorkDoneToken = None
PartialResultToken = None }

let! res = server.WorkspaceSymbol p

match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Ok None -> failtest "Request none"
| Ok(Some(U2.C1 res)) ->
Expect.equal res.Length 0 "Workspace found symbols when we didn't expect to find any"
| Ok(Some(U2.C2 res)) ->
Expect.equal res.Length 0 "Workspace found symbols when we didn't expect to find any"
}
]

Expect.exists
res
(fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class)
"Document symbol contains given symbol"
| Result.Ok(Some(U2.C2 _res)) -> raise (NotImplementedException("DocumentSymbol isn't used in FSAC yet"))
}) ]

let foldingTests state =
let server =
Expand Down
1 change: 1 addition & 0 deletions test/FsAutoComplete.Tests.Lsp/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ let lspTests =

CodeLens.tests createServer
documentSymbolTest createServer
workspaceSymbolTest createServer
Completion.autocompleteTest createServer
Completion.autoOpenTests createServer
Completion.fullNameExternalAutocompleteTest createServer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type X = { Name: string }

module X =

let getName x = x.Name

module X =

let doSideEffect() = ()

module Y =

let getName x = x.Name

type Z = { Name: string }
Loading