From 42fe4a7bbcafd4637db7740029a2f0a61583708a Mon Sep 17 00:00:00 2001 From: Jose V Sebastian Date: Sun, 18 Feb 2024 12:55:52 +0530 Subject: [PATCH 1/4] refactor: clean code --- .eslintrc => .eslintrc.json | 5 +- .github/workflows/release.yaml | 2 +- package.json | 4 +- src/Cache.ts | 39 +++++++++ src/Executable.ts | 118 ++++++++++++++++++++++++++++ src/ExtensionConfiguration.ts | 51 +++++++----- src/GoogleJavaFormatEditProvider.ts | 53 +++++++------ src/GoogleJavaFormatEditService.ts | 40 +++------- src/GoogleJavaFormatterSync.ts | 65 ++++++--------- src/IGoogleJavaFormatter.ts | 11 +-- src/extension.ts | 80 +++++-------------- src/getExtensionCacheFolder.ts | 8 -- src/getJarLocalPathFromConfig.ts | 4 +- src/getUriFromString.ts | 9 +++ src/utils.ts | 18 ----- 15 files changed, 300 insertions(+), 207 deletions(-) rename .eslintrc => .eslintrc.json (86%) create mode 100644 src/Cache.ts create mode 100644 src/Executable.ts delete mode 100644 src/getExtensionCacheFolder.ts create mode 100644 src/getUriFromString.ts delete mode 100644 src/utils.ts diff --git a/.eslintrc b/.eslintrc.json similarity index 86% rename from .eslintrc rename to .eslintrc.json index 51b1b6d..b2ebc68 100644 --- a/.eslintrc +++ b/.eslintrc.json @@ -16,6 +16,10 @@ "rules": { "@typescript-eslint/naming-convention": "warn", "@typescript-eslint/semi": "warn", + "@typescript-eslint/no-unused-vars": [ + "error", + { "ignoreRestSiblings": true } + ], "curly": "warn", "eqeqeq": "warn", "no-throw-literal": "warn", @@ -24,4 +28,3 @@ }, "ignorePatterns": ["out", "dist", "**/*.d.ts"] } - diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 28a786f..e0e9013 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,7 +23,7 @@ jobs: node-version: 20 - name: Install dependencies - run: npm ci + run: yarn install --frozen-lockfile - name: Semantic Release run: npx semantic-release diff --git a/package.json b/package.json index 5cc497f..9e0e670 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ }, "java.format.settings.google.version": { "type": "string", - "markdownDescription": "*Recommended.* Specifies version to be used of [Google Java Format jar executable](https://github.com/google/google-java-format/releases) in format `{major}.{minor}.{patch}`. Default: `latest`.", - "default": null, + "markdownDescription": "*Recommended.* Specifies version to be used of [Google Java Format executable](https://github.com/google/google-java-format/releases) in format `{major}.{minor}.{patch}`. Default: `latest`.", + "default": "latest", "scope": "window" }, "java.format.settings.google.extra": { diff --git a/src/Cache.ts b/src/Cache.ts new file mode 100644 index 0000000..dd6187e --- /dev/null +++ b/src/Cache.ts @@ -0,0 +1,39 @@ +import { ExtensionContext, LogOutputChannel, Uri, workspace } from "vscode"; + +export class Cache { + private uri: Uri; + private constructor( + context: ExtensionContext, + private log: LogOutputChannel, + cacheFolder: string, + ) { + this.uri = Uri.joinPath(context.extensionUri, cacheFolder); + } + + public static async getInstance( + context: ExtensionContext, + log: LogOutputChannel, + cacheFolder = "cache", + ) { + const cache = new Cache(context, log, cacheFolder); + await cache.init(); + // TODO: clear old cache + return cache; + } + + private init = async () => { + // Create the cache directory if it doesn't exist + try { + await workspace.fs.createDirectory(this.uri); + this.log.debug(`Cache directory created at ${this.uri.toString()}`); + } catch (error) { + this.log.error(`Failed to create cache directory: ${error}`); + throw error; + } + }; + + // TODO: remove this + get dir(): Uri { + return this.uri; + } +} diff --git a/src/Executable.ts b/src/Executable.ts new file mode 100644 index 0000000..0280396 --- /dev/null +++ b/src/Executable.ts @@ -0,0 +1,118 @@ +import { execSync } from "node:child_process"; +import { + // CancellationToken, + ExtensionContext, + LogOutputChannel, + // Progress, + ProgressLocation, + window, +} from "vscode"; +import { Cache } from "./Cache"; +import { ExtensionConfiguration } from "./ExtensionConfiguration"; +import getJarLocalPathFromConfig from "./getJarLocalPathFromConfig"; +import path = require("node:path"); + +export class Executable { + private runner: string = null!; + private cwd: string = null!; + + private constructor( + private context: ExtensionContext, + private config: ExtensionConfiguration, + private cache: Cache, + private log: LogOutputChannel, + ) {} + + public static async getInstance( + context: ExtensionContext, + config: ExtensionConfiguration, + cache: Cache, + log: LogOutputChannel, + ) { + const instance = new Executable(context, config, cache, log); + await instance.load(); + return instance; + } + + run = async ({ + args, + stdin, + }: { + args: string[]; + stdin: string; + signal?: AbortSignal; + }) => { + return new Promise((resolve, reject) => { + try { + const command = `${this.runner} ${args.join(" ")} -`; + + this.log.debug(`> ${command}`); + + const stdout: string = execSync(command, { + cwd: this.cwd, + encoding: "utf8", + input: stdin, + maxBuffer: Infinity, + windowsHide: true, + }); + + resolve(stdout); + } catch (e) { + reject(e); + } + }); + }; + + subscribe = () => { + this.config.subscriptions.push(this.configurationChangeListener); + }; + + private load = async () => + // progress?: Progress<{ + // message?: string | undefined; + // increment?: number | undefined; + // }>, + // token?: CancellationToken, + { + // TODO: move caching logic to class Cache + // TODO: move versioning logic to class GoogleJavaFormatVersionManager + const { fsPath } = await getJarLocalPathFromConfig({ + cacheDir: this.cache.dir, + log: this.log, + config: this.config, + }); + + const extname = path.extname(fsPath); + const basename = path.basename(fsPath); + const dirname = path.dirname(fsPath); + + this.runner = + extname === ".jar" ? `java -jar ${basename}` : extname; + + this.cwd = dirname; + }; + + private configurationChangeListener = async () => { + this.log.info("Configuration change detected."); + const action = await window.showInformationMessage( + "Configuration change detected. Update executable?", + "Update", + "Ignore", + ); + + if (action !== "Update") { + this.log.debug("User ignored updating executable."); + return; + } + + this.log.debug("Updating executable..."); + window.withProgress( + { + location: ProgressLocation.Notification, + title: "Updating executable...", + cancellable: false, + }, + this.load, + ); + }; +} diff --git a/src/ExtensionConfiguration.ts b/src/ExtensionConfiguration.ts index 924662f..b011b1b 100644 --- a/src/ExtensionConfiguration.ts +++ b/src/ExtensionConfiguration.ts @@ -1,5 +1,6 @@ -import { Uri } from "vscode"; -import { getJavaConfiguration } from "./utils"; +import { ConfigurationChangeEvent, ExtensionContext, workspace } from "vscode"; + +const SECTION = `java.format.settings.google`; export type GoogleJavaFormatVersion = | `${number}.${number}.${number}` @@ -9,28 +10,42 @@ export interface GoogleJavaFormatConfiguration { executable?: string; version?: GoogleJavaFormatVersion; extra?: string; - jarUri: Uri; } export class ExtensionConfiguration implements GoogleJavaFormatConfiguration { - readonly executable?: string; - readonly version?: GoogleJavaFormatVersion; - readonly extra?: string; - jarUri: Uri = null!; + executable?: string; + version?: GoogleJavaFormatVersion; + extra?: string; + readonly subscriptions: (( + config: GoogleJavaFormatConfiguration, + ) => void)[] = []; - constructor() { - return new Proxy(this, this.handler); + constructor(private context: ExtensionContext) { + this.load(); } - private handler: ProxyHandler = { - get(target, prop) { - if (prop === "jarUri") { - return target[prop]; - } + subscribe = () => { + this.context.subscriptions.push( + workspace.onDidChangeConfiguration( + this.configurationChangeListener, + ), + ); + }; + + private load = () => { + const { subscriptions, ...config } = + workspace.getConfiguration(SECTION); + Object.assign(this, config); + }; + + private configurationChangeListener = async ( + event: ConfigurationChangeEvent, + ) => { + if (!event.affectsConfiguration(SECTION)) { + return; + } - return getJavaConfiguration().get( - `format.settings.google.${String(prop)}`, - ); - }, + this.load(); + this.subscriptions.forEach((fn) => fn(this)); }; } diff --git a/src/GoogleJavaFormatEditProvider.ts b/src/GoogleJavaFormatEditProvider.ts index 6ccdba0..66b3d61 100644 --- a/src/GoogleJavaFormatEditProvider.ts +++ b/src/GoogleJavaFormatEditProvider.ts @@ -1,8 +1,9 @@ import { - // CancellationToken, + CancellationToken, DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider, - // FormattingOptions, + // ExtensionContext, + FormattingOptions, LogOutputChannel, Range, TextDocument, @@ -21,21 +22,29 @@ export default class GoogleJavaFormatEditProvider private log: LogOutputChannel, ) {} - private async formatText(text: string, range: Range): Promise { + private formatText = async ( + text: string, + range: Range, + token: CancellationToken, + ): Promise => { const startTime = new Date().getTime(); - const result = await this.formatter.format(text, [ - range.start.line + 1, - range.end.line + 1, - ]); + const controller = new AbortController(); + token.onCancellationRequested(controller.abort); + + const result = await this.formatter.format( + text, + [range.start.line + 1, range.end.line + 1], + controller.signal, + ); const duration = new Date().getTime() - startTime; this.log.info(`Formatting completed in ${duration}ms.`); return result; - } + }; - private errorHandler(error: unknown): TextEdit[] { + private errorHandler = (error: unknown): TextEdit[] => { const message = (error as Error)?.message ?? "Failed to format java code using Google Java Format"; @@ -44,44 +53,44 @@ export default class GoogleJavaFormatEditProvider window.showErrorMessage(message); return []; - } + }; - public async provideDocumentRangeFormattingEdits( + public provideDocumentRangeFormattingEdits = async ( document: TextDocument, range: Range, - // TODO: implement formatting options - // options: FormattingOptions, - // TODO: cancellation token - // token: CancellationToken, - ): Promise { + options: FormattingOptions, + token: CancellationToken, + ): Promise => { const documentRange = new Range(0, 0, document.lineCount, 0); try { const textAfterFormat = await this.formatText( document.getText(), range, + token, ); return [TextEdit.replace(documentRange, textAfterFormat)]; } catch (error) { return this.errorHandler(error); } - } + }; - public async provideDocumentFormattingEdits( + public provideDocumentFormattingEdits = async ( document: TextDocument, - // options: FormattingOptions, - // token: CancellationToken, - ): Promise { + options: FormattingOptions, + token: CancellationToken, + ): Promise => { const documentRange = new Range(0, 0, document.lineCount, 0); try { const textAfterFormat = await this.formatText( document.getText(), documentRange, + token, ); return [TextEdit.replace(documentRange, textAfterFormat)]; } catch (error) { return this.errorHandler(error); } - } + }; } diff --git a/src/GoogleJavaFormatEditService.ts b/src/GoogleJavaFormatEditService.ts index 1a70b64..427d7bb 100644 --- a/src/GoogleJavaFormatEditService.ts +++ b/src/GoogleJavaFormatEditService.ts @@ -1,45 +1,27 @@ import { - Disposable, DocumentRangeFormattingEditProvider, DocumentSelector, + ExtensionContext, languages, LogOutputChannel, } from "vscode"; -export default class GoogleJavaFormatEditService implements Disposable { - private formatterHandler: Disposable | undefined; - - private get selector(): DocumentSelector { - return { language: "java" }; - } +export default class GoogleJavaFormatEditService { + private readonly selector: DocumentSelector = { language: "java" }; constructor( private editProvider: DocumentRangeFormattingEditProvider, + private context: ExtensionContext, private log: LogOutputChannel, ) {} - public registerGlobal(): Disposable { - this.registerDocumentFormatEditorProviders(this.selector); - this.log.debug( - "Enabling Google Java Formatter globally", - this.selector, - ); - - return this; - } - - public dispose() { - this.formatterHandler?.dispose(); - this.formatterHandler = undefined; - } - - private registerDocumentFormatEditorProviders(selector: DocumentSelector) { - this.dispose(); - - this.formatterHandler = + public subscribe = () => { + this.context.subscriptions.push( languages.registerDocumentRangeFormattingEditProvider( - selector, + this.selector, this.editProvider, - ); - } + ), + ); + this.log.debug("Enabled Google Java Formatter globally", this.selector); + }; } diff --git a/src/GoogleJavaFormatterSync.ts b/src/GoogleJavaFormatterSync.ts index c942877..b06fb7e 100644 --- a/src/GoogleJavaFormatterSync.ts +++ b/src/GoogleJavaFormatterSync.ts @@ -1,49 +1,34 @@ -import { execSync } from "child_process"; -import { IGoogleJavaFormatter } from "./IGoogleJavaFormatter"; import { LogOutputChannel } from "vscode"; -import { GoogleJavaFormatConfiguration } from "./ExtensionConfiguration"; +import { Executable } from "./Executable"; +import { ExtensionConfiguration } from "./ExtensionConfiguration"; +import { IGoogleJavaFormatter } from "./IGoogleJavaFormatter"; export default class GoogleJavaFormatterSync implements IGoogleJavaFormatter { constructor( - private config: GoogleJavaFormatConfiguration, + private executable: Executable, + private config: ExtensionConfiguration, private log: LogOutputChannel, ) {} - dispose() { - return; - } - - init() { - return this; - } - - public format(text: string, range?: [number, number]): Promise { - return new Promise((resolve, reject) => { - try { - let command = `java -jar "${this.config.jarUri.fsPath}"`; - - if (this.config.extra) { - command += ` ${this.config.extra}`; - } - - if (range) { - command += ` --lines ${range[0]}:${range[1]}`; - } - - command += " -"; - - this.log.debug(`> ${command}`); - - const stdout: string = execSync(command, { - encoding: "utf8", - input: text, - maxBuffer: Infinity, - windowsHide: true, - }); - resolve(stdout); - } catch (e) { - reject(e); - } + public format = async ( + text: string, + range?: [number, number], + signal?: AbortSignal, + ): Promise => { + const args = []; + + if (this.config.extra) { + args.push(this.config.extra); + } + + if (range) { + args.push(`--lines ${range[0]}:${range[1]}`); + } + + return this.executable.run({ + args, + stdin: text, + signal, }); - } + }; } diff --git a/src/IGoogleJavaFormatter.ts b/src/IGoogleJavaFormatter.ts index 7f4abc8..432150b 100644 --- a/src/IGoogleJavaFormatter.ts +++ b/src/IGoogleJavaFormatter.ts @@ -1,6 +1,7 @@ -import { Disposable } from "vscode"; - -export interface IGoogleJavaFormatter extends Disposable { - format(text: string, range?: [number, number]): Promise; - init(): Disposable; +export interface IGoogleJavaFormatter { + format( + text: string, + range?: [number, number], + signal?: AbortSignal, + ): Promise; } diff --git a/src/extension.ts b/src/extension.ts index c105654..3daf622 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,16 +1,10 @@ -import { - ConfigurationChangeEvent, - ExtensionContext, - ProgressLocation, - window, - workspace, -} from "vscode"; +import { ExtensionContext, window } from "vscode"; +import { Cache } from "./Cache"; +import { Executable } from "./Executable"; import { ExtensionConfiguration } from "./ExtensionConfiguration"; import GoogleJavaFormatEditProvider from "./GoogleJavaFormatEditProvider"; import GoogleJavaFormatEditService from "./GoogleJavaFormatEditService"; import GoogleJavaFormatterSync from "./GoogleJavaFormatterSync"; -import getExtensionCacheFolder from "./getExtensionCacheFolder"; -import getJarLocalPathFromConfig from "./getJarLocalPathFromConfig"; export async function activate(context: ExtensionContext) { const log = window.createOutputChannel("Google Java Format for VS Code", { @@ -18,62 +12,26 @@ export async function activate(context: ExtensionContext) { }); context.subscriptions.push(log); - const config = new ExtensionConfiguration(); - const cacheDir = getExtensionCacheFolder(context); + const config = new ExtensionConfiguration(context); + config.subscribe(); - const configureJarFile = async () => { - config.jarUri = await getJarLocalPathFromConfig({ - cacheDir, - log, - config, - }); - }; - await configureJarFile(); - - const configurationChangeListener = async ( - event: ConfigurationChangeEvent, - ) => { - if ( - // check if configuration updated - !event.affectsConfiguration("java.format.settings.google") - ) { - // jar update not needed - return; - } - - log.info("Configuration change detected."); - const action = await window.showInformationMessage( - "Configuration change detected. Update jar file?", - "Update", - "Ignore", - ); - - if (action !== "Update") { - log.debug("Change ignored."); - return; - } - - log.debug("Updating jar file..."); - window.withProgress( - { - location: ProgressLocation.Notification, - title: "Updating jar file...", - cancellable: false, - }, - configureJarFile, - ); - }; - context.subscriptions.push( - workspace.onDidChangeConfiguration(configurationChangeListener), + const cache = await Cache.getInstance(context, log); + const executable = await Executable.getInstance( + context, + config, + cache, + log, ); + executable.subscribe(); - const formatter = new GoogleJavaFormatterSync(config, log); - context.subscriptions.push(formatter.init()); - + const formatter = new GoogleJavaFormatterSync(executable, config, log); const editProvider = new GoogleJavaFormatEditProvider(formatter, log); - - const editService = new GoogleJavaFormatEditService(editProvider, log); - context.subscriptions.push(editService.registerGlobal()); + const editService = new GoogleJavaFormatEditService( + editProvider, + context, + log, + ); + editService.subscribe(); } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/src/getExtensionCacheFolder.ts b/src/getExtensionCacheFolder.ts deleted file mode 100644 index be5dff4..0000000 --- a/src/getExtensionCacheFolder.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ExtensionContext, Uri } from "vscode"; - -export default function getExtensionCacheFolder( - context: ExtensionContext, - cacheFolder = "cache", -): Uri { - return Uri.joinPath(context.extensionUri, cacheFolder); -} diff --git a/src/getJarLocalPathFromConfig.ts b/src/getJarLocalPathFromConfig.ts index 81e273a..704b1f4 100644 --- a/src/getJarLocalPathFromConfig.ts +++ b/src/getJarLocalPathFromConfig.ts @@ -1,11 +1,11 @@ import { LogOutputChannel, Uri } from "vscode"; +import { GoogleJavaFormatConfiguration } from "./ExtensionConfiguration"; import { downloadGoogleJavaFormatJar, downloadGoogleJavaFormatJarByVersion, } from "./downloadGoogleJavaFormatJar"; -import { GoogleJavaFormatConfiguration } from "./ExtensionConfiguration"; import { getListOfGoogleJavaFormatVersions } from "./getListOfGoogleJavaFormatVersions"; -import { getUriFromString } from "./utils"; +import { getUriFromString } from "./getUriFromString"; export type GetJarLocalPathFromConfigOptions = { cacheDir: Uri; diff --git a/src/getUriFromString.ts b/src/getUriFromString.ts new file mode 100644 index 0000000..dca9f22 --- /dev/null +++ b/src/getUriFromString.ts @@ -0,0 +1,9 @@ +import { Uri } from "vscode"; + +export function getUriFromString(value: string) { + try { + return Uri.parse(value, true); + } catch (e) { + return Uri.file(value); + } +} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index ddf4ab7..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Uri, workspace, WorkspaceConfiguration } from "vscode"; - -export function getJavaConfiguration(): WorkspaceConfiguration { - return workspace.getConfiguration("java"); -} - -export function isRemote(f: string | null) { - return ( - f !== null && - (f.startsWith("http:/") || - f.startsWith("https:/") || - f.startsWith("file:/")) - ); -} - -export function getUriFromString(f: string) { - return isRemote(f) ? Uri.parse(f) : Uri.file(f); -} From 4805f80b444f7efe9ffda3f64ca3443ba84851ce Mon Sep 17 00:00:00 2001 From: Jose V Sebastian Date: Sun, 18 Feb 2024 16:51:01 +0530 Subject: [PATCH 2/4] feat: add support for native executable closes #17 --- README.md | 3 +- package.json | 18 ++++++ src/Cache.ts | 52 ++++++++++++++++-- src/Executable.ts | 33 ++++++----- src/ExtensionConfiguration.ts | 4 ++ src/GoogleJavaFormatRelease.ts | 34 ++++++++++++ src/downloadFile.ts | 53 ------------------ src/downloadGoogleJavaFormatJar.ts | 42 -------------- src/getJarLocalPathFromConfig.ts | 58 -------------------- src/getLatestReleaseOfGoogleJavaFormat.ts | 17 ++++++ src/getListOfGoogleJavaFormatVersions.ts | 44 --------------- src/getReleaseOfGoogleJavaFormatByVersion.ts | 20 +++++++ src/resolveExecutableFileFromConfig.ts | 37 +++++++++++++ 13 files changed, 200 insertions(+), 215 deletions(-) create mode 100644 src/GoogleJavaFormatRelease.ts delete mode 100644 src/downloadFile.ts delete mode 100644 src/downloadGoogleJavaFormatJar.ts delete mode 100644 src/getJarLocalPathFromConfig.ts create mode 100644 src/getLatestReleaseOfGoogleJavaFormat.ts delete mode 100644 src/getListOfGoogleJavaFormatVersions.ts create mode 100644 src/getReleaseOfGoogleJavaFormatByVersion.ts create mode 100644 src/resolveExecutableFileFromConfig.ts diff --git a/README.md b/README.md index 8e15378..5271ebc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ Format your java files using Google Java Format program which follows Google Jav This extension contributes the following settings: * `java.format.settings.google.executable`: *Not Recommended.* Specifies url or file path to [Google Java Format jar executable](https://github.com/google/google-java-format/releases). Overrides `java.format.settings.google.version`. -* `java.format.settings.google.version`: *Recommended.* Specifies version to be used of [Google Java Format jar executable](https://github.com/google/google-java-format/releases) in format `{major}.{minor}.{patch}`. Default: `latest`. +* `java.format.settings.google.version`: *Recommended.* Specifies version to be used of [Google Java Format executable](https://github.com/google/google-java-format/releases) in format `{major}.{minor}.{patch}`. Default: `latest`. +* `java.format.settings.google.mode`: Specifies the runtime mode of [Google Java Format](https://github.com/google/google-java-format/releases). Used with `java.format.settings.google.version`. Default: `native-binary`. * `java.format.settings.google.extra`: Extra CLI arguments to pass to [Google Java Format](https://github.com/google/google-java-format). Please refer [Google Java Format repository](https://github.com/google/google-java-format) for available versions and CLI arguments. diff --git a/package.json b/package.json index 9e0e670..ea7b830 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,24 @@ "default": "latest", "scope": "window" }, + "java.format.settings.google.mode": { + "type": "string", + "markdownDescription": "Specifies the runtime mode of [Google Java Format](https://github.com/google/google-java-format/releases). Used with `java.format.settings.google.version`", + "default": "native-binary", + "enum": [ + "jar-file", + "native-binary" + ], + "enumItemLabels": [ + "Jar File", + "Native Binary" + ], + "enumDescriptions": [ + "Use Java runtime to execute jar file of Google Java Format.", + "Use Native Binary of Google Java Format, if available, otherwise, revert to Jar File." + ], + "scope": "window" + }, "java.format.settings.google.extra": { "type": "string", "markdownDescription": "Extra CLI arguments to pass to [Google Java Format](https://github.com/google/google-java-format).", diff --git a/src/Cache.ts b/src/Cache.ts index dd6187e..ba660d9 100644 --- a/src/Cache.ts +++ b/src/Cache.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; +import path = require("node:path"); import { ExtensionContext, LogOutputChannel, Uri, workspace } from "vscode"; export class Cache { @@ -32,8 +34,50 @@ export class Cache { } }; - // TODO: remove this - get dir(): Uri { - return this.uri; - } + get = async (url: string) => { + const basename = path.basename(url); + + const dirname = Uri.joinPath( + this.uri, + createHash("md5").update(url).digest("hex"), + ); + const localPath = Uri.joinPath(dirname, basename); + + try { + // Check if the file is already cached locally + await workspace.fs.stat(localPath); + + this.log.info(`Using cached file at ${localPath.toString()}`); + + return localPath; + } catch (error) { + // Create the cache directory if it doesn't exist + try { + await workspace.fs.createDirectory(dirname); + this.log.debug( + `Cache directory created at ${dirname.toString()}`, + ); + } catch (error) { + this.log.error(`Failed to create cache directory: ${error}`); + throw error; + } + + // Download the file and write it to the cache directory + this.log.info(`Downloading file from ${url}`); + + const response = await fetch(url); + if (response.ok) { + const buffer = await response.arrayBuffer(); + await workspace.fs.writeFile(localPath, new Uint8Array(buffer)); + + this.log.info(`File saved to ${localPath.toString()}`); + + return localPath; + } else { + throw new Error( + `Failed to download file from ${url}: ${response.status} ${response.statusText}`, + ); + } + } + }; } diff --git a/src/Executable.ts b/src/Executable.ts index 0280396..9acc4be 100644 --- a/src/Executable.ts +++ b/src/Executable.ts @@ -9,7 +9,7 @@ import { } from "vscode"; import { Cache } from "./Cache"; import { ExtensionConfiguration } from "./ExtensionConfiguration"; -import getJarLocalPathFromConfig from "./getJarLocalPathFromConfig"; +import { resolveExecutableFileFromConfig } from "./resolveExecutableFileFromConfig"; import path = require("node:path"); export class Executable { @@ -74,20 +74,27 @@ export class Executable { // }>, // token?: CancellationToken, { - // TODO: move caching logic to class Cache - // TODO: move versioning logic to class GoogleJavaFormatVersionManager - const { fsPath } = await getJarLocalPathFromConfig({ - cacheDir: this.cache.dir, - log: this.log, - config: this.config, - }); - - const extname = path.extname(fsPath); - const basename = path.basename(fsPath); + const uri = await resolveExecutableFileFromConfig( + this.config, + this.log, + ); + + const { fsPath } = + uri.scheme === "file" + ? uri + : await this.cache.get(uri.toString()); + + const isJar = fsPath.endsWith(".jar"); const dirname = path.dirname(fsPath); + const basename = path.basename(fsPath); - this.runner = - extname === ".jar" ? `java -jar ${basename}` : extname; + if (isJar) { + this.runner = `java -jar ./${basename}`; + } else if (process.platform === "win32") { + this.runner = basename; + } else { + this.runner = `./${basename}`; + } this.cwd = dirname; }; diff --git a/src/ExtensionConfiguration.ts b/src/ExtensionConfiguration.ts index b011b1b..2824b0a 100644 --- a/src/ExtensionConfiguration.ts +++ b/src/ExtensionConfiguration.ts @@ -6,15 +6,19 @@ export type GoogleJavaFormatVersion = | `${number}.${number}.${number}` | "latest"; +export type GoogleJavaFormatMode = "jar-file" | "native-binary"; // | "background-service" + export interface GoogleJavaFormatConfiguration { executable?: string; version?: GoogleJavaFormatVersion; + mode?: GoogleJavaFormatMode; extra?: string; } export class ExtensionConfiguration implements GoogleJavaFormatConfiguration { executable?: string; version?: GoogleJavaFormatVersion; + mode?: GoogleJavaFormatMode; extra?: string; readonly subscriptions: (( config: GoogleJavaFormatConfiguration, diff --git a/src/GoogleJavaFormatRelease.ts b/src/GoogleJavaFormatRelease.ts new file mode 100644 index 0000000..cdf388e --- /dev/null +++ b/src/GoogleJavaFormatRelease.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +export type GoogleJavaFormatReleaseResponse = { + tag_name: string; + assets: { + browser_download_url: string; + }[]; +}; + +// FIXME: Only valid combinations should be here. +type System = `${NodeJS.Platform}-${NodeJS.Architecture}`; + +export const parseGoogleJavaFormatReleaseResponse = ({ + tag_name: tag, + assets: arr, +}: GoogleJavaFormatReleaseResponse) => { + const assets = arr.reduce((map, { browser_download_url: url }) => { + if (url.endsWith("all-deps.jar")) { + map.set("java", url); + } else if (url.endsWith("darwin-arm64")) { + map.set("darwin-arm64", url); + } else if (url.endsWith("linux-x86-64")) { + map.set("linux-x64", url); + } else if (url.endsWith("windows-x86-64.exe")) { + map.set("win32-x64", url); + } + return map; + }, new Map()); + + return { tag, assets }; +}; + +export type GoogleJavaFormatRelease = ReturnType< + typeof parseGoogleJavaFormatReleaseResponse +>; diff --git a/src/downloadFile.ts b/src/downloadFile.ts deleted file mode 100644 index 05087eb..0000000 --- a/src/downloadFile.ts +++ /dev/null @@ -1,53 +0,0 @@ -import fetch from "node-fetch"; -import { LogOutputChannel, Uri, workspace } from "vscode"; - -export type DownloadFileOptions = { - cacheDir: Uri; - url: string; - filename: string; - log: LogOutputChannel; -}; - -export default async function downloadFile({ - cacheDir, - url, - filename, - log, -}: DownloadFileOptions): Promise { - const localPath = Uri.joinPath(cacheDir, filename); - - try { - // Check if the file is already cached locally - await workspace.fs.stat(localPath); - - log.info(`Using cached file at ${localPath.toString()}`); - - return localPath; - } catch (error) { - // Create the cache directory if it doesn't exist - try { - await workspace.fs.createDirectory(cacheDir); - log.debug(`Cache directory created at ${cacheDir.toString()}`); - } catch (error) { - log.error(`Failed to create cache directory: ${error}`); - throw error; - } - - // Download the file and write it to the cache directory - log.info(`Downloading file from ${url}`); - - const response = await fetch(url); - if (response.ok) { - const buffer = await response.arrayBuffer(); - await workspace.fs.writeFile(localPath, new Uint8Array(buffer)); - - log.info(`File saved to ${localPath.toString()}`); - - return localPath; - } else { - throw new Error( - `Failed to download file from ${url}: ${response.status} ${response.statusText}`, - ); - } - } -} diff --git a/src/downloadGoogleJavaFormatJar.ts b/src/downloadGoogleJavaFormatJar.ts deleted file mode 100644 index 0d0ae59..0000000 --- a/src/downloadGoogleJavaFormatJar.ts +++ /dev/null @@ -1,42 +0,0 @@ -import path = require("path"); -import { LogOutputChannel, Uri } from "vscode"; -import downloadFile from "./downloadFile"; - -export type DownloadGoogleJavaFormatJarByVersionOptions = { - version: string; - cacheDir: Uri; - log: LogOutputChannel; -}; - -export function downloadGoogleJavaFormatJarByVersion({ - version, - cacheDir, - log, -}: DownloadGoogleJavaFormatJarByVersionOptions): Promise { - return downloadGoogleJavaFormatJar({ - url: `https://github.com/google/google-java-format/releases/download/v${version}/google-java-format-${version}-all-deps.jar`, - cacheDir, - log, - }); -} - -export type DownloadGoogleJavaFormatJarOptions = { - url: string; - cacheDir: Uri; - log: LogOutputChannel; -}; - -export function downloadGoogleJavaFormatJar({ - url, - cacheDir, - log, -}: DownloadGoogleJavaFormatJarOptions): Promise { - const filename = path.basename(url); - - return downloadFile({ - cacheDir, - url, - filename, - log, - }); -} diff --git a/src/getJarLocalPathFromConfig.ts b/src/getJarLocalPathFromConfig.ts deleted file mode 100644 index 704b1f4..0000000 --- a/src/getJarLocalPathFromConfig.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { LogOutputChannel, Uri } from "vscode"; -import { GoogleJavaFormatConfiguration } from "./ExtensionConfiguration"; -import { - downloadGoogleJavaFormatJar, - downloadGoogleJavaFormatJarByVersion, -} from "./downloadGoogleJavaFormatJar"; -import { getListOfGoogleJavaFormatVersions } from "./getListOfGoogleJavaFormatVersions"; -import { getUriFromString } from "./getUriFromString"; - -export type GetJarLocalPathFromConfigOptions = { - cacheDir: Uri; - log: LogOutputChannel; - config: GoogleJavaFormatConfiguration; -}; - -export default async function getJarLocalPathFromConfig({ - cacheDir, - log, - config: { executable, version }, -}: GetJarLocalPathFromConfigOptions): Promise { - if (executable) { - log.debug( - `Retrieving jar file from 'executable' config: ${executable}`, - ); - const jarUri = getUriFromString(executable); - - if (jarUri.scheme === "file") { - log.info(`Using local jar file ${jarUri.toString()}`); - return jarUri; - } - - return downloadGoogleJavaFormatJar({ - url: jarUri.toString(), - cacheDir, - log, - }); - } - - if (version && version !== "latest") { - log.debug(`Retrieving jar file using 'version' config: ${version}`); - return downloadGoogleJavaFormatJarByVersion({ - version, - cacheDir, - log, - }); - } - - log.debug(`Retrieving list of available versions`); - const [{ major, minor, patch }] = await getListOfGoogleJavaFormatVersions(); - const latestVersion = `${major}.${minor}.${patch}` as const; - - log.debug(`Retrieving jar file for latest version (${latestVersion})`); - return downloadGoogleJavaFormatJarByVersion({ - version: latestVersion, - cacheDir, - log, - }); -} diff --git a/src/getLatestReleaseOfGoogleJavaFormat.ts b/src/getLatestReleaseOfGoogleJavaFormat.ts new file mode 100644 index 0000000..03e57af --- /dev/null +++ b/src/getLatestReleaseOfGoogleJavaFormat.ts @@ -0,0 +1,17 @@ +import { + GoogleJavaFormatReleaseResponse, + parseGoogleJavaFormatReleaseResponse, +} from "./GoogleJavaFormatRelease"; + +export const getLatestReleaseOfGoogleJavaFormat = async () => { + const response = await fetch( + "https://api.github.com/repos/google/google-java-format/releases/latest", + ); + if (!response.ok) { + throw new Error("Failed to get latest release of Google Java Format."); + } + + return parseGoogleJavaFormatReleaseResponse( + (await response.json()) as GoogleJavaFormatReleaseResponse, + ); +}; diff --git a/src/getListOfGoogleJavaFormatVersions.ts b/src/getListOfGoogleJavaFormatVersions.ts deleted file mode 100644 index 557f97a..0000000 --- a/src/getListOfGoogleJavaFormatVersions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import fetch from "node-fetch"; - -export type Version = { - major: number; - minor: number; - patch: number; -}; - -export const getListOfGoogleJavaFormatVersions = async () => { - const response = await fetch( - `https://api.github.com/repos/google/google-java-format/tags`, - ); - if (!response.ok) { - throw new Error( - "Failed to retrieve list of google java format versions", - ); - } - - const tags = (await response.json()) as { name: string }[]; - const versions = tags.reduce((acc, { name }) => { - const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(name); - - if (match) { - const [, major, minor, patch] = match; - acc.push({ - major: parseInt(major), - minor: parseInt(minor), - patch: parseInt(patch), - }); - } - - return acc; - }, []); - - return versions.sort((a, b) => { - if (a.major !== b.major) { - return b.major - a.major; - } - if (a.minor !== b.minor) { - return b.minor - a.minor; - } - return b.patch - a.patch; - }); -}; diff --git a/src/getReleaseOfGoogleJavaFormatByVersion.ts b/src/getReleaseOfGoogleJavaFormatByVersion.ts new file mode 100644 index 0000000..5268cbb --- /dev/null +++ b/src/getReleaseOfGoogleJavaFormatByVersion.ts @@ -0,0 +1,20 @@ +import { GoogleJavaFormatVersion } from "./ExtensionConfiguration"; +import { + GoogleJavaFormatReleaseResponse, + parseGoogleJavaFormatReleaseResponse, +} from "./GoogleJavaFormatRelease"; + +export const getReleaseOfGoogleJavaFormatByVersion = async ( + version: Exclude, +) => { + const response = await fetch( + `https://api.github.com/repos/google/google-java-format/releases/tags/v${version}`, + ); + if (!response.ok) { + throw new Error(`Failed to get v${version} of Google Java Format.`); + } + + return parseGoogleJavaFormatReleaseResponse( + (await response.json()) as GoogleJavaFormatReleaseResponse, + ); +}; diff --git a/src/resolveExecutableFileFromConfig.ts b/src/resolveExecutableFileFromConfig.ts new file mode 100644 index 0000000..9a8cbe9 --- /dev/null +++ b/src/resolveExecutableFileFromConfig.ts @@ -0,0 +1,37 @@ +import { LogOutputChannel, Uri } from "vscode"; +import { ExtensionConfiguration } from "./ExtensionConfiguration"; +import { getLatestReleaseOfGoogleJavaFormat } from "./getLatestReleaseOfGoogleJavaFormat"; +import { getReleaseOfGoogleJavaFormatByVersion } from "./getReleaseOfGoogleJavaFormatByVersion"; +import { getUriFromString } from "./getUriFromString"; + +export async function resolveExecutableFileFromConfig( + { executable, mode, version }: ExtensionConfiguration, + log: LogOutputChannel, +): Promise { + if (executable) { + log.debug(`Using config key 'executable': ${executable}`); + + return getUriFromString(executable); + } + + const shouldCheckNativeBinary = mode === "native-binary"; + const system = `${process.platform}-${process.arch}` as const; + if (shouldCheckNativeBinary) { + log.debug(`Using native binary for ${system} if available`); + } + + if (version === "latest") { + log.debug(`Using latest version...`); + } + + const { assets } = + version && version !== "latest" + ? await getReleaseOfGoogleJavaFormatByVersion(version) + : await getLatestReleaseOfGoogleJavaFormat(); + + const url = + (shouldCheckNativeBinary && assets.get(system)) || assets.get("java")!; + log.debug(`Using url: ${url}`); + + return Uri.parse(url); +} From e7a3576ebc39eedcb8c04ef91b9c2f94421e501f Mon Sep 17 00:00:00 2001 From: Jose V Sebastian Date: Sun, 18 Feb 2024 17:14:58 +0530 Subject: [PATCH 3/4] ci: cleanup --- .github/workflows/release.yaml | 2 +- release.config.cjs | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e0e9013..28a786f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,7 +23,7 @@ jobs: node-version: 20 - name: Install dependencies - run: yarn install --frozen-lockfile + run: npm ci - name: Semantic Release run: npx semantic-release diff --git a/release.config.cjs b/release.config.cjs index 94a6268..d960700 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -1,13 +1,6 @@ /** @type {import('semantic-release').GlobalConfig} */ const config = { - branches: [ - "+([0-9])?(.{+([0-9]),x}).x", - "main", - "next", - "next-major", - { name: "beta", prerelease: "beta" }, - { name: "alpha", prerelease: "alpha" }, - ], + branches: ["main"], plugins: [ [ "@semantic-release/commit-analyzer", @@ -25,13 +18,7 @@ const config = { }, ], ["semantic-release-vsce", { packageVsix: true }], - [ - "@semantic-release/git", - { - message: - "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", - }, - ], + "@semantic-release/git", ["@semantic-release/github", { assets: "*.vsix" }], ], }; From 230405e7bd7606bf3237db8cd3a2a14443495580 Mon Sep 17 00:00:00 2001 From: Jose V Sebastian Date: Sun, 18 Feb 2024 18:36:16 +0530 Subject: [PATCH 4/4] feat: register command to clear cache closes #8 --- README.md | 7 +++++++ package.json | 10 ++++++++++ src/Cache.ts | 37 ++++++++++++++++++++++++++++++------- src/Executable.ts | 7 +++++++ src/extension.ts | 2 ++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5271ebc..7a2bdc7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ This extension contributes the following settings: Please refer [Google Java Format repository](https://github.com/google/google-java-format) for available versions and CLI arguments. +## Extension Commands + +This extension contributes the following commands: + +* `Google Java Format For VS Code: Clear Cache`: Clear cache of [Google Java Format executable](https://github.com/google/google-java-format/releases) downloads by the extension. +* `Google Java Format For VS Code: Reload Executable`: Reload the [Google Java Format executable](https://github.com/google/google-java-format/releases) using the current configuration. + ## How to Debug To debug this extension and see how exactly it invokes the formatter, use *Developer: Set Log Level...* to enable *Debug* for this extension, and then open the *Output* tab and select this extension. diff --git a/package.json b/package.json index ea7b830..33c867f 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,16 @@ } } } + ], + "commands": [ + { + "command": "googleJavaFormatForVSCode.reloadExecutable", + "title": "Google Java Format For VS Code: Reload Executable" + }, + { + "command": "googleJavaFormatForVSCode.clearCache", + "title": "Google Java Format For VS Code: Clear Cache" + } ] }, "scripts": { diff --git a/src/Cache.ts b/src/Cache.ts index ba660d9..a6e8bab 100644 --- a/src/Cache.ts +++ b/src/Cache.ts @@ -1,11 +1,18 @@ import { createHash } from "node:crypto"; import path = require("node:path"); -import { ExtensionContext, LogOutputChannel, Uri, workspace } from "vscode"; +import { + ExtensionContext, + LogOutputChannel, + Uri, + commands, + workspace, +} from "vscode"; export class Cache { private uri: Uri; + private constructor( - context: ExtensionContext, + private context: ExtensionContext, private log: LogOutputChannel, cacheFolder: string, ) { @@ -19,10 +26,28 @@ export class Cache { ) { const cache = new Cache(context, log, cacheFolder); await cache.init(); - // TODO: clear old cache return cache; } + subscribe = () => { + this.context.subscriptions.push( + commands.registerCommand( + "googleJavaFormatForVSCode.clearCache", + this.clear, + ), + ); + }; + + clear = async () => { + // clear cache + await workspace.fs.delete(this.uri, { recursive: true }); + this.log.info("Cache cleared."); + // reload executable after clearing cache + await commands.executeCommand( + "googleJavaFormatForVSCode.reloadExecutable", + ); + }; + private init = async () => { // Create the cache directory if it doesn't exist try { @@ -37,10 +62,8 @@ export class Cache { get = async (url: string) => { const basename = path.basename(url); - const dirname = Uri.joinPath( - this.uri, - createHash("md5").update(url).digest("hex"), - ); + const hash = createHash("md5").update(url).digest("hex"); + const dirname = Uri.joinPath(this.uri, hash); const localPath = Uri.joinPath(dirname, basename); try { diff --git a/src/Executable.ts b/src/Executable.ts index 9acc4be..4104e1b 100644 --- a/src/Executable.ts +++ b/src/Executable.ts @@ -5,6 +5,7 @@ import { LogOutputChannel, // Progress, ProgressLocation, + commands, window, } from "vscode"; import { Cache } from "./Cache"; @@ -65,6 +66,12 @@ export class Executable { subscribe = () => { this.config.subscriptions.push(this.configurationChangeListener); + this.context.subscriptions.push( + commands.registerCommand( + "googleJavaFormatForVSCode.reloadExecutable", + this.load, + ), + ); }; private load = async () => diff --git a/src/extension.ts b/src/extension.ts index 3daf622..f6bed23 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,8 @@ export async function activate(context: ExtensionContext) { config.subscribe(); const cache = await Cache.getInstance(context, log); + cache.subscribe(); + const executable = await Executable.getInstance( context, config,