. 단어 단위 이동

키보드로 블록을 선택할 때는 <Shift>키와 커서이동키의 조합을 사용하는데 <Shift>키를 누른 채로 이동하면 캐럿의 이동을 따라가며 블록이 확장된다. 블록선택 모드라는 별도의 모드를 정의하는 편집기들도 있는데 <Shift>키를 사용하는 방법이 가장 직관적이며 대부분의 편집기가 이 방식을 채택하고 있다.

커서이동키를 누를 때 <Shift>키의 상태에 따라 키의 기능이 달라져야 하는데 이는 <Ctrl>키에 대해서도 마찬가지이다. <Ctrl>키와 커서이동키의 조합도 결합되는 키에 따라 고유의 기능을 가진다. 이 조합키의 기능은 편집기마다 조금씩 다를 수도 있는데 ApiEdit는 다음과 같이 Ctrl 조합키의 기능을 정의하고 있다.

 

조합키

기능

Ctrl+,

단어 단위로 이동한다.

Ctrl+,

캐럿을 제자리에 두고 화면을 스크롤한다.

Ctrl+Home, End

문서의 처음과 끝으로 이동한다.

Ctrl+PgUp, PgDn

기능이 없다.

 

<Shift>키와 <Ctrl>키를 동시에 누르는 조합도 가능하다. 예를 들어 <Ctrl>키와 <Shift>키를 누른 채로 왼쪽으로 이동하면 단어 단위로 이동하면서 블록을 확장한다. 키보드 선택 기능은 이런 복잡한 키 조합에 따라 위치를 이동하면서 선택영역을 관리해야 하는데 먼저 <Ctrl>키 처리에 필요한 함수부터 만들어보자.

제자리 스크롤이나 문서의 처음과 끝으로 이동하는 기능은 메시지 핸들러에서 바로 처리할 수 있으나 단어 단위 이동은 다소 복잡하므로 별도의 함수를 만드는 것이 좋을 것 같다. ApiEdit4프로젝트로부터 새로운 프로젝트 ApiEdit5를 작성하고 다음 세 함수를 추가한다.

 

BOOL IsDelimiter(int nPos)

{

     static TCHAR deli[]=TEXT(" \t\r\n\"\’\\.,<>:;/()[]{}~!@#$%^&*-+?=");

     return (strchr(deli,buf[nPos]) || buf[nPos]==0);

}

 

int GetPrevWord(int nPos)

{

     if (nPos == 0)

          return nPos;

 

     for (nPos--;;nPos--) {

          if (IsDelimiter(nPos)==FALSE || nPos==0) {

               break;

          }

     }

 

     for (;;nPos--) {

          if (IsDelimiter(nPos)==TRUE || nPos==0) {

               break;

          }

     }

 

     if (nPos != 0)

          nPos++;

     return nPos;

}

 

int GetNextWord(int nPos)

{

     for (;;nPos++) {

          if (IsDelimiter(nPos)==TRUE) {

               break;

          }

     }

 

     for (;;nPos++) {

          if (IsDelimiter(nPos)==FALSE || buf[nPos]==0) {

               break;

          }

     }

     return nPos;

}

 

단어란 구분자에 의해 나누어진 글자의 집합인데 보통 구분자는 공백인 경우가 대부분이지만 탭이나 개행코드도 단어를 나누는 구분자가 된다. 뿐만 아니라 쉼표나 마침표 같은 구두점들도 구분자가 되는데 IsDelimiter 함수는 nPos 위치의 문자가 구분자인지 아닌지를 조사한다. deli 배열에 구분자로 사용되는 문자들이 정의되어 있으며 buf[nPos] 문자가 deli 배열에 있는지 검사하여 그 결과를 리턴한다. buf[nPos] 0인 경우, 즉 문서의 끝인 경우도 구분자로 인정된다. deli 배열은 지역변수이지만 최초 한 번만 초기화하도록 하기 위해 static으로 선언하였다.

어떤 문자가 구분자가 될 것인가는 편집하는 문서에 따라 조금씩 달라질 수 있다. C 소스파일에서의 구분자와 HTML 파일, PAS 파일 등 각각의 문서는 문법에 따라 구분자를 다르게 정의하고 있으므로 deli 배열은 차후 편집 문서의 종류에 따라 달라져야 할 것이다. ApiEdit는 디폴트로 거의 대부분의 기호들을 구분자로 인정하고 있다.

GetPrevWord 함수는 nPos에서 이전 단어의 위치를 찾아준다. 이때 이전 단어란 현재 캐럿이 있는 위치의 단어보다 앞에 있는 단어를 의미하는 것이 아니라 현재 위치보다 앞에 있는 가장 가까운 단어의 시작을 의미한다. 그래서 현재 단어의 중간에 캐럿이 있는 경우는 현재 단어의 처음을 찾아 주고 현재 단어의 처음에 캐럿이 있으면 이전 단어를 찾아 주도록 되어 있다.

이 함수의 코드를 분석해보자. 선두의 if문은 버퍼 처음인 경우의 예외 처리를 하는데 nPos 0이면 즉, 문서의 처음이면 더 이상 앞쪽으로 갈 곳이 없으므로 nPos를 그대로 리턴했다. 문서의 처음이 아니면 이전 단어를 찾는데 두 개의 for 루프로 이전 단어를 찾을 수 있다.

첫 번째 for 루프에서는 일단 한 칸 앞으로(nPos--) 이동하는데 현재 위치가 단어의 처음인 경우 이 단어를 건너 뛰도록 한다. 그리고 계속 앞쪽으로 이동하면서 구분자가 아닌 최초의 문자를 찾음으로써 앞 단어의 끝(또는 현재 단어의 중간)으로 이동한다. 한꺼번에 구분자가 여러 개 있을 수 있으므로 이 문장은 if문으로 쓸 수 없으며 반드시 for 루프를 구성해야 한다.

두 번째 for 루프에서는 계속 앞쪽으로 이동하면서 구분자를 찾는데 문서의 처음도 일종의 구분자로 취급하였다. 이렇게 두 개의 루프가 끝났을 때 검색된 nPos 위치의 다음 위치를 취하면 이전 단어의 처음이 된다. , nPos가 문서의 처음일 때는 예외적으로 첫 위치를 취한다. 설명을 읽으면 오히려 더 복잡하게 느껴지는데 그림으로 이 함수의 동작을 그려 보았다.

만약 그래도 잘 이해가 되지 않는다면 연습장에 글자를 몇 자 적어 놓고 직접 컴퓨터가 되어서 어떻게 하면 앞 단어를 찾을 수 있는지 방법을 찾아 보아라. 사실 컴퓨터란 놈은 사람이 한 번 찾아 놓은 방법을 반복적으로 실행할 뿐이므로 제대로 방법을 연구했다면 아마 위 코드와 비슷한 코드가 나오게 될 것이다.

다음 단어는 현재 위치보다 뒤에 있는 가장 가까운 단어의 시작을 찾으면 된다. 이전 단어와 코드가 유사하다. 뒤로 이동하면서 구분자를 찾고 다시 뒤로 이동하면서 구분자가 아닌 최초의 문자를 찾으면 된다.

이 함수들은 잠시 후 키보드 메시지에서 <Ctrl>키와 조합될 때 호출된다. ApiEdit는 이전, 다음 단어 찾기 기능만 제공하는데 어떤 편집기는 현재 단어의 끝, 전후 단어의 끝을 찾는 함수를 제공하기도 한다. 이런 함수들은 코드가 비슷해서 마음만 먹으면 쉽게 만들 수 있다.