diff --git a/CHANGELOG.md b/CHANGELOG.md index 962771e..e6ea4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 1.3.2 + +- Replaced vscode config `adonisjs.inertia.pagesDirectory` by `adonisjs.inertia.pagesDirectories` to support inertia page resolver working with multiple directories. +- Added vscode config `adonisjs.app.controllersDirectories` to support projects using modules with multiple controllers directories via `@adonisjs-community/modules`. + ## 1.3.1 - Handle case where the `node_modules` folder is not present in your project and an error is shown in the output panel when executing an ace command. This is now fixed and the extension will instead display a small warning `Please install your dependencies`. diff --git a/README.md b/README.md index a33e1e3..b17a1e3 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ Autocompletion for the name and the handler of controllers. Ctrl + Ctrl + Click - `runMigrationInBackground`: Run migration/seeds commands in background. By default, they are executed in the built-in terminal of VSCode so that you can see the output. -- `pagesDirectory` : The directory where your Inertia.js pages are located. Default is `inertia/pages` +- `controllersDirectories` : Array of controllers directories. Default: `["app/controllers", "app/Controllers/Http"]`. +- `pagesDirectories` : Array of Inertia pages directories. Default: `["inertia/pages"]`. ## IntelliSense while typing In the context of controller and view autocompletion, we are inside strings. By default, VSCode totally disables the display of IntelliSense suggestions inside strings. If you want to see the autocompletion of your controllers and views, you will have to press Ctrl + Space to manually trigger IntelliSense. diff --git a/src/index.ts b/src/index.ts index 9b0b538..a108764 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,19 +2,21 @@ import { commands, languages } from 'vscode' import type { ExtensionContext } from 'vscode' import { Logger } from '#vscode/logger' -import { Extension } from '#vscode/extension' import ExtConfig from '#vscode/utilities/config' import ProjectManager from '#vscode/project_manager' import type { AdonisProject } from '#types/projects' import { registerAceCommands } from '#vscode/commands' import { ViewContainer } from '#vscode/tree_views/index' import { registerDocsCommands } from '#vscode/commands/docs' +import { Extension, setExtensionContext } from '#vscode/extension' import InertiaLinkProvider from '#vscode/providers/inertia/link_provider' import { RouteControllerLinkProvider } from '#vscode/providers/routes/link_provider' import { InertiaCompletionProvider } from '#vscode/providers/inertia/completion_provider' import RouteControllerCompletionProvider from '#vscode/providers/routes/completion_provider' export async function activate(context: ExtensionContext) { + setExtensionContext(context) + console.log('Activating AdonisJS extension...') const projects = await ProjectManager.load() diff --git a/src/linkers/controllers_linker.ts b/src/linkers/controllers_linker.ts index 0979346..523ca92 100644 --- a/src/linkers/controllers_linker.ts +++ b/src/linkers/controllers_linker.ts @@ -37,26 +37,24 @@ export class ControllersLinker { return { controllerPath: null, controller: null, position } } - let controllersDirectory = '' - - if (options.project.isAdonis5()) { - controllersDirectory = 'app/Controllers/Http' - } else if (options.project.isAdonis6()) { - controllersDirectory = 'app/controllers' + const directories = options.project.isAdonis5() + ? ['app/Controllers/Http', 'app/controllers'] + : ['app/controllers', 'app/Controllers/Http'] + + for (const dir of directories) { + const absPath = join(options.project.path, dir) + const filePath = `${join(absPath, controller.namespace || '', controller.name)}.ts` + + if (existsSync(filePath)) { + return { + controller, + controllerPath: filePath, + position, + } + } } - const absPath = join(options.project.path, controllersDirectory) - const filePath = `${join(absPath, controller.namespace || '', controller.name)}.ts` - - if (!existsSync(filePath)) { - return { controller, controllerPath: null, position } - } - - return { - controller, - controllerPath: filePath, - position, - } + return { controller, controllerPath: null, position } }) return Promise.all(promises) diff --git a/src/linkers/inertia_linker.ts b/src/linkers/inertia_linker.ts index 5d11c62..cf3ae4f 100644 --- a/src/linkers/inertia_linker.ts +++ b/src/linkers/inertia_linker.ts @@ -1,6 +1,7 @@ import fg from 'fast-glob' import { join } from 'node:path' +import { Logger } from '#vscode/logger' import { slash } from '../utilities/index' import { inertiaRegex } from '../utilities/regexes' import type { InertiaLink } from '../types/linkers' @@ -24,42 +25,82 @@ export class InertiaLinker { static async getLinks(options: { fileContent: string project: AdonisProject - pagesDirectory: string + pagesDirectories: string[] }): Promise { + Logger.debug( + `[InertiaLinker] pagesDirectories=${JSON.stringify(options.pagesDirectories)} project=${options.project.path}` + ) const matches = options.fileContent.matchAll(inertiaRegex) || [] const matchesArray = Array.from(matches) const promises = matchesArray.map(async (match) => { - const fileName = match[1]!.replace(/"|'/g, '').replace(/\./g, '/') + const pageName = match[1]!.replace(/"|'/g, '').replace(/\./g, '/') - const fullName = join( - options.project.path, - options.pagesDirectory, - `${fileName}.{vue,jsx,tsx,svelte}` + const pathPatterns = options.pagesDirectories.map((dir) => + slash(join(options.project.path, dir, `${pageName}.{vue,jsx,tsx,svelte}`)) ) - const pattern = slash(fullName) + Logger.debug( + `[InertiaLinker] looking for page via path pageName="${pageName}" pathPatterns=${JSON.stringify( + pathPatterns + )}` + ) - const files = await fg(pattern, { + const filesViaPath = await fg(pathPatterns, { onlyFiles: true, caseSensitiveMatch: false, cwd: slash(options.project.path), }) + Logger.debug( + `[InertiaLinker] matched files via path for pageName="${pageName}": ${JSON.stringify(filesViaPath)}` + ) + const position = InertiaLinker.#matchIndexToPosition({ fileContent: options.fileContent, match, }) - if (!files.length) { - return { templatePath: null, position } + if (filesViaPath.length) { + return { + templatePath: slash(filesViaPath[0]!), + position, + } } - return { - templatePath: slash(files[0]!), - position, + const fileName = pageName.split('/').pop()! + + const wildcardPatterns = options.pagesDirectories.map((dir) => + slash(join(options.project.path, dir, `**/${fileName}.{vue,jsx,tsx,svelte}`)) + ) + + Logger.debug( + `[InertiaLinker] looking for page via wildcard fileName="${fileName}" wildcardPatterns=${JSON.stringify( + wildcardPatterns + )}` + ) + + const filesViaWildcard = await fg(wildcardPatterns, { + onlyFiles: true, + caseSensitiveMatch: false, + cwd: slash(options.project.path), + }) + + Logger.debug( + `[InertiaLinker] matched files via wildcard for fileName="${fileName}": ${JSON.stringify( + filesViaWildcard + )}` + ) + + if (filesViaWildcard.length) { + return { + templatePath: slash(filesViaWildcard[0]!), + position, + } } + + return { templatePath: null, position } }) const result = await Promise.all(promises) diff --git a/src/suggesters/controller_suggester.ts b/src/suggesters/controller_suggester.ts index b8b557b..123de30 100644 --- a/src/suggesters/controller_suggester.ts +++ b/src/suggesters/controller_suggester.ts @@ -1,8 +1,8 @@ import fg from 'fast-glob' import { join, normalize, relative } from 'node:path' -import { slash } from '../utilities/index' import type { Suggestion } from '../types' +import { slash } from '../utilities/index' import type { AdonisProject } from '../types/projects' import { getMethodsInSourceFile } from '../utilities/misc' @@ -25,30 +25,21 @@ export class ControllerSuggester { } /** - * Returns an absolute path to the app controllers directory - * - * app/controllers for Adonis 6 - * app/Controllers/Http for Adonis 5 - * - * TODO: Will need to use RC file namespaces to support custom directories + * Returns absolute paths to the app controllers directories */ - static #getAppControllersDirectory(project: AdonisProject) { - let relativePath = '' - if (project.isAdonis6()) { - relativePath = 'app/controllers' - } else { - relativePath = 'app/Controllers/Http' - } - - return join(project.path, relativePath) + static #getAppControllersDirectories(project: AdonisProject) { + const dirs = project.isAdonis6() + ? ['app/controllers', 'app/Controllers/Http'] + : ['app/Controllers/Http', 'app/controllers'] + return dirs.map((dir) => join(project.path, dir)) } /** * Get all controllers file paths in the project. */ - static async #getAllControllers(controllersDirectory: string) { - const globPattern = slash(`${controllersDirectory}/**/**.ts`) - return fg(globPattern, { + static async #getAllControllers(controllersDirectories: string[]) { + const globPatterns = controllersDirectories.map((dir) => slash(`${dir}/**/*.ts`)) + return fg(globPatterns, { onlyFiles: true, caseSensitiveMatch: false, }) @@ -58,16 +49,9 @@ export class ControllerSuggester { * Given a list of controllers files and the text input, filter the files * to keep only the ones matching */ - static #filterControllersFiles(options: { - controllersFiles: string[] - controllersDirectory: string - text: string - }) { - const { controllersFiles, controllersDirectory, text } = options - - const regexPattern = `${controllersDirectory}/(.*)${text}(.*).ts`.replaceAll('\\', '/') - const regex = new RegExp(regexPattern, 'i') - + static #filterControllersFiles(options: { controllersFiles: string[]; text: string }) { + const { controllersFiles, text } = options + const regex = new RegExp(`(.*)${text}(.*)\\.ts$`, 'i') return controllersFiles.filter((file) => regex.test(file)) } @@ -76,22 +60,22 @@ export class ControllerSuggester { project: AdonisProject }): Promise { const text = this.#sanitizeInput(options.project, options.text) - - const controllersDirectory = this.#getAppControllersDirectory(options.project) - const controllersFiles = await this.#getAllControllers(controllersDirectory) + const controllersDirectories = this.#getAppControllersDirectories(options.project) + const controllersFiles = await this.#getAllControllers(controllersDirectories) const foundFiles = this.#filterControllersFiles({ controllersFiles, - controllersDirectory, text, }) return foundFiles.map((file) => { - const controllerName = slash(relative(controllersDirectory, file).replace('.ts', '')) + const baseDir = + controllersDirectories.find((dir) => file.startsWith(dir)) || controllersDirectories[0]! + const controllerName = slash(relative(baseDir, file).replace('.ts', '')) const withSubpathPrefix = `#controllers/${controllerName}` const fileMethods = getMethodsInSourceFile(normalize(file)) - const bulletListMethods = fileMethods.map((method) => `* ${method}`).join('\n') + const bulletListMethods = fileMethods.map((method) => `* ${method}`)?.join('\n') return { text: options.project.isAdonis6() ? withSubpathPrefix : controllerName, diff --git a/src/suggesters/inertia_suggester.ts b/src/suggesters/inertia_suggester.ts index 749ec53..31d8668 100644 --- a/src/suggesters/inertia_suggester.ts +++ b/src/suggesters/inertia_suggester.ts @@ -1,37 +1,36 @@ import fg from 'fast-glob' import { join, normalize, relative } from 'node:path' -import { slash } from '../utilities/index' import type { Suggestion } from '../types' +import { slash } from '../utilities/index' import type { AdonisProject } from '../types/projects' export class InertiaSuggester { static async getInertiaSuggestions(options: { text: string project: AdonisProject - pagesDirectory: string + pagesDirectories: string[] }): Promise { - const text = options.text.replaceAll(/"|'/g, '').replaceAll('.', '/').replaceAll(/\s/g, '') - - const pagesDirectory = join(options.project.path, options.pagesDirectory) - const globPattern = slash(`${pagesDirectory}/**/**.{vue,jsx,tsx,svelte}`) - const matchedFiles = await fg(globPattern, { + const text = options.text.replaceAll(/"|'|/g, '').replaceAll('.', '/').replaceAll(/\s/g, '') + const pagesDirectoriesAbs = options.pagesDirectories.map((dir) => + join(options.project.path, dir) + ) + const globPatterns = pagesDirectoriesAbs.map((dir) => slash(`${dir}/**/*.{vue,jsx,tsx,svelte}`)) + const matchedFiles = await fg(globPatterns, { onlyFiles: true, caseSensitiveMatch: false, }) - // Check if the filename includes the text - const regexPattern = `${pagesDirectory}/(.*)${text}(.*).(vue|jsx|tsx|svelte)`.replaceAll( - '\\', - '/' + // Check if the filename includes the text across any pages directory + const foundFiles = matchedFiles.filter((file) => + new RegExp(`(.*)${text}(.*)\\.(vue|jsx|tsx|svelte)$`, 'i').test(file) ) - const regex = new RegExp(regexPattern, 'i') - const foundFiles = matchedFiles.filter((file) => regex.test(file)) - return foundFiles.map((file) => { + const baseDir = + pagesDirectoriesAbs.find((dir) => file.startsWith(dir)) || pagesDirectoriesAbs[0]! return { - text: slash(relative(pagesDirectory, file).replace(/\.(vue|jsx|tsx|svelte)$/, '')), + text: slash(relative(baseDir, file).replace(/\.(vue|jsx|tsx|svelte)$/, '')), detail: slash(relative(options.project.path, file)), documentation: '', filePath: normalize(file), diff --git a/src/utilities/misc.ts b/src/utilities/misc.ts index 49fa463..d5f6004 100644 --- a/src/utilities/misc.ts +++ b/src/utilities/misc.ts @@ -7,19 +7,17 @@ import type { Controller } from '../types' import type { AdonisProject } from '../types/projects' export function controllerMagicStringToPath(project: AdonisProject, controller: Controller) { - let controllersDirectory = '' + const directories = project.isAdonis5() + ? ['app/Controllers/Http', 'app/controllers'] + : ['app/controllers', 'app/Controllers/Http'] - if (project.isAdonis5()) { - controllersDirectory = 'app/Controllers/Http' - } else if (project.isAdonis6()) { - controllersDirectory = 'app/controllers' - } - - const absPath = join(project.path, controllersDirectory) - const path = `${join(absPath, controller.namespace || '', controller.name)}.ts` + for (const dir of directories) { + const absPath = join(project.path, dir) + const path = `${join(absPath, controller.namespace || '', controller.name)}.ts` - if (fs.existsSync(path)) { - return slash(path) + if (fs.existsSync(path)) { + return slash(path) + } } return null diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index fb2b582..dca23e6 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -1,3 +1,5 @@ +import type { ExtensionContext } from 'vscode' + import type { RoutesTreeDataProvider } from './tree_views/routes/tree_data_provider' import type { CommandsTreeDataProvider } from './tree_views/commands/tree_data_provider' @@ -8,3 +10,19 @@ export class Extension { static routesTreeDataProvider: RoutesTreeDataProvider static commandsTreeDataProvider: CommandsTreeDataProvider } + +let extensionContext: ExtensionContext + +/** + * Save a referece for this extension's context + */ +export function setExtensionContext(context: ExtensionContext) { + extensionContext = context +} + +/** + * Return a reference for this extension's context + */ +export function getExtensionContext(): ExtensionContext { + return extensionContext +} diff --git a/src/vscode/logger.ts b/src/vscode/logger.ts index ff69562..0b61c19 100644 --- a/src/vscode/logger.ts +++ b/src/vscode/logger.ts @@ -1,5 +1,7 @@ import dedent from 'dedent' -import { window } from 'vscode' +import { ExtensionMode, window } from 'vscode' + +import { getExtensionContext } from './extension' export class Logger { static #channel = window.createOutputChannel('AdonisJS') @@ -9,6 +11,14 @@ export class Logger { this.#channel.appendLine(`[${timestamp}] ${dedent(message)}`) } + static debug(message: string) { + if (getExtensionContext().extensionMode !== ExtensionMode.Development) { + return + } + + this.#baseLog(`[DEBUG] ${message}`) + } + static info(message: string) { this.#baseLog(`[INFO] ${message}`) } diff --git a/src/vscode/project_manager.ts b/src/vscode/project_manager.ts index 2496397..a594206 100644 --- a/src/vscode/project_manager.ts +++ b/src/vscode/project_manager.ts @@ -37,7 +37,7 @@ export default class ProjectManager { * Log all found projects in the output channel */ static #logFoundProjects() { - const projetsDetails = this.#projects + const projectsDetails = this.#projects .map((project) => { return dedent` - Path : ${project.path} @@ -48,7 +48,7 @@ export default class ProjectManager { Logger.info( `Found ${this.#projects.length} AdonisJS project(s) : - ${projetsDetails}` + ${projectsDetails}` ) } diff --git a/src/vscode/providers/inertia/completion_provider.ts b/src/vscode/providers/inertia/completion_provider.ts index 55a4e4f..6f92cbf 100644 --- a/src/vscode/providers/inertia/completion_provider.ts +++ b/src/vscode/providers/inertia/completion_provider.ts @@ -18,7 +18,7 @@ export class InertiaCompletionProvider implements CompletionItemProvider { return InertiaSuggester.getInertiaSuggestions({ text, project, - pagesDirectory: ExtConfig.inertia.pagesDirectory, + pagesDirectories: ExtConfig.inertia.pagesDirectories, }) } diff --git a/src/vscode/providers/inertia/link_provider.ts b/src/vscode/providers/inertia/link_provider.ts index 16bfc83..0f042f5 100644 --- a/src/vscode/providers/inertia/link_provider.ts +++ b/src/vscode/providers/inertia/link_provider.ts @@ -11,7 +11,7 @@ export default class InertiaLinkProvider implements DocumentLinkProvider { const links = await InertiaLinker.getLinks({ fileContent: doc.getText(), project: project!, - pagesDirectory: ExtConfig.inertia.pagesDirectory, + pagesDirectories: ExtConfig.inertia.pagesDirectories, }) return DocumentLinkFactory.fromViewLink(links) diff --git a/src/vscode/utilities/config.ts b/src/vscode/utilities/config.ts index a7ec8a2..d167527 100644 --- a/src/vscode/utilities/config.ts +++ b/src/vscode/utilities/config.ts @@ -45,9 +45,16 @@ class ExtConfig { /** * Inertia configuration */ - static get inertia(): { pagesDirectory: string } { + static get inertia(): { pagesDirectories: string[] } { return workspace.getConfiguration(this.CONFIG_NAME).inertia } + + /** + * App configuration + */ + static get app(): { controllersDirectories: string[] } { + return workspace.getConfiguration(this.CONFIG_NAME).app + } } export default ExtConfig