Skip to content

Razor snippets#58857

Merged
dibarbet merged 12 commits into
dotnet:mainfrom
dibarbet:razor_snippets
Feb 2, 2022
Merged

Razor snippets#58857
dibarbet merged 12 commits into
dotnet:mainfrom
dibarbet:razor_snippets

Conversation

@dibarbet
Copy link
Copy Markdown
Member

@dibarbet dibarbet commented Jan 14, 2022

Related to dotnet/razor#6012

Implements support for built in snippets for Razor LSP using inline completions. This reads the XML snippet information, applies function results and then converts into an LSP formatted snippet.

While this should work for many XML snippets including custom ones, there are edge cases that I haven't tackled yet (no selection support, no add imports/references support). As such I've restricted this to built in snippets . Support for the remaining will require further iteration on both the lsp client portion and server world.

I will additionally have a followup PR to cache the snippets as we parse from XML.
razor_snippets

@dibarbet dibarbet marked this pull request as ready for review January 26, 2022 04:55
@dibarbet dibarbet requested review from a team as code owners January 26, 2022 04:55
@dibarbet dibarbet added the LSP issues related to the roslyn language server protocol implementation label Jan 26, 2022
namespace Microsoft.CodeAnalysis.CSharp;

[ExportLanguageService(typeof(SnippetFunctionService), LanguageNames.CSharp), Shared]
internal class CSharpSnippetFunctionService : SnippetFunctionService
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis;
internal abstract class SnippetFunctionService : ILanguageService
Copy link
Copy Markdown
Member Author

@dibarbet dibarbet Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class was extracted from https://github.com/dotnet/roslyn/tree/main/src/VisualStudio/Core/Def/Implementation/Snippets/SnippetFunctions and converted into a service to be shared by lsp

namespace Microsoft.CodeAnalysis;
internal class SnippetUtilities
{
public static bool TryGetWordOnLeft(int position, SourceText currentText, ISyntaxFactsService syntaxFactsService, [NotNullWhen(true)] out TextSpan? wordSpan)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracted from

var endPosition = endPositionInSubjectBuffer.Value.Position;


namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlineCompletions;

internal partial class InlineCompletionsHandler
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code here was extracted from similar editor side code that reads snippet xml files and modified a bit to support our use case.

If we need to support XML based snippets long term, it would be nice if an API existed on the editor side to retrieve these. We would have to still do some conversion since some snippets are context dependent, but reading and parsing the XML could be left to the editor.

That being said, semantic snippets will probably make this irrelevant for built in c# snippets, and the editor is re-thinking the snippet experience so I don't believe it's worth doing anything right now.

}
}

public ParsedXmlSnippet? Parse()
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much of the parsing logic remains the same as the battle-tested editor code, the major change is to convert the snippet into 'SnippetParts' instead of just a pure string with default values.

// Generate switch cases requires a multi-step snippet interaction, where the snippet is inserted first then the
// client calls back to on commit so that we can generate the cases for the specified switch value.
// This is not yet supported via LSP.
return this;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't realize this is a two step process at the time of writing the LSP api :(
Future work here is to figure out a way for LSP to call back on the 'commit' of a tab stop and ask the server for more edits.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, is this also handled in the new semantic snippets?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semantic snippets would have to support it in some manner - the cases can only be known when the field is committed

/// </summary>
[ExportRoslynLanguagesLspRequestHandlerProvider, Shared]
[ProvidesMethod(VSInternalMethods.TextDocumentInlineCompletionName)]
internal partial class InlineCompletionsHandler : AbstractStatelessRequestHandler<VSInternalInlineCompletionRequest, VSInternalInlineCompletionList>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the meat and potatoes of the actual new logic (that wasn't copied from the editor)

var expansion = new ExpansionTemplate(matchingSnippet);

// Parse the snippet XML
var parsedSnippet = expansion.Parse();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the parsed snippet needs to be cached. Am working on that, but will do as a separate PR to reduce the size here

Note that we cannot cache the actual LSP snippet, as it is context dependent (class names / formatting), but we can cache the snippet parts we read from XML.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also may want to build specific error handling around this given it's reading user content. Also might make sense to do the same with RetrieveSnippetFromXml above given that reads content from disk

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm OK with the amount of error handling in there right now. currently we do some basic checks ot make sure the file exists. But otherwise if there are malformed snippets, imho it is ok to let those exceptions bubble up to the queue and let them be automatically reported and logged.

Happy to hear other thoughts though.

Copy link
Copy Markdown
Member Author

@dibarbet dibarbet Jan 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did find a few cases I should probably handle actually, so will update updated


namespace Microsoft.VisualStudio.LanguageServices.CSharp.Snippets.SnippetFunctions
{
internal sealed class SnippetFunctionClassName : AbstractSnippetFunctionClassName
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the logic here can now live in a single impl for each language since the language specific logic is now in the snippetfunctionservice (and children)

/// C:\Program Files\Microsoft Visual Studio\2022\VS_INSTANCE\VC#\Snippets\1033\Visual C#
/// These are currently the only snippets supported.
/// </summary>
public static ImmutableHashSet<string> BuiltInSnippets = ImmutableHashSet.Create(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've limited these to built in snippets for now. While the parsing could probably handle most custom snippets, there are a few things that would not work, namely snippets with additional references (requires lsp response to support additional text edits) and selection (currently snippets are only invocable via tab-tab, so selection does not make sense in that context).

Depending on the direction that the editor takes xml snippets and where we go with semantic snippets, we can potentially open this up to more with a few improvements on the lsp api.

Copy link
Copy Markdown

@NTaylorMullen NTaylorMullen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super exciting and great work! I imagine the path you're taking here would also be used by our friends over on the XAML language service side too given it could potentially enable custom snippets too. Quite flexible.

I had some overarching concerns around the snippet parsing tech but reaching out offline about that.

// Generate switch cases requires a multi-step snippet interaction, where the snippet is inserted first then the
// client calls back to on commit so that we can generate the cases for the specified switch value.
// This is not yet supported via LSP.
return this;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, is this also handled in the new semantic snippets?


namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlineCompletions;

internal partial class InlineCompletionsHandler
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you typically utilize partial classes heavily in Roslyn? They typically fall over in some source linked debug scenarios + make understanding implementation details a bit more tedious. Would it be awful to pull all the XML snippet tech into a service that the primary InlineCompletionsHandler utilizes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partial classes are not uncommon in roslyn. It sounds like source link debug scenarios need to be more resilient ;)

I don't mind either way really

var expansion = new ExpansionTemplate(matchingSnippet);

// Parse the snippet XML
var parsedSnippet = expansion.Parse();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also may want to build specific error handling around this given it's reading user content. Also might make sense to do the same with RetrieveSnippetFromXml above given that reads content from disk

@mgoertz-msft
Copy link
Copy Markdown
Contributor

This is super exciting and great work! I imagine the path you're taking here would also be used by our friends over on the XAML language service side too given it could potentially enable custom snippets too. Quite flexible.

Totally agree! I haven't fully wrapped my mind around how to utilize this for custom snippets in XAML but I'm excited to see that the foundation has been laid.

@NTaylorMullen
Copy link
Copy Markdown

/cc @jimmylewis @ToddGrun if you're curious on snippets 😄

Copy link
Copy Markdown

@NTaylorMullen NTaylorMullen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit: It's happpppennnningggg!@!!!

@dibarbet dibarbet merged commit 3f5adda into dotnet:main Feb 2, 2022
@dibarbet dibarbet deleted the razor_snippets branch February 2, 2022 20:39
@ghost ghost added this to the Next milestone Feb 2, 2022
@RikkiGibson RikkiGibson modified the milestones: Next, 17.2.P1 Feb 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-IDE LSP issues related to the roslyn language server protocol implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants