From 2c8d87c49a1da87a06f9e5ca02e3cb73130ead2b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 17:32:07 +0200 Subject: [PATCH 1/4] Give the quick parsing options the file they parse With no options from the project system yet, the quick parsing options carried no source files, and ParseFile throws looking for the last compiland. Nothing parsed with them before a project had loaded; the Navigate To search that runs during load does. Co-Authored-By: Claude Opus 5 --- .../LanguageService/FSharpProjectOptionsManager.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index f5ea528a9e6..4c5a01b6682 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -663,7 +663,9 @@ type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Wor match reactor.TryGetCachedOptionsByProjectId(documentId.ProjectId) with | Some(_, parsingOptions, _) -> parsingOptions | _ -> + // ParseFile takes the last entry of SourceFiles as the last compiland; with none it throws. { FSharpParsingOptions.Default with + SourceFiles = [| path |] IsInteractive = CompilerEnvironment.IsScriptFile path } From f1bb721f9e0fea4de043d08db04d6fa87b8d10e9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 01:46:43 +0200 Subject: [PATCH 2/4] Answer the Navigate-To search that runs while the solution loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigate To runs a search of its own during load, dispatched through `IAdvancedNavigateToSearchService`. F# did not implement it, so every F# project was reported complete and searched not at all, and no full search follows by design — nothing F# declares could be found until the user searched again. The search is a parse away. What it lacked is the project's compilation options, which do not exist yet during load, so `GetFSharpParseResultsAsync` raises. `GetFSharpQuickParseResultsAsync` parses with whatever parsing options the project system has already produced, or defaults: a dictionary read, no reactor, no I/O, which is what makes it safe to call for every document of every project while the solution loads. Those defines can be the wrong ones, and the document's version does not change when the real options arrive, so the version stamp alone would let an approximate parse answer the accurate search: a declaration behind `#if` could be missed, or reported from a branch that never compiles. The cache entry carries whether it was approximate, and the accurate path refuses those, reparsing instead. The loading path takes either, since its contract allows out-of-date results. `SearchCachedDocumentsAsync` follows the shape of the C# and VB service: priority documents and the projects that hold them are searched first, results are reported per document rather than per project so the first ones appear while the rest are still parsing, and each project is reported complete once it is done. The parses of every project take turns on one throttle that leaves a core free, so a search during load cannot take the cores away from the load itself. Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 + .../LanguageService/WorkspaceExtensions.fs | 6 + .../Navigation/NavigateToSearchService.fs | 169 +++++++++++++----- .../FSharp.Editor.Tests.fsproj | 1 + .../NavigateToSearchWhileLoadingTests.fs | 132 ++++++++++++++ 5 files changed, 269 insertions(+), 41 deletions(-) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 2357fbcdff4..3603357b874 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -13,6 +13,8 @@ * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128), [PR #20695](https://github.com/dotnet/fsharp/pull/20695)) * Fix record-type Rename corrupting copy-and-update expressions, including from abstract signatures, while retaining Find All References results. ([Issue #20791](https://github.com/dotnet/fsharp/issues/20791), [PR #20797](https://github.com/dotnet/fsharp/pull/20797)) +* Navigate To lists F# declarations while the solution is still loading. Until now the search that runs during load skipped F# entirely, and the full search that follows it is never started, so nothing F# declares could be found until the next search. ([PR #20492](https://github.com/dotnet/fsharp/pull/20492)) +* Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Avoid using `cancellableTask` in `DocumentCache`; the editor cache now uses direct `CancellationToken`-aware `task` wrappers, avoiding the background `Task.Run` offload and a larger wrapper closure from the `cancellableTask` builder. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 2406f3a6e32..0d11a4e6b8d 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -581,6 +581,12 @@ type Document with return! checker.ParseDocument(this, parsingOptions, userOpName) } + /// Parses the given F# document with the parsing options its project has already produced, or with defaults when + /// it has none yet: the only parse available while the project system is still loading. The defines can be the + /// wrong ones, so the tree describes a compilation that may never happen. + member this.GetFSharpQuickParseResultsAsync(userOpName) = + this.GetFSharpChecker().ParseDocument(this, this.GetFSharpQuickParsingOptions(), userOpName) + /// Parses and checks the given F# document. member this.GetFSharpParseAndCheckResultsAsync(userOpName) = cancellableTask { diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index ced67f2412e..642ef91e60e 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -7,8 +7,10 @@ open System.IO open System.Composition open System.Collections.Immutable open System.Collections.Concurrent -open System.Threading.Tasks open System.Globalization +open System.Linq +open System.Threading +open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation @@ -19,19 +21,44 @@ open Microsoft.VisualStudio.Text.PatternMatching open FSharp.Compiler.EditorServices open CancellableTasks +/// The navigable items of one parse of a document, and the text version it was taken from. +[] +type private NavigableItemsEntry = + { + Version: VersionStamp + /// Parsed without the project's compilation options, while the solution was still loading. + Approximate: bool + Items: NavigableItem array + } + +/// Parse-tree navigable items per document, cached on the document's text version. +/// Shared by NavigateTo and by the Copilot chat mention provider. [] type internal FSharpNavigableItemsCache [] (patternMatcherFactory: IPatternMatcherFactory, [] workspace: VisualStudioWorkspace) = - let cache = ConcurrentDictionary() + let cache = ConcurrentDictionary() do - if workspace <> null then - workspace.WorkspaceChanged.Add - <| fun e -> + match workspace with + | null -> () + | workspace -> + workspace.WorkspaceChanged.Add(fun e -> if e.NewSolution.Id <> e.OldSolution.Id then - cache.Clear() + cache.Clear()) + + let store (document: Document) version approximate parseTree = + let items = NavigateTo.GetNavigableItems parseTree + + cache[document.Id] <- + { + Version = version + Approximate = approximate + Items = items + } + + items member _.GetNavigableItems(document: Document) = cancellableTask { @@ -39,19 +66,26 @@ type internal FSharpNavigableItemsCache let! currentVersion = document.GetTextVersionAsync(ct) match cache.TryGetValue document.Id with - | true, (version, items) when version = currentVersion -> return items + | true, entry when entry.Version = currentVersion && not entry.Approximate -> return entry.Items | _ -> let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigableItemsCache)) - let items = NavigateTo.GetNavigableItems parseResults.ParseTree - cache[document.Id] <- currentVersion, items - return items + return store document currentVersion false parseResults.ParseTree } - /// Uses the version the document already knows: asking for it could read a closed file from disk. - member _.TryGetCachedNavigableItems(document: Document) = - match cache.TryGetValue document.Id, document.TryGetTextVersion() with - | (true, (version, items)), (true, currentVersion) when version = currentVersion -> ValueSome items - | _ -> ValueNone + /// The items of a parse that does not wait for the project's compilation options, for the search that runs while + /// the solution is still loading. A file behind `#if` can be read under the wrong defines, so the entry it leaves + /// behind never answers `GetNavigableItems`. + member _.GetNavigableItemsWhileLoading(document: Document) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let! currentVersion = document.GetTextVersionAsync(ct) + + match cache.TryGetValue document.Id with + | true, entry when entry.Version = currentVersion -> return entry.Items + | _ -> + let! parseResults = document.GetFSharpQuickParseResultsAsync(nameof (FSharpNavigableItemsCache)) + return store document currentVersion true parseResults.ParseTree + } member _.CreateMatcherFor(searchPattern: string) = let patternMatcher = @@ -84,7 +118,9 @@ type internal FSharpNavigableItemsCache [); Shared>] type internal FSharpNavigateToSearchService [] (itemsCache: FSharpNavigableItemsCache) = - let getNavigableItems (document: Document) = itemsCache.GetNavigableItems document + /// The parses of the search that runs while the solution loads take turns across all its projects, and leave a + /// core to the load itself. + let loadingThrottle = new SemaphoreSlim(max 1 (Environment.ProcessorCount - 1)) let kindsProvided = ImmutableHashSet.Create( @@ -157,16 +193,21 @@ type internal FSharpNavigateToSearchService [] (itemsCache let createMatcherFor (searchPattern: string) = itemsCache.CreateMatcherFor searchPattern - let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet) (document: Document) = + let processDocument + (getItems: Document -> CancellableTask) + (tryMatch: NavigableItem -> PatternMatch voption) + (kinds: IImmutableSet) + (document: Document) + = cancellableTask { let! ct = CancellableTask.getCancellationToken () let! sourceText = document.GetTextAsync ct - let! items = getNavigableItems document + let! items = getItems document let processed = - [| + seq { for item in items do let contains = kinds.Contains(navigateToItemKindToRoslynKind item.Kind) let patternMatch = tryMatch item @@ -196,9 +237,25 @@ type internal FSharpNavigateToSearchService [] (itemsCache ) ) | _ -> () - |] + } + + return processed |> Seq.toImmutableArray + } + + /// Priority items first, each half in its original order, as NavigateTo's own service orders its work. + let prioritize isPriority items = + let priority, rest = items |> Seq.toArray |> Array.partition isPriority + [| yield! priority; yield! rest |] - return processed + let throttled (work: CancellableTask<'a>) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + do! loadingThrottle.WaitAsync ct + + try + return! work + finally + loadingThrottle.Release() |> ignore } interface IFSharpNavigateToSearchService with @@ -208,32 +265,62 @@ type internal FSharpNavigateToSearchService [] (itemsCache cancellableTask { let tryMatch = createMatcherFor searchPattern - let tasks = - [| - for doc in project.Documents do - yield processDocument tryMatch kinds doc - |] - - let! results = CancellableTask.whenAll tasks - - let results' = ImmutableArray.CreateBuilder() - - for navResults in results do - for navResult in navResults do - results'.Add navResult - - return results'.ToImmutable() + let! results = + project.Documents + |> Seq.map (processDocument itemsCache.GetNavigableItems tryMatch kinds) + |> CancellableTask.whenAll + return results |> Seq.collect _.AsEnumerable() |> Seq.toImmutableArray } |> CancellableTask.start cancellationToken member _.SearchDocumentAsync(document: Document, searchPattern, kinds, cancellationToken) = - cancellableTask { - let! result = processDocument (createMatcherFor searchPattern) kinds document - return Array.toImmutableArray result - } - |> CancellableTask.start cancellationToken + processDocument itemsCache.GetNavigableItems (createMatcherFor searchPattern) kinds document cancellationToken member _.KindsProvided = kindsProvided member _.CanFilter = true + + interface IFSharpAdvancedNavigateToSearchService with + member _.SearchCachedDocumentsAsync + ( + _solution, + projects, + priorityDocuments, + searchPattern, + kinds, + _activeDocument, + onResultsFound, + onProjectCompleted, + cancellationToken + ) : Task = + let tryMatch = createMatcherFor searchPattern + let priorityIds = ImmutableHashSet.CreateRange(priorityDocuments |> Seq.map _.Id) + let isPriority (document: Document) = priorityIds.Contains document.Id + + let searchDocumentWhileLoading document = + cancellableTask { + let! results = throttled (processDocument itemsCache.GetNavigableItemsWhileLoading tryMatch kinds document) + + if results.Length > 0 then + do! onResultsFound.Invoke results + } + + // Every document waits on the throttle in the order it is started, so priority documents, and the + // projects that hold them, are parsed first. + let searchProjectWhileLoading (project: Project) = + cancellableTask { + let! _ = + project.Documents + |> prioritize isPriority + |> Seq.map searchDocumentWhileLoading + |> CancellableTask.whenAll + + do! onProjectCompleted.Invoke() + } + + projects + |> prioritize (fun project -> project.Documents |> Seq.exists isPriority) + |> Seq.map searchProjectWhileLoading + |> CancellableTask.whenAll + |> CancellableTask.startAsTask cancellationToken diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index d5d1a09c79a..ceb44d9b616 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -35,6 +35,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs new file mode 100644 index 00000000000..0f171ded8ca --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// Navigate To runs a search of its own while the solution is still loading, and nothing it searches +/// then has its compilation options yet. +module FSharp.Editor.Tests.NavigateToSearchWhileLoadingTests + +open System +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks + +open Xunit + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo + +open FSharp.Editor.Tests.Helpers + +/// A project the project system has not yet handed its command line options: the state every project +/// of the solution is in until it has loaded. +let private loadingProject name source = + let projectId = ProjectId.CreateNewId() + + [ RoslynTestHelpers.CreateDocumentInfo projectId $"C:\\{name}.fs" source ] + |> RoslynTestHelpers.CreateProjectInfo projectId $"C:\\{name}.fsproj" + +let private loadingSolution source = + let project = loadingProject "test" source + let solution = RoslynTestHelpers.CreateSolution [ project ] + project.Id, solution, solution.Projects |> Seq.exactlyOne + +/// One export provider per test: the navigable items are cached in a shared one. +let private searchServices () = + let service: IFSharpNavigateToSearchService = + MefHelpers.createExportProvider().GetExportedValue() + + service, service :?> IFSharpAdvancedNavigateToSearchService + +let private namesFound (results: ImmutableArray) = results |> Seq.map _.Name |> Seq.toList + +/// The loading search as the searcher drives it: results and project completions arrive through callbacks. +/// Returns the names found and how many times a project was reported complete. +let private searchWhileLoading + (service: IFSharpNavigateToSearchService, advanced: IFSharpAdvancedNavigateToSearchService) + (projects: Project list) + pattern + = + task { + let found = ResizeArray() + let completed = ref 0 + + do! + advanced.SearchCachedDocumentsAsync( + (List.head projects).Solution, + ImmutableArray.CreateRange projects, + ImmutableArray.Empty, + pattern, + service.KindsProvided, + null, + (fun results -> + lock found (fun () -> found.AddRange results) + Task.CompletedTask), + (fun () -> + Interlocked.Increment &completed.contents |> ignore + Task.CompletedTask), + CancellationToken.None + ) + + return found |> Seq.map _.Name |> Seq.toList, completed.Value + } + +[] +let ``the loading search finds what the search that waits for the options cannot`` () : Task = + task { + let _, _, project = + loadingSolution "module Sample =\n let declaredWhileLoading = 1\n" + + let (service, _) as services = searchServices () + + let searchAccurately () = + service.SearchProjectAsync(project, ImmutableArray.Empty, "declaredWhileLoading", service.KindsProvided, CancellationToken.None) + :> Task + + let! _ = Assert.ThrowsAnyAsync(fun () -> searchAccurately ()) + + let! names, completed = searchWhileLoading services [ project ] "declaredWhileLoading" + + Assert.Equal([ "declaredWhileLoading" ], names) + Assert.Equal(1, completed) + + // What the loading search left in the cache must not be handed to the accurate search: it was + // read without the project's defines. + let! _ = Assert.ThrowsAnyAsync(fun () -> searchAccurately ()) + () + } + +[] +let ``the loading search reads a file under the wrong defines and does not keep the answer`` () : Task = + task { + let projectId, solution, project = + loadingSolution "#if FOO\nlet fooOnly = 1\n#endif\n" + + let (service, _) as services = searchServices () + + let! namesWhileLoading, _ = searchWhileLoading services [ project ] "fooOnly" + Assert.Equal([], namesWhileLoading) + + { RoslynTestHelpers.DefaultProjectOptions with + OtherOptions = [| "--define:FOO" |] + } + |> RoslynTestHelpers.SetProjectOptions projectId solution + + let! found = service.SearchProjectAsync(project, ImmutableArray.Empty, "fooOnly", service.KindsProvided, CancellationToken.None) + + Assert.Equal([ "fooOnly" ], namesFound found) + } + +[] +let ``every project is reported complete once, whether or not anything is found in it`` () : Task = + task { + let solution = + RoslynTestHelpers.CreateSolution + [ + loadingProject "first" "let found = 1\n" + loadingProject "second" "let other = 2\n" + ] + + let! names, completed = searchWhileLoading (searchServices ()) (solution.Projects |> Seq.toList) "found" + + Assert.Equal([ "found" ], names) + Assert.Equal(2, completed) + } From 91bf88257baf24abd705622df4197f29e857353e Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 26 Sep 2026 13:47:47 +0200 Subject: [PATCH 3/4] Move the release note to the version in development `main` opened 11.0.200 for SDK 11.0.200; 11.0.100 has shipped. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 17bcd329342..7faa0ed9925 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -43,3 +43,7 @@ * Add `WhenKeyword` to `SynMatchClauseTrivia` with the range of the `when` keyword in match clauses. ([PR #20734](https://github.com/dotnet/fsharp/pull/20734)) * Add `OfKeyword` to `SynUnionCaseTrivia` with the range of the `of` keyword in union cases and exception definitions. ([PR #20635](https://github.com/dotnet/fsharp/pull/20635)) * Optimizer: key debug inline specializations on type arguments and witnesses ([PR #20844](https://github.com/dotnet/fsharp/pull/20844)) + +### Breaking Changes + +* `FSharp.Compiler.EditorServices.Structure.getOutliningRanges` now takes the source lines as `ReadOnlyMemory[]` instead of `string[]`, so a caller that already holds the whole text can slice it instead of building a string per line. Callers passing a `string[]` can migrate with `Array.map (fun line -> line.AsMemory())`. From b17a05b4bd0b3fda1a4a2ed09a66fb213d72cd4c Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Thu, 29 Oct 2026 22:15:12 +0100 Subject: [PATCH 4/4] Bring the rebased branch to what it had before the rebase Replaying the branch onto main left some files short of the branch's own content; this restores it. Co-Authored-By: Claude Opus 5.5 --- .../Navigation/NavigateToSearchService.fs | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index 642ef91e60e..3b623dd312e 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -7,10 +7,10 @@ open System.IO open System.Composition open System.Collections.Immutable open System.Collections.Concurrent -open System.Globalization open System.Linq open System.Threading open System.Threading.Tasks +open System.Globalization open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation @@ -41,12 +41,11 @@ type internal FSharpNavigableItemsCache let cache = ConcurrentDictionary() do - match workspace with - | null -> () - | workspace -> - workspace.WorkspaceChanged.Add(fun e -> + if workspace <> null then + workspace.WorkspaceChanged.Add + <| fun e -> if e.NewSolution.Id <> e.OldSolution.Id then - cache.Clear()) + cache.Clear() let store (document: Document) version approximate parseTree = let items = NavigateTo.GetNavigableItems parseTree @@ -87,6 +86,12 @@ type internal FSharpNavigableItemsCache return store document currentVersion true parseResults.ParseTree } + /// Uses the version the document already knows: asking for it could read a closed file from disk. + member _.TryGetCachedNavigableItems(document: Document) = + match cache.TryGetValue document.Id, document.TryGetTextVersion() with + | (true, entry), (true, currentVersion) when entry.Version = currentVersion && not entry.Approximate -> ValueSome entry.Items + | _ -> ValueNone + member _.CreateMatcherFor(searchPattern: string) = let patternMatcher = patternMatcherFactory.CreatePatternMatcher( @@ -207,7 +212,7 @@ type internal FSharpNavigateToSearchService [] (itemsCache let! items = getItems document let processed = - seq { + [| for item in items do let contains = kinds.Contains(navigateToItemKindToRoslynKind item.Kind) let patternMatch = tryMatch item @@ -237,9 +242,9 @@ type internal FSharpNavigateToSearchService [] (itemsCache ) ) | _ -> () - } + |] - return processed |> Seq.toImmutableArray + return processed } /// Priority items first, each half in its original order, as NavigateTo's own service orders its work. @@ -265,17 +270,31 @@ type internal FSharpNavigateToSearchService [] (itemsCache cancellableTask { let tryMatch = createMatcherFor searchPattern - let! results = - project.Documents - |> Seq.map (processDocument itemsCache.GetNavigableItems tryMatch kinds) - |> CancellableTask.whenAll + let tasks = + [| + for doc in project.Documents do + yield processDocument itemsCache.GetNavigableItems tryMatch kinds doc + |] + + let! results = CancellableTask.whenAll tasks + + let results' = ImmutableArray.CreateBuilder() + + for navResults in results do + for navResult in navResults do + results'.Add navResult + + return results'.ToImmutable() - return results |> Seq.collect _.AsEnumerable() |> Seq.toImmutableArray } |> CancellableTask.start cancellationToken member _.SearchDocumentAsync(document: Document, searchPattern, kinds, cancellationToken) = - processDocument itemsCache.GetNavigableItems (createMatcherFor searchPattern) kinds document cancellationToken + cancellableTask { + let! result = processDocument itemsCache.GetNavigableItems (createMatcherFor searchPattern) kinds document + return Array.toImmutableArray result + } + |> CancellableTask.start cancellationToken member _.KindsProvided = kindsProvided @@ -303,7 +322,7 @@ type internal FSharpNavigateToSearchService [] (itemsCache let! results = throttled (processDocument itemsCache.GetNavigableItemsWhileLoading tryMatch kinds document) if results.Length > 0 then - do! onResultsFound.Invoke results + do! onResultsFound.Invoke(Array.toImmutableArray results) } // Every document waits on the throttle in the order it is started, so priority documents, and the