This directory contains Tree-sitter query files (.scm) that define which parts of each language's source code are extracted for spell checking.
Every capture name is a tag that categorizes the matched text. Tags use a dot-separated hierarchy so users can filter what gets checked via include_tags/exclude_tags in codebook.toml. Matching is prefix-based: "comment" matches comment, comment.line, comment.block, etc.
Not every language needs every tag. HTML, for example, only uses @comment and @string. You can get a feel for which tags are available for a specific language by looking at the scm file for that language in this directory.
In addition to tree-sitter's built-in predicates (#eq?, #match?, #any-of?, …), codebook evaluates one extra predicate:
| Predicate | Effect |
|---|---|
(#not-has-ancestor? @capture "kind" ["kind" …]) |
Drop the capture if any ancestor of the captured node has one of the listed tree-sitter node kinds. |
Use this to narrow a broad capture instead of enumerating every positive context. For example, Python's (string_content) matches every string in the file, but inside type annotations (forward references, generic arguments) it's another tool's job:
((string_content) @string
(#not-has-ancestor? @string "type"))Injection tags tell codebook to re-parse a region of the file using a different language's grammar. This is how Markdown code blocks, HTML <script> tags, and similar multi-language files are handled.
Example from markdown.scm:
; Prose content:spell-checked as text
(paragraph (inline) @string)
(atx_heading (inline) @string)
; HTML blocks:re-parsed with the HTML grammar
(html_block) @injection.html
; Fenced code blocks:language read from the info string
(fenced_code_block
(info_string (language) @injection.language)
(code_fence_content) @injection.content)The @injection.language value is resolved against the ids and extensions fields in LANGUAGE_SETTINGS. Common aliases like py, js, sh, rs, etc. work automatically.
Create a new .scm file in this directory named after your language (e.g., java.scm).
Use namespaced capture names from the table above. Example:
(comment) @comment
(string_content) @string
(function_declaration name: (identifier) @identifier.function)
(parameter name: (identifier) @identifier.parameter)
(variable_declaration (identifier) @identifier.variable)
(class_declaration name: (identifier) @identifier.type)Use these tools to explore the grammar's node types:
A good approach:
- Write sample code with identifiers, strings, and comments
- Paste it into the playground/visualizer
- Observe the node types used for each element
- Create capture patterns that target only definition nodes, not usages
Add your language to queries.rs:
- Add a new variant to the
LanguageTypeenum - Add a new entry to the
LANGUAGE_SETTINGSarray with the language type, file extensions, language identifiers, and path to your query file
Add the grammar as a dependency in Cargo.toml and update the language() function in queries.rs to return the correct parser.
cargo test -p codebook queries::tests::test_all_queries_are_validAdditional language tests go in crates/codebook/tests/. Example files with at least one spelling error go in examples/.
- Focus on capturing definitions, not usages
- Only capture nodes that contain user-defined text (not keywords)
- Always use namespaced capture names (
@identifier.function, not@func_declaration) - Use the most specific tag that fits (e.g.,
@identifier.typeover@identifier) - Avoid overlapping captures:don't let two patterns capture the same node at the same byte range. For example, a blanket
(atom) @string.specialwould overlap with(function_clause name: (atom) @identifier.function). Instead, capture atoms only in specific parent contexts like(tuple (atom) @string.special). Adebug_assertwill catch overlaps during testing. - Start simple and add complexity as needed
- Look at existing query files for patterns
