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
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ curl -fsSL https://raw.githubusercontent.com/flamestro/deff/main/install.sh | ba
Installer source: https://github.com/flamestro/deff/blob/main/install.sh

The script checks for `cargo`, clones this project into a temporary directory, installs it, and removes the temporary checkout.
Bundled syntax grammars are compiled into the binary, so removing the checkout does not affect highlighting.

If you have local edits (including untracked files) and want to review them before committing, run:

Expand Down Expand Up @@ -109,16 +110,10 @@ Theme selection:

Custom syntax grammars:

- `deff` loads syntect defaults plus any extra `.sublime-syntax` files found in:
- `deff` loads syntect defaults, bundled deff grammars, plus any extra `.sublime-syntax` files found in:
- `assets/syntaxes` (current working directory)
- `.deff/syntaxes` (current working directory)
- `DEFF_SYNTAX_DIR`
- `DEFF_SYNTAX_PATHS` (path list, colon-separated on macOS/Linux)
- Example:

```bash
DEFF_SYNTAX_DIR="$HOME/.config/deff/syntaxes" deff
```
- Any `*.sublime-syntax` file added under this repo's `assets/syntaxes` is auto-bundled at build time.

Search and reviewed workflow:

Expand Down
8 changes: 4 additions & 4 deletions assets/syntaxes/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
This directory contains extra `.sublime-syntax` grammars loaded by deff at startup.
These `.sublime-syntax` grammars are bundled into the deff binary at compile time.

You can add more grammar files here to extend language coverage further.
Any `*.sublime-syntax` file added here is automatically bundled; no manual `src/syntax.rs` update is required.

You can also add more grammar files here (or in the locations below) to extend language coverage further.

deff loads custom syntaxes from these locations (if present):

- `assets/syntaxes` (relative to the current working directory)
- `.deff/syntaxes` (relative to the current working directory)
- `DEFF_SYNTAX_DIR`
- `DEFF_SYNTAX_PATHS` (OS path list, e.g. colon-separated on macOS/Linux)

Syntactic detection still uses syntect APIs first, then first-line/shebang fallback.
69 changes: 69 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use std::{
env, fs, io,
path::{Path, PathBuf},
};

fn main() {
let syntax_directory = Path::new("assets/syntaxes");
println!("cargo:rerun-if-changed={}", syntax_directory.display());

let syntax_files = collect_syntax_files(syntax_directory)
.unwrap_or_else(|error| panic!("failed to scan {}: {error}", syntax_directory.display()));

for syntax_file in &syntax_files {
println!("cargo:rerun-if-changed={}", syntax_file.display());
}

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo"));
let output_path = out_dir.join("bundled_syntaxes.rs");
write_bundled_syntaxes(&output_path, &syntax_files)
.unwrap_or_else(|error| panic!("failed to write {}: {error}", output_path.display()));
}

fn collect_syntax_files(directory: &Path) -> io::Result<Vec<PathBuf>> {
let mut files = Vec::new();
let mut pending = vec![directory.to_path_buf()];

while let Some(current_directory) = pending.pop() {
for entry in fs::read_dir(&current_directory)? {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;

if file_type.is_dir() {
pending.push(path);
continue;
}

if file_type.is_file()
&& path
.extension()
.is_some_and(|extension| extension == "sublime-syntax")
{
files.push(path);
}
}
}

files.sort();
Ok(files)
}

fn write_bundled_syntaxes(output_path: &Path, syntax_files: &[PathBuf]) -> io::Result<()> {
let mut contents = String::from("const BUNDLED_SYNTAXES: &[(&str, &str)] = &[\n");

for syntax_file in syntax_files {
let file_name = syntax_file
.file_name()
.and_then(|name| name.to_str())
.expect("syntax file name should be valid UTF-8");
let relative_path = syntax_file.to_string_lossy().replace('\\', "/");

contents.push_str(&format!(
" ({file_name:?}, include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{relative_path}\"))),\n"
));
}

contents.push_str("];\n");
fs::write(output_path, contents)
}
61 changes: 49 additions & 12 deletions src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ use std::{
};

use once_cell::sync::Lazy;
use syntect::parsing::SyntaxSet;
use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};

const DEFAULT_RELATIVE_SYNTAX_DIRS: &[&str] = &["assets/syntaxes", ".deff/syntaxes"];
const ENV_SYNTAX_DIR: &str = "DEFF_SYNTAX_DIR";
const ENV_SYNTAX_PATHS: &str = "DEFF_SYNTAX_PATHS";

include!(concat!(env!("OUT_DIR"), "/bundled_syntaxes.rs"));

static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(load_syntax_set);

Expand All @@ -18,6 +18,7 @@ pub(crate) fn syntax_set() -> &'static SyntaxSet {

fn load_syntax_set() -> SyntaxSet {
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
add_bundled_syntaxes(&mut builder);

for directory in syntax_directories() {
if let Err(error) = builder.add_from_folder(&directory, true) {
Expand All @@ -31,18 +32,24 @@ fn load_syntax_set() -> SyntaxSet {
builder.build()
}

fn syntax_directories() -> Vec<PathBuf> {
let mut candidates = Vec::new();
candidates.push(Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/syntaxes"));
candidates.extend(DEFAULT_RELATIVE_SYNTAX_DIRS.iter().map(PathBuf::from));
fn add_bundled_syntaxes(builder: &mut SyntaxSetBuilder) {
for (file_name, source) in BUNDLED_SYNTAXES {
let fallback_name = Path::new(file_name)
.file_stem()
.and_then(|stem| stem.to_str());

if let Some(value) = std::env::var_os(ENV_SYNTAX_PATHS) {
candidates.extend(std::env::split_paths(&value));
match SyntaxDefinition::load_from_str(source, true, fallback_name) {
Ok(definition) => builder.add(definition),
Err(error) => {
eprintln!("deff: failed to load bundled syntax {}: {error}", file_name);
}
}
}
}

if let Some(value) = std::env::var_os(ENV_SYNTAX_DIR) {
candidates.push(PathBuf::from(value));
}
fn syntax_directories() -> Vec<PathBuf> {
let mut candidates = Vec::new();
candidates.extend(DEFAULT_RELATIVE_SYNTAX_DIRS.iter().map(PathBuf::from));

let cwd = std::env::current_dir().ok();
let mut unique = HashSet::new();
Expand All @@ -68,3 +75,33 @@ fn syntax_directories() -> Vec<PathBuf> {

resolved
}

#[cfg(test)]
mod tests {
use std::path::Path;

use syntect::parsing::SyntaxDefinition;

use super::{BUNDLED_SYNTAXES, load_syntax_set};

#[test]
fn every_bundled_syntax_file_is_loaded() {
let syntaxes = load_syntax_set();

for (file_name, source) in BUNDLED_SYNTAXES {
let fallback_name = Path::new(file_name)
.file_stem()
.and_then(|stem| stem.to_str());
let definition = SyntaxDefinition::load_from_str(source, true, fallback_name)
.unwrap_or_else(|error| {
panic!("failed to parse bundled syntax {file_name}: {error}")
});

assert!(
syntaxes.find_syntax_by_name(&definition.name).is_some(),
"expected bundled syntax {} from {file_name}",
definition.name
);
}
}
}