Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Named Blade slot closers are recognized.** A slot written as `<x-slot:title>…</x-slot:title>` no longer reports a mismatched component tag. Contributed by @shuvroroy.
- **`phpantom_lsp fix` runs its workers on the same stack as `analyze`.** The parse and fix workers were spawned with the 2 MB default a thread gets, where `analyze` gives its workers the 8 MB the recursive parser and type walker need, so a project holding a deeply nested file could crash `fix` outright where `analyze` completed. Both commands now share one parse phase, and `fix` also runs the Laravel discovery `analyze` does before fixing, so the two see the same project.
- **A formatting edit measures the last line in UTF-16 units.** The whole-document replacement a formatter produces ended at a column counted in bytes, so a file whose unterminated last line held multibyte text was sent an end position past that line.
- **Pint reads the project's `pint.json`.** Pint looks for its configuration in the directory it is started from, and it was started in the language server's own directory, so an editor that launches the server from a subdirectory or a multi-root workspace had Blade and PHP files formatted with Pint's default `laravel` preset rather than the project's. Pint, php-cs-fixer, and phpcbf now run with the workspace root as their working directory.
Expand Down
50 changes: 35 additions & 15 deletions src/blade/component_tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,16 +494,12 @@ pub(crate) fn tag_spans(content: &str) -> Vec<TagSpan> {
/// Whether a closing tag spelled `closer_name` ends an opener of kind
/// `kind` named `name`.
///
/// Every tag closes under its own name, with one exception Blade's own
/// compiler carves out: a named slot (`<x-slot:title>`, or the legacy
/// `<x-slot name="title">`) still closes with the bare `</x-slot>`, never
/// repeating the slot's own name in the closing tag.
/// Every tag closes under its own name. A named slot (`<x-slot:title>`)
/// also accepts the bare `</x-slot>` used by the legacy
/// `<x-slot name="title">` form.
fn closer_matches(kind: TagKind, name: &str, closer_name: &str) -> bool {
if kind == TagKind::Blade && is_slot_tag_name(name) {
closer_name == "slot"
} else {
name == closer_name
}
name == closer_name
|| (kind == TagKind::Blade && is_slot_tag_name(name) && closer_name == "slot")
}

/// A place a component tag's block structure does not add up: the same
Expand Down Expand Up @@ -1468,15 +1464,25 @@ mod tests {
assert_eq!(names, ["card", "alert", "note"]);
}

/// A named inline slot closes with the bare `</x-slot>`, not
/// `</x-slot:title>`; `tag_spans` has to know that too or every named
/// slot in the file comes back unclosed.
/// A named inline slot may close with the bare `</x-slot>`.
#[test]
fn a_named_slot_closes_with_the_bare_tag() {
let blade = "<x-card>\n<x-slot:title>\nHi\n</x-slot>\n</x-card>\n";
assert_eq!(tag_bodies(blade).len(), 2);
}

#[test]
fn a_named_slot_closes_with_its_own_name() {
let blade = "<x-card>\n<x-slot:title>\nHi\n</x-slot:title>\n</x-card>\n";
let tags = tag_spans(blade);
assert_eq!(tags.len(), 2);
assert!(tags.iter().all(|tag| tag.closed));
assert_eq!(
&blade[tags[1].span.clone()],
"<x-slot:title>\nHi\n</x-slot:title>"
);
}

/// The imbalances [`tag_imbalances`] finds, as short strings for
/// readable assertions: `"mismatched </x-card>/<x-alert>"`,
/// `"unexpected </x-card>"`, `"unclosed <x-alert>"`.
Expand Down Expand Up @@ -1550,10 +1556,24 @@ mod tests {
);
}

/// A named inline slot that is properly closed reports nothing, even
/// though its closing tag never repeats the slot's own name.
/// Both closing spellings of a named inline slot are balanced.
#[test]
fn a_properly_closed_named_slot_reports_nothing() {
assert!(tag_report("<x-card>\n<x-slot:title>\nHi\n</x-slot>\n</x-card>\n").is_empty());
for closer in ["slot", "slot:title"] {
let blade = format!("<x-card>\n<x-slot:title>\nHi\n</x-{closer}>\n</x-card>\n");
assert!(tag_report(&blade).is_empty(), "{blade}");
}
}

#[test]
fn a_named_slot_closed_by_a_different_tag_is_mismatched() {
for closer in ["slot:other", "alert"] {
let blade = format!("<x-card>\n<x-slot:title>\nHi\n</x-{closer}>\n</x-card>\n");
assert_eq!(
tag_report(&blade),
[format!("mismatched </x-{closer}>/<x-slot:title>")],
"{blade}"
);
}
}
}
Loading