Skip to content

Commit 368d418

Browse files
committed
Implement code action providers, resolvers, and ExtractToCodeBehind
1 parent 9e92a99 commit 368d418

26 files changed

+1623
-0
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,14 @@ public static class LanguageServerConstants
2626
public const string RazorMapToDocumentRangesEndpoint = "razor/mapToDocumentRanges";
2727

2828
public const string SemanticTokensProviderName = "semanticTokensProvider";
29+
30+
public const string RazorCodeActionRunnerCommand = "razor/runCodeAction";
31+
32+
public const string RazorCodeActionResolutionEndpoint = "razor/resolveCodeAction";
33+
34+
public static class CodeActions
35+
{
36+
public const string ExtractToCodeBehindAction = "ExtractToCodeBehind";
37+
}
2938
}
3039
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Razor.Language;
9+
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
10+
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
11+
using Microsoft.CodeAnalysis.Razor;
12+
using Microsoft.CodeAnalysis.Text;
13+
using Microsoft.Extensions.Logging;
14+
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
15+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
16+
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
17+
18+
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
19+
{
20+
internal class CodeActionEndpoint : ICodeActionHandler
21+
{
22+
private readonly IEnumerable<RazorCodeActionProvider> _providers;
23+
private readonly ForegroundDispatcher _foregroundDispatcher;
24+
private readonly DocumentResolver _documentResolver;
25+
private readonly ILogger _logger;
26+
27+
private CodeActionCapability _capability;
28+
29+
public CodeActionEndpoint(
30+
IEnumerable<RazorCodeActionProvider> providers,
31+
ForegroundDispatcher foregroundDispatcher,
32+
DocumentResolver documentResolver,
33+
ILoggerFactory loggerFactory)
34+
{
35+
if (loggerFactory is null)
36+
{
37+
throw new ArgumentNullException(nameof(loggerFactory));
38+
}
39+
40+
_providers = providers ?? throw new ArgumentNullException(nameof(providers));
41+
_foregroundDispatcher = foregroundDispatcher ?? throw new ArgumentNullException(nameof(foregroundDispatcher));
42+
_documentResolver = documentResolver ?? throw new ArgumentNullException(nameof(documentResolver));
43+
_logger = loggerFactory.CreateLogger<CodeActionEndpoint>();
44+
}
45+
46+
public CodeActionRegistrationOptions GetRegistrationOptions()
47+
{
48+
return new CodeActionRegistrationOptions()
49+
{
50+
DocumentSelector = RazorDefaults.Selector
51+
};
52+
}
53+
54+
public async Task<CommandOrCodeActionContainer> Handle(CodeActionParams request, CancellationToken cancellationToken)
55+
{
56+
if (request is null)
57+
{
58+
throw new ArgumentNullException(nameof(request));
59+
}
60+
61+
var document = await Task.Factory.StartNew(() =>
62+
{
63+
_documentResolver.TryResolveDocument(request.TextDocument.Uri.GetAbsoluteOrUNCPath(), out var documentSnapshot);
64+
return documentSnapshot;
65+
}, cancellationToken, TaskCreationOptions.None, _foregroundDispatcher.ForegroundScheduler).ConfigureAwait(false);
66+
67+
if (document is null)
68+
{
69+
return null;
70+
}
71+
72+
var codeDocument = await document.GetGeneratedOutputAsync().ConfigureAwait(false);
73+
if (codeDocument.IsUnsupported())
74+
{
75+
return null;
76+
}
77+
78+
var sourceText = await document.GetTextAsync().ConfigureAwait(false);
79+
var linePosition = new LinePosition((int)request.Range.Start.Line, (int)request.Range.Start.Character);
80+
var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition);
81+
var location = new SourceLocation(hostDocumentIndex, (int)request.Range.Start.Line, (int)request.Range.Start.Character);
82+
83+
var context = new RazorCodeActionContext(request, codeDocument, location);
84+
var tasks = new List<Task<CommandOrCodeActionContainer>>();
85+
86+
foreach (var provider in _providers)
87+
{
88+
var result = provider.ProvideAsync(context, cancellationToken);
89+
if (result != null)
90+
{
91+
tasks.Add(result);
92+
}
93+
}
94+
95+
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
96+
var container = new List<CommandOrCodeAction>();
97+
foreach (var result in results)
98+
{
99+
if (result != null)
100+
{
101+
foreach (var commandOrCodeAction in result)
102+
{
103+
container.Add(commandOrCodeAction);
104+
}
105+
}
106+
}
107+
108+
return new CommandOrCodeActionContainer(container);
109+
}
110+
111+
public void SetCapability(CodeActionCapability capability)
112+
{
113+
_capability = capability;
114+
}
115+
}
116+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
12+
{
13+
internal class CodeActionResolutionEndpoint : IRazorCodeActionResolutionHandler
14+
{
15+
private readonly IReadOnlyDictionary<string, RazorCodeActionResolver> _resolvers;
16+
private readonly ILogger _logger;
17+
18+
public CodeActionResolutionEndpoint(
19+
IEnumerable<RazorCodeActionResolver> resolvers,
20+
ILoggerFactory loggerFactory)
21+
{
22+
if (loggerFactory is null)
23+
{
24+
throw new ArgumentNullException(nameof(loggerFactory));
25+
}
26+
27+
_logger = loggerFactory.CreateLogger<CodeActionResolutionEndpoint>();
28+
29+
if (resolvers is null)
30+
{
31+
throw new ArgumentNullException(nameof(resolvers));
32+
}
33+
34+
var resolverMap = new Dictionary<string, RazorCodeActionResolver>();
35+
foreach (var resolver in resolvers)
36+
{
37+
if (resolverMap.ContainsKey(resolver.Action))
38+
{
39+
Debug.Fail($"Duplicate resolver action for {resolver.Action}.");
40+
}
41+
resolverMap[resolver.Action] = resolver;
42+
}
43+
_resolvers = resolverMap;
44+
}
45+
46+
public async Task<RazorCodeActionResolutionResponse> Handle(RazorCodeActionResolutionParams request, CancellationToken cancellationToken)
47+
{
48+
if (request is null)
49+
{
50+
throw new ArgumentNullException(nameof(request));
51+
}
52+
53+
_logger.LogDebug($"Resolving action {request.Action} with data {request.Data}.");
54+
55+
if (!_resolvers.TryGetValue(request.Action, out var resolver))
56+
{
57+
Debug.Fail($"No resolver registered for {request.Action}.");
58+
return new RazorCodeActionResolutionResponse();
59+
}
60+
61+
var edit = await resolver.ResolveAsync(request.Data, cancellationToken).ConfigureAwait(false);
62+
return new RazorCodeActionResolutionResponse() { Edit = edit };
63+
}
64+
}
65+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.AspNetCore.Razor.Language;
10+
using Microsoft.AspNetCore.Razor.Language.Components;
11+
using Microsoft.AspNetCore.Razor.Language.Extensions;
12+
using Microsoft.AspNetCore.Razor.Language.Syntax;
13+
using Microsoft.AspNetCore.Razor.Language.Legacy;
14+
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
15+
using Newtonsoft.Json.Linq;
16+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
17+
18+
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
19+
{
20+
internal class ExtractToCodeBehindCodeActionProvider : RazorCodeActionProvider
21+
{
22+
private static readonly Task<CommandOrCodeActionContainer> EmptyResult = Task.FromResult<CommandOrCodeActionContainer>(null);
23+
24+
override public Task<CommandOrCodeActionContainer> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
25+
{
26+
if (context is null)
27+
{
28+
return EmptyResult;
29+
}
30+
31+
if (!FileKinds.IsComponent(context.Document.GetFileKind()))
32+
{
33+
return EmptyResult;
34+
}
35+
36+
var change = new SourceChange(context.Location.AbsoluteIndex, length: 0, newText: string.Empty);
37+
var syntaxTree = context.Document.GetSyntaxTree();
38+
if (syntaxTree?.Root is null)
39+
{
40+
return EmptyResult;
41+
}
42+
43+
var owner = syntaxTree.Root.LocateOwner(change);
44+
var node = owner.Ancestors().FirstOrDefault(n => n.Kind == SyntaxKind.RazorDirective);
45+
if (node == null || !(node is RazorDirectiveSyntax directiveNode))
46+
{
47+
return EmptyResult;
48+
}
49+
50+
// Make sure we've found a @code or @functions
51+
if (directiveNode.DirectiveDescriptor != ComponentCodeDirective.Directive && directiveNode.DirectiveDescriptor != FunctionsDirective.Directive)
52+
{
53+
return EmptyResult;
54+
}
55+
56+
// No code action if malformed
57+
if (directiveNode.GetDiagnostics().Any(d => d.Severity == RazorDiagnosticSeverity.Error))
58+
{
59+
return EmptyResult;
60+
}
61+
62+
var cSharpCodeBlockNode = directiveNode.Body.DescendantNodes().FirstOrDefault(n => n is CSharpCodeBlockSyntax);
63+
if (cSharpCodeBlockNode is null)
64+
{
65+
return EmptyResult;
66+
}
67+
68+
if (HasUnsupportedChildren(cSharpCodeBlockNode))
69+
{
70+
return EmptyResult;
71+
}
72+
73+
// Do not provide code action if the cursor is inside the code block
74+
if (context.Location.AbsoluteIndex > cSharpCodeBlockNode.SpanStart)
75+
{
76+
return EmptyResult;
77+
}
78+
79+
var actionParams = new ExtractToCodeBehindParams()
80+
{
81+
Uri = context.Request.TextDocument.Uri,
82+
ExtractStart = cSharpCodeBlockNode.Span.Start,
83+
ExtractEnd = cSharpCodeBlockNode.Span.End,
84+
RemoveStart = directiveNode.Span.Start,
85+
RemoveEnd = directiveNode.Span.End
86+
};
87+
var data = JObject.FromObject(actionParams);
88+
89+
var resolutionParams = new RazorCodeActionResolutionParams()
90+
{
91+
Action = LanguageServerConstants.CodeActions.ExtractToCodeBehindAction,
92+
Data = data,
93+
};
94+
var serializedParams = JToken.FromObject(resolutionParams);
95+
var arguments = new JArray(serializedParams);
96+
97+
var container = new List<CommandOrCodeAction>
98+
{
99+
new Command()
100+
{
101+
Title = "Extract code block into backing document",
102+
Name = LanguageServerConstants.RazorCodeActionRunnerCommand,
103+
Arguments = arguments,
104+
}
105+
};
106+
107+
return Task.FromResult((CommandOrCodeActionContainer)container);
108+
}
109+
110+
private static bool HasUnsupportedChildren(Language.Syntax.SyntaxNode node)
111+
{
112+
return node.DescendantNodes().Any(n => n is MarkupBlockSyntax || n is CSharpTransitionSyntax || n is RazorCommentBlockSyntax);
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)