From 0907554363feda78e7216e61f2eb5ed037b5b7d0 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 23 Aug 2025 16:55:58 +0000 Subject: [PATCH 01/11] Improvements for linked parameter vscode.Diagnostic.code for diagnosticProvider Fixes N1ebieski/vs-code-extension#55 --- src/features/config.ts | 13 +++--- src/features/env.ts | 5 ++- src/features/translation.ts | 13 +++--- src/repositories/configs.ts | 76 +++++++++++++++++++++++++------- src/repositories/translations.ts | 55 +++++++++++++++++++---- 5 files changed, 128 insertions(+), 34 deletions(-) diff --git a/src/features/config.ts b/src/features/config.ts index 51b493eb..57209fbc 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -167,13 +167,16 @@ 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)), - } + value: "config", + target: vscode.Uri.file(configPath.path) + .with(configPath.line ? { + fragment: `L${configPath.line}` + } : {}), + } : "config"; return notFound("Config", param.value, detectedRange(param), code); 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..69dd7061 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -191,16 +191,19 @@ 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)), - } + value: "translation", + target: vscode.Uri.file(translationPath.path) + .with(translationPath.line ? { + fragment: `L${translationPath.line}` + } : {}), + } : "translation"; return notFound( diff --git a/src/repositories/configs.ts b/src/repositories/configs.ts index a7d2f00d..480fb1f8 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,68 @@ 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..a9b6fe43 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,54 @@ 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"); + // Firstly, we try to get the parent TranslationItem, because it has a path and a line + const parentItem = getParentTranslationItemByName(match); - return getTranslations().items.paths.find((path) => { - return ( - !path.startsWith("vendor/") && - path.endsWith(`${lang}/${fileName}.php`) - ); - }); + 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({ From 5189d47531aae568c9e24aa992d4ae3e6c7ad528 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sun, 24 Aug 2025 08:31:42 +0000 Subject: [PATCH 02/11] Improvements for linked parameter vscode.Diagnostic.code for diagnosticProvider Fixes N1ebieski/vs-code-extension#55 --- src/features/config.ts | 15 ++++++++------ src/features/translation.ts | 15 ++++++++------ src/repositories/configs.ts | 35 +++++++++++++++++++------------- src/repositories/translations.ts | 22 ++++++++++++-------- 4 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/features/config.ts b/src/features/config.ts index 57209fbc..9b5eb8e4 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -171,12 +171,15 @@ export const diagnosticProvider = ( const code: NotFoundCode = configPath ? { - value: "config", - target: vscode.Uri.file(configPath.path) - .with(configPath.line ? { - fragment: `L${configPath.line}` - } : {}), - } + value: "config", + target: vscode.Uri.file(configPath.path).with( + configPath.line + ? { + fragment: `L${configPath.line}`, + } + : {}, + ), + } : "config"; return notFound("Config", param.value, detectedRange(param), code); diff --git a/src/features/translation.ts b/src/features/translation.ts index 69dd7061..fe66b48c 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -198,12 +198,15 @@ export const diagnosticProvider = ( const code: NotFoundCode = translationPath ? { - value: "translation", - target: vscode.Uri.file(translationPath.path) - .with(translationPath.line ? { - fragment: `L${translationPath.line}` - } : {}), - } + value: "translation", + target: vscode.Uri.file(translationPath.path).with( + translationPath.line + ? { + fragment: `L${translationPath.line}`, + } + : {}, + ), + } : "translation"; return notFound( diff --git a/src/repositories/configs.ts b/src/repositories/configs.ts index 480fb1f8..e2443717 100644 --- a/src/repositories/configs.ts +++ b/src/repositories/configs.ts @@ -11,15 +11,13 @@ interface ConfigGroupResult { 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 => { +export const getParentConfigByName = (match: string): Config | undefined => { const name = match.match(/^(.*)\./)?.[0]; if (!name) { @@ -37,7 +35,7 @@ export const getConfigPathByName = (match: string): ConfigPath | undefined => { let path = parentItem?.file; - // If the path is not found (because, for example, config file is empty), + // 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(/\.[^.]+$/, ""); @@ -45,10 +43,16 @@ export const getConfigPathByName = (match: string): ConfigPath | undefined => { // 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(); + 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, @@ -56,7 +60,8 @@ export const getConfigPathByName = (match: string): ConfigPath | undefined => { ]) { path = getConfigs().items.paths.find((path) => { return ( - !path.startsWith("vendor/") && path.endsWith(`${tryPath}.php`) + !path.startsWith("vendor/") && + path.endsWith(`${tryPath}.php`) ); }); @@ -66,10 +71,12 @@ export const getConfigPathByName = (match: string): ConfigPath | undefined => { } } - return path ? { - path: projectPath(path), - line: parentItem?.line, - } : undefined; + 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 a9b6fe43..22fc78f4 100644 --- a/src/repositories/translations.ts +++ b/src/repositories/translations.ts @@ -38,7 +38,7 @@ interface TranslationGroupPhpResult { interface TranslationPath { path: string; line?: number; -}; +} let dirsToWatch: string[] | null = null; @@ -92,8 +92,8 @@ export const getParentTranslationItemByName = ( return undefined; } - const parentName = Object.keys(getTranslations().items.translations).find((key) => - key.startsWith(name.replaceAll("\\", "")), + const parentName = Object.keys(getTranslations().items.translations).find( + (key) => key.startsWith(name.replaceAll("\\", "")), ); return parentName ? getTranslationItemByName(parentName) : undefined; @@ -110,10 +110,12 @@ export const getTranslationPathByName = ( let path = parentItem?.[lang]?.path; - // If the path is not found (because, for example, translation file is empty), + // 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"); + const fileName = match + .replace(/^.*::/, "") + .replace(/^([^.]+)\..*$/, "$1"); path = getTranslations().items.paths.find((path) => { return ( @@ -127,10 +129,12 @@ export const getTranslationPathByName = ( } } - return path ? { - path: path, - line: parentItem?.[lang]?.line, - } : undefined; + return path + ? { + path: path, + line: parentItem?.[lang]?.line, + } + : undefined; }; export const getTranslations = repository({ From e3ff4a954ad282533f65cc880e8bc68bfd5e936c Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 15 Sep 2025 16:37:54 -0400 Subject: [PATCH 03/11] withLineFragment helper --- src/features/config.ts | 8 ++------ src/features/translation.ts | 15 ++++++++------- src/support/util.ts | 6 ++++++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/features/config.ts b/src/features/config.ts index 9b5eb8e4..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 { @@ -173,11 +173,7 @@ export const diagnosticProvider = ( ? { value: "config", target: vscode.Uri.file(configPath.path).with( - configPath.line - ? { - fragment: `L${configPath.line}`, - } - : {}, + withLineFragment(configPath.line), ), } : "config"; diff --git a/src/features/translation.ts b/src/features/translation.ts index fe66b48c..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 ".."; @@ -200,11 +205,7 @@ export const diagnosticProvider = ( ? { value: "translation", target: vscode.Uri.file(translationPath.path).with( - translationPath.line - ? { - fragment: `L${translationPath.line}`, - } - : {}, + withLineFragment(translationPath.line), ), } : "translation"; 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); From cd301e22d9f8aa54c1f3a370bfce3adb0a2ecfe0 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 20 Dec 2025 09:28:35 +0000 Subject: [PATCH 04/11] Fixes N1ebieski/vs-code-extension#72 Fix for QuickFix for vscode.Diagnostic.code --- src/codeAction/codeActionProvider.ts | 19 ++++++++++++++----- src/features/env.ts | 3 ++- src/features/inertia.ts | 3 ++- src/features/view.ts | 3 ++- src/index.d.ts | 1 + 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/codeAction/codeActionProvider.ts b/src/codeAction/codeActionProvider.ts index 29616b10..7114f421 100644 --- a/src/codeAction/codeActionProvider.ts +++ b/src/codeAction/codeActionProvider.ts @@ -23,11 +23,20 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ): vscode.ProviderResult { return Promise.all( context.diagnostics - .map((diagnostic) => - providers.map((provider) => - provider(diagnostic, document, range, token), - ), - ) + .map((diagnostic) => { + const code = + typeof diagnostic.code === "object" + ? diagnostic.code?.value + : diagnostic.code; + + if (typeof code !== "string") { + return []; + } + + return providers.map((provider) => + provider(code, diagnostic, document, range, token), + ); + }) .flat(), ).then((actions) => actions.flat()); } diff --git a/src/features/env.ts b/src/features/env.ts index b045f241..d023f356 100644 --- a/src/features/env.ts +++ b/src/features/env.ts @@ -192,12 +192,13 @@ export const viteEnvCodeActionProvider: vscode.CodeActionProvider = { }; export const codeActionProvider: CodeActionProviderFunction = async ( + code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (diagnostic.code !== "env") { + if (code !== "env") { return []; } diff --git a/src/features/inertia.ts b/src/features/inertia.ts index 92fcc66d..51cd8c46 100644 --- a/src/features/inertia.ts +++ b/src/features/inertia.ts @@ -141,12 +141,13 @@ export const diagnosticProvider = ( }; export const codeActionProvider: CodeActionProviderFunction = async ( + code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (diagnostic.code !== "inertia") { + if (code !== "inertia") { return []; } diff --git a/src/features/view.ts b/src/features/view.ts index 014b8528..bfaa9c59 100644 --- a/src/features/view.ts +++ b/src/features/view.ts @@ -171,12 +171,13 @@ export const diagnosticProvider = ( }; export const codeActionProvider: CodeActionProviderFunction = async ( + code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (diagnostic.code !== "view") { + if (code !== "view") { return []; } diff --git a/src/index.d.ts b/src/index.d.ts index dbc537e8..486d3114 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import AutocompleteResult from "./parser/AutocompleteResult"; type CodeActionProviderFunction = ( + code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, From f600bf14eda80cec98924c79c5ec32a097eb253d Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 20 Dec 2025 13:27:05 +0000 Subject: [PATCH 05/11] Fixes N1ebieski/vs-code-extension#55 Add QuickFix for configs and translations --- src/codeAction/codeActionProvider.ts | 4 + src/features/config.ts | 117 ++++++++++++++++++++++ src/features/translation.ts | 141 ++++++++++++++++++++++++--- src/repositories/configs.ts | 2 +- src/repositories/translations.ts | 2 +- src/support/indent.ts | 32 ++++++ 6 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 src/support/indent.ts diff --git a/src/codeAction/codeActionProvider.ts b/src/codeAction/codeActionProvider.ts index 7114f421..a2d88ac3 100644 --- a/src/codeAction/codeActionProvider.ts +++ b/src/codeAction/codeActionProvider.ts @@ -1,5 +1,7 @@ +import { codeActionProvider as configProvider } from "@src/features/config"; import { codeActionProvider as envProvider } from "@src/features/env"; import { codeActionProvider as inertiaProvider } from "@src/features/inertia"; +import { codeActionProvider as translationProvider } from "@src/features/translation"; import { codeActionProvider as viewProvider } from "@src/features/view"; import * as vscode from "vscode"; import { CodeActionProviderFunction } from ".."; @@ -8,6 +10,8 @@ const providers: CodeActionProviderFunction[] = [ envProvider, inertiaProvider, viewProvider, + translationProvider, + configProvider, ]; export class CodeActionProvider implements vscode.CodeActionProvider { diff --git a/src/features/config.ts b/src/features/config.ts index bca34812..8a66be26 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -1,8 +1,10 @@ +import { openFile } from "@src/commands"; import { notFound, NotFoundCode } from "@src/diagnostic"; import AutocompleteResult from "@src/parser/AutocompleteResult"; import { getConfigPathByName, getConfigs } from "@src/repositories/configs"; import { config } from "@src/support/config"; import { findHoverMatchesInDoc } from "@src/support/doc"; +import { getIndentNumber } from "@src/support/indent"; import { detectedRange, detectInDoc } from "@src/support/parser"; import { wordMatchRegex } from "@src/support/patterns"; import { projectPath } from "@src/support/project"; @@ -10,6 +12,7 @@ import { contract, facade, withLineFragment } from "@src/support/util"; import { AutocompleteParsingResult } from "@src/types"; import * as vscode from "vscode"; import { + CodeActionProviderFunction, CompletionProvider, FeatureTag, HoverProvider, @@ -183,6 +186,120 @@ export const diagnosticProvider = ( ); }; +export const codeActionProvider: CodeActionProviderFunction = async ( + code: string, + diagnostic: vscode.Diagnostic, + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + token: vscode.CancellationToken, +): Promise => { + if (code !== "config") { + return []; + } + + const missingVar = document.getText(diagnostic.range); + + if (!missingVar) { + return []; + } + + const actions = await Promise.all([addToFile(diagnostic, missingVar)]); + + return actions.filter((action) => action !== null); +}; + +const addToFile = async ( + diagnostic: vscode.Diagnostic, + missingVar: string, +): Promise => { + return getCodeAction( + "Add variable to the configuration file", + missingVar, + diagnostic, + ); +}; + +const getCodeAction = async ( + title: string, + missingVar: string, + diagnostic: vscode.Diagnostic, + value?: string, +) => { + const edit = new vscode.WorkspaceEdit(); + + const config = getConfigs().items.configs.find( + (c) => c.name === missingVar, + ); + + if (config) { + return null; + } + + const configPath = getConfigPathByName(missingVar); + + if (!configPath) { + return null; + } + + const configContents = await vscode.workspace.fs.readFile( + vscode.Uri.file(configPath.path), + ); + + let lineNumber = configPath.line ? Number(configPath.line) - 1 : undefined; + + if (!lineNumber) { + // Default to the end of the file + const lines = configContents.toString().split("\n"); + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith("];")) { + lineNumber = i; + } + } + } + + if (!lineNumber) { + return null; + } + + const key = missingVar.split(".").pop(); + + const fileName = configPath.path.split("/").pop()?.replace(".php", ""); + + if (!fileName) { + return null; + } + + // Remember that Laravel config keys can go to subfolders, 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 parents = + missingVar.substring(missingVar.indexOf(`${fileName}.`)).split(".") + .length - 1; + + const indent = " ".repeat((getIndentNumber("php") ?? 4) * parents); + + const finalValue = `${indent}'${key}' => '',\n`; + + edit.insert( + vscode.Uri.file(configPath.path), + new vscode.Position(lineNumber, 0), + finalValue, + ); + + const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + + action.edit = edit; + action.command = openFile( + configPath.path, + lineNumber, + finalValue.length - 3, + ); + action.diagnostics = [diagnostic]; + action.isPreferred = value === undefined; + + return action; +}; + export const completionProvider: CompletionProvider = { tags() { return toFind; diff --git a/src/features/translation.ts b/src/features/translation.ts index bb6c08ef..f7c223fa 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -1,3 +1,4 @@ +import { openFile } from "@src/commands"; import { notFound, NotFoundCode } from "@src/diagnostic"; import AutocompleteResult from "@src/parser/AutocompleteResult"; import { @@ -8,6 +9,7 @@ import { } from "@src/repositories/translations"; import { config } from "@src/support/config"; import { findHoverMatchesInDoc } from "@src/support/doc"; +import { getIndentNumber } from "@src/support/indent"; import { detectedRange, detectInDoc } from "@src/support/parser"; import { wordMatchRegex } from "@src/support/patterns"; import { relativePath } from "@src/support/project"; @@ -19,7 +21,12 @@ import { } from "@src/support/util"; import { AutocompleteParsingResult } from "@src/types"; import * as vscode from "vscode"; -import { FeatureTag, HoverProvider, LinkProvider } from ".."; +import { + CodeActionProviderFunction, + FeatureTag, + HoverProvider, + LinkProvider, +} from ".."; const toFind: FeatureTag = [ { @@ -196,10 +203,9 @@ export const diagnosticProvider = ( return null; } - const translationPath = getTranslationPathByName( - param.value, - getLang(item as AutocompleteParsingResult.MethodCall), - ); + const lang = getLang(item as AutocompleteParsingResult.MethodCall); + + const translationPath = getTranslationPathByName(param.value, lang); const code: NotFoundCode = translationPath ? { @@ -210,16 +216,129 @@ export const diagnosticProvider = ( } : "translation"; - return notFound( - "Translation", - param.value, - detectedRange(param), - code, - ); + let message = "Translation"; + + if (lang) { + message += ` for locale: "${lang}"`; + } + + return notFound(message, param.value, detectedRange(param), code); }, ); }; +export const codeActionProvider: CodeActionProviderFunction = async ( + code: string, + diagnostic: vscode.Diagnostic, + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + token: vscode.CancellationToken, +): Promise => { + if (code !== "translation") { + return []; + } + + const missingVar = document.getText(diagnostic.range); + + if (!missingVar) { + return []; + } + + const actions = await Promise.all([addToFile(diagnostic, missingVar)]); + + return actions.filter((action) => action !== null); +}; + +const addToFile = async ( + diagnostic: vscode.Diagnostic, + missingVar: string, +): Promise => { + return getCodeAction( + "Add variable to the translation file", + missingVar, + diagnostic, + ); +}; + +const getCodeAction = async ( + title: string, + missingVar: string, + diagnostic: vscode.Diagnostic, + value?: string, +) => { + const edit = new vscode.WorkspaceEdit(); + + const translation = getTranslationItemByName(missingVar); + + if (translation) { + return null; + } + + /** + * This is ugly, but we don't have a way to get the language from method parameters, + * maybe {@link vscode.Diagnostic.relatedInformation} can help + **/ + const match = diagnostic.message.match(/for locale:\s+\"([^"]+)\"/); + + const lang = match?.[1]; + + const translationPath = getTranslationPathByName(missingVar, lang); + + if (!translationPath) { + return null; + } + + const translationContents = await vscode.workspace.fs.readFile( + vscode.Uri.file(translationPath.path), + ); + + let lineNumber = translationPath.line + ? translationPath.line - 1 + : undefined; + + if (!lineNumber) { + // Default to the end of the file + const lines = translationContents.toString().split("\n"); + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith("];")) { + lineNumber = i; + } + } + } + + if (!lineNumber) { + return null; + } + + const key = missingVar.split(".").pop(); + + const parents = missingVar.split(".").length - 1; + + const indent = " ".repeat((getIndentNumber("php") ?? 4) * parents); + + const finalValue = `${indent}'${key}' => '',\n`; + + edit.insert( + vscode.Uri.file(translationPath.path), + new vscode.Position(lineNumber, 0), + finalValue, + ); + + const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + + action.edit = edit; + action.command = openFile( + translationPath.path, + lineNumber, + finalValue.length - 3, + ); + action.diagnostics = [diagnostic]; + action.isPreferred = value === undefined; + + return action; +}; + export const completionProvider = { tags() { return toFind; diff --git a/src/repositories/configs.ts b/src/repositories/configs.ts index e2443717..0d3ba928 100644 --- a/src/repositories/configs.ts +++ b/src/repositories/configs.ts @@ -25,7 +25,7 @@ export const getParentConfigByName = (match: string): Config | undefined => { } return getConfigs().items.configs.find((config) => - config.name.startsWith(name), + new RegExp(`^${name}[^.]*$`).test(config.name), ); }; diff --git a/src/repositories/translations.ts b/src/repositories/translations.ts index 22fc78f4..3c4dddb6 100644 --- a/src/repositories/translations.ts +++ b/src/repositories/translations.ts @@ -101,7 +101,7 @@ export const getParentTranslationItemByName = ( export const getTranslationPathByName = ( match: string, - lang: string | undefined, + lang?: string | undefined, ): TranslationPath | undefined => { lang = lang ?? getTranslations().items.default; diff --git a/src/support/indent.ts b/src/support/indent.ts new file mode 100644 index 00000000..85abd69a --- /dev/null +++ b/src/support/indent.ts @@ -0,0 +1,32 @@ +import * as vscode from "vscode"; + +export function getIndentNumber(extension: string): number | undefined { + /** + * {@link vscode.TextEditorOptions} + */ + const config = vscode.workspace.getConfiguration("editor", { + languageId: extension, + }); + + const insertSpaces = config.get( + "insertSpaces", + ); + + if (insertSpaces !== true) { + return undefined; + } + + const indentSize = config.get("indentSize"); + + if (typeof indentSize === "number") { + return indentSize; + } + + const tabSize = config.get("tabSize"); + + if (typeof tabSize === "number") { + return tabSize; + } + + return undefined; +} From 1c1ac9e7e5f8e486c3318634ad7d65ee30c396db Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sat, 20 Dec 2025 15:54:45 +0000 Subject: [PATCH 06/11] Fixes N1ebieski/vs-code-extension#55 refactoring --- src/diagnostic/index.ts | 9 ++++++++- src/features/translation.ts | 32 ++++++++++++++------------------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/diagnostic/index.ts b/src/diagnostic/index.ts index b2de380f..51fc78cc 100644 --- a/src/diagnostic/index.ts +++ b/src/diagnostic/index.ts @@ -1,3 +1,4 @@ +import { AutocompleteParsingResult } from "@src/types"; import { Diagnostic, DiagnosticSeverity, Range, Uri } from "vscode"; export type DiagnosticCodeWithTarget = { @@ -22,15 +23,21 @@ export type DiagnosticCode = export type NotFoundCode = DiagnosticCode | DiagnosticCodeWithTarget; +export class DiagnosticWithContext extends Diagnostic { + context?: AutocompleteParsingResult.ContextValue; +} + export const notFound = ( descriptor: string, match: string, range: Range, code: NotFoundCode, -): Diagnostic => ({ + context?: AutocompleteParsingResult.ContextValue, +): DiagnosticWithContext => ({ message: `${descriptor} [${match}] not found.`, severity: DiagnosticSeverity.Warning, range, source: "Laravel Extension", code, + context, }); diff --git a/src/features/translation.ts b/src/features/translation.ts index f7c223fa..d12c3cf8 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -1,5 +1,5 @@ import { openFile } from "@src/commands"; -import { notFound, NotFoundCode } from "@src/diagnostic"; +import { DiagnosticWithContext, notFound, NotFoundCode } from "@src/diagnostic"; import AutocompleteResult from "@src/parser/AutocompleteResult"; import { getTranslationItemByName, @@ -216,20 +216,20 @@ export const diagnosticProvider = ( } : "translation"; - let message = "Translation"; - - if (lang) { - message += ` for locale: "${lang}"`; - } - - return notFound(message, param.value, detectedRange(param), code); + return notFound( + "Translation", + param.value, + detectedRange(param), + code, + item, + ); }, ); }; export const codeActionProvider: CodeActionProviderFunction = async ( code: string, - diagnostic: vscode.Diagnostic, + diagnostic: DiagnosticWithContext, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, @@ -250,7 +250,7 @@ export const codeActionProvider: CodeActionProviderFunction = async ( }; const addToFile = async ( - diagnostic: vscode.Diagnostic, + diagnostic: DiagnosticWithContext, missingVar: string, ): Promise => { return getCodeAction( @@ -263,7 +263,7 @@ const addToFile = async ( const getCodeAction = async ( title: string, missingVar: string, - diagnostic: vscode.Diagnostic, + diagnostic: DiagnosticWithContext, value?: string, ) => { const edit = new vscode.WorkspaceEdit(); @@ -274,13 +274,9 @@ const getCodeAction = async ( return null; } - /** - * This is ugly, but we don't have a way to get the language from method parameters, - * maybe {@link vscode.Diagnostic.relatedInformation} can help - **/ - const match = diagnostic.message.match(/for locale:\s+\"([^"]+)\"/); - - const lang = match?.[1]; + const lang = getLang( + diagnostic.context as AutocompleteParsingResult.MethodCall, + ); const translationPath = getTranslationPathByName(missingVar, lang); From 5e9cd9ecb2829532b37abaa89305907f75e18aa3 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Sun, 21 Dec 2025 10:06:18 +0000 Subject: [PATCH 07/11] Fixes N1ebieski/vs-code-extension#55 Improvements for linked parameter vscode.Diagnostic.code for diagnosticProvider --- src/features/config.ts | 31 ++++++++++++++++++------------- src/features/translation.ts | 11 ++++++++--- src/repositories/configs.ts | 2 +- src/repositories/translations.ts | 2 +- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/features/config.ts b/src/features/config.ts index 8a66be26..83bf0d93 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -241,6 +241,23 @@ const getCodeAction = async ( return null; } + const fileName = configPath.path.split("/").pop()?.replace(".php", ""); + + if (!fileName) { + return null; + } + + // Remember that Laravel config keys can go to subfolders, 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 nestedKeys = + missingVar.substring(missingVar.indexOf(`${fileName}.`)).split(".") + .length - 1; + + // Case when a user tries to add a new config key to an existing key that is not an array + if (!configPath.line && nestedKeys > 1) { + return null; + } + const configContents = await vscode.workspace.fs.readFile( vscode.Uri.file(configPath.path), ); @@ -264,19 +281,7 @@ const getCodeAction = async ( const key = missingVar.split(".").pop(); - const fileName = configPath.path.split("/").pop()?.replace(".php", ""); - - if (!fileName) { - return null; - } - - // Remember that Laravel config keys can go to subfolders, 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 parents = - missingVar.substring(missingVar.indexOf(`${fileName}.`)).split(".") - .length - 1; - - const indent = " ".repeat((getIndentNumber("php") ?? 4) * parents); + const indent = " ".repeat((getIndentNumber("php") ?? 4) * nestedKeys); const finalValue = `${indent}'${key}' => '',\n`; diff --git a/src/features/translation.ts b/src/features/translation.ts index d12c3cf8..f9d5a017 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -284,6 +284,13 @@ const getCodeAction = async ( return null; } + const nestedKeys = missingVar.split(".").length - 1; + + // Case when a user tries to add a new translation key to an existing key that is not an array + if (!translationPath.line && nestedKeys > 1) { + return null; + } + const translationContents = await vscode.workspace.fs.readFile( vscode.Uri.file(translationPath.path), ); @@ -309,9 +316,7 @@ const getCodeAction = async ( const key = missingVar.split(".").pop(); - const parents = missingVar.split(".").length - 1; - - const indent = " ".repeat((getIndentNumber("php") ?? 4) * parents); + const indent = " ".repeat((getIndentNumber("php") ?? 4) * nestedKeys); const finalValue = `${indent}'${key}' => '',\n`; diff --git a/src/repositories/configs.ts b/src/repositories/configs.ts index 0d3ba928..d23d2d5b 100644 --- a/src/repositories/configs.ts +++ b/src/repositories/configs.ts @@ -18,7 +18,7 @@ export const getConfigByName = (name: string): Config | undefined => { }; export const getParentConfigByName = (match: string): Config | undefined => { - const name = match.match(/^(.*)\./)?.[0]; + const name = match.match(/^(.+\..+)\./)?.[0]; if (!name) { return undefined; diff --git a/src/repositories/translations.ts b/src/repositories/translations.ts index 3c4dddb6..1740bb17 100644 --- a/src/repositories/translations.ts +++ b/src/repositories/translations.ts @@ -86,7 +86,7 @@ export const getTranslationItemByName = ( export const getParentTranslationItemByName = ( match: string, ): TranslationItem | undefined => { - const name = match.match(/^(.*)\./)?.[0]; + const name = match.match(/^(.+\..+)\./)?.[0]; if (!name) { return undefined; From 8b2e32184c015ab3e8a6d56abc8fa5b8b3272451 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Mon, 22 Dec 2025 18:21:52 +0000 Subject: [PATCH 08/11] remove the hover links --- src/codeAction/codeActionProvider.ts | 19 +++++-------------- src/diagnostic/index.ts | 9 +-------- src/features/config.ts | 23 ++++++++--------------- src/features/env.ts | 8 ++------ src/features/inertia.ts | 3 +-- src/features/translation.ts | 20 +++----------------- src/features/view.ts | 3 +-- src/index.d.ts | 1 - 8 files changed, 21 insertions(+), 65 deletions(-) diff --git a/src/codeAction/codeActionProvider.ts b/src/codeAction/codeActionProvider.ts index a2d88ac3..778e81fd 100644 --- a/src/codeAction/codeActionProvider.ts +++ b/src/codeAction/codeActionProvider.ts @@ -27,20 +27,11 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ): vscode.ProviderResult { return Promise.all( context.diagnostics - .map((diagnostic) => { - const code = - typeof diagnostic.code === "object" - ? diagnostic.code?.value - : diagnostic.code; - - if (typeof code !== "string") { - return []; - } - - return providers.map((provider) => - provider(code, diagnostic, document, range, token), - ); - }) + .map((diagnostic) => + providers.map((provider) => + provider(diagnostic, document, range, token), + ), + ) .flat(), ).then((actions) => actions.flat()); } diff --git a/src/diagnostic/index.ts b/src/diagnostic/index.ts index 51fc78cc..1ca7bb6c 100644 --- a/src/diagnostic/index.ts +++ b/src/diagnostic/index.ts @@ -1,11 +1,6 @@ import { AutocompleteParsingResult } from "@src/types"; import { Diagnostic, DiagnosticSeverity, Range, Uri } from "vscode"; -export type DiagnosticCodeWithTarget = { - value: DiagnosticCode; - target: Uri; -}; - export type DiagnosticCode = | "appBinding" | "asset" @@ -21,8 +16,6 @@ export type DiagnosticCode = | "view" | "storage_disk"; -export type NotFoundCode = DiagnosticCode | DiagnosticCodeWithTarget; - export class DiagnosticWithContext extends Diagnostic { context?: AutocompleteParsingResult.ContextValue; } @@ -31,7 +24,7 @@ export const notFound = ( descriptor: string, match: string, range: Range, - code: NotFoundCode, + code: DiagnosticCode, context?: AutocompleteParsingResult.ContextValue, ): DiagnosticWithContext => ({ message: `${descriptor} [${match}] not found.`, diff --git a/src/features/config.ts b/src/features/config.ts index 83bf0d93..a3e170da 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -1,5 +1,5 @@ import { openFile } from "@src/commands"; -import { notFound, NotFoundCode } from "@src/diagnostic"; +import { notFound } from "@src/diagnostic"; import AutocompleteResult from "@src/parser/AutocompleteResult"; import { getConfigPathByName, getConfigs } from "@src/repositories/configs"; import { config } from "@src/support/config"; @@ -170,30 +170,23 @@ export const diagnosticProvider = ( return null; } - const configPath = getConfigPathByName(param.value); - - const code: NotFoundCode = configPath - ? { - value: "config", - target: vscode.Uri.file(configPath.path).with( - withLineFragment(configPath.line), - ), - } - : "config"; - - return notFound("Config", param.value, detectedRange(param), code); + return notFound( + "Config", + param.value, + detectedRange(param), + "config", + ); }, ); }; export const codeActionProvider: CodeActionProviderFunction = async ( - code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (code !== "config") { + if (diagnostic.code !== "config") { return []; } diff --git a/src/features/env.ts b/src/features/env.ts index 9bf7c0e2..b045f241 100644 --- a/src/features/env.ts +++ b/src/features/env.ts @@ -91,10 +91,7 @@ export const diagnosticProvider = ( return null; } - return notFound("Env", param.value, detectedRange(param), { - value: "env", - target: vscode.Uri.file(projectPath(".env")), - }); + return notFound("Env", param.value, detectedRange(param), "env"); }, ); }; @@ -195,13 +192,12 @@ export const viteEnvCodeActionProvider: vscode.CodeActionProvider = { }; export const codeActionProvider: CodeActionProviderFunction = async ( - code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (code !== "env") { + if (diagnostic.code !== "env") { return []; } diff --git a/src/features/inertia.ts b/src/features/inertia.ts index 51cd8c46..92fcc66d 100644 --- a/src/features/inertia.ts +++ b/src/features/inertia.ts @@ -141,13 +141,12 @@ export const diagnosticProvider = ( }; export const codeActionProvider: CodeActionProviderFunction = async ( - code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (code !== "inertia") { + if (diagnostic.code !== "inertia") { return []; } diff --git a/src/features/translation.ts b/src/features/translation.ts index f9d5a017..303300c6 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -1,5 +1,5 @@ import { openFile } from "@src/commands"; -import { DiagnosticWithContext, notFound, NotFoundCode } from "@src/diagnostic"; +import { DiagnosticWithContext, notFound } from "@src/diagnostic"; import AutocompleteResult from "@src/parser/AutocompleteResult"; import { getTranslationItemByName, @@ -203,24 +203,11 @@ export const diagnosticProvider = ( return null; } - const lang = getLang(item as AutocompleteParsingResult.MethodCall); - - const translationPath = getTranslationPathByName(param.value, lang); - - const code: NotFoundCode = translationPath - ? { - value: "translation", - target: vscode.Uri.file(translationPath.path).with( - withLineFragment(translationPath.line), - ), - } - : "translation"; - return notFound( "Translation", param.value, detectedRange(param), - code, + "translation", item, ); }, @@ -228,13 +215,12 @@ export const diagnosticProvider = ( }; export const codeActionProvider: CodeActionProviderFunction = async ( - code: string, diagnostic: DiagnosticWithContext, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (code !== "translation") { + if (diagnostic.code !== "translation") { return []; } diff --git a/src/features/view.ts b/src/features/view.ts index bfaa9c59..014b8528 100644 --- a/src/features/view.ts +++ b/src/features/view.ts @@ -171,13 +171,12 @@ export const diagnosticProvider = ( }; export const codeActionProvider: CodeActionProviderFunction = async ( - code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, token: vscode.CancellationToken, ): Promise => { - if (code !== "view") { + if (diagnostic.code !== "view") { return []; } diff --git a/src/index.d.ts b/src/index.d.ts index 486d3114..dbc537e8 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,7 +2,6 @@ import * as vscode from "vscode"; import AutocompleteResult from "./parser/AutocompleteResult"; type CodeActionProviderFunction = ( - code: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument, range: vscode.Range | vscode.Selection, From e1b28340e23352a6a9ea33c643ecd75458def8b7 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Mon, 22 Dec 2025 19:43:14 +0000 Subject: [PATCH 09/11] support for json files --- src/features/config.ts | 19 ++----- src/features/translation.ts | 102 +++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 33 deletions(-) diff --git a/src/features/config.ts b/src/features/config.ts index a3e170da..929318f3 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -205,19 +205,6 @@ const addToFile = async ( diagnostic: vscode.Diagnostic, missingVar: string, ): Promise => { - return getCodeAction( - "Add variable to the configuration file", - missingVar, - diagnostic, - ); -}; - -const getCodeAction = async ( - title: string, - missingVar: string, - diagnostic: vscode.Diagnostic, - value?: string, -) => { const edit = new vscode.WorkspaceEdit(); const config = getConfigs().items.configs.find( @@ -284,7 +271,10 @@ const getCodeAction = async ( finalValue, ); - const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + const action = new vscode.CodeAction( + "Add config to the file", + vscode.CodeActionKind.QuickFix, + ); action.edit = edit; action.command = openFile( @@ -293,7 +283,6 @@ const getCodeAction = async ( finalValue.length - 3, ); action.diagnostics = [diagnostic]; - action.isPreferred = value === undefined; return action; }; diff --git a/src/features/translation.ts b/src/features/translation.ts index 303300c6..6380c085 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -12,7 +12,7 @@ import { findHoverMatchesInDoc } from "@src/support/doc"; import { getIndentNumber } from "@src/support/indent"; import { detectedRange, detectInDoc } from "@src/support/parser"; import { wordMatchRegex } from "@src/support/patterns"; -import { relativePath } from "@src/support/project"; +import { projectPath, relativePath } from "@src/support/project"; import { contract, createIndexMapping, @@ -230,28 +230,93 @@ export const codeActionProvider: CodeActionProviderFunction = async ( return []; } - const actions = await Promise.all([addToFile(diagnostic, missingVar)]); + const actions = await Promise.all([ + addToPhpFile(diagnostic, missingVar), + addToJsonFile(diagnostic, missingVar), + ]); return actions.filter((action) => action !== null); }; -const addToFile = async ( +const addToJsonFile = async ( diagnostic: DiagnosticWithContext, missingVar: string, ): Promise => { - return getCodeAction( - "Add variable to the translation file", - missingVar, - diagnostic, + const edit = new vscode.WorkspaceEdit(); + + const translation = getTranslationItemByName(missingVar); + + if (translation) { + return null; + } + + const translationPath = getTranslations().items.paths.find((path) => { + const lang = + getLang( + diagnostic.context as AutocompleteParsingResult.MethodCall, + ) ?? getTranslations().items.default; + + return !path.startsWith("vendor/") && path.endsWith(`${lang}.json`); + }); + + if (!translationPath) { + return null; + } + + const translationContents = await vscode.workspace.fs.readFile( + vscode.Uri.file(projectPath(translationPath)), ); + + const lines = translationContents.toString().split("\n"); + + let lineNumber = undefined; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith("}")) { + lineNumber = i; + } + } + + if (!lineNumber) { + return null; + } + + const indent = " ".repeat(getIndentNumber("json") ?? 4); + + const finalValue = `${indent}"${missingVar}": ""\n`; + + edit.insert( + vscode.Uri.file(projectPath(translationPath)), + new vscode.Position(lineNumber - 1, lines[lineNumber - 1].length), + ",", + ); + + edit.insert( + vscode.Uri.file(projectPath(translationPath)), + new vscode.Position(lineNumber, 0), + finalValue, + ); + + const action = new vscode.CodeAction( + "Add translation to the json file", + vscode.CodeActionKind.QuickFix, + ); + + action.edit = edit; + action.command = openFile( + projectPath(translationPath), + lineNumber, + finalValue.length - 2, + ); + action.diagnostics = [diagnostic]; + + return action; }; -const getCodeAction = async ( - title: string, - missingVar: string, +const addToPhpFile = async ( diagnostic: DiagnosticWithContext, - value?: string, -) => { + missingVar: string, +): Promise => { const edit = new vscode.WorkspaceEdit(); const translation = getTranslationItemByName(missingVar); @@ -260,12 +325,11 @@ const getCodeAction = async ( return null; } - const lang = getLang( - diagnostic.context as AutocompleteParsingResult.MethodCall, + const translationPath = getTranslationPathByName( + missingVar, + getLang(diagnostic.context as AutocompleteParsingResult.MethodCall), ); - const translationPath = getTranslationPathByName(missingVar, lang); - if (!translationPath) { return null; } @@ -312,7 +376,10 @@ const getCodeAction = async ( finalValue, ); - const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + const action = new vscode.CodeAction( + "Add translation to the php file", + vscode.CodeActionKind.QuickFix, + ); action.edit = edit; action.command = openFile( @@ -321,7 +388,6 @@ const getCodeAction = async ( finalValue.length - 3, ); action.diagnostics = [diagnostic]; - action.isPreferred = value === undefined; return action; }; From 7121cd78cdf46c782be932ad1c525e10244109be Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Mon, 22 Dec 2025 19:49:56 +0000 Subject: [PATCH 10/11] uppercase --- src/features/translation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/translation.ts b/src/features/translation.ts index 6380c085..ca9c92a0 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -298,7 +298,7 @@ const addToJsonFile = async ( ); const action = new vscode.CodeAction( - "Add translation to the json file", + "Add translation to the JSON file", vscode.CodeActionKind.QuickFix, ); @@ -377,7 +377,7 @@ const addToPhpFile = async ( ); const action = new vscode.CodeAction( - "Add translation to the php file", + "Add translation to the PHP file", vscode.CodeActionKind.QuickFix, ); From 5d756ce42261762321fc815d4ad9a32b6fabcc96 Mon Sep 17 00:00:00 2001 From: N1ebieski Date: Mon, 22 Dec 2025 21:59:28 +0000 Subject: [PATCH 11/11] refactoring --- src/features/config.ts | 28 ++++++++++----------- src/features/translation.ts | 49 ++++++++++++++----------------------- 2 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/features/config.ts b/src/features/config.ts index 929318f3..189e7709 100644 --- a/src/features/config.ts +++ b/src/features/config.ts @@ -229,12 +229,12 @@ const addToFile = async ( // Remember that Laravel config keys can go to subfolders, 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 nestedKeys = + const countNestedKeys = missingVar.substring(missingVar.indexOf(`${fileName}.`)).split(".") .length - 1; // Case when a user tries to add a new config key to an existing key that is not an array - if (!configPath.line && nestedKeys > 1) { + if (!configPath.line && countNestedKeys > 1) { return null; } @@ -242,26 +242,24 @@ const addToFile = async ( vscode.Uri.file(configPath.path), ); - let lineNumber = configPath.line ? Number(configPath.line) - 1 : undefined; + const lineNumberFromConfig = configPath.line + ? Number(configPath.line) - 1 + : undefined; - if (!lineNumber) { - // Default to the end of the file - const lines = configContents.toString().split("\n"); + const lineNumber = + lineNumberFromConfig ?? + configContents + .toString() + .split("\n") + .findIndex((line) => line.startsWith("];")); - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith("];")) { - lineNumber = i; - } - } - } - - if (!lineNumber) { + if (lineNumber === -1) { return null; } const key = missingVar.split(".").pop(); - const indent = " ".repeat((getIndentNumber("php") ?? 4) * nestedKeys); + const indent = " ".repeat((getIndentNumber("php") ?? 4) * countNestedKeys); const finalValue = `${indent}'${key}' => '',\n`; diff --git a/src/features/translation.ts b/src/features/translation.ts index ca9c92a0..affaaf6b 100644 --- a/src/features/translation.ts +++ b/src/features/translation.ts @@ -250,14 +250,13 @@ const addToJsonFile = async ( return null; } - const translationPath = getTranslations().items.paths.find((path) => { - const lang = - getLang( - diagnostic.context as AutocompleteParsingResult.MethodCall, - ) ?? getTranslations().items.default; + const lang = + getLang(diagnostic.context as AutocompleteParsingResult.MethodCall) ?? + getTranslations().items.default; - return !path.startsWith("vendor/") && path.endsWith(`${lang}.json`); - }); + const translationPath = getTranslations().items.paths.find( + (path) => !path.startsWith("vendor/") && path.endsWith(`${lang}.json`), + ); if (!translationPath) { return null; @@ -269,15 +268,9 @@ const addToJsonFile = async ( const lines = translationContents.toString().split("\n"); - let lineNumber = undefined; - - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith("}")) { - lineNumber = i; - } - } + const lineNumber = lines.findIndex((line) => line.startsWith("}")); - if (!lineNumber) { + if (lineNumber === -1) { return null; } @@ -334,10 +327,10 @@ const addToPhpFile = async ( return null; } - const nestedKeys = missingVar.split(".").length - 1; + const countNestedKeys = missingVar.split(".").length - 1; // Case when a user tries to add a new translation key to an existing key that is not an array - if (!translationPath.line && nestedKeys > 1) { + if (!translationPath.line && countNestedKeys > 1) { return null; } @@ -345,28 +338,24 @@ const addToPhpFile = async ( vscode.Uri.file(translationPath.path), ); - let lineNumber = translationPath.line + const lineNumberFromConfig = translationPath.line ? translationPath.line - 1 : undefined; - if (!lineNumber) { - // Default to the end of the file - const lines = translationContents.toString().split("\n"); - - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith("];")) { - lineNumber = i; - } - } - } + const lineNumber = + lineNumberFromConfig ?? + translationContents + .toString() + .split("\n") + .findIndex((line) => line.startsWith("];")); - if (!lineNumber) { + if (lineNumber === -1) { return null; } const key = missingVar.split(".").pop(); - const indent = " ".repeat((getIndentNumber("php") ?? 4) * nestedKeys); + const indent = " ".repeat((getIndentNumber("php") ?? 4) * countNestedKeys); const finalValue = `${indent}'${key}' => '',\n`;