diff --git a/CHANGELOG.md b/CHANGELOG.md index fd03efd..d564d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Added Svelte symbol indexing — components (from filename), props, $state, $derived, functions via TS delegation (#375) ### Added diff --git a/src/engine/context/extraction.rs b/src/engine/context/extraction.rs index 3a801b6..e5cf926 100644 --- a/src/engine/context/extraction.rs +++ b/src/engine/context/extraction.rs @@ -228,7 +228,7 @@ pub fn extract_symbols_from_line(line: &str, language: &str) -> Vec } } } - "java" | "kt" | "kts" | "dart" => { + "java" | "kt" | "kts" | "dart" | "svelte" => { for cap in RE_JAVA_IMPORT.captures_iter(line) { add_unique( &mut symbols, @@ -387,7 +387,7 @@ pub fn extract_definitions_from_diff( (Some(&RE_DEF_JS_FN), Some(&RE_DEF_JS_TYPE)) } "go" => (Some(&RE_DEF_GO_FN), Some(&RE_DEF_GO_TYPE)), - "java" | "kt" | "kts" | "dart" => (None, Some(&RE_DEF_JAVA_TYPE)), + "java" | "kt" | "kts" | "dart" | "svelte" => (None, Some(&RE_DEF_JAVA_TYPE)), _ => (None, None), }; diff --git a/src/index/extract.rs b/src/index/extract.rs index 4ebfdc4..1224f8d 100644 --- a/src/index/extract.rs +++ b/src/index/extract.rs @@ -166,6 +166,24 @@ static RE_DART_TYPEDEF: LazyLock = static RE_DART_GETTER: LazyLock = LazyLock::new(|| { Regex::new(r"^\s*(?:[A-Z]\w*\s+)?(?:[a-zA-Z]\w*(?:<[^>]+>)?)\s+get\s+(\w+)").unwrap() }); +// ─── Svelte ─── + +static RE_SVELTE_SCRIPT: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?s)]*)?> +?(.*?) +?", + ) + .unwrap() +}); +static RE_SVELTE_EXPORT_PROP: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*export\s+let\s+(\w+)").unwrap()); +static RE_SVELTE_STATE: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*let\s+(\w+)\s*=\s*\$state(?:<[^>]*>)?\s*\(").unwrap()); +static RE_SVELTE_DERIVED: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*let\s+(\w+)\s*=\s*\$derived(?:<[^>]*>)?\s*\(").unwrap()); +static RE_SVELTE_EFFECT: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*\$effect\s*\(\s*(?:async\s+)?\(\s*\)\s*=>").unwrap()); /// Extract symbols from source code. /// @@ -206,6 +224,7 @@ pub fn extract_symbols(content: &str, language: &str, file_path: &str) -> Vec extract_lua(line, line_no, file_path, line, &mut symbols), "zig" => extract_zig(line, line_no, file_path, line, &mut symbols), "dart" => extract_dart(line, line_no, file_path, line, &mut symbols), + "svelte" => extract_svelte(content, file_path, &mut symbols), _ => {} } } @@ -409,6 +428,79 @@ fn extract_dart(line: &str, line_no: u32, file: &str, raw: &str, out: &mut Vec` blocks and delegate to TS/JS extractor +/// 3. Extract Svelte-specific: export props, $state, $derived +fn extract_svelte(content: &str, file: &str, out: &mut Vec) { + // 1. Component name from filename + let component_name = std::path::Path::new(file) + .file_stem() + .and_then(|s| s.to_str()) + .map(|s| { + let mut result = String::new(); + let mut capitalize_next = true; + for ch in s.chars() { + if ch == '-' || ch == '_' { + capitalize_next = true; + } else if capitalize_next { + result.push(ch.to_ascii_uppercase()); + capitalize_next = false; + } else { + result.push(ch); + } + } + result + }) + .unwrap_or_default(); + + if !component_name.is_empty() { + out.push(ExtractedDef { + name: component_name.clone(), + kind: SymbolKind::Struct, + file: file.to_string(), + line: 1, + signature: format!("", component_name), + }); + } + + // 2. Extract + + + +{#if count > 0} +

Counting!

+{/if} + + +"#; + + let symbols = extract_symbols(content, "svelte", "src/lib/Counter.svelte"); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect(); + println!("Svelte symbols: {:?}", names); + + assert!(names.contains(&"Counter"), "missing component name"); + assert!(names.contains(&"name"), "missing prop: name"); + assert!(names.contains(&"count"), "missing prop: count"); + assert!( + names.iter().any(|n| n.starts_with("doubled")), + "missing $derived: doubled" + ); + assert!( + names.iter().any(|n| n.starts_with("items")), + "missing $state: items" + ); + assert!( + names.contains(&"handleClick"), + "missing function: handleClick" + ); + assert!(names.contains(&"fetchData"), "missing function: fetchData"); + } }