зеркало из https://github.com/dotnet/fsharp.git
Merge pull request #15143 from dotnet/merges/main-to-release/dev17.7
Merge main to release/dev17.7
This commit is contained in:
Коммит
1bbbdb332e
|
@ -363,9 +363,15 @@ stages:
|
|||
displayName: Setup VS Hive
|
||||
condition: or(eq(variables['_testKind'], 'testVs'), eq(variables['_testKind'], 'testIntegration'))
|
||||
|
||||
# yes, this is miserable, but - https://github.com/dotnet/arcade/issues/13239
|
||||
- script: eng\CIBuild.cmd -compressallmetadata -configuration $(_configuration) -$(_testKind)
|
||||
displayName: Build / Test
|
||||
continueOnError: ${{ eq(variables['_testKind'], 'testIntegration') }}
|
||||
condition: ne(variables['_testKind'], 'testIntegration')
|
||||
- script: eng\CIBuild.cmd -compressallmetadata -configuration $(_configuration) -$(_testKind)
|
||||
displayName: Build / Integration Test
|
||||
continueOnError: true
|
||||
condition: eq(variables['_testKind'], 'testIntegration')
|
||||
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
|
|
|
@ -160,7 +160,7 @@
|
|||
<MicrosoftVisualStudioTextInternalVersion>$(VisualStudioEditorPackagesVersion)</MicrosoftVisualStudioTextInternalVersion>
|
||||
<MicrosoftVisualStudioComponentModelHostVersion>$(VisualStudioEditorPackagesVersion)</MicrosoftVisualStudioComponentModelHostVersion>
|
||||
<NuGetSolutionRestoreManagerInteropVersion>5.6.0</NuGetSolutionRestoreManagerInteropVersion>
|
||||
<MicrosoftVisualStudioExtensibilityTestingVersion>0.1.149-beta</MicrosoftVisualStudioExtensibilityTestingVersion>
|
||||
<MicrosoftVisualStudioExtensibilityTestingVersion>0.1.169-beta</MicrosoftVisualStudioExtensibilityTestingVersion>
|
||||
<MicrosoftVisualStudioExtensibilityTestingSourceGeneratorVersion>$(MicrosoftVisualStudioExtensibilityTestingVersion)</MicrosoftVisualStudioExtensibilityTestingSourceGeneratorVersion>
|
||||
<MicrosoftVisualStudioExtensibilityTestingXunitVersion>$(MicrosoftVisualStudioExtensibilityTestingVersion)</MicrosoftVisualStudioExtensibilityTestingXunitVersion>
|
||||
<!-- Visual Studio Threading packags -->
|
||||
|
|
|
@ -108,22 +108,6 @@ let rec processStateEntry (queryTrie: QueryTrie) (state: FileContentQueryState)
|
|||
FoundDependencies = foundDependencies
|
||||
}
|
||||
|
||||
/// Return all files contained in the trie.
|
||||
let filesInTrie (node: TrieNode) : Set<FileIndex> =
|
||||
let rec collect (node: TrieNode) (continuation: FileIndex list -> FileIndex list) : FileIndex list =
|
||||
let continuations: ((FileIndex list -> FileIndex list) -> FileIndex list) list =
|
||||
[
|
||||
for node in node.Children.Values do
|
||||
yield collect node
|
||||
]
|
||||
|
||||
let finalContinuation indexes =
|
||||
continuation [ yield! node.Files; yield! List.concat indexes ]
|
||||
|
||||
Continuation.sequence continuations finalContinuation
|
||||
|
||||
Set.ofList (collect node id)
|
||||
|
||||
/// <summary>
|
||||
/// For a given file's content, collect all missing ("ghost") file dependencies that the core resolution algorithm didn't return,
|
||||
/// but are required to satisfy the type-checker.
|
||||
|
@ -136,16 +120,16 @@ let filesInTrie (node: TrieNode) : Set<FileIndex> =
|
|||
/// - the namespace does not contain any children that can be referenced implicitly (eg. by type inference),
|
||||
/// then the main resolution algorithm does not create a link to any file defining the namespace.</para>
|
||||
/// <para>However, to satisfy the type-checker, the namespace must be resolved.
|
||||
/// This function returns a list of extra dependencies that makes sure that any such namespaces can be resolved (if it exists).
|
||||
/// For each unused open namespace we return one or more file links that define it.</para>
|
||||
/// This function returns an array with a potential extra dependencies that makes sure that any such namespaces can be resolved (if they exists).
|
||||
/// For each unused namespace `open` we return at most one file that defines that namespace.</para>
|
||||
/// </remarks>
|
||||
let collectGhostDependencies (fileIndex: FileIndex) (trie: TrieNode) (queryTrie: QueryTrie) (result: FileContentQueryState) =
|
||||
// Go over all open namespaces, and assert all those links eventually went anywhere
|
||||
// For each opened namespace, if none of already resolved dependencies define it, return the top-most file that defines it.
|
||||
Set.toArray result.OpenedNamespaces
|
||||
|> Array.collect (fun path ->
|
||||
|> Array.choose (fun path ->
|
||||
match queryTrie path with
|
||||
| QueryTrieNodeResult.NodeExposesData _
|
||||
| QueryTrieNodeResult.NodeDoesNotExist -> Array.empty
|
||||
| QueryTrieNodeResult.NodeDoesNotExist -> None
|
||||
| QueryTrieNodeResult.NodeDoesNotExposeData ->
|
||||
// At this point we are following up if an open namespace really lead nowhere.
|
||||
let node =
|
||||
|
@ -156,25 +140,23 @@ let collectGhostDependencies (fileIndex: FileIndex) (trie: TrieNode) (queryTrie:
|
|||
|
||||
find trie path
|
||||
|
||||
let filesDefiningNamespace =
|
||||
filesInTrie node |> Set.filter (fun idx -> idx < fileIndex)
|
||||
|
||||
let dependenciesDefiningNamespace =
|
||||
Set.intersect result.FoundDependencies filesDefiningNamespace
|
||||
|
||||
[|
|
||||
if Set.isEmpty dependenciesDefiningNamespace then
|
||||
// There is no existing dependency defining the namespace,
|
||||
// so we need to add one.
|
||||
if Set.isEmpty filesDefiningNamespace then
|
||||
// No file defines inferrable symbols for this namespace, but the namespace might exist.
|
||||
// Because we don't track what files define a namespace without any relevant content,
|
||||
// the only way to ensure the namespace is in scope is to add a link to every preceding file.
|
||||
yield! [| 0 .. (fileIndex - 1) |]
|
||||
else
|
||||
// At least one file defines the namespace - add a dependency to the first (top) one.
|
||||
yield Seq.head filesDefiningNamespace
|
||||
|])
|
||||
match node.Current with
|
||||
// Both Root and module would expose data, so we can ignore them.
|
||||
| Root _
|
||||
| Module _ -> None
|
||||
| Namespace (filesDefiningNamespaceWithoutTypes = filesDefiningNamespaceWithoutTypes) ->
|
||||
if filesDefiningNamespaceWithoutTypes.Overlaps(result.FoundDependencies) then
|
||||
// The ghost dependency is already covered by a real dependency.
|
||||
None
|
||||
else
|
||||
// We are only interested in any file that contained the namespace when they came before the current file.
|
||||
// If the namespace is defined in a file after the current file then there is no way the current file can reference it.
|
||||
// Which means that namespace would come from a different assembly.
|
||||
filesDefiningNamespaceWithoutTypes
|
||||
|> Seq.sort
|
||||
|> Seq.tryFind (fun connectedFileIdx ->
|
||||
// We pick the lowest file index from the namespace to satisfy the type-checker for the open statement.
|
||||
connectedFileIdx < fileIndex))
|
||||
|
||||
let mkGraph (compilingFSharpCore: bool) (filePairs: FilePairMap) (files: FileInProject array) : Graph<FileIndex> =
|
||||
// We know that implementation files backed by signatures cannot be depended upon.
|
||||
|
@ -190,61 +172,71 @@ let mkGraph (compilingFSharpCore: bool) (filePairs: FilePairMap) (files: FileInP
|
|||
let trie = TrieMapping.mkTrie trieInput
|
||||
let queryTrie: QueryTrie = queryTrieMemoized trie
|
||||
|
||||
let fileContents = files |> Array.Parallel.map FileContentMapping.mkFileContent
|
||||
let fileContents =
|
||||
files
|
||||
|> Array.Parallel.map (fun file ->
|
||||
if file.Idx = 0 then
|
||||
List.empty
|
||||
else
|
||||
FileContentMapping.mkFileContent file)
|
||||
|
||||
let findDependencies (file: FileInProject) : FileIndex array =
|
||||
let fileContent = fileContents[file.Idx]
|
||||
if file.Idx = 0 then
|
||||
// First file cannot have any dependencies.
|
||||
Array.empty
|
||||
else
|
||||
let fileContent = fileContents[file.Idx]
|
||||
|
||||
let knownFiles = [ 0 .. (file.Idx - 1) ] |> set
|
||||
// File depends on all files above it that define accessible symbols at the root level (global namespace).
|
||||
let filesFromRoot = trie.Files |> Set.filter (fun rootIdx -> rootIdx < file.Idx)
|
||||
// Start by listing root-level dependencies.
|
||||
let initialDepsResult =
|
||||
(FileContentQueryState.Create file.Idx knownFiles filesFromRoot), fileContent
|
||||
// Sequentially process all relevant entries of the file and keep updating the state and set of dependencies.
|
||||
let depsResult =
|
||||
initialDepsResult
|
||||
// Seq is faster than List in this case.
|
||||
||> Seq.fold (processStateEntry queryTrie)
|
||||
let knownFiles = [ 0 .. (file.Idx - 1) ] |> set
|
||||
// File depends on all files above it that define accessible symbols at the root level (global namespace).
|
||||
let filesFromRoot = trie.Files |> Set.filter (fun rootIdx -> rootIdx < file.Idx)
|
||||
// Start by listing root-level dependencies.
|
||||
let initialDepsResult =
|
||||
(FileContentQueryState.Create file.Idx knownFiles filesFromRoot), fileContent
|
||||
// Sequentially process all relevant entries of the file and keep updating the state and set of dependencies.
|
||||
let depsResult =
|
||||
initialDepsResult
|
||||
// Seq is faster than List in this case.
|
||||
||> Seq.fold (processStateEntry queryTrie)
|
||||
|
||||
// Add missing links for cases where an unused open namespace did not create a link.
|
||||
let ghostDependencies = collectGhostDependencies file.Idx trie queryTrie depsResult
|
||||
// Add missing links for cases where an unused open namespace did not create a link.
|
||||
let ghostDependencies = collectGhostDependencies file.Idx trie queryTrie depsResult
|
||||
|
||||
// Add a link from implementation files to their signature files.
|
||||
let signatureDependency =
|
||||
match filePairs.TryGetSignatureIndex file.Idx with
|
||||
| None -> Array.empty
|
||||
| Some sigIdx -> Array.singleton sigIdx
|
||||
// Add a link from implementation files to their signature files.
|
||||
let signatureDependency =
|
||||
match filePairs.TryGetSignatureIndex file.Idx with
|
||||
| None -> Array.empty
|
||||
| Some sigIdx -> Array.singleton sigIdx
|
||||
|
||||
// Files in FSharp.Core have an implicit dependency on `prim-types-prelude.fsi` - add it.
|
||||
let fsharpCoreImplicitDependencies =
|
||||
let filename = "prim-types-prelude.fsi"
|
||||
// Files in FSharp.Core have an implicit dependency on `prim-types-prelude.fsi` - add it.
|
||||
let fsharpCoreImplicitDependencies =
|
||||
let filename = "prim-types-prelude.fsi"
|
||||
|
||||
let implicitDepIdx =
|
||||
files
|
||||
|> Array.tryFindIndex (fun f -> FileSystemUtils.fileNameOfPath f.FileName = filename)
|
||||
let implicitDepIdx =
|
||||
files
|
||||
|> Array.tryFindIndex (fun f -> FileSystemUtils.fileNameOfPath f.FileName = filename)
|
||||
|
||||
[|
|
||||
if compilingFSharpCore then
|
||||
match implicitDepIdx with
|
||||
| Some idx ->
|
||||
if file.Idx > idx then
|
||||
yield idx
|
||||
| None ->
|
||||
exn $"Expected to find file '{filename}' during compilation of FSharp.Core, but it was not found."
|
||||
|> raise
|
||||
|]
|
||||
[|
|
||||
if compilingFSharpCore then
|
||||
match implicitDepIdx with
|
||||
| Some idx ->
|
||||
if file.Idx > idx then
|
||||
yield idx
|
||||
| None ->
|
||||
exn $"Expected to find file '{filename}' during compilation of FSharp.Core, but it was not found."
|
||||
|> raise
|
||||
|]
|
||||
|
||||
let allDependencies =
|
||||
[|
|
||||
yield! depsResult.FoundDependencies
|
||||
yield! ghostDependencies
|
||||
yield! signatureDependency
|
||||
yield! fsharpCoreImplicitDependencies
|
||||
|]
|
||||
|> Array.distinct
|
||||
let allDependencies =
|
||||
[|
|
||||
yield! depsResult.FoundDependencies
|
||||
yield! ghostDependencies
|
||||
yield! signatureDependency
|
||||
yield! fsharpCoreImplicitDependencies
|
||||
|]
|
||||
|> Array.distinct
|
||||
|
||||
allDependencies
|
||||
allDependencies
|
||||
|
||||
files
|
||||
|> Array.Parallel.map (fun file -> file.Idx, findDependencies file)
|
||||
|
|
|
@ -56,14 +56,18 @@ let doesFileExposeContentToTheRoot (ast: ParsedInput) : bool =
|
|||
|| kind = SynModuleOrNamespaceKind.GlobalNamespace)
|
||||
|
||||
let mergeTrieNodes (defaultChildSize: int) (tries: TrieNode array) =
|
||||
/// Add the current node as child node to the root node.
|
||||
/// If the node already exists and is a namespace node, the existing node will be updated with new information via mutation.
|
||||
let rec mergeTrieNodesAux (root: TrieNode) (KeyValue (k, v)) =
|
||||
if root.Children.ContainsKey k then
|
||||
let node = root.Children[k]
|
||||
|
||||
match node.Current, v.Current with
|
||||
| TrieNodeInfo.Namespace (filesThatExposeTypes = currentFiles), TrieNodeInfo.Namespace (filesThatExposeTypes = otherFiles) ->
|
||||
for otherFile in otherFiles do
|
||||
currentFiles.Add(otherFile) |> ignore
|
||||
| TrieNodeInfo.Namespace (filesThatExposeTypes = currentFilesThatExposeTypes
|
||||
filesDefiningNamespaceWithoutTypes = currentFilesWithoutTypes),
|
||||
TrieNodeInfo.Namespace (filesThatExposeTypes = otherFiles; filesDefiningNamespaceWithoutTypes = otherFilesWithoutTypes) ->
|
||||
currentFilesThatExposeTypes.UnionWith otherFiles
|
||||
currentFilesWithoutTypes.UnionWith otherFilesWithoutTypes
|
||||
| _ -> ()
|
||||
|
||||
for kv in v.Children do
|
||||
|
@ -142,13 +146,13 @@ let processSynModuleOrNamespace<'Decl>
|
|||
// The reasoning is that a type could be inferred and a nested auto open module will lift its content one level up.
|
||||
let current =
|
||||
if isNamespace then
|
||||
TrieNodeInfo.Namespace(
|
||||
name,
|
||||
(if hasTypesOrAutoOpenNestedModules then
|
||||
HashSet.singleton idx
|
||||
else
|
||||
HashSet.empty ())
|
||||
)
|
||||
let filesThatExposeTypes, filesDefiningNamespaceWithoutTypes =
|
||||
if hasTypesOrAutoOpenNestedModules then
|
||||
HashSet.singleton idx, HashSet.empty ()
|
||||
else
|
||||
HashSet.empty (), HashSet.singleton idx
|
||||
|
||||
TrieNodeInfo.Namespace(name, filesThatExposeTypes, filesDefiningNamespaceWithoutTypes)
|
||||
else
|
||||
TrieNodeInfo.Module(name, idx)
|
||||
|
||||
|
@ -167,7 +171,7 @@ let processSynModuleOrNamespace<'Decl>
|
|||
|
||||
visit
|
||||
(fun node ->
|
||||
let files =
|
||||
let filesThatExposeTypes, filesDefiningNamespaceWithoutTypes =
|
||||
match tail with
|
||||
| [ _ ] ->
|
||||
// In case you have:
|
||||
|
@ -179,12 +183,13 @@ let processSynModuleOrNamespace<'Decl>
|
|||
let topLevelModuleOrNamespaceHasAutoOpen = isAnyAttributeAutoOpen attributes
|
||||
|
||||
if topLevelModuleOrNamespaceHasAutoOpen && not isNamespace then
|
||||
HashSet.singleton idx
|
||||
HashSet.singleton idx, HashSet.empty ()
|
||||
else
|
||||
HashSet.empty ()
|
||||
| _ -> HashSet.empty ()
|
||||
HashSet.empty (), HashSet.singleton idx
|
||||
| _ -> HashSet.empty (), HashSet.singleton idx
|
||||
|
||||
let current = TrieNodeInfo.Namespace(name, files)
|
||||
let current =
|
||||
TrieNodeInfo.Namespace(name, filesThatExposeTypes, filesDefiningNamespaceWithoutTypes)
|
||||
|
||||
mkSingletonDict name { Current = current; Children = node } |> continuation)
|
||||
tail
|
||||
|
|
|
@ -32,7 +32,7 @@ type internal FileInProject =
|
|||
type internal TrieNodeInfo =
|
||||
| Root of files: HashSet<FileIndex>
|
||||
| Module of name: Identifier * file: FileIndex
|
||||
| Namespace of name: Identifier * filesThatExposeTypes: HashSet<FileIndex>
|
||||
| Namespace of name: Identifier * filesThatExposeTypes: HashSet<FileIndex> * filesDefiningNamespaceWithoutTypes: HashSet<FileIndex>
|
||||
|
||||
member x.Files: Set<FileIndex> =
|
||||
match x with
|
||||
|
|
|
@ -30,7 +30,12 @@ type internal FileInProject =
|
|||
type internal TrieNodeInfo =
|
||||
| Root of files: HashSet<FileIndex>
|
||||
| Module of name: Identifier * file: FileIndex
|
||||
| Namespace of name: Identifier * filesThatExposeTypes: HashSet<FileIndex>
|
||||
| Namespace of
|
||||
name: Identifier *
|
||||
/// Files that expose types that are part of this namespace.
|
||||
filesThatExposeTypes: HashSet<FileIndex> *
|
||||
/// Files that use this namespace but don't contain any types.
|
||||
filesDefiningNamespaceWithoutTypes: HashSet<FileIndex>
|
||||
|
||||
member Files: Set<FileIndex>
|
||||
|
||||
|
|
|
@ -34,8 +34,8 @@ let ``Calculate transitive graph`` () =
|
|||
[<Fact>]
|
||||
let ``Reverse graph`` () =
|
||||
let graph = Graph.make [ "a", Array.empty; "b", [| "a" |] ]
|
||||
let reserved = Graph.reverse graph
|
||||
let valueA = reserved["a"]
|
||||
let reversed = Graph.reverse graph
|
||||
let valueA = reversed["a"]
|
||||
Assert.Equal<string array>([| "b" |], valueA)
|
||||
let valueB = reserved["b"]
|
||||
let valueB = reversed["b"]
|
||||
Assert.Equal<string array>(Array.empty, valueB)
|
||||
|
|
|
@ -636,7 +636,7 @@ let private fantomasCoreTrie: TrieNode =
|
|||
[|
|
||||
"System",
|
||||
{
|
||||
Current = TrieNodeInfo.Namespace("System", emptyHS ())
|
||||
Current = TrieNodeInfo.Namespace("System", emptyHS (), emptyHS ())
|
||||
Children =
|
||||
dictionary
|
||||
[|
|
||||
|
@ -649,13 +649,35 @@ let private fantomasCoreTrie: TrieNode =
|
|||
}
|
||||
"Fantomas",
|
||||
{
|
||||
Current = TrieNodeInfo.Namespace("Fantomas", emptyHS ())
|
||||
Current =
|
||||
TrieNodeInfo.Namespace(
|
||||
"Fantomas",
|
||||
emptyHS (),
|
||||
HashSet([|
|
||||
indexOf "ISourceTextExtensions.fs"
|
||||
indexOf "RangeHelpers.fs"
|
||||
indexOf "AstExtensions.fs"
|
||||
indexOf "TriviaTypes.fs"
|
||||
indexOf "Utils.fs"
|
||||
indexOf "SourceParser.fs"
|
||||
|]))
|
||||
Children =
|
||||
dictionary
|
||||
[|
|
||||
"Core",
|
||||
{
|
||||
Current = TrieNodeInfo.Namespace("Core", emptyHS ())
|
||||
Current =
|
||||
TrieNodeInfo.Namespace(
|
||||
"Core",
|
||||
emptyHS (),
|
||||
HashSet([|
|
||||
indexOf "ISourceTextExtensions.fs"
|
||||
indexOf "RangeHelpers.fs"
|
||||
indexOf "AstExtensions.fs"
|
||||
indexOf "TriviaTypes.fs"
|
||||
indexOf "Utils.fs"
|
||||
indexOf "SourceParser.fs"
|
||||
|]))
|
||||
Children =
|
||||
dictionary
|
||||
[|
|
||||
|
|
|
@ -263,12 +263,12 @@ let c = 0
|
|||
"""
|
||||
(set [| 0 |])
|
||||
]
|
||||
// This is a very last resort measure to link C to all files that came before it.
|
||||
// `open X` does exist but there is no file that is actively contributing to the X namespace
|
||||
// `open X` does exist but there is no file that is actively contributing to the X namespace.
|
||||
// This is a trade-off scenario, if A.fs had a type or nested module we would consider it to contribute to the X namespace.
|
||||
// As it is empty, we don't include the file index in the trie.
|
||||
// To satisfy the open statement we link it to the lowest file idx of the found namespace node X in the trie.
|
||||
scenario
|
||||
"A open statement that leads nowhere should link to every file that came above it."
|
||||
"An open statement that leads to a namespace node without any types, should link to the lowest file idx of that namespace node."
|
||||
[
|
||||
sourceFile
|
||||
"A.fs"
|
||||
|
@ -289,7 +289,7 @@ namespace Z
|
|||
|
||||
open X
|
||||
"""
|
||||
(set [| 0; 1 |])
|
||||
(set [| 0 |])
|
||||
]
|
||||
// The nested module in this case adds content to the namespace
|
||||
// Similar if a namespace had a type.
|
||||
|
@ -577,4 +577,39 @@ let Foo () : unit =
|
|||
"""
|
||||
(set [| 0 |])
|
||||
]
|
||||
scenario
|
||||
"Ghost dependency takes file index into account"
|
||||
[
|
||||
sourceFile "X.fs" "module X" Set.empty
|
||||
// opened namespace 'System.IO' will be found in the Trie.
|
||||
// However, we should not link A.fs to X.fs (because of the ghost dependency mechanism)
|
||||
// because B.fs introduces nodes `System` and `IO` and comes after A.fs.
|
||||
sourceFile
|
||||
"A.fs"
|
||||
"""
|
||||
module A
|
||||
|
||||
open System.IO
|
||||
"""
|
||||
Set.empty
|
||||
sourceFile "B.fs" "namespace System.IO" Set.empty
|
||||
]
|
||||
scenario
|
||||
"Ghost dependency that is already linked via module"
|
||||
[
|
||||
sourceFile "X.fs" "module Foo.Bar.X" Set.empty
|
||||
sourceFile "Y.fs" "module Foo.Bar.Y" Set.empty
|
||||
// This file is linked to Y.fs due to opening the module `Foo.Bar.Y`
|
||||
// The link to Y.fs should also satisfy the ghost dependency created after opening `Foo.Bar`.
|
||||
// There is no need to add an additional link to the lowest index in node `Foo.Bar`.
|
||||
sourceFile
|
||||
"Z.fs"
|
||||
"""
|
||||
module Z
|
||||
|
||||
open Foo.Bar // ghost dependency
|
||||
open Foo.Bar.Y // Y.fs
|
||||
"""
|
||||
(set [| 1 |])
|
||||
]
|
||||
]
|
||||
|
|
|
@ -127,7 +127,7 @@ type internal FSharpWorkspaceServiceFactory [<System.Composition.ImportingConstr
|
|||
let useSyntaxTreeCache = editorOptions.LanguageServicePerformance.UseSyntaxTreeCache
|
||||
|
||||
let enableFastFindReferences =
|
||||
editorOptions.LanguageServicePerformance.EnableFastFindReferences
|
||||
editorOptions.LanguageServicePerformance.EnableFastFindReferencesAndRename
|
||||
|
||||
let isInlineParameterNameHintsEnabled =
|
||||
editorOptions.Advanced.IsInlineParameterNameHintsEnabled
|
||||
|
|
|
@ -81,7 +81,7 @@ type LanguageServicePerformanceOptions =
|
|||
AllowStaleCompletionResults: bool
|
||||
TimeUntilStaleCompletion: int
|
||||
EnableParallelReferenceResolution: bool
|
||||
EnableFastFindReferences: bool
|
||||
EnableFastFindReferencesAndRename: bool
|
||||
EnablePartialTypeChecking: bool
|
||||
UseSyntaxTreeCache: bool
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ type LanguageServicePerformanceOptions =
|
|||
AllowStaleCompletionResults = true
|
||||
TimeUntilStaleCompletion = 2000 // In ms, so this is 2 seconds
|
||||
EnableParallelReferenceResolution = false
|
||||
EnableFastFindReferences = FSharpExperimentalFeaturesEnabledAutomatically
|
||||
EnableFastFindReferencesAndRename = true
|
||||
EnablePartialTypeChecking = true
|
||||
UseSyntaxTreeCache = FSharpExperimentalFeaturesEnabledAutomatically
|
||||
}
|
||||
|
@ -237,4 +237,4 @@ module EditorOptionsExtensions =
|
|||
this.EditorOptions.Advanced.IsBlockStructureEnabled
|
||||
|
||||
member this.IsFastFindReferencesEnabled =
|
||||
this.EditorOptions.LanguageServicePerformance.EnableFastFindReferences
|
||||
this.EditorOptions.LanguageServicePerformance.EnableFastFindReferencesAndRename
|
||||
|
|
|
@ -65,8 +65,8 @@
|
|||
</GroupBox>
|
||||
<GroupBox Header="{x:Static local:Strings.Find_References_Performance}">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="enableFastFindReferences"
|
||||
IsChecked="{Binding EnableFastFindReferences}"
|
||||
<CheckBox x:Name="enableFastFindReferencesAndRename"
|
||||
IsChecked="{Binding EnableFastFindReferencesAndRename}"
|
||||
Content="{x:Static local:Strings.Enable_Fast_Find_References}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
|
|
@ -35,4 +35,60 @@ let increment = add 1
|
|||
|
||||
Assert.Contains(expectedText, actualText);
|
||||
}
|
||||
|
||||
[IdeFact]
|
||||
public async Task FsiAndFsFilesGoToCorrespondentDefinitions()
|
||||
{
|
||||
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
|
||||
|
||||
var fsi = """
|
||||
module Module
|
||||
|
||||
type SomeType =
|
||||
| Number of int
|
||||
| Letter of char
|
||||
|
||||
val id: t: SomeType -> SomeType
|
||||
""";
|
||||
var fs = """
|
||||
module Module
|
||||
|
||||
type SomeType =
|
||||
| Number of int
|
||||
| Letter of char
|
||||
|
||||
let id (t: SomeType) = t
|
||||
""";
|
||||
|
||||
await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
|
||||
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
|
||||
|
||||
// hack: when asked to add a file, VS API seems to insert it in the alphabetical order
|
||||
// so adding Module.fsi and Module.fs we'll end up having signature file below the code file
|
||||
// and this won't work. But it's possible to achieve having the right file order via their renaming
|
||||
await SolutionExplorer.AddFileAsync("Library", "AModule.fsi", fsi, TestToken);
|
||||
await SolutionExplorer.AddFileAsync("Library", "Module.fs", fs, TestToken);
|
||||
await SolutionExplorer.RenameFileAsync("Library", "AModule.fsi", "Module.fsi", TestToken);
|
||||
await SolutionExplorer.BuildSolutionAsync(TestToken);
|
||||
|
||||
await SolutionExplorer.OpenFileAsync("Library", "Module.fsi", TestToken);
|
||||
await Editor.PlaceCaretAsync("SomeType ->", TestToken);
|
||||
await Shell.ExecuteCommandAsync(VSStd97CmdID.GotoDefn, TestToken);
|
||||
var expectedText = "type SomeType =";
|
||||
var expectedWindow = "Module.fsi";
|
||||
var actualText = await Editor.GetCurrentLineTextAsync(TestToken);
|
||||
var actualWindow = await Shell.GetActiveWindowCaptionAsync(TestToken);
|
||||
Assert.Equal(expectedText, actualText);
|
||||
Assert.Equal(expectedWindow, actualWindow);
|
||||
|
||||
await SolutionExplorer.OpenFileAsync("Library", "Module.fs", TestToken);
|
||||
await Editor.PlaceCaretAsync("SomeType)", TestToken);
|
||||
await Shell.ExecuteCommandAsync(VSStd97CmdID.GotoDefn, TestToken);
|
||||
expectedText = "type SomeType =";
|
||||
expectedWindow = "Module.fs";
|
||||
actualText = await Editor.GetCurrentLineTextAsync(TestToken);
|
||||
actualWindow = await Shell.GetActiveWindowCaptionAsync(TestToken);
|
||||
Assert.Equal(expectedText, actualText);
|
||||
Assert.Equal(expectedWindow, actualWindow);
|
||||
}
|
||||
}
|
|
@ -35,25 +35,6 @@ internal partial class SolutionExplorerInProcess
|
|||
var solutionPath = CreateTemporaryPath();
|
||||
await CreateSolutionAsync(solutionPath, solutionName, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task OpenFileAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
||||
var filePath = await GetAbsolutePathForProjectRelativeFilePathAsync(projectName, relativeFilePath, cancellationToken);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException(filePath);
|
||||
}
|
||||
|
||||
VsShellUtilities.OpenDocument(ServiceProvider.GlobalProvider, filePath, VSConstants.LOGVIEWID.Code_guid, out _, out _, out _, out var view);
|
||||
|
||||
// Reliably set focus using NavigateToLineAndColumn
|
||||
var textManager = await GetRequiredGlobalServiceAsync<SVsTextManager, IVsTextManager>(cancellationToken);
|
||||
ErrorHandler.ThrowOnFailure(view.GetBuffer(out var textLines));
|
||||
ErrorHandler.ThrowOnFailure(view.GetCaretPos(out var line, out var column));
|
||||
ErrorHandler.ThrowOnFailure(textManager.NavigateToLineAndColumn(textLines, VSConstants.LOGVIEWID.Code_guid, line, column, line, column));
|
||||
}
|
||||
|
||||
private async Task CreateSolutionAsync(string solutionPath, string solutionName, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -207,19 +188,6 @@ internal partial class SolutionExplorerInProcess
|
|||
return Path.Combine(Path.GetTempPath(), "fsharp-test", Path.GetRandomFileName());
|
||||
}
|
||||
|
||||
private async Task<string> GetAbsolutePathForProjectRelativeFilePathAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
||||
var dte = await GetRequiredGlobalServiceAsync<SDTE, EnvDTE.DTE>(cancellationToken);
|
||||
var solution = dte.Solution;
|
||||
Assumes.Present(solution);
|
||||
|
||||
var project = solution.Projects.Cast<EnvDTE.Project>().First(x => x.Name == projectName);
|
||||
var projectPath = Path.GetDirectoryName(project.FullName);
|
||||
return Path.Combine(projectPath, relativeFilePath);
|
||||
}
|
||||
|
||||
private async Task<EnvDTE.Project> GetProjectAsync(string nameOrFileName, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using Microsoft.VisualStudio.Shell.Interop;
|
||||
using Microsoft.VisualStudio.TextManager.Interop;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Microsoft.VisualStudio.Extensibility.Testing;
|
||||
|
||||
internal partial class SolutionExplorerInProcess
|
||||
{
|
||||
public async Task OpenFileAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
||||
var filePath = await GetAbsolutePathForProjectRelativeFilePathAsync(projectName, relativeFilePath, cancellationToken);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException(filePath);
|
||||
}
|
||||
|
||||
VsShellUtilities.OpenDocument(ServiceProvider.GlobalProvider, filePath, VSConstants.LOGVIEWID.Code_guid, out _, out _, out _, out var view);
|
||||
|
||||
// Reliably set focus using NavigateToLineAndColumn
|
||||
var textManager = await GetRequiredGlobalServiceAsync<SVsTextManager, IVsTextManager>(cancellationToken);
|
||||
ErrorHandler.ThrowOnFailure(view.GetBuffer(out var textLines));
|
||||
ErrorHandler.ThrowOnFailure(view.GetCaretPos(out var line, out var column));
|
||||
ErrorHandler.ThrowOnFailure(textManager.NavigateToLineAndColumn(textLines, VSConstants.LOGVIEWID.Code_guid, line, column, line, column));
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string projectName, string fileName, string contents, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
||||
var project = await GetProjectAsync(projectName, cancellationToken);
|
||||
var projectDirectory = Path.GetDirectoryName(project.FullName);
|
||||
var filePath = Path.Combine(projectDirectory, fileName);
|
||||
var directoryPath = Path.GetDirectoryName(filePath);
|
||||
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
File.WriteAllText(filePath, contents);
|
||||
|
||||
_ = project.ProjectItems.AddFromFile(filePath);
|
||||
}
|
||||
|
||||
public async Task RenameFileAsync(string projectName, string oldFileName, string newFileName, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
var projectItem = await GetProjectItemAsync(projectName, oldFileName, cancellationToken);
|
||||
|
||||
projectItem.Name = newFileName;
|
||||
}
|
||||
|
||||
private async Task<string> GetAbsolutePathForProjectRelativeFilePathAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
||||
var dte = await GetRequiredGlobalServiceAsync<SDTE, EnvDTE.DTE>(cancellationToken);
|
||||
var solution = dte.Solution;
|
||||
|
||||
var project = solution.Projects.Cast<EnvDTE.Project>().First(x => x.Name == projectName);
|
||||
var projectPath = Path.GetDirectoryName(project.FullName);
|
||||
return Path.Combine(projectPath, relativeFilePath);
|
||||
}
|
||||
|
||||
private async Task<EnvDTE.ProjectItem> GetProjectItemAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken)
|
||||
{
|
||||
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
|
||||
var solution = (await GetRequiredGlobalServiceAsync<SDTE, EnvDTE.DTE>(cancellationToken)).Solution;
|
||||
var projects = solution.Projects.Cast<EnvDTE.Project>();
|
||||
var project = projects.FirstOrDefault(x => x.Name == projectName);
|
||||
var projectPath = Path.GetDirectoryName(project.FullName);
|
||||
var fullFilePath = Path.Combine(projectPath, relativeFilePath);
|
||||
var projectItems = project.ProjectItems.Cast<EnvDTE.ProjectItem>();
|
||||
var document = projectItems.FirstOrDefault(d => d.get_FileNames(1).Equals(fullFilePath));
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче