Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Autocompletion for the name and the handler of controllers. <kbd>Ctrl</kbd> + <k
- `useUnixCd`: Use Unix-style `cd` for windows terminals (Useful when using Cygwin or Git Bash)
- `quickJump`: Enable quick jump by using <kbd>Ctrl</kbd> + <kbd>Click</kbd>
- `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 <kbd>Ctrl</kbd> + <kbd>Space</kbd> to manually trigger IntelliSense.
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
34 changes: 16 additions & 18 deletions src/linkers/controllers_linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 54 additions & 13 deletions src/linkers/inertia_linker.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -24,42 +25,82 @@ export class InertiaLinker {
static async getLinks(options: {
fileContent: string
project: AdonisProject
pagesDirectory: string
pagesDirectories: string[]
}): Promise<InertiaLink[]> {
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)
Expand Down
54 changes: 19 additions & 35 deletions src/suggesters/controller_suggester.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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,
})
Expand All @@ -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))
}

Expand All @@ -76,22 +60,22 @@ export class ControllerSuggester {
project: AdonisProject
}): Promise<Suggestion[]> {
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,
Expand Down
29 changes: 14 additions & 15 deletions src/suggesters/inertia_suggester.ts
Original file line number Diff line number Diff line change
@@ -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<Suggestion[]> {
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),
Expand Down
20 changes: 9 additions & 11 deletions src/utilities/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/vscode/extension.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
}
Loading