diff --git a/Cargo.lock b/Cargo.lock
index d1cbc7ba..6c8c3a1d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -841,6 +841,7 @@ dependencies = [
"tree-sitter-php",
"tree-sitter-python",
"tree-sitter-rust",
+ "tree-sitter-svelte-next",
"tree-sitter-swift",
"tree-sitter-typescript",
"walkdir",
@@ -5413,6 +5414,16 @@ dependencies = [
"tree-sitter-language",
]
+[[package]]
+name = "tree-sitter-svelte-next"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f88190d0743e897c3e148a7e241aba0a8844b8afe816943851426e3f7b9753"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
[[package]]
name = "tree-sitter-swift"
version = "0.7.2"
diff --git a/Cargo.toml b/Cargo.toml
index 62cb5cf9..f506f153 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,6 +105,7 @@ async-trait = "0.1.89"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
sysinfo = "0.39.2"
indexmap = { version = "2.14.0", features = ["serde"] }
+tree-sitter-svelte-next = "0.1.1"
[dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] }
diff --git a/contributing/parsers/svelte/AUDIT_REPORT.md b/contributing/parsers/svelte/AUDIT_REPORT.md
new file mode 100644
index 00000000..6c681c03
--- /dev/null
+++ b/contributing/parsers/svelte/AUDIT_REPORT.md
@@ -0,0 +1,66 @@
+# Svelte Parser Symbol Extraction Coverage Report
+
+*Generated: 2026-05-22 19:33:09 UTC*
+
+## Summary
+- Key nodes: 5/20 (25%)
+- Symbol kinds extracted: 4
+
+> **Note:** Svelte delegates `
+
+
+
+
+ {title} (v{COMPONENT_VERSION})
+
+
+ {#snippet userRow(user: User)}
+ {greeting(user)}
+ {/snippet}
+
+
+
+ Doubled: {doubled}
+
+ {#if isMaxed}
+ Maxed out!
+ {:else}
+
+
+ {/if}
+
+
+ {#each users as user (user.id)}
+ {@render userRow(user)}
+ {/each}
+
+
+
+
diff --git a/flake.lock b/flake.lock
index fe873248..5feb4391 100644
--- a/flake.lock
+++ b/flake.lock
@@ -109,11 +109,11 @@
]
},
"locked": {
- "lastModified": 1774062094,
- "narHash": "sha256-ba3c+hS7KzEiwtZRGHagIAYdcmdY3rCSWVCyn64rx7s=",
+ "lastModified": 1779333539,
+ "narHash": "sha256-lpmN2lrBDZDPjov2cbD3bOOJsI0fkKolKXasYPCqSys=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "c807e83cc2e32adc35f51138b3bdef722c0812ab",
+ "rev": "672fa5fc5608d5cd82286a6f69aaf84a40b4fe41",
"type": "github"
},
"original": {
diff --git a/src/io/parse.rs b/src/io/parse.rs
index b059d337..3385d5aa 100644
--- a/src/io/parse.rs
+++ b/src/io/parse.rs
@@ -265,6 +265,7 @@ pub fn execute_parse(
Language::Kotlin => tree_sitter_kotlin::language(),
Language::Lua => tree_sitter_lua::LANGUAGE.into(),
Language::Swift => tree_sitter_swift::LANGUAGE.into(),
+ Language::Svelte => tree_sitter_svelte_next::LANGUAGE.into(),
};
parser
diff --git a/src/parsing/factory.rs b/src/parsing/factory.rs
index d35037a3..1ed75a02 100644
--- a/src/parsing/factory.rs
+++ b/src/parsing/factory.rs
@@ -8,8 +8,8 @@ use super::{
CppParser, GdscriptBehavior, GdscriptParser, GoBehavior, GoParser, JavaBehavior, JavaParser,
JavaScriptBehavior, JavaScriptParser, KotlinBehavior, KotlinParser, Language, LanguageBehavior,
LanguageId, LanguageParser, LuaBehavior, LuaParser, PhpBehavior, PhpParser, PythonBehavior,
- PythonParser, RustBehavior, RustParser, SwiftBehavior, SwiftParser, TypeScriptBehavior,
- TypeScriptParser, get_registry,
+ PythonParser, RustBehavior, RustParser, SvelteBehavior, SvelteParser, SwiftBehavior,
+ SwiftParser, TypeScriptBehavior, TypeScriptParser, get_registry,
};
use crate::{IndexError, IndexResult, Settings};
use std::sync::Arc;
@@ -190,6 +190,10 @@ impl ParserFactory {
let parser = SwiftParser::new().map_err(|e| IndexError::General(e.to_string()))?;
Ok(Box::new(parser))
}
+ Language::Svelte => {
+ let parser = SvelteParser::new().map_err(|e| IndexError::General(e.to_string()))?;
+ Ok(Box::new(parser))
+ }
}
}
@@ -336,6 +340,13 @@ impl ParserFactory {
behavior: Box::new(SwiftBehavior::new()),
}
}
+ Language::Svelte => {
+ let parser = SvelteParser::new().map_err(|e| IndexError::General(e.to_string()))?;
+ ParserWithBehavior {
+ parser: Box::new(parser),
+ behavior: Box::new(SvelteBehavior::new()),
+ }
+ }
};
Ok(result)
@@ -376,6 +387,7 @@ impl ParserFactory {
Language::Php,
Language::Python,
Language::Rust,
+ Language::Svelte,
Language::Swift,
Language::TypeScript,
]
diff --git a/src/parsing/language.rs b/src/parsing/language.rs
index e0d85092..35e5916e 100644
--- a/src/parsing/language.rs
+++ b/src/parsing/language.rs
@@ -23,6 +23,7 @@ pub enum Language {
Kotlin,
Lua,
Swift,
+ Svelte,
}
impl Language {
@@ -48,6 +49,7 @@ impl Language {
Language::Kotlin => super::LanguageId::new("kotlin"),
Language::Lua => super::LanguageId::new("lua"),
Language::Swift => super::LanguageId::new("swift"),
+ Language::Svelte => super::LanguageId::new("svelte"),
}
}
@@ -72,6 +74,7 @@ impl Language {
"kotlin" => Some(Language::Kotlin),
"lua" => Some(Language::Lua),
"swift" => Some(Language::Swift),
+ "svelte" => Some(Language::Svelte),
_ => None,
}
}
@@ -111,6 +114,7 @@ impl Language {
"kt" | "kts" => Some(Language::Kotlin),
"lua" => Some(Language::Lua),
"swift" => Some(Language::Swift),
+ "svelte" => Some(Language::Svelte),
_ => None,
}
}
@@ -142,6 +146,7 @@ impl Language {
Language::Kotlin => &["kt", "kts"],
Language::Lua => &["lua"],
Language::Swift => &["swift"],
+ Language::Svelte => &["svelte"],
}
}
@@ -163,6 +168,7 @@ impl Language {
Language::Kotlin => "kotlin",
Language::Lua => "lua",
Language::Swift => "swift",
+ Language::Svelte => "svelte",
}
}
@@ -184,6 +190,7 @@ impl Language {
Language::Kotlin => "Kotlin",
Language::Lua => "Lua",
Language::Swift => "Swift",
+ Language::Svelte => "Svelte",
}
}
}
diff --git a/src/parsing/mod.rs b/src/parsing/mod.rs
index 2837f68c..4ba3951e 100644
--- a/src/parsing/mod.rs
+++ b/src/parsing/mod.rs
@@ -22,6 +22,7 @@ pub mod python;
pub mod registry;
pub mod resolution;
pub mod rust;
+pub mod svelte;
pub mod swift;
pub mod typescript;
@@ -58,5 +59,6 @@ pub use resolution::{
PipelineSymbolCache, ResolutionScope, ResolveResult, ScopeLevel,
};
pub use rust::{RustBehavior, RustParser};
+pub use svelte::{SvelteBehavior, SvelteParser};
pub use swift::{SwiftBehavior, SwiftParser};
pub use typescript::{TypeScriptBehavior, TypeScriptParser};
diff --git a/src/parsing/registry.rs b/src/parsing/registry.rs
index 9a3fda11..87463dac 100644
--- a/src/parsing/registry.rs
+++ b/src/parsing/registry.rs
@@ -392,6 +392,7 @@ fn initialize_registry(registry: &mut LanguageRegistry) {
super::clojure::register(registry);
super::lua::register(registry);
super::swift::register(registry);
+ super::svelte::register(registry);
}
/// Get the global registry
diff --git a/src/parsing/svelte/audit.rs b/src/parsing/svelte/audit.rs
new file mode 100644
index 00000000..108f767c
--- /dev/null
+++ b/src/parsing/svelte/audit.rs
@@ -0,0 +1,250 @@
+//! Svelte parser audit module
+//!
+//! Tracks which AST nodes the parser handles vs what's available in the grammar.
+//!
+//! Svelte symbol extraction is split: `
+
+{#snippet card(item)}
+
{item}
+{/snippet}
+"#;
+
+ let audit = SvelteParserAudit::audit_code(code).unwrap();
+
+ assert!(audit.grammar_nodes.contains_key("script_element"));
+ assert!(audit.grammar_nodes.contains_key("snippet_statement"));
+
+ // Script body delegates to TS: greet is a Function.
+ assert!(audit.extracted_symbol_kinds.contains("Function"));
+
+ // Svelte-level nodes the parser acts on are registered.
+ assert!(audit.implemented_nodes.contains("script_element"));
+ assert!(audit.implemented_nodes.contains("snippet_statement"));
+ }
+
+ #[test]
+ fn test_template_node_names() {
+ let code = r#"{#if ready}
+ ok
+{/if}
+{#each items as item}
+ {@render row(item)}
+{/each}
+"#;
+
+ let audit = SvelteParserAudit::audit_code(code).unwrap();
+
+ assert!(audit.grammar_nodes.contains_key("if_statement"));
+ assert!(audit.grammar_nodes.contains_key("each_statement"));
+ assert!(audit.grammar_nodes.contains_key("render_tag"));
+ }
+}
diff --git a/src/parsing/svelte/behavior.rs b/src/parsing/svelte/behavior.rs
new file mode 100644
index 00000000..a9645926
--- /dev/null
+++ b/src/parsing/svelte/behavior.rs
@@ -0,0 +1,66 @@
+//! Svelte language behavior
+
+use super::resolution::{SvelteInheritanceResolver, SvelteResolutionContext};
+use crate::parsing::{InheritanceResolver, LanguageBehavior, ResolutionScope};
+use crate::{FileId, Visibility};
+use tree_sitter::Language;
+
+pub struct SvelteBehavior;
+
+impl SvelteBehavior {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+impl Default for SvelteBehavior {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl LanguageBehavior for SvelteBehavior {
+ fn language_id(&self) -> crate::parsing::registry::LanguageId {
+ crate::parsing::registry::LanguageId::new("svelte")
+ }
+
+ fn format_module_path(&self, base_path: &str, _symbol_name: &str) -> String {
+ base_path.to_string()
+ }
+
+ fn module_separator(&self) -> &'static str {
+ "."
+ }
+
+ fn source_roots(&self) -> &'static [&'static str] {
+ &["src", "lib", "routes", "components", "pages"]
+ }
+
+ fn format_path_as_module(&self, components: &[&str]) -> Option {
+ if components.is_empty() {
+ None
+ } else {
+ Some(components.join("."))
+ }
+ }
+
+ fn get_language(&self) -> Language {
+ tree_sitter_svelte_next::LANGUAGE.into()
+ }
+
+ fn parse_visibility(&self, signature: &str) -> Visibility {
+ if signature.contains("export ") {
+ Visibility::Public
+ } else {
+ Visibility::Private
+ }
+ }
+
+ fn create_resolution_context(&self, file_id: FileId) -> Box {
+ Box::new(SvelteResolutionContext::new(file_id))
+ }
+
+ fn create_inheritance_resolver(&self) -> Box {
+ Box::new(SvelteInheritanceResolver::new())
+ }
+}
diff --git a/src/parsing/svelte/definition.rs b/src/parsing/svelte/definition.rs
new file mode 100644
index 00000000..2d35ac38
--- /dev/null
+++ b/src/parsing/svelte/definition.rs
@@ -0,0 +1,50 @@
+//! Svelte language definition and registration
+
+use crate::parsing::{
+ LanguageBehavior, LanguageDefinition, LanguageId, LanguageParser, LanguageRegistry,
+};
+use crate::{IndexError, IndexResult, Settings};
+use std::sync::Arc;
+
+use super::{SvelteBehavior, SvelteParser};
+
+pub struct SvelteLanguage;
+
+impl LanguageDefinition for SvelteLanguage {
+ fn id(&self) -> LanguageId {
+ LanguageId::new("svelte")
+ }
+
+ fn name(&self) -> &'static str {
+ "Svelte"
+ }
+
+ fn extensions(&self) -> &'static [&'static str] {
+ &["svelte"]
+ }
+
+ fn create_parser(&self, _settings: &Settings) -> IndexResult> {
+ let parser = SvelteParser::new().map_err(|e| IndexError::General(e.to_string()))?;
+ Ok(Box::new(parser))
+ }
+
+ fn create_behavior(&self) -> Box {
+ Box::new(SvelteBehavior::new())
+ }
+
+ fn default_enabled(&self) -> bool {
+ true
+ }
+
+ fn is_enabled(&self, settings: &Settings) -> bool {
+ settings
+ .languages
+ .get("svelte")
+ .map(|config| config.enabled)
+ .unwrap_or(self.default_enabled())
+ }
+}
+
+pub(crate) fn register(registry: &mut LanguageRegistry) {
+ registry.register(Arc::new(SvelteLanguage));
+}
diff --git a/src/parsing/svelte/mod.rs b/src/parsing/svelte/mod.rs
new file mode 100644
index 00000000..7a633029
--- /dev/null
+++ b/src/parsing/svelte/mod.rs
@@ -0,0 +1,30 @@
+//! Svelte language parser implementation
+//!
+//! Svelte files are HTML-shaped templates whose `
+
+Hello
+"#;
+ let symbols = parser.parse(code, file_id(), &mut counter);
+ assert!(
+ symbols.iter().any(|s| s.name.as_ref() == "greet"),
+ "should extract greet function; got: {:?}",
+ symbols.iter().map(|s| s.name.as_ref()).collect::>()
+ );
+ }
+
+ #[test]
+ fn test_snippet_symbols() {
+ let mut parser = SvelteParser::new().unwrap();
+ let mut counter = SymbolCounter::new();
+ let code = r#"
+
+{#snippet card(item)}
+ {item.name}
+{/snippet}
+"#;
+ let symbols = parser.parse(code, file_id(), &mut counter);
+ assert!(
+ symbols.iter().any(|s| s.name.as_ref() == "card"),
+ "should extract snippet 'card'; got: {:?}",
+ symbols.iter().map(|s| s.name.as_ref()).collect::>()
+ );
+ }
+
+ #[test]
+ fn test_range_offset() {
+ // Script starts at line 1 (0-indexed), col 0
+ let r = Range::new(0, 4, 0, 9); // line 0, col 4-9 in script
+ let offset = SvelteParser::offset_range(r, 1, 0);
+ assert_eq!(offset.start_line, 1);
+ assert_eq!(offset.start_column, 4);
+
+ // Multi-line symbol: line 2 in script → line 3 in file
+ let r2 = Range::new(2, 0, 4, 1);
+ let offset2 = SvelteParser::offset_range(r2, 1, 0);
+ assert_eq!(offset2.start_line, 3);
+ assert_eq!(offset2.end_line, 5);
+ }
+
+ #[test]
+ fn test_find_imports() {
+ let mut parser = SvelteParser::new().unwrap();
+ let code = r#"
+
+"#;
+ let fid = file_id();
+ let imports = parser.find_imports(code, fid);
+ assert!(
+ imports.iter().any(|i| i.path.contains("math")),
+ "should find math import; got: {:?}",
+ imports.iter().map(|i| &i.path).collect::>()
+ );
+ }
+
+ #[test]
+ fn test_typescript_script_symbols() {
+ // Svelte 5 defaults to `lang="ts"`; symbols must route through the TS parser.
+ let mut parser = SvelteParser::new().unwrap();
+ let mut counter = SymbolCounter::new();
+ let code = r#"
+
+Hello
+"#;
+ let symbols = parser.parse(code, file_id(), &mut counter);
+ let names: Vec<&str> = symbols.iter().map(|s| s.name.as_ref()).collect();
+ assert!(
+ names.contains(&"greet"),
+ "should extract greet function from TS block; got: {names:?}"
+ );
+ assert!(
+ names.contains(&"User"),
+ "should extract User interface (TS-only construct); got: {names:?}"
+ );
+ }
+
+ #[test]
+ fn test_module_and_instance_scripts() {
+ // Both the module `
+
+
+
+{VERSION}
+"#;
+ let symbols = parser.parse(code, file_id(), &mut counter);
+ let names: Vec<&str> = symbols.iter().map(|s| s.name.as_ref()).collect();
+ assert!(
+ names.contains(&"VERSION"),
+ "should extract VERSION from module script; got: {names:?}"
+ );
+ assert!(
+ names.contains(&"start"),
+ "should extract start from instance script; got: {names:?}"
+ );
+ }
+
+ #[test]
+ fn test_typescript_imports() {
+ let mut parser = SvelteParser::new().unwrap();
+ let code = r#"
+"#;
+ let imports = parser.find_imports(code, file_id());
+ let paths: Vec<&String> = imports.iter().map(|i| &i.path).collect();
+ assert!(
+ paths.iter().any(|p| p.contains("api")),
+ "should find api import from TS block; got: {paths:?}"
+ );
+ }
+}
diff --git a/src/parsing/svelte/resolution.rs b/src/parsing/svelte/resolution.rs
new file mode 100644
index 00000000..e96f5e74
--- /dev/null
+++ b/src/parsing/svelte/resolution.rs
@@ -0,0 +1,181 @@
+//! Svelte-specific resolution and inheritance implementation.
+//!
+//! Svelte components hold JavaScript or TypeScript inside their `\n{x}
\n",
+ &node_categories(),
+ |path| {
+ let audit = SvelteParserAudit::audit_file(path).map_err(|e| e.to_string())?;
+ let report = audit.generate_report();
+ Ok((
+ AuditData::new(
+ audit.grammar_nodes,
+ audit.implemented_nodes,
+ audit.extracted_symbol_kinds,
+ ),
+ report,
+ ))
+ },
+ );
+}