Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Semantic snippets - for loop snippet #62990

Closed
wants to merge 25 commits into from

Conversation

akhera99
Copy link
Member

The next snippet in the long list of snippets to implement :)

This also checks for the option set for var for built in types in the editorconfig.

@akhera99 akhera99 marked this pull request as ready for review July 27, 2022 21:35
@akhera99 akhera99 requested a review from a team as a code owner July 27, 2022 21:35

// Creating the variable declaration based on if the user has
// 'var for built in types' set in their editorconfig.
var variableDeclarationSyntax = varBuiltInType
Copy link
Contributor

Choose a reason for hiding this comment

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

Could the conditional be used to create the varIdentifier? Seems like everything other than the first parameter to SyntaxFactory.VariableDeclaration here is identical.

indexVariable,
argumentList: null,
SyntaxFactory.EqualsValueClause((ExpressionSyntax)generator.LiteralExpression(0)))))
: SyntaxFactory.VariableDeclaration(compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(allowVar: false),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious, what does allowVar do here? Can it just be set to varBuildInType and does the syntax generator do magic for you then?

Copy link
Member

Choose a reason for hiding this comment

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

@davidwengier To my knowledge, it adds DoNotAllowVarAnnotation.Annotation. This annotation is used by CSharpUseImplicitTypeHelper.

if (!allowVar)
syntax = syntax.WithAdditionalAnnotations(DoNotAllowVarAnnotation.Annotation);

if (typeName.HasAnnotation(DoNotAllowVarAnnotation.Annotation))
{
return default;
}

One important use of CSharpUseImplicitTypeHelper is CSharpVarReducer, which is run after every CodeAction is applied.

I think no code actions are involved here, so most likely this annotation will do nothing?

I'm curious though why we will ever want to put a DoNotAllowVarAnnotation.Annotation

Copy link
Member

Choose a reason for hiding this comment

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

@davidwengier Note that if this was a CodeAction (e.g, in a CodeFixProvider or CodeRefactoringProvider), we can always generate a non-var TypeSyntax and add Simplifier.Annotation, and it will magically get converted to var if user prefers it.

See dotnet/roslyn-analyzers#6023 :)

Copy link
Member

@Youssef1313 Youssef1313 Jul 28, 2022

Choose a reason for hiding this comment

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

Ooh snippets are calling CleanupDocumentAsync:

var reformattedDocument = await CleanupDocumentAsync(formatAnnotatedSnippetDocument, cancellationToken).ConfigureAwait(false);

So things I was saying specific to CodeActions might be applicable to snippets as well.

@akhera99 Can you try to unconditionally produce compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(allowVar: true).WithAdditionalAnnotations(Simplifier.Annotation)?

The CleanupDocumentAsync call should be able to convert to var if user prefers that.

Copy link
Member Author

Choose a reason for hiding this comment

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

@Youssef1313 yep you were correct, thanks :)

var x = 5;
void LocalMethod()
{
for (global::System.Int32 i = 0; (i) < (length); i++)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does it look so different when in a local function?

Copy link
Member

Choose a reason for hiding this comment

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

@davidwengier Known bug #58135 😕

using Microsoft.CodeAnalysis.Snippets.SnippetProviders;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using static Humanizer.In;
Copy link
Member

Choose a reason for hiding this comment

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

Is this used?

Copy link
Member Author

Choose a reason for hiding this comment

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

nope

namespace Microsoft.CodeAnalysis.CSharp.Snippets
{
[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared]
internal class CSharpForLoopSnippetProvider : AbstractForLoopSnippetProvider
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
internal class CSharpForLoopSnippetProvider : AbstractForLoopSnippetProvider
internal sealed class CSharpForLoopSnippetProvider : AbstractForLoopSnippetProvider

Comment on lines 3153 to 3155
<data name="Insert_a_for_loop" xml:space="preserve">
<value>Insert a 'for' loop</value>
</data>
Copy link
Member

Choose a reason for hiding this comment

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

Will the same string be used for VB in future?

If not, consider moving to CSharpFeaturesResources

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point

Comment on lines 41 to 43
for (int i = 0; i < length; i++)
{$$
}
Copy link
Member

Choose a reason for hiding this comment

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

I think it is much better if the cursor can be placed like this:

        for (int i = 0; i < length; i++)
        {
            $$
        }

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, but it is a question of if the LSP Snippet API should insert that indentation or if we should manually insert spaces for blocks when generating the syntax.

Copy link
Member

Choose a reason for hiding this comment

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

i feel like we shoudl be able to tell them: you should place the caret on this line at the appropriate indentation.

{
var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var generator = SyntaxGenerator.GetGenerator(document);
var options = await document.GetAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

why only the analyzer config options? not just all the otpions? then you don't need the null check below either.

Copy link
Member

Choose a reason for hiding this comment

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

@CyrusNajmabadi I think we really don't have to look at options at all. Always generating compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(allowVar: true).WithAdditionalAnnotations(Simplifier.Annotation) should just work and respect the user option.

varBuiltInType = options.GetOption(CSharpCodeStyleOptions.VarForBuiltInTypes).Value;
}

var indexVariable = generator.Identifier("i");
Copy link
Member

Choose a reason for hiding this comment

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

can we generate j/k/l etc. if we're already in a scope where that identifier is in use?

SyntaxFactory.VariableDeclarator(
indexVariable,
argumentList: null,
SyntaxFactory.EqualsValueClause((ExpressionSyntax)generator.LiteralExpression(0)))));
Copy link
Member

Choose a reason for hiding this comment

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

the declarator/list is duplicated. extract out and only do once. you only need to vary the TypeSyntax for var/int

Comment on lines 76 to 77
var forLoopSyntax = SyntaxFactory.ForStatement(variableDeclarationSyntax,
SyntaxFactory.SeparatedList<ExpressionSyntax>(),
Copy link
Member

Choose a reason for hiding this comment

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

nit.

Suggested change
var forLoopSyntax = SyntaxFactory.ForStatement(variableDeclarationSyntax,
SyntaxFactory.SeparatedList<ExpressionSyntax>(),
var forLoopSyntax = SyntaxFactory.ForStatement(
variableDeclarationSyntax,
SyntaxFactory.SeparatedList<ExpressionSyntax>(),

generator.IdentifierName(indexVariable),
// Using a temporary identifier name for now, could later be changed
// to look for an iterable item in the scope of the insertion.
generator.IdentifierName("length")),
Copy link
Member

Choose a reason for hiding this comment

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

should this be some sort of LSP marker that the user can then tab through?

Copy link
Member Author

Choose a reason for hiding this comment

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

what do you mean? It's a placeholder so the user can tab through and also rename?

Copy link
Member

Choose a reason for hiding this comment

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

I guess Cyrus means we should place this into the SnippetPlaceholder ?

protected override ImmutableArray<SnippetPlaceholder> GetForLoopSnippetPlaceholders(SyntaxNode node, ISyntaxFacts syntaxFacts)
{
using var _ = ArrayBuilder<SnippetPlaceholder>.GetInstance(out var arrayBuilder);
var placeHolderBuilder = new Dictionary<string, List<int>>();
Copy link
Member

Choose a reason for hiding this comment

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

is this just a MultiDictionary?

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess so, wasn't aware this existed

GetPartsOfForStatement(node, out var declaration, out var condition, out var incrementor, out var _1);

// Retrieves the placeholder present in the variable declaration as well as its location.
if (declaration != null)
Copy link
Member

Choose a reason for hiding this comment

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

when is teh decl, condition, or incrementor null? don't you always create all 3?

if (declaration != null)
{
var variableDeclarator = ((VariableDeclarationSyntax)declaration).Variables.FirstOrDefault();
if (variableDeclarator != null)
Copy link
Member

Choose a reason for hiding this comment

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

when is this null? don't you always create it?

if (variableDeclarator != null)
{
var declaratorIdentifier = variableDeclarator.Identifier;
placeHolderBuilder.Add(declaratorIdentifier.ToString(), new List<int>() { declaratorIdentifier.SpanStart });
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
placeHolderBuilder.Add(declaratorIdentifier.ToString(), new List<int>() { declaratorIdentifier.SpanStart });
placeHolderBuilder.Add(declaratorIdentifier.ValueText, new List<int>() { declaratorIdentifier.SpanStart });

placeHolderBuilder.Add(operand.ToString(), incrementorList);
}

incrementorList.Add(operand.SpanStart);
Copy link
Member

Choose a reason for hiding this comment

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

MultiDictionary will make all of this a lot easier :)

foreach (var kvp in placeHolderBuilder)
{
arrayBuilder.Add(new SnippetPlaceholder(kvp.Key, kvp.Value.ToImmutableArray()));
}
Copy link
Member

Choose a reason for hiding this comment

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

do you need to sort this array in any special way? or does it not matter?

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't matter because when we construct the LSP string, we create a new dictionary that has a mapping of the position it should be inserted into the string to the identifier

}

return arrayBuilder.ToImmutableArray();

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

{
GetPartsOfForStatement(caretTarget, out _, out _, out _, out var statement);
return statement.SpanStart + 1;

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

Overall looking great!

var generator = SyntaxGenerator.GetGenerator(document);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

var iteratorName = NameGenerator.GenerateUniqueName(
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to use this GenerateUniqueName to get a great name?

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps, but what would be the difference?

Copy link
Member

Choose a reason for hiding this comment

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

Nop, I just mean if it is possible to reuse the code, if it doesn't fit your needs that's fine

// Creating the variable declaration based on if the user has
// 'var for built in types' set in their editorconfig.
var variableDeclarationSyntax =
SyntaxFactory.VariableDeclaration(compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(allowVar: false),
Copy link
Member

Choose a reason for hiding this comment

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

Why allowVar is false here? Is it possible to to link this to the editorconfig value here
https://sourceroslyn.io/#Microsoft.CodeAnalysis.CSharp.CodeStyle/CSharpCodeStyleOptions.cs,cb8ef73fcae9e59f,references

Copy link
Member

@Youssef1313 Youssef1313 Aug 9, 2022

Choose a reason for hiding this comment

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

Why allowVar is false here?

Yeah it should be true.


Is it possible to to link this to the editorconfig value here

WithAdditionalAnnotations(Simplifier.Annotation) is needed. That way the code cleanup will be responsible of respecting the user preference. This should happen here:

var reformattedDocument = await CleanupDocumentAsync(formatAnnotatedSnippetDocument, cancellationToken).ConfigureAwait(false);

(specifically the Simplifier.ReduceAsync call)


// Retrieves the placeholder present in the variable declaration as well as its location.
// The declaration is constructed so it can't be null.
var variableDeclarator = ((VariableDeclarationSyntax)declaration!).Variables.FirstOrDefault();
Copy link
Member

@Cosifne Cosifne Aug 9, 2022

Choose a reason for hiding this comment

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

small nit: if it can't be null or empty because we own the code generation logic, then just call First() or Single()

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@Cosifne
Copy link
Member

Cosifne commented Aug 9, 2022

I feel this looks good, once all the existing comments are addressed it should be good to go

declaration = forStatement.Declaration;
condition = forStatement.Condition;
// We can assume there will only be one incrementor since it is only constructed with one.
incrementor = forStatement.Incrementors.First();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
incrementor = forStatement.Incrementors.First();
incrementor = forStatement.Incrementors.Single();

protected override async Task<Document> AddIndentationToDocumentAsync(Document document, int position, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var snippet = root.GetAnnotatedNodes(_findSnippetAnnotation).FirstOrDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var snippet = root.GetAnnotatedNodes(_findSnippetAnnotation).FirstOrDefault();
var snippetNode = root.GetAnnotatedNodes(_findSnippetAnnotation).First();


// Retrieves the placeholder present in the variable declaration as well as its location.
// The declaration is constructed so it can't be null.
var variableDeclarator = ((VariableDeclarationSyntax)declaration!).Variables.FirstOrDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@akhera99 akhera99 removed the request for review from CyrusNajmabadi September 23, 2022 16:04
@DoctorKrolic
Copy link
Contributor

@akhera99 Do you plan to continue working on this? If so, I have a little suggestion: since 'inline statement snippets' are a thing now (#67819) I think for can be implemented as inline statement snippet as well. So if there is code like someInt.$$, the for snippet can generate for (int i = 0; i < someInt; i++) {...} and not have a placeholder for someInt. On the other hand, if you don't want/have time to fully implement this snippet I would like to take it 🙂

@akhera99
Copy link
Member Author

@akhera99 Do you plan to continue working on this? If so, I have a little suggestion: since 'inline statement snippets' are a thing now (#67819) I think for can be implemented as inline statement snippet as well. So if there is code like someInt.$$, the for snippet can generate for (int i = 0; i < someInt; i++) {...} and not have a placeholder for someInt. On the other hand, if you don't want/have time to fully implement this snippet I would like to take it 🙂

@DoctorKrolic feel free to implement the snippet!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants