From a85cf723ecb392e4037df214ab7adc609fadd70d Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Thu, 27 Mar 2025 21:09:42 +0000 Subject: [PATCH 01/15] Add a repository for Livewire components Fixes N1ebieski/vs-code-extension#29 --- php-templates/livewire-components.php | 209 ++++++++++++++++++++++++ src/repositories/configs.ts | 4 + src/repositories/livewireComponents.ts | 49 ++++++ src/support/str.ts | 3 + src/templates/index.ts | 2 + src/templates/livewire-components.ts | 210 +++++++++++++++++++++++++ 6 files changed, 477 insertions(+) create mode 100644 php-templates/livewire-components.php create mode 100644 src/repositories/livewireComponents.ts create mode 100644 src/support/str.ts create mode 100644 src/templates/livewire-components.ts diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php new file mode 100644 index 00000000..6906e5bc --- /dev/null +++ b/php-templates/livewire-components.php @@ -0,0 +1,209 @@ +getStandardComponents(), + $this->getVoltComponents() + ))->groupBy('key')->map(fn($items) => [ + 'isVendor' => $items->first()['isVendor'], + 'paths' => $items->pluck('path')->values(), + 'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i), + ]); + + return [ + 'components' => $components + ]; + } + + /** + * @return array + */ + protected function findFiles(string $path, string $extension, \Closure $keyCallback): array + { + if (! is_dir($path)) { + return []; + } + + $files = \Symfony\Component\Finder\Finder::create() + ->files() + ->name("*." . $extension) + ->in($path); + $components = []; + $pathRealPath = realpath($path); + + foreach ($files as $file) { + $realPath = $file->getRealPath(); + + $key = str($realPath) + ->replace($pathRealPath, '') + ->ltrim('/\\') + ->replace('.' . $extension, '') + ->replace(['/', '\\'], '.') + ->pipe(fn(string $str): string => $str); + + $components[] = [ + "path" => LaravelVsCode::relativePath($realPath), + "isVendor" => LaravelVsCode::isVendor($realPath), + "key" => $keyCallback ? $keyCallback($key) : $key, + ]; + } + + return $components; + } + + protected function getStandardComponents(): array + { + /** @var string|null $classNamespace */ + $classNamespace = config('livewire.class_namespace'); + + if (! $classNamespace) { + return []; + } + + $path = str($classNamespace) + ->replace('\\', DIRECTORY_SEPARATOR) + ->replace('App', 'app') + ->toString(); + + $items = $this->findFiles( + $path, + 'php', + fn(\Illuminate\Support\Stringable $key): string => $key->explode('.') + ->map(fn(string $p): string => \Illuminate\Support\Str::kebab($p)) + ->implode('.'), + ); + + $components = []; + + foreach ($items as $item) { + $class = str($item['path']) + ->replace('.php', '') + ->replace('/', '\\') + ->ucfirst() + ->toString(); + + if (! class_exists($class)) { + continue; + } + + $reflection = new \ReflectionClass($class); + + if (! $reflection->isSubclassOf('Livewire\Component')) { + continue; + } + + $components[] = [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + } + + return $components; + } + + protected function getVoltComponents(): array + { + /** @var string|null $viewPath */ + $path = config('livewire.view_path'); + + if (! $path) { + return []; + } + + $items = $this->findFiles( + $path, + 'php', + fn(\Illuminate\Support\Stringable $key): string => $key->explode('.') + ->map(fn(string $p): string => \Illuminate\Support\Str::kebab($p)) + ->implode('.'), + ); + + $components = []; + + foreach ($items as $item) { + // This is ugly, I know, but I don't have better idea how to get + // anonymous classes from Volt components + ob_start(); + + try { + require_once $item['path']; + } catch (\Throwable $e) { + continue; + } + + ob_get_clean(); + + $declaredClasses = get_declared_classes(); + $class = end($declaredClasses); + + if (! \Illuminate\Support\Str::contains($class, '@anonymous')) { + continue; + } + + $reflection = new \ReflectionClass($class); + + if (! $reflection->isSubclassOf('Livewire\Volt\Component')) { + continue; + } + + $components[] = [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + } + + return $components; + } + + /** + * @return array + */ + protected function getComponentProps(ReflectionClass $reflection): array + { + $props = collect(); + + // Firstly we need to get the mount method parameters. Remember that + // Livewire components can have multiple mount methods in traits. + + $methods = $reflection->getMethods(); + + $mountMethods = array_filter( + $methods, + fn (\ReflectionMethod $method): bool => strpos($method->getName(), 'mount') === 0 + ); + + foreach ($mountMethods as $method) { + $parameters = $method->getParameters(); + + $parameters = collect($parameters) + ->map(fn(\ReflectionParameter $p) => [ + 'name' => \Illuminate\Support\Str::kebab($p->getName()), + 'type' => (string) ($p->getType() ?? 'mixed'), + 'default' => $p->isOptional() ? $p->getDefaultValue() : null + ]) + ->all(); + + $props = $props->merge($parameters); + } + + // Then we need to get the public properties + + $properties = collect($reflection->getProperties()) + ->filter(fn(\ReflectionProperty $p) => $p->isPublic() && $p->getDeclaringClass()->getName() === $reflection->getName()) + ->map(fn(\ReflectionProperty $p) => [ + 'name' => \Illuminate\Support\Str::kebab($p->getName()), + 'type' => (string) ($p->getType() ?? 'mixed'), + 'default' => $p->getDefaultValue() + ]) + ->all(); + + return $props + ->merge($properties) + ->unique('name') // Mount parameters always overwrite public properties + ->all(); + } +}; + +echo json_encode($components->all()); diff --git a/src/repositories/configs.ts b/src/repositories/configs.ts index cea56b83..69c2c426 100644 --- a/src/repositories/configs.ts +++ b/src/repositories/configs.ts @@ -2,6 +2,10 @@ import { repository } from "."; import { Config } from ".."; import { runInLaravel, template } from "../support/php"; +export const getConfigByName = (name: string): Config | undefined => { + return getConfigs().items.find((item) => item.name === name); +}; + export const getConfigs = repository({ load: () => { return runInLaravel(template("configs"), "Configs").then( diff --git a/src/repositories/livewireComponents.ts b/src/repositories/livewireComponents.ts new file mode 100644 index 00000000..4aaa9cc7 --- /dev/null +++ b/src/repositories/livewireComponents.ts @@ -0,0 +1,49 @@ +import { runInLaravel, template } from "@src/support/php"; +import { projectPath } from "@src/support/project"; +import { lcfirst } from "@src/support/str"; +import { waitForValue } from "@src/support/util"; +import { repository } from "."; +import { getConfigByName, getConfigs } from "./configs"; + +let livewirePaths: string[] | null = null; + +export interface LivewireComponents { + components: { + [key: string]: { + paths: string[]; + isVendor: boolean; + props: { + name: string; + type: string; + default: string | null; + }[]; + }; + }; +} + +const load = () => { + getConfigs().whenLoaded(() => { + livewirePaths = [ + projectPath(lcfirst(getConfigByName('livewire.class_namespace')?.value?.replace('\\', '/') ?? 'app/Livewire')), + getConfigByName('livewire.view_path')?.value ?? 'resources/views/livewire' + ]; + }); + + return runInLaravel(template("livewireComponents")); +}; + +export const getLivewireComponents = repository({ + load, + pattern: () => + waitForValue(() => livewirePaths).then((paths) => { + if (paths === null || paths.length === 0) { + return null; + } + + return paths.map(path => path + "/{*,**/*}"); + }), + itemsDefault: { + components: {}, + }, + fileWatcherEvents: ["create", "delete"], +}); diff --git a/src/support/str.ts b/src/support/str.ts new file mode 100644 index 00000000..ac64063e --- /dev/null +++ b/src/support/str.ts @@ -0,0 +1,3 @@ +export const lcfirst = (str: string): string => { + return str.charAt(0).toLowerCase() + str.slice(1); +}; \ No newline at end of file diff --git a/src/templates/index.ts b/src/templates/index.ts index 4b0572f3..40617165 100644 --- a/src/templates/index.ts +++ b/src/templates/index.ts @@ -5,6 +5,7 @@ import bladeDirectives from "./blade-directives"; import bootstrapLaravel from "./bootstrap-laravel"; import configs from "./configs"; import inertia from "./inertia"; +import livewireComponents from "./livewire-components"; import middleware from "./middleware"; import models from "./models"; import routes from "./routes"; @@ -14,6 +15,7 @@ import views from "./views"; const templates = { app, auth, + livewireComponents, bladeComponents, bladeDirectives, bootstrapLaravel, diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts new file mode 100644 index 00000000..74f6689e --- /dev/null +++ b/src/templates/livewire-components.ts @@ -0,0 +1,210 @@ +// This file was generated from php-templates/livewire-components.php, do not edit directly +export default ` +$components = new class { + public function all(): array + { + $components = collect(array_merge( + $this->getStandardComponents(), + $this->getVoltComponents() + ))->groupBy('key')->map(fn($items) => [ + 'isVendor' => $items->first()['isVendor'], + 'paths' => $items->pluck('path')->values(), + 'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i), + ]); + + return [ + 'components' => $components + ]; + } + + /** + * @return array + */ + protected function findFiles(string $path, string $extension, \\Closure $keyCallback): array + { + if (! is_dir($path)) { + return []; + } + + $files = \\Symfony\\Component\\Finder\\Finder::create() + ->files() + ->name("*." . $extension) + ->in($path); + $components = []; + $pathRealPath = realpath($path); + + foreach ($files as $file) { + $realPath = $file->getRealPath(); + + $key = str($realPath) + ->replace($pathRealPath, '') + ->ltrim('/\\\\') + ->replace('.' . $extension, '') + ->replace(['/', '\\\\'], '.') + ->pipe(fn(string $str): string => $str); + + $components[] = [ + "path" => LaravelVsCode::relativePath($realPath), + "isVendor" => LaravelVsCode::isVendor($realPath), + "key" => $keyCallback ? $keyCallback($key) : $key, + ]; + } + + return $components; + } + + protected function getStandardComponents(): array + { + /** @var string|null $classNamespace */ + $classNamespace = config('livewire.class_namespace'); + + if (! $classNamespace) { + return []; + } + + $path = str($classNamespace) + ->replace('\\\\', DIRECTORY_SEPARATOR) + ->replace('App', 'app') + ->toString(); + + $items = $this->findFiles( + $path, + 'php', + fn(\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') + ->map(fn(string $p): string => \\Illuminate\\Support\\Str::kebab($p)) + ->implode('.'), + ); + + $components = []; + + foreach ($items as $item) { + $class = str($item['path']) + ->replace('.php', '') + ->replace('/', '\\\\') + ->ucfirst() + ->toString(); + + if (! class_exists($class)) { + continue; + } + + $reflection = new \\ReflectionClass($class); + + if (! $reflection->isSubclassOf('Livewire\\Component')) { + continue; + } + + $components[] = [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + } + + return $components; + } + + protected function getVoltComponents(): array + { + /** @var string|null $viewPath */ + $path = config('livewire.view_path'); + + if (! $path) { + return []; + } + + $items = $this->findFiles( + $path, + 'php', + fn(\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') + ->map(fn(string $p): string => \\Illuminate\\Support\\Str::kebab($p)) + ->implode('.'), + ); + + $components = []; + + foreach ($items as $item) { + // This is ugly, I know, but I don't have better idea how to get + // anonymous classes from Volt components + ob_start(); + + try { + require_once $item['path']; + } catch (\\Throwable $e) { + continue; + } + + ob_get_clean(); + + $declaredClasses = get_declared_classes(); + $class = end($declaredClasses); + + if (! \\Illuminate\\Support\\Str::contains($class, '@anonymous')) { + continue; + } + + $reflection = new \\ReflectionClass($class); + + if (! $reflection->isSubclassOf('Livewire\\Volt\\Component')) { + continue; + } + + $components[] = [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + } + + return $components; + } + + /** + * @return array + */ + protected function getComponentProps(ReflectionClass $reflection): array + { + $props = collect(); + + // Firstly we need to get the mount method parameters. Remember that + // Livewire components can have multiple mount methods in traits. + + $methods = $reflection->getMethods(); + + $mountMethods = array_filter( + $methods, + fn (\\ReflectionMethod $method): bool => strpos($method->getName(), 'mount') === 0 + ); + + foreach ($mountMethods as $method) { + $parameters = $method->getParameters(); + + $parameters = collect($parameters) + ->map(fn(\\ReflectionParameter $p) => [ + 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), + 'type' => (string) ($p->getType() ?? 'mixed'), + 'default' => $p->isOptional() ? $p->getDefaultValue() : null + ]) + ->all(); + + $props = $props->merge($parameters); + } + + // Then we need to get the public properties + + $properties = collect($reflection->getProperties()) + ->filter(fn(\\ReflectionProperty $p) => $p->isPublic() && $p->getDeclaringClass()->getName() === $reflection->getName()) + ->map(fn(\\ReflectionProperty $p) => [ + 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), + 'type' => (string) ($p->getType() ?? 'mixed'), + 'default' => $p->getDefaultValue() + ]) + ->all(); + + return $props + ->merge($properties) + ->unique('name') // Mount parameters always overwrite public properties + ->all(); + } +}; + +echo json_encode($components->all()); +`; \ No newline at end of file From 9e40eecfa606400118c0b7bd9ea423935d716a80 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Thu, 27 Mar 2025 21:26:48 +0000 Subject: [PATCH 02/15] fix extension for volt components --- php-templates/livewire-components.php | 2 +- src/templates/livewire-components.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index 6906e5bc..1ac44b34 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -114,7 +114,7 @@ protected function getVoltComponents(): array $items = $this->findFiles( $path, - 'php', + 'blade.php', fn(\Illuminate\Support\Stringable $key): string => $key->explode('.') ->map(fn(string $p): string => \Illuminate\Support\Str::kebab($p)) ->implode('.'), diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index 74f6689e..526d55b9 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -114,7 +114,7 @@ $components = new class { $items = $this->findFiles( $path, - 'php', + 'blade.php', fn(\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') ->map(fn(string $p): string => \\Illuminate\\Support\\Str::kebab($p)) ->implode('.'), From 3dcb68106a3d01273afc4585d543b60b7a1867da Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Thu, 27 Mar 2025 22:25:42 +0000 Subject: [PATCH 03/15] fix duplicate props and refactoring --- php-templates/livewire-components.php | 129 ++++++++++++++++---------- src/templates/livewire-components.ts | 129 ++++++++++++++++---------- 2 files changed, 162 insertions(+), 96 deletions(-) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index 1ac44b34..888c5827 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -1,11 +1,38 @@ getMessage()); + } +} + $components = new class { public function all(): array { $components = collect(array_merge( - $this->getStandardComponents(), - $this->getVoltComponents() + $this->getStandardClasses(), + $this->getStandardViews() ))->groupBy('key')->map(fn($items) => [ 'isVendor' => $items->first()['isVendor'], 'paths' => $items->pluck('path')->values(), @@ -53,7 +80,7 @@ protected function findFiles(string $path, string $extension, \Closure $keyCallb return $components; } - protected function getStandardComponents(): array + protected function getStandardClasses(): array { /** @var string|null $classNamespace */ $classNamespace = config('livewire.class_namespace'); @@ -75,35 +102,35 @@ protected function getStandardComponents(): array ->implode('.'), ); - $components = []; - - foreach ($items as $item) { - $class = str($item['path']) + return collect($items) + ->map(function ($item) { + $class = str($item['path']) ->replace('.php', '') ->replace('/', '\\') ->ucfirst() ->toString(); - if (! class_exists($class)) { - continue; - } + if (! class_exists($class)) { + return null; + } - $reflection = new \ReflectionClass($class); + $reflection = new \ReflectionClass($class); - if (! $reflection->isSubclassOf('Livewire\Component')) { - continue; - } + if (! $reflection->isSubclassOf('Livewire\Component')) { + return null; + } - $components[] = [ - ...$item, - 'props' => $this->getComponentProps($reflection), - ]; - } - - return $components; + return [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + }) + ->filter() + ->values() + ->all(); } - protected function getVoltComponents(): array + protected function getStandardViews(): array { /** @var string|null $viewPath */ $path = config('livewire.view_path'); @@ -120,41 +147,47 @@ protected function getVoltComponents(): array ->implode('.'), ); - $components = []; + $previousClass = null; - foreach ($items as $item) { - // This is ugly, I know, but I don't have better idea how to get - // anonymous classes from Volt components - ob_start(); + return collect($items) + ->map(function ($item) use (&$previousClass) { + // This is ugly, I know, but I don't have better idea how to get + // anonymous classes from Volt components + ob_start(); - try { - require_once $item['path']; - } catch (\Throwable $e) { - continue; - } + try { + require_once $item['path']; + } catch (\Throwable $e) { + return $item; + } - ob_get_clean(); + ob_clean(); - $declaredClasses = get_declared_classes(); - $class = end($declaredClasses); + $declaredClasses = get_declared_classes(); + $class = end($declaredClasses); - if (! \Illuminate\Support\Str::contains($class, '@anonymous')) { - continue; - } + if ($previousClass === $class) { + return $item; + } - $reflection = new \ReflectionClass($class); + $previousClass = $class; - if (! $reflection->isSubclassOf('Livewire\Volt\Component')) { - continue; - } + if (! \Illuminate\Support\Str::contains($class, '@anonymous')) { + return $item; + } - $components[] = [ - ...$item, - 'props' => $this->getComponentProps($reflection), - ]; - } + $reflection = new \ReflectionClass($class); - return $components; + if (! $reflection->isSubclassOf('Livewire\Volt\Component')) { + return $item; + } + + return [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + }) + ->all(); } /** diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index 526d55b9..a0e95284 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -1,11 +1,38 @@ // This file was generated from php-templates/livewire-components.php, do not edit directly export default ` +class LaravelVsCode +{ + public static function relativePath($path) + { + if (!str_contains($path, base_path())) { + return (string) $path; + } + + return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR); + } + + public static function isVendor($path) + { + return str_contains($path, base_path("vendor")); + } + + public static function outputMarker($key) + { + return '__VSCODE_LARAVEL_' . $key . '__'; + } + + public static function startupError(\\Throwable $e) + { + throw new Error(self::outputMarker('STARTUP_ERROR') . ': ' . $e->getMessage()); + } +} + $components = new class { public function all(): array { $components = collect(array_merge( - $this->getStandardComponents(), - $this->getVoltComponents() + $this->getStandardClasses(), + $this->getStandardViews() ))->groupBy('key')->map(fn($items) => [ 'isVendor' => $items->first()['isVendor'], 'paths' => $items->pluck('path')->values(), @@ -53,7 +80,7 @@ $components = new class { return $components; } - protected function getStandardComponents(): array + protected function getStandardClasses(): array { /** @var string|null $classNamespace */ $classNamespace = config('livewire.class_namespace'); @@ -75,35 +102,35 @@ $components = new class { ->implode('.'), ); - $components = []; - - foreach ($items as $item) { - $class = str($item['path']) + return collect($items) + ->map(function ($item) { + $class = str($item['path']) ->replace('.php', '') ->replace('/', '\\\\') ->ucfirst() ->toString(); - if (! class_exists($class)) { - continue; - } + if (! class_exists($class)) { + return null; + } - $reflection = new \\ReflectionClass($class); + $reflection = new \\ReflectionClass($class); - if (! $reflection->isSubclassOf('Livewire\\Component')) { - continue; - } + if (! $reflection->isSubclassOf('Livewire\\Component')) { + return null; + } - $components[] = [ - ...$item, - 'props' => $this->getComponentProps($reflection), - ]; - } - - return $components; + return [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + }) + ->filter() + ->values() + ->all(); } - protected function getVoltComponents(): array + protected function getStandardViews(): array { /** @var string|null $viewPath */ $path = config('livewire.view_path'); @@ -120,41 +147,47 @@ $components = new class { ->implode('.'), ); - $components = []; + $previousClass = null; - foreach ($items as $item) { - // This is ugly, I know, but I don't have better idea how to get - // anonymous classes from Volt components - ob_start(); + return collect($items) + ->map(function ($item) use (&$previousClass) { + // This is ugly, I know, but I don't have better idea how to get + // anonymous classes from Volt components + ob_start(); - try { - require_once $item['path']; - } catch (\\Throwable $e) { - continue; - } + try { + require_once $item['path']; + } catch (\\Throwable $e) { + return $item; + } - ob_get_clean(); + ob_clean(); - $declaredClasses = get_declared_classes(); - $class = end($declaredClasses); + $declaredClasses = get_declared_classes(); + $class = end($declaredClasses); - if (! \\Illuminate\\Support\\Str::contains($class, '@anonymous')) { - continue; - } + if ($previousClass === $class) { + return $item; + } - $reflection = new \\ReflectionClass($class); + $previousClass = $class; - if (! $reflection->isSubclassOf('Livewire\\Volt\\Component')) { - continue; - } + if (! \\Illuminate\\Support\\Str::contains($class, '@anonymous')) { + return $item; + } - $components[] = [ - ...$item, - 'props' => $this->getComponentProps($reflection), - ]; - } + $reflection = new \\ReflectionClass($class); - return $components; + if (! $reflection->isSubclassOf('Livewire\\Volt\\Component')) { + return $item; + } + + return [ + ...$item, + 'props' => $this->getComponentProps($reflection), + ]; + }) + ->all(); } /** From 0d9fc180527460092e26e729996c13992434174b Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Thu, 27 Mar 2025 22:26:43 +0000 Subject: [PATCH 04/15] fix --- php-templates/livewire-components.php | 27 --------------------------- src/templates/livewire-components.ts | 27 --------------------------- 2 files changed, 54 deletions(-) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index 888c5827..9c4abc75 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -1,32 +1,5 @@ getMessage()); - } -} - $components = new class { public function all(): array { diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index a0e95284..84295742 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -1,32 +1,5 @@ // This file was generated from php-templates/livewire-components.php, do not edit directly export default ` -class LaravelVsCode -{ - public static function relativePath($path) - { - if (!str_contains($path, base_path())) { - return (string) $path; - } - - return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR); - } - - public static function isVendor($path) - { - return str_contains($path, base_path("vendor")); - } - - public static function outputMarker($key) - { - return '__VSCODE_LARAVEL_' . $key . '__'; - } - - public static function startupError(\\Throwable $e) - { - throw new Error(self::outputMarker('STARTUP_ERROR') . ': ' . $e->getMessage()); - } -} - $components = new class { public function all(): array { From fd9f181e6ea61f6c92bea2354709d79f725a9380 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 08:06:10 +0000 Subject: [PATCH 05/15] refactoring --- php-templates/livewire-components.php | 33 +++++++++++++++------------ src/templates/livewire-components.ts | 33 +++++++++++++++------------ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index 9c4abc75..f5b966a4 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -6,10 +6,10 @@ public function all(): array $components = collect(array_merge( $this->getStandardClasses(), $this->getStandardViews() - ))->groupBy('key')->map(fn($items) => [ + ))->groupBy('key')->map(fn (\Illuminate\Support\Collection $items) => [ 'isVendor' => $items->first()['isVendor'], 'paths' => $items->pluck('path')->values(), - 'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i), + 'props' => $items->pluck('props')->values()->filter()->flatMap(fn ($i) => $i), ]); return [ @@ -41,7 +41,7 @@ protected function findFiles(string $path, string $extension, \Closure $keyCallb ->ltrim('/\\') ->replace('.' . $extension, '') ->replace(['/', '\\'], '.') - ->pipe(fn(string $str): string => $str); + ->pipe(fn (string $str): string => $str); $components[] = [ "path" => LaravelVsCode::relativePath($realPath), @@ -64,24 +64,24 @@ protected function getStandardClasses(): array $path = str($classNamespace) ->replace('\\', DIRECTORY_SEPARATOR) - ->replace('App', 'app') + ->lcfirst() ->toString(); $items = $this->findFiles( $path, 'php', - fn(\Illuminate\Support\Stringable $key): string => $key->explode('.') - ->map(fn(string $p): string => \Illuminate\Support\Str::kebab($p)) + fn (\Illuminate\Support\Stringable $key): string => $key->explode('.') + ->map(fn (string $p): string => \Illuminate\Support\Str::kebab($p)) ->implode('.'), ); return collect($items) ->map(function ($item) { $class = str($item['path']) - ->replace('.php', '') - ->replace('/', '\\') - ->ucfirst() - ->toString(); + ->replace('.php', '') + ->replace(DIRECTORY_SEPARATOR, '\\') + ->ucfirst() + ->toString(); if (! class_exists($class)) { return null; @@ -115,7 +115,7 @@ protected function getStandardViews(): array $items = $this->findFiles( $path, 'blade.php', - fn(\Illuminate\Support\Stringable $key): string => $key->explode('.') + fn (\Illuminate\Support\Stringable $key): string => $key->explode('.') ->map(fn(string $p): string => \Illuminate\Support\Str::kebab($p)) ->implode('.'), ); @@ -177,14 +177,15 @@ protected function getComponentProps(ReflectionClass $reflection): array $mountMethods = array_filter( $methods, - fn (\ReflectionMethod $method): bool => strpos($method->getName(), 'mount') === 0 + fn (\ReflectionMethod $method): bool => + \Illuminate\Support\Str::startsWith($method->getName(), 'mount') ); foreach ($mountMethods as $method) { $parameters = $method->getParameters(); $parameters = collect($parameters) - ->map(fn(\ReflectionParameter $p) => [ + ->map(fn (\ReflectionParameter $p): array => [ 'name' => \Illuminate\Support\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), 'default' => $p->isOptional() ? $p->getDefaultValue() : null @@ -197,8 +198,10 @@ protected function getComponentProps(ReflectionClass $reflection): array // Then we need to get the public properties $properties = collect($reflection->getProperties()) - ->filter(fn(\ReflectionProperty $p) => $p->isPublic() && $p->getDeclaringClass()->getName() === $reflection->getName()) - ->map(fn(\ReflectionProperty $p) => [ + ->filter(fn (\ReflectionProperty $p): bool => + $p->isPublic() && $p->getDeclaringClass()->getName() === $reflection->getName() + ) + ->map(fn (\ReflectionProperty $p): array => [ 'name' => \Illuminate\Support\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), 'default' => $p->getDefaultValue() diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index 84295742..d6849036 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -6,10 +6,10 @@ $components = new class { $components = collect(array_merge( $this->getStandardClasses(), $this->getStandardViews() - ))->groupBy('key')->map(fn($items) => [ + ))->groupBy('key')->map(fn (\\Illuminate\\Support\\Collection $items) => [ 'isVendor' => $items->first()['isVendor'], 'paths' => $items->pluck('path')->values(), - 'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i), + 'props' => $items->pluck('props')->values()->filter()->flatMap(fn ($i) => $i), ]); return [ @@ -41,7 +41,7 @@ $components = new class { ->ltrim('/\\\\') ->replace('.' . $extension, '') ->replace(['/', '\\\\'], '.') - ->pipe(fn(string $str): string => $str); + ->pipe(fn (string $str): string => $str); $components[] = [ "path" => LaravelVsCode::relativePath($realPath), @@ -64,24 +64,24 @@ $components = new class { $path = str($classNamespace) ->replace('\\\\', DIRECTORY_SEPARATOR) - ->replace('App', 'app') + ->lcfirst() ->toString(); $items = $this->findFiles( $path, 'php', - fn(\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') - ->map(fn(string $p): string => \\Illuminate\\Support\\Str::kebab($p)) + fn (\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') + ->map(fn (string $p): string => \\Illuminate\\Support\\Str::kebab($p)) ->implode('.'), ); return collect($items) ->map(function ($item) { $class = str($item['path']) - ->replace('.php', '') - ->replace('/', '\\\\') - ->ucfirst() - ->toString(); + ->replace('.php', '') + ->replace(DIRECTORY_SEPARATOR, '\\\\') + ->ucfirst() + ->toString(); if (! class_exists($class)) { return null; @@ -115,7 +115,7 @@ $components = new class { $items = $this->findFiles( $path, 'blade.php', - fn(\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') + fn (\\Illuminate\\Support\\Stringable $key): string => $key->explode('.') ->map(fn(string $p): string => \\Illuminate\\Support\\Str::kebab($p)) ->implode('.'), ); @@ -177,14 +177,15 @@ $components = new class { $mountMethods = array_filter( $methods, - fn (\\ReflectionMethod $method): bool => strpos($method->getName(), 'mount') === 0 + fn (\\ReflectionMethod $method): bool => + \\Illuminate\\Support\\Str::startsWith($method->getName(), 'mount') ); foreach ($mountMethods as $method) { $parameters = $method->getParameters(); $parameters = collect($parameters) - ->map(fn(\\ReflectionParameter $p) => [ + ->map(fn (\\ReflectionParameter $p): array => [ 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), 'default' => $p->isOptional() ? $p->getDefaultValue() : null @@ -197,8 +198,10 @@ $components = new class { // Then we need to get the public properties $properties = collect($reflection->getProperties()) - ->filter(fn(\\ReflectionProperty $p) => $p->isPublic() && $p->getDeclaringClass()->getName() === $reflection->getName()) - ->map(fn(\\ReflectionProperty $p) => [ + ->filter(fn (\\ReflectionProperty $p): bool => + $p->isPublic() && $p->getDeclaringClass()->getName() === $reflection->getName() + ) + ->map(fn (\\ReflectionProperty $p): array => [ 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), 'default' => $p->getDefaultValue() From ced4cb6b183591ba2641ec79d13776e479626c31 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 08:39:10 +0000 Subject: [PATCH 06/15] add isOptional to method parameters --- php-templates/livewire-components.php | 3 +++ src/templates/livewire-components.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index f5b966a4..520b4894 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -188,6 +188,9 @@ protected function getComponentProps(ReflectionClass $reflection): array ->map(fn (\ReflectionParameter $p): array => [ 'name' => \Illuminate\Support\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), + // We need to add isOptional, because null can be also a default value, + // it can't be a flag of no default + 'isOptional' => $p->isOptional(), 'default' => $p->isOptional() ? $p->getDefaultValue() : null ]) ->all(); diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index d6849036..48760287 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -188,6 +188,9 @@ $components = new class { ->map(fn (\\ReflectionParameter $p): array => [ 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), + // We need to add isOptional, because null can be also a default value, + // it can't be a flag of no default + 'isOptional' => $p->isOptional(), 'default' => $p->isOptional() ? $p->getDefaultValue() : null ]) ->all(); From 28f626d8b9e4b91de35da6ce0ba4de9f09c3eb2f Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 08:42:00 +0000 Subject: [PATCH 07/15] rename isOptional to hasDefault --- php-templates/livewire-components.php | 5 +++-- src/templates/livewire-components.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index 520b4894..bfe8de78 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -188,9 +188,9 @@ protected function getComponentProps(ReflectionClass $reflection): array ->map(fn (\ReflectionParameter $p): array => [ 'name' => \Illuminate\Support\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), - // We need to add isOptional, because null can be also a default value, + // We need to add hasDefault, because null can be also a default value, // it can't be a flag of no default - 'isOptional' => $p->isOptional(), + 'hasDefault' => $p->isDefaultValueAvailable(), 'default' => $p->isOptional() ? $p->getDefaultValue() : null ]) ->all(); @@ -207,6 +207,7 @@ protected function getComponentProps(ReflectionClass $reflection): array ->map(fn (\ReflectionProperty $p): array => [ 'name' => \Illuminate\Support\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), + 'hasDefault' => $p->hasDefaultValue(), 'default' => $p->getDefaultValue() ]) ->all(); diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index 48760287..9cffa42d 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -190,7 +190,7 @@ $components = new class { 'type' => (string) ($p->getType() ?? 'mixed'), // We need to add isOptional, because null can be also a default value, // it can't be a flag of no default - 'isOptional' => $p->isOptional(), + 'hasDefault' => $p->isDefaultValueAvailable(), 'default' => $p->isOptional() ? $p->getDefaultValue() : null ]) ->all(); @@ -207,6 +207,7 @@ $components = new class { ->map(fn (\\ReflectionProperty $p): array => [ 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), + 'hasDefault' => $p->hasDefaultValue(), 'default' => $p->getDefaultValue() ]) ->all(); From 830ef90e28ffd19a2279ced8dafe2cf3f8ddd454 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 08:42:22 +0000 Subject: [PATCH 08/15] rename isOptional to hasDefault --- src/templates/livewire-components.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index 9cffa42d..a6ed3dc8 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -188,7 +188,7 @@ $components = new class { ->map(fn (\\ReflectionParameter $p): array => [ 'name' => \\Illuminate\\Support\\Str::kebab($p->getName()), 'type' => (string) ($p->getType() ?? 'mixed'), - // We need to add isOptional, because null can be also a default value, + // We need to add hasDefault, because null can be also a default value, // it can't be a flag of no default 'hasDefault' => $p->isDefaultValueAvailable(), 'default' => $p->isOptional() ? $p->getDefaultValue() : null From 0ef652444c940e0f5f9b12575f023d8bce830c80 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 09:20:41 +0000 Subject: [PATCH 09/15] fix docblock --- php-templates/livewire-components.php | 2 +- src/templates/livewire-components.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/php-templates/livewire-components.php b/php-templates/livewire-components.php index bfe8de78..16230fcd 100644 --- a/php-templates/livewire-components.php +++ b/php-templates/livewire-components.php @@ -164,7 +164,7 @@ protected function getStandardViews(): array } /** - * @return array + * @return array */ protected function getComponentProps(ReflectionClass $reflection): array { diff --git a/src/templates/livewire-components.ts b/src/templates/livewire-components.ts index a6ed3dc8..f708577b 100644 --- a/src/templates/livewire-components.ts +++ b/src/templates/livewire-components.ts @@ -164,7 +164,7 @@ $components = new class { } /** - * @return array + * @return array */ protected function getComponentProps(ReflectionClass $reflection): array { From 574cdf74d79665a75f2aea81a4f185dd26ec3e73 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 11:05:42 +0000 Subject: [PATCH 10/15] remove project path --- src/repositories/livewireComponents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/livewireComponents.ts b/src/repositories/livewireComponents.ts index 4aaa9cc7..00295458 100644 --- a/src/repositories/livewireComponents.ts +++ b/src/repositories/livewireComponents.ts @@ -24,7 +24,7 @@ export interface LivewireComponents { const load = () => { getConfigs().whenLoaded(() => { livewirePaths = [ - projectPath(lcfirst(getConfigByName('livewire.class_namespace')?.value?.replace('\\', '/') ?? 'app/Livewire')), + lcfirst(getConfigByName('livewire.class_namespace')?.value?.replace('\\', '/') ?? 'app/Livewire'), getConfigByName('livewire.view_path')?.value ?? 'resources/views/livewire' ]; }); From c566ee5c88b3ee97b44967f0e402066efb83e8d5 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Fri, 28 Mar 2025 11:10:24 +0000 Subject: [PATCH 11/15] Add a repository for Livewire components Fixes N1ebieski/vs-code-extension#29 --- src/repositories/livewireComponents.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/repositories/livewireComponents.ts b/src/repositories/livewireComponents.ts index 00295458..7e57f1a0 100644 --- a/src/repositories/livewireComponents.ts +++ b/src/repositories/livewireComponents.ts @@ -1,5 +1,5 @@ import { runInLaravel, template } from "@src/support/php"; -import { projectPath } from "@src/support/project"; +import { relativePath } from "@src/support/project"; import { lcfirst } from "@src/support/str"; import { waitForValue } from "@src/support/util"; import { repository } from "."; @@ -15,6 +15,7 @@ export interface LivewireComponents { props: { name: string; type: string; + hasDefault: boolean; default: string | null; }[]; }; @@ -24,8 +25,8 @@ export interface LivewireComponents { const load = () => { getConfigs().whenLoaded(() => { livewirePaths = [ - lcfirst(getConfigByName('livewire.class_namespace')?.value?.replace('\\', '/') ?? 'app/Livewire'), - getConfigByName('livewire.view_path')?.value ?? 'resources/views/livewire' + lcfirst(getConfigByName('livewire.class_namespace')?.value?.replace(/\\/g, '/') ?? 'app/Livewire'), + relativePath(getConfigByName('livewire.view_path')?.value ?? 'resources/views/livewire') ]; }); From 2cd96eca5435b666c2e7fd5e2f5843cb69f0b109 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 12 Apr 2025 08:17:02 +0000 Subject: [PATCH 12/15] Support for autocompletion component attributes Fixes N1ebieski/vs-code-extension#35 --- generatable.json | 6 ++-- package.json | 20 +++++++++--- src/extension.ts | 20 ++++++++++-- src/features/bladeComponent.ts | 53 +++++++++++++++++++++++++++++-- src/features/livewireComponent.ts | 48 ++++++++++++++++++++++++++-- src/support/generated-config.ts | 2 +- 6 files changed, 136 insertions(+), 13 deletions(-) diff --git a/generatable.json b/generatable.json index fe1a9b6f..44bd7088 100644 --- a/generatable.json +++ b/generatable.json @@ -49,7 +49,8 @@ "label": "Blade components", "features": [ "link", - "completion", + "completion_component", + "completion_attribute", "hover" ] }, @@ -58,7 +59,8 @@ "label": "Livewire components", "features": [ "link", - "completion" + "completion_component", + "completion_attribute" ] }, { diff --git a/package.json b/package.json index d021e5d4..4f6bc77f 100644 --- a/package.json +++ b/package.json @@ -206,11 +206,17 @@ "generated": true, "description": "Enable linking for Blade components." }, - "Laravel.bladeComponent.completion": { + "Laravel.bladeComponent.completion_component": { "type": "boolean", "default": true, "generated": true, - "description": "Enable completion for Blade components." + "description": null + }, + "Laravel.bladeComponent.completion_attribute": { + "type": "boolean", + "default": true, + "generated": true, + "description": null }, "Laravel.bladeComponent.hover": { "type": "boolean", @@ -320,11 +326,17 @@ "generated": true, "description": "Enable linking for Livewire components." }, - "Laravel.livewireComponent.completion": { + "Laravel.livewireComponent.completion_component": { + "type": "boolean", + "default": true, + "generated": true, + "description": null + }, + "Laravel.livewireComponent.completion_attribute": { "type": "boolean", "default": true, "generated": true, - "description": "Enable completion for Livewire components." + "description": null }, "Laravel.middleware.diagnostics": { "type": "boolean", diff --git a/src/extension.ts b/src/extension.ts index 9be7f982..0838779e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,9 +14,15 @@ import EloquentCompletion from "./completion/Eloquent"; import Registry from "./completion/Registry"; import ValidationCompletion from "./completion/Validation"; import { updateDiagnostics } from "./diagnostic/diagnostic"; -import { completionProvider as bladeComponentCompletion } from "./features/bladeComponent"; +import { + completionAttributeProvider as bladeComponentAttributeCompletion, + completionComponentProvider as bladeComponentCompletion +} from "./features/bladeComponent"; import { viteEnvCodeActionProvider } from "./features/env"; -import { completionProvider as livewireComponentCompletion } from "./features/livewireComponent"; +import { + completionAttributeProvider as livewireComponentAttributeCompletion, + completionComponentProvider as livewireComponentCompletion +} from "./features/livewireComponent"; import { hoverProviders } from "./hover/HoverProvider"; import { linkProviders } from "./link/LinkProvider"; import { configAffected } from "./support/config"; @@ -129,11 +135,21 @@ export function activate(context: vscode.ExtensionContext) { "x", "-", ), + vscode.languages.registerCompletionItemProvider( + BLADE_LANGUAGES, + bladeComponentAttributeCompletion, + ":", + ), vscode.languages.registerCompletionItemProvider( BLADE_LANGUAGES, livewireComponentCompletion, ":", ), + vscode.languages.registerCompletionItemProvider( + BLADE_LANGUAGES, + livewireComponentAttributeCompletion, + ":", + ), vscode.languages.registerCompletionItemProvider( BLADE_LANGUAGES, new BladeCompletion(), diff --git a/src/features/bladeComponent.ts b/src/features/bladeComponent.ts index cefe279a..4e0223a8 100644 --- a/src/features/bladeComponent.ts +++ b/src/features/bladeComponent.ts @@ -53,12 +53,61 @@ export const linkProvider: LinkProvider = (doc: vscode.TextDocument) => { return Promise.resolve(links); }; -export const completionProvider: vscode.CompletionItemProvider = { +export const completionAttributeProvider: vscode.CompletionItemProvider = { provideCompletionItems( doc: vscode.TextDocument, pos: vscode.Position, ): vscode.ProviderResult { - if (!config("bladeComponent.completion", true)) { + if (!config("bladeComponent.completion_attribute", true)) { + return undefined; + } + + const components = getBladeComponents().items; + const text = doc.getText(new vscode.Range(new vscode.Position(0, 0), pos)); + + const regexes = [new RegExp(/]+)[^>]*:$/)]; + + if (components.prefixes.length > 0) { + regexes.push( + new RegExp(`<((${components.prefixes.join("|")})\\:[^\\s>]+)[^>]*:$`), + ); + } + + for (const regex of regexes) { + const match = text.match(regex); + + if (!match || match.index === undefined) { + continue; + } + + const component = components.components[match[1]]; + + if (!component) { + return undefined; + } + + return Object.entries(component.props).map(([, value]) => { + let completeItem = new vscode.CompletionItem( + value.name, + vscode.CompletionItemKind.Property, + ); + + completeItem.detail = value.type; + + return completeItem; + }); + } + + return undefined; + } +}; + +export const completionComponentProvider: vscode.CompletionItemProvider = { + provideCompletionItems( + doc: vscode.TextDocument, + pos: vscode.Position, + ): vscode.ProviderResult { + if (!config("bladeComponent.completion_component", true)) { return undefined; } diff --git a/src/features/livewireComponent.ts b/src/features/livewireComponent.ts index f05dcf6b..441a8270 100644 --- a/src/features/livewireComponent.ts +++ b/src/features/livewireComponent.ts @@ -1,3 +1,4 @@ +import { getLivewireComponents } from "@src/repositories/livewireComponents"; import { getViews } from "@src/repositories/views"; import { config } from "@src/support/config"; import { projectPath } from "@src/support/project"; @@ -40,12 +41,55 @@ export const linkProvider: LinkProvider = (doc: vscode.TextDocument) => { return Promise.resolve(links); }; -export const completionProvider: vscode.CompletionItemProvider = { +export const completionAttributeProvider: vscode.CompletionItemProvider = { provideCompletionItems( doc: vscode.TextDocument, pos: vscode.Position, ): vscode.ProviderResult { - if (!config("livewireComponent.completion", true)) { + if (!config("livewireComponent.completion_attribute", true)) { + return undefined; + } + + const components = getLivewireComponents().items; + const text = doc.getText(new vscode.Range(new vscode.Position(0, 0), pos)); + + const regexes = [new RegExp(/]+)[^>]*:$/)]; + + for (const regex of regexes) { + const match = text.match(regex); + + if (!match || match.index === undefined) { + continue; + } + + const component = components.components[match[1]]; + + if (!component) { + return undefined; + } + + return Object.entries(component.props).map(([, value]) => { + let completeItem = new vscode.CompletionItem( + value.name, + vscode.CompletionItemKind.Property, + ); + + completeItem.detail = value.type; + + return completeItem; + }); + } + + return undefined; + } +}; + +export const completionComponentProvider: vscode.CompletionItemProvider = { + provideCompletionItems( + doc: vscode.TextDocument, + pos: vscode.Position, + ): vscode.ProviderResult { + if (!config("livewireComponent.completion_component", true)) { return undefined; } diff --git a/src/support/generated-config.ts b/src/support/generated-config.ts index f5758a9d..204557a3 100644 --- a/src/support/generated-config.ts +++ b/src/support/generated-config.ts @@ -1 +1 @@ -export type GeneratedConfigKey = 'appBinding.diagnostics' | 'appBinding.hover' | 'appBinding.link' | 'appBinding.completion' | 'asset.diagnostics' | 'asset.hover' | 'asset.link' | 'asset.completion' | 'auth.diagnostics' | 'auth.hover' | 'auth.link' | 'auth.completion' | 'bladeComponent.link' | 'bladeComponent.completion' | 'bladeComponent.hover' | 'config.diagnostics' | 'config.hover' | 'config.link' | 'config.completion' | 'controllerAction.diagnostics' | 'controllerAction.hover' | 'controllerAction.link' | 'controllerAction.completion' | 'env.diagnostics' | 'env.hover' | 'env.link' | 'env.completion' | 'inertia.diagnostics' | 'inertia.hover' | 'inertia.link' | 'inertia.completion' | 'livewireComponent.link' | 'livewireComponent.completion' | 'middleware.diagnostics' | 'middleware.hover' | 'middleware.link' | 'middleware.completion' | 'mix.diagnostics' | 'mix.hover' | 'mix.link' | 'mix.completion' | 'paths.link' | 'route.diagnostics' | 'route.hover' | 'route.link' | 'route.completion' | 'storage.link' | 'storage.completion' | 'storage.diagnostics' | 'translation.diagnostics' | 'translation.hover' | 'translation.link' | 'translation.completion' | 'view.diagnostics' | 'view.hover' | 'view.link' | 'view.completion'; +export type GeneratedConfigKey = 'appBinding.diagnostics' | 'appBinding.hover' | 'appBinding.link' | 'appBinding.completion' | 'asset.diagnostics' | 'asset.hover' | 'asset.link' | 'asset.completion' | 'auth.diagnostics' | 'auth.hover' | 'auth.link' | 'auth.completion' | 'bladeComponent.link' | 'bladeComponent.completion_component' | 'bladeComponent.completion_attribute' | 'bladeComponent.hover' | 'config.diagnostics' | 'config.hover' | 'config.link' | 'config.completion' | 'controllerAction.diagnostics' | 'controllerAction.hover' | 'controllerAction.link' | 'controllerAction.completion' | 'env.diagnostics' | 'env.hover' | 'env.link' | 'env.completion' | 'inertia.diagnostics' | 'inertia.hover' | 'inertia.link' | 'inertia.completion' | 'livewireComponent.link' | 'livewireComponent.completion_component' | 'livewireComponent.completion_attribute' | 'middleware.diagnostics' | 'middleware.hover' | 'middleware.link' | 'middleware.completion' | 'mix.diagnostics' | 'mix.hover' | 'mix.link' | 'mix.completion' | 'paths.link' | 'route.diagnostics' | 'route.hover' | 'route.link' | 'route.completion' | 'storage.link' | 'storage.completion' | 'storage.diagnostics' | 'translation.diagnostics' | 'translation.hover' | 'translation.link' | 'translation.completion' | 'view.diagnostics' | 'view.hover' | 'view.link' | 'view.completion'; From ab457bfc70b379e3dfd792544e1ac0e2eb94f6bc Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 12 Apr 2025 08:29:21 +0000 Subject: [PATCH 13/15] Support for autocompletion component attributes Fixes N1ebieski/vs-code-extension#35 --- src/features/bladeComponent.ts | 4 ++-- src/features/livewireComponent.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/bladeComponent.ts b/src/features/bladeComponent.ts index 4e0223a8..ff232182 100644 --- a/src/features/bladeComponent.ts +++ b/src/features/bladeComponent.ts @@ -65,11 +65,11 @@ export const completionAttributeProvider: vscode.CompletionItemProvider = { const components = getBladeComponents().items; const text = doc.getText(new vscode.Range(new vscode.Position(0, 0), pos)); - const regexes = [new RegExp(/]+)[^>]*:$/)]; + const regexes = [new RegExp(/]+)[^<]*:$/)]; if (components.prefixes.length > 0) { regexes.push( - new RegExp(`<((${components.prefixes.join("|")})\\:[^\\s>]+)[^>]*:$`), + new RegExp(`<((${components.prefixes.join("|")})\\:[^\\s>]+)[^<]*:$`), ); } diff --git a/src/features/livewireComponent.ts b/src/features/livewireComponent.ts index 441a8270..5e191b13 100644 --- a/src/features/livewireComponent.ts +++ b/src/features/livewireComponent.ts @@ -53,7 +53,7 @@ export const completionAttributeProvider: vscode.CompletionItemProvider = { const components = getLivewireComponents().items; const text = doc.getText(new vscode.Range(new vscode.Position(0, 0), pos)); - const regexes = [new RegExp(/]+)[^>]*:$/)]; + const regexes = [new RegExp(/]+)[^<]*:$/)]; for (const regex of regexes) { const match = text.match(regex); From ab6efa92576e5cc48ac9a7efa7021b4b183bb441 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 12 Apr 2025 09:00:56 +0000 Subject: [PATCH 14/15] refactoring config names --- generatable.json | 4 ++-- generate-config.php | 1 + package.json | 12 ++++++------ src/features/bladeComponent.ts | 2 +- src/features/livewireComponent.ts | 2 +- src/support/generated-config.ts | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/generatable.json b/generatable.json index 44bd7088..3f55ff96 100644 --- a/generatable.json +++ b/generatable.json @@ -49,7 +49,7 @@ "label": "Blade components", "features": [ "link", - "completion_component", + "completion", "completion_attribute", "hover" ] @@ -59,7 +59,7 @@ "label": "Livewire components", "features": [ "link", - "completion_component", + "completion", "completion_attribute" ] }, diff --git a/generate-config.php b/generate-config.php index b87ce7cf..073e05df 100644 --- a/generate-config.php +++ b/generate-config.php @@ -24,6 +24,7 @@ 'hover' => "Enable hover information for {$label}.", 'link' => "Enable linking for {$label}.", 'completion' => "Enable completion for {$label}.", + 'completion_attribute' => "Enable completion for {$label} attributes.", default => null, }, ]; diff --git a/package.json b/package.json index 4f6bc77f..3268bc7d 100644 --- a/package.json +++ b/package.json @@ -206,17 +206,17 @@ "generated": true, "description": "Enable linking for Blade components." }, - "Laravel.bladeComponent.completion_component": { + "Laravel.bladeComponent.completion": { "type": "boolean", "default": true, "generated": true, - "description": null + "description": "Enable completion for Blade components." }, "Laravel.bladeComponent.completion_attribute": { "type": "boolean", "default": true, "generated": true, - "description": null + "description": "Enable completion for Blade components attributes." }, "Laravel.bladeComponent.hover": { "type": "boolean", @@ -326,17 +326,17 @@ "generated": true, "description": "Enable linking for Livewire components." }, - "Laravel.livewireComponent.completion_component": { + "Laravel.livewireComponent.completion": { "type": "boolean", "default": true, "generated": true, - "description": null + "description": "Enable completion for Livewire components." }, "Laravel.livewireComponent.completion_attribute": { "type": "boolean", "default": true, "generated": true, - "description": null + "description": "Enable completion for Livewire components attributes." }, "Laravel.middleware.diagnostics": { "type": "boolean", diff --git a/src/features/bladeComponent.ts b/src/features/bladeComponent.ts index ff232182..573ab56e 100644 --- a/src/features/bladeComponent.ts +++ b/src/features/bladeComponent.ts @@ -107,7 +107,7 @@ export const completionComponentProvider: vscode.CompletionItemProvider = { doc: vscode.TextDocument, pos: vscode.Position, ): vscode.ProviderResult { - if (!config("bladeComponent.completion_component", true)) { + if (!config("bladeComponent.completion", true)) { return undefined; } diff --git a/src/features/livewireComponent.ts b/src/features/livewireComponent.ts index 5e191b13..a213daa6 100644 --- a/src/features/livewireComponent.ts +++ b/src/features/livewireComponent.ts @@ -89,7 +89,7 @@ export const completionComponentProvider: vscode.CompletionItemProvider = { doc: vscode.TextDocument, pos: vscode.Position, ): vscode.ProviderResult { - if (!config("livewireComponent.completion_component", true)) { + if (!config("livewireComponent.completion", true)) { return undefined; } diff --git a/src/support/generated-config.ts b/src/support/generated-config.ts index 204557a3..115d5fba 100644 --- a/src/support/generated-config.ts +++ b/src/support/generated-config.ts @@ -1 +1 @@ -export type GeneratedConfigKey = 'appBinding.diagnostics' | 'appBinding.hover' | 'appBinding.link' | 'appBinding.completion' | 'asset.diagnostics' | 'asset.hover' | 'asset.link' | 'asset.completion' | 'auth.diagnostics' | 'auth.hover' | 'auth.link' | 'auth.completion' | 'bladeComponent.link' | 'bladeComponent.completion_component' | 'bladeComponent.completion_attribute' | 'bladeComponent.hover' | 'config.diagnostics' | 'config.hover' | 'config.link' | 'config.completion' | 'controllerAction.diagnostics' | 'controllerAction.hover' | 'controllerAction.link' | 'controllerAction.completion' | 'env.diagnostics' | 'env.hover' | 'env.link' | 'env.completion' | 'inertia.diagnostics' | 'inertia.hover' | 'inertia.link' | 'inertia.completion' | 'livewireComponent.link' | 'livewireComponent.completion_component' | 'livewireComponent.completion_attribute' | 'middleware.diagnostics' | 'middleware.hover' | 'middleware.link' | 'middleware.completion' | 'mix.diagnostics' | 'mix.hover' | 'mix.link' | 'mix.completion' | 'paths.link' | 'route.diagnostics' | 'route.hover' | 'route.link' | 'route.completion' | 'storage.link' | 'storage.completion' | 'storage.diagnostics' | 'translation.diagnostics' | 'translation.hover' | 'translation.link' | 'translation.completion' | 'view.diagnostics' | 'view.hover' | 'view.link' | 'view.completion'; +export type GeneratedConfigKey = 'appBinding.diagnostics' | 'appBinding.hover' | 'appBinding.link' | 'appBinding.completion' | 'asset.diagnostics' | 'asset.hover' | 'asset.link' | 'asset.completion' | 'auth.diagnostics' | 'auth.hover' | 'auth.link' | 'auth.completion' | 'bladeComponent.link' | 'bladeComponent.completion' | 'bladeComponent.completion_attribute' | 'bladeComponent.hover' | 'config.diagnostics' | 'config.hover' | 'config.link' | 'config.completion' | 'controllerAction.diagnostics' | 'controllerAction.hover' | 'controllerAction.link' | 'controllerAction.completion' | 'env.diagnostics' | 'env.hover' | 'env.link' | 'env.completion' | 'inertia.diagnostics' | 'inertia.hover' | 'inertia.link' | 'inertia.completion' | 'livewireComponent.link' | 'livewireComponent.completion' | 'livewireComponent.completion_attribute' | 'middleware.diagnostics' | 'middleware.hover' | 'middleware.link' | 'middleware.completion' | 'mix.diagnostics' | 'mix.hover' | 'mix.link' | 'mix.completion' | 'paths.link' | 'route.diagnostics' | 'route.hover' | 'route.link' | 'route.completion' | 'storage.link' | 'storage.completion' | 'storage.diagnostics' | 'translation.diagnostics' | 'translation.hover' | 'translation.link' | 'translation.completion' | 'view.diagnostics' | 'view.hover' | 'view.link' | 'view.completion'; From 022a4ba3bb43cd9a5eec10256a76375e2da3d057 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Mon, 14 Apr 2025 22:05:32 +0000 Subject: [PATCH 15/15] refactoring --- src/features/livewireComponent.ts | 38 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/features/livewireComponent.ts b/src/features/livewireComponent.ts index a213daa6..03d0ce61 100644 --- a/src/features/livewireComponent.ts +++ b/src/features/livewireComponent.ts @@ -53,34 +53,30 @@ export const completionAttributeProvider: vscode.CompletionItemProvider = { const components = getLivewireComponents().items; const text = doc.getText(new vscode.Range(new vscode.Position(0, 0), pos)); - const regexes = [new RegExp(/]+)[^<]*:$/)]; + const regex = new RegExp(/]+)[^<]*:$/); - for (const regex of regexes) { - const match = text.match(regex); + const match = text.match(regex); - if (!match || match.index === undefined) { - continue; - } - - const component = components.components[match[1]]; + if (!match || match.index === undefined) { + return undefined; + } - if (!component) { - return undefined; - } + const component = components.components[match[1]]; - return Object.entries(component.props).map(([, value]) => { - let completeItem = new vscode.CompletionItem( - value.name, - vscode.CompletionItemKind.Property, - ); + if (!component) { + return undefined; + } - completeItem.detail = value.type; + return Object.entries(component.props).map(([, value]) => { + let completeItem = new vscode.CompletionItem( + value.name, + vscode.CompletionItemKind.Property, + ); - return completeItem; - }); - } + completeItem.detail = value.type; - return undefined; + return completeItem; + }); } };