diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d71419..d70966d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Added the "Rebuild code search index" which instructs Lexical to rebuild the code search index for the current project. +- Added a notification after automatic installation of a new version of Lexical. + This notification can be disabled through the ǹew + `lexical.notifyOnServerAutoUpdate` configuration setting. ## [0.0.15] diff --git a/README.md b/README.md index b01408c..86b4b7c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,11 @@ script (assumed to be `start_lexical.sh`) or any executable launcher script. The path should look something like `/home/username/Projects/lexical/_build/dev/package/lexical/bin/start_lexical.sh`. +### lexical.notifyOnServerAutoUpdate + +Controls whether notifications are shown after automatic installs of new Lexcial +versions. Defaults to `true`. + ### Erlang and Elixir version compatibility #### Erlang diff --git a/package-lock.json b/package-lock.json index 470fe73..4233557 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "jest-extended": "4.0.2", "ovsx": "0.8.3", "prettier": "3.2.5", + "synchronous-promise": "^2.0.17", "ts-jest": "29.1.2", "typescript": "5.4.5", "vscode-uri": "3.0.8" @@ -6607,6 +6608,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synchronous-promise": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", + "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", + "dev": true + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", diff --git a/package.json b/package.json index ac09f03..11cd448 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,12 @@ "scope": "window", "type": "string", "markdownDescription": "A subdirectory of the current workspace in which to start Lexical." + }, + "lexical.notifyOnServerAutoUpdate": { + "scope": "window", + "type": "boolean", + "default": true, + "markdownDescription": "Whether to notify when a new release of Lexical is installed automatically." } } }, @@ -184,6 +190,7 @@ "jest-extended": "4.0.2", "ovsx": "0.8.3", "prettier": "3.2.5", + "synchronous-promise": "2.0.17", "ts-jest": "29.1.2", "typescript": "5.4.5", "vscode-uri": "3.0.8" diff --git a/src/auto-installer.ts b/src/auto-installer.ts index c953bcf..853cbf5 100644 --- a/src/auto-installer.ts +++ b/src/auto-installer.ts @@ -6,6 +6,8 @@ import * as fs from "fs"; import Zip from "./zip"; import Paths from "./paths"; import Logger from "./logger"; +import Notifications from "./notifications"; +import Configuration from "./configuration"; namespace AutoInstaller { export function isInstalledReleaseLatest( @@ -41,6 +43,10 @@ namespace AutoInstaller { fs.writeFileSync(zipUri.fsPath, zipBuffer, "binary"); await Zip.extract(zipUri, releaseUri, latestRelease.version); + + if (Configuration.getAutoInstallUpdateNotification()) { + Notifications.notifyAutoInstallSuccess(latestRelease.version); + } } } diff --git a/src/configuration.ts b/src/configuration.ts index 147a617..33b9fdb 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,6 +1,7 @@ import path = require("path"); -import type { workspace as vsWorkspace } from "vscode"; +import { ConfigurationTarget, workspace as vsWorkspace } from "vscode"; import { URI } from "vscode-uri"; +import Logger from "./logger"; namespace Configuration { type GetConfig = (section: string) => unknown; @@ -27,6 +28,19 @@ namespace Configuration { return URI.file(workspacePath); } } + + export function disableAutoInstallUpdateNotification(): void { + vsWorkspace + .getConfiguration("lexical") + .update("notifyOnServerAutoUpdate", false, ConfigurationTarget.Global) + .then(undefined, (e) => Logger.error(e.toString())); + } + + export function getAutoInstallUpdateNotification(): boolean { + return vsWorkspace + .getConfiguration("lexical") + .get("notifyOnServerAutoUpdate", true); + } } export default Configuration; diff --git a/src/notifications.ts b/src/notifications.ts new file mode 100644 index 0000000..70e3294 --- /dev/null +++ b/src/notifications.ts @@ -0,0 +1,22 @@ +import { window } from "vscode"; +import ReleaseVersion from "./release/version"; +import Configuration from "./configuration"; + +namespace Notifications { + export function notifyAutoInstallSuccess(version: ReleaseVersion.T): void { + const disableNotificationMessage = "Disable this notification"; + const serializedVersion = ReleaseVersion.serialize(version); + const releaseUrl = `https://github.com/lexical-lsp/lexical/releases/tag/v${serializedVersion}`; + const message = `Lexical was automatically updated to version ${serializedVersion}. See [what's new](${releaseUrl}).`; + + window + .showInformationMessage(message, disableNotificationMessage) + .then((fulfilledValue) => { + if (fulfilledValue === disableNotificationMessage) { + Configuration.disableAutoInstallUpdateNotification(); + } + }); + } +} + +export default Notifications; diff --git a/src/test/auto-installer.test.ts b/src/test/auto-installer.test.ts index c404f97..03ad3e6 100644 --- a/src/test/auto-installer.test.ts +++ b/src/test/auto-installer.test.ts @@ -10,6 +10,8 @@ import * as fs from "fs"; import * as os from "os"; import Zip from "../zip"; import Release from "../release"; +import Notifications from "../notifications"; +import Configuration from "../configuration"; jest.mock("fs", () => { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -26,6 +28,8 @@ jest.mock("fs", () => { describe("AutoInstaller", () => { beforeEach(() => { mockResolvedValue(Zip, "extract"); + mockReturnValue(Notifications, "notifyAutoInstallSuccess"); + mockReturnValue(Configuration, "getAutoInstallUpdateNotification", false); }); describe("isInstalledReleaseLatest", () => { @@ -110,6 +114,26 @@ describe("AutoInstaller", () => { A_RELEASE.version, ); }); + + test("notifies the user of the newly installed version", async () => { + mockReturnValue(Configuration, "getAutoInstallUpdateNotification", true); + mockReturnValue(Notifications, "notifyAutoInstallSuccess"); + givenDownloadedZip(); + await AutoInstaller.install(A_PROGRESS, A_RELEASE, A_RELEASE_URI); + + expect(Notifications.notifyAutoInstallSuccess).toHaveBeenCalledWith( + ReleaseVersion.deserialize(ReleaseVersion.serialize(A_RELEASE.version)), + ); + }); + + test("given auto-install notifications are disabled, it does not notify user of the newly installed version", async () => { + mockReturnValue(Configuration, "getAutoInstallUpdateNotification", false); + mockReturnValue(Notifications, "notifyAutoInstallSuccess"); + givenDownloadedZip(); + await AutoInstaller.install(A_PROGRESS, A_RELEASE, A_RELEASE_URI); + + expect(Notifications.notifyAutoInstallSuccess).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/test/configuration.test.ts b/src/test/configuration.test.ts index b7abaf4..6eb32bc 100644 --- a/src/test/configuration.test.ts +++ b/src/test/configuration.test.ts @@ -12,7 +12,6 @@ describe("Configuration", () => { workspace, ); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(projectDirUri).toEqual(workspace.workspaceFolders![0].uri); }); @@ -24,7 +23,6 @@ describe("Configuration", () => { workspace, ); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(projectDirUri).toEqual(URI.file("/stub/subdirectory")); }); }); diff --git a/src/test/notifications.test.ts b/src/test/notifications.test.ts new file mode 100644 index 0000000..8cff066 --- /dev/null +++ b/src/test/notifications.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "@jest/globals"; +import { mockReturnValue } from "./utils/strict-mocks"; +import { MessageItem, window } from "vscode"; +import { SynchronousPromise } from "synchronous-promise"; +import Notifications from "../notifications"; +import ReleaseVersion from "../release/version"; +import Configuration from "../configuration"; + +describe("notifyAutoInstallSuccess", () => { + test("sends an information message with the installed version", () => { + mockReturnValue( + window, + "showInformationMessage", + Promise.resolve(undefined), + ); + + Notifications.notifyAutoInstallSuccess(ReleaseVersion.deserialize("1.2.3")); + + expect(window.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("version 1.2.3"), + "Disable this notification", + ); + }); + + test("when user requests to disable the notification, it updates the configuration", () => { + mockReturnValue(Configuration, "disableAutoInstallUpdateNotification"); + mockReturnValue( + window, + "showInformationMessage", + SynchronousPromise.resolve( + "Disable this notification" as unknown as MessageItem, + ), + ); + + Notifications.notifyAutoInstallSuccess(ReleaseVersion.deserialize("1.2.3")); + + expect( + Configuration.disableAutoInstallUpdateNotification, + ).toHaveBeenCalled(); + }); + + test("notification should include a link to the release notes", () => { + mockReturnValue( + window, + "showInformationMessage", + Promise.resolve(undefined), + ); + + Notifications.notifyAutoInstallSuccess(ReleaseVersion.deserialize("1.2.3")); + + expect(window.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining( + "https://github.com/lexical-lsp/lexical/releases/tag/v1.2.3", + ), + "Disable this notification", + ); + }); +});