|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Xaml;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.Xaml.Features.Completion;
using Microsoft.CodeAnalysis.Extensions;
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
/// <summary>
/// Handle a completion request.
/// </summary>
[ExportStatelessXamlLspService(typeof(CompletionHandler)), Shared]
[Method(Methods.TextDocumentCompletionName)]
internal class CompletionHandler : ILspServiceDocumentRequestHandler<CompletionParams, CompletionList?>
{
private const string CreateEventHandlerCommandTitle = "Create Event Handler";
private static readonly Command s_retriggerCompletionCommand = new Command()
{
CommandIdentifier = StringConstants.RetriggerCompletionCommand,
Title = "Re-trigger completions"
};
public bool MutatesSolutionState => false;
public bool RequiresLSPSolution => true;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompletionHandler()
{
}
public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request) => request.TextDocument;
public async Task<CompletionList?> HandleRequestAsync(CompletionParams request, RequestContext context, CancellationToken cancellationToken)
{
if (request.Context is VSInternalCompletionContext completionContext && completionContext.InvokeKind == VSInternalCompletionInvokeKind.Deletion)
{
// Don't trigger completions on backspace.
return null;
}
var document = context.Document;
if (document == null)
{
return null;
}
var completionService = document.Project.Services.GetRequiredService<IXamlCompletionService>();
var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var offset = text.Lines.GetPosition(ProtocolConversions.PositionToLinePosition(request.Position));
var completionResult = await completionService.GetCompletionsAsync(new XamlCompletionContext(document, offset, request.Context?.TriggerCharacter?.FirstOrDefault() ?? '\0'), cancellationToken: cancellationToken).ConfigureAwait(false);
if (completionResult?.Completions == null)
{
return null;
}
var commitCharactersCache = new Dictionary<XamlCompletionKind, ImmutableArray<VSInternalCommitCharacter>>();
return new VSInternalCompletionList
{
Items = completionResult.Completions.Select(c => CreateCompletionItem(c, document.Id, text, request.Position, request.TextDocument, commitCharactersCache)).ToArray(),
SuggestionMode = false,
};
}
private static CompletionItem CreateCompletionItem(XamlCompletionItem xamlCompletion, DocumentId documentId, SourceText text, Position position, TextDocumentIdentifier textDocument, Dictionary<XamlCompletionKind, ImmutableArray<VSInternalCommitCharacter>> commitCharactersCach)
{
var item = new VSInternalCompletionItem
{
Label = xamlCompletion.DisplayText,
VsCommitCharacters = GetCommitCharacters(xamlCompletion, commitCharactersCach),
Detail = xamlCompletion.Detail,
InsertText = xamlCompletion.InsertText,
Preselect = xamlCompletion.Preselect.GetValueOrDefault(),
SortText = xamlCompletion.SortText,
FilterText = xamlCompletion.FilterText,
Kind = GetItemKind(xamlCompletion.Kind),
Description = xamlCompletion.Description.ToLSPElement(),
Icon = xamlCompletion.Icon.ToLSPImageElement(),
InsertTextFormat = xamlCompletion.IsSnippet ? InsertTextFormat.Snippet : InsertTextFormat.Plaintext,
Data = new CompletionResolveData { ProjectGuid = documentId.ProjectId.Id, DocumentGuid = documentId.Id, Position = position, DisplayText = xamlCompletion.DisplayText }
};
if (xamlCompletion.Span.HasValue)
{
item.TextEdit = new TextEdit
{
NewText = xamlCompletion.InsertText,
Range = ProtocolConversions.LinePositionToRange(text.Lines.GetLinePositionSpan(xamlCompletion.Span.Value))
};
}
if (xamlCompletion.EventDescription.HasValue)
{
item.Command = new Command()
{
CommandIdentifier = StringConstants.CreateEventHandlerCommand,
Arguments = [textDocument, xamlCompletion.EventDescription],
Title = CreateEventHandlerCommandTitle
};
}
else if (xamlCompletion.RetriggerCompletion)
{
// Retriger completion after commit
item.Command = s_retriggerCompletionCommand;
}
return item;
}
private static SumType<string[], VSInternalCommitCharacter[]> GetCommitCharacters(XamlCompletionItem completionItem, Dictionary<XamlCompletionKind, ImmutableArray<VSInternalCommitCharacter>> commitCharactersCache)
{
if (!completionItem.XamlCommitCharacters.HasValue)
{
return completionItem.CommitCharacters;
}
if (commitCharactersCache.TryGetValue(completionItem.Kind, out var cachedCharacters))
{
// If we have already cached the commit characters, return the cached ones
return cachedCharacters.ToArray();
}
var xamlCommitCharacters = completionItem.XamlCommitCharacters.Value;
var commitCharacters = xamlCommitCharacters.Characters.Select(c => new VSInternalCommitCharacter { Character = c.ToString(), Insert = !xamlCommitCharacters.NonInsertCharacters.Contains(c) }).ToImmutableArray();
commitCharactersCache.Add(completionItem.Kind, commitCharacters);
return commitCharacters.ToArray();
}
private static CompletionItemKind GetItemKind(XamlCompletionKind kind)
{
switch (kind)
{
case XamlCompletionKind.Element:
case XamlCompletionKind.ElementName:
return CompletionItemKind.Element;
case XamlCompletionKind.EndTag:
return CompletionItemKind.CloseElement;
case XamlCompletionKind.Attribute:
case XamlCompletionKind.AttachedPropertyValue:
case XamlCompletionKind.ConditionalArgument:
case XamlCompletionKind.DataBoundProperty:
case XamlCompletionKind.MarkupExtensionParameter:
case XamlCompletionKind.PropertyElement:
return CompletionItemKind.Property;
case XamlCompletionKind.ConditionValue:
case XamlCompletionKind.MarkupExtensionValue:
case XamlCompletionKind.PropertyValue:
case XamlCompletionKind.Value:
return CompletionItemKind.Value;
case XamlCompletionKind.Event:
case XamlCompletionKind.EventHandlerDescription:
return CompletionItemKind.Event;
case XamlCompletionKind.NamespaceValue:
case XamlCompletionKind.Prefix:
return CompletionItemKind.Namespace;
case XamlCompletionKind.AttachedPropertyTypePrefix:
case XamlCompletionKind.MarkupExtensionClass:
case XamlCompletionKind.Type:
case XamlCompletionKind.TypePrefix:
return CompletionItemKind.Class;
case XamlCompletionKind.LocalResource:
return CompletionItemKind.LocalResource;
case XamlCompletionKind.SystemResource:
return CompletionItemKind.SystemResource;
case XamlCompletionKind.CData:
case XamlCompletionKind.Comment:
case XamlCompletionKind.ProcessingInstruction:
case XamlCompletionKind.RegionStart:
case XamlCompletionKind.RegionEnd:
return CompletionItemKind.Keyword;
case XamlCompletionKind.Snippet:
return CompletionItemKind.Snippet;
default:
Debug.Fail($"Unhandled {nameof(XamlCompletionKind)}: {Enum.GetName(typeof(XamlCompletionKind), kind)}");
return CompletionItemKind.Text;
}
}
}
}
|