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)
13601정성태4/19/2024232닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/2024295닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024353닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024394닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드1
13597정성태4/15/2024451닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/2024817닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/2024944닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241022닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241051닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241207C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241169닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241073Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241143닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241197닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신파일 다운로드1
13587정성태3/27/20241156오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241300Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241096Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241049개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241158Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241420Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241588개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241138닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241494오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241629닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241876닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...