Skip to content

Commit

Permalink
Added ability to search workspace by symbol name (#1348)
Browse files Browse the repository at this point in the history
* Added ability to search workspace by symbol name

* Discarded unused variable causing error

* Updated `applyQuery` to use `IBaseSymbolInformation`

* Fantomas formatting
  • Loading branch information
1eyewonder authored Jan 28, 2025
1 parent 9277ff9 commit 091727c
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 35 deletions.
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 }

0 comments on commit 091727c

Please sign in to comment.