From d116634f2735e8fbc1e20500cf6c6c4cbdecceef Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Sun, 29 Oct 2023 23:09:31 +0200 Subject: [PATCH] Add support for namespace suggestions --- AvaloniaVS.Shared/AvaloniaVS.Shared.projitems | 9 + .../IntelliSense/CompletionEngineSource.cs | 15 ++ .../XamlCompletionCommandHandler.cs | 9 +- .../XamlCompletionHandlerProvider.cs | 9 +- .../IntelliSense/XamlCompletionSource.cs | 15 +- .../XamlCompletionSourceProvider.cs | 14 +- .../Actions/Base/BaseSuggestedAction.cs | 37 ++++ .../Actions/MissingAliasSuggestedAction.cs | 56 ++++++ ...MissingNamespaceAndAliasSuggestedAction.cs | 78 ++++++++ .../MissingNamespaceSuggestedAction.cs | 64 +++++++ .../Helpers/PreviewProvider.cs | 45 +++++ .../SuggestedActionsSource.cs | 181 ++++++++++++++++++ .../SuggestedActionsSourceProvider.cs | 44 +++++ .../Completion/CompletionEngine.cs | 2 +- 14 files changed, 557 insertions(+), 21 deletions(-) create mode 100644 AvaloniaVS.Shared/IntelliSense/CompletionEngineSource.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/Actions/Base/BaseSuggestedAction.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/Actions/MissingAliasSuggestedAction.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceAndAliasSuggestedAction.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceSuggestedAction.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/Helpers/PreviewProvider.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSource.cs create mode 100644 AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSourceProvider.cs diff --git a/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems b/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems index c120acde..4b375241 100644 --- a/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems +++ b/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems @@ -14,6 +14,7 @@ + @@ -39,6 +40,13 @@ + + + + + + + @@ -94,5 +102,6 @@ + \ No newline at end of file diff --git a/AvaloniaVS.Shared/IntelliSense/CompletionEngineSource.cs b/AvaloniaVS.Shared/IntelliSense/CompletionEngineSource.cs new file mode 100644 index 00000000..a77bd64a --- /dev/null +++ b/AvaloniaVS.Shared/IntelliSense/CompletionEngineSource.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.Composition; +using Avalonia.Ide.CompletionEngine; + +namespace AvaloniaVS.Shared.IntelliSense +{ + [Export] + public class CompletionEngineSource + { + public CompletionEngineSource() + { + CompletionEngine = new CompletionEngine(); + } + public CompletionEngine CompletionEngine { get; } + } +} diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs index f8998fea..96e81770 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs @@ -37,24 +37,25 @@ internal class XamlCompletionCommandHandler : IOleCommandTarget private readonly ICompletionBroker _completionBroker; private readonly IOleCommandTarget _nextCommandHandler; private readonly ITextView _textView; + private readonly CompletionEngine _engine; private ICompletionSession _session; public XamlCompletionCommandHandler( IServiceProvider serviceProvider, ICompletionBroker completionBroker, ITextView textView, - IVsTextView textViewAdapter) + IVsTextView textViewAdapter, + CompletionEngine completionEngine) { _serviceProvider = serviceProvider; _completionBroker = completionBroker; _textView = textView; + _engine = completionEngine; // Add ourselves as a command to the text view. textViewAdapter.AddCommandFilter(this, out _nextCommandHandler); } - public CompletionEngine Engine { get; set; } - public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) { ThreadHelper.ThrowIfNotOnUIThread(); @@ -255,7 +256,7 @@ private bool HandleSessionCompletion(char c) if (state == XmlParser.ParserState.AttributeValue || state == XmlParser.ParserState.AfterAttributeValue) { - var type = Engine.Helper.LookupType(parser.TagName); + var type = _engine.Helper.LookupType(parser.TagName); if (type != null && type.Events.FirstOrDefault(x => x.Name == parser.AttributeName) != null) { GenerateEventHandler(type.FullName, parser.AttributeName, selected.InsertionText); diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs index 9b31aeb2..e92782bc 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.Composition; using AvaloniaVS.Models; +using AvaloniaVS.Shared.IntelliSense; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Shell; @@ -22,16 +23,19 @@ internal class XamlCompletionHandlerProvider : IVsTextViewCreationListener private readonly IServiceProvider _serviceProvider; private readonly IVsEditorAdaptersFactoryService _adapterService; private readonly ICompletionBroker _completionBroker; + private readonly CompletionEngineSource _completionEngineSource; [ImportingConstructor] public XamlCompletionHandlerProvider( [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, IVsEditorAdaptersFactoryService adapterService, - ICompletionBroker completionBroker) + ICompletionBroker completionBroker, + CompletionEngineSource completionEngineSource) { _serviceProvider = serviceProvider; _adapterService = adapterService; _completionBroker = completionBroker; + _completionEngineSource = completionEngineSource; } public void VsTextViewCreated(IVsTextView textViewAdapter) @@ -46,7 +50,8 @@ public void VsTextViewCreated(IVsTextView textViewAdapter) _serviceProvider, _completionBroker, textView, - textViewAdapter)); + textViewAdapter, + _completionEngineSource.CompletionEngine)); } } } diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletionSource.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletionSource.cs index 469b42dc..f4e45c52 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletionSource.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletionSource.cs @@ -1,25 +1,24 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using AvaloniaVS.Models; +using AvaloniaVS.Shared.IntelliSense; using Microsoft.CodeAnalysis; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Serilog; -using CompletionEngine = Avalonia.Ide.CompletionEngine.CompletionEngine; namespace AvaloniaVS.IntelliSense { internal class XamlCompletionSource : ICompletionSource { private readonly ITextBuffer _buffer; - private readonly CompletionEngine _engine; + private readonly CompletionEngineSource _engine; - public XamlCompletionSource(ITextBuffer textBuffer) + public XamlCompletionSource(ITextBuffer textBuffer, CompletionEngineSource completionEngineSource) { _buffer = textBuffer; - _engine = new CompletionEngine(); + _engine = completionEngineSource; } public void AugmentCompletionSession(ICompletionSession session, IList completionSets) @@ -27,13 +26,11 @@ public void AugmentCompletionSession(ICompletionSession session, IList(typeof(XamlBufferMetadata), out var metadata) && metadata.CompletionMetadata != null) { - session.TextView.Properties.TryGetProperty(typeof(XamlCompletionCommandHandler), out var property); - property.Engine = _engine; var sw = Stopwatch.StartNew(); var pos = session.TextView.Caret.Position.BufferPosition; var text = pos.Snapshot.GetText(); _buffer.Properties.TryGetProperty("AssemblyName", out string assemblyName); - var completions = _engine.GetCompletions(metadata.CompletionMetadata, text, pos, assemblyName); + var completions = _engine.CompletionEngine.GetCompletions(metadata.CompletionMetadata, text, pos, assemblyName); if (completions?.Completions.Count > 0) { diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletionSourceProvider.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletionSourceProvider.cs index 1a4071a9..e5b3ffa7 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletionSourceProvider.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletionSourceProvider.cs @@ -1,9 +1,7 @@ -using System; -using System.ComponentModel.Composition; +using System.ComponentModel.Composition; using AvaloniaVS.Models; +using AvaloniaVS.Shared.IntelliSense; using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; @@ -14,11 +12,17 @@ namespace AvaloniaVS.IntelliSense [Name("Avalonia XAML Completion")] internal class XamlCompletionSourceProvider : ICompletionSourceProvider { + [ImportingConstructor] + public XamlCompletionSourceProvider([Import] CompletionEngineSource completionEngineSource) + { + _completionEngineSource = completionEngineSource; + } + private readonly CompletionEngineSource _completionEngineSource; public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer) { if (textBuffer.Properties.ContainsProperty(typeof(XamlBufferMetadata))) { - return new XamlCompletionSource(textBuffer); + return new XamlCompletionSource(textBuffer, _completionEngineSource); } return null; diff --git a/AvaloniaVS.Shared/SuggestedActions/Actions/Base/BaseSuggestedAction.cs b/AvaloniaVS.Shared/SuggestedActions/Actions/Base/BaseSuggestedAction.cs new file mode 100644 index 00000000..6a87c820 --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/Actions/Base/BaseSuggestedAction.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Imaging.Interop; +using Microsoft.VisualStudio.Language.Intellisense; + +namespace AvaloniaVS.Shared.SuggestedActions.Actions.Base +{ + internal class BaseSuggestedAction + { + public bool HasActionSets { get; } + + public ImageMoniker IconMoniker { get; } + + public string IconAutomationText { get; } + + public string InputGestureText { get; } + + public bool HasPreview => true; + + public void Dispose() + { + } + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = Guid.Empty; + return false; + } + + public Task> GetActionSetsAsync(CancellationToken cancellationToken) + { + return Task.FromResult>(null); + } + } +} diff --git a/AvaloniaVS.Shared/SuggestedActions/Actions/MissingAliasSuggestedAction.cs b/AvaloniaVS.Shared/SuggestedActions/Actions/MissingAliasSuggestedAction.cs new file mode 100644 index 00000000..149fb880 --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/Actions/MissingAliasSuggestedAction.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AvaloniaVS.Shared.SuggestedActions.Actions.Base; +using AvaloniaVS.Shared.SuggestedActions.Helpers; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; + +namespace AvaloniaVS.Shared.SuggestedActions.Actions +{ + internal class MissingAliasSuggestedAction : BaseSuggestedAction, ISuggestedAction + { + private readonly ITrackingSpan _span; + private readonly ITextSnapshot _snapshot; + private readonly string _targetClassName; + private readonly string _namespaceAlias; + private readonly IWpfDifferenceViewerFactoryService _diffFactory; + private readonly IDifferenceBufferFactoryService _diffBufferFactory; + private readonly ITextBufferFactoryService _bufferFactory; + private readonly ITextViewRoleSet _previewRoleSet; + + public MissingAliasSuggestedAction(ITrackingSpan span, IWpfDifferenceViewerFactoryService diffFactory, IDifferenceBufferFactoryService diffBufferFactory, ITextBufferFactoryService bufferFactory, ITextEditorFactoryService textEditorFactoryService, IReadOnlyDictionary inverseNamespaces) + { + _span = span; + _snapshot = _span.TextBuffer.CurrentSnapshot; + _targetClassName = _span.GetText(_snapshot); + var targetClassMetadata = inverseNamespaces.FirstOrDefault(x => x.Key.Split('.').Last() == _targetClassName); + _namespaceAlias = targetClassMetadata.Value.Split(':').Last().Split('.').Last(); + _diffFactory = diffFactory; + _diffBufferFactory = diffBufferFactory; + _bufferFactory = bufferFactory; + _previewRoleSet = textEditorFactoryService.CreateTextViewRoleSet(PredefinedTextViewRoles.Analyzable); + DisplayText = $"Use {_namespaceAlias.ToLower()} ({targetClassMetadata.Value})"; + } + + public string DisplayText { get; } + + public Task GetPreviewAsync(CancellationToken cancellationToken) + { + return Task.FromResult(PreviewProvider.GetPreview(_bufferFactory, _span, _diffBufferFactory, _diffFactory, _previewRoleSet, ApplySuggestion)); + } + + private void ApplySuggestion(ITextBuffer buffer) + { + buffer.Replace(_span.GetSpan(_snapshot), $"{_namespaceAlias.ToLower()}:{_targetClassName}"); + } + + public void Invoke(CancellationToken cancellationToken) + { + ApplySuggestion(_span.TextBuffer); + } + } +} diff --git a/AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceAndAliasSuggestedAction.cs b/AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceAndAliasSuggestedAction.cs new file mode 100644 index 00000000..1a020af2 --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceAndAliasSuggestedAction.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AvaloniaVS.Shared.SuggestedActions.Actions.Base; +using AvaloniaVS.Shared.SuggestedActions.Helpers; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; + +namespace AvaloniaVS.Shared.SuggestedActions.Actions +{ + internal class MissingNamespaceAndAliasSuggestedAction : BaseSuggestedAction, ISuggestedAction + { + private readonly ITrackingSpan _span; + private readonly ITextSnapshot _snapshot; + private readonly string _namespaceAlias; + private readonly string _targetClassName; + private readonly KeyValuePair _targetClassMetadata; + private readonly IWpfDifferenceViewerFactoryService _diffFactory; + private readonly IDifferenceBufferFactoryService _diffBufferFactory; + private readonly ITextBufferFactoryService _bufferFactory; + private readonly Dictionary _aliases; + private readonly ITextViewRoleSet _previewRoleSet; + + public MissingNamespaceAndAliasSuggestedAction(ITrackingSpan span, IWpfDifferenceViewerFactoryService diffFactory, + IDifferenceBufferFactoryService diffBufferFactory, ITextBufferFactoryService bufferFactory, ITextEditorFactoryService textEditorFactoryService, + IReadOnlyDictionary inverseNamespaces, Dictionary aliases) + { + _span = span; + _snapshot = _span.TextBuffer.CurrentSnapshot; + _targetClassName = _span.GetText(_snapshot); + _targetClassMetadata = inverseNamespaces.FirstOrDefault(x => x.Key.Split('.').Last() == _targetClassName); + + // _targetClassMetadata.Value is the namespace of the control we are trying to add the namespace to. + // It is usually in the format using:MyNamespace.Something. + // So to get the prefix for the control we are splitting it by ':' + // Then taking the MyNamespace.Something part and splitting it by '.' and getting Something. + _namespaceAlias = _targetClassMetadata.Value.Split(':').Last().Split('.').Last(); + DisplayText = $"Add xmlns {_namespaceAlias}"; + _diffFactory = diffFactory; + _diffBufferFactory = diffBufferFactory; + _bufferFactory = bufferFactory; + _aliases = aliases; + _previewRoleSet = textEditorFactoryService.CreateTextViewRoleSet(PredefinedTextViewRoles.Analyzable); + } + + public string DisplayText { get; } + + + public Task GetPreviewAsync(CancellationToken cancellationToken) + { + return Task.FromResult(PreviewProvider.GetPreview(_bufferFactory, _span, _diffBufferFactory, _diffFactory, _previewRoleSet, ApplySuggestion)); + } + + public void Invoke(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + ApplySuggestion(_span.TextBuffer); + } + + private void ApplySuggestion(ITextBuffer buffer) + { + var lastNs = _aliases.Last().Value; + + buffer.Replace(_span.GetSpan(_snapshot), $"{_namespaceAlias.ToLower()}:{_targetClassName}"); + + // We get the index of the last namespace in the list and add the last namespace length without quotes and add 2. + // One for qutation mark and one to place the new namespace in an empty space. + buffer.Insert(buffer.CurrentSnapshot.GetText().IndexOf(lastNs) + lastNs.Length + 2, $"xmlns:{_namespaceAlias.ToLower()}=\"{_targetClassMetadata.Value}\""); + } + + } +} diff --git a/AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceSuggestedAction.cs b/AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceSuggestedAction.cs new file mode 100644 index 00000000..33e73feb --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/Actions/MissingNamespaceSuggestedAction.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AvaloniaVS.Shared.SuggestedActions.Actions.Base; +using AvaloniaVS.Shared.SuggestedActions.Helpers; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; + +namespace AvaloniaVS.Shared.SuggestedActions.Actions +{ + internal class MissingNamespaceSuggestedAction : BaseSuggestedAction, ISuggestedAction + { + private readonly ITrackingSpan _span; + private readonly KeyValuePair _targetClassMetadata; + private readonly IWpfDifferenceViewerFactoryService _diffFactory; + private readonly IDifferenceBufferFactoryService _diffBufferFactory; + private readonly ITextBufferFactoryService _bufferFactory; + private readonly Dictionary _aliases; + private readonly string _alias; + private readonly ITextViewRoleSet _previewRoleSet; + + public MissingNamespaceSuggestedAction(ITrackingSpan span, IWpfDifferenceViewerFactoryService diffFactory, IDifferenceBufferFactoryService diffBufferFactory, + ITextBufferFactoryService bufferFactory, ITextEditorFactoryService textEditorFactoryService, IReadOnlyDictionary inverseNamespaces, + Dictionary aliases, string alias) + { + _span = span; + _targetClassMetadata = inverseNamespaces.FirstOrDefault(x => x.Key.Split('.').Last() == _span.GetText(_span.TextBuffer.CurrentSnapshot)); + DisplayText = $"Add xmlns {alias}"; + _diffFactory = diffFactory; + _diffBufferFactory = diffBufferFactory; + _bufferFactory = bufferFactory; + _aliases = aliases; + _alias = alias; + _previewRoleSet = textEditorFactoryService.CreateTextViewRoleSet(PredefinedTextViewRoles.Analyzable); + } + + public string DisplayText { get; } + + public Task GetPreviewAsync(CancellationToken cancellationToken) + { + return Task.FromResult(PreviewProvider.GetPreview(_bufferFactory, _span, _diffBufferFactory, _diffFactory, _previewRoleSet, ApplySuggestion)); + } + + public void Invoke(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + ApplySuggestion(_span.TextBuffer); + } + + private void ApplySuggestion(ITextBuffer buffer) + { + var lastNs = _aliases.Last().Value; + + buffer.Insert(buffer.CurrentSnapshot.GetText().IndexOf(lastNs) + lastNs.Length + 2, $"xmlns:{_alias}=\"{_targetClassMetadata.Value}\""); + } + + } +} diff --git a/AvaloniaVS.Shared/SuggestedActions/Helpers/PreviewProvider.cs b/AvaloniaVS.Shared/SuggestedActions/Helpers/PreviewProvider.cs new file mode 100644 index 00000000..9107fbb4 --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/Helpers/PreviewProvider.cs @@ -0,0 +1,45 @@ +using System; +using System.Windows; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; + +namespace AvaloniaVS.Shared.SuggestedActions.Helpers +{ + internal static class PreviewProvider + { + public static FrameworkElement GetPreview(ITextBufferFactoryService bufferFactory, ITrackingSpan span, IDifferenceBufferFactoryService diffBufferFactory, IWpfDifferenceViewerFactoryService diffFactory, ITextViewRoleSet previewRoleSet, Action applyNamespaceSuggestionAction) + { + var snapshot = span.TextBuffer.CurrentSnapshot; + + var leftBuffer = bufferFactory.CreateTextBuffer(snapshot.GetText(), span.TextBuffer.ContentType); + + var rightBuffer = bufferFactory.CreateTextBuffer(snapshot.GetText(), span.TextBuffer.ContentType); + + applyNamespaceSuggestionAction(rightBuffer); + + var diffBuffer = diffBufferFactory.CreateDifferenceBuffer(leftBuffer, rightBuffer); + var diffView = diffFactory.CreateDifferenceView(diffBuffer, previewRoleSet); + diffView.ViewMode = DifferenceViewMode.Inline; + diffView.InlineView.VisualElement.Focusable = false; + + // DiffView size to content + diffView.DifferenceBuffer.SnapshotDifferenceChanged += (sender, args) => + { + diffView.InlineView.DisplayTextLineContainingBufferPosition( + new SnapshotPoint(diffView.DifferenceBuffer.CurrentInlineBufferSnapshot, 0), + 0.0, ViewRelativePosition.Top, double.MaxValue, double.MaxValue + ); + + var width = Math.Max(diffView.InlineView.MaxTextRightCoordinate * (diffView.InlineView.ZoomLevel / 100), 400); // Width of the widest line. + var height = diffView.InlineView.LineHeight * (diffView.InlineView.ZoomLevel / 100) * // Height of each line. + diffView.DifferenceBuffer.CurrentInlineBufferSnapshot.LineCount; + + diffView.VisualElement.Width = width; + diffView.VisualElement.Height = height; + }; + return diffView.VisualElement; + } + + } +} diff --git a/AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSource.cs b/AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSource.cs new file mode 100644 index 00000000..51907953 --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSource.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Avalonia.Ide.CompletionEngine; +using AvaloniaVS.Models; +using AvaloniaVS.Shared.SuggestedActions.Actions; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Operations; + +namespace AvaloniaVS.Shared.SuggestedActions +{ + class SuggestedActionsSource : ISuggestedActionsSource + { + private readonly SuggestedActionsSourceProvider _factory; + private readonly ITextBuffer _textBuffer; + private readonly IWpfDifferenceViewerFactoryService _diffFactory; + private readonly IDifferenceBufferFactoryService _diffBufferFactory; + private readonly ITextBufferFactoryService _bufferFactory; + private readonly ITextEditorFactoryService _textEditorFactoryService; + private readonly ITextView _textView; + + public SuggestedActionsSource(SuggestedActionsSourceProvider testSuggestedActionsSourceProvider, ITextView textView, ITextBuffer textBuffer, + IWpfDifferenceViewerFactoryService diffFactory, IDifferenceBufferFactoryService diffBufferFactory, ITextBufferFactoryService bufferFactory, + ITextEditorFactoryService textEditorFactoryService) + { + _factory = testSuggestedActionsSourceProvider; + _textBuffer = textBuffer; + _diffFactory = diffFactory; + _diffBufferFactory = diffBufferFactory; + _bufferFactory = bufferFactory; + _textEditorFactoryService = textEditorFactoryService; + _textView = textView; + } + + public event EventHandler SuggestedActionsChanged; + + public void Dispose() + { + } + + public IEnumerable GetSuggestedActions(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, + CancellationToken cancellationToken) + { + var availableSuggestedActions = SuggestedActionsAreAvailable(range); + if (TryGetWordUnderCaret(out var extent) && (availableSuggestedActions.Item1 || availableSuggestedActions.Item2 || availableSuggestedActions.Item3)) + { + extent.Span.Snapshot.TextBuffer.Properties.TryGetProperty(typeof(XamlBufferMetadata), out var metadata); + var trackingSpan = range.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive); + ISuggestedAction suggestedAction = null; + if (availableSuggestedActions.Item1) + { + suggestedAction = new MissingNamespaceAndAliasSuggestedAction(trackingSpan, _diffFactory, _diffBufferFactory, _bufferFactory, _textEditorFactoryService, + metadata.CompletionMetadata.InverseNamespace, CompletionEngine.GetNamespaceAliases(extent.Span.Snapshot.TextBuffer.CurrentSnapshot.GetText())); + } + else if (availableSuggestedActions.Item2) + { + suggestedAction = new MissingAliasSuggestedAction(trackingSpan, _diffFactory, _diffBufferFactory, _bufferFactory, _textEditorFactoryService, + metadata.CompletionMetadata.InverseNamespace); + } + else if (availableSuggestedActions.Item3) + { + HasAlias(out var alias); + suggestedAction = new MissingNamespaceSuggestedAction(trackingSpan, _diffFactory, _diffBufferFactory, _bufferFactory, _textEditorFactoryService, + metadata.CompletionMetadata.InverseNamespace, CompletionEngine.GetNamespaceAliases(extent.Span.Snapshot.TextBuffer.CurrentSnapshot.GetText()), alias); + } + return new SuggestedActionSet[] { new SuggestedActionSet(new ISuggestedAction[] { suggestedAction }) }; + } + return Enumerable.Empty(); + } + + public Task HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken) + { + var availableSuggestedActions = SuggestedActionsAreAvailable(range); + if (availableSuggestedActions.Item1 || availableSuggestedActions.Item2 || availableSuggestedActions.Item3) + { + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = Guid.Empty; + return false; + } + + private bool TryGetWordUnderCaret(out TextExtent wordExtent) + { + var caret = _textView.Caret; + SnapshotPoint point; + + if (caret.Position.BufferPosition > 0) + { + point = caret.Position.BufferPosition - 1; + } + else + { + wordExtent = default; + return false; + } + + var navigator = _factory.NavigatorService.GetTextStructureNavigator(_textBuffer); + + wordExtent = navigator.GetExtentOfWord(point); + return true; + } + + + /// + /// This method returns 3 bool values. First one defines whether MissingNamespaceAndAliasSuggestedAction should be applied + /// Second one defines whether MissingAliasSuggestedAction should be applied. + /// Third one defines whether MissingNamespaceSuggestedAction should be applied. + /// + private (bool, bool, bool) SuggestedActionsAreAvailable(SnapshotSpan range) + { + if (TryGetWordUnderCaret(out var extent)) + { + var span = range.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive); + var snapshot = span.TextBuffer.CurrentSnapshot; + var targetClassName = span.GetText(snapshot); + span.TextBuffer.Properties.TryGetProperty(typeof(XamlBufferMetadata), out var metadata); + if (metadata == null || metadata.CompletionMetadata?.InverseNamespace == null) + { + return (false, false, false); + } + var targetClassMetadata = metadata.CompletionMetadata.InverseNamespace.FirstOrDefault(x => x.Key.Split('.').Last() == targetClassName); + + // Exclude all classes from avaloniaui namespace because controls from this namespace are included by default. + if (targetClassMetadata.Value != null && targetClassMetadata.Key != null && !metadata.CompletionMetadata.Namespaces.First(x => x.Key == "https://github.com/avaloniaui").Value.ContainsKey(targetClassName)) + { + if (!CompletionEngine.GetNamespaceAliases(span.TextBuffer.CurrentSnapshot.GetText()).ContainsValue(targetClassMetadata.Value)) + { + if (!HasAlias(out var _)) + { + return (true, false, false); + } + else + { + return (false, false, true); + } + } + else if (!HasAlias(out var _)) + { + return (false, true, false); + } + } + + } + return (false, false, false); + } + + private bool HasAlias(out string alias) + { + + var span = _textView.Caret.ContainingTextViewLine.Extent.GetText().Trim(); + var xmlReader = XmlReader.Create(new StringReader(span)); + try + { + xmlReader.Read(); + } + catch + { + if (xmlReader.NodeType == XmlNodeType.Element && !string.IsNullOrEmpty(xmlReader.Prefix)) + { + alias = xmlReader.Prefix; + return true; + } + } + alias = null; + return false; + } + } +} diff --git a/AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSourceProvider.cs b/AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSourceProvider.cs new file mode 100644 index 00000000..6847ac7d --- /dev/null +++ b/AvaloniaVS.Shared/SuggestedActions/SuggestedActionsSourceProvider.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Operations; +using Microsoft.VisualStudio.Utilities; + +namespace AvaloniaVS.Shared.SuggestedActions +{ + [Export(typeof(ISuggestedActionsSourceProvider))] + [Name("SuggestedActionsSourceProvider")] + [ContentType("xml")] + internal class SuggestedActionsSourceProvider : ISuggestedActionsSourceProvider + { + private readonly IWpfDifferenceViewerFactoryService _diffFactory; + private readonly IDifferenceBufferFactoryService _diffBufferFactory; + private readonly ITextBufferFactoryService _bufferFactory; + private readonly ITextEditorFactoryService _textEditorFactoryService; + + [ImportingConstructor] + public SuggestedActionsSourceProvider([Import] IWpfDifferenceViewerFactoryService diffFactory, [Import] IDifferenceBufferFactoryService diffBufferFactory, + [Import] ITextBufferFactoryService bufferFactory, [Import] ITextEditorFactoryService textEditorFactoryService) + { + _diffFactory = diffFactory; + _diffBufferFactory = diffBufferFactory; + _bufferFactory = bufferFactory; + _textEditorFactoryService = textEditorFactoryService; + } + + [Import(typeof(ITextStructureNavigatorSelectorService))] + internal ITextStructureNavigatorSelectorService NavigatorService { get; set; } + + public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer) + { + if (textBuffer == null && textView == null) + { + return null; + } + return new SuggestedActionsSource(this, textView, textBuffer, _diffFactory, _diffBufferFactory, + _bufferFactory, _textEditorFactoryService); + } + } +} diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs index 0dadde4b..6ab7b955 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs @@ -171,7 +171,7 @@ public IEnumerable FilterEventNames(string typeName, string? propName, public MetadataHelper Helper { get; set; } = new MetadataHelper(); - private static Dictionary GetNamespaceAliases(string xml) + public static Dictionary GetNamespaceAliases(string xml) { var rv = new Dictionary(); try