. 재분석

한 번 분석해놓은 분석 정보는 현재 상태에서만 유효하다. 만약 한 문자라도 삽입되거나 삭제되면 편집된 위치 이후는 다시 분석해야 한다. 한 줄 내에서 편집이 일어났다 하더라도 그 뒷줄 이하는 전부 다시 분석해야 한다. 만약 /* "같이 다음 줄에도 영향을 주는 문자가 입력되었다면 문서 끝까지 영향을 줄 수 있기 때문이다. 그러나 다행히 어떤 문자가 입력되더라도 문서의 앞쪽 문단으로는 영향을 주지 않는다.

ApiEdit는 문서가 편집될 때나 또는 문서의 형태가 바뀔 가능성이 있는 모든 부분에서 구문 분석을 다시 하도록 해야 한다. 그러나 문서 내용이 바뀌었을 때 강제로 분석기를 호출하여 재분석할 필요는 없다. 만약 편집할 때마다 재분석을 한다면 프로그램의 반응이 느려질 뿐만 아니라 중복 분석을 하게 될 것이다.

구문 분석은 결국 화면출력을 위해 하는 것이므로 문서가 변경되더라도 당장 재분석할 필요없이 다음 번 화면출력시 재분석하면 된다. 그래서 재분석이 필요하면 즉시 재분석할 것이 아니라 기회가 되면 분석하라는 표시만 해놓으면 된다. 이것을 분석 정보 무효화라고 하며 분석 정보가 무효화되면 ParseLines에서 알아서 재분석을 하게 된다. 문서가 변경되는 대표적인 경우는 Insert 함수에서 문자를 삽입할 때인데 이 함수에서 분석 정보를 무효화한다.

 

void CApiEdit::Insert(int nPos, TCHAR *str, BOOL bRec/*=TRUE*/)

{

     ....

     if (bReadOnly)

          return;

 

    // 여기서 분석 정보 무효화

     ....

 

분석 정보를 무효화할 때는 분석기의 DeleteParseInfo 함수를 호출하는데 이 함수는 pInfo의 지정한 줄 이하에 있는 모든 유닛을 삭제한다. 다음 번 화면출력시 OnPaint ParseLines를 호출할 것이고 ParseLines는 출력 대상 줄의 분석 정보가 무효화된 것을 확인하고 재분석할 것이다. 이 함수가 OnPaint로 리턴했을 때는 그려야 할 모든 줄이 이미 분석 완료된다. 즉 필요한 줄이 모두 유효화된다.

이런 무효화/유효화 전략은 Win32 GDI의 무효화 전략과 동일하다. 화면 갱신이 필요할 때 즉시 그리는 것이 아니라 InvalidateRect 함수로 화면을 무효화해놓으면 다음 번 WM_PAINT 메시지를 받았을 때 이 영역을 다시 그리게 되는 것처럼 구문 분석 정보도 다시 작성할 필요가 있으면 아예 이 정보를 삭제하여 다시 만들도록 하는 것이다. Delete 함수에도 동일한 처리가 필요하다. 문자가 삭제될 때도 삭제된 이후 줄의 분석 정보를 무효화하여 재분석하도록 해야 한다.

Insert, Delete 와 같이 문서에 직접적인 변화를 주는 곳 외에도 분석 정보를 무효화해야 할 경우가 아주 많이 있다. 자동개행 상태가 바뀌면 줄의 배치상태가 바뀌므로 분석 정보는 모두 무효화된다. 글꼴이나 탭 크기가 바뀌면 문서의 구조가 완전히 바뀌므로 처음부터 다시 재분석해야 하며 마진의 보기 상태도 분석결과에 영향을 미친다. 더 자주 일어나는 일로는 자동개행 상태에서 윈도우 크기를 바꿀 때인데 이 때도 모든 줄이 완전히 재구성되므로 문서를 처음부터 다시 분석해야 한다.

Insert, Delete, OnSize, SetWrap, SetTabWidth, SetFont 등의 함수 곳곳에 DeleteParseInfo 함수를 호출해야 한다는 얘기다. 그러나 다행히 이 함수들이 호출되는 시점과 구문 분석을 다시 해야 할 시점간의 공통점이 있는데 바로 문서 전체 또는 일부가 재정렬된다는 점이다. 그래서 개별 함수에서 분석 정보 무효화를 할 필요없이 UpdateLineInfo 함수에서 딱 한 번만 DeleteParseInfo 함수를 호출하면 된다.

 

void CApiEdit::UpdateLineInfo(int nPos/*=-1*/, int nCount/*=-1*/)

{

     int l,s,e;

     int nPara, nLine=0;

     int ParaStart;

     int ps=-1,pe=-1,pp;

     int dPara;

     int i;

 

     if (nPos==-1) {

          l=0;

          nPara=0;

     } else {

          ParaStart=FindParaStart(nPos);

          if (ParaStart == 0) {

              l=0;

              nPara=0;

          } else {

              GetRCFromOff(ParaStart-2,l,e);

              nPara=pLine[l].nPara+1;

              l++;

          }

     }

    Parser->DeleteParseInfo(l);

 

     for (;;l++) {

     ....

 

nPos -1일 때는 문서 전체를 재정렬 할 때이므로 0번 줄 이후 즉, 모든 분석 정보를 삭제한다. Insert, Delete 함수에서 UpdateLineInfo 함수를 호출할 때는 최적화 코드에 의해 nPos -1이 아닌데 이때는 nPos가 속한 문단의 첫 줄부터 분석 정보를 삭제하면 된다. 재정렬할 대상 줄과 분석 정보를 삭제할 줄이 동일한데 이 줄번호는 이미 변수 l에 조사되어 있으므로 l줄 이후의 분석 정보를 삭제하면 된다. 이렇게 삭제된 분석 정보는 다음 번 화면출력시 ParseLines 함수에 의해 다시 만들어진다.

결국 문서에 조금이라도 변화가 있을 때마다 문서가 재분석되는데 키보드를 두드릴 때마다 다시 분석한다고 생각하면 속도가 많이 느려질 것 같고 CPU를 너무 괴롭힐 것 같아 안타깝기까지 하다. 하지만 그 분량이 기껏 한 페이지에 국한되기 때문에 실행속도에는 크게 불리하지 않으며 더구나 화면 아래쪽에서 입력중일 때는 겨우 두세 줄 정도밖에 재분석되지 않는다. 이런 최적화는 이미 분석기를 기획할 때부터 고려되었던 것이다.

여기까지 코드를 작성한 후 실행해보면 C 분석기의 동작을 직접 확인할 수 있다. , 아직 CParsetHtml, CParseSql 객체가 아직 만들어지지 않았으므로 SetParser에서 이 두 줄은 주석으로 잠시 막아 두어야 예제를 실행해 볼 수 있다.