Skip to content

Commit

Permalink
Merge pull request #408 from AvaloniaUI/namespace-suggestions
Browse files Browse the repository at this point in the history
Add support for Namespace suggestions
  • Loading branch information
Takoooooo authored Oct 30, 2023
2 parents c3dabfe + 1155221 commit 2aba1a2
Show file tree
Hide file tree
Showing 14 changed files with 557 additions and 21 deletions.
9 changes: 9 additions & 0 deletions AvaloniaVS.Shared/AvaloniaVS.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Converters\EnumValuesConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Converters\NotNullOrEmptyToVisibilityConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Constants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IntelliSense\CompletionEngineSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IntelliSense\TextChangeAdapter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IntelliSense\XamlCompletion.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IntelliSense\XamlCompletionCommandHandler.cs" />
Expand All @@ -39,6 +40,13 @@
<Compile Include="$(MSBuildThisFileDirectory)Services\PreviewerProcess.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\SolutionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\Throttle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\Base\BaseSuggestedAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\MissingAliasSuggestedAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\MissingNamespaceAndAliasSuggestedAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\MissingNamespaceSuggestedAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Helpers\PreviewProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\SuggestedActionsSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\SuggestedActionsSourceProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TaskExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TextViewExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\FrameworkInfoUtils.cs" />
Expand Down Expand Up @@ -94,5 +102,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Commands\" />
<Folder Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\Base\" />
</ItemGroup>
</Project>
15 changes: 15 additions & 0 deletions AvaloniaVS.Shared/IntelliSense/CompletionEngineSource.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -46,7 +50,8 @@ public void VsTextViewCreated(IVsTextView textViewAdapter)
_serviceProvider,
_completionBroker,
textView,
textViewAdapter));
textViewAdapter,
_completionEngineSource.CompletionEngine));
}
}
}
Expand Down
15 changes: 6 additions & 9 deletions AvaloniaVS.Shared/IntelliSense/XamlCompletionSource.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,36 @@
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<CompletionSet> completionSets)
{
if (_buffer.Properties.TryGetProperty<XamlBufferMetadata>(typeof(XamlBufferMetadata), out var metadata) &&
metadata.CompletionMetadata != null)
{
session.TextView.Properties.TryGetProperty<XamlCompletionCommandHandler>(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)
{
Expand Down
14 changes: 9 additions & 5 deletions AvaloniaVS.Shared/IntelliSense/XamlCompletionSourceProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
{
return Task.FromResult<IEnumerable<SuggestedActionSet>>(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<string, string> 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<object> GetPreviewAsync(CancellationToken cancellationToken)
{
return Task.FromResult<object>(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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<string, string> _targetClassMetadata;
private readonly IWpfDifferenceViewerFactoryService _diffFactory;
private readonly IDifferenceBufferFactoryService _diffBufferFactory;
private readonly ITextBufferFactoryService _bufferFactory;
private readonly Dictionary<string, string> _aliases;
private readonly ITextViewRoleSet _previewRoleSet;

public MissingNamespaceAndAliasSuggestedAction(ITrackingSpan span, IWpfDifferenceViewerFactoryService diffFactory,
IDifferenceBufferFactoryService diffBufferFactory, ITextBufferFactoryService bufferFactory, ITextEditorFactoryService textEditorFactoryService,
IReadOnlyDictionary<string, string> inverseNamespaces, Dictionary<string, string> 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<object> GetPreviewAsync(CancellationToken cancellationToken)
{
return Task.FromResult<object>(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}\"");
}

}
}
Loading

0 comments on commit 2aba1a2

Please sign in to comment.