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)
13510정성태1/3/20242105닷넷: 2192. C# - 특정 실행 파일이 있는지 확인하는 방법 (Linux)
13509정성태1/3/20242144오류 유형: 887. .NET Core 2 이하의 프로젝트에서 System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.0.
13508정성태1/3/20242164오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/20242829닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/20232395닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20232949닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232524닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232386Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232487닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232298개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232364디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20233053닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232452오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232434Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232405Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232536Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232629닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232313개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232261Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232387개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232171개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232100오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232405개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232217개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232098오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232174개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...