From ff09a5ad97f461163b5a3ad3f9616c7074de6d9f Mon Sep 17 00:00:00 2001
From: Patrick Dewey
Date: Wed, 4 Feb 2026 16:42:49 -0500
Subject: [PATCH] feat: CLAUDE.md and tree sitter init
---
CLAUDE.md | 76 ++++++++++++++
editors/.editorconfig | 46 +++++++++
editors/.gitattributes | 42 ++++++++
editors/.gitignore | 50 ++++++++++
editors/CMakeLists.txt | 66 +++++++++++++
editors/Cargo.toml | 34 +++++++
editors/Makefile | 99 +++++++++++++++++++
editors/Package.swift | 41 ++++++++
editors/binding.gyp | 35 +++++++
editors/bindings/c/tree-sitter-snapshot.pc.in | 10 ++
.../c/tree_sitter/tree-sitter-snapshot.h | 16 +++
editors/bindings/go/binding.go | 15 +++
editors/bindings/go/binding_test.go | 15 +++
editors/bindings/node/binding.cc | 19 ++++
editors/bindings/node/binding_test.js | 9 ++
editors/bindings/node/index.d.ts | 27 +++++
editors/bindings/node/index.js | 11 +++
editors/bindings/python/tests/test_binding.py | 12 +++
.../python/tree_sitter_snapshot/__init__.py | 42 ++++++++
.../python/tree_sitter_snapshot/__init__.pyi | 10 ++
.../python/tree_sitter_snapshot/binding.c | 35 +++++++
.../python/tree_sitter_snapshot/py.typed | 0
editors/bindings/rust/build.rs | 21 ++++
editors/bindings/rust/lib.rs | 51 ++++++++++
.../swift/TreeSitterSnapshot/snapshot.h | 16 +++
.../TreeSitterSnapshotTests.swift | 12 +++
editors/go.mod | 5 +
editors/grammar.js | 17 ++++
editors/package.json | 52 ++++++++++
editors/pyproject.toml | 29 ++++++
editors/setup.py | 77 +++++++++++++++
editors/tree-sitter.json | 39 ++++++++
32 files changed, 1029 insertions(+)
create mode 100644 CLAUDE.md
create mode 100644 editors/.editorconfig
create mode 100644 editors/.gitattributes
create mode 100644 editors/.gitignore
create mode 100644 editors/CMakeLists.txt
create mode 100644 editors/Cargo.toml
create mode 100644 editors/Makefile
create mode 100644 editors/Package.swift
create mode 100644 editors/binding.gyp
create mode 100644 editors/bindings/c/tree-sitter-snapshot.pc.in
create mode 100644 editors/bindings/c/tree_sitter/tree-sitter-snapshot.h
create mode 100644 editors/bindings/go/binding.go
create mode 100644 editors/bindings/go/binding_test.go
create mode 100644 editors/bindings/node/binding.cc
create mode 100644 editors/bindings/node/binding_test.js
create mode 100644 editors/bindings/node/index.d.ts
create mode 100644 editors/bindings/node/index.js
create mode 100644 editors/bindings/python/tests/test_binding.py
create mode 100644 editors/bindings/python/tree_sitter_snapshot/__init__.py
create mode 100644 editors/bindings/python/tree_sitter_snapshot/__init__.pyi
create mode 100644 editors/bindings/python/tree_sitter_snapshot/binding.c
create mode 100644 editors/bindings/python/tree_sitter_snapshot/py.typed
create mode 100644 editors/bindings/rust/build.rs
create mode 100644 editors/bindings/rust/lib.rs
create mode 100644 editors/bindings/swift/TreeSitterSnapshot/snapshot.h
create mode 100644 editors/bindings/swift/TreeSitterSnapshotTests/TreeSitterSnapshotTests.swift
create mode 100644 editors/go.mod
create mode 100644 editors/grammar.js
create mode 100644 editors/package.json
create mode 100644 editors/pyproject.toml
create mode 100644 editors/setup.py
create mode 100644 editors/tree-sitter.json
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..69aa6ef
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,76 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+Shutter is a snapshot testing library for Go, inspired by Rust's [insta](https://github.com/mitsuhiko/insta) and Gleam's [birdie](https://github.com/giacomocavalieri/birdie). It provides functions for capturing test output as snapshots and tools for reviewing snapshot changes.
+
+## Build Commands
+
+```bash
+# Build the TUI binary
+just build
+
+# Run tests with coverage
+just test
+
+# Clean snapshots and run tests
+just clean-test
+
+# Run the TUI review tool (after building)
+just review
+
+# Run CLI review tool
+just cli
+```
+
+Direct Go commands:
+```bash
+go test ./... # Run all tests
+go test ./... -run TestName # Run specific test
+go test -v ./internal/transform/... # Run tests in specific package
+cd ./cmd/shutter && go build -o shutter ./main.go # Build TUI
+```
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Public API (shutter.go) │
+│ Snap() | SnapMany() | SnapString() | SnapJSON() │
+├─────────────────────────────────────────────────────────────────┤
+│ Options (scrubbers.go, ignore.go) │
+│ Scrubbers: text transformation before snapshot │
+│ IgnorePatterns: field removal (SnapJSON only) │
+├─────────────────────────────────────────────────────────────────┤
+│ Internal Modules │
+│ ├─ internal/snapshots/ - Core comparison logic │
+│ ├─ internal/files/ - Snapshot file I/O (YAML headers) │
+│ ├─ internal/transform/ - JSON ignore pattern application │
+│ ├─ internal/diff/ - Histogram diff algorithm │
+│ ├─ internal/pretty/ - Formatting and display boxes │
+│ └─ internal/review/ - Review workflow logic │
+├─────────────────────────────────────────────────────────────────┤
+│ Review Tools │
+│ ├─ cmd/shutter/ - TUI (Bubbletea) - separate go.mod │
+│ └─ cmd/cli/ - CLI review tool │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Data flow:** Test Value → Pretty format (utter) → Ignore Patterns → Scrubbers → Snapshot file
+
+**Snapshot storage:** `__snapshots__/` directories contain YAML-header files with metadata (title, test_name, file_name, version) followed by `---` delimiter and content.
+
+## Key Design Decisions
+
+- **Option interface pattern**: `Scrubber` and `IgnorePattern` both implement `Option` for type-safe compile-time separation
+- **IgnorePatterns only work with SnapJSON()** - using them with Snap/SnapMany/SnapString returns an error
+- **TUI is a separate Go module** (cmd/shutter/) to keep Bubbletea dependencies optional
+- **Execution order**: Ignore patterns run first, then scrubbers
+
+## Module Structure
+
+- Root module (`go.mod`): Main library - Go 1.23.12+
+- TUI module (`cmd/shutter/go.mod`): Separate module with Bubbletea dependencies - Go 1.25.2
+- `/editors/`: Tree-sitter grammar for snapshot format (Node.js/Rust/Python/Swift bindings)
diff --git a/editors/.editorconfig b/editors/.editorconfig
new file mode 100644
index 0000000..65330c4
--- /dev/null
+++ b/editors/.editorconfig
@@ -0,0 +1,46 @@
+root = true
+
+[*]
+charset = utf-8
+
+[*.{json,toml,yml,gyp}]
+indent_style = space
+indent_size = 2
+
+[*.js]
+indent_style = space
+indent_size = 2
+
+[*.scm]
+indent_style = space
+indent_size = 2
+
+[*.{c,cc,h}]
+indent_style = space
+indent_size = 4
+
+[*.rs]
+indent_style = space
+indent_size = 4
+
+[*.{py,pyi}]
+indent_style = space
+indent_size = 4
+
+[*.swift]
+indent_style = space
+indent_size = 4
+
+[*.go]
+indent_style = tab
+indent_size = 8
+
+[Makefile]
+indent_style = tab
+indent_size = 8
+
+[parser.c]
+indent_size = 2
+
+[{alloc,array,parser}.h]
+indent_size = 2
diff --git a/editors/.gitattributes b/editors/.gitattributes
new file mode 100644
index 0000000..7772c94
--- /dev/null
+++ b/editors/.gitattributes
@@ -0,0 +1,42 @@
+* text=auto eol=lf
+
+# Generated source files
+src/*.json linguist-generated
+src/parser.c linguist-generated
+src/tree_sitter/* linguist-generated
+
+# C bindings
+bindings/c/** linguist-generated
+CMakeLists.txt linguist-generated
+Makefile linguist-generated
+
+# Rust bindings
+bindings/rust/* linguist-generated
+Cargo.toml linguist-generated
+Cargo.lock linguist-generated
+
+# Node.js bindings
+bindings/node/* linguist-generated
+binding.gyp linguist-generated
+package.json linguist-generated
+package-lock.json linguist-generated
+
+# Python bindings
+bindings/python/** linguist-generated
+setup.py linguist-generated
+pyproject.toml linguist-generated
+
+# Go bindings
+bindings/go/* linguist-generated
+go.mod linguist-generated
+go.sum linguist-generated
+
+# Swift bindings
+bindings/swift/** linguist-generated
+Package.swift linguist-generated
+Package.resolved linguist-generated
+
+# Zig bindings
+bindings/zig/* linguist-generated
+build.zig linguist-generated
+build.zig.zon linguist-generated
diff --git a/editors/.gitignore b/editors/.gitignore
new file mode 100644
index 0000000..87a0c80
--- /dev/null
+++ b/editors/.gitignore
@@ -0,0 +1,50 @@
+# Rust artifacts
+target/
+Cargo.lock
+
+# Node artifacts
+build/
+prebuilds/
+node_modules/
+package-lock.json
+
+# Swift artifacts
+.build/
+Package.resolved
+
+# Go artifacts
+_obj/
+
+# Python artifacts
+.venv/
+dist/
+*.egg-info
+*.whl
+
+# C artifacts
+*.a
+*.so
+*.so.*
+*.dylib
+*.dll
+*.pc
+*.exp
+*.lib
+
+# Zig artifacts
+.zig-cache/
+zig-cache/
+zig-out/
+
+# Example dirs
+/examples/*/
+
+# Grammar volatiles
+*.wasm
+*.obj
+*.o
+
+# Archives
+*.tar.gz
+*.tgz
+*.zip
diff --git a/editors/CMakeLists.txt b/editors/CMakeLists.txt
new file mode 100644
index 0000000..1c559e2
--- /dev/null
+++ b/editors/CMakeLists.txt
@@ -0,0 +1,66 @@
+cmake_minimum_required(VERSION 3.13)
+
+project(tree-sitter-snapshot
+ VERSION "0.1.0"
+ DESCRIPTION "Snapshot test files generated by Shutter"
+ HOMEPAGE_URL "https://github.com/ptdewey/shutter"
+ LANGUAGES C)
+
+option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
+option(TREE_SITTER_REUSE_ALLOCATOR "Reuse the library allocator" OFF)
+
+set(TREE_SITTER_ABI_VERSION 15 CACHE STRING "Tree-sitter ABI version")
+if(NOT ${TREE_SITTER_ABI_VERSION} MATCHES "^[0-9]+$")
+ unset(TREE_SITTER_ABI_VERSION CACHE)
+ message(FATAL_ERROR "TREE_SITTER_ABI_VERSION must be an integer")
+endif()
+
+include(GNUInstallDirs)
+
+find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI")
+
+add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
+ DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
+ COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
+ --abi=${TREE_SITTER_ABI_VERSION}
+ WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+ COMMENT "Generating parser.c")
+
+add_library(tree-sitter-snapshot src/parser.c)
+if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/scanner.c)
+ target_sources(tree-sitter-snapshot PRIVATE src/scanner.c)
+endif()
+target_include_directories(tree-sitter-snapshot
+ PRIVATE src
+ INTERFACE $
+ $)
+
+target_compile_definitions(tree-sitter-snapshot PRIVATE
+ $<$:TREE_SITTER_REUSE_ALLOCATOR>
+ $<$:TREE_SITTER_DEBUG>)
+
+set_target_properties(tree-sitter-snapshot
+ PROPERTIES
+ C_STANDARD 11
+ POSITION_INDEPENDENT_CODE ON
+ SOVERSION "${TREE_SITTER_ABI_VERSION}.${PROJECT_VERSION_MAJOR}"
+ DEFINE_SYMBOL "")
+
+configure_file(bindings/c/tree-sitter-snapshot.pc.in
+ "${CMAKE_CURRENT_BINARY_DIR}/tree-sitter-snapshot.pc" @ONLY)
+
+install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+ FILES_MATCHING PATTERN "*.h")
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/tree-sitter-snapshot.pc"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
+install(TARGETS tree-sitter-snapshot
+ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}")
+
+file(GLOB QUERIES queries/*.scm)
+install(FILES ${QUERIES}
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/tree-sitter/queries/snapshot")
+
+add_custom_target(ts-test "${TREE_SITTER_CLI}" test
+ WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+ COMMENT "tree-sitter test")
diff --git a/editors/Cargo.toml b/editors/Cargo.toml
new file mode 100644
index 0000000..07f3105
--- /dev/null
+++ b/editors/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "tree-sitter-snapshot"
+description = "Snapshot test files generated by Shutter"
+version = "0.1.0"
+authors = ["ptdewey"]
+license = "MIT"
+readme = "README.md"
+keywords = ["incremental", "parsing", "tree-sitter", "snapshot"]
+categories = ["parser-implementations", "parsing", "text-editors"]
+repository = "https://github.com/ptdewey/shutter"
+edition = "2021"
+autoexamples = false
+
+build = "bindings/rust/build.rs"
+include = [
+ "bindings/rust/*",
+ "grammar.js",
+ "queries/*",
+ "src/*",
+ "tree-sitter.json",
+ "/LICENSE",
+]
+
+[lib]
+path = "bindings/rust/lib.rs"
+
+[dependencies]
+tree-sitter-language = "0.1"
+
+[build-dependencies]
+cc = "1.2"
+
+[dev-dependencies]
+tree-sitter = "0.25.10"
diff --git a/editors/Makefile b/editors/Makefile
new file mode 100644
index 0000000..cd5a682
--- /dev/null
+++ b/editors/Makefile
@@ -0,0 +1,99 @@
+ifeq ($(OS),Windows_NT)
+$(error Windows is not supported)
+endif
+
+LANGUAGE_NAME := tree-sitter-snapshot
+HOMEPAGE_URL := https://github.com/ptdewey/shutter
+VERSION := 0.1.0
+
+# repository
+SRC_DIR := src
+
+TS ?= tree-sitter
+
+# install directory layout
+PREFIX ?= /usr/local
+DATADIR ?= $(PREFIX)/share
+INCLUDEDIR ?= $(PREFIX)/include
+LIBDIR ?= $(PREFIX)/lib
+PCLIBDIR ?= $(LIBDIR)/pkgconfig
+
+# source/object files
+PARSER := $(SRC_DIR)/parser.c
+EXTRAS := $(filter-out $(PARSER),$(wildcard $(SRC_DIR)/*.c))
+OBJS := $(patsubst %.c,%.o,$(PARSER) $(EXTRAS))
+
+# flags
+ARFLAGS ?= rcs
+override CFLAGS += -I$(SRC_DIR) -std=c11 -fPIC
+
+# ABI versioning
+SONAME_MAJOR = $(shell sed -n 's/\#define LANGUAGE_VERSION //p' $(PARSER))
+SONAME_MINOR = $(word 1,$(subst ., ,$(VERSION)))
+
+# OS-specific bits
+ifeq ($(shell uname),Darwin)
+ SOEXT = dylib
+ SOEXTVER_MAJOR = $(SONAME_MAJOR).$(SOEXT)
+ SOEXTVER = $(SONAME_MAJOR).$(SONAME_MINOR).$(SOEXT)
+ LINKSHARED = -dynamiclib -Wl,-install_name,$(LIBDIR)/lib$(LANGUAGE_NAME).$(SOEXTVER),-rpath,@executable_path/../Frameworks
+else
+ SOEXT = so
+ SOEXTVER_MAJOR = $(SOEXT).$(SONAME_MAJOR)
+ SOEXTVER = $(SOEXT).$(SONAME_MAJOR).$(SONAME_MINOR)
+ LINKSHARED = -shared -Wl,-soname,lib$(LANGUAGE_NAME).$(SOEXTVER)
+endif
+ifneq ($(filter $(shell uname),FreeBSD NetBSD DragonFly),)
+ PCLIBDIR := $(PREFIX)/libdata/pkgconfig
+endif
+
+all: lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) $(LANGUAGE_NAME).pc
+
+lib$(LANGUAGE_NAME).a: $(OBJS)
+ $(AR) $(ARFLAGS) $@ $^
+
+lib$(LANGUAGE_NAME).$(SOEXT): $(OBJS)
+ $(CC) $(LDFLAGS) $(LINKSHARED) $^ $(LDLIBS) -o $@
+ifneq ($(STRIP),)
+ $(STRIP) $@
+endif
+
+$(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in
+ sed -e 's|@PROJECT_VERSION@|$(VERSION)|' \
+ -e 's|@CMAKE_INSTALL_LIBDIR@|$(LIBDIR:$(PREFIX)/%=%)|' \
+ -e 's|@CMAKE_INSTALL_INCLUDEDIR@|$(INCLUDEDIR:$(PREFIX)/%=%)|' \
+ -e 's|@PROJECT_DESCRIPTION@|$(DESCRIPTION)|' \
+ -e 's|@PROJECT_HOMEPAGE_URL@|$(HOMEPAGE_URL)|' \
+ -e 's|@CMAKE_INSTALL_PREFIX@|$(PREFIX)|' $< > $@
+
+$(PARSER): $(SRC_DIR)/grammar.json
+ $(TS) generate $^
+
+install: all
+ install -d '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/snapshot '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)'
+ install -m644 bindings/c/tree_sitter/$(LANGUAGE_NAME).h '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h
+ install -m644 $(LANGUAGE_NAME).pc '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
+ install -m644 lib$(LANGUAGE_NAME).a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a
+ install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER)
+ ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR)
+ ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT)
+ifneq ($(wildcard queries/*.scm),)
+ install -m644 queries/*.scm '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/snapshot
+endif
+
+uninstall:
+ $(RM) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a \
+ '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) \
+ '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) \
+ '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) \
+ '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h \
+ '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
+ $(RM) -r '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/snapshot
+
+clean:
+ $(RM) $(OBJS) $(LANGUAGE_NAME).pc lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT)
+
+test:
+ $(TS) test
+
+.PHONY: all install uninstall clean test
diff --git a/editors/Package.swift b/editors/Package.swift
new file mode 100644
index 0000000..5deecd5
--- /dev/null
+++ b/editors/Package.swift
@@ -0,0 +1,41 @@
+// swift-tools-version:5.3
+
+import Foundation
+import PackageDescription
+
+var sources = ["src/parser.c"]
+if FileManager.default.fileExists(atPath: "src/scanner.c") {
+ sources.append("src/scanner.c")
+}
+
+let package = Package(
+ name: "TreeSitterSnapshot",
+ products: [
+ .library(name: "TreeSitterSnapshot", targets: ["TreeSitterSnapshot"]),
+ ],
+ dependencies: [
+ .package(name: "SwiftTreeSitter", url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.9.0"),
+ ],
+ targets: [
+ .target(
+ name: "TreeSitterSnapshot",
+ dependencies: [],
+ path: ".",
+ sources: sources,
+ resources: [
+ .copy("queries")
+ ],
+ publicHeadersPath: "bindings/swift",
+ cSettings: [.headerSearchPath("src")]
+ ),
+ .testTarget(
+ name: "TreeSitterSnapshotTests",
+ dependencies: [
+ "SwiftTreeSitter",
+ "TreeSitterSnapshot",
+ ],
+ path: "bindings/swift/TreeSitterSnapshotTests"
+ )
+ ],
+ cLanguageStandard: .c11
+)
diff --git a/editors/binding.gyp b/editors/binding.gyp
new file mode 100644
index 0000000..876acdc
--- /dev/null
+++ b/editors/binding.gyp
@@ -0,0 +1,35 @@
+{
+ "targets": [
+ {
+ "target_name": "tree_sitter_snapshot_binding",
+ "dependencies": [
+ "
+
+typedef struct TSLanguage TSLanguage;
+
+extern "C" TSLanguage *tree_sitter_snapshot();
+
+// "tree-sitter", "language" hashed with BLAKE2
+const napi_type_tag LANGUAGE_TYPE_TAG = {
+ 0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
+};
+
+Napi::Object Init(Napi::Env env, Napi::Object exports) {
+ auto language = Napi::External::New(env, tree_sitter_snapshot());
+ language.TypeTag(&LANGUAGE_TYPE_TAG);
+ exports["language"] = language;
+ return exports;
+}
+
+NODE_API_MODULE(tree_sitter_snapshot_binding, Init)
diff --git a/editors/bindings/node/binding_test.js b/editors/bindings/node/binding_test.js
new file mode 100644
index 0000000..55becac
--- /dev/null
+++ b/editors/bindings/node/binding_test.js
@@ -0,0 +1,9 @@
+const assert = require("node:assert");
+const { test } = require("node:test");
+
+const Parser = require("tree-sitter");
+
+test("can load grammar", () => {
+ const parser = new Parser();
+ assert.doesNotThrow(() => parser.setLanguage(require(".")));
+});
diff --git a/editors/bindings/node/index.d.ts b/editors/bindings/node/index.d.ts
new file mode 100644
index 0000000..528e060
--- /dev/null
+++ b/editors/bindings/node/index.d.ts
@@ -0,0 +1,27 @@
+type BaseNode = {
+ type: string;
+ named: boolean;
+};
+
+type ChildNode = {
+ multiple: boolean;
+ required: boolean;
+ types: BaseNode[];
+};
+
+type NodeInfo =
+ | (BaseNode & {
+ subtypes: BaseNode[];
+ })
+ | (BaseNode & {
+ fields: { [name: string]: ChildNode };
+ children: ChildNode[];
+ });
+
+type Language = {
+ language: unknown;
+ nodeTypeInfo: NodeInfo[];
+};
+
+declare const language: Language;
+export = language;
diff --git a/editors/bindings/node/index.js b/editors/bindings/node/index.js
new file mode 100644
index 0000000..2726bab
--- /dev/null
+++ b/editors/bindings/node/index.js
@@ -0,0 +1,11 @@
+const root = require("path").join(__dirname, "..", "..");
+
+module.exports =
+ typeof process.versions.bun === "string"
+ // Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
+ ? require(`../../prebuilds/${process.platform}-${process.arch}/tree-sitter-snapshot.node`)
+ : require("node-gyp-build")(root);
+
+try {
+ module.exports.nodeTypeInfo = require("../../src/node-types.json");
+} catch (_) {}
diff --git a/editors/bindings/python/tests/test_binding.py b/editors/bindings/python/tests/test_binding.py
new file mode 100644
index 0000000..286ee27
--- /dev/null
+++ b/editors/bindings/python/tests/test_binding.py
@@ -0,0 +1,12 @@
+from unittest import TestCase
+
+from tree_sitter import Language, Parser
+import tree_sitter_snapshot
+
+
+class TestLanguage(TestCase):
+ def test_can_load_grammar(self):
+ try:
+ Parser(Language(tree_sitter_snapshot.language()))
+ except Exception:
+ self.fail("Error loading Snapshot grammar")
diff --git a/editors/bindings/python/tree_sitter_snapshot/__init__.py b/editors/bindings/python/tree_sitter_snapshot/__init__.py
new file mode 100644
index 0000000..4389aae
--- /dev/null
+++ b/editors/bindings/python/tree_sitter_snapshot/__init__.py
@@ -0,0 +1,42 @@
+"""Snapshot test files generated by Shutter"""
+
+from importlib.resources import files as _files
+
+from ._binding import language
+
+
+def _get_query(name, file):
+ query = _files(f"{__package__}.queries") / file
+ globals()[name] = query.read_text()
+ return globals()[name]
+
+
+def __getattr__(name):
+ # NOTE: uncomment these to include any queries that this grammar contains:
+
+ # if name == "HIGHLIGHTS_QUERY":
+ # return _get_query("HIGHLIGHTS_QUERY", "highlights.scm")
+ # if name == "INJECTIONS_QUERY":
+ # return _get_query("INJECTIONS_QUERY", "injections.scm")
+ # if name == "LOCALS_QUERY":
+ # return _get_query("LOCALS_QUERY", "locals.scm")
+ # if name == "TAGS_QUERY":
+ # return _get_query("TAGS_QUERY", "tags.scm")
+
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+__all__ = [
+ "language",
+ # "HIGHLIGHTS_QUERY",
+ # "INJECTIONS_QUERY",
+ # "LOCALS_QUERY",
+ # "TAGS_QUERY",
+]
+
+
+def __dir__():
+ return sorted(__all__ + [
+ "__all__", "__builtins__", "__cached__", "__doc__", "__file__",
+ "__loader__", "__name__", "__package__", "__path__", "__spec__",
+ ])
diff --git a/editors/bindings/python/tree_sitter_snapshot/__init__.pyi b/editors/bindings/python/tree_sitter_snapshot/__init__.pyi
new file mode 100644
index 0000000..abf6633
--- /dev/null
+++ b/editors/bindings/python/tree_sitter_snapshot/__init__.pyi
@@ -0,0 +1,10 @@
+from typing import Final
+
+# NOTE: uncomment these to include any queries that this grammar contains:
+
+# HIGHLIGHTS_QUERY: Final[str]
+# INJECTIONS_QUERY: Final[str]
+# LOCALS_QUERY: Final[str]
+# TAGS_QUERY: Final[str]
+
+def language() -> object: ...
diff --git a/editors/bindings/python/tree_sitter_snapshot/binding.c b/editors/bindings/python/tree_sitter_snapshot/binding.c
new file mode 100644
index 0000000..3c0020b
--- /dev/null
+++ b/editors/bindings/python/tree_sitter_snapshot/binding.c
@@ -0,0 +1,35 @@
+#include
+
+typedef struct TSLanguage TSLanguage;
+
+TSLanguage *tree_sitter_snapshot(void);
+
+static PyObject* _binding_language(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) {
+ return PyCapsule_New(tree_sitter_snapshot(), "tree_sitter.Language", NULL);
+}
+
+static struct PyModuleDef_Slot slots[] = {
+#ifdef Py_GIL_DISABLED
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
+#endif
+ {0, NULL}
+};
+
+static PyMethodDef methods[] = {
+ {"language", _binding_language, METH_NOARGS,
+ "Get the tree-sitter language for this grammar."},
+ {NULL, NULL, 0, NULL}
+};
+
+static struct PyModuleDef module = {
+ .m_base = PyModuleDef_HEAD_INIT,
+ .m_name = "_binding",
+ .m_doc = NULL,
+ .m_size = 0,
+ .m_methods = methods,
+ .m_slots = slots,
+};
+
+PyMODINIT_FUNC PyInit__binding(void) {
+ return PyModuleDef_Init(&module);
+}
diff --git a/editors/bindings/python/tree_sitter_snapshot/py.typed b/editors/bindings/python/tree_sitter_snapshot/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/editors/bindings/rust/build.rs b/editors/bindings/rust/build.rs
new file mode 100644
index 0000000..fff3a6b
--- /dev/null
+++ b/editors/bindings/rust/build.rs
@@ -0,0 +1,21 @@
+fn main() {
+ let src_dir = std::path::Path::new("src");
+
+ let mut c_config = cc::Build::new();
+ c_config.std("c11").include(src_dir);
+
+ #[cfg(target_env = "msvc")]
+ c_config.flag("-utf-8");
+
+ let parser_path = src_dir.join("parser.c");
+ c_config.file(&parser_path);
+ println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
+
+ let scanner_path = src_dir.join("scanner.c");
+ if scanner_path.exists() {
+ c_config.file(&scanner_path);
+ println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
+ }
+
+ c_config.compile("tree-sitter-snapshot");
+}
diff --git a/editors/bindings/rust/lib.rs b/editors/bindings/rust/lib.rs
new file mode 100644
index 0000000..5385648
--- /dev/null
+++ b/editors/bindings/rust/lib.rs
@@ -0,0 +1,51 @@
+//! This crate provides Snapshot language support for the [tree-sitter] parsing library.
+//!
+//! Typically, you will use the [`LANGUAGE`] constant to add this language to a
+//! tree-sitter [`Parser`], and then use the parser to parse some code:
+//!
+//! ```
+//! let code = r#"
+//! "#;
+//! let mut parser = tree_sitter::Parser::new();
+//! let language = tree_sitter_snapshot::LANGUAGE;
+//! parser
+//! .set_language(&language.into())
+//! .expect("Error loading Snapshot parser");
+//! let tree = parser.parse(code, None).unwrap();
+//! assert!(!tree.root_node().has_error());
+//! ```
+//!
+//! [`Parser`]: https://docs.rs/tree-sitter/0.25.10/tree_sitter/struct.Parser.html
+//! [tree-sitter]: https://tree-sitter.github.io/
+
+use tree_sitter_language::LanguageFn;
+
+extern "C" {
+ fn tree_sitter_snapshot() -> *const ();
+}
+
+/// The tree-sitter [`LanguageFn`] for this grammar.
+pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_snapshot) };
+
+/// The content of the [`node-types.json`] file for this grammar.
+///
+/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types
+pub const NODE_TYPES: &str = include_str!("../../src/node-types.json");
+
+// NOTE: uncomment these to include any queries that this grammar contains:
+
+// pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
+// pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
+// pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
+// pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn test_can_load_grammar() {
+ let mut parser = tree_sitter::Parser::new();
+ parser
+ .set_language(&super::LANGUAGE.into())
+ .expect("Error loading Snapshot parser");
+ }
+}
diff --git a/editors/bindings/swift/TreeSitterSnapshot/snapshot.h b/editors/bindings/swift/TreeSitterSnapshot/snapshot.h
new file mode 100644
index 0000000..e93efa3
--- /dev/null
+++ b/editors/bindings/swift/TreeSitterSnapshot/snapshot.h
@@ -0,0 +1,16 @@
+#ifndef TREE_SITTER_SNAPSHOT_H_
+#define TREE_SITTER_SNAPSHOT_H_
+
+typedef struct TSLanguage TSLanguage;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+const TSLanguage *tree_sitter_snapshot(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_SNAPSHOT_H_
diff --git a/editors/bindings/swift/TreeSitterSnapshotTests/TreeSitterSnapshotTests.swift b/editors/bindings/swift/TreeSitterSnapshotTests/TreeSitterSnapshotTests.swift
new file mode 100644
index 0000000..c3fa83b
--- /dev/null
+++ b/editors/bindings/swift/TreeSitterSnapshotTests/TreeSitterSnapshotTests.swift
@@ -0,0 +1,12 @@
+import XCTest
+import SwiftTreeSitter
+import TreeSitterSnapshot
+
+final class TreeSitterSnapshotTests: XCTestCase {
+ func testCanLoadGrammar() throws {
+ let parser = Parser()
+ let language = Language(language: tree_sitter_snapshot())
+ XCTAssertNoThrow(try parser.setLanguage(language),
+ "Error loading Snapshot grammar")
+ }
+}
diff --git a/editors/go.mod b/editors/go.mod
new file mode 100644
index 0000000..dd905b2
--- /dev/null
+++ b/editors/go.mod
@@ -0,0 +1,5 @@
+module github.com/ptdewey/shutter
+
+go 1.22
+
+require github.com/tree-sitter/go-tree-sitter v0.24.0
diff --git a/editors/grammar.js b/editors/grammar.js
new file mode 100644
index 0000000..1de8b67
--- /dev/null
+++ b/editors/grammar.js
@@ -0,0 +1,17 @@
+/**
+ * @file Snapshot test files generated by Shutter
+ * @author ptdewey
+ * @license MIT
+ */
+
+///
+// @ts-check
+
+module.exports = grammar({
+ name: "snapshot",
+
+ rules: {
+ // TODO: add the actual grammar rules
+ source_file: $ => "hello"
+ }
+});
diff --git a/editors/package.json b/editors/package.json
new file mode 100644
index 0000000..0b803e8
--- /dev/null
+++ b/editors/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "tree-sitter-snapshot",
+ "version": "0.1.0",
+ "description": "Snapshot test files generated by Shutter",
+ "repository": "https://github.com/ptdewey/shutter",
+ "license": "MIT",
+ "author": {
+ "name": "ptdewey",
+ "url": "https://github.com/ptdewey"
+ },
+ "main": "bindings/node",
+ "types": "bindings/node",
+ "keywords": [
+ "incremental",
+ "parsing",
+ "tree-sitter",
+ "snapshot"
+ ],
+ "files": [
+ "grammar.js",
+ "tree-sitter.json",
+ "binding.gyp",
+ "prebuilds/**",
+ "bindings/node/*",
+ "queries/*",
+ "src/**",
+ "*.wasm"
+ ],
+ "dependencies": {
+ "node-addon-api": "^8.5.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "devDependencies": {
+ "prebuildify": "^6.0.1",
+ "tree-sitter": "^0.22.4",
+ "tree-sitter-cli": "^0.25.10"
+ },
+ "peerDependencies": {
+ "tree-sitter": "^0.22.4"
+ },
+ "peerDependenciesMeta": {
+ "tree-sitter": {
+ "optional": true
+ }
+ },
+ "scripts": {
+ "install": "node-gyp-build",
+ "prestart": "tree-sitter build --wasm",
+ "start": "tree-sitter playground",
+ "test": "node --test bindings/node/*_test.js"
+ }
+}
diff --git a/editors/pyproject.toml b/editors/pyproject.toml
new file mode 100644
index 0000000..9bc1e3b
--- /dev/null
+++ b/editors/pyproject.toml
@@ -0,0 +1,29 @@
+[build-system]
+requires = ["setuptools>=62.4.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "tree-sitter-snapshot"
+description = "Snapshot test files generated by Shutter"
+version = "0.1.0"
+keywords = ["incremental", "parsing", "tree-sitter", "snapshot"]
+classifiers = [
+ "Intended Audience :: Developers",
+ "Topic :: Software Development :: Compilers",
+ "Topic :: Text Processing :: Linguistic",
+ "Typing :: Typed",
+]
+authors = [{ name = "ptdewey" }]
+requires-python = ">=3.10"
+license.text = "MIT"
+readme = "README.md"
+
+[project.urls]
+Homepage = "https://github.com/ptdewey/shutter"
+
+[project.optional-dependencies]
+core = ["tree-sitter~=0.24"]
+
+[tool.cibuildwheel]
+build = "cp310-*"
+build-frontend = "build"
diff --git a/editors/setup.py b/editors/setup.py
new file mode 100644
index 0000000..9e4e2b4
--- /dev/null
+++ b/editors/setup.py
@@ -0,0 +1,77 @@
+from os import path
+from sysconfig import get_config_var
+
+from setuptools import Extension, find_packages, setup
+from setuptools.command.build import build
+from setuptools.command.build_ext import build_ext
+from setuptools.command.egg_info import egg_info
+from wheel.bdist_wheel import bdist_wheel
+
+
+class Build(build):
+ def run(self):
+ if path.isdir("queries"):
+ dest = path.join(self.build_lib, "tree_sitter_snapshot", "queries")
+ self.copy_tree("queries", dest)
+ super().run()
+
+
+class BuildExt(build_ext):
+ def build_extension(self, ext: Extension):
+ if self.compiler.compiler_type != "msvc":
+ ext.extra_compile_args = ["-std=c11", "-fvisibility=hidden"]
+ else:
+ ext.extra_compile_args = ["/std:c11", "/utf-8"]
+ if path.exists("src/scanner.c"):
+ ext.sources.append("src/scanner.c")
+ if ext.py_limited_api:
+ ext.define_macros.append(("Py_LIMITED_API", "0x030A0000"))
+ super().build_extension(ext)
+
+
+class BdistWheel(bdist_wheel):
+ def get_tag(self):
+ python, abi, platform = super().get_tag()
+ if python.startswith("cp"):
+ python, abi = "cp310", "abi3"
+ return python, abi, platform
+
+
+class EggInfo(egg_info):
+ def find_sources(self):
+ super().find_sources()
+ self.filelist.recursive_include("queries", "*.scm")
+ self.filelist.include("src/tree_sitter/*.h")
+
+
+setup(
+ packages=find_packages("bindings/python"),
+ package_dir={"": "bindings/python"},
+ package_data={
+ "tree_sitter_snapshot": ["*.pyi", "py.typed"],
+ "tree_sitter_snapshot.queries": ["*.scm"],
+ },
+ ext_package="tree_sitter_snapshot",
+ ext_modules=[
+ Extension(
+ name="_binding",
+ sources=[
+ "bindings/python/tree_sitter_snapshot/binding.c",
+ "src/parser.c",
+ ],
+ define_macros=[
+ ("PY_SSIZE_T_CLEAN", None),
+ ("TREE_SITTER_HIDE_SYMBOLS", None),
+ ],
+ include_dirs=["src"],
+ py_limited_api=not get_config_var("Py_GIL_DISABLED"),
+ )
+ ],
+ cmdclass={
+ "build": Build,
+ "build_ext": BuildExt,
+ "bdist_wheel": BdistWheel,
+ "egg_info": EggInfo,
+ },
+ zip_safe=False
+)
diff --git a/editors/tree-sitter.json b/editors/tree-sitter.json
new file mode 100644
index 0000000..7ddeee7
--- /dev/null
+++ b/editors/tree-sitter.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json",
+ "grammars": [
+ {
+ "name": "snapshot",
+ "camelcase": "Snapshot",
+ "title": "Snapshot",
+ "scope": "source.snapshot",
+ "file-types": [
+ "snapshot"
+ ],
+ "injection-regex": "^snapshot$",
+ "class-name": "TreeSitterSnapshot"
+ }
+ ],
+ "metadata": {
+ "version": "0.1.0",
+ "license": "MIT",
+ "description": "Snapshot test files generated by Shutter",
+ "authors": [
+ {
+ "name": "ptdewey",
+ "url": "https://github.com/ptdewey"
+ }
+ ],
+ "links": {
+ "repository": "https://github.com/ptdewey/shutter"
+ }
+ },
+ "bindings": {
+ "c": true,
+ "go": true,
+ "node": true,
+ "python": true,
+ "rust": true,
+ "swift": true,
+ "zig": false
+ }
+}
\ No newline at end of file
--
2.51.2