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/README.md b/README.md index 8e15378..7a2bdc7 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,19 @@ 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. +## 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 5cc497f..33c867f 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,26 @@ }, "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.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": { @@ -40,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/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" }], ], }; diff --git a/src/Cache.ts b/src/Cache.ts new file mode 100644 index 0000000..a6e8bab --- /dev/null +++ b/src/Cache.ts @@ -0,0 +1,106 @@ +import { createHash } from "node:crypto"; +import path = require("node:path"); +import { + ExtensionContext, + LogOutputChannel, + Uri, + commands, + workspace, +} from "vscode"; + +export class Cache { + private uri: Uri; + + private constructor( + private 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(); + 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 { + 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; + } + }; + + get = async (url: string) => { + const basename = path.basename(url); + + const hash = createHash("md5").update(url).digest("hex"); + const dirname = Uri.joinPath(this.uri, hash); + 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 new file mode 100644 index 0000000..4104e1b --- /dev/null +++ b/src/Executable.ts @@ -0,0 +1,132 @@ +import { execSync } from "node:child_process"; +import { + // CancellationToken, + ExtensionContext, + LogOutputChannel, + // Progress, + ProgressLocation, + commands, + window, +} from "vscode"; +import { Cache } from "./Cache"; +import { ExtensionConfiguration } from "./ExtensionConfiguration"; +import { resolveExecutableFileFromConfig } from "./resolveExecutableFileFromConfig"; +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); + this.context.subscriptions.push( + commands.registerCommand( + "googleJavaFormatForVSCode.reloadExecutable", + this.load, + ), + ); + }; + + private load = async () => + // progress?: Progress<{ + // message?: string | undefined; + // increment?: number | undefined; + // }>, + // token?: CancellationToken, + { + 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); + + if (isJar) { + this.runner = `java -jar ./${basename}`; + } else if (process.platform === "win32") { + this.runner = basename; + } else { + this.runner = `./${basename}`; + } + + 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..2824b0a 100644 --- a/src/ExtensionConfiguration.ts +++ b/src/ExtensionConfiguration.ts @@ -1,36 +1,55 @@ -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}` | "latest"; +export type GoogleJavaFormatMode = "jar-file" | "native-binary"; // | "background-service" + export interface GoogleJavaFormatConfiguration { executable?: string; version?: GoogleJavaFormatVersion; + mode?: GoogleJavaFormatMode; 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; + mode?: GoogleJavaFormatMode; + 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/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/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/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/extension.ts b/src/extension.ts index c105654..f6bed23 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,28 @@ 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 cache = await Cache.getInstance(context, log); + cache.subscribe(); - 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 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 deleted file mode 100644 index 81e273a..0000000 --- a/src/getJarLocalPathFromConfig.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { LogOutputChannel, Uri } from "vscode"; -import { - downloadGoogleJavaFormatJar, - downloadGoogleJavaFormatJarByVersion, -} from "./downloadGoogleJavaFormatJar"; -import { GoogleJavaFormatConfiguration } from "./ExtensionConfiguration"; -import { getListOfGoogleJavaFormatVersions } from "./getListOfGoogleJavaFormatVersions"; -import { getUriFromString } from "./utils"; - -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/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/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); +} 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); -}