Merge pull request #15143 from dotnet/merges/main-to-release/dev17.7

Merge main to release/dev17.7
This commit is contained in:
Vlad Zarytovskii 2023-04-25 21:08:23 +02:00 коммит произвёл GitHub
Родитель 563d6c6937 39e4ebdbf0
Коммит 1bbbdb332e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 329 добавлений и 152 удалений

Просмотреть файл

@ -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;
}
}