. 블록 들여쓰기

블록 들여쓰기는 이미 입력된 문장의 들여쓰기를 일괄적으로 조정하는 방법이다. 조정하고자 하는 문단을 블록으로 선택해놓고 <Tab>키를 누르면 블록 내의 모든 문단들을 한 칸 더 들여쓰고 <Shift+Tab>을 누르면 한 칸 내어쓰는 기능이다. 편집에 의해 블록의 구조가 바뀌었다거나 할 경우 이 기능으로 블록을 조정할 수 있다. 예를 들어 다음 왼쪽 문장을 오른쪽처럼 변경한다거나 반대로 오른쪽 문장을 왼쪽처럼 변경하고자 할 때 이 기능을 사용한다.

 

if (조건1) {

     if (조건2) {

          명령1;

          명령2;

          명령3;

     }

}

if (조건1 && 조건2) {

     명령1;

     명령2;

     명령3;

}

 

 

보다시피 두 문장은 완전히 같은 문장인데 명령들이 속한 블록의 들여쓰기가 일괄적으로 조정되어야 할 필요가 있다. 이럴 때 명령1~명령3까지 선택해놓고 <Tab>이나 <Shift+Tab>을 누르면 된다. 비주얼 스튜디오가 이 기능을 지원하고 있고 여러분들도 이미 사용해 본 적이 있으므로 기능 자체는 쉽게 이해가 갈 것이다.

이 기능은 소스와 같이 들여쓰기로 구조를 표현하는 문서에 특히 유용하다. 하지만 순수한 텍스트의 경우는 오히려 이 기능이 부자연스러울 수도 있다. 선택영역이 있는 상태에서 문자를 입력하는 동작은 선택영역을 대체하는 것으로 정의되어 있는데 <Tab>키에 대해서만 특수한 예외를 인정하는 것이므로 선택영역을 지우고 Tab을 입력할 수는 없게 된다. 그래서 이 기능의 사용 여부는 bBlockIndentWithTab 옵션으로 선택할 수 있도록 하되 디폴트값은 TRUE이다.

블록 들여쓰기를 할 시점은 선택영역이 있는 상태에서 <Tab>키를 눌렀을 때이다. <Tab>키를 입력받는 시점은 OnChar이므로 이 함수에서 블록 들여쓰기를 처리하되 코드량이 많기 때문에 직접 처리하지 않고 따로 함수를 만들고 OnChar에서는 이 함수만 호출한다. 탭키입력 부분에 다음 코드를 추가한다.

 

void CApiEdit::OnChar(HWND hWnd, TCHAR ch, int cRepeat)

{

     ....

     if (ch==‘\t’) {

          if (bSpaceForTab) {

              for (i=0;i<TabWidth;i++) {

                   szChar[i]=‘ ‘;

              }

              szChar[i]=0;

          }

        if (bBlockIndentWithTab && SelStart != SelEnd) {

           BlockIndent((GetKeyState(VK_SHIFT) & 0x8000) != 0);

           return;

        }

     }

 

BlockIndent 함수는 선택된 블록의 들여쓰기를 조정하되 인수로 전달된 bUnindent TRUE이면 내어쓰기를 하고 FALSE이면 들여쓰기를 한다. 이 인수의 값은 <Shift>키가 눌러져 있는가 아닌가로 쉽게 결정할 수 있다. <Tab>키입력에 의해 블록 들여쓰기를 했으면 OnChar는 더 이상 <Tab>키입력을 다룰 필요가 없으므로 리턴해야 한다.

BlockIndent 함수의 코드는 다음과 같이 작성한다. 길이가 짧지 않은 것으로 봐서 그다지 간단하지는 않은 모양이다. 지금까지 작성했던 대소문자 변환, 공백과 탭의 변환 코드와 전체적인 구조는 비슷하지만 고려해야 할 것들이 좀 더 많다.

 

void CApiEdit::BlockIndent(BOOL bUnindent)

{

     int SelFirst, SelSecond;

     int st,ed;

     int pr1,pr2,pc;

     int Line;

     TCHAR *src,*dest;

     TCHAR *s,*d;

     int i;

     int nTab;

     TCHAR szIndent[1024];

     int nDec, nDiff;

     BOOL bFirstPara=TRUE;

 

     if (SelStart==SelEnd) {

          SelFirst=SelSecond=off;

     } else {

          SelFirst=min(SelStart,SelEnd);

          SelSecond=max(SelStart,SelEnd);

     }

 

     GetParaFromOff(SelFirst,pr1,pc);

     Line=GetParaFirstLine(pr1);

     st=pLine[Line].Start;

 

     GetParaFromOff(SelSecond,pr2,pc);

     Line=GetParaFirstLine(pr2);

     if (SelSecond==pLine[Line].Start && SelStart!=SelEnd) {

          pr2--;

     }

     Line=GetParaLastLine(pr2);

     ed=pLine[Line].End;

 

     src=(TCHAR *)malloc(ed-st+1);

     dest=(TCHAR *)malloc(ed-st+1+(pr2-pr1+1)*TabWidth);

     lstrcpyn(src,buf+st,ed-st+1);

 

     s=src;

     d=dest;

     nDiff=0;

     if (!bUnindent) {

          for (;;) {

              if (!IsParaEmpty(s)) {

                   MakeIndentString(1,d,4096);

                   d=d+lstrlen(d);

                   nDiff++;

              }

 

              while (*s!=‘\r’ && *s != 0) {

                   *d++=*s++;

              }

              if (*s==0) {

                   *d=*s;

                   break;

              } else {

                   *d++=*s++;

                   *d++=*s++;

              }

          }

 

          if (SelFirst!=st) {

              SelFirst+=(bSpaceForTab ? TabWidth:1);

          }

          SelSecond+=(bSpaceForTab ? TabWidth:1)*nDiff;

     } else {

          for (;;) {

              nTab=GetIndentLevel(s)/TabWidth;

 

              nTab=max(0,nTab-1);

 

              MakeIndentString(nTab, szIndent,4096);

 

              nDec=0;

              while (AeIsWhiteSpace(*s)) {

                   s++;

                   nDec++;

              }

 

              for (i=0;i<lstrlen(szIndent);i++) {

                   *d++=szIndent[i];

              }

 

              nDiff+=(nDec-lstrlen(szIndent));

              if (bFirstPara) {

                   SelFirst-=nDiff;

                   SelFirst=max(SelFirst,st);

                   bFirstPara=FALSE;

              }

 

              while (*s!=‘\r’ && *s != 0) {

                   *d++=*s++;

              }

              if (*s==0) {

                   *d=*s;

                   break;

              } else {

                   *d++=*s++;

                   *d++=*s++;

              }

          }

          SelSecond-=nDiff;

     }

 

     StartUndoGroup();

     Delete(st,lstrlen(src));

     Insert(st,dest);

     EndUndoGroup();

 

     if (SelStart == SelEnd) {

          off=SelStart=SelEnd=SelSecond;

     } else if (SelStart < SelEnd) {

          off=SelEnd=SelSecond;

          SelStart=SelFirst;

     } else {

          off=SelEnd=SelFirst;

          SelStart=SelSecond;

     }

 

     Invalidate(st);

     SetCaret();

     free(src);

     free(dest);

}

 

다른 변환 함수들과 마찬가지로 먼저 변환할 대상을 고르는데 선택영역이 있을 때는 선택영역이 걸쳐 있는 문단들이 그 대상이 되며 선택영역이 없으면 현재 캐럿이 있는 문단만 대상이 된다. 왜 선택영역이 없을 때의 처리를 할 필요가 있을까? OnChar에서 이 함수를 호출할 때 선택영역이 있음을 이미 확인했기 때문에 이런 경우는 절대로 없다. 선택영역이 없는 상태에서 <Tab>키입력은 블록 들여쓰기 명령이 아니라 단순한 탭문자입력에 불과하므로 먼저 선택을 하지 않으면 블록 들여쓰기를 할 수 없다. 이것은 ApiEdit 컨트롤 수준에서는 분명한 사실이다.

그러나 호스트의 입장에서는 그렇지가 않다. 호스트는 블록 들여쓰기를 <Tab>키입력으로 하는 것이 아니라 메뉴나 액셀러레이터로 한다. 선택영역이 있건 없건 상관없이 메뉴로 이 명령을 전달할 수 있으며 그래서 BlockIndent 함수는 선택영역이 없을 때 현재 문단에 대해서 블록 들여쓰기를 처리할 수 있어야 한다. 만약 이 상황이 잘 이해가 안 간다면 OnChar에서 이 함수를 부를 때와 호스트가 이 함수를 부를 때의 차이점에 대해 생각해보자. OnChar <Tab>키가 입력되었을 때 이것이 탭문자입력인지 블록 들여쓰기인지를 선택영역의 유무로 구분해야 하지만 호스트는 블록 들여쓰기라는 명시적인 명령을 가지고 있으므로 그럴 필요가 없는 것이다.

변환 대상은 오프셋의 범위로 조사되는데 이 범위는 실제 선택영역보다 더 확대된다. 블록 들여쓰기의 조정 대상은 선택 시작점의 문단 선두에서부터 선택 끝점의 문단 끝까지이며 이 범위의 모든 문자열이 조정 대상이다. 다음 그림을 보자. 각 줄은 자동개행된 것이 아니라 개행코드로 분리된 문단이다.

보다~싶으면까지 선택되어 있는데 만약 변환 대상을 선택영역에만 국한해 버린다면 실제 앞에는 들여쓰기가 되지 않을 것이다. 사용자가 저런 모양으로 선택해놓은 것은 선택영역이 걸친 문단 모두에 대해 블록 들여쓰기를 하라는 의사 표현을 한 것으로 간주해야 한다. 그렇지 않으면 사용자는 블록 들여쓰기할 문단 전체를 완전히 정확하게 다 선택해야 하는데 이렇게 하자면 무척 피곤해진다. 사용자가 대충 선택해 놔도 그 의도를 제대로 파악하기 위해 선택영역을 확대하여 변경 대상을 선정했는데 이는 사용자에 대한 일종의 서비스다.

선택의 끝이 문단 선두일 때는 그 문단은 변경 대상에서 제외된다. 앞의 그림에서 세 번째 문단 이 그림을\r\n까지 선택되어 있다고 할 때 SelEnd는 다음 문단의 첫 글자인 을 가리키고 있을 것이다. 이때 마지막 문단은 변환 대상에서 제외되는 것이 옳다. 왜냐하면 범위의 법칙에 따라 선택의 끝점은 선택영역에 포함되지 않기 때문이다. , 선택영역이 아예 없으면 한 문단에 대해서만 변환을 하는 것이기 때문에 설사 문단 선두라도 포함시켜야 한다.

변환 대상을 선정하는 조건들이 참 복잡한데 이런 조건은 설명을 읽는 것보다 코드를 읽고 이해하는 것이 더 빠를 것 같다. 대상 선정의 결과는 st, ed 변수에 오프셋으로 계산되며 이 오프셋 범위에 있는 모든 문자열이 변환 대상이다. 블록 들여쓰기를 하면 변환 전과는 길이가 달라지므로 src, dest 두 개의 버퍼를 준비했다. src에는 원본 문자열을 복사했으며 dest는 변환 후의 길이를 고려하여 충분한 길이를 할당했다. 변환 후의 최대 길이는 원본 길이+문단수*탭폭으로 계산된다. bSpaceForTab TRUE일 경우 탭문자 대신 공백이 삽입되므로 각 문단마다 최대 TabWidth만큼 늘어날 수 있다.

변환 대상이 준비되면 bUnindent 인수값에 따라 변환을 시작한다. 먼저 블록 들여쓰기 코드를 보자. src의 처음부터 각 문단의 선두에 한 칸씩 더 들여쓰기를 하여 dest로 복사하기를 src의 끝까지 반복하면 된다. 이때 빈 문단에는 들여쓰기를 할 필요가 없다. 들여쓰기를 해 봐야 불필요한 공백만 추가될 뿐 눈에 보이지도 않는다.

변환중에 nDiff 변수는 들여쓰기된 횟수를 계산하는데 들여쓰기를 할 때는 문자가 삽입되므로 nDiff가 늘어나기만 할 것이다. 변환 후에 nDiff는 변환 대상 문단 개수 - 빈 문단 개수가 될 것이다. 들여쓰기가 완료된 후 선택 시작점은 들여쓰기된만큼 이동시키고 선택 끝점은 총 삽입된 바이트 수만큼 뒤로 이동시켜 들여쓰기를 한 후에도 선택영역이 원래의 문자열을 가리키도록 했다.

, 선택 시작점과 변환 시작점이 같을 경우는 선택 시작점을 그대로 두어 추가된 들여쓰기 문자열까지 같이 선택하도록 한다. 위 그림에서 만약 if부터 선택되어 있었다면 들여쓰기에 의해 삽입된 탭문자도 같이 선택한다. 이렇게 하는 특별한 이유는 없고 그렇게 하는 것이 보기에 좋기 때문이다.

다음은 내어쓰기 코드를 보자. 들여쓰기에 비해서는 조금 더 복잡한데 첫 문단부터 원본의 들여쓰기를 탭 단위로 조사한 후 이 값을 1빼서 들여쓰기 문자열을 만들고 dest로 출력하기를 마지막 문단까지 반복하면 된다. , 원본 문단이 들여쓰기되어 있지 않다면 더 이상 내어쓰기를 할 필요가 없다.

내어쓰기를 할 때는 원본 문자열보다 결과 문자열이 보통 짧아지므로 nDiff는 감소된 바이트 수를 계산한다. 하지만 아주 특수한 경우 결과 문자열이 더 길어질 수도 있다. 아니! 문단 앞쪽에 있는 들여쓰기 문자열을 줄여 나가는데 어떻게 결과 문자열이 더 늘어날 수 있다는 말인가? 이런 궁금증을 가지는 것이 당연할 것이다. 아래 설명을 읽기 전에 어째서 그런지 재미있는 퀴즈를 푼다는 기분으로 2분만 고민해보면 그럴 수도 있다는 생각이 들 것이다.

원본 문자열에 두 개의 탭이 있을 때 내어쓰기를 하면 하나의 탭으로 줄어들 것이다. 하지만 bSpaceForTab TRUE일 경우 두 개의 탭이 네 개의 공백 문자로 바뀌게 되는데 이때는 결과 문자열이 원본 문자열보다 더 길어진다. 선택의 시작점은 첫 줄이 감소된 만큼 감소하되 단 문단의 처음보다 더 앞쪽으로 이동할 수는 없다. 선택의 끝점은 nDiff만큼 감소시켜주면 된다.

변환이 끝난 후 원본 문자열을 삭제하고 결과 문자열을 삽입하여 변환 결과를 문서에 반영한다. 물론 두 동작은 하나의 편집 그룹으로 묶어야 한다. 이제 블록을 선택해놓고 <Tab>, <Shift+Tab>을 누르면 블록 들여쓰기를 할 수 있을 것이다. 호스트에서도 이 기능을 호출할 수 있도록 메뉴 명령을 처리한다.

 

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

{

     ....

     case IDM_EDIT_INDENT:

          pSi->Ae.BlockIndent(FALSE);

          break;

     case IDM_EDIT_UNINDENT:

          pSi->Ae.BlockIndent(TRUE);

          break;

 

호스트에서 이 함수를 호출할 때는 선택영역이 있는지 없는지 점검할 필요가 없다. 선택영역이 없으면 캐럿 위치의 문단에 대해서만 들여쓰기 내어쓰기를 한다. 이 두 명령은 컨트롤이 <Tab>키입력을 처리하기 때문에 별도의 액셀러레이터를 정의하지 않았다.