From d66b766dc7ca81ca85ce7e2fbe19b0ad0baf0b3b Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Thu, 9 Jun 2022 23:10:50 +0200 Subject: [PATCH] feature: show Metals' release notes directly in vscode --- package.json | 4 + src/extension.ts | 4 +- src/releaseNotesProvider.ts | 180 ++++++++++++++++++++++++++++++++++++ yarn.lock | 55 +++++++++++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/releaseNotesProvider.ts diff --git a/package.json b/package.json index e182ab0e5..4dc311099 100644 --- a/package.json +++ b/package.json @@ -890,6 +890,8 @@ "@types/glob": "^7.2.0", "@types/mocha": "^9.1.1", "@types/node": "17.0.27", + "@types/remarkable": "^2.0.3", + "@types/semver": "^7.3.9", "@types/vscode": "1.59.0", "@typescript-eslint/eslint-plugin": "^5.27.0", "@typescript-eslint/parser": "^5.27.0", @@ -902,6 +904,7 @@ "mocha": "^10.0.0", "ovsx": "0.3.0", "prettier": "2.6.2", + "remarkable": "^2.0.1", "rimraf": "^3.0.2", "ts-mocha": "^10.0.0", "typescript": "4.7.3", @@ -911,6 +914,7 @@ "ansicolor": "^1.1.100", "metals-languageclient": "0.5.15", "promisify-child-process": "4.1.1", + "semver": "^7.3.7", "vscode-languageclient": "7.0.0" }, "extensionPack": [ diff --git a/src/extension.ts b/src/extension.ts index 3d75cb906..3a29856c8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -90,6 +90,7 @@ import * as workbenchCommands from "./workbenchCommands"; import { getServerVersion } from "./getServerVersion"; import { getCoursierMirrorPath } from "./mirrors"; import { DoctorProvider } from "./doctor"; +import { showReleaseNotesIfNeeded } from "./releaseNotesProvider"; const outputChannel = window.createOutputChannel("Metals"); const openSettingsAction = "Open settings"; @@ -130,7 +131,8 @@ export async function activate(context: ExtensionContext): Promise { commands.executeCommand("setContext", "metals:enabled", true); try { const javaHome = await getJavaHome(getJavaHomeFromConfig()); - return fetchAndLaunchMetals(context, javaHome, serverVersion); + await fetchAndLaunchMetals(context, javaHome, serverVersion); + await showReleaseNotesIfNeeded(context, serverVersion, outputChannel); } catch (err) { outputChannel.appendLine(`${err}`); showMissingJavaMessage(); diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts new file mode 100644 index 000000000..5f8bd75e0 --- /dev/null +++ b/src/releaseNotesProvider.ts @@ -0,0 +1,180 @@ +import { env, ExtensionContext } from "vscode"; +import * as vscode from "vscode"; +import * as semver from "semver"; +import { Remarkable } from "remarkable"; +import http from "https"; + +const versionKey = "metals-server-version"; + +// prettier-ignore +const releaseNotesMarkdownUrl: Record = { + "0.11.5": "https://raw.githubusercontent.com/scalameta/metals/main/website/blog/2022-04-28-aluminium.md", + "0.11.6": "https://raw.githubusercontent.com/scalameta/metals/main/website/blog/2022-06-03-aluminium.md", +}; + +/** + * Show release notes if possible, swallow errors since its not a crucial feature. + * Treats snapshot versions like 0.11.6+67-926ec9a3-SNAPSHOT as a 0.11.6. + */ +export async function showReleaseNotesIfNeeded( + context: ExtensionContext, + serverVersion: string, + outputChannel: vscode.OutputChannel +) { + try { + await showReleaseNotes(context, serverVersion); + } catch (error) { + outputChannel.appendLine( + `Couldn't show release notes for Metals ${serverVersion}` + ); + outputChannel.appendLine(`${error}`); + } +} + +async function showReleaseNotes( + context: ExtensionContext, + currentVersion: string +): Promise { + const state = context.globalState; + + const version = getVersion(); + const releaseNotesUrl = version + ? releaseNotesMarkdownUrl[version] + : undefined; + if (isNotRemote() && version && releaseNotesUrl) { + await showPanel(version, releaseNotesUrl); + } + + async function showPanel(version: string, releaseNotesUrl: string) { + const releaseNotes = await getReleaseNotesMarkdown(releaseNotesUrl); + + if (typeof releaseNotes === "string") { + const panel = vscode.window.createWebviewPanel( + `scalameta.metals.whatsNew`, + `Metals ${version} release notes`, + vscode.ViewColumn.One + ); + + panel.webview.html = releaseNotes; + panel.reveal(); + + // set wrong value for local development + // const newVersion = "0.11.5"; + + const newVersion = version; + + // Update current device's latest server version when there's no value or it was a older one. + // Then sync this value across other devices. + state.update(versionKey, newVersion); + state.setKeysForSync([versionKey]); + + context.subscriptions.push(panel); + } + } + + /** + * Don't show panel for remote environment because it installs extension on every time. + * TODO: what about wsl? + */ + function isNotRemote(): boolean { + const isNotRemote = env.remoteName == null; + // const isWsl = env.remoteName === "wsl"; + return isNotRemote; + } + + /** + * Return version for which release notes should be displayed + * or + * undefined if notes shouldn't be displayed + */ + function getVersion(): string | undefined { + const previousVersion: string | undefined = state.get(versionKey); + // strip version to 'major.minor.patch' + const cleanVersion = semver.clean(currentVersion); + if (cleanVersion) { + if (previousVersion) { + const compare = semver.compare(cleanVersion, previousVersion); + const diff = semver.diff(cleanVersion, previousVersion); + + // take into account only major, minor and patch, ignore snapshot releases + const isNewerVersion = + compare === 1 && + (diff === "major" || diff === "minor" || diff === "patch"); + + return isNewerVersion ? cleanVersion : undefined; + } + + // if there was no previous version then show release notes for current cleaned version + return currentVersion; + } + } +} + +async function getReleaseNotesMarkdown( + releaseNotesUrl: string +): Promise { + const ps = new Promise((resolve, reject) => { + http.get(releaseNotesUrl, (resp) => { + let body = ""; + resp.on("data", (chunk) => (body += chunk)); + resp.on("end", () => resolve(body)); + resp.on("error", (e) => reject(e)); + }); + }); + + const text = await ps; + // every release notes starts with that + const beginning = "We're happy to announce the release of"; + const notesStartIdx = text.indexOf(beginning); + const releaseNotes = text.substring(notesStartIdx); + + // cut metadata yaml from release notes, it start with --- and ends with --- + const metadata = text + .substring(0, notesStartIdx - 1) + .replace("---", "") + .replace("---", "") + .trim() + .split("\n"); + const author = metadata[0].slice("author: ".length); + const title = metadata[1].slice("title: ".length); + const authorUrl = metadata[2].slice("authorURL: ".length); + + const md = new Remarkable({ html: true }); + const renderedNotes = md.render(releaseNotes); + + const notes = getHtmlContent(renderedNotes, author, title, authorUrl); + return notes; +} + +function getHtmlContent( + renderedNotes: string, + author: string, + title: string, + authorURL: string +): string { + return ` + + + + + + + +

${title}

+
+

+ Showing Metals' release notes embedded in vscode is an experimental feature, in case of any issues report them at + https://github.com/scalameta/metals-vscode. +

+
+

+ +

+
+ ${renderedNotes} + + +`; +} diff --git a/yarn.lock b/yarn.lock index e68c0ffaa..a5a61a8ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -65,6 +65,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/highlight.js@^9.7.0": + version "9.12.4" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.4.tgz#8c3496bd1b50cc04aeefd691140aa571d4dbfa34" + integrity sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -90,6 +95,19 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.27.tgz#f4df3981ae8268c066e8f49995639f855469081e" integrity sha512-4/Ke7bbWOasuT3kceBZFGakP1dYN2XFd8v2l9bqF2LNWrmeU07JLpp56aEeG6+Q3olqO5TvXpW0yaiYnZJ5CXg== +"@types/remarkable@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/remarkable/-/remarkable-2.0.3.tgz#ba2e5edada8f0fe64c658beb2127e06ac0b9c1c8" + integrity sha512-QQUBeYApuHCNl9Br6ZoI3PlKmwZ69JHrlJktJXnjxobia9liZgsI70fm8PnCqVFAcefYK+9PGzR5L/hzCslNYQ== + dependencies: + "@types/highlight.js" "^9.7.0" + highlight.js "^9.7.0" + +"@types/semver@^7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + "@types/vscode@1.59.0": version "1.59.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.59.0.tgz#11c93f5016926126bf30b47b9ece3bd617eeef31" @@ -280,6 +298,13 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +argparse@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -300,6 +325,13 @@ async@^3.2.2: resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== +autolinker@^3.11.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-3.15.0.tgz#03956088648f236642a5783612f9ca16adbbed38" + integrity sha512-N/5Dk5AZnqL9k6kkHdFIGLm/0/rRuSnJwqYYhLCJjU7ZtiaJwCBzNTvjzy1zzJADngv/wvtHYcrPHytPnASeFA== + dependencies: + tslib "^2.3.0" + azure-devops-node-api@^11.0.1: version "11.1.0" resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-11.1.0.tgz#ea3ca49de8583b0366d000f3c3f8a75b8104055f" @@ -1102,6 +1134,11 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +highlight.js@^9.7.0: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== + hosted-git-info@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" @@ -1758,6 +1795,14 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +remarkable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-2.0.1.tgz#280ae6627384dfb13d98ee3995627ca550a12f31" + integrity sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA== + dependencies: + argparse "^1.0.10" + autolinker "^3.11.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -1901,6 +1946,11 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2071,6 +2121,11 @@ tslib@^2.2.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"