Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0dae2a6
Add component props from the @props directive to the repository
N1ebieski Apr 13, 2025
2daa589
fix undefined type
N1ebieski Apr 13, 2025
7248197
rename undefined to mixed
N1ebieski Apr 13, 2025
944e79b
fix for array items without key
N1ebieski Apr 13, 2025
0fb0c6b
refactoring
N1ebieski Apr 13, 2025
cdd76cf
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
7b372fd
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
3f13d29
fix null type
N1ebieski Apr 13, 2025
a41d9c3
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
7d695d5
Add component props from @props directive to repository
N1ebieski Apr 13, 2025
427ac8d
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
2b740ed
Add component props from @props directive to repository
N1ebieski Apr 13, 2025
559e4e5
wip
N1ebieski Apr 13, 2025
58f8dda
wip
N1ebieski Apr 13, 2025
55935d8
Add component props from @props directive to repository
N1ebieski Apr 13, 2025
565bf6b
refactoring
N1ebieski Apr 13, 2025
6fa1fa3
refactoring
N1ebieski Apr 14, 2025
f6ae23b
remove props duplicates
N1ebieski Apr 14, 2025
72a3d94
support for older versions of laravel
N1ebieski Apr 24, 2025
7ac6206
Merge branch 'main' of https://github.com/laravel/vs-code-extension i…
N1ebieski Jun 11, 2025
dfe7880
Merge branch 'main' of https://github.com/laravel/vs-code-extension i…
N1ebieski Sep 21, 2025
4480d24
check if pcntl is available
N1ebieski Sep 21, 2025
546433d
check if pcntl is available
N1ebieski Sep 21, 2025
6d53774
template
N1ebieski Sep 21, 2025
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
2 changes: 1 addition & 1 deletion generate-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
$content = file_get_contents($template);

$content = str_replace('\\', '\\\\', $content);
$content = str_replace('<?php', '', $content);
$content = preg_replace('/^<\?php/', '', $content);
$content = implode("\n", [
"// This file was generated from {$template}, do not edit directly",
'export default `',
Expand Down
209 changes: 205 additions & 4 deletions php-templates/blade-components.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function all()
))->groupBy('key')->map(fn($items) => [
'isVendor' => $items->first()['isVendor'],
'paths' => $items->pluck('path')->values(),
'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i),
'props' => $items->pluck('props')->unique()->values()->filter()->flatMap(fn($i) => $i),
]);

return [
Expand All @@ -32,11 +32,209 @@ public function all()
];
}

private function runConcurrently(\Illuminate\Support\Collection $items, \Closure $callback, int $concurrency = 8): array
{
if (app()->version() > 11 && function_exists('pcntl_fork') && \Composer\InstalledVersions::isInstalled('spatie/fork')) {
$tasks = $items
->split($concurrency)
->map(fn (\Illuminate\Support\Collection $chunk) => fn (): array => $callback($chunk))
->toArray();

$results = \Illuminate\Support\Facades\Concurrency::driver('fork')->run($tasks);

return array_merge(...$results);
}

return $callback($items);
}

private function getComponentPropsFromDirective(string $path): array
{
if (!\Illuminate\Support\Facades\File::exists($path)) {
return [];
}

$contents = \Illuminate\Support\Facades\File::get($path);

$match = str($contents)->match('/\@props\(\[(.*?)\]\)/s');

if ($match->isEmpty()) {
return [];
}

$parser = (new \PhpParser\ParserFactory)->createForNewestSupportedVersion();

$propsAsString = $match->wrap('[', ']')->toString();

try {
$ast = $parser->parse("<?php return {$propsAsString};");
} catch (\Throwable $e) {
return [];
}

$traverser = new \PhpParser\NodeTraverser();
$visitor = new class extends \PhpParser\NodeVisitorAbstract {
public array $props = [];

private function getClassConstNodeValue(\PhpParser\Node\Expr\ClassConstFetch $node): string
{
return match (true) {
$node->name instanceof \PhpParser\Node\Identifier => "{$node->class->toString()}::{$node->name->toString()}",
default => $node->class->toString(),
};
}

private function getConstNodeValue(\PhpParser\Node\Expr\ConstFetch $node): string
{
return $node->name->toString();
}

private function getStringNodeValue(\PhpParser\Node\Scalar\String_ $node): string
{
return $node->value;
}

private function getIntNodeValue(\PhpParser\Node\Scalar\Int_ $node): int
{
return $node->value;
}

private function getFloatNodeValue(\PhpParser\Node\Scalar\Float_ $node): float
{
return $node->value;
}

private function getNodeValue(\PhpParser\Node $node): mixed
{
return match (true) {
$node instanceof \PhpParser\Node\Expr\ConstFetch => $this->getConstNodeValue($node),
$node instanceof \PhpParser\Node\Expr\ClassConstFetch => $this->getClassConstNodeValue($node),
$node instanceof \PhpParser\Node\Scalar\String_ => $this->getStringNodeValue($node),
$node instanceof \PhpParser\Node\Scalar\Int_ => $this->getIntNodeValue($node),
$node instanceof \PhpParser\Node\Scalar\Float_ => $this->getFloatNodeValue($node),
$node instanceof \PhpParser\Node\Expr\Array_ => $this->getArrayNodeValue($node),
$node instanceof \PhpParser\Node\Expr\New_ => $this->getObjectNodeValue($node),
default => null
};
}

private function getObjectNodeValue(\PhpParser\Node\Expr\New_ $node): array
{
if (! $node->class instanceof \PhpParser\Node\Stmt\Class_) {
return [];
}

$array = [];

foreach ($node->class->getProperties() as $property) {
foreach ($property->props as $item) {
$array[$item->name->name] = $this->getNodeValue($item->default);
}
}

return array_filter($array);
}

private function getArrayNodeValue(\PhpParser\Node\Expr\Array_ $node): array
{
$array = [];
$i = 0;

foreach ($node->items as $item) {
$value = $this->getNodeValue($item->value);

$array[$item->key?->value ?? $i++] = $value;
}

return array_filter($array);
}

public function enterNode(\PhpParser\Node $node) {
if (
$node instanceof \PhpParser\Node\Stmt\Return_
&& $node->expr instanceof \PhpParser\Node\Expr\Array_
) {
foreach ($node->expr->items as $item) {
$this->props[] = match (true) {
$item->value instanceof \PhpParser\Node\Scalar\String_ => [
'name' => \Illuminate\Support\Str::kebab($item->key?->value ?? $item->value->value),
'type' => $item->key ? 'string' : 'mixed',
'hasDefault' => $item->key ? true : false,
'default' => $item->key ? $this->getStringNodeValue($item->value) : null,
],
$item->value instanceof \PhpParser\Node\Expr\ConstFetch => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => $item->value->name->toString() !== 'null' ? 'boolean' : 'mixed',
'hasDefault' => true,
'default' => $this->getConstNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Expr\ClassConstFetch => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => $item->value->class->toString(),
'hasDefault' => true,
'default' => $this->getClassConstNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Scalar\Int_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => 'integer',
'hasDefault' => true,
'default' => $this->getIntNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Scalar\Float_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => 'float',
'hasDefault' => true,
'default' => $this->getFloatNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Expr\Array_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => 'array',
'hasDefault' => true,
'default' => $this->getArrayNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Expr\New_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => $item->value->class->toString(),
'hasDefault' => true,
'default' => $this->getObjectNodeValue($item->value),
],
default => null
};
}
}
}
};
$traverser->addVisitor($visitor);
$traverser->traverse($ast);

return array_filter($visitor->props);
}

private function mapComponentPropsFromDirective(array $files): array
{
if (! \Composer\InstalledVersions::isInstalled('nikic/php-parser')) {
return $files;
}

return $this->runConcurrently(
collect($files),
fn (\Illuminate\Support\Collection $files): array => $files->map(function (array $item): array {
$props = $this->getComponentPropsFromDirective($item['path']);

if ($props !== []) {
$item['props'] = $props;
}

return $item;
})->all()
);
}

protected function getStandardViews()
{
$path = resource_path('views/components');

return $this->findFiles($path, 'blade.php');
return $this->mapComponentPropsFromDirective($this->findFiles($path, 'blade.php'));
}

protected function findFiles($path, $extension, $keyCallback = null)
Expand Down Expand Up @@ -110,6 +308,9 @@ protected function getStandardClasses()
->map(fn($p) => [
'name' => \Illuminate\Support\Str::kebab($p->getName()),
'type' => (string) ($p->getType() ?? 'mixed'),
// We need to add hasDefault, because null can be also a default value,
// it can't be a flag of no default
'hasDefault' => $p->hasDefaultValue(),
'default' => $p->getDefaultValue() ?? $parameters[$p->getName()] ?? null,
]);

Expand Down Expand Up @@ -201,7 +402,7 @@ function (\Illuminate\Support\Stringable $key) use ($item) {
}
}

return $components;
return $this->mapComponentPropsFromDirective($components);
}

protected function getVendorComponents(): array
Expand Down Expand Up @@ -236,7 +437,7 @@ protected function getVendorComponents(): array
}
}

return $components;
return $this->mapComponentPropsFromDirective($components);
}

protected function handleIndexComponents($str)
Expand Down
5 changes: 4 additions & 1 deletion src/features/bladeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getBladeComponents } from "@src/repositories/bladeComponents";
import { config } from "@src/support/config";
import { projectPath } from "@src/support/project";
import { defaultToString } from "@src/support/util";
import * as vscode from "vscode";
import { HoverProvider, LinkProvider } from "..";

Expand Down Expand Up @@ -131,7 +132,9 @@ export const hoverProvider: HoverProvider = (
[
"`" + prop.type + "` ",
"`" + prop.name + "`",
prop.default ? ` = ${prop.default}` : "",
prop.hasDefault
? ` = ${defaultToString(prop.default)}`
: "",
].join(""),
),
);
Expand Down
1 change: 1 addition & 0 deletions src/repositories/bladeComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface BladeComponents {
props: {
name: string;
type: string;
hasDefault: boolean;
default: string | null;
}[];
};
Expand Down
21 changes: 21 additions & 0 deletions src/support/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,24 @@ export const createIndexMapping = (
},
};
};

export const defaultToString = (value: any): string => {
switch (typeof value) {
case "object":
if (value === null) {
return "null";
}

const json: string = JSON.stringify(value, null, 2);

if (json.length > 1000) {
return "object";
}

return json;
case "function":
return "function";
default:
return value.toString();
}
};
Loading