1#![cfg_attr(not(any(test, doctest)), doc = include_str!("../README.md"))]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4#[cfg(unix)]
5use std::fmt::Write as _;
6#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
7use std::ops::Range;
8use std::{
9 collections::HashMap,
10 env, fs,
11 hash::{Hash as _, Hasher as _},
12 io::{BufRead, BufReader},
13 marker::PhantomData,
14 mem,
15 path::{Path, PathBuf},
16 process::Command,
17 sync::Mutex,
18 time::{Duration, Instant, SystemTime},
19};
20
21use etcetera::BaseStrategy as _;
22use libloading::{Library, Symbol};
23use log::{error, info, warn};
24use once_cell::sync::OnceCell;
25use regex::{Regex, RegexBuilder};
26use semver::Version;
27use serde::{Deserialize, Deserializer, Serialize};
28use thiserror::Error;
29use tree_sitter::Language;
30#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
31use tree_sitter::QueryError;
32#[cfg(feature = "tree-sitter-highlight")]
33use tree_sitter::QueryErrorKind;
34#[cfg(feature = "wasm")]
35use tree_sitter::WasmError;
36#[cfg(feature = "tree-sitter-highlight")]
37use tree_sitter_highlight::HighlightConfiguration;
38#[cfg(feature = "tree-sitter-tags")]
39use tree_sitter_tags::{Error as TagsError, TagsConfiguration};
40
41static WASM_TOOL_LOCK: Mutex<()> = Mutex::new(());
42
43const WASI_SDK_VERSION: &str = include_str!("../wasi-sdk-version").trim_ascii();
44const BINARYEN_VERSION: &str = include_str!("../binaryen-version").trim_ascii();
45
46#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
47const ARCH_OS: Result<&str, LoaderError> = Ok("arm64-macos");
48#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
49const ARCH_OS: Result<&str, LoaderError> = Ok("x86_64-macos");
50#[cfg(all(
51 target_os = "macos",
52 not(any(target_arch = "aarch64", target_arch = "x86_64"))
53))]
54const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
55
56#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
57const ARCH_OS: Result<&str, LoaderError> = Ok("arm64-windows");
58#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
59const ARCH_OS: Result<&str, LoaderError> = Ok("x86_64-windows");
60#[cfg(all(
61 target_os = "windows",
62 not(any(target_arch = "aarch64", target_arch = "x86_64"))
63))]
64const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
65
66#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
67const ARCH_OS: Result<&str, LoaderError> = Ok("arm64-linux");
68#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
69const ARCH_OS: Result<&str, LoaderError> = Ok("x86_64-linux");
70#[cfg(all(
71 target_os = "linux",
72 not(any(target_arch = "aarch64", target_arch = "x86_64"))
73))]
74const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
75
76#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
77const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
78
79pub type LoaderResult<T> = Result<T, LoaderError>;
80
81#[derive(Debug, Error)]
82pub enum LoaderError {
83 #[error(transparent)]
84 Compiler(CompilerError),
85 #[error("Parser compilation failed.\nStdout: {0}\nStderr: {1}")]
86 Compilation(String, String),
87 #[error(
88 "Lock file '{0}' appears stale\nIf there isn't another concurrent tree-sitter instance, remove it"
89 )]
90 LockFileTimeout(PathBuf),
91 #[error("Failed to execute curl for {0} -- {1}")]
92 Curl(String, std::io::Error),
93 #[error("Failed to load language in current directory:\n{0}")]
94 CurrentDirectoryLoad(Box<Self>),
95 #[error("External file path {0} is outside of parser directory {1}")]
96 ExternalFile(String, String),
97 #[error("Failed to extract archive {0} to {1}")]
98 Extraction(String, String),
99 #[error("Failed to load language for file name {0}:\n{1}")]
100 FileNameLoad(String, Box<Self>),
101 #[error("Failed to parse the language name from grammar.json at {0}")]
102 GrammarJSON(String),
103 #[error(transparent)]
104 HomeDir(#[from] etcetera::HomeDirError),
105 #[error(transparent)]
106 IO(IoError),
107 #[error(transparent)]
108 Library(LibraryError),
109 #[error("Failed to compare binary and source timestamps:\n{0}")]
110 ModifiedTime(Box<Self>),
111 #[error("No language found")]
112 NoLanguage,
113 #[error(transparent)]
114 Query(LoaderQueryError),
115 #[error("Failed to load language for scope '{0}':\n{1}")]
116 ScopeLoad(String, Box<Self>),
117 #[error(transparent)]
118 Serialization(#[from] serde_json::Error),
119 #[error(transparent)]
120 Symbol(SymbolError),
121 #[error(transparent)]
122 Tags(#[from] TagsError),
123 #[error("Failed to execute tar for {0} -- {1}")]
124 Tar(String, std::io::Error),
125 #[error("Unknown scope '{0}'")]
126 UnknownScope(String),
127 #[error("Failed to download {tool} from {url}")]
128 WasmToolDownload { tool: &'static str, url: String },
129 #[error(transparent)]
130 WasmTool(#[from] WasmToolError),
131 #[error("Unsupported platform for wasi-sdk")]
132 WasiSDKPlatform,
133 #[cfg(feature = "wasm")]
134 #[error(transparent)]
135 Wasm(#[from] WasmError),
136 #[error("Failed to run wasi-sdk clang -- {0}")]
137 WasmCompiler(std::io::Error),
138 #[error("Failed to run wasm-opt -- {0}")]
139 WasmOptimizer(std::io::Error),
140 #[error("wasi-sdk clang command failed: {0}")]
141 WasmCompilation(String),
142 #[error("wasm-opt command failed: {0}")]
143 WasmOptimization(String),
144}
145
146#[derive(Debug, Error)]
147pub struct CompilerError {
148 pub error: std::io::Error,
149 pub command: Box<Command>,
150}
151
152impl std::fmt::Display for CompilerError {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
155 f,
156 "Failed to execute the C compiler with the following command:\n{:?}\nError: {}",
157 *self.command, self.error
158 )?;
159 Ok(())
160 }
161}
162
163#[derive(Debug, Error)]
164pub struct IoError {
165 pub error: std::io::Error,
166 pub path: Option<PathBuf>,
167}
168
169impl IoError {
170 fn new(error: std::io::Error, path: Option<&Path>) -> Self {
171 Self {
172 error,
173 path: path.map(Path::to_path_buf),
174 }
175 }
176}
177
178impl std::fmt::Display for IoError {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 write!(f, "{}", self.error)?;
181 if let Some(ref path) = self.path {
182 write!(f, " ({})", path.display())?;
183 }
184 Ok(())
185 }
186}
187
188#[derive(Debug, Error)]
189pub struct LibraryError {
190 pub error: libloading::Error,
191 pub path: String,
192}
193
194impl std::fmt::Display for LibraryError {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(
197 f,
198 "Error opening dynamic library {} -- {}",
199 self.path, self.error
200 )?;
201 Ok(())
202 }
203}
204
205#[derive(Debug, Error)]
206pub struct LoaderQueryError {
207 pub error: QueryError,
208 pub file: Option<String>,
209}
210
211impl std::fmt::Display for LoaderQueryError {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 if let Some(ref path) = self.file {
214 writeln!(f, "Error in query file {path}:")?;
215 }
216 write!(f, "{}", self.error)?;
217 Ok(())
218 }
219}
220
221#[derive(Debug, Error)]
222pub struct SymbolError {
223 pub error: libloading::Error,
224 pub symbol_name: String,
225 pub path: String,
226}
227
228impl std::fmt::Display for SymbolError {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 write!(
231 f,
232 "Failed to load symbol {} from {} -- {}",
233 self.symbol_name, self.path, self.error
234 )?;
235 Ok(())
236 }
237}
238
239#[derive(Debug, Error)]
240pub struct WasmToolError {
241 pub exe: &'static str,
242 pub toolchain: &'static str,
243 pub tool_dir: String,
244 pub possible_executables: Vec<&'static str>,
245 pub download: bool,
246}
247
248impl std::fmt::Display for WasmToolError {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 if self.download {
251 write!(
252 f,
253 "Failed to find {} executable in downloaded {} at '{}'.",
254 self.exe, self.toolchain, self.tool_dir
255 )?;
256 } else {
257 let toolchain_upper = self.toolchain.replace('-', "_").to_ascii_uppercase();
258 write!(
259 f,
260 "TREE_SITTER_{toolchain_upper}_PATH is set to '{}', but no clang executable found in 'bin/' directory.",
261 self.tool_dir
262 )?;
263 }
264
265 let possible_exes = self.possible_executables.join(", ");
266 write!(f, " Looked for: {possible_exes}.")?;
267
268 Ok(())
269 }
270}
271
272pub const DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME: &str = "highlights.scm";
273
274pub const DEFAULT_INJECTIONS_QUERY_FILE_NAME: &str = "injections.scm";
275
276pub const DEFAULT_LOCALS_QUERY_FILE_NAME: &str = "locals.scm";
277
278pub const DEFAULT_TAGS_QUERY_FILE_NAME: &str = "tags.scm";
279
280#[derive(Default, Deserialize, Serialize)]
281pub struct Config {
282 #[serde(default)]
283 #[serde(
284 rename = "parser-directories",
285 deserialize_with = "deserialize_parser_directories"
286 )]
287 pub parser_directories: Vec<PathBuf>,
288}
289
290#[derive(Serialize, Deserialize, Clone, Default)]
291#[serde(untagged)]
292pub enum PathsJSON {
293 #[default]
294 Empty,
295 Single(PathBuf),
296 Multiple(Vec<PathBuf>),
297}
298
299impl PathsJSON {
300 fn into_vec(self) -> Option<Vec<PathBuf>> {
301 match self {
302 Self::Empty => None,
303 Self::Single(s) => Some(vec![s]),
304 Self::Multiple(s) => Some(s),
305 }
306 }
307
308 const fn is_empty(&self) -> bool {
309 matches!(self, Self::Empty)
310 }
311
312 #[must_use]
314 pub fn to_variable_value<'a>(&'a self, default: &'a PathBuf) -> &'a str {
315 match self {
316 Self::Empty => Some(default),
317 Self::Single(path_buf) => Some(path_buf),
318 Self::Multiple(paths) => paths.first(),
319 }
320 .map_or("", |path| path.as_os_str().to_str().unwrap_or(""))
321 }
322}
323
324#[derive(Serialize, Deserialize, Clone)]
325#[serde(untagged)]
326pub enum PackageJSONAuthor {
327 String(String),
328 Object {
329 name: String,
330 email: Option<String>,
331 url: Option<String>,
332 },
333}
334
335#[derive(Serialize, Deserialize, Clone)]
336#[serde(untagged)]
337pub enum PackageJSONRepository {
338 String(String),
339 Object { url: String },
340}
341
342#[derive(Serialize, Deserialize)]
343pub struct PackageJSON {
344 pub name: String,
345 pub version: Version,
346 pub description: Option<String>,
347 pub author: Option<PackageJSONAuthor>,
348 pub maintainers: Option<Vec<PackageJSONAuthor>>,
349 pub license: Option<String>,
350 pub repository: Option<PackageJSONRepository>,
351 #[serde(default)]
352 #[serde(rename = "tree-sitter", skip_serializing_if = "Option::is_none")]
353 pub tree_sitter: Option<Vec<LanguageConfigurationJSON>>,
354}
355
356fn default_path() -> PathBuf {
357 PathBuf::from(".")
358}
359
360#[derive(Serialize, Deserialize, Clone)]
361#[serde(rename_all = "kebab-case")]
362pub struct LanguageConfigurationJSON {
363 #[serde(default = "default_path")]
364 pub path: PathBuf,
365 pub scope: Option<String>,
366 pub file_types: Option<Vec<String>>,
367 pub content_regex: Option<String>,
368 pub first_line_regex: Option<String>,
369 pub injection_regex: Option<String>,
370 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
371 pub highlights: PathsJSON,
372 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
373 pub injections: PathsJSON,
374 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
375 pub locals: PathsJSON,
376 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
377 pub tags: PathsJSON,
378 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
379 pub external_files: PathsJSON,
380}
381
382#[derive(Serialize, Deserialize)]
383#[serde(rename_all = "kebab-case")]
384pub struct TreeSitterJSON {
385 #[serde(rename = "$schema")]
386 pub schema: Option<String>,
387 pub grammars: Vec<Grammar>,
388 pub metadata: Metadata,
389 #[serde(default)]
390 pub bindings: Bindings,
391}
392
393impl TreeSitterJSON {
394 pub fn from_file(path: &Path) -> LoaderResult<Self> {
395 let path = path.join("tree-sitter.json");
396 Ok(serde_json::from_str(&fs::read_to_string(&path).map_err(
397 |e| LoaderError::IO(IoError::new(e, Some(path.as_path()))),
398 )?)?)
399 }
400
401 #[must_use]
402 pub const fn has_multiple_language_configs(&self) -> bool {
403 self.grammars.len() > 1
404 }
405}
406
407#[derive(Serialize, Deserialize)]
408#[serde(rename_all = "kebab-case")]
409pub struct Grammar {
410 pub name: String,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub camelcase: Option<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub title: Option<String>,
415 pub scope: String,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub path: Option<PathBuf>,
418 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
419 pub external_files: PathsJSON,
420 pub file_types: Option<Vec<String>>,
421 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
422 pub highlights: PathsJSON,
423 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
424 pub injections: PathsJSON,
425 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
426 pub locals: PathsJSON,
427 #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
428 pub tags: PathsJSON,
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub injection_regex: Option<String>,
431 #[serde(skip_serializing_if = "Option::is_none")]
432 pub first_line_regex: Option<String>,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub content_regex: Option<String>,
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub class_name: Option<String>,
437}
438
439#[derive(Serialize, Deserialize)]
440pub struct Metadata {
441 pub version: Version,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub license: Option<String>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub description: Option<String>,
446 #[serde(skip_serializing_if = "Option::is_none")]
447 pub authors: Option<Vec<Author>>,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 pub links: Option<Links>,
450 #[serde(skip)]
451 pub namespace: Option<String>,
452}
453
454#[derive(Serialize, Deserialize)]
455pub struct Author {
456 pub name: String,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub email: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub url: Option<String>,
461}
462
463#[derive(Serialize, Deserialize)]
464pub struct Links {
465 pub repository: String,
466 #[serde(skip_serializing_if = "Option::is_none")]
467 pub funding: Option<String>,
468}
469
470#[derive(Serialize, Deserialize, Clone)]
471#[serde(default)]
472pub struct Bindings {
473 pub c: bool,
474 pub go: bool,
475 pub java: bool,
476 #[serde(skip)]
477 pub kotlin: bool,
478 pub node: bool,
479 pub python: bool,
480 pub rust: bool,
481 pub swift: bool,
482 pub zig: bool,
483}
484
485impl Bindings {
486 #[must_use]
488 pub const fn languages(&self) -> [(&'static str, bool); 8] {
489 [
490 ("c", true),
491 ("go", true),
492 ("java", false),
493 ("node", true),
496 ("python", true),
497 ("rust", true),
498 ("swift", true),
499 ("zig", false),
500 ]
501 }
502
503 pub fn with_enabled_languages<'a, I>(languages: I) -> Result<Self, &'a str>
505 where
506 I: Iterator<Item = &'a str>,
507 {
508 let mut out = Self {
509 c: false,
510 go: false,
511 java: false,
512 kotlin: false,
513 node: false,
514 python: false,
515 rust: false,
516 swift: false,
517 zig: false,
518 };
519
520 for v in languages {
521 match v {
522 "c" => out.c = true,
523 "go" => out.go = true,
524 "java" => out.java = true,
525 "node" => out.node = true,
528 "python" => out.python = true,
529 "rust" => out.rust = true,
530 "swift" => out.swift = true,
531 "zig" => out.zig = true,
532 unsupported => return Err(unsupported),
533 }
534 }
535
536 Ok(out)
537 }
538}
539
540impl Default for Bindings {
541 fn default() -> Self {
542 Self {
543 c: true,
544 go: true,
545 java: false,
546 kotlin: false,
547 node: true,
548 python: true,
549 rust: true,
550 swift: true,
551 zig: false,
552 }
553 }
554}
555
556fn deserialize_parser_directories<'de, D>(deserializer: D) -> Result<Vec<PathBuf>, D::Error>
560where
561 D: Deserializer<'de>,
562{
563 let paths = Vec::<PathBuf>::deserialize(deserializer)?;
564 let Ok(home) = etcetera::home_dir() else {
565 return Ok(paths);
566 };
567 let standardized = paths
568 .into_iter()
569 .map(|path| standardize_path(path, &home))
570 .collect();
571 Ok(standardized)
572}
573
574fn standardize_path(path: PathBuf, home: &Path) -> PathBuf {
575 if let Ok(p) = path.strip_prefix("~") {
576 return home.join(p);
577 }
578 if let Ok(p) = path.strip_prefix("$HOME") {
579 return home.join(p);
580 }
581 path
582}
583
584fn display_build_cmd(cmd: &Command) {
585 let mut env_vars = String::new();
586 for (key, val) in cmd.get_envs() {
587 env_vars.push_str(&key.to_string_lossy());
588 if let Some(v) = val {
589 env_vars.push('=');
590 env_vars.push_str(&v.to_string_lossy());
591 }
592 env_vars.push('\n');
593 }
594 if !env_vars.is_empty() {
595 env_vars.pop(); }
597 info!(
598 "[{}] {} {}\n",
599 cmd.get_current_dir()
600 .unwrap_or_else(|| Path::new(""))
601 .display(),
602 cmd.get_program().to_string_lossy(),
603 cmd.get_args()
604 .map(|s| s.to_string_lossy())
605 .collect::<Vec<_>>()
606 .join(" "),
607 );
608 for (key, val) in cmd.get_envs() {
609 let mut env_str = key.to_string_lossy().to_string();
610 if let Some(v) = val {
611 env_str.push('=');
612 env_str.push_str(&v.to_string_lossy());
613 }
614 info!("{env_str}");
615 }
616}
617
618impl Config {
619 #[must_use]
620 pub fn initial() -> Self {
621 let home_dir = etcetera::home_dir().expect("Cannot determine home directory");
622 Self {
623 parser_directories: vec![
624 home_dir.join("github"),
625 home_dir.join("src"),
626 home_dir.join("source"),
627 home_dir.join("projects"),
628 home_dir.join("dev"),
629 home_dir.join("git"),
630 ],
631 }
632 }
633}
634
635const BUILD_TARGET: &str = env!("BUILD_TARGET");
636
637pub struct LanguageConfiguration<'a> {
638 pub scope: Option<String>,
639 pub content_regex: Option<Regex>,
640 pub first_line_regex: Option<Regex>,
641 pub injection_regex: Option<Regex>,
642 pub file_types: Vec<String>,
643 pub root_path: PathBuf,
644 pub highlights_filenames: Option<Vec<PathBuf>>,
645 pub injections_filenames: Option<Vec<PathBuf>>,
646 pub locals_filenames: Option<Vec<PathBuf>>,
647 pub tags_filenames: Option<Vec<PathBuf>>,
648 pub language_name: String,
649 language_id: usize,
650 #[cfg(feature = "tree-sitter-highlight")]
651 highlight_config: OnceCell<Option<HighlightConfiguration>>,
652 #[cfg(feature = "tree-sitter-tags")]
653 tags_config: OnceCell<Option<TagsConfiguration>>,
654 #[cfg(feature = "tree-sitter-highlight")]
655 highlight_names: &'a Mutex<Vec<String>>,
656 #[cfg(feature = "tree-sitter-highlight")]
657 use_all_highlight_names: bool,
658 _phantom: PhantomData<&'a ()>,
659}
660
661pub struct Loader {
662 pub parser_lib_path: PathBuf,
663 languages_by_id: Vec<(PathBuf, OnceCell<Language>, Option<Vec<PathBuf>>)>,
664 language_configurations: Vec<LanguageConfiguration<'static>>,
665 language_configuration_ids_by_file_type: HashMap<String, Vec<usize>>,
666 language_configuration_in_current_path: Option<usize>,
667 language_configuration_ids_by_first_line_regex: HashMap<String, Vec<usize>>,
668 #[cfg(feature = "tree-sitter-highlight")]
669 highlight_names: Box<Mutex<Vec<String>>>,
670 #[cfg(feature = "tree-sitter-highlight")]
671 use_all_highlight_names: bool,
672 debug_build: bool,
673 sanitize_build: bool,
674 force_rebuild: bool,
675 verbose: bool,
676
677 #[cfg(feature = "wasm")]
678 wasm_store: Mutex<Option<tree_sitter::WasmStore>>,
679}
680
681pub struct CompileConfig<'a> {
682 pub src_path: &'a Path,
683 pub header_paths: Vec<&'a Path>,
684 pub parser_path: PathBuf,
685 pub scanner_path: Option<PathBuf>,
686 pub external_files: Option<&'a [PathBuf]>,
687 pub output_path: Option<PathBuf>,
688 pub flags: &'a [&'a str],
689 pub sanitize: bool,
690 pub name: String,
691}
692
693impl<'a> CompileConfig<'a> {
694 #[must_use]
695 pub fn new(
696 src_path: &'a Path,
697 externals: Option<&'a [PathBuf]>,
698 output_path: Option<PathBuf>,
699 ) -> Self {
700 Self {
701 src_path,
702 header_paths: vec![src_path],
703 parser_path: src_path.join("parser.c"),
704 scanner_path: None,
705 external_files: externals,
706 output_path,
707 flags: &[],
708 sanitize: false,
709 name: String::new(),
710 }
711 }
712}
713
714fn temp_path(path: &Path) -> PathBuf {
718 let filename = path
719 .file_name()
720 .expect("output_path must have a filename")
721 .to_string_lossy();
722 path.with_file_name(format!(
723 ".{filename}.{}.{:?}",
724 std::process::id(),
725 std::thread::current().id()
726 ))
727}
728
729struct LockFile {
733 path: PathBuf,
734}
735
736impl LockFile {
737 fn create(path: &Path) -> LoaderResult<Option<Self>> {
742 match fs::OpenOptions::new()
743 .create_new(true)
744 .write(true)
745 .open(path)
746 {
747 Ok(_) => Ok(Some(Self {
748 path: path.to_path_buf(),
749 })),
750 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(None),
751 Err(e) => Err(LoaderError::IO(IoError::new(e, Some(path)))),
752 }
753 }
754
755 fn wait_for_removal(path: &Path, timeout: Duration) -> LoaderResult<()> {
758 let mut sleep_ms = 100;
759 let deadline = Instant::now() + timeout;
760 while path.exists() {
761 if Instant::now() > deadline {
762 return Err(LoaderError::LockFileTimeout(path.to_path_buf()));
763 }
764 std::thread::sleep(Duration::from_millis(sleep_ms));
765 sleep_ms = (sleep_ms * 2).min(1000);
766 }
767
768 Ok(())
769 }
770}
771
772impl Drop for LockFile {
773 fn drop(&mut self) {
774 match fs::remove_file(&self.path) {
775 Ok(()) => {}
776 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
777 Err(e) => warn!("Failed to remove lock file '{}': {e}.", self.path.display()),
778 }
779 }
780}
781
782impl Loader {
783 pub fn new() -> LoaderResult<Self> {
784 let parser_lib_path = if let Ok(path) = env::var("TREE_SITTER_LIBDIR") {
785 PathBuf::from(path)
786 } else {
787 if cfg!(target_os = "macos") {
788 let legacy_apple_path = etcetera::base_strategy::Apple::new()?
789 .cache_dir() .join("tree-sitter");
791 if legacy_apple_path.exists() && legacy_apple_path.is_dir() {
792 std::fs::remove_dir_all(&legacy_apple_path).map_err(|e| {
793 LoaderError::IO(IoError::new(e, Some(legacy_apple_path.as_path())))
794 })?;
795 }
796 }
797
798 etcetera::choose_base_strategy()?
799 .cache_dir()
800 .join("tree-sitter")
801 .join("lib")
802 };
803 Ok(Self::with_parser_lib_path(parser_lib_path))
804 }
805
806 #[must_use]
807 pub fn with_parser_lib_path(parser_lib_path: PathBuf) -> Self {
808 Self {
809 parser_lib_path,
810 languages_by_id: Vec::new(),
811 language_configurations: Vec::new(),
812 language_configuration_ids_by_file_type: HashMap::new(),
813 language_configuration_in_current_path: None,
814 language_configuration_ids_by_first_line_regex: HashMap::new(),
815 #[cfg(feature = "tree-sitter-highlight")]
816 highlight_names: Box::new(Mutex::new(Vec::new())),
817 #[cfg(feature = "tree-sitter-highlight")]
818 use_all_highlight_names: true,
819 debug_build: false,
820 sanitize_build: false,
821 force_rebuild: false,
822 verbose: false,
823
824 #[cfg(feature = "wasm")]
825 wasm_store: Mutex::default(),
826 }
827 }
828
829 #[cfg(feature = "tree-sitter-highlight")]
830 #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))]
831 pub fn configure_highlights(&mut self, names: &[String]) {
832 self.use_all_highlight_names = false;
833 let mut highlights = self.highlight_names.lock().unwrap();
834 highlights.clear();
835 highlights.extend(names.iter().cloned());
836 }
837
838 #[must_use]
839 #[cfg(feature = "tree-sitter-highlight")]
840 #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))]
841 pub fn highlight_names(&self) -> Vec<String> {
842 self.highlight_names.lock().unwrap().clone()
843 }
844
845 pub fn find_all_languages(&mut self, config: &Config) -> LoaderResult<()> {
846 if config.parser_directories.is_empty() {
847 warn!(concat!(
848 "You have not configured any parser directories!\n",
849 "Please run `tree-sitter init-config` and edit the resulting\n",
850 "configuration file to indicate where we should look for\n",
851 "language grammars.\n"
852 ));
853 }
854 for parser_container_dir in &config.parser_directories {
855 if let Ok(entries) = fs::read_dir(parser_container_dir) {
856 for entry in entries {
857 let entry = entry.map_err(|e| LoaderError::IO(IoError::new(e, None)))?;
858 if let Some(parser_dir_name) = entry.file_name().to_str()
859 && parser_dir_name.starts_with("tree-sitter-")
860 {
861 self.find_language_configurations_at_path(
862 &parser_container_dir.join(parser_dir_name),
863 false,
864 )
865 .ok();
866 }
867 }
868 }
869 }
870 Ok(())
871 }
872
873 pub fn languages_at_path(&mut self, path: &Path) -> LoaderResult<Vec<(Language, String)>> {
874 if let Ok(configurations) = self.find_language_configurations_at_path(path, true) {
875 let mut language_ids = configurations
876 .iter()
877 .map(|c| (c.language_id, c.language_name.clone()))
878 .collect::<Vec<_>>();
879 language_ids.sort_unstable();
880 language_ids.dedup();
881 language_ids
882 .into_iter()
883 .map(|(id, name)| Ok((self.language_for_id(id)?, name)))
884 .collect::<LoaderResult<Vec<_>>>()
885 } else {
886 Ok(Vec::new())
887 }
888 }
889
890 #[must_use]
891 pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration<'static>, &Path)> {
892 self.language_configurations
893 .iter()
894 .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref()))
895 .collect()
896 }
897
898 pub fn language_configuration_for_scope(
899 &self,
900 scope: &str,
901 ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
902 for configuration in &self.language_configurations {
903 if configuration.scope.as_ref().is_some_and(|s| s == scope) {
904 let language = self.language_for_id(configuration.language_id)?;
905 return Ok(Some((language, configuration)));
906 }
907 }
908 Ok(None)
909 }
910
911 pub fn language_configuration_for_first_line_regex(
912 &self,
913 path: &Path,
914 ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
915 self.language_configuration_ids_by_first_line_regex
916 .iter()
917 .try_fold(None, |_, (regex, ids)| {
918 if let Some(regex) = Self::regex(Some(regex)) {
919 let file = fs::File::open(path)
920 .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?;
921 let reader = BufReader::new(file);
922 let first_line = reader
923 .lines()
924 .next()
925 .transpose()
926 .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?;
927 if let Some(first_line) = first_line
928 && regex.is_match(&first_line)
929 && !ids.is_empty()
930 {
931 let configuration = &self.language_configurations[ids[0]];
932 let language = self.language_for_id(configuration.language_id)?;
933 return Ok(Some((language, configuration)));
934 }
935 }
936
937 Ok(None)
938 })
939 }
940
941 pub fn language_configuration_for_file_name(
942 &self,
943 path: &Path,
944 ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
945 let configuration_ids = path
948 .file_name()
949 .and_then(|n| n.to_str())
950 .and_then(|file_name| self.language_configuration_ids_by_file_type.get(file_name))
951 .or_else(|| {
952 let mut path = path.to_owned();
953 let mut extensions = Vec::with_capacity(2);
954 while let Some(extension) = path.extension() {
955 extensions.push(extension.to_str()?.to_string());
956 path = PathBuf::from(path.file_stem()?.to_os_string());
957 }
958 extensions.reverse();
959 (0..extensions.len())
962 .map(|i| extensions[i..].join("."))
963 .find_map(|key| self.language_configuration_ids_by_file_type.get(&key))
964 });
965
966 if let Some(configuration_ids) = configuration_ids
967 && !configuration_ids.is_empty()
968 {
969 let configuration = if configuration_ids.len() == 1 {
970 &self.language_configurations[configuration_ids[0]]
971 }
972 else {
975 let file_contents =
976 fs::read(path).map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?;
977 let file_contents = String::from_utf8_lossy(&file_contents);
978 let mut best_score = -2isize;
979 let mut best_configuration_id = None;
980 for configuration_id in configuration_ids {
981 let config = &self.language_configurations[*configuration_id];
982
983 let score;
986 if let Some(content_regex) = &config.content_regex {
987 if let Some(mat) = content_regex.find(&file_contents) {
988 score = (mat.end() - mat.start()) as isize;
989 }
990 else {
995 score = -1;
996 }
997 } else {
998 score = 0;
999 }
1000 if score > best_score {
1001 best_configuration_id = Some(*configuration_id);
1002 best_score = score;
1003 }
1004 }
1005
1006 &self.language_configurations[best_configuration_id.unwrap()]
1007 };
1008
1009 let language = self.language_for_id(configuration.language_id)?;
1010 return Ok(Some((language, configuration)));
1011 }
1012
1013 Ok(None)
1014 }
1015
1016 pub fn language_configuration_for_injection_string(
1017 &self,
1018 string: &str,
1019 ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
1020 let mut best_match_length = 0;
1021 let mut best_match_position = None;
1022 for (i, configuration) in self.language_configurations.iter().enumerate() {
1023 if let Some(injection_regex) = &configuration.injection_regex
1024 && let Some(mat) = injection_regex.find(string)
1025 {
1026 let length = mat.end() - mat.start();
1027 if length > best_match_length {
1028 best_match_position = Some(i);
1029 best_match_length = length;
1030 }
1031 }
1032 }
1033
1034 if let Some(i) = best_match_position {
1035 let configuration = &self.language_configurations[i];
1036 let language = self.language_for_id(configuration.language_id)?;
1037 Ok(Some((language, configuration)))
1038 } else {
1039 Ok(None)
1040 }
1041 }
1042
1043 pub fn language_for_configuration(
1044 &self,
1045 configuration: &LanguageConfiguration,
1046 ) -> LoaderResult<Language> {
1047 self.language_for_id(configuration.language_id)
1048 }
1049
1050 fn language_for_id(&self, id: usize) -> LoaderResult<Language> {
1051 let (path, language, externals) = &self.languages_by_id[id];
1052 language
1053 .get_or_try_init(|| {
1054 let src_path = path.join("src");
1055 self.load_language_at_path(CompileConfig::new(
1056 &src_path,
1057 externals.as_deref(),
1058 None,
1059 ))
1060 })
1061 .cloned()
1062 }
1063
1064 pub fn compile_parser_at_path(
1065 &self,
1066 grammar_path: &Path,
1067 output_path: PathBuf,
1068 flags: &[&str],
1069 ) -> LoaderResult<()> {
1070 let src_path = grammar_path.join("src");
1071 let mut config = CompileConfig::new(&src_path, None, Some(output_path));
1072 config.flags = flags;
1073 self.load_language_at_path(config).map(|_| ())
1074 }
1075
1076 pub fn load_language_at_path(&self, mut config: CompileConfig) -> LoaderResult<Language> {
1077 let grammar_path = config.src_path.join("grammar.json");
1078 config.name = Self::grammar_json_name(&grammar_path)?;
1079 self.load_language_at_path_with_name(config)
1080 }
1081
1082 pub fn load_language_at_path_with_name(
1083 &self,
1084 mut config: CompileConfig,
1085 ) -> LoaderResult<Language> {
1086 let mut lib_name = config.name.clone();
1087 let language_fn_name = format!("tree_sitter_{}", config.name.replace('-', "_"));
1088 if self.debug_build {
1089 lib_name.push_str(".debug._");
1090 }
1091
1092 if self.sanitize_build {
1093 lib_name.push_str(".sanitize._");
1094 config.sanitize = true;
1095 }
1096
1097 if config.output_path.is_none() {
1098 fs::create_dir_all(&self.parser_lib_path).map_err(|e| {
1099 LoaderError::IO(IoError::new(e, Some(self.parser_lib_path.as_path())))
1100 })?;
1101 }
1102
1103 let mut recompile = self.force_rebuild || config.output_path.is_some(); let output_path = config.output_path.unwrap_or_else(|| {
1106 let mut path = self.parser_lib_path.join(lib_name);
1107 path.set_extension(env::consts::DLL_EXTENSION);
1108 #[cfg(feature = "wasm")]
1109 if self.wasm_store.lock().unwrap().is_some() {
1110 path.set_extension("wasm");
1111 }
1112 path
1113 });
1114 config.output_path = Some(output_path.clone());
1115
1116 let parser_path = config.src_path.join("parser.c");
1117 config.scanner_path = self.get_scanner_path(config.src_path);
1118
1119 let mut paths_to_check = vec![parser_path];
1120
1121 if let Some(scanner_path) = config.scanner_path.as_ref() {
1122 paths_to_check.push(scanner_path.clone());
1123 }
1124
1125 paths_to_check.extend(
1126 config
1127 .external_files
1128 .unwrap_or_default()
1129 .iter()
1130 .map(|p| config.src_path.join(p)),
1131 );
1132
1133 if !recompile {
1134 recompile = needs_recompile(&output_path, &paths_to_check)?;
1135 }
1136
1137 let lock_hash = {
1141 let mut hasher = std::hash::DefaultHasher::new();
1142 output_path.hash(&mut hasher);
1143 format!("{:x}", hasher.finish())
1144 };
1145
1146 let mut lock_path = etcetera::choose_base_strategy()?.cache_dir();
1147 lock_path.push(format!(
1148 "tree-sitter{slash}lock{slash}{name}-{lock_hash}.lock",
1149 slash = std::path::MAIN_SEPARATOR,
1150 name = config.name
1151 ));
1152
1153 if recompile {
1165 let parent_path = lock_path.parent().unwrap();
1166 fs::create_dir_all(parent_path)
1167 .map_err(|e| LoaderError::IO(IoError::new(e, Some(parent_path))))?;
1168
1169 match LockFile::create(&lock_path)? {
1170 Some(_lock) => {
1171 let compile_wasm;
1173 #[cfg(feature = "wasm")]
1174 {
1175 compile_wasm = self.wasm_store.lock().unwrap().is_some();
1176 }
1177 #[cfg(not(feature = "wasm"))]
1178 {
1179 compile_wasm = false;
1180 };
1181 #[cfg(feature = "wasm")]
1182 if compile_wasm {
1183 self.compile_parser_to_wasm(
1184 &config.name,
1185 config.src_path,
1186 config
1187 .scanner_path
1188 .as_ref()
1189 .and_then(|p| p.strip_prefix(config.src_path).ok()),
1190 &output_path,
1191 )?;
1192 }
1193 if !compile_wasm {
1194 self.compile_parser_to_dylib(&config)?;
1195 if config.scanner_path.is_some() {
1196 Self::check_external_scanner(&output_path);
1197 }
1198 }
1199 }
1201 None => LockFile::wait_for_removal(&lock_path, Duration::from_secs(30))?,
1204 }
1205 }
1206
1207 #[cfg(feature = "wasm")]
1208 if let Some(wasm_store) = self.wasm_store.lock().unwrap().as_mut() {
1209 let wasm_bytes = fs::read(&output_path)
1210 .map_err(|e| LoaderError::IO(IoError::new(e, Some(output_path.as_path()))))?;
1211 return Ok(wasm_store.load_language(&config.name, &wasm_bytes)?);
1212 }
1213
1214 Self::load_language(&output_path, &language_fn_name)
1215 }
1216
1217 pub fn load_language(path: &Path, function_name: &str) -> LoaderResult<Language> {
1218 let library = unsafe { Library::new(path) }.map_err(|e| {
1219 LoaderError::Library(LibraryError {
1220 error: e,
1221 path: path.to_string_lossy().to_string(),
1222 })
1223 })?;
1224 let language = unsafe {
1225 let language_fn = library
1226 .get::<Symbol<unsafe extern "C" fn() -> Language>>(function_name.as_bytes())
1227 .map_err(|e| {
1228 LoaderError::Symbol(SymbolError {
1229 error: e,
1230 symbol_name: function_name.to_string(),
1231 path: path.to_string_lossy().to_string(),
1232 })
1233 })?;
1234 language_fn()
1235 };
1236 mem::forget(library);
1237 Ok(language)
1238 }
1239
1240 fn compile_parser_to_dylib(&self, config: &CompileConfig) -> LoaderResult<()> {
1241 let mut cc_config = cc::Build::new();
1242 cc_config
1243 .cargo_metadata(false)
1244 .cargo_warnings(false)
1245 .target(BUILD_TARGET)
1246 .host(BUILD_TARGET)
1250 .debug(self.debug_build)
1251 .file(&config.parser_path)
1252 .includes(&config.header_paths)
1253 .std("c11");
1254
1255 if let Some(scanner_path) = config.scanner_path.as_ref() {
1256 cc_config.file(scanner_path);
1257 }
1258
1259 if self.debug_build {
1260 cc_config.opt_level(0).extra_warnings(true);
1261 } else {
1262 cc_config.opt_level(2).extra_warnings(false);
1263 }
1264
1265 for flag in config.flags {
1266 cc_config.define(flag, None);
1267 }
1268
1269 let compiler = cc_config.get_compiler();
1270 let mut command = compiler.to_command();
1271
1272 let output_path = config.output_path.as_ref().unwrap();
1273
1274 let temp_output = temp_path(output_path);
1277
1278 let temp_dir = if compiler.is_like_msvc() {
1279 let out = format!("-out:{}", temp_output.to_str().unwrap());
1280 command.arg(if self.debug_build { "-LDd" } else { "-LD" });
1281 command.arg("-utf-8");
1282
1283 let temp_dir = output_path.parent().unwrap().join(format!(
1287 "tmp_{}_{:?}",
1288 std::process::id(),
1289 std::thread::current().id()
1290 ));
1291 std::fs::create_dir_all(&temp_dir).unwrap();
1292
1293 command.arg(format!("/Fo{}\\", temp_dir.display()));
1294 command.args(cc_config.get_files());
1295 command.arg("-link").arg(out);
1296 command.arg(format!("/IMPLIB:{}.lib", temp_dir.join("temp").display()));
1297
1298 Some(temp_dir)
1299 } else {
1300 command.arg("-Werror=implicit-function-declaration");
1301 if cfg!(any(target_os = "macos", target_os = "ios")) {
1302 command.arg("-dynamiclib");
1303 command.arg("-UTREE_SITTER_REUSE_ALLOCATOR");
1305 } else {
1306 command.arg("-shared");
1307 let sanitizing = compiler
1310 .args()
1311 .iter()
1312 .any(|a| a.to_str().is_some_and(|s| s.starts_with("-fsanitize=")));
1313 if !sanitizing {
1314 command.arg("-Wl,--no-undefined");
1315 }
1316 #[cfg(target_os = "openbsd")]
1317 command.arg("-lc");
1318 }
1319 command.args(cc_config.get_files());
1320 command.arg("-o").arg(&temp_output);
1321
1322 None
1323 };
1324
1325 if self.verbose {
1326 display_build_cmd(&command);
1327 }
1328
1329 let output = command.output().map_err(|e| {
1330 LoaderError::Compiler(CompilerError {
1331 error: e,
1332 command: Box::new(command),
1333 })
1334 })?;
1335
1336 if self.verbose {
1337 if !output.stdout.is_empty() {
1338 info!("stdout:{}", String::from_utf8_lossy(&output.stdout));
1339 }
1340 if !output.stderr.is_empty() {
1341 info!("stderr:{}", String::from_utf8_lossy(&output.stderr));
1342 }
1343 }
1344
1345 if let Some(temp_dir) = temp_dir {
1346 let _ = fs::remove_dir_all(temp_dir);
1347 }
1348
1349 if output.status.success() {
1350 fs::rename(&temp_output, output_path).map_err(|e| {
1351 let _ = fs::remove_file(&temp_output);
1352 LoaderError::IO(IoError::new(e, Some(output_path)))
1353 })?;
1354 Ok(())
1355 } else {
1356 let _ = fs::remove_file(&temp_output);
1357 Err(LoaderError::Compilation(
1358 String::from_utf8_lossy(&output.stdout).to_string(),
1359 String::from_utf8_lossy(&output.stderr).to_string(),
1360 ))
1361 }
1362 }
1363
1364 #[cfg(unix)]
1365 fn check_external_scanner(library_path: &Path) {
1366 let section = " T ";
1367 let old_ppc_section = if cfg!(all(target_arch = "powerpc64", target_os = "linux")) {
1370 Some(" D ")
1371 } else {
1372 None
1373 };
1374 let nm_cmd = env::var("NM").unwrap_or_else(|_| "nm".to_owned());
1375 let command = Command::new(nm_cmd)
1376 .arg("--defined-only")
1377 .arg(library_path)
1378 .output();
1379 if let Ok(output) = command
1380 && output.status.success()
1381 {
1382 let mut non_static_symbols = String::new();
1383 for line in String::from_utf8_lossy(&output.stdout).lines() {
1384 if (line.contains(section) || old_ppc_section.is_some_and(|s| line.contains(s)))
1385 && let Some(function_name) = line.split_whitespace().collect::<Vec<_>>().get(2)
1386 && !line.contains("tree_sitter_")
1387 {
1388 writeln!(&mut non_static_symbols, " `{function_name}`").unwrap();
1389 }
1390 }
1391 if !non_static_symbols.is_empty() {
1392 warn!(
1393 "Found non-static non-tree-sitter functions in the external scanner\n{non_static_symbols}\n{}",
1394 concat!(
1395 "Consider making these functions static, they can cause conflicts ",
1396 "when another tree-sitter project uses the same function name."
1397 )
1398 );
1399 }
1400 } else {
1401 warn!(
1402 "Failed to run `nm` to verify symbols in {}",
1403 library_path.display()
1404 );
1405 }
1406 }
1407
1408 #[cfg(windows)]
1409 fn check_external_scanner(_library_path: &Path) {
1410 }
1412
1413 pub fn compile_parser_to_wasm(
1414 &self,
1415 language_name: &str,
1416 src_path: &Path,
1417 scanner_filename: Option<&Path>,
1418 output_path: &Path,
1419 ) -> LoaderResult<()> {
1420 let tool_lock = WASM_TOOL_LOCK.lock().expect("Wasm tool mutex poisoned");
1421 let clang_exe = Self::ensure_wasi_sdk_exists()?;
1422 let wasm_opt_exe = Self::ensure_binaryen_exists()?;
1423 drop(tool_lock);
1424
1425 let temp_output = temp_path(output_path);
1428 let temp_output_str = temp_output.to_str().unwrap();
1429
1430 let mut compile_command = Command::new(&clang_exe);
1431 compile_command.current_dir(src_path).args([
1432 "--target=wasm32-wasip1",
1433 "-o",
1434 temp_output_str,
1435 "-fPIC",
1436 "-shared",
1437 "--no-wasm-opt",
1438 if self.debug_build { "-g" } else { "-Os" },
1439 format!("-Wl,--export=tree_sitter_{language_name}").as_str(),
1440 "-Wl,--allow-undefined",
1441 "-Wl,--no-entry",
1442 "-nostdlib",
1443 "-fno-exceptions",
1444 "-fvisibility=hidden",
1445 "-I",
1446 ".",
1447 "parser.c",
1448 ]);
1449
1450 if let Some(scanner_filename) = scanner_filename {
1451 compile_command.arg(scanner_filename);
1452 }
1453
1454 if self.verbose {
1455 display_build_cmd(&compile_command);
1456 }
1457
1458 let compile_output = compile_command
1459 .output()
1460 .map_err(LoaderError::WasmCompiler)?;
1461 if self.verbose {
1462 if !compile_output.stdout.is_empty() {
1463 info!("stdout:{}", String::from_utf8_lossy(&compile_output.stdout));
1464 }
1465 if !compile_output.stderr.is_empty() {
1466 info!("stderr:{}", String::from_utf8_lossy(&compile_output.stderr));
1467 }
1468 }
1469
1470 if !compile_output.status.success() {
1471 let _ = fs::remove_file(&temp_output);
1472 return Err(LoaderError::WasmCompilation(
1473 String::from_utf8_lossy(&compile_output.stderr).to_string(),
1474 ));
1475 }
1476
1477 let mut opt_command = Command::new(&wasm_opt_exe);
1478 opt_command
1479 .current_dir(src_path)
1480 .args([temp_output_str, "-Os", "-o", temp_output_str]);
1481
1482 if self.verbose {
1483 display_build_cmd(&opt_command);
1484 }
1485
1486 let opt_output = opt_command.output().map_err(LoaderError::WasmOptimizer)?;
1487 if self.verbose {
1488 if !opt_output.stdout.is_empty() {
1489 info!("stdout:{}", String::from_utf8_lossy(&opt_output.stdout));
1490 }
1491 if !opt_output.stderr.is_empty() {
1492 info!("stderr:{}", String::from_utf8_lossy(&opt_output.stderr));
1493 }
1494 }
1495
1496 if !opt_output.status.success() {
1497 let _ = fs::remove_file(&temp_output);
1498 return Err(LoaderError::WasmOptimization(
1499 String::from_utf8_lossy(&opt_output.stderr).to_string(),
1500 ));
1501 }
1502
1503 fs::rename(&temp_output, output_path).map_err(|e| {
1504 let _ = fs::remove_file(&temp_output);
1505 LoaderError::IO(IoError::new(e, Some(output_path)))
1506 })?;
1507
1508 Ok(())
1509 }
1510
1511 fn extract_tar_gz_with_strip(archive_path: &Path, destination: &Path) -> LoaderResult<()> {
1513 let status = Command::new("tar")
1514 .arg("-xzf")
1515 .arg(archive_path)
1516 .arg("--strip-components=1")
1517 .arg("-C")
1518 .arg(destination)
1519 .status()
1520 .map_err(|e| LoaderError::Tar(archive_path.to_string_lossy().to_string(), e))?;
1521
1522 if !status.success() {
1523 return Err(LoaderError::Extraction(
1524 archive_path.to_string_lossy().to_string(),
1525 destination.to_string_lossy().to_string(),
1526 ));
1527 }
1528
1529 Ok(())
1530 }
1531
1532 fn ensure_wasi_sdk_exists() -> LoaderResult<PathBuf> {
1537 let possible_executables = if cfg!(windows) {
1538 vec![
1539 "clang.exe",
1540 "wasm32-unknown-wasi-clang.exe",
1541 "wasm32-wasi-clang.exe",
1542 ]
1543 } else {
1544 vec!["clang", "wasm32-unknown-wasi-clang", "wasm32-wasi-clang"]
1545 };
1546
1547 if let Some(path) = Self::get_existing_tool(
1548 "clang",
1549 "wasi-sdk",
1550 WASI_SDK_VERSION,
1551 &possible_executables,
1552 "TREE_SITTER_WASI_SDK_PATH",
1553 )? {
1554 return Ok(path);
1555 }
1556
1557 let arch_os = ARCH_OS?;
1558 let sdk_filename = format!("wasi-sdk-{WASI_SDK_VERSION}-{arch_os}.tar.gz");
1559 let wasi_sdk_major_version = WASI_SDK_VERSION
1560 .trim_end_matches(char::is_numeric) .trim_end_matches('.'); let sdk_url = format!(
1563 "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-{wasi_sdk_major_version}/{sdk_filename}",
1564 );
1565 Self::download_tool(
1566 "clang",
1567 "wasi-sdk",
1568 WASI_SDK_VERSION,
1569 &sdk_filename,
1570 &sdk_url,
1571 &possible_executables,
1572 )
1573 }
1574
1575 fn ensure_binaryen_exists() -> LoaderResult<PathBuf> {
1580 let possible_executables = if cfg!(windows) {
1581 vec![
1582 "wasm-opt.exe",
1583 "wasm32-unknown-wasm-opt.exe",
1584 "wasm32-wasm-opt.exe",
1585 ]
1586 } else {
1587 vec!["wasm-opt", "wasm32-unknown-wasm-opt", "wasm32-wasm-opt"]
1588 };
1589 if let Some(path) = Self::get_existing_tool(
1590 "wasm-opt",
1591 "binaryen",
1592 BINARYEN_VERSION,
1593 &possible_executables,
1594 "TREE_SITTER_BINARYEN_PATH",
1595 )? {
1596 return Ok(path);
1597 }
1598
1599 let arch_os = ARCH_OS?.replace("arm64-linux", "aarch64-linux");
1600 let binaryen_filename = format!("binaryen-version_{BINARYEN_VERSION}-{arch_os}.tar.gz");
1601 let binaryen_url = format!(
1602 "https://github.com/WebAssembly/binaryen/releases/download/version_{BINARYEN_VERSION}/{binaryen_filename}"
1603 );
1604 Self::download_tool(
1605 "wasm-opt",
1606 "binaryen",
1607 BINARYEN_VERSION,
1608 &binaryen_filename,
1609 &binaryen_url,
1610 &possible_executables,
1611 )
1612 }
1613
1614 fn get_existing_tool(
1615 tool_name: &'static str,
1616 toolchain: &'static str,
1617 version: &str,
1618 possible_exes: &[&'static str],
1619 env_var: &str,
1620 ) -> LoaderResult<Option<PathBuf>> {
1621 if let Ok(tool_path) = std::env::var(env_var) {
1622 let tool_dir = PathBuf::from(tool_path).join("bin");
1623
1624 for exe in possible_exes {
1625 let tool_exe = tool_dir.join(exe);
1626 if tool_exe.exists() {
1627 return Ok(Some(tool_exe));
1628 }
1629 }
1630
1631 Err(LoaderError::WasmTool(WasmToolError {
1632 exe: tool_name,
1633 toolchain,
1634 tool_dir: tool_dir.to_string_lossy().to_string(),
1635 possible_executables: possible_exes.to_vec(),
1636 download: false,
1637 }))?;
1638 }
1639
1640 let cache_dir = etcetera::choose_base_strategy()?
1641 .cache_dir()
1642 .join("tree-sitter");
1643 fs::create_dir_all(&cache_dir).map_err(|error| {
1644 LoaderError::IO(IoError {
1645 error,
1646 path: Some(cache_dir.clone()),
1647 })
1648 })?;
1649
1650 let toolchain_dir = cache_dir.join(toolchain);
1651 let version_file = toolchain_dir.join(".version");
1652
1653 if toolchain_dir.exists() {
1655 let cached_version =
1656 fs::read_to_string(&version_file).unwrap_or_else(|_| "unknown".to_string());
1657 if cached_version.trim() != version {
1658 info!(
1659 "Cached {toolchain} version ({}) doesn't match expected version ({version}), re-downloading",
1660 cached_version.trim(),
1661 );
1662 fs::remove_dir_all(&toolchain_dir).ok();
1663 return Ok(None);
1664 }
1665 }
1666
1667 let tool_dir = toolchain_dir.join("bin");
1668
1669 for exe in possible_exes {
1670 let tool_exe = tool_dir.join(exe);
1671 if tool_exe.exists() {
1672 return Ok(Some(tool_exe));
1673 }
1674 }
1675
1676 Ok(None)
1677 }
1678
1679 fn download_tool(
1680 tool_name: &'static str,
1681 toolchain: &'static str,
1682 version: &str,
1683 filename: &str,
1684 url: &str,
1685 possible_exes: &[&'static str],
1686 ) -> LoaderResult<PathBuf> {
1687 let cache_dir = etcetera::choose_base_strategy()?
1688 .cache_dir()
1689 .join("tree-sitter");
1690 let tool_dir = cache_dir.join(toolchain);
1691
1692 fs::create_dir_all(&tool_dir).map_err(|error| {
1693 LoaderError::IO(IoError {
1694 error,
1695 path: Some(tool_dir.clone()),
1696 })
1697 })?;
1698
1699 info!("Downloading {tool_name} from {url}...");
1700 let temp_tar_dir = tempfile::tempdir_in(&cache_dir).map_err(|e| {
1701 LoaderError::IO(IoError {
1702 error: e,
1703 path: Some(cache_dir.clone()),
1704 })
1705 })?;
1706 let temp_tar_path = temp_tar_dir.path().join(filename);
1707
1708 let status = Command::new("curl")
1709 .arg("-f")
1710 .arg("-L")
1711 .arg("-o")
1712 .arg(&temp_tar_path)
1713 .arg(url)
1714 .status()
1715 .map_err(|e| LoaderError::Curl(url.to_string(), e))?;
1716
1717 if !status.success() {
1718 Err(LoaderError::WasmToolDownload {
1719 tool: tool_name,
1720 url: url.to_string(),
1721 })?;
1722 }
1723
1724 info!("Extracting {tool_name} to {}...", tool_dir.display());
1725 Self::extract_tar_gz_with_strip(&temp_tar_path, &tool_dir)?;
1726
1727 fs::write(tool_dir.join(".version"), version).ok();
1728
1729 for exe in possible_exes {
1730 let tool_exe = tool_dir.join("bin").join(exe);
1731 if tool_exe.exists() {
1732 return Ok(tool_exe);
1733 }
1734 }
1735
1736 Err(LoaderError::WasmTool(WasmToolError {
1737 exe: tool_name,
1738 toolchain,
1739 tool_dir: tool_dir.to_string_lossy().to_string(),
1740 possible_executables: possible_exes.to_vec(),
1741 download: true,
1742 }))?
1743 }
1744
1745 #[must_use]
1746 #[cfg(feature = "tree-sitter-highlight")]
1747 pub fn highlight_config_for_injection_string<'a>(
1748 &'a self,
1749 string: &str,
1750 ) -> Option<&'a HighlightConfiguration> {
1751 match self.language_configuration_for_injection_string(string) {
1752 Err(e) => {
1753 error!("Failed to load language for injection string '{string}': {e}");
1754 None
1755 }
1756 Ok(None) => None,
1757 Ok(Some((language, configuration))) => {
1758 match configuration.highlight_config(language, None) {
1759 Err(e) => {
1760 error!(
1761 "Failed to load highlight config for injection string '{string}': {e}"
1762 );
1763 None
1764 }
1765 Ok(None) => None,
1766 Ok(Some(config)) => Some(config),
1767 }
1768 }
1769 }
1770 }
1771
1772 #[must_use]
1773 pub fn get_language_configuration_in_current_path(
1774 &self,
1775 ) -> Option<&LanguageConfiguration<'static>> {
1776 self.language_configuration_in_current_path
1777 .map(|i| &self.language_configurations[i])
1778 }
1779
1780 pub fn find_language_configurations_at_path(
1781 &mut self,
1782 parser_path: &Path,
1783 set_current_path_config: bool,
1784 ) -> LoaderResult<&[LanguageConfiguration<'static>]> {
1785 let initial_language_configuration_count = self.language_configurations.len();
1786
1787 match TreeSitterJSON::from_file(parser_path) {
1788 Ok(config) => {
1789 let language_count = self.languages_by_id.len();
1790 for grammar in config.grammars {
1791 let language_path =
1795 parser_path.join(grammar.path.unwrap_or_else(|| PathBuf::from(".")));
1796
1797 let mut language_id = None;
1800 for (id, (path, _, _)) in
1801 self.languages_by_id.iter().enumerate().skip(language_count)
1802 {
1803 if language_path == *path {
1804 language_id = Some(id);
1805 }
1806 }
1807
1808 let language_id = if let Some(language_id) = language_id {
1810 language_id
1811 } else {
1812 self.languages_by_id.push((
1813 language_path,
1814 OnceCell::new(),
1815 grammar
1816 .external_files
1817 .clone()
1818 .into_vec()
1819 .map(|files| {
1820 files
1821 .into_iter()
1822 .map(|path| {
1823 let path = parser_path.join(path);
1824 if path.starts_with(parser_path) {
1826 Ok(path)
1827 } else {
1828 Err(LoaderError::ExternalFile(
1829 path.to_string_lossy().to_string(),
1830 parser_path.to_string_lossy().to_string(),
1831 ))
1832 }
1833 })
1834 .collect::<LoaderResult<Vec<_>>>()
1835 })
1836 .transpose()?,
1837 ));
1838 self.languages_by_id.len() - 1
1839 };
1840
1841 let configuration = LanguageConfiguration {
1842 root_path: parser_path.to_path_buf(),
1843 language_name: grammar.name,
1844 scope: Some(grammar.scope),
1845 language_id,
1846 file_types: grammar.file_types.unwrap_or_default(),
1847 content_regex: Self::regex(grammar.content_regex.as_deref()),
1848 first_line_regex: Self::regex(grammar.first_line_regex.as_deref()),
1849 injection_regex: Self::regex(grammar.injection_regex.as_deref()),
1850 injections_filenames: grammar.injections.into_vec(),
1851 locals_filenames: grammar.locals.into_vec(),
1852 tags_filenames: grammar.tags.into_vec(),
1853 highlights_filenames: grammar.highlights.into_vec(),
1854 #[cfg(feature = "tree-sitter-highlight")]
1855 highlight_config: OnceCell::new(),
1856 #[cfg(feature = "tree-sitter-tags")]
1857 tags_config: OnceCell::new(),
1858 #[cfg(feature = "tree-sitter-highlight")]
1859 highlight_names: &self.highlight_names,
1860 #[cfg(feature = "tree-sitter-highlight")]
1861 use_all_highlight_names: self.use_all_highlight_names,
1862 _phantom: PhantomData,
1863 };
1864
1865 for file_type in &configuration.file_types {
1866 self.language_configuration_ids_by_file_type
1867 .entry(file_type.clone())
1868 .or_default()
1869 .push(self.language_configurations.len());
1870 }
1871 if let Some(first_line_regex) = &configuration.first_line_regex {
1872 self.language_configuration_ids_by_first_line_regex
1873 .entry(first_line_regex.to_string())
1874 .or_default()
1875 .push(self.language_configurations.len());
1876 }
1877
1878 self.language_configurations.push(unsafe {
1879 mem::transmute::<LanguageConfiguration<'_>, LanguageConfiguration<'static>>(
1880 configuration,
1881 )
1882 });
1883
1884 if set_current_path_config
1885 && self.language_configuration_in_current_path.is_none()
1886 {
1887 self.language_configuration_in_current_path =
1888 Some(self.language_configurations.len() - 1);
1889 }
1890 }
1891 }
1892 Err(LoaderError::Serialization(e)) => {
1893 warn!(
1894 "Failed to parse {} -- {e}",
1895 parser_path.join("tree-sitter.json").display()
1896 );
1897 }
1898 _ => {}
1899 }
1900
1901 if self.language_configurations.len() == initial_language_configuration_count
1905 && parser_path.join("src").join("grammar.json").exists()
1906 {
1907 let grammar_path = parser_path.join("src").join("grammar.json");
1908 let language_name = Self::grammar_json_name(&grammar_path)?;
1909 let configuration = LanguageConfiguration {
1910 root_path: parser_path.to_owned(),
1911 language_name,
1912 language_id: self.languages_by_id.len(),
1913 file_types: Vec::new(),
1914 scope: None,
1915 content_regex: None,
1916 first_line_regex: None,
1917 injection_regex: None,
1918 injections_filenames: None,
1919 locals_filenames: None,
1920 highlights_filenames: None,
1921 tags_filenames: None,
1922 #[cfg(feature = "tree-sitter-highlight")]
1923 highlight_config: OnceCell::new(),
1924 #[cfg(feature = "tree-sitter-tags")]
1925 tags_config: OnceCell::new(),
1926 #[cfg(feature = "tree-sitter-highlight")]
1927 highlight_names: &self.highlight_names,
1928 #[cfg(feature = "tree-sitter-highlight")]
1929 use_all_highlight_names: self.use_all_highlight_names,
1930 _phantom: PhantomData,
1931 };
1932 self.language_configurations.push(unsafe {
1933 mem::transmute::<LanguageConfiguration<'_>, LanguageConfiguration<'static>>(
1934 configuration,
1935 )
1936 });
1937 self.languages_by_id
1938 .push((parser_path.to_owned(), OnceCell::new(), None));
1939 }
1940
1941 Ok(&self.language_configurations[initial_language_configuration_count..])
1942 }
1943
1944 fn regex(pattern: Option<&str>) -> Option<Regex> {
1945 pattern.and_then(|r| RegexBuilder::new(r).multi_line(true).build().ok())
1946 }
1947
1948 fn grammar_name(json_text: &str) -> Option<String> {
1950 let i = json_text.find("\"name\":")? + "\"name\":".len();
1951 let rest = json_text[i..].trim_start();
1952 let rest = rest.strip_prefix('\"')?;
1953 let end = rest.find('\"')?;
1954
1955 Some(rest[..end].to_string())
1956 }
1957
1958 fn grammar_json_name(grammar_path: &Path) -> LoaderResult<String> {
1959 let file = fs::File::open(grammar_path)
1960 .map_err(|e| LoaderError::IO(IoError::new(e, Some(grammar_path))))?;
1961
1962 let first_three_lines = BufReader::new(file)
1963 .lines()
1964 .take(3)
1965 .collect::<Result<Vec<_>, std::io::Error>>()
1966 .map_err(|_| LoaderError::GrammarJSON(grammar_path.to_string_lossy().to_string()))?
1967 .join("\n");
1968
1969 let name = Self::grammar_name(&first_three_lines)
1970 .ok_or_else(|| LoaderError::GrammarJSON(grammar_path.to_string_lossy().to_string()))?;
1971
1972 Ok(name)
1973 }
1974
1975 pub fn select_language(
1976 &mut self,
1977 path: Option<&Path>,
1978 current_dir: &Path,
1979 scope: Option<&str>,
1980 lib_info: Option<&(PathBuf, &str)>,
1982 ) -> LoaderResult<Language> {
1983 if let Some((lib_path, language_name)) = lib_info {
1984 let language_fn_name = format!("tree_sitter_{}", language_name.replace('-', "_"));
1985 Self::load_language(lib_path, &language_fn_name)
1986 } else if let Some(scope) = scope {
1987 if let Some(config) = self
1988 .language_configuration_for_scope(scope)
1989 .map_err(|e| LoaderError::ScopeLoad(scope.to_string(), Box::new(e)))?
1990 {
1991 Ok(config.0)
1992 } else {
1993 Err(LoaderError::UnknownScope(scope.to_string()))
1994 }
1995 } else if let Some((lang, _)) = if let Some(path) = path {
1996 self.language_configuration_for_file_name(path)
1997 .map_err(|e| {
1998 LoaderError::FileNameLoad(
1999 path.file_name().unwrap().to_string_lossy().to_string(),
2000 Box::new(e),
2001 )
2002 })?
2003 } else {
2004 None
2005 } {
2006 Ok(lang)
2007 } else if let Some(id) = self.language_configuration_in_current_path {
2008 Ok(self.language_for_id(self.language_configurations[id].language_id)?)
2009 } else if let Some(lang) = self
2010 .languages_at_path(current_dir)
2011 .map_err(|e| LoaderError::CurrentDirectoryLoad(Box::new(e)))?
2012 .first()
2013 .cloned()
2014 {
2015 Ok(lang.0)
2016 } else if let Some(lang) = if let Some(path) = path {
2017 self.language_configuration_for_first_line_regex(path)?
2018 } else {
2019 None
2020 } {
2021 Ok(lang.0)
2022 } else {
2023 Err(LoaderError::NoLanguage)
2024 }
2025 }
2026
2027 pub const fn debug_build(&mut self, flag: bool) {
2028 self.debug_build = flag;
2029 }
2030
2031 pub const fn sanitize_build(&mut self, flag: bool) {
2032 self.sanitize_build = flag;
2033 }
2034
2035 pub const fn force_rebuild(&mut self, rebuild: bool) {
2036 self.force_rebuild = rebuild;
2037 }
2038
2039 pub const fn verbose_build(&mut self, verbose: bool) {
2040 self.verbose = verbose;
2041 }
2042
2043 #[cfg(feature = "wasm")]
2044 #[cfg_attr(docsrs, doc(cfg(feature = "wasm")))]
2045 pub fn use_wasm(&mut self, engine: &tree_sitter::wasmtime::Engine) {
2046 *self.wasm_store.lock().unwrap() = Some(tree_sitter::WasmStore::new(engine).unwrap());
2047 }
2048
2049 #[must_use]
2050 pub fn get_scanner_path(&self, src_path: &Path) -> Option<PathBuf> {
2051 let path = src_path.join("scanner.c");
2052 path.exists().then_some(path)
2053 }
2054}
2055
2056impl LanguageConfiguration<'_> {
2057 #[cfg(feature = "tree-sitter-highlight")]
2058 pub fn highlight_config(
2059 &self,
2060 language: Language,
2061 paths: Option<&[PathBuf]>,
2062 ) -> LoaderResult<Option<&HighlightConfiguration>> {
2063 let (highlights_filenames, injections_filenames, locals_filenames) = match paths {
2064 Some(paths) => (
2065 Some(
2066 paths
2067 .iter()
2068 .filter(|p| p.ends_with(DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME))
2069 .cloned()
2070 .collect::<Vec<_>>(),
2071 ),
2072 Some(
2073 paths
2074 .iter()
2075 .filter(|p| p.ends_with(DEFAULT_TAGS_QUERY_FILE_NAME))
2076 .cloned()
2077 .collect::<Vec<_>>(),
2078 ),
2079 Some(
2080 paths
2081 .iter()
2082 .filter(|p| p.ends_with(DEFAULT_LOCALS_QUERY_FILE_NAME))
2083 .cloned()
2084 .collect::<Vec<_>>(),
2085 ),
2086 ),
2087 None => (None, None, None),
2088 };
2089 self.highlight_config
2090 .get_or_try_init(|| {
2091 let (highlights_query, highlight_ranges) = self.read_queries(
2092 if highlights_filenames.is_some() {
2093 highlights_filenames.as_deref()
2094 } else {
2095 self.highlights_filenames.as_deref()
2096 },
2097 DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME,
2098 )?;
2099 let (injections_query, injection_ranges) = self.read_queries(
2100 if injections_filenames.is_some() {
2101 injections_filenames.as_deref()
2102 } else {
2103 self.injections_filenames.as_deref()
2104 },
2105 DEFAULT_INJECTIONS_QUERY_FILE_NAME,
2106 )?;
2107 let (locals_query, locals_ranges) = self.read_queries(
2108 if locals_filenames.is_some() {
2109 locals_filenames.as_deref()
2110 } else {
2111 self.locals_filenames.as_deref()
2112 },
2113 DEFAULT_LOCALS_QUERY_FILE_NAME,
2114 )?;
2115
2116 if highlights_query.is_empty() {
2117 Ok(None)
2118 } else {
2119 let mut result = HighlightConfiguration::new(
2120 language,
2121 &self.language_name,
2122 &highlights_query,
2123 &injections_query,
2124 &locals_query,
2125 )
2126 .map_err(|error| match error.kind {
2127 QueryErrorKind::Language => {
2128 LoaderError::Query(LoaderQueryError { error, file: None })
2129 }
2130 _ => {
2131 if error.offset < injections_query.len() {
2132 Self::include_path_in_query_error(
2133 error,
2134 &injection_ranges,
2135 &injections_query,
2136 0,
2137 )
2138 } else if error.offset < injections_query.len() + locals_query.len() {
2139 Self::include_path_in_query_error(
2140 error,
2141 &locals_ranges,
2142 &locals_query,
2143 injections_query.len(),
2144 )
2145 } else {
2146 Self::include_path_in_query_error(
2147 error,
2148 &highlight_ranges,
2149 &highlights_query,
2150 injections_query.len() + locals_query.len(),
2151 )
2152 }
2153 }
2154 })?;
2155 let mut all_highlight_names = self.highlight_names.lock().unwrap();
2156 if self.use_all_highlight_names {
2157 for capture_name in result.query.capture_names() {
2158 if !all_highlight_names.iter().any(|x| x == capture_name) {
2159 all_highlight_names.push((*capture_name).to_string());
2160 }
2161 }
2162 }
2163 result.configure(all_highlight_names.as_slice());
2164 drop(all_highlight_names);
2165 Ok(Some(result))
2166 }
2167 })
2168 .map(Option::as_ref)
2169 }
2170
2171 #[cfg(feature = "tree-sitter-tags")]
2172 pub fn tags_config(&self, language: Language) -> LoaderResult<Option<&TagsConfiguration>> {
2173 self.tags_config
2174 .get_or_try_init(|| {
2175 let (tags_query, tags_ranges) = self
2176 .read_queries(self.tags_filenames.as_deref(), DEFAULT_TAGS_QUERY_FILE_NAME)?;
2177 let (locals_query, locals_ranges) = self.read_queries(
2178 self.locals_filenames.as_deref(),
2179 DEFAULT_LOCALS_QUERY_FILE_NAME,
2180 )?;
2181 if tags_query.is_empty() {
2182 Ok(None)
2183 } else {
2184 TagsConfiguration::new(language, &tags_query, &locals_query)
2185 .map(Some)
2186 .map_err(|error| {
2187 if let TagsError::Query(error) = error {
2188 if error.offset < locals_query.len() {
2189 Self::include_path_in_query_error(
2190 error,
2191 &locals_ranges,
2192 &locals_query,
2193 0,
2194 )
2195 } else {
2196 Self::include_path_in_query_error(
2197 error,
2198 &tags_ranges,
2199 &tags_query,
2200 locals_query.len(),
2201 )
2202 }
2203 } else {
2204 error.into()
2205 }
2206 })
2207 }
2208 })
2209 .map(Option::as_ref)
2210 }
2211
2212 #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
2213 fn include_path_in_query_error(
2214 mut error: QueryError,
2215 ranges: &[(PathBuf, Range<usize>)],
2216 source: &str,
2217 start_offset: usize,
2218 ) -> LoaderError {
2219 let offset_within_section = error.offset - start_offset;
2220 let (path, range) = ranges
2221 .iter()
2222 .find(|(_, range)| range.contains(&offset_within_section))
2223 .unwrap_or_else(|| ranges.last().unwrap());
2224 error.offset = offset_within_section - range.start;
2225 error.row = source[range.start..offset_within_section]
2226 .matches('\n')
2227 .count();
2228 LoaderError::Query(LoaderQueryError {
2229 error,
2230 file: Some(path.to_string_lossy().to_string()),
2231 })
2232 }
2233
2234 #[expect(
2235 clippy::type_complexity,
2236 reason = "return type pairs query text with source file ranges"
2237 )]
2238 #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
2239 fn read_queries(
2240 &self,
2241 paths: Option<&[PathBuf]>,
2242 default_path: &str,
2243 ) -> LoaderResult<(String, Vec<(PathBuf, Range<usize>)>)> {
2244 let mut query = String::new();
2245 let mut path_ranges = Vec::new();
2246 if let Some(paths) = paths {
2247 for path in paths {
2248 let abs_path = self.root_path.join(path);
2249 let prev_query_len = query.len();
2250 query += &fs::read_to_string(&abs_path)
2251 .map_err(|e| LoaderError::IO(IoError::new(e, Some(abs_path.as_path()))))?;
2252 path_ranges.push((path.clone(), prev_query_len..query.len()));
2253 }
2254 } else {
2255 if default_path == DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME
2257 || default_path == DEFAULT_TAGS_QUERY_FILE_NAME
2258 {
2259 warn!(
2260 concat!(
2261 "You should add a `{}` entry pointing to the {} path in the `tree-sitter` ",
2262 "object in the grammar's tree-sitter.json file. See more here: ",
2263 "https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting#query-paths"
2264 ),
2265 default_path.replace(".scm", ""),
2266 default_path
2267 );
2268 }
2269 let queries_path = self.root_path.join("queries");
2270 let path = queries_path.join(default_path);
2271 if path.exists() {
2272 query = fs::read_to_string(&path)
2273 .map_err(|e| LoaderError::IO(IoError::new(e, Some(path.as_path()))))?;
2274 path_ranges.push((PathBuf::from(default_path), 0..query.len()));
2275 }
2276 }
2277
2278 Ok((query, path_ranges))
2279 }
2280}
2281
2282fn needs_recompile(lib_path: &Path, paths_to_check: &[PathBuf]) -> LoaderResult<bool> {
2283 if !lib_path.exists() {
2284 return Ok(true);
2285 }
2286 let lib_mtime = mtime(lib_path).map_err(|e| LoaderError::ModifiedTime(Box::new(e)))?;
2287 for path in paths_to_check {
2288 if mtime(path).map_err(|e| LoaderError::ModifiedTime(Box::new(e)))? > lib_mtime {
2289 return Ok(true);
2290 }
2291 }
2292 Ok(false)
2293}
2294
2295fn mtime(path: &Path) -> LoaderResult<SystemTime> {
2296 fs::metadata(path)
2297 .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?
2298 .modified()
2299 .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))
2300}