Skip to content

Commit

Permalink
Implement code action providers, resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
noahbkim committed Jun 22, 2020
1 parent 6dd0162 commit 9fd4ee9
Show file tree
Hide file tree
Showing 28 changed files with 1,632 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ public static class LanguageServerConstants
public const string RazorLanguageQueryEndpoint = "razor/languageQuery";

public const string RazorMapToDocumentRangesEndpoint = "razor/mapToDocumentRanges";

public const string RazorCodeActionResolutionEndpoint = "razor/resolveCodeAction";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using OmniSharp.Extensions.JsonRpc;

namespace Microsoft.AspNetCore.Razor.LanguageServer
{
[Serial, Method(LanguageServerConstants.RazorCodeActionResolutionEndpoint)]
internal interface IRazorCodeActionResolutionHandler : IJsonRpcRequestHandler<RazorCodeActionResolutionParams, RazorCodeActionResolutionResponse>
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using MediatR;
using Newtonsoft.Json.Linq;

namespace Microsoft.AspNetCore.Razor.LanguageServer
{
class RazorCodeActionResolutionParams : IRequest<RazorCodeActionResolutionResponse>
{
public string Action { get; set; }
public JObject Data { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using OmniSharp.Extensions.LanguageServer.Protocol.Models;

namespace Microsoft.AspNetCore.Razor.LanguageServer
{
class RazorCodeActionResolutionResponse
{
public WorkspaceEdit Edit;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.AspNetCore.Razor.LanguageServer.Hover;
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
using Microsoft.AspNetCore.Razor.LanguageServer.Semantic;
using Microsoft.AspNetCore.Razor.LanguageServer.Refactoring;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Completion;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
Expand Down Expand Up @@ -80,6 +81,8 @@ public static Task<ILanguageServer> CreateAsync(Stream input, Stream output, Tra
.WithHandler<RazorSemanticTokensEndpoint>()
.WithHandler<RazorSemanticTokensLegendEndpoint>()
.WithHandler<OnAutoInsertEndpoint>()
.WithHandler<CodeActionEndpoint>()
.WithHandler<CodeActionResolutionEndpoint>()
.WithServices(services =>
{
var filePathNormalizer = new FilePathNormalizer();
Expand Down Expand Up @@ -156,6 +159,10 @@ public static Task<ILanguageServer> CreateAsync(Stream input, Stream output, Tra
services.AddSingleton<RazorSemanticTokensInfoService, DefaultRazorSemanticTokensInfoService>();
services.AddSingleton<RazorHoverInfoService, DefaultRazorHoverInfoService>();
services.AddSingleton<HtmlFactsService, DefaultHtmlFactsService>();

// Refactoring
services.AddSingleton<RazorCodeActionProvider, ExtractToCodeBehindCodeActionProvider>();
services.AddSingleton<RazorCodeActionResolver, ExtractToCodeBehindCodeActionResolver>();
}));

try
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring
{
class CodeActionEndpoint : ICodeActionHandler
{
private readonly IEnumerable<RazorCodeActionProvider> _providers;
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly DocumentResolver _documentResolver;
private readonly ILogger _logger;

private CodeActionCapability _capability;

public CodeActionEndpoint(
IEnumerable<RazorCodeActionProvider> providers,
ForegroundDispatcher foregroundDispatcher,
DocumentResolver documentResolver,
ILoggerFactory loggerFactory)
{
if (providers is null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}

if (foregroundDispatcher is null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}

if (documentResolver is null)
{
throw new ArgumentNullException(nameof(documentResolver));
}

if (loggerFactory is null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}

_providers = providers;
_foregroundDispatcher = foregroundDispatcher;
_documentResolver = documentResolver;
_logger = loggerFactory.CreateLogger<CodeActionEndpoint>();
}

public CodeActionRegistrationOptions GetRegistrationOptions()
{
return new CodeActionRegistrationOptions()
{
DocumentSelector = RazorDefaults.Selector
};
}

public async Task<CommandOrCodeActionContainer> Handle(CodeActionParams request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

var document = await Task.Factory.StartNew(() =>
{
_documentResolver.TryResolveDocument(request.TextDocument.Uri.GetAbsoluteOrUNCPath(), out var documentSnapshot);
return documentSnapshot;
}, cancellationToken, TaskCreationOptions.None, _foregroundDispatcher.ForegroundScheduler);

if (document is null)
{
return null;
}

var codeDocument = await document.GetGeneratedOutputAsync();
if (codeDocument.IsUnsupported())
{
return null;
}

var sourceText = await document.GetTextAsync();
var linePosition = new LinePosition((int)request.Range.Start.Line, (int)request.Range.Start.Character);
var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition);
var location = new SourceLocation(hostDocumentIndex, (int)request.Range.Start.Line, (int)request.Range.Start.Character);

var context = new RazorCodeActionContext(request, codeDocument, location);
var tasks = new List<Task<CommandOrCodeActionContainer>>();

foreach (var provider in _providers)
{
var result = provider.ProvideAsync(context, cancellationToken);
if (result != null)
{
tasks.Add(result);
}
}

var results = await Task.WhenAll(tasks);
var container = new List<CommandOrCodeAction>();
foreach (var result in results)
{
if (result != null)
{
foreach (var commandOrCodeAction in result)
{
container.Add(commandOrCodeAction);
}
}
}

return container;
}

public void SetCapability(CodeActionCapability capability)
{
_capability = capability;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring
{
class CodeActionResolutionEndpoint : IRazorCodeActionResolutionHandler
{
private readonly Dictionary<string, RazorCodeActionResolver> _resolvers;
private readonly ILogger _logger;

public CodeActionResolutionEndpoint(
IEnumerable<RazorCodeActionResolver> resolvers,
ILoggerFactory loggerFactory)
{
if (resolvers is null)
{
throw new ArgumentNullException(nameof(resolvers));
}

if (loggerFactory is null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}

_resolvers = new Dictionary<string, RazorCodeActionResolver>();
foreach (var resolver in resolvers)
{
if (_resolvers.ContainsKey(resolver.Action))
{
Debug.Fail($"duplicate resolver action for {resolver.Action}");
}
_resolvers[resolver.Action] = resolver;
}

_logger = loggerFactory.CreateLogger<CodeActionResolutionEndpoint>();
}

public async Task<RazorCodeActionResolutionResponse> Handle(RazorCodeActionResolutionParams request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

_logger.LogDebug($"resolving action {request.Action} with data {request.Data}");
if (!_resolvers.ContainsKey(request.Action))
{
Debug.Fail($"no resolver registered for {request.Action}");
return new RazorCodeActionResolutionResponse() { Edit = null };
}

var edit = await _resolvers[request.Action].ResolveAsync(request.Data, cancellationToken);
return new RazorCodeActionResolutionResponse() { Edit = edit };
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring
{
class Constants
{
public const string ExtractToCodeBehindAction = "ExtractToCodeBehind";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring
{
class ExtractToCodeBehindCodeActionProvider : RazorCodeActionProvider
{
override public Task<CommandOrCodeActionContainer> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
{
if (context.Document.IsUnsupported())
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

if (!FileKinds.IsComponent(context.Document.GetFileKind()))
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

var change = new SourceChange(context.Location.AbsoluteIndex, length: 0, newText: "");
var node = context.Document.GetSyntaxTree().Root.LocateOwner(change);
if (node is null)
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

while (!(node is RazorDirectiveSyntax))
{
node = node.Parent;
if (node == null)
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}
}

if (!(node is RazorDirectiveSyntax))
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}
var directiveNode = (RazorDirectiveSyntax)node;

if (directiveNode.DirectiveDescriptor != ComponentCodeDirective.Directive && directiveNode.DirectiveDescriptor != FunctionsDirective.Directive)
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

if (node.GetDiagnostics().Any(d => d.Severity == RazorDiagnosticSeverity.Error))
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

var cSharpCodeBlockNode = directiveNode.Body.DescendantNodes().FirstOrDefault(n => n is CSharpCodeBlockSyntax);
if (cSharpCodeBlockNode is null)
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

if (cSharpCodeBlockNode.DescendantNodes().Any(n => n is MarkupBlockSyntax || n is CSharpTransitionSyntax || n is RazorCommentBlockSyntax))
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

if (context.Location.AbsoluteIndex > cSharpCodeBlockNode.SpanStart)
{
return Task.FromResult<CommandOrCodeActionContainer>(null);
}

var actionParams = new ExtractToCodeBehindParams()
{
Uri = context.Request.TextDocument.Uri,
ExtractStart = cSharpCodeBlockNode.Span.Start,
ExtractEnd = cSharpCodeBlockNode.Span.End,
RemoveStart = directiveNode.Span.Start,
RemoveEnd = directiveNode.Span.End
};
var data = JObject.FromObject(actionParams);

var resolutionParams = new RazorCodeActionResolutionParams()
{
Action = Constants.ExtractToCodeBehindAction, // Extract
Data = data,
};
var serializedParams = JToken.FromObject(resolutionParams);
var arguments = new JArray(serializedParams);

var container = new List<CommandOrCodeAction>
{
new Command()
{
Title = "Extract code block into backing document",
Name = "razor/runCodeAction",
Arguments = arguments,
}
};

return Task.FromResult((CommandOrCodeActionContainer)container);
}
}
}
Loading

0 comments on commit 9fd4ee9

Please sign in to comment.