Compare commits

..

6 Commits

Author SHA1 Message Date
Jan-Bulthuis
c1057be18e chore: clippy 2026-07-19 15:26:53 +02:00
Jan-Bulthuis
06173555f6 fix: limit loading of grammars 2026-07-19 15:25:44 +02:00
Jan-Bulthuis
308dcf2911 feat: load language grammars 2026-07-19 15:20:38 +02:00
Jan-Bulthuis
d5ecc05345 feat: setup for tree-sitter syntax highlighting 2026-07-19 15:20:38 +02:00
Jan-Bulthuis
853efc406d chore: remove directory 2026-07-19 14:01:42 +02:00
Jan-Bulthuis
035acb614f feat: add helix language configuration 2026-07-19 14:01:01 +02:00
7 changed files with 465 additions and 10 deletions

171
Cargo.lock generated
View File

@ -32,6 +32,12 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "1.0.0"
@ -100,6 +106,15 @@ dependencies = [
"object",
]
[[package]]
name = "arc-swap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [
"rustversion",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@ -585,6 +600,12 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "displaydoc"
version = "0.2.6"
@ -733,6 +754,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
@ -842,8 +869,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e"
dependencies = [
"bytemuck",
"foldhash",
"hashbrown",
"foldhash 0.2.0",
"hashbrown 0.17.1",
"log",
"peniko",
"png",
@ -872,13 +899,24 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash",
"foldhash 0.2.0",
]
[[package]]
@ -1341,7 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.17.1",
"rayon",
"serde",
"serde_core",
@ -1385,18 +1423,32 @@ dependencies = [
"clap",
"flate2",
"itertools",
"ropey",
"tar",
"time",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"tree-house",
"tree-house-bindings",
"typst",
"typst-html",
"typst-kit",
"typst-library",
"typst-utils",
"ureq",
]
[[package]]
name = "kstring"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e"
dependencies = [
"static_assertions",
]
[[package]]
name = "kurbo"
version = "0.13.1"
@ -1421,6 +1473,16 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.16"
@ -1822,6 +1884,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
@ -1974,6 +2046,19 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-cursor"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0497c781d2f982ae8284d2932aee6a877e58a4541daa5e8fadc18cc75c23a61d"
dependencies = [
"log",
"memchr",
"regex-automata",
"regex-syntax",
"ropey",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
@ -2000,6 +2085,16 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e"
[[package]]
name = "ropey"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
dependencies = [
"smallvec",
"str_indices",
]
[[package]]
name = "roxmltree"
version = "0.21.1"
@ -2073,6 +2168,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "rustybuzz"
version = "0.20.1"
@ -2308,6 +2409,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str_indices"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
[[package]]
name = "strict-num"
version = "0.1.1"
@ -2715,6 +2828,38 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "tree-house"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333476442882205ab249a8ced263ae53db3d0797c727cc3424836a6fb221d7b1"
dependencies = [
"arc-swap",
"hashbrown 0.15.5",
"kstring",
"once_cell",
"pretty_assertions",
"regex",
"regex-cursor",
"ropey",
"slab",
"tree-house-bindings",
"unicode-width",
]
[[package]]
name = "tree-house-bindings"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5d0eed0db98578618e598b8f6f413f7c172bade1f12f8242d71c38dd5deb0f"
dependencies = [
"cc",
"libloading",
"regex-cursor",
"ropey",
"thiserror",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
@ -3155,6 +3300,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
@ -3282,7 +3433,7 @@ dependencies = [
"bytemuck",
"fearless_simd",
"guillotiere",
"hashbrown",
"hashbrown 0.17.1",
"log",
"peniko",
"png",
@ -3299,7 +3450,7 @@ dependencies = [
"bytemuck",
"fearless_simd",
"guillotiere",
"hashbrown",
"hashbrown 0.17.1",
"log",
"peniko",
"png",
@ -3315,7 +3466,7 @@ checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0"
dependencies = [
"bytemuck",
"glifo",
"hashbrown",
"hashbrown 0.17.1",
"png",
"vello_common 0.0.8",
]
@ -3546,6 +3697,12 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.3"

View File

@ -23,3 +23,8 @@ typst-kit = "0.15.1"
typst = "0.15.1"
ureq = "3.3.0"
clap = { version = "4.6.2", features = ["derive"] }
typst-library = "0.15.1"
typst-utils = "0.15.1"
tree-house = "0.4.0"
tree-house-bindings = { version = "0.3.2", features = ["ropey"] }
ropey = "1.6.1"

59
flake.lock generated
View File

@ -18,7 +18,42 @@
"type": "github"
}
},
"helix": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1784219418,
"narHash": "sha256-yfNBfV6HWAQU/xZXwrFoedsSYsDmCDIlTHRxAZI6teY=",
"owner": "helix-editor",
"repo": "helix",
"rev": "f6f3eb1fe4a73933466f30175843059a4381a985",
"type": "github"
},
"original": {
"owner": "helix-editor",
"repo": "helix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1784120854,
"narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=",
@ -37,7 +72,29 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"helix": "helix",
"nixpkgs": "nixpkgs_2"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"helix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775358767,
"narHash": "sha256-f2eC+WIfhjevCPQILuV08i/kmKZzYZpUvkom/33VxCA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "20fd44bc663daa53a2575e01293e24e681d62244",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {

View File

@ -2,6 +2,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
helix.url = "github:helix-editor/helix";
};
outputs =
@ -9,6 +10,7 @@
self,
nixpkgs,
flake-utils,
helix,
...
}:
flake-utils.lib.eachDefaultSystem (
@ -19,6 +21,31 @@
};
in
rec {
packages.syntax =
let
grammars = pkgs.callPackage "${helix}/grammars.nix" { };
in
pkgs.stdenv.mkDerivation {
name = "syntax";
phases = [
"buildPhase"
"installPhase"
];
buildPhase = ''
cp -r ${helix}/runtime/queries/* ./
chmod u+w ./* -R
for so in ${grammars}/*.so; do
name=$(basename "$so" .so)
if [ -d "./$name" ]; then
cp "$so" "./$name/$name.so"
fi
done
'';
installPhase = ''
mkdir -p $out
cp -r ./* $out/
'';
};
packages.karnaugh = pkgs.rustPlatform.buildRustPackage (final: {
pname = "karnaugh";
version = "0.1.5";

View File

@ -25,6 +25,7 @@ use tracing_subscriber::FmtSubscriber;
use crate::typst::TypstContext;
mod syntax;
mod typst;
#[derive(Clone)]
@ -59,6 +60,15 @@ struct AppConfig {
#[arg(short = 'd', long, value_name = "DIR", default_value = "/tmp/karnaugh")]
packages_root: PathBuf,
/// The directory where the syntax files are stored.
#[arg(short = 'g', long, value_name = "DIR", default_value = None)]
syntax_root: Option<PathBuf>,
/// Grammars to load, separated by commas.
/// Useful to avoid the memory required to load all languages.
#[arg(short = 'L', long = "lang", value_name = "NAME", value_delimiter = ',')]
languages: Vec<String>,
/// The log level to use
#[arg(short = 'l', long, value_name = "LEVEL", default_value_t = Level::INFO)]
log_level: Level,
@ -94,6 +104,8 @@ async fn main() {
.with_max_level(state.config.log_level)
.init();
syntax::init(&state.config);
// TODO: Replace with config option
let mut favicon = state.config.get_full_assets_path();
favicon.push(PathBuf::from("favicon.svg"));
@ -117,7 +129,7 @@ async fn main() {
info!("Serving Karnaugh on socket {}", socket);
let listener = tokio::net::TcpListener::bind(socket)
.await
.expect(&format!("Failed to bind to socket {}", socket));
.unwrap_or_else(|_| panic!("Failed to bind to socket {}", socket));
axum::serve(listener, app)
.await
.expect("Failed to serve site");

189
src/syntax.rs Normal file
View File

@ -0,0 +1,189 @@
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
error::Error,
fs,
path::Path,
sync::OnceLock,
};
use tracing::{info, warn};
use tree_house::{
InjectionLanguageMarker, Language, LanguageConfig, LanguageLoader, highlighter::Highlight,
read_query,
};
use tree_house_bindings::Grammar;
use typst::foundations::{Func, NativeFunc, func};
use crate::AppConfig;
static REGISTRY: OnceLock<LanguageRegistry> = OnceLock::new();
/// Class names used in highlighted code.
const HIGHLIGHT_NAMES: &[&str] = &[
"attribute",
"comment",
"comment.block",
"comment.line",
"constant",
"constant.builtin",
"constructor",
"function",
"function.builtin",
"function.macro",
"function.method",
"keyword",
"keyword.control",
"keyword.operator",
"label",
"namespace",
"number",
"operator",
"property",
"punctuation",
"punctuation.bracket",
"punctuation.delimiter",
"special",
"string",
"string.special",
"tag",
"type",
"type.builtin",
"variable",
"variable.builtin",
"variable.parameter",
];
/// Get the longest matching highlight name for a given scope.
fn highlight_for_scope(scope: &str) -> Option<Highlight> {
let mut candidate = scope;
loop {
if let Some(i) = HIGHLIGHT_NAMES.iter().position(|&n| n == candidate) {
return Some(Highlight::new(i as u32));
}
match candidate.rfind('.') {
Some(dot) => candidate = &candidate[..dot],
None => return None,
}
}
}
struct LanguageRegistry {
by_name: HashMap<String, Language>,
configs: Vec<LanguageConfig>,
}
impl LanguageRegistry {
/// Loads grammars from `syntax_root`. When `languages` is non-empty, only
/// grammars with those names are loaded; otherwise every grammar found is.
fn new(syntax_root: &Path, languages: &[String]) -> Self {
let filter: Option<HashSet<&str>> =
(!languages.is_empty()).then(|| languages.iter().map(String::as_str).collect());
let mut by_name = HashMap::new();
let mut configs = Vec::new();
for entry in fs::read_dir(syntax_root).into_iter().flatten().flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if filter.as_ref().is_some_and(|f| !f.contains(name.as_str())) {
continue;
}
let so_path = entry.path().join(format!("{name}.so"));
if !so_path.exists() {
continue;
}
match load_language(&name, &entry.path(), syntax_root) {
Ok(config) => {
by_name.insert(name, Language::new(configs.len() as u32));
configs.push(config);
}
Err(e) => warn!("Failed to load grammar {name}: {e}"),
}
}
// Warn about explicitly requested grammars that were not found.
for lang in languages {
if !by_name.contains_key(lang) {
warn!("Requested grammar {lang} was not found in the syntax root");
}
}
info!("Loaded {} syntax highlighting grammars", configs.len());
Self { by_name, configs }
}
}
fn load_language(
name: &str,
lang_dir: &Path,
syntax_root: &Path,
) -> Result<LanguageConfig, Box<dyn Error>> {
let grammar = unsafe { Grammar::new(name, &lang_dir.join(format!("{name}.so")))? };
let read = |kind: &str| {
read_query(name, |lang| {
fs::read_to_string(syntax_root.join(lang).join(format!("{kind}.scm")))
.unwrap_or_default()
})
};
let config = LanguageConfig::new(
grammar,
&read("highlights"),
&read("injections"),
&read("locals"),
)?;
config.configure(highlight_for_scope);
Ok(config)
}
impl LanguageLoader for LanguageRegistry {
fn language_for_marker(&self, marker: InjectionLanguageMarker<'_>) -> Option<Language> {
match marker {
InjectionLanguageMarker::Name(name) => self.by_name.get(name).copied(),
InjectionLanguageMarker::Match(text) => {
let name: Cow<str> = text.into();
self.by_name.get(name.as_ref()).copied()
}
_ => None,
}
}
fn get_config(&self, lang: Language) -> Option<&LanguageConfig> {
self.configs.get(lang.idx())
}
}
pub fn init(config: &AppConfig) {
match &config.syntax_root {
Some(syntax_root) => {
info!("Loading syntax files from {}", syntax_root.display());
REGISTRY.get_or_init(|| LanguageRegistry::new(syntax_root, &config.languages));
}
None => {
warn!(
"No syntax root has been configured, improved tree-sitter highlighting will not be possible."
)
}
}
}
pub fn highlight_func() -> Func {
highlight::func()
}
#[func]
fn highlight(
text: String,
#[named]
#[default(false)]
block: bool,
#[named]
#[default(None)]
lang: Option<String>,
) -> String {
let rev = text.chars().rev();
rev.collect()
}

View File

@ -5,7 +5,7 @@ use tracing::info;
use typst::{
Library, LibraryExt, World,
diag::{FileError, FileResult, PackageError, PackageResult, SourceDiagnostic},
foundations::{Bytes, Datetime, Duration},
foundations::{Bytes, Datetime, Dict, Duration, Value},
syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot, package::PackageSpec},
text::{Font, FontBook},
utils::LazyHash,
@ -23,7 +23,15 @@ pub struct TypstContext {
impl TypstContext {
pub fn new(config: AppConfig) -> Self {
let mut inputs = Dict::new();
if config.syntax_root.is_some() {
inputs.insert(
"highlight".into(),
Value::Func(crate::syntax::highlight_func()),
);
}
let library = Library::builder()
.with_inputs(inputs)
.with_features([typst::Feature::Html].into_iter().collect())
.build();
let library = LazyHash::new(library);