Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
* Build a single-file project's `OtherOptions` reference flags with one array comprehension instead of two `Array.ofSeq` calls and an `Array.append`. ([PR #20499](https://github.com/dotnet/fsharp/pull/20499))
* Fix syntax coloring being lost for a whole file when one symbol resolves into metadata that could not be read. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274))
* Go To Definition into an F# project built with a path map (`DeterministicSourcePaths` or `PathMap`) opens its source instead of a generated signature: the IDE no longer applies `--pathmap` to the project options it checks with. ([PR #20470](https://github.com/dotnet/fsharp/pull/20470))
* Go To All (Ctrl+T) on a multi-targeted F# project no longer parses every file once per target framework. ([PR #20483](https://github.com/dotnet/fsharp/pull/20483))
* Go To All (Ctrl+T) searches the documents Visual Studio marks as priority, the ones the user has open, before the rest of a project. ([PR #20615](https://github.com/dotnet/fsharp/pull/20615))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,9 @@ type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Wor

CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions, parsingOptions.LangVersionText

member _.TryGetCachedOptionsByProjectId(projectId) =
reactor.TryGetCachedOptionsByProjectId(projectId)

member _.TryGetOptionsByProject(project) =
reactor.TryGetOptionsByProjectAsync(project)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo
open Microsoft.VisualStudio.LanguageServices
open Microsoft.VisualStudio.Text.PatternMatching

open FSharp.Compiler
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Syntax
open CancellableTasks

[<Struct>]
type private ParseKey =
| AnyDefines of filePath: string * langVersion: string
| DefinedAs of filePath: string * langVersion: string * defines: string list

[<Export(typeof<IFSharpNavigateToSearchService>); Shared>]
type internal FSharpNavigateToSearchService
[<ImportingConstructor>]
(patternMatcherFactory: IPatternMatcherFactory, [<Import(AllowDefault = true)>] workspace: VisualStudioWorkspace) =

let cache = ConcurrentDictionary<DocumentId, VersionStamp * NavigableItem array>()
let cache = ConcurrentDictionary<ParseKey, VersionStamp * NavigableItem array>()

do
if workspace <> null then
Expand All @@ -33,17 +40,48 @@ type internal FSharpNavigateToSearchService
if e.NewSolution.Id <> e.OldSolution.Id then
cache.Clear()

// Awaiting a project's options recomputes them after an edit to a C# project it references.
let parsingOptionsOf (document: Document) =
let optionsManager =
document.Project.Solution.GetFSharpWorkspaceService().FSharpProjectOptionsManager

match optionsManager.TryGetCachedOptionsByProjectId document.Project.Id with
| Some(_, parsingOptions, _) -> CancellableTask.singleton parsingOptions
| None ->
document.GetFSharpCompilationOptionsAsync(nameof (FSharpNavigateToSearchService))
|> CancellableTask.map (fun (_, _, parsingOptions, _) -> parsingOptions)

let storedUnder anyDefinesKey definedAsKey =
function
| ParsedInput.ImplFile(ParsedImplFileInput(trivia = trivia))
| ParsedInput.SigFile(ParsedSigFileInput(trivia = trivia)) when trivia.ConditionalDirectives.IsEmpty -> anyDefinesKey
| _ -> definedAsKey

let getNavigableItems (document: Document) =
cancellableTask {
let! ct = CancellableTask.getCancellationToken ()
let! currentVersion = document.GetTextVersionAsync(ct)
let! parsingOptions = parsingOptionsOf document
let anyDefinesKey = AnyDefines(document.FilePath, parsingOptions.LangVersionText)

let definedAsKey =
DefinedAs(
document.FilePath,
parsingOptions.LangVersionText,
CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions
)

match cache.TryGetValue document.Id with
| true, (version, items) when version = currentVersion -> return items
| _ ->
let cached key =
match cache.TryGetValue key with
| true, (version, items) when version = currentVersion -> ValueSome items
| _ -> ValueNone

match cached anyDefinesKey |> ValueOption.orElseWith (fun () -> cached definedAsKey) with
| ValueSome items -> return items
| ValueNone ->
let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigateToSearchService))
let items = NavigateTo.GetNavigableItems parseResults.ParseTree
cache[document.Id] <- currentVersion, items
cache[storedUnder anyDefinesKey definedAsKey parseResults.ParseTree] <- currentVersion, items
return items
}

Expand Down Expand Up @@ -145,20 +183,26 @@ type internal FSharpNavigateToSearchService

let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet<string>) (document: Document) =
cancellableTask {
let! ct = CancellableTask.getCancellationToken ()

let! sourceText = document.GetTextAsync ct

let! items = getNavigableItems document

let processed =
let matches =
[|
for item in items do
let contains = kinds.Contains(navigateToItemKindToRoslynKind item.Kind)
let patternMatch = tryMatch item
if kinds.Contains(navigateToItemKindToRoslynKind item.Kind) then
match tryMatch item with
| ValueSome m -> yield struct (item, m)
| ValueNone -> ()
|]

match contains, patternMatch with
| true, ValueSome m ->
if matches.Length = 0 then
return [||]
else
let! ct = CancellableTask.getCancellationToken ()
let! sourceText = document.GetTextAsync ct

return
[|
for struct (item, m) in matches do
let sourceSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range)

match sourceSpan with
Expand All @@ -181,34 +225,26 @@ type internal FSharpNavigateToSearchService
sourceSpan
)
)
| _ -> ()
|]

return processed
|]
}

interface IFSharpNavigateToSearchService with
member _.SearchProjectAsync
(project, _priorityDocuments, searchPattern, kinds, cancellationToken)
(project, priorityDocuments, searchPattern, kinds, cancellationToken)
: Task<ImmutableArray<FSharpNavigateToSearchResult>> =
cancellableTask {
let tryMatch = createMatcherFor searchPattern
let priorityIds = ImmutableHashSet.CreateRange(priorityDocuments |> Seq.map _.Id)

let tasks =
[|
for doc in project.Documents do
for doc in project.Documents |> Seq.sortBy (fun doc -> not (priorityIds.Contains doc.Id)) 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
let! results = CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) tasks

return results'.ToImmutable()
return results |> Array.concat |> Array.toImmutableArray

}
|> CancellableTask.start cancellationToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<Compile Include="QuickInfoTests.fs" />
<Compile Include="TaskListServiceTests.fs" />
<Compile Include="NavigateToSearchServiceTests.fs" />
<Compile Include="MultiTargetNavigateToSearchTests.fs" />
<Compile Include="CodeFixes\CodeFixTestFramework.fs" />
<Compile Include="CodeFixes\AddInstanceMemberParameterTests.fs" />
<Compile Include="CodeFixes\ConvertToAnonymousRecordTests.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module FSharp.Editor.Tests.MultiTargetNavigateToSearchTests

open System.IO
open Xunit
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Text
open FSharp.Editor.Tests.Helpers
open FSharp.Test.ProjectGeneration

let private exactNamesIn = NavigateToSearchServiceTests.exactNamesIn

let private syntheticProject =
SyntheticProject.Create(
{ sourceFile "Plain" [] with
ExtraSource = "let plainUse = 1"
},
{ sourceFile "Conditional" [] with
ExtraSource = "#if FOO\nlet fooUse = 1\n#endif"
}
)

let private linkedText path (text: string) (solution: Solution) =
let linked = TextAndVersion.Create(SourceText.From text, VersionStamp.Create())

(solution, solution.GetDocumentIdsWithFilePath path)
||> Seq.fold (fun (solution: Solution) id -> solution.WithDocumentText(id, linked))

[<AutoOpen>]
module private Fixture =
let struct (unlinked, instances) =
RoslynTestHelpers.CreateMultiTargetSolution(
syntheticProject,
[ for defs in [ []; [ "FOO" ] ] -> { Defines = defs; ExcludedFileIds = [] } ]
)

let solution =
(unlinked, syntheticProject.SourceFilePaths)
||> List.fold (fun solution path -> linkedText path (File.ReadAllText path) solution)

let withoutFoo, withFoo = instances[0], instances[1]

[<Theory>]
[<InlineData(0)>]
[<InlineData(1)>]
let ``an instance reports a shared declaration on its own, and again once the parse is cached`` (instance: int) =
let project = solution.GetProject instances[instance]

Assert.Equal<string list>([ "plainUse" ], exactNamesIn project "plainUse")
Assert.Equal<string list>([ "plainUse" ], exactNamesIn project "plainUse")

[<Fact>]
let ``an instance reuses the parse another instance made of the same text`` () =
let path = syntheticProject.GetFilePath "Plain"
let linked = linkedText path "module ModulePlain\nlet reusedUse = 1\n" solution

let searchEveryInstance () =
for instance in instances do
Assert.Equal<string list>([ "reusedUse" ], exactNamesIn (linked.GetProject instance) "reusedUse")

Assert.Equal(1, NavigateToSearchServiceTests.countParseRequests path searchEveryInstance)

[<Fact>]
let ``a declaration behind a directive is reported only by the instance that defines it`` () =
Assert.Equal<string list>([ "fooUse" ], exactNamesIn (solution.GetProject withFoo) "fooUse")
Assert.Equal<string list>([], exactNamesIn (solution.GetProject withoutFoo) "fooUse")

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``a declaration an edit puts behind a directive is reported by the instance that defines it`` (fooFirst: bool) =
Assert.Equal<string list>([ "plainUse" ], exactNamesIn (solution.GetProject withoutFoo) "plainUse")

let edited =
solution.WithDocumentText(
solution.GetDocumentIdsWithFilePath(syntheticProject.GetFilePath "Plain"),
SourceText.From "module ModulePlain\n#if FOO\nlet addedFoo = 1\n#endif\n"
)

let searches = [ withoutFoo, []; withFoo, [ "addedFoo" ] ]

for instance, expected in (if fooFirst then List.rev searches else searches) do
Assert.Equal<string list>(expected, exactNamesIn (edited.GetProject instance) "addedFoo")
Loading
Loading