diff --git a/src/features/config.ts b/src/features/config.ts index 51b493eb..bca34812 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -6,7 +6,7 @@ import { findHoverMatchesInDoc } from "@src/support/doc"; import { detectedRange, detectInDoc } from "@src/support/parser"; import { wordMatchRegex } from "@src/support/patterns"; import { projectPath } from "@src/support/project"; -import { contract, facade } from "@src/support/util"; +import { contract, facade, withLineFragment } from "@src/support/util"; import { AutocompleteParsingResult } from "@src/types"; import * as vscode from "vscode"; import { @@ -167,12 +167,14 @@ export const diagnosticProvider = ( return null; } - const pathToFile = getConfigPathByName(param.value); + const configPath = getConfigPathByName(param.value); - const code: NotFoundCode = pathToFile + const code: NotFoundCode = configPath ? { value: "config", - target: vscode.Uri.file(projectPath(pathToFile)), + target: vscode.Uri.file(configPath.path).with( + withLineFragment(configPath.line), + ), } : "config"; diff --git a/src/features/env.ts b/src/features/env.ts index b045f241..42695ba2 100644 --- a/src/features/env.ts +++ b/src/features/env.ts @@ -91,7 +91,10 @@ export const diagnosticProvider = ( return null; } - return notFound("Env", param.value, detectedRange(param), "env"); + return notFound("Env", param.value, detectedRange(param), { + value: "env", + target: vscode.Uri.file(projectPath(".env")), + }); }, ); }; diff --git a/src/features/translation.ts b/src/features/translation.ts index f97b4aa1..bb6c08ef 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -10,8 +10,13 @@ import { config } from "@src/support/config"; import { findHoverMatchesInDoc } from "@src/support/doc"; import { detectedRange, detectInDoc } from "@src/support/parser"; import { wordMatchRegex } from "@src/support/patterns"; -import { projectPath, relativePath } from "@src/support/project"; -import { contract, createIndexMapping, facade } from "@src/support/util"; +import { relativePath } from "@src/support/project"; +import { + contract, + createIndexMapping, + facade, + withLineFragment, +} from "@src/support/util"; import { AutocompleteParsingResult } from "@src/types"; import * as vscode from "vscode"; import { FeatureTag, HoverProvider, LinkProvider } from ".."; @@ -191,15 +196,17 @@ export const diagnosticProvider = ( return null; } - const pathToFile = getTranslationPathByName( + const translationPath = getTranslationPathByName( param.value, getLang(item as AutocompleteParsingResult.MethodCall), ); - const code: NotFoundCode = pathToFile + const code: NotFoundCode = translationPath ? { value: "translation", - target: vscode.Uri.file(projectPath(pathToFile)), + target: vscode.Uri.file(translationPath.path).with( + withLineFragment(translationPath.line), + ), } : "translation"; diff --git a/src/repositories/configs.ts b/src/repositories/configs.ts index a7d2f00d..e2443717 100644 --- a/src/repositories/configs.ts +++ b/src/repositories/configs.ts @@ -1,3 +1,4 @@ +import { projectPath } from "@src/support/project"; import { repository } from "."; import { Config } from ".."; import { runInLaravel, template } from "../support/php"; @@ -7,23 +8,75 @@ interface ConfigGroupResult { paths: string[]; } -export const getConfigPathByName = (match: string): string | undefined => { - const filePath = match.replace(/\.[^.]+$/, ""); - - for (const tryPath of [ - filePath.replaceAll(".", "/"), - filePath.replace(/^([^.]+)\..*$/, "$1"), - ]) { - const configPath = getConfigs().items.paths.find((path) => { - return ( - !path.startsWith("vendor/") && path.endsWith(`${tryPath}.php`) - ); - }); - - if (configPath) { - return configPath; +interface ConfigPath { + path: string; + line?: string | null; +} + +export const getConfigByName = (name: string): Config | undefined => { + return getConfigs().items.configs.find((item) => item.name === name); +}; + +export const getParentConfigByName = (match: string): Config | undefined => { + const name = match.match(/^(.*)\./)?.[0]; + + if (!name) { + return undefined; + } + + return getConfigs().items.configs.find((config) => + config.name.startsWith(name), + ); +}; + +export const getConfigPathByName = (match: string): ConfigPath | undefined => { + // Firstly, we try to get the parent Config, because it has a path and a line + const parentItem = getParentConfigByName(match); + + let path = parentItem?.file; + + // If the path is not found (because, for example, config file is empty), + // we try to find the path by the file name + if (!path) { + const fileName = match.replace(/\.[^.]+$/, ""); + + // We have to check every possible subfolder, for example: foo.bar.baz.example + // can be: foo/bar.php with a key "baz.example" but also foo/bar/baz.php with a key "example" + const parts = fileName.split("."); + const subfolderPaths = parts + .slice(1) + .map((_, i) => + ( + parts.slice(0, i + 2).join("/") + + "." + + parts.slice(i + 2).join(".") + ).replace(/^([^.]+)\..*$/, "$1"), + ) + .reverse(); + + for (const tryPath of [ + ...subfolderPaths, + fileName.replace(/^([^.]+)\..*$/, "$1"), + ]) { + path = getConfigs().items.paths.find((path) => { + return ( + !path.startsWith("vendor/") && + path.endsWith(`${tryPath}.php`) + ); + }); + + if (path) { + break; + } } } + + return path + ? { + path: projectPath(path), + line: parentItem?.line, + } + : undefined; }; export const getConfigs = repository({ diff --git a/src/repositories/translations.ts b/src/repositories/translations.ts index a55b3263..22fc78f4 100644 --- a/src/repositories/translations.ts +++ b/src/repositories/translations.ts @@ -35,6 +35,11 @@ interface TranslationGroupPhpResult { languages: string[]; } +interface TranslationPath { + path: string; + line?: number; +} + let dirsToWatch: string[] | null = null; const load = () => { @@ -78,20 +83,58 @@ export const getTranslationItemByName = ( return getTranslations().items.translations[match.replaceAll("\\", "")]; }; +export const getParentTranslationItemByName = ( + match: string, +): TranslationItem | undefined => { + const name = match.match(/^(.*)\./)?.[0]; + + if (!name) { + return undefined; + } + + const parentName = Object.keys(getTranslations().items.translations).find( + (key) => key.startsWith(name.replaceAll("\\", "")), + ); + + return parentName ? getTranslationItemByName(parentName) : undefined; +}; + export const getTranslationPathByName = ( match: string, lang: string | undefined, -): string | undefined => { +): TranslationPath | undefined => { lang = lang ?? getTranslations().items.default; - const fileName = match.replace(/^.*::/, "").replace(/^([^.]+)\..*$/, "$1"); - - return getTranslations().items.paths.find((path) => { - return ( - !path.startsWith("vendor/") && - path.endsWith(`${lang}/${fileName}.php`) - ); - }); + // Firstly, we try to get the parent TranslationItem, because it has a path and a line + const parentItem = getParentTranslationItemByName(match); + + let path = parentItem?.[lang]?.path; + + // If the path is not found (because, for example, translation file is empty), + // we try to find the path by the file name + if (!path) { + const fileName = match + .replace(/^.*::/, "") + .replace(/^([^.]+)\..*$/, "$1"); + + path = getTranslations().items.paths.find((path) => { + return ( + !path.startsWith("vendor/") && + path.endsWith(`${lang}/${fileName}.php`) + ); + }); + + if (path) { + path = projectPath(path); + } + } + + return path + ? { + path: path, + line: parentItem?.[lang]?.line, + } + : undefined; }; export const getTranslations = repository({ diff --git a/src/support/util.ts b/src/support/util.ts index 72325cc3..8a5f48f7 100644 --- a/src/support/util.ts +++ b/src/support/util.ts @@ -18,6 +18,12 @@ export const indent = (text: string = "", repeat: number = 1): string => { return "\t" + text; }; +export const withLineFragment = ( + line: number | string | undefined | null, +): { fragment?: string } => { + return line ? { fragment: `L${line}` } : {}; +}; + export const trimQuotes = (text: string): string => text.substring(1, text.length - 1);