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
84 changes: 76 additions & 8 deletions php-templates/views.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

use Illuminate\Support\Facades\File;
use Illuminate\Support\Stringable;

$blade = new class {
public function getAllViews()
{
Expand All @@ -17,6 +20,16 @@ public function getAllViews()

[$local, $vendor] = $paths
->merge($hints)
->map(function (array $data) {
/** @var array{path: string, key: Stringable|string} $data */
$path = $data["path"];
$key = $data["key"] instanceof Stringable ? $data["key"]->toString() : $data["key"];

$data["isLivewire"] = $this->isLivewire($path, $key);
$data["isMfc"] = $data["isLivewire"] && $this->isMfc($path);

return $data;
})
->values()
->partition(fn($v) => !$v["isVendor"]);

Expand Down Expand Up @@ -60,24 +73,79 @@ public function getAllComponents()

foreach ($files as $file) {
$realPath = $file->getRealPath();
$path = str_replace(base_path(DIRECTORY_SEPARATOR), '', $realPath);
$key = str($realPath)
->replace(realpath($path), "")
->replace(".php", "")
->ltrim(DIRECTORY_SEPARATOR)
->replace(DIRECTORY_SEPARATOR, ".")
->kebab()
->prepend($key . "::");
$isLivewire = $this->isLivewire($path, $key->toString());

$components[] = [
"path" => str_replace(base_path(DIRECTORY_SEPARATOR), '', $realPath),
"path" => $path,
"isLivewire" => $isLivewire,
"isMfc" => $isLivewire && $this->isMfc($path),
"isVendor" => str_contains($realPath, base_path("vendor")),
"key" => str($realPath)
->replace(realpath($path), "")
->replace(".php", "")
->ltrim(DIRECTORY_SEPARATOR)
->replace(DIRECTORY_SEPARATOR, ".")
->kebab()
->prepend($key . "::"),
"key" => $key,
];
}
}

return $components;
}

protected function isMfc(string $path): bool
{
$directoryPath = base_path(dirname($path));

$folderName = str(basename($directoryPath))
->replace("⚡", "")
->toString();
$fileName = str(basename($path))
->replace("⚡", "")
->before(".")
->toString();

if ($folderName !== $fileName) {
return false;
}

$componentPath = $directoryPath . '/' . $fileName . '.php';

return File::exists($componentPath);
}

protected function isLivewire(string $path, string $key): bool
{
if (str_contains($key, "::")) {
/** @var array<int, string> */
$componentNamespaces = array_keys(config("livewire.component_namespaces", []));

[$prefix,] = explode("::", $key);

if (in_array($prefix, $componentNamespaces)) {
return true;
}
}

/** @var array<int, string> */
$componentLocations = array_map(
fn (string $path) => LaravelVsCode::relativePath($path),
// Backward compatibility for Livewire 3
config("livewire.component_locations", [config("livewire.view_path", resource_path('views/livewire'))]),
);

foreach ($componentLocations as $componentLocation) {
if (str_starts_with($path, $componentLocation)) {
return true;
}
}

return false;
}

protected function findViews($path)
{
$paths = [];
Expand Down
29 changes: 17 additions & 12 deletions src/features/livewireComponent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { getViews } from "@src/repositories/views";
import {
getLivewireViewItems,
getViewItemByKey,
} from "@src/repositories/views";
import { config } from "@src/support/config";
import { projectPath } from "@src/support/project";
import * as vscode from "vscode";
Expand All @@ -8,17 +11,17 @@ export const linkProvider: LinkProvider = (doc: vscode.TextDocument) => {
const links: vscode.DocumentLink[] = [];
const text = doc.getText();
const lines = text.split("\n");
const views = getViews().items;

lines.forEach((line, index) => {
const match = line.match(/<\/?livewire:([^\s>]+)/);

if (match && match.index !== undefined) {
const componentName = match[1];
// Standard component
const viewName = `livewire.${componentName}`;
// Index component
const view = views.find((v) => v.key === viewName);

// Livewire 3 needs "livewire." prefix, but Livewire 4 doesn't
const view = [componentName, `livewire.${componentName}`]
.map((key) => getViewItemByKey(key))
.find((value) => value !== undefined);

if (view) {
links.push(
Expand Down Expand Up @@ -61,11 +64,13 @@ export const completionProvider: vscode.CompletionItemProvider = {
return undefined;
}

return getViews()
.items.filter((view) => view.key.startsWith(pathPrefix))
.map(
(view) =>
new vscode.CompletionItem(view.key.replace(pathPrefix, "")),
);
return [
// If we remove pathPrefix, we have to remove duplicates
...new Set(
getLivewireViewItems().map((view) =>
view.key.replace(pathPrefix, "").replaceAll("⚡", ""),
),
),
].map((key) => new vscode.CompletionItem(key));
},
};
29 changes: 18 additions & 11 deletions src/features/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
LinkProvider,
} from "@src/index";
import AutocompleteResult from "@src/parser/AutocompleteResult";
import { getViews, ViewItem } from "@src/repositories/views";
import {
getLivewireViewItems,
getViewItemByKey,
getViews,
ViewItem,
} from "@src/repositories/views";
import { config } from "@src/support/config";
import { findHoverMatchesInDoc } from "@src/support/doc";
import { detectedRange, detectInDoc } from "@src/support/parser";
Expand Down Expand Up @@ -43,7 +48,7 @@ const toFind: FeatureTag = [
},
{
class: facade("Route"),
method: ["view"],
method: ["view", "livewire"],
argumentIndex: 1,
},
{
Expand Down Expand Up @@ -105,9 +110,7 @@ export const linkProvider: LinkProvider = (doc: vscode.TextDocument) => {
return null;
}

const path = getViews().items.find(
(view) => view.key === param.value,
)?.path;
const path = getViewItemByKey(param.value)?.path;

if (!path) {
return null;
Expand All @@ -126,15 +129,15 @@ export const hoverProvider: HoverProvider = (
pos: vscode.Position,
): vscode.ProviderResult<vscode.Hover> => {
return findHoverMatchesInDoc(doc, pos, toFind, getViews, (match) => {
const item = getViews().items.find((view) => view.key === match);
const view = getViewItemByKey(match);

if (!item) {
if (!view) {
return null;
}

return new vscode.Hover(
new vscode.MarkdownString(
`[${item.path}](${vscode.Uri.file(projectPath(item.path))})`,
`[${view.path}](${vscode.Uri.file(projectPath(view.path))})`,
),
);
});
Expand All @@ -157,9 +160,7 @@ export const diagnosticProvider = (
return null;
}

const view = getViews().items.find(
(view) => view.key === param.value,
);
const view = getViewItemByKey(param.value);

if (view) {
return null;
Expand Down Expand Up @@ -269,6 +270,12 @@ export const completionProvider = {
}

if (result.isFacade("Route")) {
if (result.func() === "livewire" && result.isParamIndex(1)) {
return getLivewireViewItems().map((view) =>
getCompletionItem(view, document, position),
);
}

if (result.func() === "view" && result.isParamIndex(1)) {
return views.map((view) =>
getCompletionItem(view, document, position),
Expand Down
83 changes: 82 additions & 1 deletion src/repositories/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { repository } from ".";
export interface ViewItem {
key: string;
path: string;
isLivewire: boolean;
isMfc: boolean;
isVendor: boolean;
}

Expand All @@ -13,19 +15,98 @@ const load = () => {
{
key: string;
path: string;
isLivewire: boolean;
isMfc: boolean;
isVendor: boolean;
}[]
>(template("views")).then((results) => {
return results.map(({ key, path, isVendor }) => {
return results.map(({ key, path, isVendor, isLivewire, isMfc }) => {
return {
key,
path,
isLivewire,
isMfc,
isVendor,
};
});
});
};

export const getViewItemByKey = (key: string) => {
key = key.replaceAll("⚡", "");

const filenames = [key];

let [prefix, viewKey] = key.split("::");

if (!viewKey) {
viewKey = prefix;
prefix = "";
}

const parts = viewKey.split(".");
const filename = parts[parts.length - 1];

if (filename) {
parts[parts.length - 1] = `⚡${filename}`;
}

let keyWithEmoji = parts.join(".");

if (prefix) {
keyWithEmoji = `${prefix}::${keyWithEmoji}`;
}

filenames.push(
// Support for emoji
keyWithEmoji,
// Support for mfc and mfc with emoji
`${key}.${filename}`,
`${keyWithEmoji}.${filename}`,
);

return getViews()
.items.filter((view) => view.path.endsWith(".blade.php"))
.find((view) => {
if (view.isLivewire) {
return filenames.includes(view.key);
}

return view.key === key;
});
};

export const getLivewireViewItems = () => {
return (
getViews()
.items.filter((view) => view.isLivewire)
// Mfc components have .php file and .blade.php. We don't want to show
// both files in the completion
.filter((view) => !(view.isMfc && view.path.endsWith(".blade.php")))
// Mfc components link to the component file using the directory name
.map((view) => {
if (view.isMfc) {
let [prefix, viewKey] = view.key.split("::");

if (!viewKey) {
viewKey = prefix;
prefix = "";
}

const parts = viewKey.split(".");
const filename = parts.at(-1);

const mfcView = { ...view };
mfcView.key = view.key.replace(`.${filename}`, "");

return mfcView;
}

return view;
})
);
};

export const getViews = repository<ViewItem[]>({
load,
pattern: inAppDirs("{,**/}{view,views}/{*,**/*}"),
Expand Down
Loading