All files / src/providers FindUsagesProvider.ts

22.78% Statements 18/79
26.31% Branches 10/38
36.36% Functions 4/11
20.28% Lines 14/69

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 1878x 1x                                                                                                                                                                                                                                                                                                 5x     5x   4x 4x 4x     3x       3x   2x                 2x       4x 4x 3x                  
// src/providers/FindUsagesProvider.ts
import * as vscode from 'vscode';
import type { CacheManager } from '../cache/CacheManager';
 
// ─── Find Usages / References Provider ───────────────────────────────────
 
/**
 * Implements vscode.ReferenceProvider so "Find All References" works for:
 * - Route names  (all route('name') / to_route('name') calls)
 * - View names   (all view('name') / @include('name') calls)
 * - Translation keys (all __('key') / trans('key') calls)
 * - Config keys  (all config('key') calls)
 * - Eloquent scopes (all ::scope() calls)
 * - Gate / policy names
 */
export class LaravelReferenceProvider implements vscode.ReferenceProvider {
  constructor(
    private readonly cache: CacheManager,
    private readonly workspaceRoot: string
  ) {}
 
  async provideReferences(
    document: vscode.TextDocument,
    position: vscode.Position,
    _context: vscode.ReferenceContext,
    _token: vscode.CancellationToken
  ): Promise<vscode.Location[]> {
    const wordRange = document.getWordRangeAtPosition(position, /['"][^'"]+['"]/);
    if (!wordRange) { return []; }
 
    const word = document.getText(wordRange).replace(/['"]/g, '');
    const index = this.cache.getIndex();
 
    // Determine what kind of symbol this is
    let searchPattern: RegExp | null = null;
 
    if (index.routes.has(word)) {
      searchPattern = new RegExp(`\\b(?:route|to_route|signedRoute|temporarySignedRoute)\\s*\\(\\s*['"]${escapeRe(word)}['"]`, 'g');
    } else if (index.views.has(word)) {
      searchPattern = new RegExp(`(?:view|View::make|@include|@extends|@component|Inertia::render)\\s*\\(\\s*['"]${escapeRe(word)}['"]`, 'g');
    } else if (Array.from(index.translations.values()).some((t) => t.key === word)) {
      searchPattern = new RegExp(`(?:__|trans|\\@lang)\\s*\\(\\s*['"]${escapeRe(word)}['"]`, 'g');
    } else if (index.configs.has(word)) {
      searchPattern = new RegExp(`\\bconfig\\s*\\(\\s*['"]${escapeRe(word)}['"]`, 'g');
    } else {
      // Try generic string search for gates/scopes
      searchPattern = new RegExp(`['"]${escapeRe(word)}['"]`, 'g');
    }
 
    if (!searchPattern) { return []; }
 
    const phpFiles = await vscode.workspace.findFiles(
      new vscode.RelativePattern(this.workspaceRoot, '**/*.{php,blade.php}'),
      '{vendor/**,node_modules/**}'
    );
 
    const locations: vscode.Location[] = [];
 
    for (const fileUri of phpFiles) {
      try {
        const fileDoc = await vscode.workspace.openTextDocument(fileUri);
        const fileText = fileDoc.getText();
        let match: RegExpExecArray | null;
        searchPattern.lastIndex = 0;
 
        while ((match = searchPattern.exec(fileText)) !== null) {
          // Find the position of the string literal within the match
          const literalStart = fileText.indexOf(`'${word}'`, match.index) !== -1
            ? fileText.indexOf(`'${word}'`, match.index)
            : fileText.indexOf(`"${word}"`, match.index);
 
          if (literalStart === -1 || literalStart > match.index + match[0].length) { continue; }
 
          const startPos = fileDoc.positionAt(literalStart + 1);
          const endPos = fileDoc.positionAt(literalStart + 1 + word.length);
          locations.push(new vscode.Location(fileUri, new vscode.Range(startPos, endPos)));
        }
      } catch { /* skip unreadable files */ }
    }
 
    return locations;
  }
}
 
// ─── Eloquent Scope / Relation Find Usages ────────────────────────────────
 
export class EloquentMemberReferenceProvider implements vscode.ReferenceProvider {
  constructor(
    _cache: CacheManager,
    private readonly workspaceRoot: string
  ) {}
 
  async provideReferences(
    document: vscode.TextDocument,
    position: vscode.Position,
    _context: vscode.ReferenceContext,
    _token: vscode.CancellationToken
  ): Promise<vscode.Location[]> {
    // Get the member name under cursor
    const wordRange = document.getWordRangeAtPosition(position, /\b[a-zA-Z_]\w*\b/);
    if (!wordRange) { return []; }
    const memberName = document.getText(wordRange);
 
    // Check if it's a scope method
    const lineText = document.lineAt(position).text;
    const isScope = /public\s+function\s+scope/.test(lineText) ||
                    /public\s+function\s+/.test(lineText);
    if (!isScope) { return []; }
 
    // For scopes: search for ::scopeName() calls
    const phpFiles = await vscode.workspace.findFiles(
      new vscode.RelativePattern(this.workspaceRoot, '**/*.{php,blade.php}'),
      '{vendor/**,node_modules/**}'
    );
 
    const locations: vscode.Location[] = [];
    // scope method name (strip "scope" prefix for usage)
    const usageName = memberName.startsWith('scope')
      ? memberName.charAt(5).toLowerCase() + memberName.slice(6)
      : memberName;
 
    const pattern = new RegExp(`::${escapeRe(usageName)}\\s*\\(|->\\s*${escapeRe(usageName)}\\s*\\(`, 'g');
 
    for (const fileUri of phpFiles) {
      try {
        const fileDoc = await vscode.workspace.openTextDocument(fileUri);
        const fileText = fileDoc.getText();
        let match: RegExpExecArray | null;
        pattern.lastIndex = 0;
 
        while ((match = pattern.exec(fileText)) !== null) {
          const nameStart = match.index + match[0].indexOf(usageName);
          const startPos = fileDoc.positionAt(nameStart);
          const endPos = fileDoc.positionAt(nameStart + usageName.length);
          locations.push(new vscode.Location(fileUri, new vscode.Range(startPos, endPos)));
        }
      } catch { /* skip */ }
    }
 
    return locations;
  }
}
 
// ─── View Usages Line Marker (CodeLens) ───────────────────────────────────
 
export class ViewUsagesCodeLensProvider implements vscode.CodeLensProvider {
  constructor(private readonly cache: CacheManager) {}
 
  provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] {
    if (!document.fileName.endsWith('.blade.php')) { return []; }
 
    const index = this.cache.getIndex();
    const viewName = this.fileToViewName(document.uri.fsPath);
    if (!viewName) { return []; }
 
    // Check if this view exists in any route action or controller
    const isReferenced = Array.from(index.routes.values()).some(
      (r) => r.action && r.action.toLowerCase().includes(viewName.replace(/\./g, ''))
    );
 
    if (!isReferenced && !index.views.has(viewName)) { return []; }
 
    const lens = new vscode.CodeLens(
      new vscode.Range(0, 0, 0, 0),
      {
        title: `👁 Find view usages`,
        command: 'laravel-maestro.findViewUsages',
        arguments: [viewName],
      }
    );
 
    return [lens];
  }
 
  private fileToViewName(filePath: string): string | undefined {
    const match = /resources[/\\]views[/\\](.+)\.blade\.php$/.exec(filePath);
    if (!match) { return undefined; }
    return match[1].replace(/[/\\]/g, '.');
  }
}
 
// ─── Helpers ──────────────────────────────────────────────────────────────
 
function escapeRe(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}