diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e29238876e9..9e8a716978b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -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: diff --git a/eng/Versions.props b/eng/Versions.props index 8cb0ebaf00a..9a57897d2ad 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -160,7 +160,7 @@ $(VisualStudioEditorPackagesVersion) $(VisualStudioEditorPackagesVersion) 5.6.0 - 0.1.149-beta + 0.1.169-beta $(MicrosoftVisualStudioExtensibilityTestingVersion) $(MicrosoftVisualStudioExtensibilityTestingVersion) diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/GoToDefinitionTests.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/GoToDefinitionTests.cs index debbdf12029..66bb33eccd9 100644 --- a/vsintegration/tests/FSharp.Editor.IntegrationTests/GoToDefinitionTests.cs +++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/GoToDefinitionTests.cs @@ -35,4 +35,60 @@ module Test 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); + } } \ No newline at end of file diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs index 4597c3bffb7..e1836606f58 100644 --- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs +++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs @@ -35,25 +35,6 @@ public async Task CreateSolutionAsync(string solutionName, CancellationToken can 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(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 @@ private string CreateTemporaryPath() return Path.Combine(Path.GetTempPath(), "fsharp-test", Path.GetRandomFileName()); } - private async Task GetAbsolutePathForProjectRelativeFilePathAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken) - { - await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - var dte = await GetRequiredGlobalServiceAsync(cancellationToken); - var solution = dte.Solution; - Assumes.Present(solution); - - var project = solution.Projects.Cast().First(x => x.Name == projectName); - var projectPath = Path.GetDirectoryName(project.FullName); - return Path.Combine(projectPath, relativeFilePath); - } - private async Task GetProjectAsync(string nameOrFileName, CancellationToken cancellationToken) { await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs new file mode 100644 index 00000000000..f639decdb82 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess_Files.cs @@ -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(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 GetAbsolutePathForProjectRelativeFilePathAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + var dte = await GetRequiredGlobalServiceAsync(cancellationToken); + var solution = dte.Solution; + + var project = solution.Projects.Cast().First(x => x.Name == projectName); + var projectPath = Path.GetDirectoryName(project.FullName); + return Path.Combine(projectPath, relativeFilePath); + } + + private async Task GetProjectItemAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + var solution = (await GetRequiredGlobalServiceAsync(cancellationToken)).Solution; + var projects = solution.Projects.Cast(); + 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(); + var document = projectItems.FirstOrDefault(d => d.get_FileNames(1).Equals(fullFilePath)); + + return document; + } +}