Skip to content

Commit

Permalink
Implement code action providers, resolvers, and ExtractToCodeBehind
Browse files Browse the repository at this point in the history
  • Loading branch information
noahbkim committed Jul 6, 2020
1 parent 9e92a99 commit 368d418
Show file tree
Hide file tree
Showing 26 changed files with 1,623 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,14 @@ public static class LanguageServerConstants
public const string RazorMapToDocumentRangesEndpoint = "razor/mapToDocumentRanges";

public const string SemanticTokensProviderName = "semanticTokensProvider";

public const string RazorCodeActionRunnerCommand = "razor/runCodeAction";

public const string RazorCodeActionResolutionEndpoint = "razor/resolveCodeAction";

public static class CodeActions
{
public const string ExtractToCodeBehindAction = "ExtractToCodeBehind";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 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.CodeActions
{
internal 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 (loggerFactory is null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}

_providers = providers ?? throw new ArgumentNullException(nameof(providers));
_foregroundDispatcher = foregroundDispatcher ?? throw new ArgumentNullException(nameof(foregroundDispatcher));
_documentResolver = documentResolver ?? throw new ArgumentNullException(nameof(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).ConfigureAwait(false);

if (document is null)
{
return null;
}

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

var sourceText = await document.GetTextAsync().ConfigureAwait(false);
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).ConfigureAwait(false);
var container = new List<CommandOrCodeAction>();
foreach (var result in results)
{
if (result != null)
{
foreach (var commandOrCodeAction in result)
{
container.Add(commandOrCodeAction);
}
}
}

return new CommandOrCodeActionContainer(container);
}

public void SetCapability(CodeActionCapability capability)
{
_capability = capability;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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 System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

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

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

_logger = loggerFactory.CreateLogger<CodeActionResolutionEndpoint>();

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

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

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.TryGetValue(request.Action, out var resolver))
{
Debug.Fail($"No resolver registered for {request.Action}.");
return new RazorCodeActionResolutionResponse();
}

var edit = await resolver.ResolveAsync(request.Data, cancellationToken).ConfigureAwait(false);
return new RazorCodeActionResolutionResponse() { Edit = edit };
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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 System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
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.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
{
internal class ExtractToCodeBehindCodeActionProvider : RazorCodeActionProvider
{
private static readonly Task<CommandOrCodeActionContainer> EmptyResult = Task.FromResult<CommandOrCodeActionContainer>(null);

override public Task<CommandOrCodeActionContainer> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
{
if (context is null)
{
return EmptyResult;
}

if (!FileKinds.IsComponent(context.Document.GetFileKind()))
{
return EmptyResult;
}

var change = new SourceChange(context.Location.AbsoluteIndex, length: 0, newText: string.Empty);
var syntaxTree = context.Document.GetSyntaxTree();
if (syntaxTree?.Root is null)
{
return EmptyResult;
}

var owner = syntaxTree.Root.LocateOwner(change);
var node = owner.Ancestors().FirstOrDefault(n => n.Kind == SyntaxKind.RazorDirective);
if (node == null || !(node is RazorDirectiveSyntax directiveNode))
{
return EmptyResult;
}

// Make sure we've found a @code or @functions
if (directiveNode.DirectiveDescriptor != ComponentCodeDirective.Directive && directiveNode.DirectiveDescriptor != FunctionsDirective.Directive)
{
return EmptyResult;
}

// No code action if malformed
if (directiveNode.GetDiagnostics().Any(d => d.Severity == RazorDiagnosticSeverity.Error))
{
return EmptyResult;
}

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

if (HasUnsupportedChildren(cSharpCodeBlockNode))
{
return EmptyResult;
}

// Do not provide code action if the cursor is inside the code block
if (context.Location.AbsoluteIndex > cSharpCodeBlockNode.SpanStart)
{
return EmptyResult;
}

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 = LanguageServerConstants.CodeActions.ExtractToCodeBehindAction,
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 = LanguageServerConstants.RazorCodeActionRunnerCommand,
Arguments = arguments,
}
};

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

private static bool HasUnsupportedChildren(Language.Syntax.SyntaxNode node)
{
return node.DescendantNodes().Any(n => n is MarkupBlockSyntax || n is CSharpTransitionSyntax || n is RazorCommentBlockSyntax);
}
}
}
Loading

0 comments on commit 368d418

Please sign in to comment.