Skip to content

Commit fd6e230

Browse files
committed
perf(comment-checker): add LSP-style background language warming
- Warmup common languages (python, typescript, javascript, tsx, go, rust, java) on plugin init - Non-blocking background initialization using Promise.then() pattern - First parse call uses pre-cached language - zero user wait time - Refactor parser manager with ManagedLanguage interface for better state tracking
1 parent 50ea492 commit fd6e230

File tree

4 files changed

+312
-38
lines changed

4 files changed

+312
-38
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Comment-Checker TypeScript Port 구현 계획
2+
3+
## 1. 아키텍처 개요
4+
5+
### 1.1 핵심 도전 과제
6+
7+
**OpenCode Hook의 제약사항:**
8+
- `tool.execute.before`: `output.args`에서 파일 경로/내용 접근 가능
9+
- `tool.execute.after`: `tool_input`**제공되지 않음** (Claude Code와의 핵심 차이점)
10+
- **해결책**: Before hook에서 데이터를 캡처하여 callID로 키잉된 Map에 저장, After hook에서 조회
11+
12+
### 1.2 디렉토리 구조
13+
14+
```
15+
src/hooks/comment-checker/
16+
├── index.ts # Hook factory, 메인 엔트리포인트
17+
├── types.ts # 모든 타입 정의
18+
├── constants.ts # 언어 레지스트리, 쿼리 템플릿, 디렉티브 목록
19+
├── detector.ts # CommentDetector - web-tree-sitter 기반 코멘트 감지
20+
├── filters/
21+
│ ├── index.ts # 필터 barrel export
22+
│ ├── bdd.ts # BDD 패턴 필터
23+
│ ├── directive.ts # 린터/타입체커 디렉티브 필터
24+
│ ├── docstring.ts # 독스트링 필터
25+
│ └── shebang.ts # Shebang 필터
26+
├── output/
27+
│ ├── index.ts # 출력 barrel export
28+
│ ├── formatter.ts # FormatHookMessage
29+
│ └── xml-builder.ts # BuildCommentsXML
30+
└── utils.ts # 유틸리티 함수
31+
```
32+
33+
### 1.3 데이터 흐름
34+
35+
```
36+
[write/edit 도구 실행]
37+
38+
39+
┌──────────────────────┐
40+
│ tool.execute.before │
41+
│ - 파일 경로 캡처 │
42+
│ - pendingCalls Map │
43+
│ 에 저장 │
44+
└──────────┬───────────┘
45+
46+
47+
[도구 실제 실행]
48+
49+
50+
┌──────────────────────┐
51+
│ tool.execute.after │
52+
│ - pendingCalls에서 │
53+
│ 데이터 조회 │
54+
│ - 파일 읽기 │
55+
│ - 코멘트 감지 │
56+
│ - 필터 적용 │
57+
│ - 메시지 주입 │
58+
└──────────────────────┘
59+
```
60+
61+
---
62+
63+
## 2. 구현 순서
64+
65+
### Phase 1: 기반 구조
66+
1. `src/hooks/comment-checker/` 디렉토리 생성
67+
2. `types.ts` - 모든 타입 정의
68+
3. `constants.ts` - 언어 레지스트리, 디렉티브 패턴
69+
70+
### Phase 2: 필터 구현
71+
4. `filters/bdd.ts` - BDD 패턴 필터
72+
5. `filters/directive.ts` - 디렉티브 필터
73+
6. `filters/docstring.ts` - 독스트링 필터
74+
7. `filters/shebang.ts` - Shebang 필터
75+
8. `filters/index.ts` - 필터 조합
76+
77+
### Phase 3: 코어 로직
78+
9. `detector.ts` - web-tree-sitter 기반 코멘트 감지
79+
10. `output/xml-builder.ts` - XML 출력
80+
11. `output/formatter.ts` - 메시지 포매팅
81+
82+
### Phase 4: Hook 통합
83+
12. `index.ts` - Hook factory 및 상태 관리
84+
13. `src/hooks/index.ts` 업데이트 - export 추가
85+
86+
### Phase 5: 의존성 및 빌드
87+
14. `package.json` 업데이트 - web-tree-sitter 추가
88+
15. typecheck 및 build 검증
89+
90+
---
91+
92+
## 3. 핵심 구현 사항
93+
94+
### 3.1 언어 레지스트리 (38개 언어)
95+
96+
```typescript
97+
const LANGUAGE_REGISTRY: Record<string, LanguageConfig> = {
98+
python: { extensions: [".py"], commentQuery: "(comment) @comment", docstringQuery: "..." },
99+
javascript: { extensions: [".js", ".jsx"], commentQuery: "(comment) @comment" },
100+
typescript: { extensions: [".ts"], commentQuery: "(comment) @comment" },
101+
tsx: { extensions: [".tsx"], commentQuery: "(comment) @comment" },
102+
go: { extensions: [".go"], commentQuery: "(comment) @comment" },
103+
rust: { extensions: [".rs"], commentQuery: "(line_comment) @comment (block_comment) @comment" },
104+
// ... 38개 전체
105+
}
106+
```
107+
108+
### 3.2 필터 로직
109+
110+
**BDD 필터**: `given, when, then, arrange, act, assert`
111+
**Directive 필터**: `noqa, pyright:, eslint-disable, @ts-ignore` 등 30+
112+
**Docstring 필터**: `IsDocstring || starts with /**`
113+
**Shebang 필터**: `starts with #!`
114+
115+
### 3.3 출력 형식 (Go 버전과 100% 동일)
116+
117+
```
118+
COMMENT/DOCSTRING DETECTED - IMMEDIATE ACTION REQUIRED
119+
120+
Your recent changes contain comments or docstrings, which triggered this hook.
121+
You need to take immediate action. You must follow the conditions below.
122+
(Listed in priority order - you must always act according to this priority order)
123+
124+
CRITICAL WARNING: This hook message MUST NEVER be ignored...
125+
126+
<comments file="/path/to/file.py">
127+
<comment line-number="10">// comment text</comment>
128+
</comments>
129+
```
130+
131+
---
132+
133+
## 4. 생성할 파일 목록
134+
135+
1. `src/hooks/comment-checker/types.ts`
136+
2. `src/hooks/comment-checker/constants.ts`
137+
3. `src/hooks/comment-checker/filters/bdd.ts`
138+
4. `src/hooks/comment-checker/filters/directive.ts`
139+
5. `src/hooks/comment-checker/filters/docstring.ts`
140+
6. `src/hooks/comment-checker/filters/shebang.ts`
141+
7. `src/hooks/comment-checker/filters/index.ts`
142+
8. `src/hooks/comment-checker/output/xml-builder.ts`
143+
9. `src/hooks/comment-checker/output/formatter.ts`
144+
10. `src/hooks/comment-checker/output/index.ts`
145+
11. `src/hooks/comment-checker/detector.ts`
146+
12. `src/hooks/comment-checker/index.ts`
147+
148+
## 5. 수정할 파일 목록
149+
150+
1. `src/hooks/index.ts` - export 추가
151+
2. `package.json` - web-tree-sitter 의존성
152+
153+
---
154+
155+
## 6. Definition of Done
156+
157+
- [ ] write/edit 도구 실행 시 코멘트 감지 동작
158+
- [ ] 4개 필터 모두 정상 작동
159+
- [ ] 최소 5개 언어 지원 (Python, JS, TS, TSX, Go)
160+
- [ ] Go 버전과 동일한 출력 형식
161+
- [ ] typecheck 통과
162+
- [ ] build 성공

local-ignore/push-and-release.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
set -e
3+
cd /Users/yeongyu/local-workspaces/oh-my-opencode
4+
5+
echo "=== Pushing to origin ==="
6+
git push -f origin master
7+
8+
echo "=== Triggering workflow ==="
9+
gh workflow run publish.yml --repo code-yeongyu/oh-my-opencode --ref master -f bump=patch -f version=$1
10+
11+
echo "=== Done! ==="
12+
echo "Usage: ./local-ignore/push-and-release.sh 0.1.6"

src/hooks/comment-checker/detector.ts

Lines changed: 134 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,73 +17,170 @@ function debugLog(...args: unknown[]) {
1717
}
1818

1919
// =============================================================================
20-
// Parser caching for performance
20+
// Parser Manager (LSP-style background initialization)
2121
// =============================================================================
2222

23+
interface ManagedLanguage {
24+
language: unknown
25+
initPromise?: Promise<unknown>
26+
isInitializing: boolean
27+
lastUsedAt: number
28+
}
29+
2330
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2431
let parserClass: any = null
25-
let parserInitialized = false
26-
const languageCache = new Map<string, unknown>()
32+
let parserInitPromise: Promise<void> | null = null
33+
const languageCache = new Map<string, ManagedLanguage>()
2734

28-
async function getParser() {
29-
if (!parserClass) {
30-
debugLog("importing web-tree-sitter (first time)...")
31-
parserClass = (await import("web-tree-sitter")).default
35+
const LANGUAGE_NAME_MAP: Record<string, string> = {
36+
golang: "go",
37+
csharp: "c_sharp",
38+
cpp: "cpp",
39+
}
40+
41+
const COMMON_LANGUAGES = [
42+
"python",
43+
"typescript",
44+
"javascript",
45+
"tsx",
46+
"go",
47+
"rust",
48+
"java",
49+
]
50+
51+
async function initParserClass(): Promise<void> {
52+
if (parserClass) return
53+
54+
if (parserInitPromise) {
55+
await parserInitPromise
56+
return
3257
}
3358

34-
if (!parserInitialized) {
35-
debugLog("initializing Parser (first time)...")
59+
parserInitPromise = (async () => {
60+
debugLog("importing web-tree-sitter...")
61+
parserClass = (await import("web-tree-sitter")).default
3662
const treeSitterWasmPath = require.resolve("web-tree-sitter/tree-sitter.wasm")
3763
debugLog("wasm path:", treeSitterWasmPath)
3864
await parserClass.init({
3965
locateFile: () => treeSitterWasmPath,
4066
})
41-
parserInitialized = true
42-
debugLog("Parser initialized")
43-
}
67+
debugLog("Parser class initialized")
68+
})()
4469

70+
await parserInitPromise
71+
}
72+
73+
async function getParser() {
74+
await initParserClass()
4575
return new parserClass()
4676
}
4777

48-
async function getLanguage(langName: string) {
49-
if (languageCache.has(langName)) {
50-
debugLog("using cached language:", langName)
51-
return languageCache.get(langName)
52-
}
53-
54-
debugLog("loading language wasm:", langName)
78+
async function loadLanguageWasm(langName: string): Promise<unknown | null> {
79+
const mappedLang = LANGUAGE_NAME_MAP[langName] || langName
5580

56-
let wasmPath: string
5781
try {
5882
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${langName}.wasm`)
59-
wasmPath = wasmModule.default
83+
return wasmModule.default
6084
} catch {
61-
const languageMap: Record<string, string> = {
62-
golang: "go",
63-
csharp: "c_sharp",
64-
cpp: "cpp",
85+
if (mappedLang !== langName) {
86+
try {
87+
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${mappedLang}.wasm`)
88+
return wasmModule.default
89+
} catch {
90+
return null
91+
}
6592
}
66-
const mappedLang = languageMap[langName] || langName
67-
try {
68-
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${mappedLang}.wasm`)
69-
wasmPath = wasmModule.default
70-
} catch (err) {
71-
debugLog("failed to load language wasm:", langName, err)
72-
return null
93+
return null
94+
}
95+
}
96+
97+
async function getLanguage(langName: string): Promise<unknown | null> {
98+
const cached = languageCache.get(langName)
99+
100+
if (cached) {
101+
if (cached.initPromise) {
102+
await cached.initPromise
73103
}
104+
cached.lastUsedAt = Date.now()
105+
debugLog("using cached language:", langName)
106+
return cached.language
74107
}
75108

76-
if (!parserClass) {
77-
await getParser() // ensure parserClass is initialized
109+
debugLog("loading language wasm:", langName)
110+
111+
const initPromise = (async () => {
112+
await initParserClass()
113+
const wasmPath = await loadLanguageWasm(langName)
114+
if (!wasmPath) {
115+
debugLog("failed to load language wasm:", langName)
116+
return null
117+
}
118+
return await parserClass!.Language.load(wasmPath)
119+
})()
120+
121+
languageCache.set(langName, {
122+
language: null as unknown,
123+
initPromise,
124+
isInitializing: true,
125+
lastUsedAt: Date.now(),
126+
})
127+
128+
const language = await initPromise
129+
const managed = languageCache.get(langName)
130+
if (managed) {
131+
managed.language = language
132+
managed.initPromise = undefined
133+
managed.isInitializing = false
78134
}
79135

80-
const language = await parserClass!.Language.load(wasmPath)
81-
languageCache.set(langName, language)
82136
debugLog("language loaded and cached:", langName)
83-
84137
return language
85138
}
86139

140+
function warmupLanguage(langName: string): void {
141+
if (languageCache.has(langName)) return
142+
143+
debugLog("warming up language (background):", langName)
144+
145+
const initPromise = (async () => {
146+
await initParserClass()
147+
const wasmPath = await loadLanguageWasm(langName)
148+
if (!wasmPath) return null
149+
return await parserClass!.Language.load(wasmPath)
150+
})()
151+
152+
languageCache.set(langName, {
153+
language: null as unknown,
154+
initPromise,
155+
isInitializing: true,
156+
lastUsedAt: Date.now(),
157+
})
158+
159+
initPromise.then((language) => {
160+
const managed = languageCache.get(langName)
161+
if (managed) {
162+
managed.language = language
163+
managed.initPromise = undefined
164+
managed.isInitializing = false
165+
debugLog("warmup complete:", langName)
166+
}
167+
}).catch((err) => {
168+
debugLog("warmup failed:", langName, err)
169+
languageCache.delete(langName)
170+
})
171+
}
172+
173+
export function warmupCommonLanguages(): void {
174+
debugLog("starting background warmup for common languages...")
175+
initParserClass().then(() => {
176+
for (const lang of COMMON_LANGUAGES) {
177+
warmupLanguage(lang)
178+
}
179+
}).catch((err) => {
180+
debugLog("warmup initialization failed:", err)
181+
})
182+
}
183+
87184
// =============================================================================
88185
// Public API
89186
// =============================================================================

0 commit comments

Comments
 (0)