All files / src/providers ArtisanCompletionProvider.ts

0% Statements 0/48
0% Branches 0/20
0% Functions 0/7
0% Lines 0/44

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                                                                                                                                                                                                                                                                                                                                                                                                                                                           
// src/providers/ArtisanCompletionProvider.ts
import * as vscode from 'vscode';
import type { CacheManager } from '../cache/CacheManager';
import type { ArtisanCommand } from '../types';
 
// ─── Built-in Artisan Commands ────────────────────────────────────────────
 
const BUILTIN_COMMANDS: ArtisanCommand[] = [
  // Make commands
  { name: 'make:model', description: 'Create a new Eloquent model class', 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 resource controller', acceptsValue: false },
    { name: '--all', shortcut: '-a', description: 'All of the above', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Model class name' }], isCustom: false },
 
  { name: 'make:controller', description: 'Create a controller class', options: [
    { name: '--resource', shortcut: '-r', description: 'Resource controller', acceptsValue: false },
    { name: '--api', shortcut: '', description: 'Exclude create/edit', acceptsValue: false },
    { name: '--model', shortcut: '-m', description: 'Model for binding', acceptsValue: true },
    { name: '--invokable', shortcut: '-i', description: 'Single __invoke method', acceptsValue: false },
    { name: '--singleton', shortcut: '', description: 'Singleton resource', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Controller name' }], isCustom: false },
 
  { name: 'make:migration', description: 'Create a new migration file', options: [
    { name: '--create', shortcut: '', description: 'Table to create', acceptsValue: true },
    { name: '--table', shortcut: '', description: 'Table to modify', acceptsValue: true },
  ], arguments: [{ name: 'name', required: true, description: 'Migration name' }], isCustom: false },
 
  { name: 'make:seeder', description: 'Create a new seeder class', options: [], arguments: [{ name: 'name', required: true, description: 'Seeder name' }], isCustom: false },
  { name: 'make:factory', description: 'Create a new model factory', options: [
    { name: '--model', shortcut: '-m', description: 'Model for factory', acceptsValue: true },
  ], arguments: [{ name: 'name', required: true, description: 'Factory name' }], isCustom: false },
  { name: 'make:request', description: 'Create a form request class', options: [], arguments: [{ name: 'name', required: true, description: 'Request class name' }], isCustom: false },
  { name: 'make:resource', description: 'Create an API resource', options: [
    { name: '--collection', shortcut: '-c', description: 'Resource collection', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Resource name' }], isCustom: false },
  { name: 'make:job', description: 'Create a new job class', options: [
    { name: '--sync', shortcut: '', description: 'Synchronous job', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Job name' }], isCustom: false },
  { name: 'make:event', description: 'Create a new event class', options: [], arguments: [{ name: 'name', required: true, description: 'Event name' }], isCustom: false },
  { name: 'make:listener', description: 'Create a new listener class', options: [
    { name: '--event', shortcut: '-e', description: 'Event to listen for', acceptsValue: true },
    { name: '--queued', shortcut: '', description: 'Make queued listener', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Listener name' }], isCustom: false },
  { name: 'make:mail', description: 'Create a new mailable class', options: [
    { name: '--markdown', shortcut: '-m', description: 'Markdown template', acceptsValue: true },
  ], arguments: [{ name: 'name', required: true, description: 'Mailable name' }], isCustom: false },
  { name: 'make:notification', description: 'Create a notification class', options: [], arguments: [{ name: 'name', required: true, description: 'Notification name' }], isCustom: false },
  { name: 'make:policy', description: 'Create a policy class', options: [
    { name: '--model', shortcut: '-m', description: 'Model for policy', acceptsValue: true },
  ], arguments: [{ name: 'name', required: true, description: 'Policy name' }], isCustom: false },
  { name: 'make:command', description: 'Create a new Artisan command', options: [
    { name: '--command', shortcut: '', description: 'Command name (e.g. app:foo)', acceptsValue: true },
  ], arguments: [{ name: 'name', required: true, description: 'Command class name' }], isCustom: false },
  { name: 'make:middleware', description: 'Create a middleware class', options: [], arguments: [{ name: 'name', required: true, description: 'Middleware name' }], isCustom: false },
  { name: 'make:provider', description: 'Create a service provider', options: [], arguments: [{ name: 'name', required: true, description: 'Provider name' }], isCustom: false },
  { name: 'make:observer', description: 'Create an Eloquent observer', options: [
    { name: '--model', shortcut: '-m', description: 'Observed model', acceptsValue: true },
  ], arguments: [{ name: 'name', required: true, description: 'Observer name' }], isCustom: false },
  { name: 'make:rule', description: 'Create a validation rule', options: [
    { name: '--implicit', shortcut: '', description: 'Implicit rule', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Rule name' }], isCustom: false },
  { name: 'make:cast', description: 'Create a custom Eloquent cast', options: [], arguments: [{ name: 'name', required: true, description: 'Cast name' }], isCustom: false },
  { name: 'make:channel', description: 'Create a channel class', options: [], arguments: [{ name: 'name', required: true, description: 'Channel name' }], isCustom: false },
  { name: 'make:enum', description: 'Create an enum', options: [
    { name: '--string', shortcut: '-s', description: 'String-backed enum', acceptsValue: false },
    { name: '--int', shortcut: '-i', description: 'Integer-backed enum', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Enum name' }], isCustom: false },
  { name: 'make:trait', description: 'Create a trait', options: [], arguments: [{ name: 'name', required: true, description: 'Trait name' }], isCustom: false },
  { name: 'make:interface', description: 'Create an interface', options: [], arguments: [{ name: 'name', required: true, description: 'Interface name' }], isCustom: false },
  { name: 'make:test', description: 'Create a test class', options: [
    { name: '--unit', shortcut: '-u', description: 'Unit test', acceptsValue: false },
    { name: '--pest', shortcut: '-p', description: 'Pest test', acceptsValue: false },
  ], arguments: [{ name: 'name', required: true, description: 'Test class name' }], isCustom: false },
 
  // DB commands
  { name: 'migrate', description: 'Run pending migrations', options: [
    { name: '--fresh', shortcut: '', description: 'Drop all and re-migrate', acceptsValue: false },
    { name: '--seed', shortcut: '', description: 'Run seeders after migration', acceptsValue: false },
    { name: '--step', shortcut: '', description: 'Migrate one at a time', acceptsValue: false },
    { name: '--pretend', shortcut: '', description: 'Dump SQL without running', acceptsValue: false },
  ], arguments: [], isCustom: false },
  { name: 'migrate:rollback', description: 'Rollback last migration batch', options: [
    { name: '--step', shortcut: '', description: 'Steps to rollback', acceptsValue: true },
  ], arguments: [], isCustom: false },
  { name: 'migrate:fresh', description: 'Drop all tables and re-run migrations', options: [
    { name: '--seed', shortcut: '', description: 'Run seeders after', acceptsValue: false },
  ], arguments: [], isCustom: false },
  { name: 'migrate:status', description: 'Show migration status', options: [], arguments: [], isCustom: false },
  { name: 'db:seed', description: 'Seed the database', options: [
    { name: '--class', shortcut: '', description: 'Seeder class', acceptsValue: true },
  ], arguments: [], isCustom: false },
  { name: 'db:wipe', description: 'Drop all tables, views, types', options: [], arguments: [], isCustom: false },
  { name: 'tinker', description: 'Interact with the application', options: [], arguments: [], isCustom: false },
  { name: 'route:list', description: 'List all routes', options: [
    { name: '--json', shortcut: '', description: 'JSON output', acceptsValue: false },
  ], arguments: [], isCustom: false },
  { name: 'route:cache', description: 'Cache route registration', options: [], arguments: [], isCustom: false },
  { name: 'route:clear', description: 'Remove route cache file', options: [], arguments: [], isCustom: false },
  { name: 'config:cache', description: 'Cache config files', options: [], arguments: [], isCustom: false },
  { name: 'config:clear', description: 'Remove config cache file', options: [], arguments: [], isCustom: false },
  { name: 'cache:clear', description: 'Flush application cache', options: [], arguments: [], isCustom: false },
  { name: 'view:cache', description: 'Compile Blade templates', options: [], arguments: [], isCustom: false },
  { name: 'view:clear', description: 'Clear compiled views', options: [], arguments: [], isCustom: false },
  { name: 'storage:link', description: 'Create symbolic link from storage to public', options: [], arguments: [], isCustom: false },
  { name: 'optimize', description: 'Cache bootstrap files', options: [], arguments: [], isCustom: false },
  { name: 'optimize:clear', description: 'Clear cached bootstrap files', options: [], arguments: [], isCustom: false },
  { name: 'queue:work', description: 'Start processing jobs', options: [
    { name: '--queue', shortcut: '', description: 'Queue to listen on', acceptsValue: true },
    { name: '--tries', shortcut: '', description: 'Max attempts', acceptsValue: true },
  ], arguments: [], isCustom: false },
  { name: 'queue:listen', description: 'Listen for queue jobs', options: [], arguments: [], isCustom: false },
  { name: 'schedule:run', description: 'Run scheduled commands', options: [], arguments: [], isCustom: false },
  { name: 'event:generate', description: 'Generate missing events and listeners', options: [], arguments: [], isCustom: false },
  { name: 'key:generate', description: 'Set application key', options: [], arguments: [], isCustom: false },
  { name: 'vendor:publish', description: 'Publish vendor assets', options: [
    { name: '--provider', shortcut: '', description: 'Service provider to publish', acceptsValue: true },
    { name: '--tag', shortcut: '', description: 'Asset tag', acceptsValue: true },
  ], arguments: [], isCustom: false },
];
 
// ─── Artisan Completion Provider ──────────────────────────────────────────
 
export class ArtisanCompletionProvider 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('|'), Artisan::call('|'), $this->call('|')
    const commandCallPattern = /(?:\$schedule->command|Artisan::(?:call|queue)|(?:\$this|self)::call)\s*\(\s*['"][^'"]*$/;
    if (!commandCallPattern.test(textBefore)) { return []; }
 
    const all = [...BUILTIN_COMMANDS, ...Array.from(index.artisanCommands.values())];
    return all.map((cmd) => {
      const item = new vscode.CompletionItem(cmd.name, vscode.CompletionItemKind.Event);
      item.detail = cmd.description;
      const md = new vscode.MarkdownString(`**${cmd.name}**\n\n${cmd.description}`);
      if (cmd.options.length > 0) {
        md.appendMarkdown('\n\n**Options:**\n');
        for (const opt of cmd.options.slice(0, 6)) {
          md.appendMarkdown(`- \`${opt.name}\` — ${opt.description}\n`);
        }
      }
      item.documentation = md;
      item.insertText = cmd.name;
      item.sortText = cmd.isCustom ? `0_${cmd.name}` : `1_${cmd.name}`;
      return item;
    });
  }
}
 
// ─── Run Artisan Command Panel ────────────────────────────────────────────
 
export class ArtisanRunner {
  static async run(workspaceRoot: string, phpExecutable: string): Promise<void> {
    const all = [...BUILTIN_COMMANDS];
    const picked = await vscode.window.showQuickPick(
      all.map((cmd) => ({
        label: cmd.name,
        description: cmd.description,
        cmd,
      })),
      { placeHolder: 'Select an Artisan command to run', matchOnDescription: true }
    );
    if (!picked) { return; }
 
    let commandArgs = picked.cmd.name;
 
    // Prompt for required arguments
    for (const arg of picked.cmd.arguments) {
      if (arg.required) {
        const val = await vscode.window.showInputBox({
          prompt: `Value for <${arg.name}>: ${arg.description}`,
          placeHolder: arg.name,
        });
        if (val === undefined) { return; }
        commandArgs += ` ${val}`;
      }
    }
 
    // Prompt for options
    if (picked.cmd.options.length > 0) {
      const selectedOpts = await vscode.window.showQuickPick(
        picked.cmd.options.map((o) => ({ label: o.name, description: o.description, opt: o, picked: false })),
        { placeHolder: 'Select options (optional)', canPickMany: true }
      );
      if (selectedOpts) {
        for (const sel of selectedOpts) {
          if (sel.opt.acceptsValue) {
            const val = await vscode.window.showInputBox({
              prompt: `Value for ${sel.opt.name}: ${sel.opt.description}`,
            });
            if (val) { commandArgs += ` ${sel.opt.name}=${val}`; }
          } else {
            commandArgs += ` ${sel.opt.name}`;
          }
        }
      }
    }
 
    // Run in integrated terminal
    const terminal = vscode.window.createTerminal({
      name: `Artisan: ${picked.cmd.name}`,
      cwd: workspaceRoot,
    });
    terminal.show();
    terminal.sendText(`${phpExecutable} artisan ${commandArgs}`);
  }
}
 
export { BUILTIN_COMMANDS };