diff --git a/eng/Versions.props b/eng/Versions.props
index 8690b4e9906..93cfd50bbfe 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -104,6 +104,7 @@
17.5.10-alpha
17.4.0-preview-22469-04
+ $(RoslynVersion)
$(RoslynVersion)
$(RoslynVersion)
$(RoslynVersion)
@@ -163,6 +164,7 @@
0.1.149-beta
$(MicrosoftVisualStudioExtensibilityTestingVersion)
$(MicrosoftVisualStudioExtensibilityTestingVersion)
+ 1.0.7
$(MicrosoftVisualStudioThreadingPackagesVersion)
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj b/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj
index f52bfdf9bad..089f38561b8 100644
--- a/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj
@@ -21,10 +21,15 @@
+
+
+
+
+
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs
index 004953d5797..2eb2490d5cb 100644
--- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs
@@ -4,7 +4,14 @@
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
+using Microsoft.VisualStudio.Editor;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Threading;
+using FSharp.Editor.IntegrationTests;
+using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.Extensibility.Testing;
@@ -28,4 +35,28 @@ public async Task SetTextAsync(string text, CancellationToken cancellationToken)
var replacementSpan = new SnapshotSpan(textSnapshot, 0, textSnapshot.Length);
view.TextBuffer.Replace(replacementSpan, text);
}
+
+ public async Task WaitForEditorOperationsAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var shell = await GetRequiredGlobalServiceAsync(cancellationToken);
+ if (shell.IsPackageLoaded(DefGuidList.guidEditorPkg, out var editorPackage) == VSConstants.S_OK)
+ {
+ var asyncPackage = (AsyncPackage)editorPackage;
+ var collection = asyncPackage.GetPropertyValue("JoinableTaskCollection");
+ await collection.JoinTillEmptyAsync(cancellationToken);
+ }
+ }
+
+ public async Task GetSelectedTextAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
+ var subjectBuffer = ITextViewExtensions.GetBufferContainingCaret(view);
+
+ var selectedSpan = view.Selection.SelectedSpans[0];
+ return subjectBuffer.CurrentSnapshot.GetText(selectedSpan);
+ }
}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/InputInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/InputInProcess.cs
new file mode 100644
index 00000000000..fd11cf30ae3
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/InputInProcess.cs
@@ -0,0 +1,55 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Threading;
+using WindowsInput;
+using WindowsInput.Native;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+[TestService]
+internal partial class InputInProcess
+{
+ internal async Task SendToNavigateToAsync(InputKey[] keys, CancellationToken cancellationToken)
+ {
+ // AbstractSendKeys runs synchronously, so switch to a background thread before the call
+ await TaskScheduler.Default;
+
+ // Take no direct action regarding activation, but assert the correct item already has focus
+ await TestServices.JoinableTaskFactory.RunAsync(async () =>
+ {
+ await TestServices.JoinableTaskFactory.SwitchToMainThreadAsync();
+ });
+
+ var inputSimulator = new InputSimulator();
+ foreach (var key in keys)
+ {
+ // If it is enter key, we need to wait for search item shows up in the search dialog.
+ if (key.VirtualKeyCode == VirtualKeyCode.RETURN)
+ {
+ await WaitNavigationItemShowsUpAsync(cancellationToken);
+ }
+
+ key.Apply(inputSimulator);
+ }
+
+ await TestServices.JoinableTaskFactory.RunAsync(async () =>
+ {
+ await WaitForApplicationIdleAsync(cancellationToken);
+ });
+ }
+
+ private async Task WaitNavigationItemShowsUpAsync(CancellationToken cancellationToken)
+ {
+ // Wait for the NavigateTo Features completes on Roslyn side.
+ await TestServices.Workspace.WaitForAllAsyncOperationsAsync(new[] { "NavigateTo" }, cancellationToken);
+ // Since the all-in-one search experience populates its results asychronously we need
+ // to give it time to update the UI. Note: This is not a perfect solution.
+ await Task.Delay(1000);
+ await WaitForApplicationIdleAsync(cancellationToken);
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/InputKey.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/InputKey.cs
new file mode 100644
index 00000000000..969d85470bc
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/InputKey.cs
@@ -0,0 +1,111 @@
+// 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.Collections.Immutable;
+using WindowsInput;
+using WindowsInput.Native;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+internal readonly struct InputKey
+{
+ public readonly ImmutableArray Modifiers;
+ public readonly VirtualKeyCode VirtualKeyCode;
+ public readonly char? Character;
+ public readonly string? Text;
+
+ public InputKey(VirtualKeyCode virtualKeyCode, ImmutableArray modifiers)
+ {
+ Modifiers = modifiers;
+ VirtualKeyCode = virtualKeyCode;
+ Character = null;
+ Text = null;
+ }
+
+ public InputKey(char character)
+ {
+ Modifiers = ImmutableArray.Empty;
+ VirtualKeyCode = 0;
+ Character = character;
+ Text = null;
+ }
+
+ public InputKey(string text)
+ {
+ Modifiers = ImmutableArray.Empty;
+ VirtualKeyCode = 0;
+ Character = null;
+ Text = text;
+ }
+
+ public static implicit operator InputKey(char character)
+ => new(character);
+
+ public static implicit operator InputKey(string text)
+ => new(text);
+
+ public static implicit operator InputKey(VirtualKeyCode virtualKeyCode)
+ => new(virtualKeyCode, ImmutableArray.Empty);
+
+ public static implicit operator InputKey((VirtualKeyCode virtualKeyCode, VirtualKeyCode modifier) modifiedKey)
+ => new(modifiedKey.virtualKeyCode, ImmutableArray.Create(modifiedKey.modifier));
+
+ public void Apply(IInputSimulator simulator)
+ {
+ if (Character is { } c)
+ {
+ if (c == '\n')
+ simulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
+ else if (c == '\t')
+ simulator.Keyboard.KeyPress(VirtualKeyCode.TAB);
+ else
+ simulator.Keyboard.TextEntry(c);
+
+ return;
+ }
+ else if (Text is not null)
+ {
+ var offset = 0;
+ while (offset < Text.Length)
+ {
+ if (Text[offset] == '\r' && offset < Text.Length - 1 && Text[offset + 1] == '\n')
+ {
+ // Treat \r\n as a single RETURN character
+ offset++;
+ continue;
+ }
+ else if (Text[offset] == '\n')
+ {
+ simulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
+ offset++;
+ continue;
+ }
+ else if (Text[offset] == '\t')
+ {
+ simulator.Keyboard.KeyPress(VirtualKeyCode.TAB);
+ offset++;
+ continue;
+ }
+ else
+ {
+ var nextSpecial = Text.IndexOfAny(new[] { '\r', '\n', '\t' }, offset);
+ var endOfCurrentSegment = nextSpecial < 0 ? Text.Length : nextSpecial;
+ simulator.Keyboard.TextEntry(Text.Substring(offset, endOfCurrentSegment - offset));
+ offset = endOfCurrentSegment;
+ }
+ }
+
+ return;
+ }
+
+ if (Modifiers.IsEmpty)
+ {
+ simulator.Keyboard.KeyPress(VirtualKeyCode);
+ }
+ else
+ {
+ simulator.Keyboard.ModifiedKeyStroke(Modifiers, VirtualKeyCode);
+ }
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess.cs
new file mode 100644
index 00000000000..ff1cfe87ab8
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ShellInProcess.cs
@@ -0,0 +1,85 @@
+// 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.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Input;
+using FSharp.Editor.IntegrationTests;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Threading;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+internal partial class ShellInProcess
+{
+ public async Task ShowNavigateToDialogAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd12CmdID.NavigateTo, cancellationToken);
+
+ await WaitForNavigateToFocusAsync(cancellationToken);
+
+ async Task WaitForNavigateToFocusAsync(CancellationToken cancellationToken)
+ {
+ bool isSearchActive = false;
+
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Take no direct action regarding activation, but assert the correct item already has focus
+ await TestServices.JoinableTaskFactory.RunAsync(async () =>
+ {
+ await TestServices.JoinableTaskFactory.SwitchToMainThreadAsync();
+ var searchBox = Assert.IsAssignableFrom(Keyboard.FocusedElement);
+ if ("PART_SearchBox" == searchBox.Name || "SearchBoxControl" == searchBox.Name)
+ {
+ isSearchActive = true;
+ }
+ });
+
+ if (isSearchActive)
+ {
+ return;
+ }
+
+ // If the dialog has not been displayed, then wait some time for it to show. The
+ // cancellation token passed in should be hang mitigating to avoid possible
+ // infinite loop.
+ await Task.Delay(100);
+ }
+ }
+ }
+
+ // This is based on WaitForQuiescenceAsync in the FileChangeService tests
+ public async Task WaitForFileChangeNotificationsAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var fileChangeService = await GetRequiredGlobalServiceAsync(cancellationToken);
+ Assumes.Present(fileChangeService);
+
+ var jobSynchronizer = fileChangeService.GetPropertyValue("JobSynchronizer");
+ Assumes.Present(jobSynchronizer);
+
+ var type = jobSynchronizer.GetType();
+ var methodInfo = type.GetMethod("GetActiveSpawnedTasks", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ Assumes.Present(methodInfo);
+
+ while (true)
+ {
+ var tasks = (Task[])methodInfo.Invoke(jobSynchronizer, null);
+ if (!tasks.Any())
+ return;
+
+ await Task.WhenAll(tasks);
+ }
+ }
+
+}
\ 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 40835e07185..3e4e5d30191 100644
--- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
@@ -66,6 +66,34 @@ public async Task AddProjectAsync(string projectName, string projectTemplate, Ca
ErrorHandler.ThrowOnFailure(solution.AddNewProjectFromTemplate(projectTemplatePath, null, null, projectPath, projectName, null, out _));
}
+ 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 OpenFileAsync(string projectName, string relativeFilePath, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var filePath = await GetAbsolutePathForProjectRelativeFilePathAsync(projectName, relativeFilePath, cancellationToken);
+ 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 GetProjectTemplatePathAsync(string projectTemplate, CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
@@ -76,6 +104,19 @@ private async Task GetProjectTemplatePathAsync(string projectTemplate, C
return solution.GetProjectTemplate(projectTemplate, "FSharp");
}
+ 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);
+ }
+
public async Task RestoreNuGetPackagesAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/WorkaroundsInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/WorkaroundsInProcess.cs
new file mode 100644
index 00000000000..7bda06a5a3b
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/WorkaroundsInProcess.cs
@@ -0,0 +1,25 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Shared.TestHooks;
+using Microsoft.VisualStudio.Extensibility.Testing;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+[TestService]
+internal partial class WorkaroundsInProcess
+{
+ public async Task WaitForNavigationAsync(CancellationToken cancellationToken)
+ {
+ await TestServices.Workspace.WaitForAllAsyncOperationsAsync(new[] { "Workspace", "NavigateTo" }, cancellationToken);
+ await TestServices.Editor.WaitForEditorOperationsAsync(cancellationToken);
+
+ // It's not clear why this delay is necessary. Navigation operations are expected to fully complete as part
+ // of one of the above waiters, but GetActiveWindowCaptionAsync appears to return the previous window
+ // caption for a short delay after the above complete.
+ await Task.Delay(2000);
+ }
+}
\ No newline at end of file
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/WorkspaceInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/WorkspaceInProcess.cs
new file mode 100644
index 00000000000..d9d35dc32e5
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/WorkspaceInProcess.cs
@@ -0,0 +1,49 @@
+// 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.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Build.Execution;
+using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.NavigateTo;
+using Microsoft.CodeAnalysis.Shared.TestHooks;
+using Microsoft.VisualStudio.LanguageServices;
+using Microsoft.VisualStudio.Threading;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+internal partial class WorkspaceInProcess
+{
+ public async Task WaitForAllAsyncOperationsAsync(string[] featureNames, CancellationToken cancellationToken)
+ {
+ if (featureNames.Contains("Workspace"))
+ {
+ await WaitForProjectSystemAsync(cancellationToken);
+ await TestServices.Shell.WaitForFileChangeNotificationsAsync(cancellationToken);
+ await TestServices.Editor.WaitForEditorOperationsAsync(cancellationToken);
+ }
+
+ var listenerProvider = await GetComponentModelServiceAsync(cancellationToken);
+ var workspace = await GetComponentModelServiceAsync(cancellationToken);
+
+ if (featureNames.Contains("NavigateTo"))
+ {
+ var statusService = workspace.Services.GetRequiredService();
+
+ // Make sure the "priming" operation has started for Nav To
+ var threadingContext = await GetComponentModelServiceAsync(cancellationToken);
+ var asyncListener = listenerProvider.GetListener(FeatureAttribute.NavigateTo);
+ var searchHost = new DefaultNavigateToSearchHost(workspace.CurrentSolution, asyncListener, threadingContext.DisposalToken);
+
+ // Calling DefaultNavigateToSearchHost.IsFullyLoadedAsync starts the fire-and-forget asynchronous
+ // operation to populate the remote host. The call to WaitAllAsync below will wait for that operation to
+ // complete.
+ await searchHost.IsFullyLoadedAsync(cancellationToken);
+ }
+
+ await listenerProvider.WaitAllAsync(workspace, featureNames).WithCancellation(cancellationToken);
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/NavigationTests.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/NavigationTests.cs
new file mode 100644
index 00000000000..e144bc5270a
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/NavigationTests.cs
@@ -0,0 +1,55 @@
+// 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 Microsoft.CodeAnalysis.Testing;
+using Microsoft.VisualStudio.Extensibility.Testing;
+using System.Threading.Tasks;
+using WindowsInput.Native;
+using Xunit;
+
+namespace FSharp.Editor.IntegrationTests;
+
+public class NavigationTests : AbstractIntegrationTest
+{
+ [IdeFact]
+ public async Task Navigate_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var shell = TestServices.Shell;
+ var editor = TestServices.Editor;
+ var input = TestServices.Input;
+ var workarounds = TestServices.Workarounds;
+ var projectName = "Library";
+ var code1 = """
+namespace Library
+
+module Math1 =
+ let add x y = x + y
+
+""";
+ var code2 = """
+namespace Library
+
+module Math2 =
+ let subtract x y = x - y
+
+""";
+
+ await solutionExplorer.CreateSolutionAsync(nameof(CreateProjectTests), token);
+ await solutionExplorer.AddProjectAsync(projectName, template, token);
+ await solutionExplorer.RestoreNuGetPackagesAsync(token);
+ await solutionExplorer.AddFileAsync(projectName, "Math1.fs", code1, token);
+ await solutionExplorer.AddFileAsync(projectName, "Math2.fs", code2, token);
+ await solutionExplorer.OpenFileAsync(projectName, "Math1.fs", token);
+
+ await shell.ShowNavigateToDialogAsync(token);
+ await input.SendToNavigateToAsync(new InputKey[] { "subtract", VirtualKeyCode.RETURN }, token);
+
+ await workarounds.WaitForNavigationAsync(token);
+ Assert.Equal("Math2.fs", await shell.GetActiveWindowCaptionAsync(token));
+ Assert.Equal("subtract", await editor.GetSelectedTextAsync(token));
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/ObjectExtensions.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/ObjectExtensions.cs
new file mode 100644
index 00000000000..1e3cc6c5908
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/ObjectExtensions.cs
@@ -0,0 +1,60 @@
+// 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.
+
+#nullable disable
+
+using System;
+using System.Reflection;
+
+namespace FSharp.Editor.IntegrationTests;
+
+public static class ObjectExtensions
+{
+ public static PropertyType GetPropertyValue(this object instance, string propertyName)
+ {
+ return (PropertyType)instance.GetPropertyValue(propertyName);
+ }
+
+ public static object GetPropertyValue(this object instance, string propertyName)
+ {
+ var type = instance.GetType();
+ var propertyInfo = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ if (propertyInfo == null)
+ {
+ throw new ArgumentException("Property " + propertyName + " was not found on type " + type.ToString());
+ }
+
+ var result = propertyInfo.GetValue(instance, null);
+ return result;
+ }
+
+ public static object GetFieldValue(this object instance, string fieldName)
+ {
+ var type = instance.GetType();
+ FieldInfo fieldInfo = null;
+ while (type != null)
+ {
+ fieldInfo = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ if (fieldInfo != null)
+ {
+ break;
+ }
+
+ type = type.BaseType;
+ }
+
+ if (fieldInfo == null)
+ {
+ throw new FieldAccessException("Field " + fieldName + " was not found on type " + type.ToString());
+ }
+
+ var result = fieldInfo.GetValue(instance);
+ return result; // you can place a breakpoint here (for debugging purposes)
+ }
+
+ public static FieldType GetFieldValue(this object instance, string fieldName)
+ {
+ return (FieldType)instance.GetFieldValue(fieldName);
+ }
+}