From b64eea03345bde8bae2d6b52cae3356c714ec843 Mon Sep 17 00:00:00 2001 From: 1eyewonder Date: Mon, 27 Jan 2025 21:06:01 -0600 Subject: [PATCH 1/4] Added ability to search workspace by symbol name --- src/FsAutoComplete/LspHelpers.fs | 74 ++++++-- src/FsAutoComplete/LspHelpers.fsi | 17 +- .../LspServers/AdaptiveFSharpLspServer.fs | 12 +- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 166 +++++++++++++++--- test/FsAutoComplete.Tests.Lsp/Program.fs | 1 + .../TestCases/WorkspaceSymbolTest/Script.fsx | 15 ++ 6 files changed, 246 insertions(+), 39 deletions(-) create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/WorkspaceSymbolTest/Script.fsx diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 28c060745..79912f699 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -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 @@ -158,25 +157,80 @@ 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 (|Name|) name = (^Name: (member Name: string) name) + let inline (|ContainerName|) name = (^ContainerName: (member ContainerName: string option) name) + let inline (|SymbolInfo|) (Name name & ContainerName cName) = name, cName + + let inline applyQuery (query: string) (SymbolInfo(name, containerName)) = match query.Split([| '.' |], StringSplitOptions.RemoveEmptyEntries) with | [||] -> false - | [| fullName |] -> info.Name.StartsWith(fullName, StringComparison.Ordinal) + | [| fullName |] -> name.StartsWith(fullName, StringComparison.Ordinal) | [| moduleName; fieldName |] -> - info.Name.StartsWith(fieldName, StringComparison.Ordinal) - && info.ContainerName = Some moduleName + name.StartsWith(fieldName, StringComparison.Ordinal) + && 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 + name.StartsWith(fieldName, StringComparison.Ordinal) + && containerName = Some cName let getCodeLensInformation (uri: DocumentUri) (typ: string) (topLevel: NavigationTopLevelDeclaration) : CodeLens[] = let map (decl: NavigationItem) : CodeLens = diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 25b0ce2e2..3cafe540b 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -71,10 +71,23 @@ 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< ^info when ^info: (member Name: string) and ^info: (member ContainerName: string option)> : + query: string -> 'info -> bool val getCodeLensInformation: uri: DocumentUri -> typ: string -> topLevel: NavigationTopLevelDeclaration -> CodeLens array diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index bf9be031b..092ee01ab 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -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 @@ -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 @@ -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 diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 2595a8d22..9adbe6d0e 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -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 = @@ -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 = diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 88f1b69ee..e85198116 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -97,6 +97,7 @@ let lspTests = CodeLens.tests createServer documentSymbolTest createServer + workspaceSymbolTest createServer Completion.autocompleteTest createServer Completion.autoOpenTests createServer Completion.fullNameExternalAutocompleteTest createServer diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/WorkspaceSymbolTest/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/WorkspaceSymbolTest/Script.fsx new file mode 100644 index 000000000..2b2edb0b7 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/WorkspaceSymbolTest/Script.fsx @@ -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 } From 120ede7e7678a149e95ef1d2afc67e00957c08ae Mon Sep 17 00:00:00 2001 From: 1eyewonder Date: Mon, 27 Jan 2025 21:13:09 -0600 Subject: [PATCH 2/4] Discarded unused variable causing error --- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 9adbe6d0e..c84bbbcb9 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -216,7 +216,7 @@ let workspaceSymbolTest state = [ testCaseAsync "Get Workspace Symbols Using Filename of Script File as Query" <| async { - let! server, path = server + let! server, _path = server let p: WorkspaceSymbolParams = { Query = "Script" @@ -245,7 +245,7 @@ let workspaceSymbolTest state = testCaseAsync "Get Workspace Symbols Using Query w/ Text" <| async { - let! server, path = server + let! server, _path = server let p: WorkspaceSymbolParams = { Query = "X" @@ -286,7 +286,7 @@ let workspaceSymbolTest state = testCaseAsync "Get Workspace Symbols Using Query w/o Text" <| async { - let! server, path = server + let! server, _path = server let p: WorkspaceSymbolParams = { Query = String.Empty From 7115e106affe9e281354316c12598c15ccf3dd44 Mon Sep 17 00:00:00 2001 From: 1eyewonder Date: Mon, 27 Jan 2025 21:13:36 -0600 Subject: [PATCH 3/4] Updated `applyQuery` to use `IBaseSymbolInformation` --- src/FsAutoComplete/LspHelpers.fs | 16 ++++++---------- src/FsAutoComplete/LspHelpers.fsi | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 79912f699..77cbd8a91 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -214,23 +214,19 @@ module Conversions = [| yield! inner None topLevel.Declaration |> Option.toArray yield! topLevel.Nested |> Array.choose (inner (Some topLevel.Declaration.LogicalName)) |] - let inline (|Name|) name = (^Name: (member Name: string) name) - let inline (|ContainerName|) name = (^ContainerName: (member ContainerName: string option) name) - let inline (|SymbolInfo|) (Name name & ContainerName cName) = name, cName - - let inline applyQuery (query: string) (SymbolInfo(name, containerName)) = + let inline applyQuery (query: string) (info: #IBaseSymbolInformation) = match query.Split([| '.' |], StringSplitOptions.RemoveEmptyEntries) with | [||] -> false - | [| fullName |] -> name.StartsWith(fullName, StringComparison.Ordinal) + | [| fullName |] -> info.Name.StartsWith(fullName, StringComparison.Ordinal) | [| moduleName; fieldName |] -> - name.StartsWith(fieldName, StringComparison.Ordinal) - && containerName = Some moduleName + info.Name.StartsWith(fieldName, StringComparison.Ordinal) + && info.ContainerName = Some moduleName | parts -> let cName = parts.[0 .. (parts.Length - 2)] |> String.concat "." let fieldName = Array.last parts - name.StartsWith(fieldName, StringComparison.Ordinal) - && containerName = Some cName + info.Name.StartsWith(fieldName, StringComparison.Ordinal) + && info.ContainerName = Some cName let getCodeLensInformation (uri: DocumentUri) (typ: string) (topLevel: NavigationTopLevelDeclaration) : CodeLens[] = let map (decl: NavigationItem) : CodeLens = diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 3cafe540b..8f2da861f 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -86,8 +86,8 @@ module Conversions = WorkspaceSymbol array - val inline applyQuery< ^info when ^info: (member Name: string) and ^info: (member ContainerName: string option)> : - query: string -> 'info -> bool + val inline applyQuery : + query: string -> info: #IBaseSymbolInformation -> bool val getCodeLensInformation: uri: DocumentUri -> typ: string -> topLevel: NavigationTopLevelDeclaration -> CodeLens array From 08cb73214377df322b39f01f8e3d7e1db8e39408 Mon Sep 17 00:00:00 2001 From: 1eyewonder Date: Mon, 27 Jan 2025 21:15:52 -0600 Subject: [PATCH 4/4] Fantomas formatting --- src/FsAutoComplete/LspHelpers.fsi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 8f2da861f..857c93fb7 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -86,8 +86,7 @@ module Conversions = WorkspaceSymbol array - val inline applyQuery : - query: string -> info: #IBaseSymbolInformation -> bool + val inline applyQuery: query: string -> info: #IBaseSymbolInformation -> bool val getCodeLensInformation: uri: DocumentUri -> typ: string -> topLevel: NavigationTopLevelDeclaration -> CodeLens array