Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | 9x 1x 1x 1x 6x 6x 6x 6x 5x 5x 5x 5x 5x 165x 165x 165x 165x 165x 5x 3x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 5x 5x 5x 5x 3x 3x 3x | // src/providers/NavigationProvider.ts
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import type { CacheManager } from '../cache/CacheManager';
// ─── Navigation Provider ──────────────────────────────────────────────────
/**
* Model → Policy, Event → Listener, Request → Rules, Form Request $request->field
* Registered as both DefinitionProvider and a standalone command.
*/
export class LaravelNavigationProvider implements vscode.DefinitionProvider {
constructor(
private readonly cache: CacheManager,
private readonly workspaceRoot: string
) {}
async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Location | vscode.Location[] | undefined> {
const wordRange = document.getWordRangeAtPosition(position, /\b[A-Z][a-zA-Z0-9_\\]+\b/);
if (!wordRange) { return undefined; }
const word = document.getText(wordRange);
const index = this.cache.getIndex();
// Model → Policy
for (const [, model] of index.models) {
if (model.className === word) {
const policyPath = this.findPolicyForModel(model.className);
if (policyPath) {
return new vscode.Location(vscode.Uri.file(policyPath), new vscode.Position(0, 0));
}
}
}
return undefined;
}
private findPolicyForModel(modelName: string): string | undefined {
const candidates = [
path.join(this.workspaceRoot, 'app', 'Policies', `${modelName}Policy.php`),
];
return candidates.find((c) => fs.existsSync(c));
}
}
// ─── $request->field completion from FormRequest rules ───────────────────
export class ControllerRequestFieldProvider implements vscode.CompletionItemProvider {
constructor(private readonly cache: CacheManager) {}
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): vscode.CompletionItem[] {
const lineText = document.lineAt(position).text;
const textBefore = lineText.substring(0, position.character);
// Must be: $request->| (in a controller method context)
if (!/\$request\s*->\s*\w*$/.test(textBefore)) { return []; }
const index = this.cache.getIndex();
const items: vscode.CompletionItem[] = [];
// Find the FormRequest for this controller method
const formRequest = this.findFormRequestForDocument(document, index);
// Standard Request methods always available
const requestMethods = [
{ name: 'validated', detail: '(key?: string): mixed', doc: 'Get all validated input data.' },
{ name: 'safe', detail: '(): ValidatedInput', doc: 'Get safe validated input.' },
{ name: 'input', detail: '(key: string, default?: mixed): mixed', doc: 'Get an input item from the request.' },
{ name: 'get', detail: '(key: string, default?: mixed): mixed', doc: 'Get an input element by key.' },
{ name: 'all', detail: '(): array', doc: 'Get all input data.' },
{ name: 'only', detail: '(keys: array): array', doc: 'Get a subset of input.' },
{ name: 'except', detail: '(keys: array): array', doc: 'Get all input except specified keys.' },
{ name: 'has', detail: '(key: string): bool', doc: 'Determine if key exists in input.' },
{ name: 'filled', detail: '(key: string): bool', doc: 'Determine if key is present and not empty.' },
{ name: 'missing', detail: '(key: string): bool', doc: 'Determine if key is absent.' },
{ name: 'when', detail: '(key: string, callback: Closure): mixed', doc: 'Execute a callback if key exists.' },
{ name: 'whenHas', detail: '(key: string, callback: Closure): mixed', doc: 'Execute a callback when key has a value.' },
{ name: 'boolean', detail: '(key: string, default?: bool): bool', doc: 'Retrieve input as boolean.' },
{ name: 'integer', detail: '(key: string, default?: int): int', doc: 'Retrieve input as integer.' },
{ name: 'float', detail: '(key: string, default?: float): float', doc: 'Retrieve input as float.' },
{ name: 'string', detail: '(key: string, default?: string): Stringable', doc: 'Retrieve input as Stringable.' },
{ name: 'collect', detail: '(key?: string): Collection', doc: 'Retrieve input as Collection.' },
{ name: 'date', detail: '(key: string, format?: string): Carbon', doc: 'Retrieve input as Carbon date.' },
{ name: 'enum', detail: '(key: string, enumClass: string): BackedEnum', doc: 'Retrieve input as enum.' },
{ name: 'file', detail: '(key: string): UploadedFile', doc: 'Retrieve an uploaded file.' },
{ name: 'files', detail: '(): array', doc: 'Get all uploaded files.' },
{ name: 'hasFile', detail: '(key: string): bool', doc: 'Determine if uploaded file is on the request.' },
{ name: 'user', detail: '(): ?User', doc: 'Get the authenticated user.' },
{ name: 'route', detail: '(param?: string): mixed', doc: 'Get a route parameter.' },
{ name: 'ip', detail: '(): string', doc: 'Get the client IP address.' },
{ name: 'method', detail: '(): string', doc: 'Get request HTTP method.' },
{ name: 'isMethod', detail: '(method: string): bool', doc: 'Check HTTP method.' },
{ name: 'ajax', detail: '(): bool', doc: 'Determine if request is AJAX.' },
{ name: 'wantsJson', detail: '(): bool', doc: 'Determine if JSON is expected.' },
{ name: 'expectsJson', detail: '(): bool', doc: 'Determine if JSON response is expected.' },
{ name: 'header', detail: '(key: string): ?string', doc: 'Retrieve a header from the request.' },
{ name: 'bearerToken', detail: '(): ?string', doc: 'Get the bearer token.' },
{ name: 'authorize', detail: '(): bool', doc: 'Check authorization (FormRequest).' },
];
for (const method of requestMethods) {
const item = new vscode.CompletionItem(method.name, vscode.CompletionItemKind.Method);
item.detail = method.detail;
item.documentation = new vscode.MarkdownString(method.doc);
item.sortText = `z_${method.name}`; // below field completions
items.push(item);
}
// FormRequest field completions (highest priority)
if (formRequest) {
for (const field of formRequest.fields) {
const item = new vscode.CompletionItem(field.name, vscode.CompletionItemKind.Field);
item.detail = field.rules.join('|');
item.documentation = new vscode.MarkdownString(
`**FormRequest field:** \`${field.name}\`\n\nRules: \`${field.rules.join('|')}\``
);
item.insertText = field.name;
item.sortText = `a_${field.name}`;
items.push(item);
// Also add ->validated('field')
const validatedItem = new vscode.CompletionItem(
`validated('${field.name}')`, vscode.CompletionItemKind.Field
);
validatedItem.detail = field.rules.join('|');
validatedItem.insertText = `validated('${field.name}')`;
validatedItem.sortText = `b_${field.name}`;
items.push(validatedItem);
}
}
return items;
}
private findFormRequestForDocument(
document: vscode.TextDocument,
index: ReturnType<CacheManager['getIndex']>
): import('../types').FormRequestDefinition | undefined {
const text = document.getText();
// Find the FormRequest type-hint in the current method
const formRequestMatch = /(\w+Request)\s+\$request/.exec(text);
if (!formRequestMatch) { return undefined; }
const requestName = formRequestMatch[1];
for (const [, req] of index.formRequests) {
Eif (req.className === requestName) { return req; }
}
return undefined;
}
}
// ─── Model ↔ Policy ↔ Factory Navigation Commands ────────────────────────
export class ModelLinkedFilesProvider {
constructor(private readonly workspaceRoot: string) {}
async navigateToPolicy(modelName: string): Promise<void> {
const policyPath = path.join(this.workspaceRoot, 'app', 'Policies', `${modelName}Policy.php`);
if (!fs.existsSync(policyPath)) {
const create = await vscode.window.showWarningMessage(
`No policy found for ${modelName}. Create it?`, 'Create Policy'
);
if (create) {
await vscode.commands.executeCommand('laravelMaestro.generatePolicy');
}
return;
}
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(policyPath));
await vscode.window.showTextDocument(doc);
}
async navigateToFactory(modelName: string): Promise<void> {
const factoryPath = path.join(this.workspaceRoot, 'database', 'factories', `${modelName}Factory.php`);
if (!fs.existsSync(factoryPath)) {
const create = await vscode.window.showWarningMessage(
`No factory found for ${modelName}. Create it?`, 'Create Factory'
);
if (create) {
await vscode.commands.executeCommand('laravelMaestro.generateFactory');
}
return;
}
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(factoryPath));
await vscode.window.showTextDocument(doc);
}
async navigateToObserver(modelName: string): Promise<void> {
const observerPath = path.join(this.workspaceRoot, 'app', 'Observers', `${modelName}Observer.php`);
if (!fs.existsSync(observerPath)) {
const create = await vscode.window.showWarningMessage(
`No observer found for ${modelName}. Create it?`, 'Create Observer'
);
if (create) {
await vscode.commands.executeCommand('laravelMaestro.generateObserver');
}
return;
}
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(observerPath));
await vscode.window.showTextDocument(doc);
}
}
// ─── Rate Limiter Completion ──────────────────────────────────────────────
export class RateLimiterCompletionProvider implements vscode.CompletionItemProvider {
constructor(private readonly cache: CacheManager) {}
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): vscode.CompletionItem[] {
const lineText = document.lineAt(position).text;
const textBefore = lineText.substring(0, position.character);
if (!/(?:new\s+RateLimited|throttle:)\s*\(\s*['"][^'"]*$/.test(textBefore) &&
!/['"]throttle:['"][^'"]*$/.test(textBefore)) {
return [];
}
// Extract rate limiter names from RateLimiter::for() calls in RouteServiceProvider
const index = this.cache.getIndex();
const names: string[] = [];
for (const [, cls] of index.classes) {
if (cls.name.includes('ServiceProvider')) {
const text = fs.existsSync(cls.filePath)
? fs.readFileSync(cls.filePath, 'utf8') : '';
const matches = text.matchAll(/RateLimiter::for\s*\(\s*['"]([^'"]+)['"]/g);
for (const m of matches) { names.push(m[1]); }
}
}
// Common built-in rate limiter names
const builtins = ['api', 'uploads', 'global'];
return [...new Set([...builtins, ...names])].map((name) => {
const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Value);
item.detail = 'Rate limiter name';
return item;
});
}
}
|