// // Language support for Dockerfiles and Compose files, backed by // docker-language-server (github.com/docker/docker-language-server). // // The binary is a single ~40 MB executable, which is too large to ship inside // the extension, so the managed mode downloads the pinned release into the // extension's global storage the first time it is needed. // const { exec } = require("../Utilities/Process"); const notify = require("../Utilities/Notify"); const RELEASE_URL = "https://github.com/docker/docker-language-server/releases/download"; // docker-language-server decides what a document is from the languageId the // editor sends, and Nova sends the syntax name. Compose support only switches // on for "dockercompose", which is why the Compose syntax carries that name. const SYNTAXES = ["dockerfile", "dockercompose"]; class DockerLanguageClient { constructor() { this.client = null; this.disposables = new CompositeDisposable(); for (let key of ["apple_container.lsp.mode", "apple_container.lsp.path", "apple_container.lsp.version"]) { this.disposables.add(nova.config.onDidChange(key, () => this.restart())); } } get mode() { return nova.config.get("apple_container.lsp.mode", "string") || "Managed Binary"; } get version() { return (nova.config.get("apple_container.lsp.version", "string") || "").trim() || "v0.20.1"; } async start() { if (this.mode === "Disabled") { return; } let path = null; try { path = this.mode === "Custom Path" ? this.customPath() : await this.managedPath(); } catch (error) { notify.configFailure("Docker Language Server unavailable", error, "apple-container-lsp"); return; } if (!path) { return; } this.stop(); try { this.client = new LanguageClient( "unsupervised.AppleContainer.dls", "Docker Language Server", { path: path, args: ["start", "--stdio"], type: "stdio" }, { syntaxes: SYNTAXES } ); this.client.onDidStop((error) => { if (error) { notify.failure( "Docker Language Server stopped", error, "apple-container-lsp-stopped" ); } }); this.client.start(); nova.subscriptions.add(this.client); } catch (error) { notify.failure("Docker Language Server failed to launch", error, "apple-container-lsp"); this.client = null; } } stop() { if (!this.client) { return; } this.client.stop(); nova.subscriptions.remove(this.client); this.client = null; } async restart() { this.stop(); await this.start(); } dispose() { this.stop(); this.disposables.dispose(); } customPath() { let path = (nova.config.get("apple_container.lsp.path", "string") || "").trim(); if (!path) { throw new Error("Set the path to docker-language-server in the extension's preferences."); } if (!nova.fs.access(path, nova.fs.X_OK)) { throw new Error(`${path} is not an executable file.`); } return path; } // Returns the managed binary, downloading it when it is missing. async managedPath() { let architecture = await this.architecture(); let name = `docker-language-server-darwin-${architecture}-${this.version}`; let directory = nova.extension.globalStoragePath; let path = nova.path.join(directory, name); if (nova.fs.access(path, nova.fs.X_OK)) { return path; } if (!nova.fs.access(directory, nova.fs.F_OK)) { nova.fs.mkdir(directory); } await this.download(`${RELEASE_URL}/${this.version}/${name}`, path); return path; } async architecture() { let result = await exec("/usr/bin/uname", ["-m"]); return result.stdout.trim() === "x86_64" ? "amd64" : "arm64"; } async download(url, destination) { notify.info( "Downloading Docker Language Server", `${this.version} for Dockerfile and Compose support. This happens once.`, "apple-container-lsp-download" ); // A partial file would look like a working binary on the next launch, // so download beside the target and move it into place at the end. let temporary = `${destination}.download`; try { await exec("/usr/bin/curl", [ "--fail", "--location", "--silent", "--show-error", "--output", temporary, url ]); await exec("/bin/chmod", ["+x", temporary]); nova.fs.move(temporary, destination); } catch (error) { try { nova.fs.remove(temporary); } catch (cleanupError) { // Nothing to clean up. } throw new Error(`Could not download ${url}: ${error.message}`); } } } module.exports = { DockerLanguageClient };