|
// 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.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Snippets;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Editor.Expansion;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Snippets
{
using Workspace = Microsoft.CodeAnalysis.Workspace;
internal abstract class AbstractSnippetCommandHandler :
ICommandHandler<TabKeyCommandArgs>,
ICommandHandler<BackTabKeyCommandArgs>,
ICommandHandler<ReturnKeyCommandArgs>,
ICommandHandler<EscapeKeyCommandArgs>,
ICommandHandler<InsertSnippetCommandArgs>
{
protected readonly IThreadingContext ThreadingContext;
protected readonly IExpansionServiceProvider ExpansionServiceProvider;
protected readonly IExpansionManager ExpansionManager;
protected readonly EditorOptionsService EditorOptionsService;
public string DisplayName => FeaturesResources.Snippets;
public AbstractSnippetCommandHandler(
IThreadingContext threadingContext,
IExpansionServiceProvider expansionServiceProvider,
IExpansionManager expansionManager,
EditorOptionsService editorOptionsService)
{
ThreadingContext = threadingContext;
ExpansionServiceProvider = expansionServiceProvider;
ExpansionManager = expansionManager;
EditorOptionsService = editorOptionsService;
}
protected abstract AbstractSnippetExpansionClient GetSnippetExpansionClient(ITextView textView, ITextBuffer subjectBuffer);
protected abstract bool IsSnippetExpansionContext(Document document, int startPosition, CancellationToken cancellationToken);
protected abstract bool TryInvokeInsertionUI(ITextView textView, ITextBuffer subjectBuffer, bool surroundWith = false);
protected virtual bool TryInvokeSnippetPickerOnQuestionMark(ITextView textView, ITextBuffer textBuffer)
{
return false;
}
public bool ExecuteCommand(TabKeyCommandArgs args, CommandExecutionContext context)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return false;
}
if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
snippetExpansionClient.TryHandleTab())
{
return true;
}
// Insert snippet/show picker only if we don't have a selection: the user probably wants to indent instead
if (args.TextView.Selection.IsEmpty)
{
if (TryHandleTypedSnippet(args.TextView, args.SubjectBuffer))
{
return true;
}
if (TryInvokeSnippetPickerOnQuestionMark(args.TextView, args.SubjectBuffer))
{
return true;
}
}
return false;
}
public CommandState GetCommandState(TabKeyCommandArgs args)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return CommandState.Unspecified;
}
if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
{
return CommandState.Unspecified;
}
return CommandState.Available;
}
public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return false;
}
if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
snippetExpansionClient.TryHandleReturn())
{
return true;
}
return false;
}
public CommandState GetCommandState(ReturnKeyCommandArgs args)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return CommandState.Unspecified;
}
if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
{
return CommandState.Unspecified;
}
return CommandState.Available;
}
public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return false;
}
if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
snippetExpansionClient.TryHandleEscape())
{
return true;
}
return false;
}
public CommandState GetCommandState(EscapeKeyCommandArgs args)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return CommandState.Unspecified;
}
if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
{
return CommandState.Unspecified;
}
return CommandState.Available;
}
public bool ExecuteCommand(BackTabKeyCommandArgs args, CommandExecutionContext context)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return false;
}
if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
snippetExpansionClient.TryHandleBackTab())
{
return true;
}
return false;
}
public CommandState GetCommandState(BackTabKeyCommandArgs args)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return CommandState.Unspecified;
}
if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
{
return CommandState.Unspecified;
}
return CommandState.Available;
}
public bool ExecuteCommand(InsertSnippetCommandArgs args, CommandExecutionContext context)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return false;
}
return TryInvokeInsertionUI(args.TextView, args.SubjectBuffer);
}
public CommandState GetCommandState(InsertSnippetCommandArgs args)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (!AreSnippetsEnabled(args))
{
return CommandState.Unspecified;
}
if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out var workspace))
{
return CommandState.Unspecified;
}
if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
{
return CommandState.Unspecified;
}
return CommandState.Available;
}
protected bool TryHandleTypedSnippet(ITextView textView, ITextBuffer subjectBuffer)
{
ThreadingContext.ThrowIfNotOnUIThread();
var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
return false;
}
var currentText = subjectBuffer.AsTextContainer().CurrentText;
var syntaxFactsService = document.GetRequiredLanguageService<ISyntaxFactsService>();
var endPositionInSubjectBuffer = textView.GetCaretPoint(subjectBuffer);
if (endPositionInSubjectBuffer == null)
{
return false;
}
var endPosition = endPositionInSubjectBuffer.Value.Position;
var startPosition = endPosition;
// Find the snippet shortcut
while (startPosition > 0)
{
var c = currentText[startPosition - 1];
if (!syntaxFactsService.IsIdentifierPartCharacter(c) && c != '#' && c != '~')
{
break;
}
startPosition--;
}
if (startPosition == endPosition)
{
return false;
}
if (!IsSnippetExpansionContext(document, startPosition, CancellationToken.None))
{
return false;
}
return GetSnippetExpansionClient(textView, subjectBuffer).TryInsertExpansion(startPosition, endPosition);
}
protected bool AreSnippetsEnabled(EditorCommandArgs args)
{
return EditorOptionsService.GlobalOptions.GetOption(SnippetsOptionsStorage.Snippets) &&
// TODO (https://github.com/dotnet/roslyn/issues/5107): enable in interactive
!(Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out var workspace) && workspace.Kind == WorkspaceKind.Interactive);
}
}
}
|