바락 언어를 위한 구문 강조 & 코드 접기

Andreas Tscharner

April 18, 2014

한글판 johnsonj 160927

요약

클링온(Klingon) 스크립트 언어 바락(var’aq)을 위하여 구문 강조와 코드 접기를 설명한다.

내용

1 들어가는 말

1.1 왜 또 자습서가 필요한가?

이미 신틸라 문서 페이지에 코드 접기와 어휘 분석 방법을 기술하는 링크가 세 군데나 있다. 그래서 왜 또 자습서가 필요한 것인가?

필자가 신틸라를 사용하기 시작할 때, 문서를 읽어 보면서 신틸라의 탁월한 ”기능”에 관하여 알게되었다. 필자가 알고 있는 다른 언어의 어휘 분석기들도 살펴 보았다. 이 모든 것들이 도움이 되었다.

그러나 어휘분석과 코드 접기라는 미지의 세계로 차근차근 필자를 인도해 줄 지침서가 필요했다.

이 자습서는 여러분을 한걸음 한걸음씩 인도해 줄 것이다.

1.2 바락(var’aq) 언어란 도데체 무엇인가?

제일 먼저 제기될 FAQ는 이것이 될 것이다: 바락(Var’aq)이 도데체 뭐야?

바락은 열정적인 팬들의 프로그래밍 언어로서 클링온(Klingon) 언어에 기반한다.

클링온 언어는 스타 트랙(Star Trek) 시리즈와 영화에 사용된다. 마크 오크랜드(Marc Okrand)가 창시했으며 한편으로 후원자의 입장에서 클링온 언어 연구소(Klingon Language Institute)에서 ”관리한다”. 그러나 실제로 전혀 독립적인 언어이다;

바락은 오직 좀 더 바락 문화를 풍요롭게 하고 더욱 사랑하며 펄 언어를 좀 더 배우고자 하는 열광적인 팬의 입장에서 탄생하였다.

1.2.1 왜 클링온 프로그래밍 언어인가?

이유가 있다; 첫 째 가장 중요한 이유는 아직 이 언어를 지원하는 어휘분석기가 없다는 것이다.

게다가 언제나 필자는 약간 독특한 언어를 좋아하며 스타 트랙의 열광적인 팬이기도 하므로 이 언어가 당연한 ”논리적인” 귀결이었다.

1.2.2 더 자세한 정보는 링크를 참조

1.3 이 자습서의 구성

1.3.1 이 자습서의 최신 버전

최신 버전은 비트버킷(Bitbucket)에서 얻으실 수 있다.

1.3.2 버그와 실수 그리고 오타에 관하여

코드에서 버그를 발견하시거나 텍스트에서 실수와 오타를 발견하신다면 알려 주시기를 바란다.

비트버킷 이슈 트래커에 새로 문제를 제기하거나 이메일을 보내 주시기를 바란다! 패치도 환영한다!

2 준비 작업

2.1 신틸라는 어떻게 구문을 강조하는가

이른바 ”어휘분석기(Lexer)”가 주어진 문서를 해석한다. 강조할 수 있는 것을 발견하면 그것을 미리-정의된 스타일로 설정한다.

예를 들어: C/C++ 어휘분석기가 숫자를 발견하면 그 위치마다 SCE_C_NUMBER 스타일을 설정한다. 주어진 스타일에 대하여 색깔을 설정하는 것은 어플리케이션의 몫이다.

2.2 어떻게 파일이 조직되어 있는가

모든 ”어휘분석기(Lexer)” 파일은 lexers라는 하위 디렉토리에 저장된다. 이름이 Lex로 시작하며 그 다음에 프로그래밍 언어의 이름이 따른다.

그래서 우리의 바락(var’aq) 어휘분석기는 이름이 다음과 같이 LexVaraq.cxx로 지어진다.

다른 언어를 위한 여러 스타일은 SciLexer.h에 저장되어 있다. 이 파일은 자동으로 생성되어 include라는 하위 디렉토리에 저장된다.

스타일에 대하여 Scintilla.iface 파일에 값을 삽입할 필요가 있다. 이 파일도 역시 include 디렉토리에 있다.

마지막으로 메이크 파일과 헤더 파일 등등을 다시 생성할 필요가 있다. 파이썬 스크립트를 실행하면 되는데 이 파일들은 scripts 디렉토리에 있다.

2.3 스타일 설정하기

바락(var’aq) 언어의 어휘분석기는 네 가지 스타일을 설정한다: 주석, 문자열, 숫자 그리고 키워드가 그것이다. 그와 더불어 또 표준/기본 텍스트에 대하여 스타일을 정의할 필요도 있다.

그래서 Scintilla.iface 파일에 스타일(상수)과 새 언어를 위한 상수 하나를 정의할 필요가 있다.

유닉스 어셈블러를 위한 설정 줄 val SCLEX_AS=113 바로 다음 줄에 새로운 언어에 대하여 val SCLEX_VARAQ=114 와 같이 간단하게 설정하면 된다:

새 스타일에 대한 상수들을 DMAP 상수들 다음에 정의한다 (val SCE_DMAP_WORD3=10 줄 다음에 배치한다; 보통은 유닉스 어셈블러(Unix Assembler) 다음에 배치하지만, 이 경우에는 새 어셈블러 어휘분석기가 그저 기존의 변형에 불과하기 때문이다):

# Lexical states for SCLEX_VARAQ
lex Varaq=SCLEX_VARAQ SCE_VARAQ_
val SCE_VARAQ_DEFAULT=0
val SCE_VARAQ_COMMENT=1
val SCE_VARAQ_STRING=2
val SCE_VARAQ_NUMBER=3
val SCE_VARAQ_IDENTIFIER=4
val SCE_VARAQ_KEYWORD=5

SCE_VARAQ_IDENTIFIER는 특별한 경우이다.

가능한 모든 식별자에 부합시키기 위하여 내부적으로 사용된다. 나중에 마지막 단어가 있고 그 단어의 시작 위치를 결정할 수 없을 경우 이 식별자가 꼭 필요하다.

일반적으로 말해: 새 언어를 위한 상수와 이 언어 안의 다양한 스타일을 위한 상수들은 현재 섹션의 끝에다 간단하게 적기만 하면 된다

scripts 디렉토리에 있는 HFacer.py를 실행하면 갱신된 include\SciLexer.h 파일 안에 새로 정의된 상수들이 들어 있는 것을 볼 수 있다.

3 어휘분석기(Lexer)

스타일을 정의하고 나면 바락(var’aq) 어휘분석기를 구현하기만 하면 된다. 그렇게 하려면 ILexer 인터페이스 또는 ILexerWithSubStyles 인터페이스를 구현해야 한다.

두 인터페이스 모두 ILexer.h에 정의되어 있고 ILexerWithSubStyles는 ILexer에서 상속 받는다.

3.1 ILexer 인터페이스와 ILexerWithSubStyles 인터페이스

3.1.1 ILexer

class ILexer {
    public:
        virtual int SCI_METHOD Version() const = 0;
        virtual void SCI_METHOD Release() = 0;
        virtual const char * SCI_METHOD PropertyNames() = 0;
        virtual int SCI_METHOD PropertyType(const char *name) = 0;
        virtual const char * SCI_METHOD DescribeProperty(const char *name) = 0;
        virtual int SCI_METHOD PropertySet(const char *key, const char *val) = 0;
        virtual const char * SCI_METHOD DescribeWordListSets() = 0;
        virtual int SCI_METHOD WordListSet(int n, const char *wl) = 0;
        virtual void SCI_METHOD Lex(unsigned int startPos, int lengthDoc, int initStyle, IDocument *pAccess) = 0;
        virtual void SCI_METHOD Fold(unsigned int startPos, int lengthDoc, int initStyle, IDocument *pAccess) = 0;
        virtual void * SCI_METHOD PrivateCall(int operation , void *pointer) = 0;
};

3.1.2 ILexerWithSubStyles

class ILexerWithSubStyles : public ILexer {
    public:
        virtual int SCI_METHOD LineEndTypesSupported() = 0;
        virtual int SCI_METHOD AllocateSubStyles(int styleBase , int numberStyles) = 0;
        virtual int SCI_METHOD SubStylesStart(int styleBase) = 0;
        virtual int SCI_METHOD SubStylesLength(int styleBase) = 0;
        virtual void SCI_METHOD FreeSubStyles() = 0;
        virtual void SCI_METHOD SetIdentifiers(int style , const char *identifiers) = 0;
        virtual int SCI_METHOD DistanceToSecondaryStyles() = 0;
        virtual const char * SCI_METHOD GetSubStyleBases() = 0;
};

3.2 특성

ILexer 인터페이스에 몇 가지 특성이 있는 것을 볼 수 있다. 신틸라는 어플리케이션이 실행중인 동안에 특성을 켜고 끌 수 있도록 허용한다. 지금은 필요가 없으므로 상응하는 메쏘드를 따로 구현하지 않는다.

3.3 어휘분석기 파일 만들기

위에서 언급했듯이 파일 이름은 LexVaraq.cxx이고 다른 어휘분석기들 처럼 lexers 디렉토리에 자리를 잡는다.

먼저 ILexer 인터페이스를 구현한다. 나중에 필요하면 문제 없이 ILexerWithSubStyles 인터페이스로 변경할 수 있다. ILexer를 상속받았기 때문이다.

구현된 인터페이스의 선택은 또한 virtual int SCI METHOD Version() 메쏘드의 반환 값을 정의한다 (아래 참조). ILexer 인터페이스에 대하여 lvOriginal을 돌려주고 ILexerWithSubStyles 인터페이스에 대하여 lvSubStyles을 돌려준다.

최소한으로 구현을 시작하자:

// Scintilla source code edit control
/** @file LexVaraq.cxx
** Lexer for the klingon scripting language var’aq
** Used for the Scintilla -var’aq-Tutorial
**/

// Copyright 2013-2014 by Andreas Tscharner >
// The License.txt file describes the conditions under which this
// software may be distributed.

#include "ILexer.h"
#ifdef SCI_NAMESPACE
    using namespace Scintilla;
#endif

class LexerVaraq : public ILexer
{
};

3.4 인터페이스 방법

인터페이스 방법은 대부분 눈에 보이는 그대로 구현된다. 여기에서 짧게 언급해 보자.

virtual int SCI METHOD Version()
virtual int SCI METHOD Version() 메쏘드는 어느 버전의 인터페이스를 사용해야 하는지 정의한다. ILexer 인터페이스라면 lvOriginal이 반환될 것이고, ILexerWithSubStyles 인터페이스라면 아마도 값이 lvSubStyles일 것이다.
virtual void SCI METHOD Release()
static ILexer *LexerFactoryVaraq()
virtual void SCI METHOD Release()는 공장 메쏘드 static ILexer *LexerFactoryVaraq()로 생성된 어휘분석기를 소멸하기 위해 호출된다.
virtual void SCI METHOD Lex(...)
지금 당장은 이 특성과 WordList를 무시하자. 가장 중요한 메쏘드 virtual void SCI METHOD Lex(...)에 집중하자. Lex 메쏘드는 구문 강조를 위해 구현해야 한다.
virtual void SCI METHOD Fold(...)
마지막 메쏘드도 중요하다 (나중에 자세히 설명함): virtual void SCI METHOD Fold(...)는 이른바 코드 접기를 위한 메쏘드이다.

그래서 우리의 첫 코드는 다음과 같다:

class LexerVaraq : public ILexer
{
    public:
        LexerVaraq() {}
        virtual ~LexerVaraq() {}
        int SCI_METHOD Version() const {
            return lvOriginal;
        }
        void SCI_METHOD Release() {
            delete this;
        }
        const char * SCI_METHOD PropertyNames() {
            return NULL;
        }
        int SCI_METHOD PropertyType(const char *name) {
            return -1;
        }
        const char * SCI_METHOD DescribeProperty(const char *name) {
            return NULL;
        }
        int SCI_METHOD PropertySet(const char *key, const char *val) {
            return -1;
        }
        const char * SCI_METHOD DescribeWordListSets() {
            return NULL;
        }
        int SCI_METHOD WordListSet(int n, const char *wl) {
            return -1;
        }
        void SCI_METHOD Lex(unsigned int startPos , int lengthDoc ,
                                    int initStyle , IDocument *pAccess);
        void SCI_METHOD Fold(unsigned int startPos , int lengthDoc ,
                                       int initStyle , IDocument *pAccess) {}
        void * SCI_METHOD PrivateCall(int operation , void *pointer) {
            return NULL;
        }
        static ILexer *LexerFactoryVaraq() {
            return new LexerVaraq();
        }
};

3.5 주석

바락 언어의 주석은 예전의 파스칼 언어의 주석과 비슷하다: 주석은 (*와 그리고 *)로 둘러 싸서 표현한다.

그래서 문서에서 그런 토큰을 찾아 start부터 end까지의 범위에 COMMENT 스타일을 추가한다.

void SCI_METHOD LexerVaraq::Lex(unsigned int startPos , int lengthDoc ,
                                             int initStyle , IDocument *pAccess)
{
    LexAccessor styler(pAccess);
    StyleContext scCTX(startPos , lengthDoc , initStyle , styler);
    for (; scCTX.More() ; scCTX.Forward()) {
        switch (scCTX.state) {
        case SCE_VARAQ_DEFAULT :
            if (scCTX.Match(’(’, ’*’)) {
                scCTX.SetState(SCE_VARAQ_COMMENT);
                scCTX.Forward(); // Move over the *
            };
            break;
        case SCE_VARAQ_COMMENT :
            if (scCTX.Match(’*’, ’)’)) {
                scCTX.Forward(); // Move over the )
                scCTX.ForwardSetState(SCE_VARAQ_DEFAULT);
            };
            break;
        };
    };
    scCTX.Complete();
}

이 코드만으로도 이미 충분히 바락 언어의 주석을 강조한다.

LexAccessor 유형의 스타일 처리기는 GetLine이나 GetLineState 같은 함수로 문서에 접근할 수 있도록 해 준다.

StyleContext scCTX 변수로 스타일 처리 문맥을 얻는다.

이제 정의된 범위를 반복 처리한다:

for (; scCTX.More() ; scCTX.Forward())
그리고 문자마다 점검한다.

주석의 시작을 발견하고 기본 스타일에 있다면, 새로운 COMMENT 스타일을 설정한다.

반면에 주석 스타일에 있고 주석 끝을 발견하면 기본 스타일을 돌려준다.

어휘분석기를 사용하려면 LexerModule의 실체를 하나 생성해야 한다. 파일에서 마지막 줄이 바로 이것이다:

LexerModule lmVaraq(SCLEX_VARAQ, LexerVaraq::LexerFactoryVaraq, "varaq", VaraqWordListDesc);

이 LexerModule을 Catalogue.cxx에 추가하려면 상응하는 디렉토리에서 LexGen.py 스크립트를 실행하면 된다.

이제 var’aq 소스 코드를 신틸라에 적재해 넣으면 주석이 강조되어 나타난다:

강조 텍스트의 색깔을 정의한다. 스타일 처리된 주석을 보여주기 위해 빨간색을 사용했지만 나중에 회색이 될 것이다.

3.6 문자열

var’aq 언어의 문자열은 다른 언어에서처럼 겹따옴표로 표식을 붙인다. 그러므로 문자열 강조는 간단하다:

var’aq 기본 블록에 문자열 스타일로 전환하는 점검 코드를 추가 배치한다:

if (scCTX.Match(’\"’)) {
    scCTX.SetState(SCE_VARAQ_STRING);
    break;
};

이제 문자열의 경우 기본 스타일로 다시 전환하는 새 블록을 추가한다:

case SCE_VARAQ_STRING :
    if (scCTX.Match(’\"’)) {
        scCTX.ForwardSetState(SCE_VARAQ_DEFAULT);
    };
    break;
이제 문자열 강조는 완성이다:

3.7 숫자

먼저 간단하게 숫자를 점검하기 위해, 숫자 또는 부호로 시작하는지 점검한다. 그 다음에 숫자 또는 선택적으로 소수점이 더 따라 오는지 점검한다.

SCE VARAQ NUMBER 스타일을 시작하기 위해 음의 부호 다음에 숫자가 따라 오는지 점검한다:

if (IsADigit(scCTX.ch) || (scCTX.Match(’-’) && IsADigit(scCTX.chNext))) {
    scCTX.SetState(SCE_VARAQ_NUMBER);
    pointInNumberUsed = false;
    break;
};

최대로 소수점 한개를 포함하여 숫자가 계속 되기만 하면 숫자 스타일을 유지할 수 있다. pointInNumberUsed는 부울 유형의 변수이다:

case SCE_VARAQ_NUMBER :
    if (scCTX.Match(’.’)) {
        if (pointInNumberUsed) {
            scCTX.SetState(SCE_VARAQ_DEFAULT);
        } else {
            pointInNumberUsed = true;
        };
    } else {
        if (!IsADigit(scCTX.ch)) {
            scCTX.SetState(SCE_VARAQ_DEFAULT);
        };
    };
    break;

언제나 그렇듯이 새 스타일 (숫자)은 빨간색이다; 문자열은 초록색이 되었다:

3.8 키워드

마지막으로 var’aq 언어의 키워드를 강조해 보자.

키워드 단어를 점검하는 것은 구문을 분석하는 것과 약간 다르다.

먼저, 이런 단어들을 정의하자:

    static const char *const VaraqKeywords = {
    "woD latlh tam chImmoH qaw qawHa\’ Hotlh pong cher HIja\’chugh ghobe\’chugh wIv"
    "chov nargh vangqa\’ SIj muv ghorqu\’ chIm\’a\’ tlheghrar naQmoH tlheghrap\’a\’"
    "tlheghpe\’ tlheghjuv jor boq boqHa\’ boq\’egh boqHa\’\’egh HabboqHa\’\’egh"
    "chuv boqHa\’qa\’ loS\’ar wa\’boq wa\’boqHa\’ joq joqHa\’ qojmI\’ qojHa\’"
    "ghurtaH maHghurtaH wejghurtaH poD Hab \’ar mIScher mIS HeHmI\’ ghurmI\’"
    "HabmI\’\’a\’ mI\’\’a\’ mI\’moH mobmoH DuD tlhoch Qo\’moH nIHghoS poSghoS"
    "law\’\’a\’ puS\’a\’ rap\’a\’ law\’rap\’a\’ puSrap\’a\’ rapbe\’a\’ pagh\’a\’"
    "taH\’a\’ je joq ghap ghobe\’ cha\’ \’Ij bep chu\’DonwI\’ chu\’tut nuqDaq_jIH"
    "pongmI\’ taghDe\’"
};

신틸라는 WordList 클래스가 있는데 이런 단어들을 한 개짜리 큰 문자열에서 간단하게 점검할 수 있다.

WordList vqKeywords;
vqKeywords.Set(VaraqKeywords);

이 키워드들을 나중에 사용해서 발견된 단어가 VARAQ_KEYWORD로 스타일 처리해야 하는 var’aq 키워드인지 알아 볼 것이다. 또 키워드로 사용할 수 있는 문자 집합이 필요하다.

위의 키워드를 살펴보면 모두 기호 문자 (대문자와 소문자) 그리고 홑따옴표라는 것을 알 수 있다.

CharacterSet setKeywordChars(CharacterSet::setAlpha , "\’");

이런 문자를 발견하면 다음 단어는 키워드일 가능성이 있다 (그러나 물론 다른 어떤 것일 수도 있다).

그래서 상태를 설정해 도움을 받는다.

SCE VARAQ IDENTIFIER :
switch (scCTX.state) {
case SCE_VARAQ_DEFAULT :
    if (setKeywordChars.Contains(scCTX.ch)) {
        scCTX.SetState(SCE_VARAQ_IDENTIFIER);
        break;
    };
};

이제 가능한 모든 단어는 상태가 SCE VARAQ IDENTIFIER이다.

이 상태에 있고 키워드가 문자가 아닌 문자에 도달하면, 그 상태를 처리해야 한다.

SCE VARAQ IDENTIFIER로 정의된 단어가 var’aq 키워드이면, 상태를 SCE VARAQ KEYWORD으로 바꾼다. 그렇지 않으면, 아무 일도 하지 않는다.

두 경우 모두, 다시 기본 상태를 설정해야 한다.

case SCE_VARAQ_IDENTIFIER :
    if (!setKeywordChars.Contains(scCTX.ch)) {
        tmpStr = new char[MAX_STR_LEN];
        memset(tmpStr , 0, sizeof(char)*MAX_STR_LEN);
        scCTX.GetCurrent(tmpStr , MAX_STR_LEN);
        if (vqKeywords.InList(tmpStr)) {
            scCTX.ChangeState(SCE_VARAQ_KEYWORD);
        };
        scCTX.SetState(SCE_VARAQ_DEFAULT);
        delete[] tmpStr;
    };
    break;

마침내 거의 완벽하게 강조 처리가 되었다 (키워드가 빨간색이다):

4 코드 접기

코드 접기도 어휘분석과 비슷한 방식으로 수행된다:

요청 구역을 문자 단위로 밟아 가면서 코드 접기를 알리는 시작 문자가 발견되면 레벨이 증가하고, 닫기 문자가 발견되면 레벨이 감소한다.

var’aq 언어에서 접기를 위한 문자들은 활괄호이다 (C/C++처럼 {그리고 }이다). 약간 코드가 다른데 이번에는 StyleContext가 없기 때문이다.

void SCI_METHOD LexerVaraq::Fold(unsigned int startPos , int lengthDoc , 
                                              int initStyle , IDocument *pAccess)
{
    LexAccessor styler(pAccess);
    unsigned int endPos = startPos + lengthDoc;
    char chNext = styler[startPos];
    int lineCurrent = styler.GetLine(startPos);
    int levelPrev = styler.LevelAt(lineCurrent) & SC_FOLDLEVELNUMBERMASK;
    int levelCurrent = levelPrev;
    char ch;
    bool atEOL;
    for (unsigned int i = startPos; i < endPos; i++) {
        ch = chNext;
        chNext = styler.SafeGetCharAt(i+1);
        atEOL = ((ch == ’\r’ && chNext != ’\n’) || (ch == ’\n’));
        if (ch == ’{’) {
            levelCurrent++;
        };
        if (ch == ’}’) {
            levelCurrent --;
        };
        if (atEOL || (i == (endPos -1))) {
            int lev = levelPrev;
            if (levelCurrent > levelPrev) {
                lev |= SC_FOLDLEVELHEADERFLAG;
             };
            if (lev != styler.LevelAt(lineCurrent)) {
                styler.SetLevel(lineCurrent , lev);
            };
            lineCurrent++;
            levelPrev = levelCurrent;
        };
    };
}

눈에 보이는 그대로 구현되었다: start부터 end까지 반복한다.

문자마다 검사한다: 여는 괄호이면, 레벨이 증가하고; 닫는 괄호이면, 다시 감소한다. 줄 끝마다, 현재 레벨을 검사한다.

이전 줄의 레벨보다 더 크면, SC FOLDLEVELHEADERFLAG이 설정된다.

이것으로 신틸라에게 이 줄이 접기 지점이라는 사실 알려준다.

이제 어플리케이션을 제어하는 일만 남았다:

#define MARGIN_SCRIPT_FOLD_INDEX 1
SSM(SCI_SETMARGINTYPEN , MARGIN_SCRIPT_FOLD_INDEX , SC_MARGIN_SYMBOL);
SSM(SCI_SETMARGINMASKN , MARGIN_SCRIPT_FOLD_INDEX , SC_MASK_FOLDERS);
SSM(SCI_SETMARGINWIDTHN , MARGIN_SCRIPT_FOLD_INDEX , 20);
SSM(SCI_SETMARGINSENSITIVEN , MARGIN_SCRIPT_FOLD_INDEX , 1);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDER , SC_MARK_BOXPLUS);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDEROPEN , SC_MARK_BOXMINUS);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDEREND , SC_MARK_EMPTY);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDERMIDTAIL , SC_MARK_EMPTY);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDEROPENMID , SC_MARK_EMPTY);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDERSUB , SC_MARK_EMPTY);
SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDERTAIL , SC_MARK_EMPTY);
SSM(SCI_SETFOLDFLAGS , SC_FOLDFLAG_LINEAFTER_CONTRACTED , 0);

이 호출들로 20 픽셀 너비의 심볼 여백을 설정한다.

접기 지점에 대하여 (내부에 저장된) 몇 가지 심볼을 정의한다.

SCI SETMARGINSENSITIVEN 호출도 주어진 여백의 여백 심볼이 마우스 클릭을 감지하도록 설정한다 ( MARGIN_SCRIPT_FOLD_INDEX ).

그러므로 신틸라 고지를 위한 역호출 함수는 다음과 같이 설정한다...

static void scintilla_notify(GtkWidget *sciWidget , gint ctrlID ,
                                struct SCNotification *notifyData , gpointer userData)
{
    int lineNr = scintilla_send_message(SCINTILLA(sciWidget),
    SCI_LINEFROMPOSITION , (uptr_t)notifyData ->position , 0);
    switch (notifyData ->margin) {
    case MARGIN_SCRIPT_FOLD_INDEX:
        scintilla_send_message(SCINTILLA(sciWidget),
        SCI_TOGGLEFOLD , lineNr , 0);
        break;
    };
}
... 이제 다음과 같이 연결한다:
    gtk_signal_connect(GTK_OBJECT(editor), "sci-notify",
                            G_CALLBACK(scintilla_notify), editor);

다음은 그 결과이다 (balmey 함수가 접혀졌다):

A 완전한 var’aq 어휘분석과 접기 코드

// Scintilla source code edit control
/** @file LexVaraq.cxx
** Lexer for the klingon scripting language var’aq
** Used for the Scintilla -var’aq-Tutorial
**/
// Copyright 2013 by Andreas Tscharner >
// The License.txt file describes the conditions under which this software may be distributed.

#include <cstdlib >
#include <cassert >
#include <cstring >
#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"
#include "LexAccessor.h"
#include "StyleContext.h"
#include "LexerModule.h"
#include "CharacterSet.h"
#include "WordList.h"
#ifdef SCI_NAMESPACE
    using namespace Scintilla;
#endif

static const char *const VaraqWordListDesc[] = {
    "Var\’aq keywords",
    0
};

static const char *const VaraqKeywords = {
    "woD latlh tam chImmoH qaw qawHa\’ Hotlh pong cher HIja\’chugh ghobe\’chugh wIv "
    "chov nargh vangqa\’ SIj muv ghorqu\’ chIm\’a\’ tlheghrar naQmoH tlheghrap\’a\’ "
    "tlheghpe\’ tlheghjuv jor boq boqHa\’ boq\’egh boqHa\’\’egh HabboqHa\’\’egh "
    "chuv boqHa\’qa\’ loS\’ar wa\’boq wa\’boqHa\’ joq joqHa\’ qojmI\’ qojHa\’ "
    "ghurtaH maHghurtaH wejghurtaH poD Hab \’ar mIScher mIS HeHmI\’ ghurmI\’ "
    "HabmI\’\’a\’ mI\’\’a\’ mI\’moH mobmoH DuD tlhoch Qo\’moH nIHghoS poSghoS "
    "law\’\’a\’ puS\’a\’ rap\’a\’ law\’rap\’a\’ puSrap\’a\’ rapbe\’a\’ pagh\’a\’ "
    "taH\’a\’ je joq ghap ghobe\’ cha\’ \’Ij bep chu\’DonwI\’ chu\’tut nuqDaq_jIH "
    "pongmI\’ taghDe\’"
};

class LexerVaraq : public ILexer
{
    public:
        LexerVaraq() {}
        virtual ~LexerVaraq() {}
        int SCI_METHOD Version() const {
            return 1;
        }
        void SCI_METHOD Release() {
            delete this;
        }
        const char * SCI_METHOD PropertyNames() {
            return NULL;
        }
        int SCI_METHOD PropertyType(const char *name) {
            return -1;
        }
        const char * SCI_METHOD DescribeProperty(const char *name) {
            return NULL;
        }
        int SCI_METHOD PropertySet(const char *key, const char *val) {
            return -1;
        }
        const char * SCI_METHOD DescribeWordListSets() {
            return VaraqWordListDesc[0];
        }
        int SCI_METHOD WordListSet(int n, const char *wl) {
            return -1;
        }
        void SCI_METHOD Lex(unsigned int startPos , int lengthDoc , int initStyle , IDocument *pAccess);
        void SCI_METHOD Fold(unsigned int startPos , int lengthDoc , int initStyle , IDocument *pAccess);
        void * SCI_METHOD PrivateCall(int operation , void *pointer) {
            return NULL;
        }
        static ILexer *LexerFactoryVaraq() {
            return new LexerVaraq();
        }
};

void SCI_METHOD LexerVaraq::Lex(unsigned int startPos , int lengthDoc , int initStyle , IDocument *pAccess)
{
    const unsigned int MAX_STR_LEN = 100;
    LexAccessor styler(pAccess);
    StyleContext scCTX(startPos , lengthDoc , initStyle , styler);
    bool pointInNumberUsed = false;
    WordList vqKeywords;
    char *tmpStr;
    CharacterSet setKeywordChars(CharacterSet::setAlpha , "\’");
    vqKeywords.Set(VaraqKeywords);
    tmpStr = new char[MAX_STR_LEN];

    for (; scCTX.More() ; scCTX.Forward()) {
        switch (scCTX.state) {
        case SCE_VARAQ_DEFAULT :
            if (scCTX.Match(’(’, ’*’)) {
                scCTX.SetState(SCE_VARAQ_COMMENT);
                scCTX.Forward();
                break;
            };
            if (scCTX.Match(’\"’)) {
                scCTX.SetState(SCE_VARAQ_STRING);
                break;
            };
            if (IsADigit(scCTX.ch) || (scCTX.Match(’-’) && IsADigit(scCTX.chNext))) {
                scCTX.SetState(SCE_VARAQ_NUMBER);
                pointInNumberUsed = false;
                break;
            };
            if (setKeywordChars.Contains(scCTX.ch)) {
                scCTX.SetState(SCE_VARAQ_IDENTIFIER);
                break;
            };
            break;
        case SCE_VARAQ_COMMENT :
            if (scCTX.Match(’*’, ’)’)) {
                scCTX.Forward();
                scCTX.ForwardSetState(SCE_VARAQ_DEFAULT);
            };
            break;
        case SCE_VARAQ_STRING :
            if (scCTX.Match(’\"’)) {
                scCTX.ForwardSetState(SCE_VARAQ_DEFAULT);
            };
            break;
        case SCE_VARAQ_NUMBER :
            if (scCTX.Match(’.’)) {
                if (pointInNumberUsed) {
                    scCTX.SetState(SCE_VARAQ_DEFAULT);
                } else {
                pointInNumberUsed = true;
                };
            } else {
                if (!IsADigit(scCTX.ch)) {
                    scCTX.SetState(SCE_VARAQ_DEFAULT);
                };
            };
            break;
        case SCE_VARAQ_IDENTIFIER :
            if (!setKeywordChars.Contains(scCTX.ch)) {
                memset(tmpStr , 0, sizeof(char)*MAX_STR_LEN);
                scCTX.GetCurrent(tmpStr , MAX_STR_LEN);
                if (vqKeywords.InList(tmpStr)) {
                    scCTX.ChangeState(SCE_VARAQ_KEYWORD);
                };
                scCTX.SetState(SCE_VARAQ_DEFAULT);
            };
            break;
        };
    };
    scCTX.Complete();
    delete[] tmpStr;
}

void SCI_METHOD LexerVaraq::Fold(unsigned int startPos , int lengthDoc , int initStyle , IDocument *pAccess)
{
    LexAccessor styler(pAccess);
    unsigned int endPos = startPos + lengthDoc;
    char chNext = styler[startPos];
    int lineCurrent = styler.GetLine(startPos);
    int levelPrev = styler.LevelAt(lineCurrent) & SC_FOLDLEVELNUMBERMASK;
    int levelCurrent = levelPrev;
    char ch;
    bool atEOL;

    for (unsigned int i = startPos; i < endPos; i++) {
        ch = chNext;
        chNext = styler.SafeGetCharAt(i+1);
        atEOL = ((ch == ’\r’ && chNext != ’\n’) || (ch == ’\n’));
        if (ch == ’{’) {
            levelCurrent++;
        };
        if (ch == ’}’) {
            levelCurrent --;
        };
        if (atEOL || (i == (endPos -1))) {
            int lev = levelPrev;
            if (levelCurrent > levelPrev) {
                lev |= SC_FOLDLEVELHEADERFLAG;
            };
            if (lev != styler.LevelAt(lineCurrent)) {
                styler.SetLevel(lineCurrent , lev);
            };
            lineCurrent++;
            levelPrev = levelCurrent;
        };
    };
}
LexerModule lmVaraq(SCLEX_VARAQ , LexerVaraq::LexerFactoryVaraq , "varaq", VaraqWordListDesc);

B 신틸라 창을 보여주는 샘플 GTK 코드

이 작은 어플리케이션은 Scintilla 페이지에 있는 bait.c를 간단하게 응용한 것이다.

#include <gtk/gtk>
#include <Scintilla.h>>
#include <SciLexer.h>>
#define PLAT_GTK 1
    #include <ScintillaWidget.h>>
#define MARGIN_SCRIPT_FOLD_INDEX 1

static int exit_app(GtkWidget*w, GdkEventAny*e, gpointer p) {
    gtk_main_quit();
    return w||e||p||1; // Avoid warnings
}

static void scintilla_notify(GtkWidget *sciWidget , gint ctrlID , struct SCNotification *notifyData , gpointer userData)
{
    int lineNr = scintilla_send_message(SCINTILLA(sciWidget), SCI_LINEFROMPOSITION , (uptr_t)notifyData ->position , 0);
    switch (notifyData ->margin) {
    case MARGIN_SCRIPT_FOLD_INDEX:
        scintilla_send_message(SCINTILLA(sciWidget), SCI_TOGGLEFOLD , lineNr , 0);
        break;
    };
}

int main(int argc, char **argv) {
    GtkWidget *app;
    GtkWidget *editor;
    ScintillaObject *sci;
    gtk_init(&argc, &argv);
    app = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    editor = scintilla_new();
    sci = SCINTILLA(editor);
    gtk_container_add(GTK_CONTAINER(app), editor);
    gtk_signal_connect(GTK_OBJECT(app), "delete_event", GTK_SIGNAL_FUNC(exit_app), 0);
    scintilla_set_id(sci, 0);
    gtk_signal_connect(GTK_OBJECT(editor), "sci-notify", G_CALLBACK(scintilla_notify), editor);
    gtk_widget_set_usize(editor , 500, 300);

#define SSM(m, w, l) scintilla_send_message(sci, m, w, l)

    SSM(SCI_STYLECLEARALL , 0, 0);
    SSM(SCI_SETLEXER , SCLEX_VARAQ , 0);
    SSM(SCI_STYLESETFORE , SCE_VARAQ_COMMENT , 0x404040);
    SSM(SCI_STYLESETFORE , SCE_VARAQ_STRING , 0x00FF00);
    SSM(SCI_STYLESETFORE , SCE_VARAQ_NUMBER , 0x20A5DA);
    SSM(SCI_STYLESETFORE , SCE_VARAQ_KEYWORD , 0xFF0000);
    SSM(SCI_SETMARGINTYPEN , MARGIN_SCRIPT_FOLD_INDEX , SC_MARGIN_SYMBOL);
    SSM(SCI_SETMARGINMASKN , MARGIN_SCRIPT_FOLD_INDEX , SC_MASK_FOLDERS);
    SSM(SCI_SETMARGINWIDTHN , MARGIN_SCRIPT_FOLD_INDEX , 20);
    SSM(SCI_SETMARGINSENSITIVEN , MARGIN_SCRIPT_FOLD_INDEX , 1);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDER , SC_MARK_BOXPLUS);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDEROPEN , SC_MARK_BOXMINUS);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDEREND , SC_MARK_EMPTY);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDERMIDTAIL , SC_MARK_EMPTY);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDEROPENMID , SC_MARK_EMPTY);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDERSUB , SC_MARK_EMPTY);
    SSM(SCI_MARKERDEFINE , SC_MARKNUM_FOLDERTAIL , SC_MARK_EMPTY);
    SSM(SCI_SETFOLDFLAGS , SC_FOLDFLAG_LINEAFTER_CONTRACTED , 0);

    SSM(SCI_INSERTTEXT , 0, (sptr_t)
        "(* 99 balmey vo’ Beer Daq var’aq *)\n"
        "(* Sum Rune Berge / klingon mu’mey Sum Tati *)\n"
        "\n"
        "~ balmey {\n"
        "latlh 1 rap\’a\’ \"\" tam\n"
        "{ woD \"s\" } ghobe\’chugh\n"
        "\" bal\" tam tlheghrar tam woD\n"
        "\" vo\’ beer\" tlheghrar\n"
        "} pong\n"
        "\n"
        "~ chuH tlhegh {\n"
        "latlh latlh bottles tlheghrar \" Daq reD\" tlheghrar cha\’\n"
        "latlh latlh bottles tlheghrar cha\’\n"
        "\"tlhap wa\’ bIng je juS \’oH bIng\" cha\’\n"
        "wa\’boqHa\’ latlh latlh { \"ghobe\’\" } ghobe’chugh latlh balmey tlheghrar \" Daq reD\" tlheghrar cha\’\n"
        "\"\" cha\’\n"
        "latlh { chuH tlhegh } HIja\’chugh\n"
        "} pong\n"
        19
        "\n"
        "99 chuH tlhegh\n"
    );

    SSM(SCI_COLOURISE , 0, -1);

    gtk_widget_show_all(app);
    gtk_widget_grab_focus(GTK_WIDGET(editor));
    gtk_main();
    return 0;
}