Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .agents/skills/rbtr-languages/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ at absolute line numbers. Every file also gets a host-language chunk (a
content-less presence chunk if it would produce none), so dedup works. See
ARCHITECTURE “Dispatch chain” for the mechanism and rationale.

### Where queries live

Every query — `reg.query`, `reg.injection_query`, and any query a chunker
compiles — is a `.scm` file in the plugin's own package
(`languages/<lang>/<name>.scm`), loaded at import via
`load_query(__package__, "<name>")`. Never inline a query as a Python string
literal (the house rule against embedding a foreign language). The `uv`
build backend ships `.scm` as package data with no extra config.

Compose in Python when a query is built from parts — the query language has
no `#include`: js/ts concatenate shared fragment files with `+`
(`load_query(pkg, "javascript") + load_query(pkg, "shared") + …`); SQL groups
its DDL verbs into `[...]` alternations within one `sql.scm`. Prefer these to
generating query text from Python data. Editing a `.scm` is an extraction
change — the `language_plugin_version` bump rule (below) applies.

## Capture conventions

The query's capture names drive the chunk kind (see `_CAPTURE_KIND` in
Expand Down Expand Up @@ -188,7 +204,9 @@ captured node, so they work for any language:
1. Read the grammar: its `queries/tags.scm` (the authors' definition list —
see the tags reference) **and** the real node structure (parse a snippet,
print the tree). Never guess node types.
2. Extend the `_QUERY` (or chunker). Verify against a parsed snippet.
2. Edit the language's `.scm` query file (or chunker) — queries live in
`languages/<lang>/*.scm`, loaded via `load_query`, never inline (see
*Where queries live*). Verify against a parsed snippet.
3. Add the construct to that language's sample in `tests/languages/samples/`
and regenerate the snapshot with `just snapshots`; review the diff.
4. Bump `language_plugin_version` — any extraction change triggers
Expand Down Expand Up @@ -232,6 +250,10 @@ itself a tree-sitter query, so it is **inspiration for ours, not a drop-in**:
(ts ← js), and only the 9 code grammars ship one.
- Running it live would couple extraction to upstream drift. We don't.

Distinct from **rbtr's own** `.scm` files (see *Where queries live*): we load
those at runtime (`reg.query` / `injection_query`) as the source of truth,
whereas `tags.scm` we only mine for ideas.

Curated per-language verdicts (take / modify / ignore) and the
`@definition.* → ChunkKind` mapping live in
`references/tags-scm-reference.md`.
Expand Down
10 changes: 6 additions & 4 deletions packages/rbtr/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1070,10 +1070,12 @@ chunks.

## Language plugins

[pluggy]-based. Each plugin is a Python file under
`rbtr/languages/` that returns `LanguageRegistration`
instances. A plugin provides whatever combination of fields
its language needs - there are no tiers or categories.
[pluggy]-based. Each plugin is a package under
`rbtr/languages/<lang>/` (a `plugin.py` plus its tree-sitter
queries as sibling `.scm` files, loaded via `load_query`)
that returns `LanguageRegistration` instances. A plugin
provides whatever combination of fields its language needs -
there are no tiers or categories.

[pluggy]: https://pluggy.readthedocs.io/

Expand Down
35 changes: 20 additions & 15 deletions packages/rbtr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,30 +375,34 @@ for how the plugin system works.

## Writing a language plugin

Every plugin is a Python file under `rbtr/languages/`
with a pluggy hook that returns `LanguageRegistration`
instances. A plugin provides whatever fields its language
needs.
Every plugin is a package under `rbtr/languages/<lang>/` — a
`plugin.py` with a pluggy hook that returns
`LanguageRegistration` instances, plus its tree-sitter query
in a sibling `.scm` file. A plugin provides whatever fields
its language needs.

### Query-based plugin (preferred)

Most languages need only a tree-sitter query. The generic
`extract_symbols` pipeline handles parsing, capture
matching, scope detection, and chunk construction.
matching, scope detection, and chunk construction. The query
lives in its own `.scm` file, loaded with `load_query`:

```python
# rbtr/languages/swift.py
from __future__ import annotations
from rbtr.languages.hookspec import LanguageRegistration, hookimpl

_QUERY = """\
```scm
; rbtr/languages/swift/swift.scm
(function_declaration
name: (identifier) @_fn_name) @function
(class_declaration
name: (type_identifier) @_cls_name) @class
(import_declaration
(identifier) @_import_module) @import
"""
```

```python
# rbtr/languages/swift/plugin.py
from __future__ import annotations
from rbtr.languages._queries import load_query
from rbtr.languages.hookspec import LanguageRegistration, hookimpl

class SwiftPlugin:
@hookimpl
Expand All @@ -408,7 +412,7 @@ class SwiftPlugin:
id="swift",
extensions=frozenset({".swift"}),
grammar_module="tree_sitter_swift",
query=_QUERY,
query=load_query(__package__, "swift"),
scope_types=frozenset({"class_declaration"}),
doc_comment_node_types=frozenset({"comment"}),
),
Expand Down Expand Up @@ -456,7 +460,7 @@ content-minus-children), write a custom chunker. The
chunker receives the grammar from the manager:

```python
# rbtr/languages/example.py
# rbtr/languages/example/plugin.py
from __future__ import annotations
from collections.abc import Iterator
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -496,7 +500,8 @@ class ExamplePlugin:

### Registration

Built-in plugins: import the class at the top of
Built-in plugins: add the `<lang>/` package (a `plugin.py`
and any `.scm` files), then import the class at the top of
`rbtr/languages/__init__.py` and list it in `_register_builtins`.

External plugins: register via setuptools entry points:
Expand Down
42 changes: 21 additions & 21 deletions packages/rbtr/src/rbtr/languages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,28 @@
import pluggy
from tree_sitter import Language

from rbtr.languages.bash import BashPlugin
from rbtr.languages.c import CPlugin
from rbtr.languages.cpp import CppPlugin
from rbtr.languages.css import CssPlugin
from rbtr.languages.go import GoPlugin
from rbtr.languages.hcl import HclPlugin
from rbtr.languages.bash.plugin import BashPlugin
from rbtr.languages.c.plugin import CPlugin
from rbtr.languages.cpp.plugin import CppPlugin
from rbtr.languages.css.plugin import CssPlugin
from rbtr.languages.go.plugin import GoPlugin
from rbtr.languages.hcl.plugin import HclPlugin
from rbtr.languages.hookspec import PROJECT_NAME, LanguageHookspec, LanguageRegistration
from rbtr.languages.html import HtmlPlugin
from rbtr.languages.java import JavaPlugin
from rbtr.languages.javascript import JavaScriptPlugin
from rbtr.languages.json import JsonPlugin
from rbtr.languages.less import LessPlugin
from rbtr.languages.markdown import MarkdownPlugin
from rbtr.languages.python import PythonPlugin
from rbtr.languages.rst import RstPlugin
from rbtr.languages.ruby import RubyPlugin
from rbtr.languages.rust import RustPlugin
from rbtr.languages.scss import ScssPlugin
from rbtr.languages.sfc import SveltePlugin, VuePlugin
from rbtr.languages.sql import SqlPlugin
from rbtr.languages.toml import TomlPlugin
from rbtr.languages.yaml import YamlPlugin
from rbtr.languages.html.plugin import HtmlPlugin
from rbtr.languages.java.plugin import JavaPlugin
from rbtr.languages.javascript.plugin import JavaScriptPlugin
from rbtr.languages.json.plugin import JsonPlugin
from rbtr.languages.less.plugin import LessPlugin
from rbtr.languages.markdown.plugin import MarkdownPlugin
from rbtr.languages.python.plugin import PythonPlugin
from rbtr.languages.rst.plugin import RstPlugin
from rbtr.languages.ruby.plugin import RubyPlugin
from rbtr.languages.rust.plugin import RustPlugin
from rbtr.languages.scss.plugin import ScssPlugin
from rbtr.languages.sfc.plugin import SveltePlugin, VuePlugin
from rbtr.languages.sql.plugin import SqlPlugin
from rbtr.languages.toml.plugin import TomlPlugin
from rbtr.languages.yaml.plugin import YamlPlugin


class LanguageManager:
Expand Down
25 changes: 25 additions & 0 deletions packages/rbtr/src/rbtr/languages/_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Load tree-sitter queries from `.scm` files beside each plugin.

Query text lives in `<name>.scm` inside the plugin's own package,
loaded at import time via `load_query` — not embedded inline in Python.
See ARCHITECTURE "Where queries live" for the rationale.
"""

from __future__ import annotations

from importlib.resources import files


def load_query(package: str, name: str) -> str:
"""Return the text of a `<name>.scm` query file in *package*.

Args:
package: The plugin package to resolve against, normally the
caller's own `__package__` (e.g. `"rbtr.languages.python"`).
name: Query filename without the `.scm` extension (e.g.
`"python"`, `"injections"`).

Returns:
The file's contents verbatim.
"""
return (files(package) / f"{name}.scm").read_text(encoding="utf-8")
1 change: 1 addition & 0 deletions packages/rbtr/src/rbtr/languages/bash/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Bash language plugin package."""
31 changes: 31 additions & 0 deletions packages/rbtr/src/rbtr/languages/bash/bash.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
(function_definition
name: (word) @_fn_name) @function

(command
name: (command_name
(word) @_cmd)
.
(word) @_import_module
(#eq? @_cmd "source")) @import

(command
name: (command_name
(word) @_cmd)
.
(word) @_import_module
(#eq? @_cmd ".")) @import

(program
(variable_assignment
name: (variable_name) @_var_name) @variable)

(program
(declaration_command
(variable_assignment
name: (variable_name) @_var_name)) @variable)

(command
name: (command_name (word) @_cmd)
argument: (concatenation (word) @_var_name)
(#eq? @_cmd "alias")
(#match? @_var_name "=")) @variable
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class system, or module structure.

from typing import TYPE_CHECKING

from rbtr.languages._queries import load_query
from rbtr.languages.hookspec import LanguageRegistration, hookimpl, resolve_name

if TYPE_CHECKING:
Expand All @@ -33,39 +34,7 @@ def _strip_alias_eq(capture_name: str, node: Node, captures: dict[str, list[Node

# ── Query ────────────────────────────────────────────────────────────

_QUERY = """\
(function_definition
name: (word) @_fn_name) @function

(command
name: (command_name
(word) @_cmd)
.
(word) @_import_module
(#eq? @_cmd "source")) @import

(command
name: (command_name
(word) @_cmd)
.
(word) @_import_module
(#eq? @_cmd ".")) @import

(program
(variable_assignment
name: (variable_name) @_var_name) @variable)

(program
(declaration_command
(variable_assignment
name: (variable_name) @_var_name)) @variable)

(command
name: (command_name (word) @_cmd)
argument: (concatenation (word) @_var_name)
(#eq? @_cmd "alias")
(#match? @_var_name "=")) @variable
"""
_QUERY = load_query(__package__, "bash")

# ── Plugin ───────────────────────────────────────────────────────────

Expand Down
1 change: 1 addition & 0 deletions packages/rbtr/src/rbtr/languages/c/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""C language plugin package."""
61 changes: 61 additions & 0 deletions packages/rbtr/src/rbtr/languages/c/c.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

(function_definition
declarator: (function_declarator
declarator: (identifier) @_fn_name)) @function

(preproc_include
path: (system_lib_string) @_import_module) @import

(preproc_include
path: (string_literal) @_import_module) @import

(struct_specifier
name: (type_identifier) @_cls_name
body: (field_declaration_list)) @class

(union_specifier
name: (type_identifier) @_cls_name
body: (field_declaration_list)) @class

(enum_specifier
name: (type_identifier) @_cls_name
body: (enumerator_list)) @class

(enumerator
name: (identifier) @_var_name) @variable

(type_definition
declarator: (type_identifier) @_cls_name) @class

(type_definition
declarator: (function_declarator
declarator: (parenthesized_declarator
(pointer_declarator
declarator: (type_identifier) @_cls_name)))) @class

(preproc_function_def
name: (identifier) @_fn_name) @function

(preproc_def
name: (identifier) @_var_name) @variable

(translation_unit
(declaration
declarator: (function_declarator
declarator: (identifier) @_fn_name)) @function)

(translation_unit
(declaration
declarator: (init_declarator
declarator: (identifier) @_var_name)) @variable)

(translation_unit
(declaration
declarator: (pointer_declarator
declarator: (identifier) @_var_name)) @variable)

(translation_unit
(declaration
declarator: (init_declarator
declarator: (pointer_declarator
declarator: (identifier) @_var_name))) @variable)
Loading