연습: 명령문 완성 표시

완성을 제공하려는 식별자를 정의하고 완성 세션을 트리거하여, 언어 기반 문 완성을 구현할 수 있습니다. 언어 서비스의 컨텍스트에서 문 완성을 정의하고, 고유한 파일 이름 확장명 및 콘텐츠 형식을 정의한 다음, 해당 형식에 대한 완성만 표시할 수 있습니다. 또는 기존 콘텐츠 형식(예: "일반 텍스트")에 대한 완성을 트리거할 수 있습니다. 이 연습에서는 텍스트 파일의 콘텐츠 형식인 "일반 텍스트" 콘텐츠 형식에 대해 문 완성을 트리거하는 방법을 보여 줍니다. "텍스트" 콘텐츠 형식은 코드 및 XML 파일을 포함한 다른 모든 콘텐츠 형식의 상위 항목입니다.

문 완성은 일반적으로 "using"과 같은 식별자의 시작을 입력하는 것처럼 특정 문자를 입력하여 트리거됩니다. 일반적으로 스페이스바, Tab 또는 Enter 키를 눌러 선택을 커밋하여 해제합니다. 키 입력에 대한 명령 처리기(IOleCommandTarget 인터페이스) 및 IVsTextViewCreationListener 인터페이스를 구현하는 처리기 공급자를 사용하여 문자를 입력할 때 트리거되는 IntelliSense 기능을 구현할 수 있습니다. 완성에 참여하는 식별자 목록인 완성 원본을 만들려면 ICompletionSource 인터페이스 및 완성 원본 공급자(ICompletionSourceProvider 인터페이스)를 구현합니다. 공급자는 MEF(Managed Extensibility Framework) 구성 요소 부분입니다. 원본 및 컨트롤러 클래스의 내보내기와, 서비스 및 브로커(예: 텍스트 버퍼에서 탐색을 사용하도록 설정하는 ITextStructureNavigatorSelectorService 및 완성 세션을 트리거하는 ICompletionBroker)의 가져오기를 담당합니다.

이 연습에서는 하드 코딩된 식별자 집합에 대해 문 완성을 구현하는 방법을 보여 줍니다. 전체 구현에서는 언어 서비스 및 언어 설명서에서 해당 콘텐츠를 제공해야 합니다.

MEF 프로젝트 만들기

MEF 프로젝트를 만들려면

  1. C# VSIX 프로젝트를 만듭니다. (새 프로젝트 대화 상자에서 Visual C#/확장성, VSIX 프로젝트를 차례로 선택합니다.) 솔루션 이름을 CompletionTest로 지정합니다.

  2. 프로젝트에 편집기 분류자 항목 템플릿을 추가합니다. 자세한 내용은 편집기 항목 템플릿을 사용하여 확장 만들기를 참조하세요.

  3. 기존 클래스 파일을 삭제합니다.

  4. 프로젝트에 다음 참조를 추가하고 CopyLocalfalse로 설정되었는지 확인합니다.

    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. ICompletionSource를 구현할 수 있게 TestCompletionSource에 대한 클래스 선언을 수정합니다.

    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 프로젝트에서 완성 창 탭의 이름은 CommonAll입니다. 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. ICompletionSourceProvider를 구현하는 TestCompletionSourceProvider라는 클래스를 추가합니다. "일반 텍스트"의 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);
    }
    

완성 명령 처리기 공급자 구현

완성 명령 처리기 공급자는 텍스트 보기 만들기 이벤트를 수신 대기하고 Visual Studio 셸의 명령 체인에 명령을 추가할 수 있게 IVsTextView에서 뷰를 변환하는 IVsTextViewCreationListener에서 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. IVsTextViewCreationListener를 구현하는 TestCompletionHandlerProvider라는 클래스를 추가합니다. "토큰 완성 처리기"의 NameAttribute, "일반 텍스트"의 ContentTypeAttributeEditableTextViewRoleAttribute를 사용하여 이 클래스를 내보냅니다.

    [Export(typeof(IVsTextViewCreationListener))]
    [Name("token completion handler")]
    [ContentType("plaintext")]
    [TextViewRole(PredefinedTextViewRoles.Editable)]
    internal class TestCompletionHandlerProvider : IVsTextViewCreationListener
    
  4. IVsTextViewITextView로 변환할 수 있게 하는 IVsEditorAdaptersFactoryService와, ICompletionBroker 및 표준 Visual Studio 서비스에 액세스할 수 있게 하는 SVsServiceProvider를 가져옵니다.

    [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. IOleCommandTarget을 구현하는 TestCompletionCommandHandler라는 클래스를 추가합니다.

    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 메서드를 구현합니다. 이 메서드는 키 입력을 받으면 다음 중 하나를 수행해야 합니다.

    • 문자를 버퍼에 쓸 수 있도록 허용한 다음, 완성을 트리거하거나 필터링합니다. (인쇄 문자가 이 작업을 수행합니다.)

    • 완성을 커밋하지만 버퍼에 문자를 쓸 수 없습니다. (완성 세션이 표시되면 공백, Enter에서 이 작업을 수행합니다.)

    • 명령이 다음 처리기에 전달되도록 허용합니다. (다른 모든 명령)

      이 메서드는 UI를 표시할 수 있으므로 자동화 컨텍스트에서 호출되지 않도록 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. 텍스트 파일을 만들고 단어 "add"가 포함된 텍스트를 입력합니다.

  4. 먼저 "a"를 입력한 다음, "d"를 입력하면 "addition" 및 "adaptation"이 포함된 목록이 표시됩니다. addition이 선택되어 있습니다. 다른 "d"를 입력하면 목록에 이제 선택된 "addition"만 포함됩니다. 스페이스바, 또는 Enter 키를 눌러 "더하기"를 커밋하거나 Esc 또는 다른 키를 입력하여 목록을 해제할 수 있습니다.