diff --git a/package.json b/package.json index e617bfe5..fed4314e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,10 @@ { "id": "laravel.wrapWithHelper.submenu", "label": "Wrap selection with helpers" + }, + { + "id": "laravel.artisanMake.submenu", + "label": "New Laravel File..." } ], "menus": { @@ -94,6 +98,130 @@ "command": "laravel.namespace.generate", "when": "resourceLangId == php", "group": "laravel" + }, + { + "command": "laravel.artisan.make.cast", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.channel", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.class", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.command", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.component", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.controller", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.enum", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.event", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.exception", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.factory", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.interface", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.job", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.job-middleware", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.listener", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.livewire", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.mail", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.middleware", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.migration", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.model", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.notification", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.observer", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.policy", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.provider", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.request", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.resource", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.rule", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.scope", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.seeder", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.test", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.trait", + "group": "laravel" + }, + { + "command": "laravel.artisan.make.view", + "group": "laravel" } ], "editor/context": [ @@ -118,6 +246,170 @@ "group": "laravel" } ], + "explorer/context": [ + { + "submenu": "laravel.artisanMake.submenu", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)(app|src|database|resource(s?)|test(s?))(\\/|\\\\|$)/i", + "group": "navigation" + } + ], + "laravel.artisanMake.submenu": [ + { + "command": "laravel.artisan.make.cast", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Cast(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.channel", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Broadcasting(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.class", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)(app|src)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.command", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Console(\\/|\\\\)Command(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.component", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)View(s?)(\\/|\\\\)Component(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.controller", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Controller(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.enum", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)(Enum|ValueObject)(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.event", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Event(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.exception", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Exception(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.factory", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Database(\\/|\\\\)Factor(y|ies)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.interface", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)(app|src)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.job", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Job(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.job-middleware", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Job(s?)(\\/|\\\\)Middleware(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.listener", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Listener(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.livewire", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Livewire(\\/|\\\\)Component(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.mail", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Mail(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.middleware", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Http(\\/|\\\\)Middleware(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.migration", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Database(\\/|\\\\)Migration(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.model", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Model(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.notification", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Notification(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.observer", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Observer(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.policy", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Polic(y|ies)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.provider", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Provider(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.request", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Http(\\/|\\\\)Request(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.resource", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Http(\\/|\\\\)Resource(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.rule", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Rule(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.scope", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Scope(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.seeder", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Database(\\/|\\\\)Seed(er?)(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.test", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)Test(s?)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.trait", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)(app|src)(\\/|\\\\|$)/i", + "group": "navigation" + }, + { + "command": "laravel.artisan.make.view", + "when": "explorerResourceIsFolder && resourcePath =~ /(\\/|\\\\)View(s?)(\\/|\\\\|$)/i", + "group": "navigation" + } + ], "laravel.wrapWithHelper.submenu": [ { "command": "laravel.wrapWithHelper.dd" @@ -218,6 +510,161 @@ "command": "laravel.namespace.generate", "title": "Generate namespace", "category": "Laravel" + }, + { + "command": "laravel.artisan.make.cast", + "title": "Make Cast", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.channel", + "title": "Make Channel", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.class", + "title": "Make Class", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.command", + "title": "Make Console Command", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.component", + "title": "Make View Component", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.controller", + "title": "Make Controller", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.enum", + "title": "Make Enum", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.event", + "title": "Make Event", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.exception", + "title": "Make Exception", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.factory", + "title": "Make Factory", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.interface", + "title": "Make Interface", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.job", + "title": "Make Job", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.job-middleware", + "title": "Make Job Middleware", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.listener", + "title": "Make Event Listener", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.livewire", + "title": "Make Livewire Component", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.mail", + "title": "Make Mail", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.middleware", + "title": "Make Middleware", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.migration", + "title": "Make Migration", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.model", + "title": "Make Model", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.notification", + "title": "Make Notification", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.observer", + "title": "Make Observer", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.policy", + "title": "Make Policy", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.provider", + "title": "Make Service Provider", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.request", + "title": "Make Form Request", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.resource", + "title": "Make Resource", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.rule", + "title": "Make Validation Rule", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.scope", + "title": "Make Scope", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.seeder", + "title": "Make Database Seeder", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.test", + "title": "Make Test", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.trait", + "title": "Make Trait", + "category": "Laravel" + }, + { + "command": "laravel.artisan.make.view", + "title": "Make View", + "category": "Laravel" } ], "configuration": { diff --git a/src/artisan/builder.ts b/src/artisan/builder.ts new file mode 100644 index 00000000..ca4cacb8 --- /dev/null +++ b/src/artisan/builder.ts @@ -0,0 +1,252 @@ +import * as vscode from "vscode"; +import path from "path"; + +import { Argument, ArgumentType, Command, Option } from "./types"; +import { getNamespace } from "@src/commands/generateNamespace"; +import { escapeNamespace } from "@src/support/util"; + +const EndSelection = "End Selection"; + +const getValueForArgumentType = async ( + value: string, + argumentType: ArgumentType | undefined, + workspaceFolder: vscode.WorkspaceFolder, + uri: vscode.Uri, +): Promise => { + switch (argumentType) { + case "namespaceOrPath": + case "namespace": + // User can input a relative path, for example: NewFolder\NewFile + // or NewFolder/NewFile, so we need to convert it to a new Uri + const newUri = vscode.Uri.joinPath(uri, value); + + const fileName = path.parse(newUri.fsPath).name; + + // Always try to get the full namespace because it supports + // projects with modular architecture + let namespace = await getNamespace(workspaceFolder, newUri); + + if (!namespace && argumentType === "namespaceOrPath") { + return getValueForArgumentType( + value, + "path", + workspaceFolder, + uri, + ); + } + + namespace = namespace ? (namespace += `\\${fileName}`) : value; + + return escapeNamespace(namespace.replaceAll("/", "\\").trim()); + + case "path": + // OS path separators + return path.normalize(value.replaceAll("\\", "/")).trim(); + + default: + return value.trim(); + } +}; + +const validateInput = (input: string, field: string): boolean => { + if (input === "") { + vscode.window.showWarningMessage(`${field} is required`); + + return false; + } + + if (/\s/.test(input)) { + vscode.window.showWarningMessage(`${field} cannot contain spaces`); + + return false; + } + + return true; +}; + +const getUserArguments = async ( + commandArguments: Argument[], + workspaceFolder: vscode.WorkspaceFolder, + uri: vscode.Uri, +): Promise | undefined> => { + const userArguments: Record = {}; + + for (const argument of commandArguments) { + let input = undefined; + + while (!input) { + input = await vscode.window.showInputBox({ + prompt: argument.description, + }); + + // Exit when the user press ESC + if (input === undefined) { + return; + } + + if (!validateInput(input, `Argument ${argument.name}`)) { + input = undefined; + } + } + + userArguments[argument.name] = await getValueForArgumentType( + input, + argument.type, + workspaceFolder, + uri, + ); + } + + return userArguments; +}; + +const getArgumentsAsString = (userArguments: Record) => + Object.values(userArguments).join(" "); + +const getUserOptions = async ( + commandOptions: Option[] | undefined, + userArguments: Record, +): Promise | undefined> => { + const userOptions: Record = {}; + + if (!commandOptions?.length) { + return userOptions; + } + + let pickOptions = commandOptions.map((option) => ({ + label: `${option.name} ${option.description}`, + command: option.name, + excludeIf: option.excludeIf, + })); + + while (true) { + const optionsAsString = getOptionsAsString(userOptions); + + const choice = await vscode.window.showQuickPick( + [ + { + label: EndSelection, + command: EndSelection, + exclude: undefined, + }, + ...pickOptions, + ], + { + canPickMany: false, + placeHolder: + optionsAsString || "Select an option or end selection.", + }, + ); + + // Exit when the user press ESC + if (choice === undefined) { + return; + } + + if (choice.command === EndSelection) { + break; + } + + let value = undefined; + + const option = commandOptions.find( + (option) => option.name === choice.command, + ); + + if (option?.type === "select" && option?.options) { + const optionsChoice = await vscode.window.showQuickPick( + Object.entries(option.options()).map(([key, value]) => ({ + label: key, + command: value, + })), + ); + + // Once again if the user cancels the selection by pressing ESC + if (optionsChoice === undefined) { + continue; + } + + value = optionsChoice.command; + } + + if (option?.type === "input") { + let input = undefined; + + while (!input) { + let _default = undefined; + + if (typeof option.default === "string") { + _default = option.default; + } + + if (typeof option.default === "function") { + _default = option.default(...Object.values(userArguments)); + } + + input = await vscode.window.showInputBox({ + prompt: option.description, + value: _default ?? "", + }); + + // Exit when the user press ESC + if (input === undefined) { + break; + } + + if (!validateInput(input, `Value for ${option.name}`)) { + input = undefined; + } + } + + // Once again if the user cancels the selection by pressing ESC + if (input === undefined) { + continue; + } + + value = input; + } + + userOptions[choice.command] = value ?? choice.command; + + pickOptions = pickOptions.filter( + (option) => + option.command !== choice.command && + !option.excludeIf?.includes(choice.command), + ); + + if (!pickOptions.length) { + break; + } + } + + return userOptions; +}; + +const getOptionsAsString = (userOptions: Record) => + Object.entries(userOptions) + .map(([key, value]) => (key !== value ? `${key}=${value}` : key)) + .join(" "); + +export const buildArtisanCommand = async ( + command: Command, + uri: vscode.Uri, + workspaceFolder: vscode.WorkspaceFolder, +): Promise => { + const userArguments = await getUserArguments( + command.arguments, + workspaceFolder, + uri, + ); + + if (!userArguments) { + return; + } + + const userOptions = await getUserOptions(command.options, userArguments); + + if (!userOptions) { + return; + } + + return `${command.name} ${getArgumentsAsString(userArguments)} ${getOptionsAsString(userOptions)}`; +}; diff --git a/src/artisan/commands/CastMakeCommand.ts b/src/artisan/commands/CastMakeCommand.ts new file mode 100644 index 00000000..8987dd62 --- /dev/null +++ b/src/artisan/commands/CastMakeCommand.ts @@ -0,0 +1,18 @@ +import { Command } from "../types"; + +export const CastMakeCommand: Command = { + name: "make:cast", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the cast class", + }, + ], + options: [ + { + name: "--inbound", + description: "Generate an inbound cast class", + }, + ], +}; diff --git a/src/artisan/commands/ChannelMakeCommand.ts b/src/artisan/commands/ChannelMakeCommand.ts new file mode 100644 index 00000000..9e1834e8 --- /dev/null +++ b/src/artisan/commands/ChannelMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "../options"; + +export const ChannelMakeCommand: Command = { + name: "make:channel", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the channel", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/ClassMakeCommand.ts b/src/artisan/commands/ClassMakeCommand.ts new file mode 100644 index 00000000..bd963fe9 --- /dev/null +++ b/src/artisan/commands/ClassMakeCommand.ts @@ -0,0 +1,18 @@ +import { Command } from "../types"; + +export const ClassMakeCommand: Command = { + name: "make:class", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the class", + }, + ], + options: [ + { + name: "--invokable", + description: "Generate a single method, invokable class", + }, + ], +}; diff --git a/src/artisan/commands/CommandMakeCommand.ts b/src/artisan/commands/CommandMakeCommand.ts new file mode 100644 index 00000000..39c66579 --- /dev/null +++ b/src/artisan/commands/CommandMakeCommand.ts @@ -0,0 +1,23 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "../options"; + +export const CommandMakeCommand: Command = { + name: "make:command", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the command", + }, + ], + options: [ + { + name: "--command", + type: "input", + description: + "The terminal command that will be used to invoke the class", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/ComponentMakeCommand.ts b/src/artisan/commands/ComponentMakeCommand.ts new file mode 100644 index 00000000..2cc77a99 --- /dev/null +++ b/src/artisan/commands/ComponentMakeCommand.ts @@ -0,0 +1,34 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "../options"; + +export const ComponentMakeCommand: Command = { + name: "make:component", + arguments: [ + { + name: "name", + type: "namespaceOrPath", + description: "The name of the component", + }, + ], + options: [ + { + name: "--path", + type: "input", + default: "components", + description: + "The location where the component view should be created", + }, + { + name: "--inline", + description: "Create a component that renders an inline view", + excludeIf: ["--view"], + }, + { + name: "--view", + description: "Create an anonymous component with only a view", + excludeIf: ["--inline"], + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/ControllerMakeCommand.ts b/src/artisan/commands/ControllerMakeCommand.ts new file mode 100644 index 00000000..91120511 --- /dev/null +++ b/src/artisan/commands/ControllerMakeCommand.ts @@ -0,0 +1,67 @@ +import { Command } from "../types"; +import { getModelClassnames } from "@src/repositories/models"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const ControllerMakeCommand: Command = { + name: "make:controller", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the controller", + }, + ], + options: [ + { + name: "--resource", + description: "Generate a resource controller class", + excludeIf: ["--invokable", "--api", "--singleton", "--creatable"], + }, + { + name: "--singleton", + description: "Generate a singleton resource controller class", + excludeIf: ["--invokable", "--api", "--resource"], + }, + { + name: "--api", + description: + "Exclude the create and edit methods from the controller", + excludeIf: [ + "--invokable", + "--resource", + "--singleton", + "--creatable", + ], + }, + { + name: "--invokable", + description: "Generate a single method, invokable class", + excludeIf: [ + "--model", + "--api", + "--resource", + "--singleton", + "--creatable", + ], + }, + { + name: "--creatable", + description: + "Indicate that a singleton resource should be creatable", + excludeIf: ["--invokable", "--api", "--resource"], + }, + { + name: "--model", + type: "select", + options: () => getModelClassnames(), + description: "Generate a resource controller for the given model", + excludeIf: ["--invokable"], + }, + { + name: "--requests", + description: "Generate FormRequest classes for store and update", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/EnumMakeCommand.ts b/src/artisan/commands/EnumMakeCommand.ts new file mode 100644 index 00000000..362a8dd5 --- /dev/null +++ b/src/artisan/commands/EnumMakeCommand.ts @@ -0,0 +1,24 @@ +import { Command } from "../types"; + +export const EnumMakeCommand: Command = { + name: "make:enum", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the enum", + }, + ], + options: [ + { + name: "--string", + description: "Generate a string backed enum.", + excludeIf: ["--int"], + }, + { + name: "--int", + description: "Generate an integer backed enum.", + excludeIf: ["--string"], + }, + ], +}; diff --git a/src/artisan/commands/EventMakeCommand.ts b/src/artisan/commands/EventMakeCommand.ts new file mode 100644 index 00000000..ccbcde56 --- /dev/null +++ b/src/artisan/commands/EventMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const EventMakeCommand: Command = { + name: "make:event", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the event", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/ExceptionMakeCommand.ts b/src/artisan/commands/ExceptionMakeCommand.ts new file mode 100644 index 00000000..77d5c212 --- /dev/null +++ b/src/artisan/commands/ExceptionMakeCommand.ts @@ -0,0 +1,22 @@ +import { Command } from "../types"; + +export const ExceptionMakeCommand: Command = { + name: "make:exception", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the exception", + }, + ], + options: [ + { + name: "--render", + description: "Create the exception with an empty render method", + }, + { + name: "--report", + description: "Create the exception with an empty report method", + }, + ], +}; diff --git a/src/artisan/commands/FactoryMakeCommand.ts b/src/artisan/commands/FactoryMakeCommand.ts new file mode 100644 index 00000000..51ca41a4 --- /dev/null +++ b/src/artisan/commands/FactoryMakeCommand.ts @@ -0,0 +1,21 @@ +import { Command } from "../types"; +import { getModelClassnames } from "@src/repositories/models"; + +export const FactoryMakeCommand: Command = { + name: "make:factory", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the factory", + }, + ], + options: [ + { + name: "--model", + type: "select", + options: () => getModelClassnames(), + description: "The name of the model", + }, + ], +}; diff --git a/src/artisan/commands/InterfaceMakeCommand.ts b/src/artisan/commands/InterfaceMakeCommand.ts new file mode 100644 index 00000000..545407a9 --- /dev/null +++ b/src/artisan/commands/InterfaceMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const InterfaceMakeCommand: Command = { + name: "make:interface", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the interface", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/JobMakeCommand.ts b/src/artisan/commands/JobMakeCommand.ts new file mode 100644 index 00000000..e57ac909 --- /dev/null +++ b/src/artisan/commands/JobMakeCommand.ts @@ -0,0 +1,21 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const JobMakeCommand: Command = { + name: "make:job", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the job", + }, + ], + options: [ + { + name: "--sync", + description: "Indicates that job should be synchronous", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/JobMiddlewareMakeCommand.ts b/src/artisan/commands/JobMiddlewareMakeCommand.ts new file mode 100644 index 00000000..7de4158c --- /dev/null +++ b/src/artisan/commands/JobMiddlewareMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const JobMiddlewareMakeCommand: Command = { + name: "make:job-middleware", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the job middleware", + }, + ], + options: [...testOptions, forceOption], +}; diff --git a/src/artisan/commands/ListenerMakeCommand.ts b/src/artisan/commands/ListenerMakeCommand.ts new file mode 100644 index 00000000..fc42333e --- /dev/null +++ b/src/artisan/commands/ListenerMakeCommand.ts @@ -0,0 +1,21 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const ListenerMakeCommand: Command = { + name: "make:listener", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the listener", + }, + ], + options: [ + { + name: "--queued", + description: "Indicates that listener should be queued", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/LivewireMakeCommand.ts b/src/artisan/commands/LivewireMakeCommand.ts new file mode 100644 index 00000000..814fc6d1 --- /dev/null +++ b/src/artisan/commands/LivewireMakeCommand.ts @@ -0,0 +1,21 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const LivewireMakeCommand: Command = { + name: "make:livewire", + arguments: [ + { + name: "name", + type: "path", + description: "The name of the Livewire component", + }, + ], + options: [ + { + name: "--inline", + description: "Create a component that renders an inline view", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/MailMakeCommand.ts b/src/artisan/commands/MailMakeCommand.ts new file mode 100644 index 00000000..2c057e04 --- /dev/null +++ b/src/artisan/commands/MailMakeCommand.ts @@ -0,0 +1,38 @@ +import { Command } from "../types"; +import { kebab } from "@src/support/str"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const MailMakeCommand: Command = { + name: "make:mail", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the mail", + }, + ], + options: [ + { + name: "--markdown", + description: "Create a new Markdown template for the mailable", + type: "input", + default: (name: string): string => + kebab( + name.replaceAll("\\\\", "/").split("/").slice(-2).join("/"), + ), + excludeIf: ["--view"], + }, + { + name: "--view", + description: "Create a new Blade template for the mailable", + type: "input", + default: (name: string): string => + kebab( + name.replaceAll("\\\\", "/").split("/").slice(-2).join("/"), + ), + excludeIf: ["--markdown"], + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/MiddlewareMakeCommand.ts b/src/artisan/commands/MiddlewareMakeCommand.ts new file mode 100644 index 00000000..e002ae96 --- /dev/null +++ b/src/artisan/commands/MiddlewareMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const MiddlewareMakeCommand: Command = { + name: "make:middleware", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the middleware", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/MigrationMakeCommand.ts b/src/artisan/commands/MigrationMakeCommand.ts new file mode 100644 index 00000000..dc7cb88e --- /dev/null +++ b/src/artisan/commands/MigrationMakeCommand.ts @@ -0,0 +1,26 @@ +import { Command } from "../types"; + +export const MigrationMakeCommand: Command = { + name: "make:migration", + arguments: [ + { + name: "name", + type: "path", + description: "The name of the migration", + }, + ], + options: [ + { + name: "--create", + type: "input", + description: "The table to be created", + excludeIf: ["--table"], + }, + { + name: "--table", + type: "input", + description: "The table to migrate", + excludeIf: ["--create"], + }, + ], +}; diff --git a/src/artisan/commands/ModelMakeCommand.ts b/src/artisan/commands/ModelMakeCommand.ts new file mode 100644 index 00000000..429c3c48 --- /dev/null +++ b/src/artisan/commands/ModelMakeCommand.ts @@ -0,0 +1,71 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const ModelMakeCommand: Command = { + name: "make:model", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the model", + }, + ], + options: [ + { + name: "--all", + description: + "Generate a migration, seeder, factory, policy, resource controller, and form request classes for the model", + }, + { + name: "--controller", + description: "Create a new controller for the model", + }, + { + name: "--factory", + description: "Create a new factory for the model", + }, + { + name: "--migration", + description: "Create a new migration file for the model", + }, + { + name: "--morph-pivot", + description: + "Indicates if the generated model should be a custom polymorphic intermediate table model", + excludeIf: ["--pivot"], + }, + { + name: "--policy", + description: "Create a new policy for the model", + }, + { + name: "--seed", + description: "Create a new seeder for the model", + }, + { + name: "--pivot", + description: + "Indicates if the generated model should be a custom intermediate table model", + excludeIf: ["--morph-pivot"], + }, + { + name: "--resource", + description: + "Indicates if the generated controller should be a resource controller", + excludeIf: ["--api"], + }, + { + name: "--api", + description: + "Indicates if the generated controller should be an API resource controller", + excludeIf: ["--resource"], + }, + { + name: "--requests", + description: + "Create new form request classes and use them in the resource controller", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/NotificationMakeCommand.ts b/src/artisan/commands/NotificationMakeCommand.ts new file mode 100644 index 00000000..4184ba49 --- /dev/null +++ b/src/artisan/commands/NotificationMakeCommand.ts @@ -0,0 +1,21 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const NotificationMakeCommand: Command = { + name: "make:notification", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the notification", + }, + ], + options: [ + { + name: "--markdown", + description: "Create a new Markdown template for the notification", + }, + ...testOptions, + forceOption, + ], +}; diff --git a/src/artisan/commands/ObserverMakeCommand.ts b/src/artisan/commands/ObserverMakeCommand.ts new file mode 100644 index 00000000..8b5aec8d --- /dev/null +++ b/src/artisan/commands/ObserverMakeCommand.ts @@ -0,0 +1,21 @@ +import { Command } from "../types"; +import { getModelClassnames } from "@src/repositories/models"; + +export const ObserverMakeCommand: Command = { + name: "make:observer", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the observer", + }, + ], + options: [ + { + name: "--model", + type: "select", + options: () => getModelClassnames(), + description: "The model that the observer applies to", + }, + ], +}; diff --git a/src/artisan/commands/PolicyMakeCommand.ts b/src/artisan/commands/PolicyMakeCommand.ts new file mode 100644 index 00000000..924ff49b --- /dev/null +++ b/src/artisan/commands/PolicyMakeCommand.ts @@ -0,0 +1,26 @@ +import { Command } from "../types"; +import { getModelClassnames } from "@src/repositories/models"; + +export const PolicyMakeCommand: Command = { + name: "make:policy", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the policy", + }, + ], + options: [ + { + name: "--model", + type: "select", + options: () => getModelClassnames(), + description: "The model that the policy applies to", + }, + { + name: "--guard", + type: "input", + description: "The guard that the policy relies on", + }, + ], +}; diff --git a/src/artisan/commands/ProviderMakeCommand.ts b/src/artisan/commands/ProviderMakeCommand.ts new file mode 100644 index 00000000..a8e8814f --- /dev/null +++ b/src/artisan/commands/ProviderMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const ProviderMakeCommand: Command = { + name: "make:provider", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the service provider", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/RequestMakeCommand.ts b/src/artisan/commands/RequestMakeCommand.ts new file mode 100644 index 00000000..ce1c5151 --- /dev/null +++ b/src/artisan/commands/RequestMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const RequestMakeCommand: Command = { + name: "make:request", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the request", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/ResourceMakeCommand.ts b/src/artisan/commands/ResourceMakeCommand.ts new file mode 100644 index 00000000..c8a10ec9 --- /dev/null +++ b/src/artisan/commands/ResourceMakeCommand.ts @@ -0,0 +1,18 @@ +import { Command } from "../types"; + +export const ResourceMakeCommand: Command = { + name: "make:resource", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the resource", + }, + ], + options: [ + { + name: "--collection", + description: "Create a resource collection", + }, + ], +}; diff --git a/src/artisan/commands/ScopeMakeCommand.ts b/src/artisan/commands/ScopeMakeCommand.ts new file mode 100644 index 00000000..ecdd0c44 --- /dev/null +++ b/src/artisan/commands/ScopeMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const ScopeMakeCommand: Command = { + name: "make:scope", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the scope", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/SeederMakeCommand.ts b/src/artisan/commands/SeederMakeCommand.ts new file mode 100644 index 00000000..8b5a7890 --- /dev/null +++ b/src/artisan/commands/SeederMakeCommand.ts @@ -0,0 +1,12 @@ +import { Command } from "../types"; + +export const SeederMakeCommand: Command = { + name: "make:seeder", + arguments: [ + { + name: "name", + type: "path", + description: "The name of the seeder", + }, + ], +}; diff --git a/src/artisan/commands/TestMakeCommand.ts b/src/artisan/commands/TestMakeCommand.ts new file mode 100644 index 00000000..5b3a3a82 --- /dev/null +++ b/src/artisan/commands/TestMakeCommand.ts @@ -0,0 +1,28 @@ +import { Command } from "../types"; + +export const TestMakeCommand: Command = { + name: "make:test", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the test", + }, + ], + options: [ + { + name: "--unit", + description: "Create a unit test", + }, + { + name: "--pest", + description: "Create a Pest test", + excludeIf: ["--phpunit"], + }, + { + name: "--phpunit", + description: "Create a PHPUnit test", + excludeIf: ["--pest"], + }, + ], +}; diff --git a/src/artisan/commands/TraitMakeCommand.ts b/src/artisan/commands/TraitMakeCommand.ts new file mode 100644 index 00000000..98351a87 --- /dev/null +++ b/src/artisan/commands/TraitMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption } from "@src/artisan/options"; + +export const TraitMakeCommand: Command = { + name: "make:trait", + arguments: [ + { + name: "name", + type: "namespace", + description: "The name of the trait", + }, + ], + options: [forceOption], +}; diff --git a/src/artisan/commands/ViewMakeCommand.ts b/src/artisan/commands/ViewMakeCommand.ts new file mode 100644 index 00000000..da8cb1b5 --- /dev/null +++ b/src/artisan/commands/ViewMakeCommand.ts @@ -0,0 +1,14 @@ +import { Command } from "../types"; +import { forceOption, testOptions } from "@src/artisan/options"; + +export const ViewMakeCommand: Command = { + name: "make:view", + arguments: [ + { + name: "name", + type: "path", + description: "The name of the view", + }, + ], + options: [...testOptions, forceOption], +}; diff --git a/src/artisan/options.ts b/src/artisan/options.ts new file mode 100644 index 00000000..b1ec57a6 --- /dev/null +++ b/src/artisan/options.ts @@ -0,0 +1,24 @@ +import { Option } from "./types"; + +export const forceOption: Option = { + name: "--force", + description: "Create the class even if the cast already exists", +}; + +export const testOptions: Option[] = [ + { + name: "--test", + description: "Generate an accompanying Test test for the class", + excludeIf: ["--pest", "--phpunit"], + }, + { + name: "--pest", + description: "Generate an accompanying Pest test for the class", + excludeIf: ["--phpunit"], + }, + { + name: "--phpunit", + description: "Generate an accompanying PHPUnit test for the class", + excludeIf: ["--pest"], + }, +]; diff --git a/src/artisan/registry.ts b/src/artisan/registry.ts new file mode 100644 index 00000000..f7491267 --- /dev/null +++ b/src/artisan/registry.ts @@ -0,0 +1,74 @@ +import * as vscode from "vscode"; +import { runArtisanMakeCommand } from "@src/commands/artisan"; + +import { CastMakeCommand } from "./commands/CastMakeCommand"; +import { ChannelMakeCommand } from "./commands/ChannelMakeCommand"; +import { ClassMakeCommand } from "./commands/ClassMakeCommand"; +import { CommandMakeCommand } from "./commands/CommandMakeCommand"; +import { ComponentMakeCommand } from "./commands/ComponentMakeCommand"; +import { ControllerMakeCommand } from "./commands/ControllerMakeCommand"; +import { EnumMakeCommand } from "./commands/EnumMakeCommand"; +import { EventMakeCommand } from "./commands/EventMakeCommand"; +import { ExceptionMakeCommand } from "./commands/ExceptionMakeCommand"; +import { FactoryMakeCommand } from "./commands/FactoryMakeCommand"; +import { InterfaceMakeCommand } from "./commands/InterfaceMakeCommand"; +import { JobMakeCommand } from "./commands/JobMakeCommand"; +import { JobMiddlewareMakeCommand } from "./commands/JobMiddlewareMakeCommand"; +import { ListenerMakeCommand } from "./commands/ListenerMakeCommand"; +import { LivewireMakeCommand } from "./commands/LivewireMakeCommand"; +import { MailMakeCommand } from "./commands/MailMakeCommand"; +import { MiddlewareMakeCommand } from "./commands/MiddlewareMakeCommand"; +import { MigrationMakeCommand } from "./commands/MigrationMakeCommand"; +import { ModelMakeCommand } from "./commands/ModelMakeCommand"; +import { NotificationMakeCommand } from "./commands/NotificationMakeCommand"; +import { ObserverMakeCommand } from "./commands/ObserverMakeCommand"; +import { PolicyMakeCommand } from "./commands/PolicyMakeCommand"; +import { ProviderMakeCommand } from "./commands/ProviderMakeCommand"; +import { RequestMakeCommand } from "./commands/RequestMakeCommand"; +import { ResourceMakeCommand } from "./commands/ResourceMakeCommand"; +import { ScopeMakeCommand } from "./commands/ScopeMakeCommand"; +import { SeederMakeCommand } from "./commands/SeederMakeCommand"; +import { TestMakeCommand } from "./commands/TestMakeCommand"; +import { TraitMakeCommand } from "./commands/TraitMakeCommand"; +import { ViewMakeCommand } from "./commands/ViewMakeCommand"; + +const artisanMakeCommands = { + "laravel.artisan.make.cast": CastMakeCommand, + "laravel.artisan.make.channel": ChannelMakeCommand, + "laravel.artisan.make.class": ClassMakeCommand, + "laravel.artisan.make.command": CommandMakeCommand, + "laravel.artisan.make.component": ComponentMakeCommand, + "laravel.artisan.make.controller": ControllerMakeCommand, + "laravel.artisan.make.enum": EnumMakeCommand, + "laravel.artisan.make.event": EventMakeCommand, + "laravel.artisan.make.exception": ExceptionMakeCommand, + "laravel.artisan.make.factory": FactoryMakeCommand, + "laravel.artisan.make.interface": InterfaceMakeCommand, + "laravel.artisan.make.job": JobMakeCommand, + "laravel.artisan.make.job-middleware": JobMiddlewareMakeCommand, + "laravel.artisan.make.listener": ListenerMakeCommand, + "laravel.artisan.make.livewire": LivewireMakeCommand, + "laravel.artisan.make.mail": MailMakeCommand, + "laravel.artisan.make.middleware": MiddlewareMakeCommand, + "laravel.artisan.make.migration": MigrationMakeCommand, + "laravel.artisan.make.model": ModelMakeCommand, + "laravel.artisan.make.notification": NotificationMakeCommand, + "laravel.artisan.make.observer": ObserverMakeCommand, + "laravel.artisan.make.policy": PolicyMakeCommand, + "laravel.artisan.make.provider": ProviderMakeCommand, + "laravel.artisan.make.request": RequestMakeCommand, + "laravel.artisan.make.resource": ResourceMakeCommand, + "laravel.artisan.make.scope": ScopeMakeCommand, + "laravel.artisan.make.seeder": SeederMakeCommand, + "laravel.artisan.make.test": TestMakeCommand, + "laravel.artisan.make.trait": TraitMakeCommand, + "laravel.artisan.make.view": ViewMakeCommand, +}; + +export const registerArtisanMakeCommands = () => { + return Object.entries(artisanMakeCommands).map(([name, command]) => { + return vscode.commands.registerCommand(name, (uri: vscode.Uri) => { + runArtisanMakeCommand(command, uri); + }); + }); +}; diff --git a/src/artisan/types.ts b/src/artisan/types.ts new file mode 100644 index 00000000..b4d61088 --- /dev/null +++ b/src/artisan/types.ts @@ -0,0 +1,24 @@ +export interface Command { + name: string; + arguments: Argument[]; + options?: Option[]; +} + +export interface Option { + name: string; + type?: OptionType | undefined; + options?: () => Record; + default?: ((...args: string[]) => string) | string; + description?: string; + excludeIf?: string[]; +} + +export type OptionType = "select" | "input"; + +export interface Argument { + name: string; + type?: ArgumentType | undefined; + description?: string; +} + +export type ArgumentType = "namespaceOrPath" | "namespace" | "path"; diff --git a/src/commands/artisan.ts b/src/commands/artisan.ts new file mode 100644 index 00000000..98438547 --- /dev/null +++ b/src/commands/artisan.ts @@ -0,0 +1,91 @@ +import * as vscode from "vscode"; + +import { Command } from "@src/artisan/types"; +import { buildArtisanCommand } from "@src/artisan/builder"; +import { artisan } from "@src/support/php"; +import { getPathFromOutput } from "@src/support/artisan"; +import { getWorkspaceFolders } from "@src/support/project"; +import { openFileCommand } from "."; + +export const runArtisanMakeCommand = async ( + command: Command, + uri?: vscode.Uri | undefined, +) => { + const result = await runArtisanCommand(command, uri); + + if (!result) { + return; + } + + const outputPath = getPathFromOutput( + result.output, + command.name, + result.workspaceFolder, + result.uri, + ); + + if (outputPath) { + openFileCommand(vscode.Uri.file(outputPath), 1, 1); + } +}; + +export const runArtisanCommand = async ( + command: Command, + uri?: vscode.Uri | undefined, +) => { + const workspaceFolder = getWorkspaceFolder(uri); + + if (!workspaceFolder) { + vscode.window.showErrorMessage("Cannot detect active workspace"); + + return; + } + + uri ??= vscode.Uri.joinPath(workspaceFolder.uri); + + const artisanCommand = await buildArtisanCommand( + command, + uri, + workspaceFolder, + ); + + if (!artisanCommand) { + return; + } + + const output = await artisan(artisanCommand, workspaceFolder.uri.fsPath); + + const error = output.match(/ERROR\s+(.*)/); + + if (error?.[1]) { + vscode.window.showErrorMessage(error[1]); + + return; + } + + return { output, workspaceFolder, uri }; +}; + +const getWorkspaceFolder = ( + uri: vscode.Uri | undefined, +): vscode.WorkspaceFolder | undefined => { + if (uri) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + + if (workspaceFolder) { + return workspaceFolder; + } + } + + if (vscode.window.activeTextEditor) { + const fileUri = vscode.window.activeTextEditor.document.uri; + + const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri); + + if (workspaceFolder) { + return workspaceFolder; + } + } + + return getWorkspaceFolders()?.[0]; +}; diff --git a/src/commands/generateNamespace.ts b/src/commands/generateNamespace.ts index 189ea8cc..087b4c4f 100644 --- a/src/commands/generateNamespace.ts +++ b/src/commands/generateNamespace.ts @@ -34,6 +34,48 @@ const getPsr4Autoloads = async ( }; }; +export const getNamespace = async ( + workspaceFolder: vscode.WorkspaceFolder, + fileUri: vscode.Uri, +): Promise => { + const composerPath = vscode.Uri.joinPath( + workspaceFolder.uri, + "composer.json", + ); + + const autoloads = await getPsr4Autoloads(composerPath); + + const namespaces: Namespace[] = Object.entries(autoloads) + .map(([namespace, path]) => ({ namespace, path })) + // We need to sort by length, because we need to check longer paths first, for example: + // + // "psr-4": { + // "App\\": "app/", + // "App\\AnotherNamespace\\": "app/anotherPath/", + // } + // + // Otherwise, the system will first find the shorter one, which also matches + .sort((a, b) => b.path.length - a.path.length); + + const findNamespace = namespaces.find((namespace) => + fileUri.path.startsWith( + `${workspaceFolder.uri.path}/${namespace.path}`, + ), + ); + + if (!findNamespace) { + return; + } + + return ( + findNamespace.namespace + + fileUri.path + .replace(`${workspaceFolder.uri.path}/${findNamespace.path}`, "") + .replace(/\/?[^\/]+$/, "") + .replace(/\//g, "\\") + ).replace(/\\$/, ""); +}; + const getNamespaceReplacement = ( newNamespace: string, content: string, @@ -88,45 +130,14 @@ export const generateNamespaceCommand = async () => { return; } - const composerPath = vscode.Uri.joinPath( - workspaceFolder.uri, - "composer.json", - ); - - const autoloads = await getPsr4Autoloads(composerPath); - - const namespaces: Namespace[] = Object.entries(autoloads) - .map(([namespace, path]) => ({ namespace, path })) - // We need to sort by length, because we need to check longer paths first, for example: - // - // "psr-4": { - // "App\\": "app/", - // "App\\AnotherNamespace\\": "app/anotherPath/", - // } - // - // Otherwise, the system will first find the shorter one, which also matches - .sort((a, b) => b.path.length - a.path.length); - - const findNamespace = namespaces.find((namespace) => - fileUri.path.startsWith( - `${workspaceFolder.uri.path}/${namespace.path}`, - ), - ); + const newNamespace = await getNamespace(workspaceFolder, fileUri); - if (!findNamespace) { + if (!newNamespace) { vscode.window.showErrorMessage("Failed to find a matching namespace"); return; } - const newNamespace = ( - findNamespace.namespace + - fileUri.path - .replace(`${workspaceFolder.uri.path}/${findNamespace.path}`, "") - .replace(/\/?[^\/]+$/, "") - .replace(/\//g, "\\") - ).replace(/\\$/, ""); - const doc = editor.document; const text = doc.getText(); diff --git a/src/commands/generatedRegisteredCommands.ts b/src/commands/generatedRegisteredCommands.ts index 4f5da2d4..fec53170 100644 --- a/src/commands/generatedRegisteredCommands.ts +++ b/src/commands/generatedRegisteredCommands.ts @@ -14,4 +14,35 @@ export type RegisteredCommand = | "laravel.refactorSelectedHtmlClassToBladeDirective" | "laravel.refactorAllHtmlClassesToBladeDirectives" | "laravel.namespace.generate" + | "laravel.artisan.make.cast" + | "laravel.artisan.make.channel" + | "laravel.artisan.make.class" + | "laravel.artisan.make.command" + | "laravel.artisan.make.component" + | "laravel.artisan.make.controller" + | "laravel.artisan.make.enum" + | "laravel.artisan.make.event" + | "laravel.artisan.make.exception" + | "laravel.artisan.make.factory" + | "laravel.artisan.make.interface" + | "laravel.artisan.make.job" + | "laravel.artisan.make.job-middleware" + | "laravel.artisan.make.listener" + | "laravel.artisan.make.livewire" + | "laravel.artisan.make.mail" + | "laravel.artisan.make.middleware" + | "laravel.artisan.make.migration" + | "laravel.artisan.make.model" + | "laravel.artisan.make.notification" + | "laravel.artisan.make.observer" + | "laravel.artisan.make.policy" + | "laravel.artisan.make.provider" + | "laravel.artisan.make.request" + | "laravel.artisan.make.resource" + | "laravel.artisan.make.rule" + | "laravel.artisan.make.scope" + | "laravel.artisan.make.seeder" + | "laravel.artisan.make.test" + | "laravel.artisan.make.trait" + | "laravel.artisan.make.view" | "laravel.open"; diff --git a/src/extension.ts b/src/extension.ts index a8f536bf..e2a8a643 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -45,6 +45,7 @@ import { } from "./support/php"; import { hasWorkspace, projectPathExists } from "./support/project"; import { cleanUpTemp } from "./support/util"; +import { registerArtisanMakeCommands } from "./artisan/registry"; let client: LanguageClient; @@ -257,6 +258,7 @@ export async function activate(context: vscode.ExtensionContext) { htmlClassToBladeDirectiveCommands.all, refactorAllHtmlClassesToBladeDirectives, ), + ...registerArtisanMakeCommands(), ); collectDebugInfo(); diff --git a/src/repositories/models.ts b/src/repositories/models.ts index 3d045a3c..000c3426 100644 --- a/src/repositories/models.ts +++ b/src/repositories/models.ts @@ -2,6 +2,7 @@ import { repository } from "."; import { Eloquent } from ".."; import { writeEloquentDocBlocks } from "../support/docblocks"; import { runInLaravel, template } from "./../support/php"; +import { escapeNamespace } from "../support/util"; const modelPaths = ["app", "app/Models"]; @@ -29,3 +30,12 @@ export const getModels = repository({ .map((path) => `${path}/*.php`), itemsDefault: {}, }); + +export const getModelClassnames = (): Record => { + return Object.fromEntries( + Object.entries(getModels().items).map(([_, model]) => [ + model.class, + escapeNamespace(model.class), + ]), + ); +}; diff --git a/src/support/artisan.ts b/src/support/artisan.ts new file mode 100644 index 00000000..034dd322 --- /dev/null +++ b/src/support/artisan.ts @@ -0,0 +1,40 @@ +import * as vscode from "vscode"; +import path from "path"; + +export const getPathFromOutput = ( + output: string, + command: string, + workspaceFolder: vscode.WorkspaceFolder, + uri: vscode.Uri, +): string | undefined => { + if (command === "make:livewire") { + const paths = output.match(/CLASS:\s+(.*)/); + + if (paths?.[1]) { + return path.join(workspaceFolder.uri.fsPath, paths?.[1]); + } + } + + const paths = output.match(/\[(.*?)\]/g)?.map((path) => path.slice(1, -1)); + + if (!paths) { + return; + } + + // If Artisan command creates multiple files, + // locate the primary path in the output. + const outputPath = paths + .map((_path) => + path.isAbsolute(_path) + ? path.relative(workspaceFolder.uri.fsPath, _path) + : _path, + ) + .map((_path) => path.join(workspaceFolder.uri.fsPath, _path)) + .find((_path) => _path.startsWith(uri.fsPath)); + + if (!outputPath) { + return; + } + + return outputPath; +}; diff --git a/src/support/php.ts b/src/support/php.ts index 52a8f99b..8d91ba84 100644 --- a/src/support/php.ts +++ b/src/support/php.ts @@ -319,14 +319,17 @@ export const runPhp = ( }); }; -export const artisan = (command: string): Promise => { - const fullCommand = projectPath("artisan") + " " + command; +export const artisan = ( + command: string, + workspaceFolder: string, +): Promise => { + const fullCommand = `${getCommand("artisan")} ${command}`.trim(); return new Promise((resolve, error) => { cp.exec( fullCommand, { - cwd: getWorkspaceFolders()[0]?.uri?.fsPath, + cwd: workspaceFolder, }, (err, stdout, stderr) => { if (err === null) { diff --git a/src/support/str.ts b/src/support/str.ts new file mode 100644 index 00000000..acfdba2f --- /dev/null +++ b/src/support/str.ts @@ -0,0 +1,8 @@ +export const kebab = (str: string): string => + str + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replace(/[\s_]+/g, "-") + .toLowerCase(); + +export const ucfirst = (str: string): string => + str.charAt(0).toUpperCase() + str.slice(1); diff --git a/src/support/util.ts b/src/support/util.ts index 72325cc3..55bcefb1 100644 --- a/src/support/util.ts +++ b/src/support/util.ts @@ -146,3 +146,16 @@ export const createIndexMapping = ( }, }; }; + +export const escapeNamespace = (namespace: string): string => { + if ( + ["linux", "openbsd", "sunos", "darwin"].some((unixPlatforms) => + os.platform().includes(unixPlatforms), + ) + ) { + // We need to escape backslashes because finally it will be a part of CLI command + return namespace.replace(/(?