Neovim에서 Telescope와 mini.pick을 이용해 매뉴얼 페이지 찾기
출처: Dev.to
나는 매번 매뉴얼 페이지를 사용하지만, 때때로 정확히 어떤 페이지를 열어야 할지 모를 때가 있다.
printf가 좋은 예다.
쉘 명령인 printf(1), C 라이브러리 함수인 printf(3)이 있고, 로컬에 설치된 상황에 따라 더 많은 항목이 존재할 수 있다.
나는 Neovim 안에서 다음과 같은 간단한 기능을 원했다.
- 로컬 매뉴얼 페이지 목록 보기
- 퍼지 검색으로 찾기
- 열기 전에 섹션 확인하기
- Neovim 내장
:Man으로 열기
그래서 작은 플러그인 man.nvim을 만들었다.
소스:
GitHub: https://github.com/moniquelive/man.nvim
무엇을 하는가
플러그인은 시스템 데이터베이스인
apropos .
를 실행한다.
그 결과를 다음과 같은 피커 아이템 형태로 파싱한다.
[1 User commands] printf(1) - format and print data
[3 Library calls] printf(3) - formatted output conversion
핵심은 항목을 선택하면 여전히 Neovim에게 다음과 같이 위임한다는 점이다.
:Man 3 printf
별도의 렌더러가 없으며, 터미널 버퍼를 매뉴얼처럼 가장하지도 않는다.
매뉴얼 페이지 탐색을 재구현하지도 않는다.
피커를 통한 탐색과 :Man을 통한 열기만 제공한다.
왜 그냥 :Man만 쓰지 않을까?
:Man은 내가 정확히 원하는 것이 무엇인지 알 때는 훌륭하다.
하지만 로컬에 설치된 매뉴얼을 탐색하고 싶을 때가 많다. 이름의 일부, 설명, 혹은 섹션만 기억하고 있을 수도 있다.
피커를 이용하면 다음과 같이 입력할 수 있다.
printf
3 printf
formatted output
아이템 텍스트에 섹션 라벨이 포함돼 있어, 명령, 라이브러리 호출, 파일 포맷 등 어떤 항목을 열게 될지 한눈에 파악하기 쉽다.
Telescope 버전
main 브랜치는 Telescope 확장을 제공한다.
설정
require('telescope').setup()
require('man_nvim').setup()
사용법
:Telescope man
내부적으로는 테이블 파인더를 사용한다. 파싱된 각 매뉴얼 페이지는 다음과 같은 데이터 구조를 가진 엔트리로 변환된다.
{
name = 'printf',
section = '3',
ref = 'printf(3)',
description = 'formatted output conversion',
}
표시와 정렬에 사용되는 검색 가능한 텍스트는 동일하다.
'[3 Library calls] printf(3) - formatted output conversion'
이 덕분에 Telescope가 잘 하는 일—프롬프트, 정렬, 프리뷰, 매핑—을 그대로 활용할 수 있다.
mini.pick 버전
mini.pick을 이용한 mini.picker 브랜치도 있다.
설정
require('mini.pick').setup()
require('man_nvim').setup()
사용법
:Pick man
흥미로운 점은 플러그인의 대부분이 개념적으로 공유된다는 것이다.
apropos출력 파싱- 별칭 분리
- 매뉴얼 섹션별 정렬
:Man명령 생성- 작은 프리뷰 표시
- 선택된 페이지 열기
피커 통합만 다를 뿐이다.
별칭
일부 apropos 항목은 많은 별칭을 가진다.
예를 들어 쉘에서는 다음과 같은 줄을 출력한다.
builtin(1), !(1), %(1), .(1), :(1), [(1) - shell built-in commands
플러그인은 별칭마다 검색 가능한 아이템을 만들지만, 실제로는 정식 페이지를 연다.
즉, !를 검색하면 !(1)을 찾을 수 있지만 선택하면 builtin(1)이 열린다.
이는 별칭 중 일부가 독립적인 매뉴얼 페이지가 아니라, 해당 apropos 라인의 첫 번째 항목 아래에 문서화되어 있기 때문에 중요하다.
잡음 결과 필터링
빨리 짜증났던 사례: Tcl/Tk 결과.
format 같은 검색어는 유용한 결과와 함께 원하지 않는 Tcl 관련 항목들을 많이 반환한다.
그래서 부정 프롬프트 용어를 추가했다.
Telescope와 mini.pick 모두에서:
format -tcl
format -tcl -tk
위 명령은 다음을 의미한다.
format을 검색tcl이 포함된 항목 제외- 옵션으로
tk도 제외
부정 용어는 스마트케이스도 적용한다.
-tcl # 스마트케이스가 허용될 때 tcl, Tcl, TCL 등을 제외
-Tcl # 대소문자 구분 제외
Telescope 버전은 이를 정렬기 래퍼로 구현한다. 프롬프트에서 부정 용어를 제거하고, 엔트리를 필터링한 뒤, 긍정 쿼리를 일반 Telescope 정렬기에 다시 전달한다.
mini.pick 버전은 커스텀 source.match로 구현한다. 먼저 엔트리를 필터링하고, 남은 긍정 쿼리를 MiniPick.default_match에 넘긴다.
이렇게 하면 각 피커의 기본 매칭 동작을 유지하면서 내 자체 퍼지 매처를 대체하지 않는다.
열기 매핑
매핑은 의도적으로 단순하게 구성했다.
:vertical Man {section} {name}
:Man {section} {name}
:vertical Man {section} {name}
:tab Man {section} {name}
: 로 시작하는 첫 번째 명령은 기본적으로 수직 분할로 연다. 이는 문서를 코드 옆에 두고 보는 경우가 많기 때문이다.
두 번째 명령은 기존 :Man과 동일하게 수평 분할을 사용한다.
이식성 주의사항
플러그인은 macOS, Linux, FreeBSD에서 동작하도록 설계되었다.
이는 주로 똑똑한 가정을 피한다는 의미다.
apropos 출력은 잡음이 많을 수 있다. 예를 들어 macOS에서는 makewhatis: ... No such file or directory 같은 줄이 나오는데, 파서는 매뉴얼 항목처럼 보이지 않는 줄을 그냥 무시한다.
섹션 번호도 반드시 1~9만 있는 것이 아니다. 다음과 같은 경우가 있다.
3p
3posix
n
따라서 파서는 괄호 안에 있는 섹션 이름을 그대로 받아들이며, 한 자리 숫자만 가정하지 않는다.
페이지를 여는 부분은 Neovim과 시스템 man 명령에 맡겨 복잡한 처리를 플러그인이 직접 하지 않는다.
설치
lazy.nvim 사용 (Telescope 버전)
{
'moniquelive/man.nvim',
dependencies = { 'nvim-telescope/telescope.nvim' },
config = function()
require('man_nvim').setup()
end,
}
lazy.nvim 사용 (mini.pick 버전)
{
'moniquelive/man.nvim',
branch = 'mini.picker',
dependencies = { 'echasnovski/mini.pick' },
config = function()
require('mini.pick').setup()
require('man_nvim').setup()
end,
}
packer.nvim 사용 (Telescope)
use({
'moniquelive/man.nvim',
requires = { 'nvim-telescope/telescope.nvim' },
config = function()
require('man_nvim').setup()
end,
})
packer.nvim 사용 (mini.pick)
use({
'moniquelive/man.nvim',
branch = 'mini.picker',
requires = { 'echasnovski/mini.pick' },
config = function()
require('mini.pick').setup()
require('man_nvim').setup()
end,
})
vim-plug 사용 (Telescope)
Plug 'nvim-telescope/telescope.nvim'
Plug 'moniquelive/man.nvim'
플러그인 로드 후 어느 곳에서든:
require('man_nvim').setup()
vim-plug 사용 (mini.pick)
Plug 'echasnovski/mini.pick'
Plug 'moniquelive/man.nvim', { 'branch': 'mini.picker' }
그 다음:
require('mini.pick').setup()
require('man_nvim').setup()
마무리 생각
T