Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace FSharp.Editor.IntegrationTests;

public class CodeActionTests : AbstractIntegrationTest
{
[IdeFact]
public async Task UnusedOpenDeclarations()
{
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;

var code = """
module Library

open System

let x = 42
""";

await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
await Editor.SetTextAsync(code, TestToken);
await Editor.PlaceCaretAsync("open System", TestToken);

await Workspace.WaitForProjectSystemAsync(TestToken);
var codeActions = await Editor.InvokeCodeActionListAsync(TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);

Assert.Single(codeActions);
var actionSet = codeActions.Single();
Assert.Equal("CodeFix", actionSet.CategoryName);

Assert.Single(actionSet.Actions);
var codeFix = actionSet.Actions.Single();
Assert.Equal("Remove unused open declarations", codeFix.DisplayText);
}

[IdeFact]
public async Task AddMissingFunKeyword()
{
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;

var code = """
module Library

let original = []
let transformed = original |> List.map (x -> x)
""";

await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
await Editor.SetTextAsync(code, TestToken);
await Editor.PlaceCaretAsync("->", TestToken);

await Workspace.WaitForProjectSystemAsync(TestToken);
var codeActions = await Editor.InvokeCodeActionListAsync(TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);

Assert.Single(codeActions);
var actionSet = codeActions.Single();
Assert.Equal("ErrorFix", actionSet.CategoryName);

Assert.Single(actionSet.Actions);
var errorFix = actionSet.Actions.Single();
Assert.Equal("Add missing 'fun' keyword", errorFix.DisplayText);
}

[IdeFact]
public async Task AddNewKeywordToDisposables()
{
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;

var code = """
module Library

let sr = System.IO.StreamReader("")
""";

await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
await Editor.SetTextAsync(code, TestToken);
await Editor.PlaceCaretAsync("let sr", TestToken);

await Workspace.WaitForProjectSystemAsync(TestToken);
var codeActions = await Editor.InvokeCodeActionListAsync(TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);

Assert.Single(codeActions);
var actionSet = codeActions.Single();
Assert.Equal("CodeFix", actionSet.CategoryName);

Assert.Single(actionSet.Actions);
var codeFix = actionSet.Actions.Single();
Assert.Equal("Add 'new' keyword", codeFix.DisplayText);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FSharp.Editor.IntegrationTests.Helpers
{
// I stole this voodoo from Razor and removed the obscurest bits
internal static class LightBulbHelper
{
public static async Task<IEnumerable<SuggestedActionSet>> WaitForItemsAsync(
ILightBulbBroker broker,
IWpfTextView view,
CancellationToken cancellationToken)
{
var activeSession = broker.GetSession(view);
var asyncSession = (IAsyncLightBulbSession)activeSession;
var tcs = new TaskCompletionSource<IEnumerable<SuggestedActionSet>>();

void Handler(object s, SuggestedActionsUpdatedArgs e)
{
// ignore these. we care about when the lightbulb items are all completed.
if (e.Status == QuerySuggestedActionCompletionStatus.InProgress)
{
return;
}

if (e.Status == QuerySuggestedActionCompletionStatus.Completed ||
e.Status == QuerySuggestedActionCompletionStatus.CompletedWithoutData)
{
tcs.SetResult(e.ActionSets);
}
else
{
tcs.SetException(new InvalidOperationException($"Light bulb transitioned to non-complete state: {e.Status}"));
}

asyncSession.SuggestedActionsUpdated -= Handler;
}

asyncSession.SuggestedActionsUpdated += Handler;

asyncSession.Dismissed += (_, _) => tcs.TrySetCanceled(new CancellationToken(true));

if (asyncSession.IsDismissed)
{
tcs.TrySetCanceled(new CancellationToken(true));
}

// Calling PopulateWithDataAsync ensures the underlying session will call SuggestedActionsUpdated at least once
// with the latest data computed. This is needed so that if the lightbulb computation is already complete
// that we hear about the results.
await asyncSession.PopulateWithDataAsync(overrideRequestedActionCategories: null, operationContext: null).ConfigureAwait(false);

return await tcs.Task.WithCancellation(cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FSharp.Editor.IntegrationTests.Extensions;
using FSharp.Editor.IntegrationTests.Helpers;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;

Expand Down Expand Up @@ -68,4 +72,23 @@ public async Task PlaceCaretAsync(string marker, CancellationToken cancellationT

view.Selection.Clear();
}

public async Task<IEnumerable<SuggestedActionSet>> InvokeCodeActionListAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var shell = await GetRequiredGlobalServiceAsync<SVsUIShell, IVsUIShell>(cancellationToken);
var cmdGroup = typeof(VSConstants.VSStd14CmdID).GUID;
var cmdExecOpt = OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;

var cmdID = VSConstants.VSStd14CmdID.ShowQuickFixes;
object? obj = null;
shell.PostExecCommand(cmdGroup, (uint)cmdID, (uint)cmdExecOpt, ref obj);

var view = await GetActiveTextViewAsync(cancellationToken);
var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);

var lightbulbs = await LightBulbHelper.WaitForItemsAsync(broker, view, cancellationToken);
return lightbulbs;
}
}