. 분석 결과 출력

분석기를 ApiEdit와 연결했으면 이제 분석기를 사용하여 구문을 분석하도록 하고 분석된 결과를 화면에 보여주도록 해야 한다. 구문 분석을 하는 시점은 화면에 출력하기 직전인 OnPaint이다. 다음 코드를 OnPaint에 작성한다.

 

void CApiEdit::OnPaint(HWND hWnd)

{

     ....

    Parser->ParseLines(*this,e);

 

     hMemDC=CreateCompatibleDC(hdc);

     ....

 

현재 선택된 분석기 Parser ParseLines 함수를 호출하여 e 줄까지 분석하도록 하였다. OnPaint의 지역변수 e는 화면에 출력할 마지막 줄번호이므로 문서끝까지 분석할 필요없이 e 줄까지만 분석하면 된다. 어차피 e 줄 뒤쪽은 분석해 봐야 화면에 출력되지도 않는다.

OnPaint는 별다른 조건 점검없이 무조건 ParseLines 함수를 호출하는데 이렇게 되면 화면을 그릴 때마다 분석을 다시 할 것 같지만 그렇지는 않다. ParseLines 함수에는 다음과 같은 조건문이 있기 때문에 이미 분석이 완료된 줄은 재분석을 하지 않도록 되어 있다.

 

void CParseCpp::ParseLines(CApiEdit &ae,int nLine)

{

     ....

     if (pInfo[nLine].pUnit[0].pos != -1)

          return;

 

그래서 OnPaint ParseLines 호출은 e 줄까지 당장 분석하라는 명령이 아니라 e 줄이 분석 완료 상태인지 점검하라는 신호이다. ParseLines e 줄의 상태를 보고 분석되어 있지 않은 경우만 재분석하고 이미 분석되어 있으면 아무 것도 하지 않는다. 어쨌든 OnPaint에서 ParseLines를 호출하면 e 줄까지는 분석 완료되며 pInfo[e]는 유효한 분석 정보를 가지게 된다.

분석된 결과를 화면에 출력하는 작업은 DrawLine 함수가 담당한다. 분석 정보가 줄단위로 되어 있으므로 이 정보를 적용하기에 가장 적합한 곳은 역시 한 줄을 출력하는 DrawLine 함수이다. 이 함수는 유닛의 경계를 세그먼트로 분할하여 각 스타일이 지정하는 색상으로 문자열을 출력한다.

 

int CApiEdit::DrawLine(HDC hdc, int Line)

{

    int nUnit;

    int style;

    BOOL bFindUnit;

     ....

 

     for (;;) {

          for (len=0;;) {

              if (buf[nowoff+len] == ‘\t’) {

                   if (len == 0)

                        len=1;

                   if (SelStart != SelEnd && nowoff >= SelFirst && nowoff < SelSecond) {

                        bInSel=TRUE;

                   } else {

                        bInSel=FALSE;

                   }

                   break;

              }

              if (nowoff+len == pLine[Line].End) {

                   if (SelStart != SelEnd && nowoff >= SelFirst && nowoff < SelSecond) {

                        bInSel=TRUE;

                   } else {

                        bInSel=FALSE;

                   }

                   break;

              }

 

              if (SelStart != SelEnd && len != 0 && nowoff+len == SelFirst) {

                   bInSel=FALSE;

                   break;

              }

              if (SelStart != SelEnd && len != 0 && nowoff+len == SelSecond) {

                   bInSel=TRUE;

                   break;

              }

 

           bFindUnit=FALSE;

           for (nUnit=0;nUnit<Parser->pInfo[Line].UnitSize;nUnit++) {

               if (Parser->pInfo[Line].pUnit[nUnit].pos == nowoff+len) {

                   bFindUnit=TRUE;

                   break;

               }

               if (Parser->pInfo[Line].pUnit[nUnit].pos == -1) {

                   break;

               }

           }

 

           if (bFindUnit) {

               if (SelStart != SelEnd && nowoff >= SelFirst && nowoff < SelSecond) {

                   bInSel=TRUE;

               } else {

                   bInSel=FALSE;

               }

 

               if (len != 0)

                   break;

           }

 

              len++;

          }

 

          if (bInSel && (GetFocus()==hWnd || HideSelType!=0)) {

              fore=SelFore;

              back=SelBack;

          } else {

           if (len==0) {

               fore=-1;

               back=-1;

           } else {

               for (nUnit=0;nUnit<Parser->pInfo[Line].UnitSize;nUnit++) {

                   if (Parser->pInfo[Line].pUnit[nUnit].pos >= nowoff+len ||

                       Parser->pInfo[Line].pUnit[nUnit].pos == -1) {

                       break;

                   }

               }

               style=Parser->pInfo[Line].pUnit[nUnit-1].style;

               Parser->GetStyleColor(style,fore,back);

           }

 

           if (fore==(DWORD)-1) {

               fore=cFore;

           }

           if (back==(DWORD)-1) {

               back=cBack;

           }

           if ((nShowCurLine==3 || nShowCurLine==4) && bCurLine) {

               back=CurColor;

           }

          }

 

          DrawSegment(hdc,x,0,nowoff,len,

              (nowoff+len==pLine[Line].End),fore,back);

          ....

}

 

세그먼트를 검색하는 for (len) 루프에서 탭이나 줄 끝, 선택영역의 경계를 세그먼트 경계로 검사하는데 여기에 유닛의 경계도 포함시킨다. pInfo에서 각 유닛의 시작위치도 세그먼트를 나누는 새로운 기준이 된다. 유닛은 스타일이 다른 문자들의 집합이므로 출력 속성도 다르고 따라서 각 유닛은 분리되어 출력되어야 한다. 다음 문장에서 각 유닛은 서로 스타일이 다르므로 유닛 하나가 세그먼트를 구성한다.

현재줄의 pUnit 배열을 모두 검사하여 pos가 현재 위치와 같으면 이 위치가 세그먼트를 나누어야 하는 위치이다. 단 유닛의 경계라 하더라도 길이가 0이면 세그먼트를 구성하지 않는다. 예를 들어 줄의 처음은 유닛의 시작위치이기는 하지만 아직 출력할 내용이 없으므로 세그먼트로 인정되지 않는다.

유닛에 의해서건 선택영역에 의해서건 출력할 세그먼트를 찾았으면 for (len) 루프를 탈출하여 조사된 세그먼트를 출력한다. 이때 유닛이 선택영역에 포함되어 있으면 스타일보다는 선택영역의 색상을 우선한다. 아무리 키워드나 주석이라도 선택영역 안에 있으면 반전하여 보여주는 것이 좋다. 선택영역 안에서도 문법을 보여주는 편집기도 있으나 ApiEdit는 선택영역 내에서는 문법을 무시하기로 한다.

유닛이 선택영역 안에 있지 않다면 적용할 스타일을 구한다. 이때 구해지는 스타일은 세그먼트를 나눈 유닛의 바로 이전 유닛 스타일이다. 직관적으로 잘 이해가 되지 않겠지만 다음 그림을 보면 쉽게 이해가 될 것이다.

유닛3 pos가 현재 위치와 일치하여 세그먼트를 찾았다. 이때 검색된 유닛3.pos는 유닛3의 시작이라는 뜻이 아니라 유닛2의 끝이라는 뜻이며 출력 대상도 유닛2. 따라서 유닛2의 스타일대로 출력해야 한다. 설사 유닛의 경계에서 세그먼트를 찾지 않았다 하더라도 이 규칙은 그대로 적용된다.

선택영역의 시작 지점에서 세그먼트의 경계를 발견했다. 이때 세그먼트 문자열인 ali의 스타일을 찾기 위해 현재 위치(nowoff+len) 다음에 있는 유닛3을 먼저 찾는다. 만약 현재 위치 다음에 유닛이 더 없다면 pos -1인 끝 유닛을 찾거나 아니면 UnitSize 위치를 찾게 된다. nUnit에는 3이 대입되며 pUnit[nUnit-1].style ali문자열에 적용될 스타일이다. 첫 번째 유닛은 항상 길이가 0이므로 세그먼트를 구성할 수 없으며 유닛에 의해 세그먼트가 나누어졌다면 nUint-1은 항상 0이상의 값을 가지므로 안전하다.

특수한 경우로 빈 줄인 경우는 길이가 0이 될 수 있으므로 길이가 0일 때는 스타일을 찾지 말아야 한다. 빈 줄일 때는 아예 출력을 안하도록 하고 싶지만 그럴 수 없는 이유는 엔터코드 보기 옵션이 켜져 있을 때 엔터코드가 출력되어야 하기 때문이다. 이 옵션은 DrawSegment 함수가 점검하므로 길이가 0이더라도 DrawSegment는 호출되어야 한다.

스타일값을 찾았으면 이 스타일로부터 문자열 출력에 사용할 전경색과 배경색을 찾는데 이 색상은 분석기가 정의하고 있다. Parser->GetStyleColor 함수를 호출하면 스타일 출력에 사용할 전경색과 배경색을 조사한다. -1인 경우는 ApiEdit의 기본 색상인 cFore, cBack이 대신 사용된다.

세그먼트를 찾았고 적용할 스타일을 찾았으면 남은 일은 DrawSegment 함수를 호출하여 세그먼트를 출력하는 것뿐이다. 이런 식으로 DrawLine 함수는 분석된 유닛을 세그먼트로 조사하여 분석기가 정의하는 색상대로 화면에 출력한다.

DrawLine 함수에는 pInfo 배열을 검색하는 루틴이 두 군데 있다. for (len) 루프 안에 하나 있고 이 루프 바깥에 또 하나가 더 있는데 두 루틴은 하는 일이 각각 다르다. for (len) 루프는 세그먼트를 검색하는 기능을 하므로 이 루프안의 pInfo 검색문은 여기가 과연 유닛에 의해 세그먼트를 나눌 자리인가 아닌가만 검사한다.

for (len) 루프 바깥의 pInfo 검색문은 현재 조사된 세그먼트에 어떤 스타일을 적용할 것인가를 조사한다. 두 루틴의 구조가 비슷하여 통합할 수도 있을 것 같다. 즉 세그먼트의 경계를 찾으면서 동시에 어떤 스타일을 적용할 것인가를 같이 조사하면 코드량을 줄일 수 있다. 가능한 일이기는 하지만 세그먼트가 반드시 유닛에 의해서만 나누어지는 것이 아니기 때문에 오히려 코드가 더 복잡해질 수도 있어 두 번 나누어 작업하였다.

 

이상으로 OnPaint DrawLine 함수를 통해 분석결과를 화면에 출력하는 코드를 작성했다. 직접 코드를 작성해 봐서 알겠지만 구문 분석을 하는 루틴과 적용하는 루틴은 엄격하게 분리되어 있다. 그래서 분석기는 자신이 알고 있는 문법에 따라 pInfo에 결과만 제대로 작성해놓으면 ApiEdit는 항상 정확하게 이 결과를 출력한다. 분석기와 ApiEdit는 오직 유닛의 pos style로부터 구해지는 색상으로만 통신한다. ApiEdit는 분석기가 내부적으로 Context style을 어떻게 정의하는가에 대해서는 전혀 관심이 없으며 신경쓰지도 않는다. 관심 대상은 pos로부터 어디를 자를 것인가, 그리고 style로부터 구해지는 fore, back 색상뿐이다.

구문 분석을 하는 루틴과 결과를 적용하는 루틴이 완전히 분리되어 있기 때문에 각 루틴을 개발할 때 해당 부분의 논리에만 치중할 수 있다. 분석 루틴은 출력에 대해 신경쓰지 않아도 되며 출력루틴은 분석에 대해서는 신경쓸 필요가 없는 것이다. 또한 두 코드의 책임소재가 분명하기 때문에 문제가 생겨도 이를 빨리 발견할 수 있다. 구문 분석이 제대로 안될 경우 pInfo만 검사해보면 분석이 잘못되었는지 아니면 적용을 잘못했는지를 금방 알 수 있는 것이다.