Elisp: latex-enclose-word
From Wiki
Emacs 설정 (elisp) 삽질
글쓴이는 가끔 글을 쓸 때 LaTeX을 써서 조판(typesetting) 합니다. 보통 보고서 형식의 문서를 쓰거나 Online에 배포할 글을 쓸 때 사용하는데, Emacs의 latex-mode를 써서 작업합니다.
Emacs의 latex-mode는 syntax-highlight 기능 이외에, latex으로 컴파일하는 명령, dvi 파일을 미리보는 기능 등을 지원합니다.
이 글은 LaTeX을 설명하기 위한 목적이 아니기 때문에, LaTeX에 대해서는 가능한 간단히 설명하도록 하겠습니다. LaTeX은 HTML처럼 문서를 mark-up하기 위한 언어입니다. 다만 보여지는 내용이 아닌, 글의 내용에 집중해서 글을 쓸 수 있도록 도와줍니다.
예를 들어 다음과 같은 HTML 단락을 생각해 봅시다:
<p> x86에는 스택을 직접적으로 활용하는 <tt>PUSH</tt>, <tt>POP</tt> 연산과 달리, 간접적으로 활용하는 instruction도 존재합니다. 함수 호출과 복귀에 따른 <tt>CALL</tt>과 <tt>RET</tt> 명령이 있으며, 스택 프레임 관리에 사용하는 <tt>ENTER</tt>와 <tt>LEAVE</tt> 명령이 있습니다. </p>
글쓴이는 위 내용을 LaTeX으로 다음과 같이 작성합니다:
x86에는 스택을 직접적으로 활용하는 \f{PUSH}, \f{POP} 연산과 달리, 간접적으로 활용하는 instruction도 존재합니다. 함수 호출과 복귀에 따른 \f{CALL}과 \f{RET} 명령이 있으며, 스택 프레임 관리에 사용하는 \f{ENTER}와 \f{LEAVE} 명령이 있습니다.
LaTeX의 명령은 대개 "\xxx{...}" 꼴로 나타나는데, 위에서 사용한 f 명령은 글쓴이가 따로 정의한 명령입니다.
글쓴이는 장문의 글을 쓸 경우, 위와 같은 formatting에 관한 것은 전혀 생각치 않고, 글의 내용만 쓴 다음에, 어느 정도 단락 또는 섹션이 완성되면, 한꺼번에 formatting하는 습관이 있습니다. 따라서 일단 글을 입력하고나서 나중에 "\f{...}"를 써서 글을 포매팅합니다.
수백개의 "\f{...}"를 추가하면서, 이런 생각이 들었습니다.
"커서가 위치한 현재 단어를 주어진 텍스트로 둘러싸서 입력해주는 기능이 있으면 좋겠는데..."
(Emacs Lisp 용어로 커서의 위치는 "point"라고 합니다.)
즉 아래와 같은 글에서 ("=!="는 현재 커서의 위치를 나타냄):
x86에는 스택을 직접적으로 활용하는 PU=!=SH, POP 연산과 달리,
특정 명령을 수행하면, 아래와 같이 만들어 주는 것입니다:
x86에는 스택을 직접적으로 활용하는 \f{PU=!=SH}, POP 연산과 달리,
조금 더 생각해보니, 지정한 텍스트로 단어를 둘러싸주고, 포인트의 위치를 현재 단어의 끝 부분으로 옮겨주는 것이, 입력을 원활하게 해 줄 것 같습니다. 즉, 명령을 수행하면 다음과 같은 상태로 만들어 주는 것입니다:
x86에는 스택을 직접적으로 활용하는 \f{PUSH=!=}, POP 연산과 달리,
이 기능을 수행하기 위해서, 글쓴이는 새로 Emacs Lisp(elisp) 함수를 만들기로 마음먹었습니다.
그리고 다음과 같이 함수 뼈대를 만들었습니다:
(defun latex-enclose-word () (interactive) ;; 현재 포인트가 위치한 곳의 단어 시작/끝의 위치를 range에 저장 (START . END) (setq range ....) ;; START 지점으로 포인트를 이동 (goto-char (car range)) ;; 지정한 텍스트 추가 "\f{" (insert "\f{") ;; END 지점으로 포인트 이동 (goto-char (cdr range)) ;; 지정한 텍스트 추가 (insert "}") )
그리고, 이 함수를 구현하기 위해, 글쓴이가 사용했거나, 관련된 함수들은 다음과 같습니다:
(insert STRING) ; 현재 버퍼, 현재 포인트 위치에 STRING을 추가 (goto-char XXX) ; 포인트를 XXX 위치로 변경 (current-word) ; 현재 포인트가 위치한 곳의 단어를 리턴
글쓴이는 이 함수를 만들기 전에, 이미 (current-word) 함수를 알고 있었습니다. 그러나 글쓴이의 얄팍한? elisp 지식으로는, 현재 포인트가 위치한 곳의 단어가 시작하는 위치와 단어가 끝나는 위치를 리턴하는 방법을 몰랐습니다.
따라서 이런 기능을 하는 함수를 따로 만들어야 할 상황에 처해 있었습니다. 그런데, 이런 함수를 만들기 위해서 생각해 볼 것이 하나 있는데, 바로 "단어(word)"에 대한 정의입니다:
- "단어"라는 것이 무엇인가?
- 단어와 단어를 구별하는 기준은 무엇인가? 즉, 단어 사이에 존재하는 구분자(delimiter, separator)는 무엇인가?
- 단순히 공백 글자 또는 newline이면 구분자에 포함되는 것인가?
- "(){}"등의 문자, 그리고 쌍따옴표, 따옴표 등도 단어 구분자에 포함해야 하는가?
- 또, 이런 구분자가 현재 편집하고 있는 파일의 상태, 즉 LaTeX 모드냐, 또는 C 언어 모드냐 등에 달라질 수 있지 않을까?
이러한 모든 사항을 만족시켜서, 현재 포인트가 위치한 곳의 단어를
리턴하는 함수가 바로 (current-word)입니다. 그런데, 글쓴이는, 이러한
사항을 모두 만족시키면서, 현재 단어의 시작 위치와 끝 위치를 알려주는
함수를 만들 자신이 없었습니다. 물론 많은 시간과 노력을 들이면
가능하겠지만, 배보다 배꼽이 더 큰 경우이며, 무엇보다도
귀찮습니다.
따라서 일단, "C-h C-f"를 써서, current-word 함수의 도움말을 살펴보기로 했습니다:
C-h C-f current-word <RET>
위를 입력하면, 다음과 같이 도움말이 나옵니다.
current-word is a compiled Lisp function in `simple.el'. (current-word &optional STRICT REALLY-WORD) Return the symbol or word that point is on (or a nearby one) as a string. The return value includes no text properties. If optional arg STRICT is non-nil, return nil unless point is within or adjacent to a symbol or word. In all cases the value can be nil if there is no word nearby. The function, belying its name, normally finds a symbol. If optional arg REALLY-WORD is non-nil, it finds just a word. [back]
이 때, 커서(포인트)를 simple.el에 위치시키고 <RET>을 누르면, 이 함수의 정의를
보여줍니다. (물론, Emacs를 설치할 때, 소스 .el 파일들을 같이 설치했을 경우)
이 함수의 정의를 살펴보면... 대충 다음과 같다는 것을 눈치챌 수 있습니다:
(defun current-word (...) (save-excursion (let* ((start ...) (end ...) ...) ... (unless (= start end) (buffer-substring-no-properties start end)))))
elisp을 모르는 분을 위해 주석을 달아보면 다음과 같습니다:
(defun current-word (...) ; 함수 current-word 정의 시작 (save-excursion (let* ((start ...) (end ...) ...) ; 지역 변수 start, end 선언 ;; start와 end 위치 계산 ... (unless (= start end) ;; 현재 버퍼의 START-END 위치의 문자열을 리턴 (buffer-substring-no-properties start end)))))
자세한 내용이 궁금하신 분은, 앞에서 설명한 대로 함수 정의를 보기 바랍니다. 실제 변수 start와 end 위치를 계산하는 부분은 꽤 복잡하며, 글쓴이도 자세한 내용을 잘 모릅니다. 중요한 것은, 어쨋든 start, end 위치를 계산하고 나서 buffer-substring-no-properties 함수를 호출한다는 것입니다.
그리고 buffer-substring-no-properties 함수는 주어진 위치 값 두 개를 받아서 이 위치에 있는 버퍼의 내용을 문자열로 리턴해 주는 함수라는 것입니다.
글쓴이는 여기까지 조사한 다음에 다음과 같은 생각이 머리에 떠 올랐습니다.
- 아.. 그러면 현재 단어의 위치를 알기 위해서, current-word 함수의 내용을 그대로 복사하면 되겠구나..
- 흠.. 이런 식으로 같은 일을 하기 위해 중복된 코드를 만드는 것은 별로 좋은 습관이 아닌데...
무엇보다도 Emacs가 upgrade되면서 current-word 함수 정의가 바뀔 수 있을 테니까...
- 그럼 어떻게 이 함수 내용을 이용할 수 있을까?
- 음... 만약 buffer-substring-no-properties 함수 대신, 내가 원하는 기능. 즉 그냥 start와 end 위치를 리턴하는 코드를 넣을 수 있으면 환상인데...
- 가만, buffer-substring-no-properties 함수 정의를 바꿔칠 수 있자나?
- 그럼 buffer-substring-no-properties 함수의 정의를 어딘가 다른 곳에 저장해두고, 내가 원하는 함수로 바꾼 다음에, 이 함수에서 start, end의 위치를 저장하면...
- 그리고 (current-word)를 호출하는 거야. 그러면 이 함수가 start, end의 위치를 내가 원하는 곳에 저장할 것이고...
- (current-word)가 끝난 다음에, buffer-substring-no-properties를 아까 저장해두었던 원래 정의로 바꿔주면... 게임 끝!

