|
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.BraceCompletion;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.Completion.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[ExportCSharpVisualBasicStatelessLspService(typeof(OnAutoInsertHandler)), Shared]
[Method(LSP.VSInternalMethods.OnAutoInsertName)]
internal sealed class OnAutoInsertHandler : ILspServiceDocumentRequestHandler<LSP.VSInternalDocumentOnAutoInsertParams, LSP.VSInternalDocumentOnAutoInsertResponseItem?>
{
private readonly ImmutableArray<IBraceCompletionService> _csharpBraceCompletionServices;
private readonly ImmutableArray<IBraceCompletionService> _visualBasicBraceCompletionServices;
private readonly IGlobalOptionService _globalOptions;
public bool MutatesSolutionState => false;
public bool RequiresLSPSolution => true;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public OnAutoInsertHandler(
[ImportMany(LanguageNames.CSharp)] IEnumerable<IBraceCompletionService> csharpBraceCompletionServices,
[ImportMany(LanguageNames.VisualBasic)] IEnumerable<IBraceCompletionService> visualBasicBraceCompletionServices,
IGlobalOptionService globalOptions)
{
_csharpBraceCompletionServices = csharpBraceCompletionServices.ToImmutableArray();
_visualBasicBraceCompletionServices = visualBasicBraceCompletionServices.ToImmutableArray();
_globalOptions = globalOptions;
}
public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.VSInternalDocumentOnAutoInsertParams request) => request.TextDocument;
public async Task<LSP.VSInternalDocumentOnAutoInsertResponseItem?> HandleRequestAsync(
LSP.VSInternalDocumentOnAutoInsertParams request,
RequestContext context,
CancellationToken cancellationToken)
{
var document = context.Document;
if (document == null)
return null;
var service = document.GetRequiredLanguageService<IDocumentationCommentSnippetService>();
// We should use the options passed in by LSP instead of the document's options.
var formattingOptions = await ProtocolConversions.GetFormattingOptionsAsync(request.Options, document, _globalOptions, cancellationToken).ConfigureAwait(false);
// The editor calls this handler for C# and VB comment characters, but we only need to process the one for the language that matches the document
if (request.Character == "\n" || request.Character == service.DocumentationCommentCharacter)
{
var docCommentOptions = _globalOptions.GetDocumentationCommentOptions(formattingOptions.LineFormatting, document.Project.Language);
var documentationCommentResponse = await GetDocumentationCommentResponseAsync(
request, document, service, docCommentOptions, cancellationToken).ConfigureAwait(false);
if (documentationCommentResponse != null)
{
return documentationCommentResponse;
}
}
// Only support this for razor as LSP doesn't support overtype yet.
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1165179/
// Once LSP supports overtype we can move all of brace completion to LSP.
if (request.Character == "\n" && context.ServerKind == WellKnownLspServerKinds.RazorLspServer)
{
var indentationOptions = new IndentationOptions(formattingOptions)
{
AutoFormattingOptions = _globalOptions.GetAutoFormattingOptions(document.Project.Language)
};
var braceCompletionAfterReturnResponse = await GetBraceCompletionAfterReturnResponseAsync(
request, document, indentationOptions, cancellationToken).ConfigureAwait(false);
if (braceCompletionAfterReturnResponse != null)
{
return braceCompletionAfterReturnResponse;
}
}
return null;
}
private static async Task<LSP.VSInternalDocumentOnAutoInsertResponseItem?> GetDocumentationCommentResponseAsync(
LSP.VSInternalDocumentOnAutoInsertParams autoInsertParams,
Document document,
IDocumentationCommentSnippetService service,
DocumentationCommentOptions options,
CancellationToken cancellationToken)
{
var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var linePosition = ProtocolConversions.PositionToLinePosition(autoInsertParams.Position);
var position = sourceText.Lines.GetPosition(linePosition);
var result = autoInsertParams.Character == "\n"
? service.GetDocumentationCommentSnippetOnEnterTyped(syntaxTree, sourceText, position, options, cancellationToken)
: service.GetDocumentationCommentSnippetOnCharacterTyped(syntaxTree, sourceText, position, options, cancellationToken);
if (result == null)
{
return null;
}
return new LSP.VSInternalDocumentOnAutoInsertResponseItem
{
TextEditFormat = LSP.InsertTextFormat.Snippet,
TextEdit = new LSP.TextEdit
{
NewText = result.SnippetText.Insert(result.CaretOffset, "$0"),
Range = ProtocolConversions.TextSpanToRange(result.SpanToReplace, sourceText)
}
};
}
private async Task<LSP.VSInternalDocumentOnAutoInsertResponseItem?> GetBraceCompletionAfterReturnResponseAsync(
LSP.VSInternalDocumentOnAutoInsertParams autoInsertParams,
Document document,
IndentationOptions options,
CancellationToken cancellationToken)
{
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var position = sourceText.Lines.GetPosition(ProtocolConversions.PositionToLinePosition(autoInsertParams.Position));
var serviceAndContext = await GetBraceCompletionContextAsync(position, document, cancellationToken).ConfigureAwait(false);
if (serviceAndContext == null)
{
return null;
}
var (service, context) = serviceAndContext.Value;
var postReturnEdit = service.GetTextChangeAfterReturn(context, options, cancellationToken);
if (postReturnEdit == null)
{
return null;
}
var textChanges = postReturnEdit.Value.TextChanges;
var desiredCaretLinePosition = postReturnEdit.Value.CaretLocation;
var newSourceText = sourceText.WithChanges(textChanges);
var caretLine = newSourceText.Lines[desiredCaretLinePosition.Line];
if (desiredCaretLinePosition.Character > caretLine.Span.Length)
{
if (caretLine.Span.IsEmpty)
{
// We have an empty line with the caret column at an indented position, let's add whitespace indentation to the text.
var indentedText = GetIndentedText(newSourceText, caretLine, desiredCaretLinePosition, options);
// Get the overall text changes between the original text and the formatted + indented text.
textChanges = indentedText.GetTextChanges(sourceText).ToImmutableArray();
newSourceText = indentedText;
// If tabs were inserted the desired caret column can remain beyond the line text.
// So just set the caret position to the end of the newly indented line.
var caretLineInIndentedText = indentedText.Lines[desiredCaretLinePosition.Line];
desiredCaretLinePosition = indentedText.Lines.GetLinePosition(caretLineInIndentedText.End);
}
else
{
// We're not on an empty line, clamp the line position to the actual line end.
desiredCaretLinePosition = new LinePosition(desiredCaretLinePosition.Line, Math.Min(desiredCaretLinePosition.Character, caretLine.End));
}
}
var textChange = await GetCollapsedChangeAsync(textChanges, document, cancellationToken).ConfigureAwait(false);
var newText = GetTextChangeTextWithCaretAtLocation(newSourceText, textChange, desiredCaretLinePosition);
var autoInsertChange = new LSP.VSInternalDocumentOnAutoInsertResponseItem
{
TextEditFormat = LSP.InsertTextFormat.Snippet,
TextEdit = new LSP.TextEdit
{
NewText = newText,
Range = ProtocolConversions.TextSpanToRange(textChange.Span, sourceText)
}
};
return autoInsertChange;
static SourceText GetIndentedText(
SourceText textToIndent,
TextLine lineToIndent,
LinePosition desiredCaretLinePosition,
IndentationOptions options)
{
// Indent by the amount needed to make the caret line contain the desired indentation column.
var amountToIndent = desiredCaretLinePosition.Character - lineToIndent.Span.Length;
// Create and apply a text change with whitespace for the indentation amount.
var indentText = amountToIndent.CreateIndentationString(options.FormattingOptions.UseTabs, options.FormattingOptions.TabSize);
var indentedText = textToIndent.WithChanges(new TextChange(new TextSpan(lineToIndent.End, 0), indentText));
return indentedText;
}
static async Task<TextChange> GetCollapsedChangeAsync(ImmutableArray<TextChange> textChanges, Document oldDocument, CancellationToken cancellationToken)
{
var documentText = await oldDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
documentText = documentText.WithChanges(textChanges);
return Collapse(documentText, textChanges);
}
static string GetTextChangeTextWithCaretAtLocation(SourceText sourceText, TextChange textChange, LinePosition desiredCaretLinePosition)
{
var desiredCaretLocation = sourceText.Lines.GetPosition(desiredCaretLinePosition);
Debug.Assert(desiredCaretLocation >= textChange.Span.Start);
var offsetInTextChange = desiredCaretLocation - textChange.Span.Start;
var newText = textChange.NewText!.Insert(offsetInTextChange, "$0");
return newText;
}
}
private async Task<(IBraceCompletionService Service, BraceCompletionContext Context)?> GetBraceCompletionContextAsync(int caretLocation, Document document, CancellationToken cancellationToken)
{
var servicesForDocument = document.Project.Language switch
{
LanguageNames.CSharp => _csharpBraceCompletionServices,
LanguageNames.VisualBasic => _visualBasicBraceCompletionServices,
_ => throw new ArgumentException($"Language {document.Project.Language} is not recognized for OnAutoInsert")
};
var parsedDocument = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
foreach (var service in servicesForDocument)
{
var context = service.GetCompletedBraceContext(parsedDocument, caretLocation);
if (context != null)
{
return (service, context.Value);
}
}
return null;
}
}
}
|