Skip to content

fix(core): snippets ts#8991

Merged
ematipico merged 3 commits intonextfrom
fix/file-source-snippets
Feb 8, 2026
Merged

fix(core): snippets ts#8991
ematipico merged 3 commits intonextfrom
fix/file-source-snippets

Conversation

@ematipico
Copy link
Member

Summary

Closes #8983

Slightly refactored how snippets are parsed. Now they are parsed based on the embedded kind, so that we surgically understand when the JsFileSource needs to change.

Test Plan

Added a new test

Docs

@changeset-bot
Copy link

changeset-bot bot commented Feb 7, 2026

⚠️ No Changeset found

Latest commit: 91ca16a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions bot added A-CLI Area: CLI A-Project Area: project labels Feb 7, 2026
@ematipico ematipico changed the title feat(lsp): report progress while scanning the project (#7961) fix(core): snippets ts Feb 7, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 7, 2026

Walkthrough

This PR refactors embedded-content parsing for HTML-like files by introducing an HtmlVariant-driven flow (Standard, Astro, Vue, Svelte) in parse_embedded_nodes. It changes parse_embedded_script to return a JsFileSource, adds parse_text_expression plus per-variant/single/double text-expression helpers (including parse_svelte_text_expression), and propagates JsFileSource through the embedding flow. The workspace server renamed a local binding from parse_embedded to parse_embedded_nodes. New CLI tests cover TypeScript handling in Svelte and Vue embedded expressions.

Possibly related PRs

  • #8642 — Overlapping changes to embedded-node parsing and Svelte/Vue text-expression helpers and JsFileSource propagation.
  • #7878 — Modifies parse_embedded_script and variant-specific script handling (Astro/TypeScript) affecting the same embedding paths.
  • #8700 — Touches parse_embedded_nodes and per-variant text-expression parsing and adds related tests for embedded bindings.

Suggested labels

A-Parser, L-HTML

Suggested reviewers

  • dyc3
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'fix(core): snippets ts' is vague and doesn't clearly convey the main change, using 'snippets ts' without explaining what the fix actually addresses. Consider a more descriptive title like 'fix: parse embedded expressions with correct file source for TypeScript components' to clarify the core change.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the changeset, referencing issue #8983 and explaining the refactoring of snippet parsing based on embedded kind and JsFileSource adjustments.
Linked Issues check ✅ Passed The changes implement the core requirement: embedded expressions in Svelte files are now parsed using the component's declared language (TypeScript when lang='ts') by refactoring snippet parsing logic to respect embedded kinds.
Out of Scope Changes check ✅ Passed All changes are directly aligned with fixing embedded expression parsing in Svelte and Vue components to respect declared languages; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/file-source-snippets

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does resolve the issue in svelte files, but not in Vue files.

Note the <script> tag does not have lang="ts" (errors on ts syntax): https://play.vuejs.org/#eNp9kE9PwzAMxb9K5PPUaYLTVCYB2gEOgIBjLlVnuo40iWJnFFX97nNS7c9h2i1+v+f42QM8el/sI8ISSqpD61kRcvQrbWtniZWNnXpQC23L+cSFSMHYeVMxSqVUuV2shiFbK1KV/VfjWM5FTF0XTpgBk3z70zbFjpyVoUPq11C7zrcGw7vnVsZqWKpMEquMcX+vWeMQcXbU6y3Wv1f0HfVJ0/ARkDDsUcOJcRUa5Amvv96wl/cJdm4TjbhvwE8kZ2LKONmeot1I7AtfTvvSeRe4tc03rXtGS8elUtDkHLNfg1z++cbq57h3xX3u03aE8QDfgZAO

And this one has lang="ts" (no errors): https://play.vuejs.org/#eNp9kUFPwzAMhf9K5PO0aYLT1E0CtAMcAAHHXKLOdB1pEsXOKKr633FSbeyAdovf++w8ywPchTA/JoQVVFTHNrAi5BSUNa5Za2DSsNGu9o5YudSptVpqVy0mVhwpGLtgDaNUSlX75WYYCmpIGfejxrFaiJi7LkiYyWwZ+9k28wN5JwGG3K+h9l1oLcaXwK18q2GlipM9Y63/fioax4Szk17vsf76Rz9QnzUNrxEJ4xE1nD02sUGe7O37M/byPpud3yUr9BXzDcnblDNO2H1yO4l9wZW0j13wkVvXfNC2Z3R0WioHzeRYeA1yhYcrq//FvZnflj7tRhh/Aadkk/A=

I'll admit that this is a rather silly example, as nobody would actually write code like this, but it still demonstrates the issue.

But on the bright side, this resolves all the obvious issues in my work's svelte codebase 🥳 (there's a few more that appear valid on the outside but I need to investigate a bit before filing an issue)

@ematipico ematipico requested a review from dyc3 February 8, 2026 13:29
@ematipico
Copy link
Member Author

@dyc3 addressed your comments, and added a few tests too

@ematipico ematipico changed the title fix(core): snippets ts fix(core): snippets ts Feb 8, 2026
@ematipico ematipico force-pushed the fix/file-source-snippets branch from 9a89883 to 91ca16a Compare February 8, 2026 13:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/biome_service/src/file_handlers/html.rs`:
- Around line 1000-1007: The code currently hardcodes EmbeddingKind::Vue {
setup: false, is_source: false } when re-wrapping js_file_source, dropping any
existing `<script setup>` context; fix by reading the existing setup flag from
js_file_source (e.g., inspect its embedding kind or an accessor on JsFileSource)
and pass that boolean into with_embedding_kind so you construct
EmbeddingKind::Vue { setup: existing_setup, is_source: false } before calling
parse_text_expression; update the js_file_source -> file_source conversion that
precedes parse_text_expression to preserve the original setup value.

Comment on lines +1000 to +1007
js_file_source: JsFileSource,
) -> Option<(EmbeddedSnippet<JsLanguage>, DocumentFileSource)> {
let expression = element.expression().ok()?;
let content = expression.html_literal_token().ok()?;
let file_source = JsFileSource::js_module().with_embedding_kind(EmbeddingKind::Vue {
let file_source = js_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
});
let document_file_source = DocumentFileSource::Js(file_source);
let options = settings.parse_options::<JsLanguage>(biome_path, &document_file_source);
let parse = parse_js_with_offset_and_cache(
content.text(),
content.text_range().start(),
file_source,
options,
cache,
);
let snippet = EmbeddedSnippet::new(
parse.into(),
expression.range(),
content.text_range(),
content.text_range().start(),
);
Some((snippet, document_file_source))
parse_text_expression(expression, cache, biome_path, settings, file_source)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n crates/biome_service/src/file_handlers/html.rs | sed -n '990,1020p'

Repository: biomejs/biome

Length of output: 1246


🏁 Script executed:

wc -l crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 108


🏁 Script executed:

git ls-files crates/biome_service/src/file_handlers/ | head -20

Repository: biomejs/biome

Length of output: 634


🏁 Script executed:

cat -n crates/biome_service/src/file_handlers/html.rs | head -50

Repository: biomejs/biome

Length of output: 2838


🏁 Script executed:

rg "fn.*js_file_source.*EmbeddedSnippet" crates/biome_service/src/file_handlers/html.rs -A 10

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "parse_vue_template_expression" crates/biome_service/src/file_handlers/html.rs -B 5 -A 15

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "setup: false" crates/biome_service/src/file_handlers/html.rs -B 8 -A 2

Repository: biomejs/biome

Length of output: 467


🏁 Script executed:

rg "EmbeddingKind::Vue" crates/biome_service/src/file_handlers/html.rs -B 3 -A 3

Repository: biomejs/biome

Length of output: 726


🏁 Script executed:

rg "enum EmbeddingKind|struct EmbeddingKind" -A 10

Repository: biomejs/biome

Length of output: 1500


🏁 Script executed:

rg "as_embedding_kind|with_embedding_kind" crates/biome_service/src/file_handlers/html.rs -B 2 -A 2

Repository: biomejs/biome

Length of output: 1882


🏁 Script executed:

rg "impl.*JsFileSource|pub struct JsFileSource|pub enum JsFileSource" -A 20

Repository: biomejs/biome

Length of output: 9853


🏁 Script executed:

rg "as_embedding_kind|with_embedding_kind" -B 2 -A 2 | head -100

Repository: biomejs/biome

Length of output: 7948


🏁 Script executed:

rg "EmbeddingKind::Vue.*setup" -B 2 -A 2

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "setup: true|setup: false" -B 3 -A 3 | head -150

Repository: biomejs/biome

Length of output: 2260


🏁 Script executed:

head -1100 crates/biome_service/src/file_handlers/html.rs | tail -200 | cat -n

Repository: biomejs/biome

Length of output: 9011


🏁 Script executed:

grep -n "parse_vue_template_expression\|parse_text_expression\|EmbeddedSnippet" crates/biome_service/src/file_handlers/html.rs | head -20

Repository: biomejs/biome

Length of output: 1276


🏁 Script executed:

sed -n '950,1100p' crates/biome_service/src/file_handlers/html.rs | cat -n

Repository: biomejs/biome

Length of output: 6890


🏁 Script executed:

rg "parse_vue_template_expression|parse_text_expression" crates/biome_service/src/file_handlers/ -B 15 -A 2 | head -200

Repository: biomejs/biome

Length of output: 6139


🏁 Script executed:

rg "fn parse_vue_template_expression|fn parse_text_expression|fn extract_vue_expression" crates/biome_service/src/file_handlers/html.rs -A 30

Repository: biomejs/biome

Length of output: 1081


🏁 Script executed:

sed -n '960,1050p' crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 3359


🏁 Script executed:

rg -n "Option.*EmbeddedSnippet.*JsLanguage.*DocumentFileSource" crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 540


🏁 Script executed:

rg "js_file_source.with_embedding_kind" crates/biome_service/src/file_handlers/html.rs -B 10 -A 5

Repository: biomejs/biome

Length of output: 649


🏁 Script executed:

ast-grep --pattern 'fn $FUNC($_) -> Option<(EmbeddedSnippet<$_>, $_)> { $$$}' crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

python3 <<'EOF'
with open('crates/biome_service/src/file_handlers/html.rs', 'r') as f:
    lines = f.readlines()
    for i, line in enumerate(lines[995:1015], start=996):
        print(f"{i}: {line}", end='')
EOF

Repository: biomejs/biome

Length of output: 940


🏁 Script executed:

python3 <<'EOF'
with open('crates/biome_service/src/file_handlers/html.rs', 'r') as f:
    content = f.read()
    # Find all occurrences of "setup: false"
    import re
    matches = list(re.finditer(r'setup: false', content))
    for match in matches:
        start = max(0, match.start() - 300)
        end = min(len(content), match.end() + 300)
        line_num = content[:match.start()].count('\n') + 1
        print(f"\n=== Around line {line_num} ===")
        print(content[start:end])
        print("---")
EOF

Repository: biomejs/biome

Length of output: 699


🏁 Script executed:

grep -n "setup: false" crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 83


🏁 Script executed:

sed -n '1000,1010p' crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 509


🏁 Script executed:

ls -la crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 153


🏁 Script executed:

rg "setup" crates/biome_service/src/file_handlers/html.rs | head -20

Repository: biomejs/biome

Length of output: 147


🏁 Script executed:

rg "parse_text_expression.*js_file_source" -B 5 -A 5

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "EmbeddingKind::Vue.*setup.*false" -B 10 -A 5

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "pub enum EmbeddingKind|EmbeddingKind::" -A 3 | head -100

Repository: biomejs/biome

Length of output: 7682


🏁 Script executed:

rg "with_embedding_kind" -B 5 -A 8

Repository: biomejs/biome

Length of output: 18033


🏁 Script executed:

rg "as_embedding_kind" -B 2 -A 10

Repository: biomejs/biome

Length of output: 16878


🏁 Script executed:

rg "fn as_embedding_kind|fn with_embedding_kind" -A 5

Repository: biomejs/biome

Length of output: 1718


🏁 Script executed:

rg "EmbeddingKind::Vue.*setup" crates/ -B 2 -A 2 | head -150

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "script setup" crates/biome_service/src/file_handlers/ -B 3 -A 3

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "template.*expression|expression.*template" crates/biome_service/src/file_handlers/html.rs -B 2 -A 2 | head -100

Repository: biomejs/biome

Length of output: 315


🏁 Script executed:

rg "setup.*true|setup.*false" crates/ -B 4 -A 2 | grep -A 4 -B 4 "Vue" | head -200

Repository: biomejs/biome

Length of output: 2204


🏁 Script executed:

rg "EmbeddingKind::Vue.*{" crates/biome_service/ -B 5 -A 10 | head -250

Repository: biomejs/biome

Length of output: 162


🏁 Script executed:

rg "pub enum JsFileSource|pub struct JsFileSource" -A 20

Repository: biomejs/biome

Length of output: 1650


🏁 Script executed:

python3 <<'EOF'
import pathlib
p = pathlib.Path('crates/biome_service/src/file_handlers/html.rs')
if p.exists():
    lines = p.read_text().splitlines()
    print(f"Total lines: {len(lines)}")
    if len(lines) > 1007:
        for i in range(999, min(1008, len(lines))):
            print(f"{i+1}: {lines[i]}")
    else:
        print("File has fewer than 1008 lines")
else:
    print("File not found")
EOF

Repository: biomejs/biome

Length of output: 492


🏁 Script executed:

find crates -name "html.rs" -type f | xargs wc -l

Repository: biomejs/biome

Length of output: 315


🏁 Script executed:

rg "fn.*Option.*EmbeddedSnippet" crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

rg "^fn |^    fn " crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 1447


🏁 Script executed:

rg "parse_vue.*expression|vue.*template" crates/biome_service/src/file_handlers/html.rs -i | head -20

Repository: biomejs/biome

Length of output: 153


🏁 Script executed:

rg "with_embedding_kind.*Vue" crates/ -B 3 -A 3 | head -200

Repository: biomejs/biome

Length of output: 3653


🏁 Script executed:

rg "EmbeddingKind::Vue.*setup" crates/ -B 3 -A 3 | sort | uniq | head -100

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

web_search
Vue script setup context preservation in template expressions

Repository: biomejs/biome

Length of output: 149


🏁 Script executed:

python3 <<'EOF'
import re
with open('crates/biome_service/src/file_handlers/html.rs', 'r') as f:
    content = f.read()

# Search for functions that return Option<(EmbeddedSnippet
pattern = r'fn\s+\w+\s*\([^)]*js_file_source[^)]*\)[^{]*Option\s*<\s*\(\s*EmbeddedSnippet'
matches = list(re.finditer(pattern, content))
print(f"Found {len(matches)} functions matching pattern\n")
for match in matches:
    line_num = content[:match.start()].count('\n') + 1
    print(f"Line {line_num}: {match.group()}\n")
    
# Also search for any with_embedding_kind calls
pattern2 = r'with_embedding_kind\s*\(\s*EmbeddingKind::Vue\s*\{'
for match in re.finditer(pattern2, content):
    line_num = content[:match.start()].count('\n') + 1
    start = max(0, match.start() - 200)
    end = min(len(content), match.end() + 200)
    print(f"\n=== with_embedding_kind call at line {line_num} ===")
    print(content[start:end])
EOF

Repository: biomejs/biome

Length of output: 1300


🏁 Script executed:

grep -n "with_embedding_kind.*EmbeddingKind::Vue" crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 226


🏁 Script executed:

sed -n '990,1020p' crates/biome_service/src/file_handlers/html.rs | cat -n

Repository: biomejs/biome

Length of output: 1246


🏁 Script executed:

head -1020 crates/biome_service/src/file_handlers/html.rs | tail -40 | cat -n

Repository: biomejs/biome

Length of output: 1527


🏁 Script executed:

git log --oneline -20 -- crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 76


🏁 Script executed:

git show HEAD:crates/biome_service/src/file_handlers/html.rs | sed -n '1000,1010p'

Repository: biomejs/biome

Length of output: 509


🏁 Script executed:

git diff HEAD crates/biome_service/src/file_handlers/html.rs | head -100

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

git status crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 122


🏁 Script executed:

python3 -c "
with open('crates/biome_service/src/file_handlers/html.rs') as f:
    lines = f.readlines()
    print('Total lines:', len(lines))
    print()
    for i in range(999, min(1010, len(lines))):
        print(f'{i+1:4d}: {lines[i]}', end='')
"

Repository: biomejs/biome

Length of output: 594


Preserve Vue setup context when re-wrapping the embedding kind.

Template expressions in Vue files with <script setup> need to maintain that context for correct variable resolution. Hardcoding setup: false drops this information even when the incoming js_file_source has setup: true, preventing proper scope analysis for setup bindings.

Extract the existing setup flag before re-wrapping:

Suggested fix
-    let file_source = js_file_source.with_embedding_kind(EmbeddingKind::Vue {
-        setup: false,
-        is_source: false,
-    });
+    let setup = matches!(
+        js_file_source.as_embedding_kind(),
+        EmbeddingKind::Vue { setup: true, .. }
+    );
+    let file_source = js_file_source.with_embedding_kind(EmbeddingKind::Vue {
+        setup,
+        is_source: false,
+    });
🤖 Prompt for AI Agents
In `@crates/biome_service/src/file_handlers/html.rs` around lines 1000 - 1007, The
code currently hardcodes EmbeddingKind::Vue { setup: false, is_source: false }
when re-wrapping js_file_source, dropping any existing `<script setup>` context;
fix by reading the existing setup flag from js_file_source (e.g., inspect its
embedding kind or an accessor on JsFileSource) and pass that boolean into
with_embedding_kind so you construct EmbeddingKind::Vue { setup: existing_setup,
is_source: false } before calling parse_text_expression; update the
js_file_source -> file_source conversion that precedes parse_text_expression to
preserve the original setup value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be valid

@ematipico
Copy link
Member Author

There's still an issue where a user could create multiple scripts that have different languages. Worth considering a rule for that

@ematipico ematipico merged commit 839c160 into next Feb 8, 2026
11 checks passed
@ematipico ematipico deleted the fix/file-source-snippets branch February 8, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Project Area: project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants