Пошаговое руководство. Отображение завершения операторов

Вы можете реализовать завершение инструкции на основе языка, определив идентификаторы, для которых требуется предоставить завершение, а затем активировав сеанс завершения. Вы можете определить завершение инструкции в контексте языковой службы, определить собственное расширение имени файла и тип контента, а затем отобразить завершение только для этого типа. Кроме того, можно активировать завершение для существующего типа контента, например "обычный текст". В этом пошаговом руководстве показано, как активировать завершение инструкции для типа контента "обычный текст", который является типом контента текстовых файлов. Тип контента "text" является предком всех других типов контента, включая код и XML-файлы.

Завершение инструкции обычно активируется путем ввода определенных символов, например путем ввода начала идентификатора, например using. Обычно оно закрывается путем нажатия клавиши ПРОБЕЛ, ВКЛАДКА или ВВОД , чтобы зафиксировать выделение. Вы можете реализовать функции IntelliSense, которые активируются при вводе символа с помощью обработчика команд для нажатий клавиш ( IOleCommandTarget интерфейса) и поставщика обработчика, реализующего IVsTextViewCreationListener интерфейс. Чтобы создать источник завершения, который представляет собой список идентификаторов, участвующих в завершении, реализуйте ICompletionSource интерфейс и поставщик источника завершения ( ICompletionSourceProvider интерфейс). Поставщики являются компонентами компонента Managed Extensibility Framework (MEF). Они отвечают за экспорт классов исходного и контроллера и импорта служб и брокеров, например ITextStructureNavigatorSelectorService, навигации в текстовом буфере, а также ICompletionBrokerактивации сеанса завершения.

В этом пошаговом руководстве показано, как реализовать завершение инструкции для жестко закодированного набора идентификаторов. В полных реализациях языковая служба и языковая документация отвечают за предоставление этого содержимого.

Создание проекта MEF

Создание проекта MEF

  1. Создайте проект VSIX на C#. (В Диалоговое окно "Новый проект" , выберите Visual C# / Расширяемость, а затем ПРОЕКТ VSIX.) Назовите решение CompletionTest.

  2. Добавьте в проект шаблон элемента классификатора редактора. Дополнительные сведения: Создание расширения с помощью шаблона элемента редактора.

  3. Удалите файлы существующих классов.

  4. Добавьте следующие ссылки в проект и убедитесь, что copyLocal имеет значение false:

    Microsoft.VisualStudio.Editor

    Microsoft.VisualStudio.Language.Intellisense

    Microsoft.VisualStudio.OLE.Interop

    Microsoft.VisualStudio.Shell.15.0

    Microsoft.VisualStudio.Shell.Immutable.10.0

    Microsoft.VisualStudio.TextManager.Interop

Реализация источника завершения

Источник завершения отвечает за сбор набора идентификаторов и добавление содержимого в окно завершения, когда пользователь вводит триггер завершения, например первые буквы идентификатора. В этом примере идентификаторы и их описания жестко закодируются в методе AugmentCompletionSession . В большинстве реальных версий используется средство синтаксического анализа языка, чтобы получить маркеры для заполнения списка завершения.

Реализация источника завершения

  1. Добавьте файл класса с именем TestCompletionSource.

  2. Добавьте следующие импорты:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio.Utilities;
    
  3. Измените объявление класса таким TestCompletionSource образом, чтобы он реализовал ICompletionSource:

    internal class TestCompletionSource : ICompletionSource
    
  4. Добавьте частные поля для исходного поставщика, текстового буфера Completion и списка объектов (которые соответствуют идентификаторам, которые будут участвовать в сеансе завершения):

    private TestCompletionSourceProvider m_sourceProvider;
    private ITextBuffer m_textBuffer;
    private List<Completion> m_compList;
    
  5. Добавьте конструктор, который задает исходный поставщик и буфер. Класс TestCompletionSourceProvider определяется в последующих шагах:

    public TestCompletionSource(TestCompletionSourceProvider sourceProvider, ITextBuffer textBuffer)
    {
        m_sourceProvider = sourceProvider;
        m_textBuffer = textBuffer;
    }
    
  6. AugmentCompletionSession Реализуйте метод, добавив набор завершения, содержащий завершения, которые необходимо предоставить в контексте. Каждый набор завершения содержит набор завершений Completion и соответствует вкладке окна завершения. (В проектах Visual Basic вкладки окна завершения называются Common and All.) Метод FindTokenSpanAtPosition определен на следующем шаге.

    void ICompletionSource.AugmentCompletionSession(ICompletionSession session, IList<CompletionSet> completionSets)
    {
        List<string> strList = new List<string>();
        strList.Add("addition");
        strList.Add("adaptation");
        strList.Add("subtraction");
        strList.Add("summation");
        m_compList = new List<Completion>();
        foreach (string str in strList)
            m_compList.Add(new Completion(str, str, str, null, null));
    
        completionSets.Add(new CompletionSet(
            "Tokens",    //the non-localized title of the tab
            "Tokens",    //the display title of the tab
            FindTokenSpanAtPosition(session.GetTriggerPoint(m_textBuffer),
                session),
            m_compList,
            null));
    }
    
  7. Следующий метод используется для поиска текущего слова из позиции курсора:

    private ITrackingSpan FindTokenSpanAtPosition(ITrackingPoint point, ICompletionSession session)
    {
        SnapshotPoint currentPoint = (session.TextView.Caret.Position.BufferPosition) - 1;
        ITextStructureNavigator navigator = m_sourceProvider.NavigatorService.GetTextStructureNavigator(m_textBuffer);
        TextExtent extent = navigator.GetExtentOfWord(currentPoint);
        return currentPoint.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive);
    }
    
  8. Dispose() Реализуйте метод:

    private bool m_isDisposed;
    public void Dispose()
    {
        if (!m_isDisposed)
        {
            GC.SuppressFinalize(this);
            m_isDisposed = true;
        }
    }
    

Реализация поставщика источника завершения

Поставщик источника завершения — это часть компонента MEF, которая создает экземпляр источника завершения.

Реализация поставщика источника завершения

  1. Добавьте класс с именем TestCompletionSourceProvider , который реализует ICompletionSourceProvider. Экспортируйте этот класс с ContentTypeAttribute помощью обычного текста и NameAttribute завершения теста.

    [Export(typeof(ICompletionSourceProvider))]
    [ContentType("plaintext")]
    [Name("token completion")]
    internal class TestCompletionSourceProvider : ICompletionSourceProvider
    
  2. ITextStructureNavigatorSelectorServiceИмпортируйте слово, которое находит текущее слово в источнике завершения.

    [Import]
    internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
    
  3. TryCreateCompletionSource Реализуйте метод для создания экземпляра источника завершения.

    public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer)
    {
        return new TestCompletionSource(this, textBuffer);
    }
    

Реализация поставщика обработчика команд завершения

Поставщик обработчика команд завершения является производным от IVsTextViewCreationListenerсобытия создания текстового представления и преобразует представление из IVsTextView/>, что позволяет добавить команду в цепочку команд оболочки Visual Studio.ITextView Так как этот класс является экспортом MEF, его также можно использовать для импорта служб, необходимых самому обработчику команд.

Реализация поставщика обработчика команд завершения

  1. Добавьте файл с именем TestCompletionCommandHandler.

  2. Добавьте следующие директивы using:

    using System;
    using System.ComponentModel.Composition;
    using System.Runtime.InteropServices;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.VisualStudio.Utilities;
    
  3. Добавьте класс с именем TestCompletionHandlerProvider , который реализует IVsTextViewCreationListener. Экспортируйте этот класс с NameAttribute помощью обработчика завершения маркера, ContentTypeAttribute открытого текста и а TextViewRoleAttributeEditable.

    [Export(typeof(IVsTextViewCreationListener))]
    [Name("token completion handler")]
    [ContentType("plaintext")]
    [TextViewRole(PredefinedTextViewRoles.Editable)]
    internal class TestCompletionHandlerProvider : IVsTextViewCreationListener
    
  4. IVsEditorAdaptersFactoryServiceИмпортируйте , который позволяет преобразовать из a IVsTextViewITextView, a, a ICompletionBrokerи aSVsServiceProvider, что обеспечивает доступ к стандартным службам Visual Studio.

    [Import]
    internal IVsEditorAdaptersFactoryService AdapterService = null;
    [Import]
    internal ICompletionBroker CompletionBroker { get; set; }
    [Import]
    internal SVsServiceProvider ServiceProvider { get; set; }
    
  5. VsTextViewCreated Реализуйте метод для создания экземпляра обработчика команд.

    public void VsTextViewCreated(IVsTextView textViewAdapter)
    {
        ITextView textView = AdapterService.GetWpfTextView(textViewAdapter);
        if (textView == null)
            return;
    
        Func<TestCompletionCommandHandler> createCommandHandler = delegate() { return new TestCompletionCommandHandler(textViewAdapter, textView, this); };
        textView.Properties.GetOrCreateSingletonProperty(createCommandHandler);
    }
    

Реализация обработчика команд завершения

Так как завершение инструкции активируется нажатием клавиш, необходимо реализовать IOleCommandTarget интерфейс для получения и обработки нажатий клавиш, которые активируют, фиксируют и закрывает сеанс завершения.

Реализация обработчика команд завершения

  1. Добавьте класс с именем TestCompletionCommandHandler , реализующим IOleCommandTarget:

    internal class TestCompletionCommandHandler : IOleCommandTarget
    
  2. Добавьте частные поля для следующего обработчика команд (в которое передается команда), текстовое представление, поставщик обработчика команд (который обеспечивает доступ к различным службам) и сеанс завершения:

    private IOleCommandTarget m_nextCommandHandler;
    private ITextView m_textView;
    private TestCompletionHandlerProvider m_provider;
    private ICompletionSession m_session;
    
  3. Добавьте конструктор, который задает текстовое представление и поля поставщика, и добавляет команду в цепочку команд:

    internal TestCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, TestCompletionHandlerProvider provider)
    {
        this.m_textView = textView;
        this.m_provider = provider;
    
        //add the command to the command chain
        textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler);
    }
    
  4. Реализуйте метод, передав команду следующим способом QueryStatus :

    public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
    {
        return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
    }
    
  5. Реализуйте метод Exec. Когда этот метод получает нажатие клавиши, он должен выполнить одно из следующих действий:

    • Разрешить запись символа в буфер, а затем активировать или отфильтровать его. (Печать символов делает это.)

    • Зафиксируйте завершение, но не разрешайте запись символа в буфер. (Пробелы, Вкладка и ввод выполняются при отображении сеанса завершения.)

    • Разрешить передаче команды следующему обработчику. (Все другие команды.)

      Так как этот метод может отображать пользовательский интерфейс, вызов IsInAutomationFunction , чтобы убедиться, что он не вызывается в контексте автоматизации:

      public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
      {
          if (VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider))
          {
              return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
          }
          //make a copy of this so we can look at it after forwarding some commands
          uint commandID = nCmdID;
          char typedChar = char.MinValue;
          //make sure the input is a char before getting it
          if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)
          {
              typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
          }
      
          //check for a commit character
          if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN
              || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB
              || (char.IsWhiteSpace(typedChar) || char.IsPunctuation(typedChar)))
          {
              //check for a selection
              if (m_session != null && !m_session.IsDismissed)
              {
                  //if the selection is fully selected, commit the current session
                  if (m_session.SelectedCompletionSet.SelectionStatus.IsSelected)
                  {
                      m_session.Commit();
                      //also, don't add the character to the buffer
                      return VSConstants.S_OK;
                  }
                  else
                  {
                      //if there is no selection, dismiss the session
                      m_session.Dismiss();
                  }
              }
          }
      
          //pass along the command so the char is added to the buffer
          int retVal = m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
          bool handled = false;
          if (!typedChar.Equals(char.MinValue) && char.IsLetterOrDigit(typedChar))
          {
              if (m_session == null || m_session.IsDismissed) // If there is no active session, bring up completion
              {
                  this.TriggerCompletion();
                  m_session.Filter();
              }
              else    //the completion session is already active, so just filter
              {
                  m_session.Filter();
              }
              handled = true;
          }
          else if (commandID == (uint)VSConstants.VSStd2KCmdID.BACKSPACE   //redo the filter if there is a deletion
              || commandID == (uint)VSConstants.VSStd2KCmdID.DELETE)
          {
              if (m_session != null && !m_session.IsDismissed)
                  m_session.Filter();
              handled = true;
          }
          if (handled) return VSConstants.S_OK;
          return retVal;
      }
      

  6. Этот код является частным методом, который активирует сеанс завершения:

    private bool TriggerCompletion()
    {
        //the caret must be in a non-projection location 
        SnapshotPoint? caretPoint =
        m_textView.Caret.Position.Point.GetPoint(
        textBuffer => (!textBuffer.ContentType.IsOfType("projection")), PositionAffinity.Predecessor);
        if (!caretPoint.HasValue)
        {
            return false;
        }
    
        m_session = m_provider.CompletionBroker.CreateCompletionSession
     (m_textView,
            caretPoint.Value.Snapshot.CreateTrackingPoint(caretPoint.Value.Position, PointTrackingMode.Positive),
            true);
    
        //subscribe to the Dismissed event on the session 
        m_session.Dismissed += this.OnSessionDismissed;
        m_session.Start();
    
        return true;
    }
    
  7. Следующий пример — частный метод, который отменяет подписку Dismissed из события:

    private void OnSessionDismissed(object sender, EventArgs e)
    {
        m_session.Dismissed -= this.OnSessionDismissed;
        m_session = null;
    }
    

Сборка и проверка кода

Чтобы проверить этот код, создайте решение CompletionTest и запустите его в экспериментальном экземпляре.

Создание и тестирование решения CompletionTest

  1. Постройте решение.

  2. При запуске этого проекта в отладчике запускается второй экземпляр Visual Studio.

  3. Создайте текстовый файл и введите текст, содержащий слово "добавить".

  4. При вводе первого "a" и "d" должен появиться список, содержащий "дополнение" и "адаптация". Обратите внимание, что выбрано добавление. При вводе другого "d" список должен содержать только "дополнение", которое теперь выбрано. Можно зафиксировать "дополнение", нажав клавишу ПРОБЕЛ, ВКЛАДКА или ВВОД , или закрыть список, введя клавишу ESC или любой другой ключ.