Smart Korean Grammar Assistant: VSCode韓国語スペルチェックExtension開発記
Smart Korean Grammar Assistant: 開発回顧と技術共有
https://github.com/Hun-Bot2/smart-korean-grammar-assistant
開発動機
普段文章を書くとき、文法を確認するためにBareun Korean [https://nara-speller.co.kr/speller/]をよく使います。ただ、毎回alt+tabで内容をコピー・貼り付けするのが不便でした。VSCode extensionにはvscode-hanspellというものがありましたが、最後の更新が3年前だったため、使う気になりませんでした。
他に良いものがないか探していたところ、Bareun AIが提供するサービスを見つけ、韓国語認識の品質が良いと感じたため、自分だけのextensionとして作ってみようと思いました。
Markdown file
まだブログが完全に開発完了したわけではないため、今はVSCodeを開いてMarkdownファイルで記事を書いており、この記事もその方式で作成しています。
そのため、今回開発したextensionは特定フォルダ内の.mdファイルだけを検査するようにしたいと考えました。そうしないと他のファイルもすべて検出してAPI使用量が増えるためです。リアルタイムでユーザーにスペルミスを知らせる形を目指し、韓国語に限って確実に検出することを目標にしました。開発のため、会社のメールへ簡単に開発したいのですが、大丈夫でしょうか?と問い合わせました。
[実際のメール]

会社からの連絡
翌日、代表の方から連絡があり、簡単な開発要件とミーティングを通じて具体的にどう開発すればよいか決めようという話になりました。 11月10日にミーティングが入り、当日はどんな機能が追加されるとよいか、どのような方式で開発するとよいかを検討しました。ミーティング後、既に開発していた部分を少しずつ整えました。
核心技術とアーキテクチャ
VSCode Extension API: 核心的に使用したAPI
このExtensionはVSCodeのさまざまなAPIを活用しました。
主なAPI:
vscode.languages.createDiagnosticCollection: Bareun応答とオフラインヒューリスティックをDiagnosticへ変換し、エディタに色の違う下線を描く。vscode.languages.registerCodeActionsProvider,registerHoverProvider: Quick FixとHoverでBareun提案・ユーザー辞書情報を表示する。vscode.workspace.onDidChangeTextDocument: Markdown文書が修正されるたびに350ms debounceで分析パイプラインを呼び出し、TextDocument.versionを比較して古い応答を捨てる。vscode.commands.registerCommand:bkga.fixSelection,bkga.syncCustomDictionary,bkga.toggleEnabledなどのユーザーコマンドを公開し、Selection単位の自動修正、ユーザー辞書パネルオープンなどを処理する。vscode.window.createStatusBarItem&createWebviewPanel: ステータスバーで分析状態/問題数を表示し、ユーザー辞書パネルをWebviewでレンダリングする。
下のコードは、「どの文書を分析するかを選ぶフィルター」と、「テキスト変更後に一定時間待って再分析するdebounceロジック」を示しています。
まずMarkdown言語かどうかを確認し、.mdファイルのpathを確認し、ワークスペース相対パスに適用します。
Markdownであり、includedPaths条件を満たすファイルだけを分析します。
すべてのテキスト変更イベントで既存タイマーをキャンセルし、新しいタイマーを350msに設定します。
function shouldAnalyze(doc: vscode.TextDocument): boolean {
if (doc.languageId !== 'markdown') return false;
const includePaths = vscode.workspace.getConfiguration('bkga').get<string[]>('includePaths', ['**/*.md']);
const workspaceFolder = vscode.workspace.getWorkspaceFolder(doc.uri);
if (!workspaceFolder) return false;
const relativePath = vscode.workspace.asRelativePath(doc.uri, false);
return includePaths.some((pattern) => {
const glob = new vscode.RelativePattern(workspaceFolder, pattern);
return vscode.languages.match({ pattern: glob, language: 'markdown' }, doc) > 0;
});
}
let debounceTimer: NodeJS.Timeout | undefined;
vscode.workspace.onDidChangeTextDocument((event) => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (!shouldAnalyze(event.document)) {
diagnosticsManager.clearDiagnostics(event.document.uri);
return;
}
diagnosticsManager.analyzeDocument(event.document);
}, 350);
});
プロジェクト構造
src/
├── extension.ts # エントリーポイント。イベント/コマンド/デコレーション登録
├── bareunClient.ts # HTTPS Bareun API呼び出しおよび応答パース
├── diagnostics.ts # Bareun Issue → Diagnostic, Markdownヒューリスティック
├── codeActions.ts # Quick Fix提供
├── hoverProvider.ts # Hoverカード + diffハイライト + ユーザー辞書リンク
├── decorations.ts # エラータイプ別カラーテーマ適用
├── status.ts # ステータスバー状態管理
├── customDictionary.ts # Bareun Custom Dictionary同期/設定管理
├── customDictionaryPanel.ts # ユーザー辞書Webviewパネル
└── customDictionaryMeta.ts # 辞書カテゴリメタデータ
extension.tsは有効化時にDiagnosticsManagerを作成し、Markdown文書だけを検査してStatusBar/Decorationsと連動します。
bareunClient.tsはNode HTTPSモジュールを直接使用してBareun REST APIへPOSTリクエストを送り、応答のrevisedBlocksを巡回しながらoffsetとcategoryを抽出します。
customDictionary*.tsファイルはVSCode settings.jsonとBareun Custom Dictionary APIの間で辞書データをシリアライズ/デシリアライズする役割を持ちます。
開発過程と問題解決
最も大きかった難しさ
問題1: Markdown特殊構文で発生する誤検知
Bareun APIは純粋なテキストoffsetを基準にエラーを返しますが、Markdown文書にはインラインコード、リンク、ショートカット案内などさまざまな形式が混ざっています。そのままDiagnosticを表示すると、コードブロックや英語キー組み合わせがスペースエラー・スペルエラーとして表示され、画面に下線が多くなり不便です。
問題2: API呼び出し頻度と古い診断結果
onDidChangeTextDocumentイベントはタイピング1文字ごとに発生します。単純なdebounceだけだと、以前のリクエストが遅れて戻り、最新入力を上書きするRace Conditionが発生しました。
問題解決過程
解決策1: Markdownに優しいフィルタリングヒューリスティック
AST(Abstract Syntax Tree)をパースするのではなく、diagnostics.tsでインラインコード範囲を先に計算し、英語中心の文、URL、ショートカットパターンを検知すると診断を削除するようにしました。
const inlineCodeOffsets = doc.languageId === 'markdown' ? this.computeInlineCodeOffsets(fullText) : [];
const diagnostics = issues
.map((issue) => {
if (this.intersectsInlineCode(issue.start, issue.end, inlineCodeOffsets)) {
return null;
}
const snippet = doc.getText(new vscode.Range(doc.positionAt(issue.start), doc.positionAt(issue.end)));
if (this.shouldIgnoreSnippet(snippet, category, ignoreEnglishInMarkdown)) {
return null;
}
return new vscode.Diagnostic(range, issue.message, this.mapSeverity(issue.severity));
})
.filter(Boolean);
shouldIgnoreSnippetは英語だけの文、[リンク](url)、Cmd+K G Fのようなショートカット組み合わせ、Bareun,APIのような韓国語+コンマパターンなどを検知し、Bareunが返したSPACING警告をフィルタリングします。これにより、Markdown特殊構文にはほとんど触れず、自然言語文だけを安定して検査できました。
解決策2: 言語検知 + Debounce + バージョン検証
shouldAnalyze関数でMarkdown + includePathsだけを検査し、350ms debounce後にもTextDocument.versionを保存しておき、応答時点で比較して最新文書でなければ結果を破棄しました。これにより、タイピング中にAPI呼び出しが重複せず、結果が遅れて上書きされる現象も消えました。Markdownではないファイルは即時DiagnosticsManager.clearDiagnosticsで空にし、APIリクエスト自体を行いません。
気に入った機能
最も気に入っている部分はHoverカードです。Bareun診断の原文/提案をdiffスタイルで比較し、同時にユーザー辞書状態を表示してすぐ追加/削除できます。
const markdown = new vscode.MarkdownString();
const diff = this.buildDiffHighlight(originalText, suggestion);
markdown.appendMarkdown(`#🇰🇷 ${categoryInfo.name}\n\n`);
markdown.appendMarkdown(`**原文**: ${diff.originalHtml}\n\n`);
markdown.appendMarkdown(`**置換語**: ${diff.suggestionHtml}\n\n`);
markdown.appendMarkdown(`**ヘルプ**: ${diagnostic.message}\n\n`);
markdown.appendMarkdown(this.buildCustomDictionarySection(hoveredWord, dictMatches));
HTMLを直接escapeしたあと<code>ブロックに強調範囲を重ねる方式なので、Markdownでも安定してdiffを表現できました。Hoverからすぐユーザー辞書コマンドを実行できる点も良かったです。
Hoverサイズを大きくするかも考えましたが、今後ユーザーのフィードバックを受けて修正することにしました。ユーザーがいれば、ですが。
結果と今後の計画
結果物: 完成したExtension使用後記
11月15日時点では、まだ自分しか使っていません。今後ユーザーが生まれたら、新しい記事として共有する予定です。
今後の計画
まず、これをChrome Extensionとしても作るか考えています。 フィードバックを通じて、エラーのある機能や不正確な部分を確実に直し、より良い拡張機能を提供したいです。
考えている部分は、API使用量削減のための何か、Bareun AIは形態素分析まで提供しているので、これをどう適用できるか、などです。今日デプロイしたばかりで試験も近いため、以後考えてみます。
以下はAIに発展方向を模索してもらった結果です。
短期計画
- SecretStorageベースAPIキー管理: 現在はVSCode設定に平文で入力する構造なので、初回実行時にQuick PickでSecretStorageへ保存し、設定と連動するUIを作る予定です。
- includePaths便利機能: よく使うブログフォルダをすばやく登録できるよう、コマンドパレットで
bkga.includePathsを修正するUIを追加する予定です。
中期計画
- 段落単位Promise Pool: 長いMarkdown文書を段落単位でchunkingし、同時に複数のBareunリクエストを送りつつ、同時性を制限してUI応答性を維持する計画です。
- Webviewパネル編集対応: ユーザー辞書パネルで直接単語を追加/削除し、Syncまで実行できる双方向UIを準備中です。
長期計画
- 高度な校正モード: Grammarlyのようなtone/tightening提案を提供するカスタムモデルと、校正前/後diffを見せるCustom Editor。
- ローカルプリチェッカー: wasmベース軽量ルールセットを追加し、単純なスペース/空白エラーはAPIなしでも検出できるよう拡張したいです。
インストールと貢献案内
Extensionインストール:
- VSCode Marketplace: Smart Korean Grammar Assistant
- VSCodeで
Extensions: Install Extensions→smart korean grammarを検索してインストール。初回実行時にBareun APIキー設定案内が表示されます。 - VSIXでインストールするには、
code --install-extension smart-korean-grammar-assistant-1.0.0.vsixを実行します。
プロジェクト貢献:
- GitHubリポジトリ: https://github.com/Hun-Bot2/smart-korean-grammar-assistant
npm install && npm run compileでローカルビルドを確認し、VSCodeでF5を押してExtension Development Hostを開いたあとPRを送ってください。- バグレポートはIssues、アイデアはDiscussionsに分けていただけると、より早く確認できます。
おわりに
拡張を作りながら、VSCode Extension Marketに自分が作ったものを上げたことが面白く、不思議でもありました。最初は自分の不便を解決しようとしてメールを送りましたが、こうして会社の代表の方とミーティングしてextensionを作るとは思いませんでした。助けてくださったBareun AIチームと機会を提供してくださったことに感謝します。フィードバックがあれば、開発アップデートとして戻ってきます。
댓글