. 괄호 짝 찾기

괄호 짝 찾기 기능은 여는 괄호와 닫는 괄호의 짝을 찾아 주며 특히 프로그래밍 소스를 편집할 때 유용하다. C 소스의 경우 루프나 조건문들이 중첩되다 보면 { 괄호와 } 괄호가 반복적으로 나와 괄호의 짝을 육안으로 찾기가 무척 피곤하다. 또한 함수나 길이가 긴 루틴의 경우 {로 연 후 몇 백줄 후에 }가 나타나기도 하는데 이럴 때 함수의 처음이나 끝으로 빠르게 이동하는 방법을 제공하기도 한다.

짝 찾기의 대상이 되는 괄호는 ( ), { }, [ ], < > 4가지 종류가 있다. 여는 괄호에서 짝 찾기 명령을 실행하면 닫는 괄호를 찾아주고 닫는 괄호에서 짝찾기 명령을 실행하면 여는 괄호를 찾아준다. 현재 위치에서 앞뒤 오프셋으로 이동하면서 대응되는 문자를 찾는 문제이므로 별로 어렵지 않다. 여러분들이 직접 실습해보더라도 불과 10여분이면 해결할 수 있을 정도로 쉬우므로 먼저 시도해보기 바란다. 결과 함수는 다음과 같다. 길이만 길지 아주 쉽다.

 

void CApiEdit::FindParen(BOOL bSelect)

{

     TCHAR ch,ch2;

     BOOL bFind;

     int Inc;

     int toff;

     int level;

     int i;

     int OldOff;

 

     bFind=FALSE;

     for (i=0;i>=-1;i--) {

          if (i==-1 && off==0)

              break;

          ch=buf[off+i];

          if (ch == ‘(‘ || ch == ‘{‘ || ch == ‘[‘ || ch == ‘<‘) {

              Inc=1;

              bFind=TRUE;

              break;

          }

          if (ch == ‘)’ || ch == ‘}’ || ch == ‘]’ || ch == ‘>‘) {

              Inc=-1;

              bFind=TRUE;

              break;

          }

     }

 

     if (bFind==FALSE) {

          return;

     }

     OldOff=off+i;

 

     switch (ch)

     {

     case ‘(‘: ch2=‘)’; break;

     case ‘{‘: ch2=‘}’; break;

     case ‘[‘: ch2=‘]’; break;

     case ‘<‘: ch2=‘>‘; break;

     case ‘)’: ch2=‘(‘; break;

     case ‘}’: ch2=‘{‘; break;

     case ‘]’: ch2=‘[‘; break;

     case ‘>‘: ch2=‘<‘; break;

     }

 

     level=0;

     for (toff=OldOff;;) {

          toff+=Inc;

 

          if (toff == doclen || toff == -1) {

              bFind=FALSE;

              break;

          }

 

          if (buf[toff]==ch2) {

              if (level == 0) {

                   bFind=TRUE;

                   break;

              }

              level--;

          }

          if (buf[toff]==ch)

              level++;

     }

 

     if (bFind) {

          off=toff;

          if (bSelect) {

              if (Inc==1) {

                   off++;

              } else {

                   OldOff++;

              }

              ExpandSelection(OldOff,off);

          } else {

              ClearSelection();

          }

          SetCaret();

     }

}

 

이 함수는 현재 위치의 괄호와 짝이 되는 괄호로 이동하되 bSelect 인수가 TRUE이면 두 괄호 사이의 문자열을 선택한다. 먼저 현재 위치에 어떤 종류의 괄호가 있는지 조사해보되 현재 위치에서 괄호가 발견되지 않으면 그 앞쪽 위치도 검사해 본다.

캐럿은 현재 위치의 문자 앞쪽에 표시되므로 원칙적으로 괄호 앞에 캐럿을 둔 상태여야만 현재 위치의 문자가 괄호가 된다. 하지만 사람은 컴퓨터가 아니므로 괄호 뒤에 캐럿을 놓고도 이 기능이 동작하리라고 기대하므로 그 기대에 부응하기 위해 현재 위치를 먼저 검사해보고 이 위치에서 괄호가 발견되지 않으면 바로 앞쪽도 같이 점검해보도록 하였다.

발견된 괄호의 종류가 { [ ( < 중 하나이면 여는 괄호에서 짝찾기를 실행한 것이므로 아래쪽으로 이동하면서 닫는 괄호를 찾아야 한다. 반대로 } ] ) > 중 하나이면 이 괄호보다 더 앞쪽에 있는 여는 괄호를 찾는 것이므로 위로 이동해야 한다. 발견된 괄호의 종류에 따라 Inc 변수에 검색 방향을 대입해두었다. 현재 위치나 한 칸 앞쪽에서 괄호가 발견되지 않았으면 짝을 찾을 수가 없으므로 그냥 리턴한다.

ch는 최초 검색을 시작한 괄호이며 ch2는 이 괄호와 대응되는 괄호이다. ch ( 이면 ch2 ) 가 될 것이고 ch } 이면 ch2 { 가 된다. 이제 Inc 방향으로 오프셋을 증감시키면서 ch2괄호가 있는 오프셋을 찾기만 하면 된다. , 괄호끼리 서로 중첩될 수 있기 때문에 레벨을 관리하면서 같은 레벨의 대응되는 괄호를 찾아야 한다. 다음 그림을 보자.

최초 여는 괄호에서 검색을 시작했으므로 아래쪽으로 이동하면서 닫는 괄호를 찾아야 한다. 이 문장에는 d다음 e다음 f다음에 각각 닫는 괄호가 있는데 d e다음의 닫는 괄호는 a앞의 여는 괄호와 대응되지 않는다. 이 괄호들은 a앞의 괄호에 포함되는 것이지 a괄호를 끝내는 것이 아니다. 대응되는 괄호를 정확하게 찾으려면 괄호간의 중첩을 관리해야 한다.

FindParen 함수는 루프를 돌기 전에 level 0으로 초기화하고 검색 시작위치에서 Inc 방향으로 이동하면서 ch ch2를 찾는다. ch를 만나면 level 1증가시켜 괄호 안쪽에서 또 다른 괄호가 시작되었음을 기억하고 ch2를 만나면 level 1감소시켜 하나의 괄호가 끝났음을 기억한다. 이렇게 루프를 돌다가 ch2를 레벨 0에서 만났다면 이 괄호가 바로 찾는 괄호가 된다. 만약 문서 끝이나 처음에 이를 때까지 이 조건에 맞는 괄호를 만나지 못했다면 문서의 구조에 문제가 있는 것이다.

대응되는 괄호를 찾았다면 그 위치로 캐럿을 옮겨준다. 만약 bSelect TRUE이면 검색 시작위치에서 검색된 위치까지 선택하는데 마지막 닫는 괄호까지 완전히 선택될 수 있도록 off 또는 OldOff를 적당히 증감시킨다. 선택영역을 확장하기 위해 ApiEdit5에서 만들어 두었던 ExpandSelection 함수를 사용하였다.

OnCommand에서 괄호 찾기 항목이 선택되면 이 함수를 불러 준다. 검색/괄호 짝 찾기 명령의 ID IDM_SEARCH_PAREN이며 이 명령의 단축키 <Ctrl+B>가 액셀러레이터 테이블에 정의되어 있다. FindParen(FALSE)만 호출하면 된다.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     case IDM_SEARCH_PAREN:

          pSi->Ae.FindParen(FALSE);

          break;

     case IDM_SEARCH_PARENSEL:

          pSi->Ae.FindParen(TRUE);

          break;

 

IDM_SEARCH_PARENSEL 명령은 메뉴에는 없으며 단축키 <Ctrl+Shift+B> 액셀러레이터로만 정의되어 있다. <Shift>키와 커서이동키의 조합이 선택영역 확장이라는 일반적인 의미를 가지므로 <Ctrl+B> <Shift>키와 함께 누르면 괄호 짝을 찾아 선택할 것이라는 것을 쉽게 유추할 수 있으므로 메뉴에는 굳이 이 명령을 두지 않았다.

괄호 짝 찾기 기능을 완성하기는 했는데 이 기능은 완전하지 않다. FindParen 함수는 단순히 괄호의 문자코드로만 검색을 할 뿐 문법적인 요소는 전혀 고려하지 않는다. 그래서 문자열 상수나 주석문 내에 있는 괄호도 괄호로 인정하는 맹점이 있다. 정확하게 검색을 하자면 괄호 짝찾기 기능은 CApiEdit에서 정의해서는 안되면 CParse의 파생 클래스에 정의해야 한다. 괄호의 짝을 찾을 때 컨텍스트를 고려해 가며 구두점으로 인정되는 괄호만 검색해야 하는데 이는 무척 어려운 일일뿐만 아니라 시간을 투자할 만큼 그다지 가치있는 일도 아닌 것 같아 그렇게까지 하지는 않았다.