편집기 지원 — .tbs 를 쓰는 동안 답하기
상태: 구현 완료 (2026-08-28) — 진단 · 이동 · 호버 · 자동완성 · 시맨틱 토큰
.tbs 는 지금 어느 편집기에서도 평범한 텍스트입니다. 키워드와 주석이 같은 색이고, 타입 이름을
잘못 적어도 변환을 돌려야만 알 수 있습니다. 선언이 여러 파일에 흩어져 있으므로 extends 가
가리키는 것을 보려면 사람이 파일을 찾습니다.
이 문서는 그것을 무엇으로 해결하는지, 그리고 왜 하이라이팅과 나머지가 다른 방법인지를 적습니다.
1. 결정 요약
| 항목 | 결정 | 근거 |
|---|---|---|
| 하이라이팅 | TextMate 문법. 확장에 정적 파일 하나 | 색칠에는 타입 해석이 필요 없습니다. 문법이 두 번째로 적히는 비용은 정규식 40줄 규모이고, 그것으로 서버 없이도 색이 나옵니다 |
| 진단 · 이동 · 호버 | tabbit lsp 하위 명령 | STRUCT DSL 검토 §27의 결정입니다. 편집기가 따로 판정하면 타입 시스템의 두 번째 구현이 생깁니다 |
| 프로토콜 구현 | 직접 씁니다. 외부 패키지 없음 | 프레이밍은 Content-Length 한 줄이고, 다루는 메서드가 10개 미만입니다. 의존성 기본값과 맞습니다 |
| 자동완성 | 줄의 텍스트에서 결정합니다 | §7 — 타자 중인 줄은 대개 파서가 받지 않는 줄입니다 |
| 시맨틱 토큰 | 선언 표에서 결정합니다 | §8 — 정규식이 갈라내지 못하는 것이 그것입니다 |
| 문서 동기화 | 전체(Full) | §4.1 |
| 해석 단위 | .tbs 가 든 디렉터리 하나 | §4.2 — 이 문서에서 가장 중요한 결정입니다 |
| 워크북 | 읽지 않습니다 | §3 |
2. 프로토콜 범위
| 메서드 | 답 |
|---|---|
initialize · initialized · shutdown · exit | 수명주기. initialize 가 아래 capabilities를 냅니다 |
textDocument/didOpen · didChange · didClose · didSave | 문서 동기화 |
textDocument/publishDiagnostics | 서버가 보내는 알림 |
textDocument/definition | §5 |
textDocument/hover | §5 |
textDocument/completion | §7 |
textDocument/semanticTokens/full | §8 |
workspace/didChangeWatchedFiles | 편집기 밖에서 바뀐 .tbs |
capabilities는 textDocumentSync: 1(Full) · definitionProvider · hoverProvider ·
completionProvider · semanticTokensProvider 입니다.
그 밖의 요청에는 MethodNotFound 를 내고, 그 밖의 알림은 버립니다. 알림에 오류를 내는
것은 프로토콜 위반이므로 둘을 나누어 처리합니다. $/ 로 시작하는 것은 규격이 무시를 허용하므로
그대로 버립니다.
3. 워크북 없이 답할 수 있는 것
서버는 워크북을 열지 않습니다. .tbs 파일 집합만으로 타입이 닫히도록 설계되어 있고
(설계 §4.4), 그것이 이 서버가 성립하는 근거입니다.
경계는 코드에 이미 그어져 있습니다.
| 호출 | 무엇을 내는가 | 서버가 부르는가 |
|---|---|---|
SchemaParser.Parse | 문법 | 부릅니다 |
SchemaDeclarations 의 수집과 LinkVariants | 중복 이름 · extends 해석 · 판별자 충돌 · 부분 번호 · 툼스톤 예약 | 부릅니다 |
SchemaDeclarations.Resolve | 테이블 이름 충돌 · 시트 enum 참조 거부 · 빈 변종 집합 | 부르지 않습니다 — 시트가 있어야 답할 수 있습니다 |
그래서 foreign 뒤의 테이블 이름은 이동도 호버도 되지 않습니다. 그 이름이 무엇인지는
워크북이 정하는 것이고, 서버가 추측하면 틀린 값을 냅니다. 색은 칠합니다 — 색칠은 그 이름이
무엇인지 몰라도 됩니다.
4. 문서와 해석 단위
4.1 전체 동기화
textDocumentSync 는 Full 입니다. 파서가 파일 전체를 다시 읽는 순수 함수이므로 증분 텍스트를
유지해도 파싱 비용이 줄지 않고, 증분 적용은 오프셋 계산 오류가 생길 자리만 더합니다.
대신 didChange 에 250밀리초 지연을 둡니다. 타자 한 글자마다 디렉터리 전체를 다시 읽지 않기
위한 것입니다. didOpen · didSave · didClose 와 이동 · 호버 요청은 지연 없이 즉시
계산합니다 — 요청의 답이 낡은 해석을 보면 사람 이 F12를 눌렀을 때 엉뚱한 곳으로 갑니다.
4.2 해석 단위는 디렉터리 하나
한 벌 = .tbs 가 든 디렉터리 하나입니다. 열린 파일의 디렉터리에서 *.tbs 를 모아 그 집합만
해석하고, 그 집합에만 진단을 발행합니다.
작업 공간 전체를 한 벌로 묶지 않는 이유는 없는 오류가 생기기 때문입니다. 이 저장소를 열면
samples/clover 와 samples/wildling 의 선언이 한 벌이 되고, 두 곳에 같은 이름의 struct가 있으면
둘 다 중복 선언으로 보고됩니다 — 실제로는 서로 다른 레시피가 읽는 별개의 집합입니다.
디렉터리를 고른 근거는 레시피입니다. SchemaSourceRecipe 가 가리키는 것이 디렉터리이고,
SchemaFiles.ReadAll 이 거기서 .tbs 를 모읍니다. 편집기의 해석 단위를 변환의 해석 단위와
같게 두면 편집기가 보고하는 것과 변환이 보고하는 것이 어긋나지 않습니다.
한계. 한 레시피가 디렉터리 여러 개를 한 벌로 읽으면 그 경계가 편집기에 보이지 않습니다. 파일 간 참조가 디렉터리를 넘으면 「선언을 찾을 수 없음」이 잘못 보고됩니다. 후속으로
tabbit.schemaRoots설정을 두거나 레시피를 읽는 방법이 있고, 어느 쪽도 1차에 넣지 않습니다.
작업 공간을 미리 훑지 않습니다. 열린 파일의 디렉터리만 봅니다 — 시작이 빠르고,
.claude/worktrees 같은 사본 트리에 닿지 않습니다.
5. 이동과 호버
| 낱말이 있는 자리 | 이동이 가는 곳 | 호버가 내놓는 것 |
|---|---|---|
struct · enum 의 이름 | 자기 선언 | 선언 한 줄과 /// 문서 |
extends 뒤의 이름 | 기반 선언 | 기반의 선언 한 줄과 문서 |
| 필드 타입의 이름 · 컨테이너 인자 | 그 타입의 선언 | 그 선언의 문서. 합성 타입이면 정식 이름과 성분 |
field · value 의 이름 | 자기 선언 | 선언 한 줄과 문서 |
foreign 뒤의 테이블 이름 | 없습니다 | 없습니다 — §3 |
이름 조회는 FindStruct · FindEnum 을 통해서만 합니다. 선언 사전이 파스칼 표기로 정규화한
뒤 대소문자를 무시하고 담으므로, 다른 방법으로 찾으면 대소문자가 다른 참조에서 답이 갈립니다.
int 같은 스칼라 내장 타입에는 호버가 없습니다. 답하려면 내장 이름의 목록을 서버가 따로
들고 있어야 하고 — 쿠커와 문법 파일에 이어 세 번째입니다 — 그렇게 해서 알리는 것이 「int 은
정수입니다」입니다. 합성 타입이 예외인 이유는 목록이 필요 없기 때문입니다. CompositeTypes 에
물으면 정식 이름과 성분이 나옵니다.
6. 진단
Diagnostics 가 모은 것을 그대로 옮깁니다. 새로 판정하는 것이 없습니다.
| LSP 항목 | 무엇에서 |
|---|---|
range 의 시작 | Location 의 행과 열. 둘 다 이미 0부터 세므로 변환이 없습니다 |
range 의 끝 | §6.1 |
severity | Error→1 · Warning→2 · Info→3 |
code | MessageId — schema. 로 시작하는 안정된 값입니다 |
message | 현지화된 본문 |
source | "tabbit" |
진단이 없어진 파일에는 빈 목록을 발행합니다. 발행하지 않으면 편집기가 직전 것을 그대로 둡니다.
6.1 끝 위치
Location 은 시작 지점만 담습니다. 끝을 담게 고치지 않습니다 — 이 타입은 히스토리 레코드와 생성
코드 주석과 빌드 리포트에 직렬화되므로, 늘리면 그 산출물들이 함께 움직입니다.
대신 그 파일을 렉서로 한 번 더 읽어 시작 위치가 같은 토큰의 끝 열을 가져옵니다. 진단은 토큰 위치로 보고되므로 대부분 일치합니다. 일치하는 토큰이 없으면(빈 줄이나 파일 끝에 붙는 진단) 줄 끝까지 긋고, 그 것도 없으면 시작과 끝을 같게 둡니다.
렉서를 두 번 도는 비용은 편집 한 번에 파일 하나 분량이고, 이 자리에서 문제가 되지 않습니다.
7. 자동완성
줄의 텍스트에서 결정합니다. 타자 중인 줄은 대개 파서가 받지 않는 줄이고, 그때 존재하는 구문 트리는 직전 키 입력까지의 것입니다. 무엇을 내놓을지 정하는 것은 그 줄에 이미 적힌 낱말이므로, 텍스트에 직접 물으면 됩니다.
| 커서가 있는 자리 | 내놓는 것 |
|---|---|
| 줄의 처음 | struct · abstract · field · enum · value |
field 이름 다음 | 스칼라 · 합성 타입 · set · map · foreign · 선언된 struct와 enum |
extends 다음 | abstract struct |
닫히지 않은 ( 안 | 그 선언이 가질 수 있는 메타데이터 키 |
enum 타입 멤버의 = 다음 | 그 enum의 값 |
이름은 전부 그것을 정하는 표에서 옵니다 — 스칼라는 ScalarTypes, 합성은 CompositeTypes,
메타데이터 키는 SchemaMetadata. 여기 다시 적은 목록이 없어서, 표기법에 이름이 늘면 자동완성
코드를 건드리지 않아도 후보에 나옵니다.
빼는 것이 둘 있습니다. abstract struct는 멤버의 타입 후보가 아닙니다 — 값이 가질 수 있는
형태가 아니라 변종 집합의 이름입니다. 표기법이 정의하지만 이 빌드가 쓰지 않는 키(uniqueBy ·
tag)도 뺍니다. 후보로 내놓고 나서 「그 키는 아무것도 하지 않습니다」라고 보고하는 것은 한
물음에 두 답입니다.
자동완성은 어림이 기준입니다. 원하지 않은 후보는 키 하나로 닫히고, 그것은 틀린 진단이 치르는 값이 아닙니다.
8. 시맨틱 토큰
정규식이 갈라내지 못하는 것을 선언 표가 갈라냅니다. TextMate 문법은 타입 자리의 낱말 하나를 볼 뿐, 그것이 struct인지 enum인지 이름을 잘못 적은 것인지 가리지 못합니다.
| 이름 | 토큰 종류 |
|---|---|
| struct 선언과 그 이름의 사용 | struct |
| enum 선언과 그 이름의 사용 | enum |
| enum 값 | enumMember |
| 멤버 이름 | property |
| 스칼라와 합성 내장 타입 | type |
| 그 밖의 이름 | 토큰을 주지 않습니다 |
선언 자리에는 declaration 수식자가 붙어 사용과 갈립니다.
마지막 줄이 이 기능의 요지입니다. 타입 이름을 잘못 적은 것은 이 서버가 문제로 보고하지 못합니다 — 그 검사는 워크북이 있어야 합니다(§3). 그래서 줄 수 있는 신호가 하나뿐입니다. 옆의 알아본 이름들이 가진 색을 그것만 잃는 것입니다.
토큰 은 이전 토큰으로부터의 걸음으로 실리므로 위치 순으로 정렬한 뒤에 싣습니다. 순서가 어긋나면 편집기가 읽는 것이 전부 어긋납니다.
9. 실행 파일 찾기
확장은 이 순서로 서버를 찾습니다.
tabbit.path설정PATH의tabbit- 작업 공간이 이 저장소이면
src/bin/Debug/net10.0/과Release의 빌드 산출물
셋 다 없으면 설정하는 방법을 알리고 멈춥니다. 조용히 기능만 사라지면 사람이 확장을 의심하지 않습니다.
10. 구현에서 지켜야 하는 것
| 자리 | 지킬 것 |
|---|---|
| 표준 출력 | 프로토콜만 나갑니다. 로그 · 버전 줄 · Console.WriteLine 이 하나라도 섞이면 연결이 끊어집니다. lsp 분기는 로깅 설정보다 먼저 갈라지고, 로그는 표준 오류로만 냅니다 |
Content-Length | UTF-8 바이트 수입니다. 본문을 문자 단위로 읽으면 한국어 /// 가 든 문서에서 어긋납니다 |
| 경로와 URI | 퍼센트 인코딩과 드라이브 문자 대소문자가 있습니다. 정규화를 함수 하나로 모으고, 파서에 넘기는 경로도 그 함수를 지나게 합니다 — 진단을 파일별로 나눌 때 그 경로가 열쇠입니다 |
| 좌표 | Location 은 0부터, 토큰은 1부터 셉니다. 변환은 끝 위치를 계산하는 한 곳에만 둡니다 |
| 메시지 언어 | MessageCatalog.Current 는 정적이고 진단 본문은 담을 때 고정됩니다. 언어를 바꾸려면 서버를 다시 시작합니다 |
11. 문법 파일의 어긋남
정본은 Models.ScalarTypes 와 Models.CompositeTypes 입니다. 서버는 두 표를 조회합니다 —
자동완성이 내놓는 이름도, 시맨틱 토큰이 「내장 타입」으로 세는 이름도 거기서 나오므로 타입을
추가하면 서버는 저절로 따라옵니다.
따라오지 않는 것이 하나 있습니다. TextMate 문법 파일에는 이름이 적혀 있고, 그것은 확장의 정적 파일이라 표를 읽을 수 없습니다. 타입을 추가하면 그 파일도 함께 고칩니다.
게이트를 두지 않습니다. 어긋남의 결과가 색 하나이고 — 시맨틱 토큰이 그 위에 덮으므로 서버가 붙은 편집기에서는 그마저 보이지 않습니다 — 타입이 추가되는 빈도가 낮기 때문입니다.