-
Notifications
You must be signed in to change notification settings - Fork 212
Use snippet InsertText in directive attributes to insert equals and quotes #12010
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
Use snippet InsertText in directive attributes to insert equals and quotes #12010
Conversation
This will insure that equals and quotes are added on attribute commit, and cursor is moved to the correct position between double quotes.
(var endIndex, var attributePrefix) = insertText.EndsWith("...", StringComparison.Ordinal) ? (^3, true) : (^0, false); | ||
|
||
// Don't allocate a new string unless we need to make a change. | ||
if (startIndex > 0 || endIndex.Value > 0) | ||
{ | ||
insertText = insertText[startIndex..endIndex]; | ||
} | ||
|
||
var isSnippet = false; | ||
if (!attributePrefix // Don't even try to add snippet to something like "@bind-..." | ||
&& TryGetSnippetText(containingAttribute, insertText, razorCompletionOptions, out var snippetText)) | ||
{ | ||
insertText = snippetText; | ||
isSnippet = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big fan of the tuple-returning-ternary, but I think this whole bit of code is pretty hard to follow, and it took me a while to work out what attributePrefix
means (I think the word "prefix" and the fact that its based on the presence of a suffix does my head in). Can it be expanded? Something like (untested):
var insertText = displayText.AsSpan();
if (insertText.StartsWith('@')
{
insertText = insertText[1..];
}
if (insertText.EndsWith("...", StringComparison.Orginal))
{
insertText = insertText[..^3];
}
else
{
var isSnippet = false;
if (TryGetSnippetText(containingAttribute, insertText, razorCompletionOptions, out var snippetText))
{
insertText = snippetText;
isSnippet = true;
}
}
Obviously either TryGetSnippetText
will need to change, because inputText
is now a span, but I think making it a span early means not having to jump through so many logic hoops to keep track of everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure all of the span calculatoins/collection operators made things easier to read, but I gave it a try. Let me know if you see ways to simplify things. Concatenation of spans seems a bit messy, not sure if there is a way to do it better. Ambiguity and different parameter sets (singe value vs span) in SpanExtensions and MemoryExtensions make things a bit messy to IMHO. Let me know if you know of a way to clean this up more. So far I'm really on the fence about this, but if it makes it easier to read for you I guess it's fine.
src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs
Outdated
Show resolved
Hide resolved
@@ -45,7 +48,7 @@ private RazorCodeDocument GetCodeDocument(string content) | |||
public void GetCompletionItems_OnNonAttributeArea_ReturnsEmptyCollection() | |||
{ | |||
// Arrange | |||
var context = CreateRazorCompletionContext(absoluteIndex: 3, "<input @ />"); | |||
var context = CreateRazorCompletionContext("<in$$put @ />"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love all of these updates to the test inputs, thank you! #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, yeah, inputs were pretty hard to read before, weren't they :)
@@ -278,15 +330,21 @@ private static void AssertDoesNotContain(IReadOnlyList<RazorCompletionItem> comp | |||
RazorCompletionItemKind.DirectiveAttribute == completion.Kind); | |||
} | |||
|
|||
private RazorCompletionContext CreateRazorCompletionContext(int absoluteIndex, string documentContent) | |||
private RazorCompletionContext CreateRazorCompletionContext(string testCodeText) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just make the parameter TestCode testCode
, and remove line 335, and everything should still comple the same. There is an implicit conversion. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, cleaned that up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I do think it's much easier to read now, though I'd love to know why they can't be proper extension method calls.
|
||
// Strip off the @ from the insertion text. This change is here to align the insertion text with the | ||
// completion hooks into VS and VSCode. Basically, completion triggers when `@` is typed so we don't | ||
// want to insert `@bind` because `@` already exists. | ||
var startIndex = insertText.StartsWith('@') ? 1 : 0; | ||
if (SpanExtensions.StartsWith(insertTextSpan, '@')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't this just be used as an extension method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both of them are in System - not sure if there is a better way to address this, but this is super-weird.
|
||
// Don't allocate a new string unless we need to make a change. | ||
if (startIndex > 0 || endIndex.Value > 0) | ||
if (MemoryExtensions.EndsWith(insertTextSpan, "...".AsSpan())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above :)
&& containingAttribute is not (MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax) | ||
&& containingAttribute.Parent is not (MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax)) | ||
{ | ||
var suffixTextSpan = razorCompletionOptions.AutoInsertAttributeQuotes ? "=\"$0\"".AsSpan() : "=$0".AsSpan(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to store these two spans as static ReadOnlyMemory<char>
so they're only created once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Learning opportunity for me. What extra allocations happen here? I would have thought the strings would be compiled into consts, and then the AsSpan calls would just create a struct around that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is the implementation so it seems allocation does happen and caching makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe they're all structs, so not really an "allocation" as we would usually talk about it (ie, not something to be garbage collected), I just meant to store then as a static readonly, like you'd do with any other thing that is only ever one value, but can't be a const for reasons.
But really I'm just copying @DustinCampbell since I assume he actually knows what he's talking about: https://github.com/dotnet/razor/blob/main/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorHtmlWriter.cs#L18
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... or maybe not, looking at it more closely. Either way, seems like caching can't hurt.
@@ -140,15 +154,42 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions( | |||
|
|||
var razorCompletionItem = RazorCompletionItem.CreateDirectiveAttribute( | |||
displayText, | |||
insertText, | |||
insertTextSpan.ToString(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we actually allocate here because ReadOnlySpan special-cases 'char' case
Also, the common case is actually use modifying the string rather than not modifying it - snippet ="{0}"
is almost always added. It's not added only for indexers (ending with "...").
However, it was easy enough to check by adding another local (let me know if I'm overlooking a clever way of doing it), so I modified the code to do so. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we actually allocate here
Isn't that specialization you linked to doing a string allocation? Either way, the check you added makes that no longer a concern.
public override string ToString()
{
if (typeof(T) == typeof(char))
{
return new string(new ReadOnlySpan<char>(ref Unsafe.As<T, char>(ref _reference), _length));
}
return $"System.ReadOnlySpan<{typeof(T).Name}>[{_length}]";
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a similar one, but I did say later ".. or maybe not, looking at it more closely" :)
|
||
completionItems.Add(razorCompletionItem); | ||
} | ||
|
||
return completionItems.ToImmutableAndClear(); | ||
|
||
bool TryGetSnippetText( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was actually a subtle bug there where I was capturing containingAttribute unintentionally. Nice suggestion, thanks. Fixed.
I'll enable auto-merge per earlier in-person conversation. Happy to address any additional feedback (if any) in a follow-up PR. |
Summary of the changes
Fixes:
#11395