Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

Visual Studio 2015 확장 - INI 파일을 위한 사용자 정의 포맷 기능 (Syntax Highlighting)

가끔은, Visual Studio에 자신이 정의한 규약을 따르는 편집 기능을 넣고 싶을 때가 있습니다. 쉬운 예로, INI 파일을 편집할 때 섹션명을 굵게 표현하고 싶다거나, 주석은 이탤릭 체로 바꿔주어 가독성을 높일 수 있습니다. 또는 자신이 만든 프로그래밍 언어의 예약어를 굵게 보여주면 좀 더 폼이 날 것입니다. ^^

사실, 몰라서 그렇지 이런 기능을 넣는 것이 그다지 어렵지 않은데요. 다음의 글 정도만 읽어도 쉽게 만들 수 있을 정도입니다.

LearnVSXNow! #38 - VS 2010 Editor - Text Coloring Sample Deep Dive
; http://dotneteers.net/blogs/divedeeper/archive/2008/11/04/LearnVSXNowPart38.aspx

이에 기반해서 간단하게 INI 파일을 위한 syntax highlighter를 만들어 보겠습니다. ^^




먼저 할 일은 Visual Studio를 시작하고, VSIX 프로젝트를 생성합니다.

colorizer_vsix_1.png

그런 다음 C# 코드 템플릿에서 "Editor Classifier"를 추가해 줍니다.

colorizer_vsix_2.png

총 4개의 파일이 생성되는데,

  • EditorClassifier.cs
  • EditorClassifierClassificationDefinition.cs
  • EditorClassifierFormat.cs
  • EditorClassifierProvider.cs

진입점은 IClassifierProvider를 상속받은 EditorClassifierProvider.cs입니다. 해당 클래스에는 다음과 같이 ContentType 특성이 적용되어,

[Export(typeof(IClassifierProvider))]
[ContentType("text")]
internal class EditorClassifierProvider : IClassifierProvider
{
    //...[생략]...
}

현재의 값이 "text"로 되어 있기 때문에 Visual Studio에서 텍스트 파일로 판정되는 파일을 열게 될 때마다 EditorClassifierProvider 인스턴스가 생성됩니다. 이 글에서는 INI 텍스트 파일을 지원할 것이므로 그냥 기본값 그대로 두면 됩니다.

이후 Visual Studio는 IClassifierProvider.GetClassifier를 호출해 주는데 기본 생성된 코드에는 EditorClassifier.cs 파일에 정의된 IClassifier를 상속받은 EditorClassifier 인스턴스를 준비하는 것만 포함되어 있습니다.

public IClassifier GetClassifier(ITextBuffer buffer)
{
    return buffer.Properties.GetOrCreateSingletonProperty<EditorClassifier>(creator: () => new EditorClassifier(this.classificationRegistry));
}

이렇게 반환하면 모든 "Plain text" 파일 유형에 우리가 만든 "Editor Classifier"가 적용되므로, INI 파일에만 한정짓는 작업이 필요합니다. 이를 위해 편집 중인 파일의 정보를 구할 수 있도록 ITextDocumentFactoryService를 Visual Studio의 MEF로부터 받는 작업을 해줘야 합니다. 따라서, EditorClassifierProvider.cs 파일의 최종 코드는 다음과 같이 작성하면 됩니다.

using System;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace IniColorizer
{
    [Export(typeof(IClassifierProvider))]
    [ContentType("text")] // This classifier applies to all text files.
    internal class EditorClassifierProvider : IClassifierProvider
    {
#pragma warning disable 649

        [Import]
        private IClassificationTypeRegistryService classificationRegistry;


        private readonly ITextDocumentFactoryService _textDocumentFactoryService;

        [ImportingConstructor]
        internal EditorClassifierProvider(ITextDocumentFactoryService textDocumentFactoryService)
        {
            _textDocumentFactoryService = textDocumentFactoryService;
        }

#pragma warning restore 649

        public IClassifier GetClassifier(ITextBuffer buffer)
        {
            if (IsINIFile(buffer) == false)
            {
                return null;
            }

            return buffer.Properties.GetOrCreateSingletonProperty<EditorClassifier>(creator: () => new EditorClassifier(this.classificationRegistry));
        }

        private bool IsINIFile(ITextBuffer buffer)
        {
            if (_textDocumentFactoryService == null)
            {
                return false;
            }

            ITextDocument textDocument;
            if (_textDocumentFactoryService.TryGetTextDocument(buffer, out textDocument) == false)
            {
                return false;
            }

            string ext = System.IO.Path.GetExtension(textDocument.FilePath);

            if (string.IsNullOrEmpty(ext) == true)
            {
                return false;
            }

            return ext.ToUpper() == ".INI";
        }
    }
}

자, 그 다음은 EditorClassifier.cs 파일에 정의된 EditorClassifier 클래스가 작업을 담당하게 됩니다. 기본 생성된 코드는 다음과 같이 간단한데요.

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;

namespace VSIXProject1
{
    internal class EditorClassifier : IClassifier
    {
        private readonly IClassificationType classificationType;

        internal EditorClassifier(IClassificationTypeRegistryService registry)
        {
            this.classificationType = registry.GetClassificationType("EditorClassifier");
        }

        #region IClassifier

#pragma warning disable 67

        public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;

#pragma warning restore 67

        public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
        {
            var result = new List<ClassificationSpan>()
            {
                new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start, span.Length)), this.classificationType)
            };

            return result;
        }

        #endregion
    }
}

사용자가 파일을 편집할 때마다 변경된 문자열 정보가 "SnapshotSpan span" 인자에 담겨 GetClassificationSpans 메서드가 호출됩니다. 그리고, 우리는 INI의 문법에 맞게 IList<ClassificationSpan> 목록에 담아 문자열 포맷 정보를 넘겨주면 됩니다. 위의 GetClassificationSpans 메서드는 사용자가 편집한 문자열을 무조건 this.classificationType으로 지정된 문자열 포맷으로 설정합니다.

그리고, this.classificationType은 생성자에서 registry.GetClassificationType("EditorClassifier"); 으로 초기화되는데, 이때의 "EditorClassifier" 문자열 값은 EditorClassifierClassificationDefinition.cs 파일에 정의된 클래스의 멤버에 등록된 [Name("....")]에 지정한 값입니다.

// EditorClassifierClassificationDefinition.cs

using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace VSIXProject1
{
    internal static class EditorClassifierClassificationDefinition
    {
#pragma warning disable 169

        [Export(typeof(ClassificationTypeDefinition))]
        [Name("EditorClassifier")]
        private static ClassificationTypeDefinition typeDefinition;

#pragma warning restore 169
    }
}

또한 이 곳의 "Name" 값은 EditorClassifierFormat.cs 파일에 문자열 포맷 양식을 정의하는 Format 클래스의 ClassificationType 특성에 지정되는 ClassificationTypeNames와 연결되는 역할도 합니다.

// EditorClassifierFormat.cs

using System.ComponentModel.Composition;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace VSIXProject1
{
    [Export(typeof(EditorFormatDefinition))]
    [ClassificationType(ClassificationTypeNames = "EditorClassifier")]
    [Name("EditorClassifier")]
    [UserVisible(true)] // This should be visible to the end user
    [Order(Before = Priority.Default)] // Set the priority to be after the default classifiers
    internal sealed class EditorClassifierFormat : ClassificationFormatDefinition
    {
        public EditorClassifierFormat()
        {
            this.DisplayName = "EditorClassifier"; // Human readable version of the name
            this.BackgroundColor = Colors.BlueViolet;
            this.TextDecorations = System.Windows.TextDecorations.Underline;
        }
    }
}

(즉, 여러분들이 이 문자열 값을 변경하려면 3군데의 값을 모두 일치시켜야 합니다. 그냥 상수로 정의해 쓰시는 것이!)

이제, 다시 EditorClassifier 클래스의 GetClassificationSpans 메서드를 보면 감이 옵니다.

public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
{
    var result = new List<ClassificationSpan>()
    {
        new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start, span.Length)), this.classificationType)
    };

    return result;
}

this.classificationType은 결국 EditorClassifierFormat에 지정된 양식을 사용하는 것과 같습니다.

그럼, 이를 바탕으로 INI 파일의 포맷팅을 완성해 볼까요? ^^

여기서 구현하는 INI 포맷팅은 다음의 2가지만을 구현합니다.

  • 섹션명은 파란색의 굵은 글씨체로 지정
  • Key = Value의 Key 값은 빨간색의 굵은 글씨체로 지정

이를 위해 각각의 포맷팅을 담은 클래스를 준비하고,

using System.ComponentModel.Composition;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace IniColorizer
{
    [Export(typeof(EditorFormatDefinition))]
    [ClassificationType(ClassificationTypeNames = "SectionNameTypeClassifier")]
    [Name("SectionNameFormat")]
    [UserVisible(true)] // This should be visible to the end user
    [Order(Before = Priority.Default)] // Set the priority to be after the default classifiers
    internal sealed class SectionNameClassifierFormat : ClassificationFormatDefinition
    {
        public SectionNameClassifierFormat()
        {
            this.DisplayName = "SectionNameClassifier"; // Human readable version of the name
            this.IsBold = true;
            this.ForegroundColor = Colors.Blue;
        }
    }

    [Export(typeof(EditorFormatDefinition))]
    [ClassificationType(ClassificationTypeNames = "KeyValueTypeClassifier")]
    [Name("KeyValueFormat")]
    [UserVisible(true)] // This should be visible to the end user
    [Order(Before = Priority.Default)] // Set the priority to be after the default classifiers
    internal sealed class KeyValueClassifierFormat : ClassificationFormatDefinition
    {
        public KeyValueClassifierFormat()
        {
            this.DisplayName = "KeyValueClassifier"; // Human readable version of the name
            this.ForegroundColor = Colors.Red;
            this.IsBold = true;
        }
    }
}

각각의 클래스에 지정된 ClassificationTypeNames에 따라 ClassificationDefinition 클래스에 동일한 이름을 갖는 속성 값을 정의합니다.

using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace IniColorizer
{
    internal static class ClassificationDefinition
    {
#pragma warning disable 169

        [Export(typeof(ClassificationTypeDefinition))]
        [Name("SectionNameTypeClassifier")]
        private static ClassificationTypeDefinition typeDefinition1;

        [Export(typeof(ClassificationTypeDefinition))]
        [Name("KeyValueTypeClassifier")]
        private static ClassificationTypeDefinition typeDefinition2;

#pragma warning restore 169
    }
}

사실 ClassificationDefinition 클래스의 정의는 별다르게 제약이 없습니다. 그래서, 위의 클래스를 다음과 같이 2개로 나눠도 상관없습니다.

using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace IniColorizer
{
    internal static class SectionNameClassificationDefinition
    {
#pragma warning disable 169

        [Export(typeof(ClassificationTypeDefinition))]
        [Name("SectionNameTypeClassifier")]
        private static ClassificationTypeDefinition typeDefinition;
#pragma warning restore 169
    }

    internal static class KeyValueClassificationDefinition
    {
#pragma warning disable 169

        [Export(typeof(ClassificationTypeDefinition))]
        [Name("KeyValueTypeClassifier")]
        private static ClassificationTypeDefinition typeDefinition;

#pragma warning restore 169
    }
}

마지막으로, EditorClassifier 클래스에서는 편집된 문자열에 따라 섹션명인지, Key=Value 쌍인지 구분해서 그에 따른 포맷팅의 구분 타입만 지정해주면 됩니다.

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.Language.StandardClassification;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;

namespace IniColorizer
{
    internal class EditorClassifier : IClassifier
    {
        private readonly IClassificationType sectionNameClassifierType;
        private readonly IClassificationType keyValueClassifierType;

        internal EditorClassifier(IClassificationTypeRegistryService registry)
        {
            this.sectionNameClassifierType = registry.GetClassificationType("SectionNameTypeClassifier");
            this.keyValueClassifierType = registry.GetClassificationType("KeyValueTypeClassifier");
        }

        #region IClassifier

#pragma warning disable 67

        public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;

#pragma warning restore 67

        public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
        {
            var result = new List<ClassificationSpan>();

            string modified = span.GetText();

            do
            {
                if (AddIfSection(result, span, modified, this.sectionNameClassifierType) == true)
                {
                    break;
                }

                if (AddIfKeyValue(result, span, modified, this.keyValueClassifierType) == true)
                {
                    break;
                }

            } while (false);

            return result;
        }
        
        #endregion

        private bool AddIfKeyValue(List<ClassificationSpan> result, SnapshotSpan span, string modified, IClassificationType type)
        {
            int pos = modified.IndexOf('=');
            if (pos == -1)
            {
                return false;
            }

            result.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start, pos)), type));

            return true;
        }

        private bool AddIfSection(List<ClassificationSpan> result, SnapshotSpan span, string modified, IClassificationType type)
        {
            int startPos, endPos;

            if (RetrieveSection(modified, out startPos, out endPos) == false)
            {
                return false;
            }

            result.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start, span.Length)), type));

            return true;
        }

        private bool RetrieveSection(string modified, out int startPos, out int endPos)
        {
            endPos = 0;
            startPos = modified.IndexOf('[');

            if (startPos == -1)
            {
                return false;
            }

            endPos = modified.IndexOf(']', startPos);

            if (endPos == -1 || startPos > endPos)
            {
                return false;
            }

            return true;
        }
    }
}

확인을 위해 F5 키를 눌러 실행된 Visual Studio 편집기에서 INI 파일을 열어 보면 다음과 같은 식으로 문자열이 포맷팅됩니다.

colorizer_vsix_3.png




포맷팅의 경우, 비주얼 스튜디오에 이미 정의된 것들을 사용하는 것도 가능합니다. 예를 들어, 일부 포맷팅과 관련된 ClassificationTypeNames 이름을 다음의 클래스에서 볼 수 있습니다.

PredefinedClassificationTypeNames Class
; https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.language.standardclassification.predefinedclassificationtypenames

예를 들어, 우리가 만든 INIColorizer 확장에서 섹션명의 포맷을 PredefinedClassificationTypeNames.Keyword로 지정하려면 이렇게 코딩할 수 있습니다.

// EditorClassifier.cs

internal EditorClassifier(IClassificationTypeRegistryService registry)
{
    // PredefinedClassificationTypeNames 사용을 위해 Microsoft.VisualStudio.Language.StandardClassification.dll 참조 추가 필요
    this.sectionNameClassifierType = registry.GetClassificationType(PredefinedClassificationTypeNames.Keyword);
}

하지만, 해당 속성값이 단순히 문자열이기 때문에 굳이 Microsoft.VisualStudio.Language.StandardClassification.dll 어셈블리를 참조 추가할 필요없이 이렇게 쓰는 것도 가능합니다.

// EditorClassifier.cs

internal EditorClassifier(IClassificationTypeRegistryService registry)
{
    this.sectionNameClassifierType = registry.GetClassificationType("keyword");
}

여기에 사용되는 문자열을 좀 더 알고 싶다면 비주얼 스튜디오의 "Tools" / "Options..."에서 "Environment" / "Fonts and Colors"를 보면 됩니다.

colorizer_vsix_4.png

이렇게 비주얼 스튜디오에 이미 정의된 포맷을 사용하면 좋은 점이, 사용자가 임의로 변경할 수 있다는 것입니다. 그리고, 개발자 입장에서도 별다르게 ClassificationDefinition.cs, EditorClassifierFormat.cs 파일을 만들지 않아도 됩니다. 그저 다음과 같이 기존 정의된 포맷을 쓰면 그만입니다.

// EditorClassifier.cs

internal EditorClassifier(IClassificationTypeRegistryService registry)
{
    this.sectionNameClassifierType = registry.GetClassificationType("keyword");
    this.keyValueClassifierType = registry.GetClassificationType("symbol definition");
}

이 정도면... 이제 여러분들이 회사에서 내부 용도로 만든 '스크립트 언어'가 있다면 비주얼 스튜디오를 이용해 쉽게 "Syntax Highlighting" 기능을 구현할 수 있을 것입니다. ^^ 그리곤 비주얼 스튜디오 갤러리에 등록해 보면 더욱 멋지겠지요. ^^

Visual Studio 제품 및 확장 기능
; https://marketplace.visualstudio.com/

저도 이 예제를 등록했답니다. ^^

Simple INI Syntax Highlighter
; https://marketplace.visualstudio.com/items?itemName=SeongTaeJeong.SimpleINISyntaxHighlighter




참고로, INI highlighting 용 확장은 이미 Visual Studio 갤러리에 좋은 것들이 있습니다.

TextHighlighterExtension2013 
; https://marketplace.visualstudio.com/items?itemName=FredericTorres.TextHighlighterExtension2013

Visual Studio 2010 용이지만 "Boo" 라는 언어를 위한 syntax hightlighting 확장을 만든 소스 코드도 공개된 것이 있고.

Boo syntax highlighting for Visual Studio 2010
; http://vs2010boo.codeplex.com/

여기서 다루진 않았지만 다음과 같은 방법도 있습니다.

Syntax Colorizing (Managed Package Framework)
; https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/syntax-colorizing-in-a-legacy-language-service

(첨부한 파일은 이 글의 예제 코드를 포함합니다. 또는 https://github.com/stjeong/IniColorizer에도 올려놨으니 참고하세요. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/17/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...
NoWriterDateCnt.TitleFile(s)
13281정성태3/12/20233853Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234607개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20234126오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20234157개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234813개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234478.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234794.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234357.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20234089.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
13272정성태2/27/20234357오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234276오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
13270정성태2/24/20233872.NET Framework: 2098. dotnet build에 /p 옵션을 적용 시 유의점
13269정성태2/23/20234438스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
13268정성태2/22/20234989개발 환경 구성: 667. WSL 2 내부에서 열고 있는 UDP 서버를 호스트 측에서 접속하는 방법
13267정성태2/21/20234901.NET Framework: 2097. C# - 비동기 소켓 사용 시 메모리 해제가 finalizer 단계에서 발생하는 사례파일 다운로드1
13266정성태2/20/20234507오류 유형: 848. .NET Core/5+ - Process terminated. Couldn't find a valid ICU package installed on the system
13265정성태2/18/20234433.NET Framework: 2096. .NET Core/5+ - PublishSingleFile 유형에 대한 runtimeconfig.json 설정
13264정성태2/17/20235935스크립트: 45. 파이썬 - uvicorn 사용자 정의 Logger 작성
13263정성태2/16/20234090개발 환경 구성: 666. 최신 버전의 ilasm.exe/ildasm.exe 사용하는 방법
13262정성태2/15/20235161디버깅 기술: 191. dnSpy를 이용한 (소스 코드가 없는) 닷넷 응용 프로그램 디버깅 방법 [1]
13261정성태2/15/20234423Windows: 224. Visual Studio - 영문 폰트가 Fullwidth Latin Character로 바뀌는 문제
13260정성태2/14/20234232오류 유형: 847. ilasm.exe 컴파일 오류 - error : syntax error at token '-' in ... -inf
13259정성태2/14/20234381.NET Framework: 2095. C# - .NET5부터 도입된 CollectionsMarshal
13258정성태2/13/20234267오류 유형: 846. .NET Framework 4.8 Developer Pack 설치 실패 - 0x81f40001
13257정성태2/13/20234355.NET Framework: 2094. C# - Job에 Process 포함하는 방법 [1]파일 다운로드1
13256정성태2/10/20235188개발 환경 구성: 665. WSL 2의 네트워크 통신 방법 - 두 번째 이야기
1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...