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

feat: Selector autocompletate #303

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AvaloniaVS.Shared/AvaloniaVS.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Services\SolutionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\Throttle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TaskExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TextViewExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\FrameworkInfoUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\AvaloniaDesigner.xaml.cs">
<DependentUpon>AvaloniaDesigner.xaml</DependentUpon>
Expand Down
39 changes: 32 additions & 7 deletions AvaloniaVS.Shared/IntelliSense/XamlCompletion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,32 @@ public XamlCompletion(Completion completion)
completion.InsertText,
completion.Description,
GetImage(completion.Kind),
completion.Kind.ToString())
completion.Kind.ToString(),
suffix: string.IsNullOrWhiteSpace(completion.Suffix) ? string.Empty : $"({completion.Suffix})")
{
if (completion.RecommendedCursorOffset.HasValue)
{
CursorOffset = completion.InsertText.Length - completion.RecommendedCursorOffset.Value;
}

Kind = completion.Kind;
DeleteTextOffset = completion.DeleteTextOffset;
}

public int? DeleteTextOffset { get; }

public override string InsertionText
{
get
{
if (HasFlag(Kind, CompletionKind.Name) && !string.IsNullOrEmpty(Suffix))
{
return $"{Suffix.Substring(1,Suffix.Length-2)}#{base.InsertionText}";
}
return base.InsertionText;
}

set => base.InsertionText = value;
}

public int CursorOffset { get; }
Expand All @@ -49,7 +67,6 @@ private static ImageMoniker GetImage(CompletionKind kind)
{
LoadImages();
}

if (HasFlag(kind, CompletionKind.DataProperty))
{
return s_images[(int)CompletionKind.DataProperty];
Expand All @@ -62,13 +79,20 @@ private static ImageMoniker GetImage(CompletionKind kind)
{
return s_images[(int)CompletionKind.Enum];
}

return s_images[(int)kind];

bool HasFlag(CompletionKind test, CompletionKind expected)
else if (HasFlag(kind, CompletionKind.Selector))
{
return s_images[(int)CompletionKind.Enum];
}
else if (HasFlag(kind, CompletionKind.Name))
{
return (test & expected) == expected;
return s_images[(int)CompletionKind.Class];
}
return s_images[(int)kind];
}

private static bool HasFlag(CompletionKind test, CompletionKind expected)
{
return (test & expected) == expected;
}

private static void LoadImages()
Expand All @@ -90,6 +114,7 @@ private static void LoadImages()
s_images[(int)CompletionKind.MarkupExtension] = KnownMonikers.Namespace;
s_images[(int)CompletionKind.DataProperty] = KnownMonikers.DatabaseProperty;
s_images[(int)CompletionKind.TargetTypeClass] = KnownMonikers.ClassPublic;
s_images[(int)CompletionKind.Selector] = KnownMonikers.Namespace;
}
}
}
58 changes: 45 additions & 13 deletions AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pv
return VSConstants.S_OK;
}
}

var result = _nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);

if (HandleSessionStart(c))
{
return VSConstants.S_OK;
Expand All @@ -107,7 +105,7 @@ private bool HandleSessionStart(char c)
{
if (_session == null || _session.IsDismissed)
{
if (TriggerCompletion() && c != '<' && c != '.' && c != ' ')
if (TriggerCompletion() && c != '<' && c != '.' && c != ' ' && c != '[' && c != '(' && c != '|' && c != '#' && c != '/')
{
_session?.Filter();
}
Expand All @@ -127,7 +125,6 @@ private bool HandleSessionStart(char c)
return true;
}
}

return false;
}

Expand Down Expand Up @@ -159,7 +156,7 @@ private bool HandleSessionCompletion(char c)
// So we only trigger on ' ' or '\t', and swallow that so it doesn't get
// inserted into the text buffer
if (_session != null && !_session.IsDismissed)
{
{
var text = line.Snapshot.GetText(start, end - start);

if (text.Contains("xmlns"))
Expand Down Expand Up @@ -190,14 +187,28 @@ private bool HandleSessionCompletion(char c)

// Also adding '#' for Selectors

if (char.IsWhiteSpace(c) || c == '\'' || c == '"' || c == '=' || c == '>' || c == '.' || c == '#')
if (char.IsWhiteSpace(c)
|| c == '\'' || c == '"' || c == '=' || c == '>' || c == '.'
|| c == '#' || c == ')' || c == ']')
{
if (_session != null && !_session.IsDismissed &&
_session.SelectedCompletionSet.SelectionStatus.IsSelected)
{
var selected = _session.SelectedCompletionSet.SelectionStatus.Completion as XamlCompletion;

var bufferPos = _textView.Caret.Position.BufferPosition;

_session.Commit();

if (selected.DeleteTextOffset is int rof)
{
var newCursorPos = bufferPos.Add(rof);
SnapshotSpan deleteSpan = newCursorPos < bufferPos
? new(newCursorPos, -rof)
: new(bufferPos, rof);
_textView.TextBuffer.Delete(deleteSpan);
}
workgroupengineering marked this conversation as resolved.
Show resolved Hide resolved

if (selected?.CursorOffset > 0)
{
// Offset the cursor if necessary e.g. to place it within the quotation
Expand All @@ -215,23 +226,24 @@ private bool HandleSessionCompletion(char c)
var state = parser.State;

bool skip = c != '>';
if (state == XmlParser.ParserState.StartElement &&
if (state == XmlParser.ParserState.StartElement &&
(c == '.' || c == ' '))
{
// Don't swallow the '.' or ' ' if this is an Xml element, like
// Window.Resources. However do swallow tab
// Window.Resources. However do swallow tab
skip = false;
}

if (state == XmlParser.ParserState.AttributeValue ||
if (state == XmlParser.ParserState.AttributeValue ||
state == XmlParser.ParserState.AfterAttributeValue)
{
var isSelector = parser.AttributeName?.Equals("Selector") == true;
if (char.IsWhiteSpace(c))
{
// For most xml attributes, swallow the space upon completion
// For selector, allow it to go into the buffer
// Also if in a markupextention
skip = !(parser.AttributeName?.Equals("Selector") == true);
skip = !(isSelector && c != '\n' && c != '\t');

// If we're in a markup extension, only swallow the space if the
// completion isn't on the Markup extension
Expand Down Expand Up @@ -286,11 +298,14 @@ private bool HandleSessionCompletion(char c)
skip = false;
}

var lastInsertionChar = (selected.InsertionText?.Length ?? 0) > 0
? selected.InsertionText[selected.InsertionText.Length - 1]
: default;

// Cases like {Binding Path= result in {Binding Path==
// as the completion includes the '=', if the entered char
// is the same as the last char here, swallow the entered char
if (!skip && (selected.InsertionText?.Length > 0 &&
selected.InsertionText[selected.InsertionText.Length - 1] == c))
if (!skip && lastInsertionChar == c)
{
skip = true;

Expand All @@ -300,6 +315,12 @@ private bool HandleSessionCompletion(char c)
if (c == '=')
TriggerCompletion();
}
else if (isSelector && lastInsertionChar is '=' or '.')
{
// Trigger Selector property Value Completation
if (c is not '=' or '.')
TriggerCompletion();
}
}
else if (state != XmlParser.ParserState.StartElement)
{
Expand All @@ -319,14 +340,25 @@ private bool HandleSessionCompletion(char c)
var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end);
var state = parser.State;

if (state == XmlParser.ParserState.AttributeValue &&
if (state == XmlParser.ParserState.AttributeValue &&
parser.AttributeName?.Equals("Selector") == true)
{
// Force new session to start to suggest pseudoclasses
_session.Dismiss();
return false;
}
}
else if (c == '(' && _session?.IsDismissed == false)
{
var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end);
var state = parser.State;
if ((state == XmlParser.ParserState.AttributeValue || state == XmlParser.ParserState.AfterAttributeValue)
&& parser.AttributeName?.Equals("Selector") == true)
{
_session.Dismiss();
return false;
}
}
else if (c == '{' && (_session != null && !_session.IsDismissed))
{
var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end);
Expand Down
28 changes: 28 additions & 0 deletions AvaloniaVS.Shared/TextViewExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text;
using System.Linq;

namespace AvaloniaVS;

internal static class TextViewExtensions
{
public static NormalizedSnapshotSpanCollection GetSpanInView(this ITextView textView, SnapshotSpan span)
=> textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot);

public static void SetSelection(
this ITextView textView, VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint)
{
var isReversed = activePoint < anchorPoint;
var start = isReversed ? activePoint : anchorPoint;
var end = isReversed ? anchorPoint : activePoint;
SetSelection(textView, new SnapshotSpan(start.Position, end.Position), isReversed);
}

public static void SetSelection(
this ITextView textView, SnapshotSpan span, bool isReversed = false)
{
var spanInView = textView.GetSpanInView(span).Single();
textView.Selection.Select(spanInView, isReversed);
textView.Caret.MoveTo(isReversed ? spanInView.Start : spanInView.End);
}
}
Loading