All files / src/providers/envfile EnvProvider.ts

0% Statements 0/79
0% Branches 0/26
0% Functions 0/12
0% Lines 0/72

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                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
// src/providers/envfile/EnvProvider.ts
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import type { CacheManager } from '../../cache/CacheManager';
import type { EnvVariable, ArtisanCommand } from '../../types';
 
// ─── .env Indexer ─────────────────────────────────────────────────────────
 
export class EnvIndexer {
  static indexEnvFile(filePath: string): EnvVariable[] {
    const vars: EnvVariable[] = [];
    let source = '';
    try { source = fs.readFileSync(filePath, 'utf8'); } catch { return vars; }
 
    const SENSITIVE_PATTERNS = /PASSWORD|SECRET|KEY|TOKEN|API|PRIVATE|CREDENTIAL|PWD/i;
    const lines = source.split('\n');
 
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i].trim();
      if (!line || line.startsWith('#')) { continue; }
 
      const eqIdx = line.indexOf('=');
      if (eqIdx === -1) { continue; }
 
      const key = line.slice(0, eqIdx).trim();
      const rawVal = line.slice(eqIdx + 1).trim();
      const value = rawVal.replace(/^["']|["']$/g, '');
 
      vars.push({
        key,
        value,
        filePath,
        lineNumber: i + 1,
        isSensitive: SENSITIVE_PATTERNS.test(key),
      });
    }
    return vars;
  }
 
  static indexAllEnvFiles(workspaceRoot: string): EnvVariable[] {
    const envFiles = ['.env', '.env.example', '.env.local', '.env.testing'];
    const seen = new Set<string>();
    const vars: EnvVariable[] = [];
 
    for (const envFile of envFiles) {
      const fp = path.join(workspaceRoot, envFile);
      if (fs.existsSync(fp)) {
        for (const v of EnvIndexer.indexEnvFile(fp)) {
          if (!seen.has(v.key)) {
            seen.add(v.key);
            vars.push(v);
          }
        }
      }
    }
    return vars;
  }
}
 
// ─── env() Completion Provider ────────────────────────────────────────────
 
export class EnvCompletionProvider 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);
 
    // env('KEY')  or  Env::get('KEY')
    if (!/\benv\s*\(\s*['"][^'"]*$/.test(textBefore)) { return []; }
 
    const index = this.cache.getIndex();
    return Array.from(index.envVariables.values()).map((v) => {
      const item = new vscode.CompletionItem(v.key, vscode.CompletionItemKind.Variable);
      item.detail = v.isSensitive ? '*** (sensitive)' : v.value.slice(0, 60);
      item.documentation = new vscode.MarkdownString(
        `**ENV:** \`${v.key}\`\n\nFile: \`${path.basename(v.filePath)}\` — line ${v.lineNumber}`
      );
      item.sortText = `0_${v.key}`;
      return item;
    });
  }
}
 
// ─── .env file key completion (inside .env files) ─────────────────────────
 
export class EnvFileCompletionProvider implements vscode.CompletionItemProvider {
  constructor(private readonly cache: CacheManager) {}
 
  provideCompletionItems(
    document: vscode.TextDocument,
    position: vscode.Position
  ): vscode.CompletionItem[] {
    // Only trigger in .env files
    if (!document.fileName.endsWith('.env') && !document.fileName.includes('.env.')) {
      return [];
    }
 
    const lineText = document.lineAt(position).text;
    // If line already has = don't complete key names
    if (lineText.includes('=')) { return []; }
 
    // Suggest keys from .env.example or existing .env keys
    const index = this.cache.getIndex();
    return Array.from(index.envVariables.values()).map((v) => {
      const item = new vscode.CompletionItem(v.key, vscode.CompletionItemKind.Variable);
      item.insertText = new vscode.SnippetString(`${v.key}=$1`);
      item.detail = path.basename(v.filePath);
      return item;
    });
  }
}
 
// ─── Artisan Command Indexer ──────────────────────────────────────────────
 
// Built-in Laravel artisan commands with full option definitions
export const BUILTIN_ARTISAN_COMMANDS: ArtisanCommand[] = [
  // Make commands
  { name: 'make:model', description: 'Create a new Eloquent model class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Model name' }], options: [{ name: '--migration', shortcut: '-m', description: 'Create migration', acceptsValue: false }, { name: '--factory', shortcut: '-f', description: 'Create factory', acceptsValue: false }, { name: '--seed', shortcut: '-s', description: 'Create seeder', acceptsValue: false }, { name: '--controller', shortcut: '-c', description: 'Create controller', acceptsValue: false }, { name: '--resource', shortcut: '-r', description: 'Resource controller', acceptsValue: false }, { name: '--api', shortcut: '', description: 'API controller', acceptsValue: false }, { name: '--policy', shortcut: '-p', description: 'Create policy', acceptsValue: false }, { name: '--all', shortcut: '-a', description: 'All (migration, factory, seeder, policy, controller)', acceptsValue: false }] },
  { name: 'make:controller', description: 'Create a new controller class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Controller name' }], options: [{ name: '--resource', shortcut: '-r', description: 'Resource controller', acceptsValue: false }, { name: '--api', shortcut: '', description: 'API resource controller', acceptsValue: false }, { name: '--invokable', shortcut: '-i', description: 'Invokable controller', acceptsValue: false }, { name: '--model', shortcut: '-m', description: 'Bind model', acceptsValue: true }, { name: '--requests', shortcut: '-R', description: 'Generate form requests', acceptsValue: false }] },
  { name: 'make:migration', description: 'Create a new migration file', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Migration name' }], options: [{ name: '--create', shortcut: '', description: 'Table to create', acceptsValue: true }, { name: '--table', shortcut: '', description: 'Table to modify', acceptsValue: true }] },
  { name: 'make:seeder', description: 'Create a new seeder class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Seeder name' }], options: [] },
  { name: 'make:factory', description: 'Create a new model factory', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Factory name' }], options: [{ name: '--model', shortcut: '-m', description: 'Model class', acceptsValue: true }] },
  { name: 'make:request', description: 'Create a new form request class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Request name' }], options: [] },
  { name: 'make:resource', description: 'Create a new resource', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Resource name' }], options: [{ name: '--collection', shortcut: '-c', description: 'Create resource collection', acceptsValue: false }] },
  { name: 'make:middleware', description: 'Create a new middleware class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Middleware name' }], options: [] },
  { name: 'make:policy', description: 'Create a new policy class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Policy name' }], options: [{ name: '--model', shortcut: '-m', description: 'Model class', acceptsValue: true }] },
  { name: 'make:command', description: 'Create a new Artisan command', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Command name' }], options: [{ name: '--command', shortcut: '', description: 'The terminal command', acceptsValue: true }] },
  { name: 'make:job', description: 'Create a new job class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Job name' }], options: [{ name: '--sync', shortcut: '', description: 'Synchronous job', acceptsValue: false }] },
  { name: 'make:event', description: 'Create a new event class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Event name' }], options: [] },
  { name: 'make:listener', description: 'Create a new listener class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Listener name' }], options: [{ name: '--event', shortcut: '-e', description: 'Event class', acceptsValue: true }, { name: '--queued', shortcut: '', description: 'Queued listener', acceptsValue: false }] },
  { name: 'make:mail', description: 'Create a new email class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Mail class name' }], options: [{ name: '--markdown', shortcut: '-m', description: 'Markdown template', acceptsValue: true }] },
  { name: 'make:notification', description: 'Create a new notification class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Notification name' }], options: [{ name: '--markdown', shortcut: '-m', description: 'Markdown template', acceptsValue: true }] },
  { name: 'make:observer', description: 'Create a new observer class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Observer name' }], options: [{ name: '--model', shortcut: '-m', description: 'Model class', acceptsValue: true }] },
  { name: 'make:rule', description: 'Create a new validation rule', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Rule name' }], options: [{ name: '--implicit', shortcut: '-i', description: 'Implicit rule', acceptsValue: false }] },
  { name: 'make:cast', description: 'Create a new custom Eloquent cast', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Cast name' }], options: [] },
  { name: 'make:channel', description: 'Create a new channel class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Channel name' }], options: [] },
  { name: 'make:scope', description: 'Create a new scope class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Scope name' }], options: [] },
  { name: 'make:enum', description: 'Create a new enum', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Enum name' }], options: [{ name: '--string', shortcut: '-s', description: 'String-backed enum', acceptsValue: false }, { name: '--int', shortcut: '-i', description: 'Integer-backed enum', acceptsValue: false }] },
  { name: 'make:trait', description: 'Create a new trait', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Trait name' }], options: [] },
  { name: 'make:interface', description: 'Create a new interface', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Interface name' }], options: [] },
  { name: 'make:exception', description: 'Create a new custom exception class', isCustom: false, arguments: [{ name: 'name', required: true, description: 'Exception name' }], options: [{ name: '--render', shortcut: '', description: 'With render method', acceptsValue: false }, { name: '--report', shortcut: '', description: 'With report method', acceptsValue: false }] },
  // Migration commands
  { name: 'migrate', description: 'Run the database migrations', isCustom: false, arguments: [], options: [{ name: '--force', shortcut: '', description: 'Force in production', acceptsValue: false }, { name: '--pretend', shortcut: '', description: 'Dump SQL without running', acceptsValue: false }, { name: '--seed', shortcut: '', description: 'Run seeders after', acceptsValue: false }, { name: '--step', shortcut: '', description: 'Migrate one batch at a time', acceptsValue: false }] },
  { name: 'migrate:rollback', description: 'Rollback the last database migration batch', isCustom: false, arguments: [], options: [{ name: '--step', shortcut: '', description: 'Number of steps to rollback', acceptsValue: true }] },
  { name: 'migrate:fresh', description: 'Drop all tables and re-run all migrations', isCustom: false, arguments: [], options: [{ name: '--seed', shortcut: '', description: 'Seed after migrating', acceptsValue: false }] },
  { name: 'migrate:refresh', description: 'Reset and re-run all migrations', isCustom: false, arguments: [], options: [{ name: '--seed', shortcut: '', description: 'Seed after migrating', acceptsValue: false }] },
  { name: 'migrate:reset', description: 'Rollback all database migrations', isCustom: false, arguments: [], options: [] },
  { name: 'migrate:status', description: 'Show the status of each migration', isCustom: false, arguments: [], options: [] },
  // DB commands
  { name: 'db:seed', description: 'Seed the database', isCustom: false, arguments: [], options: [{ name: '--class', shortcut: '', description: 'Seeder class', acceptsValue: true }, { name: '--force', shortcut: '', description: 'Force in production', acceptsValue: false }] },
  { name: 'db:wipe', description: 'Drop all tables, views, and types', isCustom: false, arguments: [], options: [] },
  // Cache commands  
  { name: 'cache:clear', description: 'Flush the application cache', isCustom: false, arguments: [], options: [] },
  { name: 'cache:forget', description: 'Remove an item from the cache', isCustom: false, arguments: [{ name: 'key', required: true, description: 'Cache key' }], options: [] },
  // Config commands
  { name: 'config:cache', description: 'Create a cache file for faster configuration loading', isCustom: false, arguments: [], options: [] },
  { name: 'config:clear', description: 'Remove the configuration cache file', isCustom: false, arguments: [], options: [] },
  { name: 'config:publish', description: 'Publish configuration files', isCustom: false, arguments: [], options: [] },
  // Route commands
  { name: 'route:cache', description: 'Create a route cache file', isCustom: false, arguments: [], options: [] },
  { name: 'route:clear', description: 'Remove the route cache file', isCustom: false, arguments: [], options: [] },
  { name: 'route:list', description: 'List all registered routes', isCustom: false, arguments: [], options: [{ name: '--json', shortcut: '', description: 'Output as JSON', acceptsValue: false }, { name: '--path', shortcut: '', description: 'Filter by URI path', acceptsValue: true }, { name: '--name', shortcut: '', description: 'Filter by route name', acceptsValue: true }, { name: '--method', shortcut: '', description: 'Filter by HTTP method', acceptsValue: true }] },
  // Queue commands
  { name: 'queue:work', description: 'Process jobs on the queue', isCustom: false, arguments: [], options: [{ name: '--queue', shortcut: '', description: 'Queue to process', acceptsValue: true }, { name: '--tries', shortcut: '', description: 'Max tries', acceptsValue: true }, { name: '--sleep', shortcut: '', description: 'Sleep seconds when no job', acceptsValue: true }] },
  { name: 'queue:listen', description: 'Listen to a queue', isCustom: false, arguments: [], options: [] },
  { name: 'queue:failed', description: 'List all failed queue jobs', isCustom: false, arguments: [], options: [] },
  { name: 'queue:retry', description: 'Retry a failed queue job', isCustom: false, arguments: [{ name: 'id', required: true, description: 'Job ID (or "all")' }], options: [] },
  // Schedule
  { name: 'schedule:run', description: 'Run the scheduled commands', isCustom: false, arguments: [], options: [] },
  { name: 'schedule:list', description: 'List all scheduled tasks', isCustom: false, arguments: [], options: [] },
  // Storage
  { name: 'storage:link', description: 'Create symbolic links for storage', isCustom: false, arguments: [], options: [] },
  // View
  { name: 'view:cache', description: 'Compile all Blade templates', isCustom: false, arguments: [], options: [] },
  { name: 'view:clear', description: 'Clear all compiled view files', isCustom: false, arguments: [], options: [] },
  // Misc
  { name: 'optimize', description: 'Cache config, routes, views', isCustom: false, arguments: [], options: [] },
  { name: 'optimize:clear', description: 'Clear all cached files', isCustom: false, arguments: [], options: [] },
  { name: 'serve', description: 'Serve the application on the PHP dev server', isCustom: false, arguments: [], options: [{ name: '--host', shortcut: '', description: 'Host address', acceptsValue: true }, { name: '--port', shortcut: '', description: 'Port', acceptsValue: true }] },
  { name: 'tinker', description: 'Interact with the application', isCustom: false, arguments: [], options: [] },
  { name: 'about', description: 'Display basic info about the application', isCustom: false, arguments: [], options: [] },
  { name: 'env', description: 'Display the current framework environment', isCustom: false, arguments: [], options: [] },
  { name: 'down', description: 'Put the application in maintenance mode', isCustom: false, arguments: [], options: [{ name: '--message', shortcut: '', description: 'Maintenance message', acceptsValue: true }, { name: '--allow', shortcut: '', description: 'Allowed IP', acceptsValue: true }] },
  { name: 'up', description: 'Bring the application out of maintenance mode', isCustom: false, arguments: [], options: [] },
];
 
// ─── Artisan Completion Provider ──────────────────────────────────────────
 
export class ScheduleArtisanProvider 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);
    const index = this.cache.getIndex();
 
    // $schedule->command('|')  or  Artisan::call('|')
    if (!/(?:\$schedule\s*->\s*command|Artisan::(?:call|queue|output))\s*\(\s*['"][^'"]*$/.test(textBefore)) {
      return [];
    }
 
    const allCommands = [
      ...BUILTIN_ARTISAN_COMMANDS,
      ...Array.from(index.artisanCommands.values()),
    ];
 
    return allCommands.map((cmd) => {
      const item = new vscode.CompletionItem(cmd.name, vscode.CompletionItemKind.Function);
      item.detail = cmd.description;
      const md = new vscode.MarkdownString();
      md.appendMarkdown(`**${cmd.name}**\n\n${cmd.description}\n\n`);
      if (cmd.options.length > 0) {
        md.appendMarkdown('**Options:**\n');
        for (const opt of cmd.options.slice(0, 8)) {
          md.appendMarkdown(`- \`${opt.name}\`: ${opt.description}\n`);
        }
      }
      item.documentation = md;
      item.sortText = cmd.isCustom ? `0_${cmd.name}` : `1_${cmd.name}`;
      return item;
    });
  }
}