From d66b766dc7ca81ca85ce7e2fbe19b0ad0baf0b3b Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Thu, 9 Jun 2022 23:10:50 +0200 Subject: [PATCH 01/10] 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" From 2157557fbe27b0ef42087f234b598758ba790cd5 Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sat, 11 Jun 2022 00:06:52 +0200 Subject: [PATCH 02/10] feat: add styles, retrieve release notes automatically --- media/styles.css | 8 +++ src/releaseNotesProvider.ts | 118 ++++++++++++++++++++++-------------- src/util.ts | 16 +++++ 3 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 media/styles.css diff --git a/media/styles.css b/media/styles.css new file mode 100644 index 000000000..fd1ac6829 --- /dev/null +++ b/media/styles.css @@ -0,0 +1,8 @@ +h2, +h3, +h4, +h5, +h6 { + margin-top: 2em; + margin-bottom: 0em; +} diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index 5f8bd75e0..ab49381f9 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -2,16 +2,10 @@ import { env, ExtensionContext } from "vscode"; import * as vscode from "vscode"; import * as semver from "semver"; import { Remarkable } from "remarkable"; -import http from "https"; +import { fetchFrom } from "./util"; 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. @@ -22,6 +16,7 @@ export async function showReleaseNotesIfNeeded( outputChannel: vscode.OutputChannel ) { try { + context.globalState.update(versionKey, "0.11.5"); await showReleaseNotes(context, serverVersion); } catch (error) { outputChannel.appendLine( @@ -38,38 +33,35 @@ async function showReleaseNotes( const state = context.globalState; const version = getVersion(); - const releaseNotesUrl = version - ? releaseNotesMarkdownUrl[version] - : undefined; - if (isNotRemote() && version && releaseNotesUrl) { - await showPanel(version, releaseNotesUrl); + if (isNotRemote() && version) { + const releaseNotesUrl = await getMarkdownLink(version); + if (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(); + const panel = vscode.window.createWebviewPanel( + `scalameta.metals.whatsNew`, + `Metals ${version} release notes`, + vscode.ViewColumn.One + ); - // set wrong value for local development - // const newVersion = "0.11.5"; + const releaseNotes = await getReleaseNotesMarkdown( + releaseNotesUrl, + context.extensionUri, + (uri) => panel.webview.asWebviewUri(uri) + ); - const newVersion = version; + panel.webview.html = releaseNotes; + panel.reveal(); - // 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]); + // 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, version); + state.setKeysForSync([versionKey]); - context.subscriptions.push(panel); - } + context.subscriptions.push(panel); } /** @@ -110,19 +102,36 @@ async function showReleaseNotes( } } +/** + * Translate server version to link to the markdown file with release notes + */ +async function getMarkdownLink(version: string): Promise { + const releaseInfoUrl = `https://api.github.com/repos/scalameta/metals/releases/tags/v${version}`; + const options = { + headers: { + "User-Agent": "metals", + }, + }; + const info = await fetchFrom(releaseInfoUrl, options); + const body = (JSON.parse(info)["body"] as string) ?? ""; + // matches (2022)/(06)/(03)/(aluminium) + const matchResult = body.match( + new RegExp("(\\d\\d\\d\\d)/(\\d\\d)/(\\d\\d)/(\\w+)") + ); + if (matchResult?.length === 5) { + const [, ...tail] = matchResult; + const name = tail.join("-"); + const url = `https://raw.githubusercontent.com/scalameta/metals/main/website/blog/${name}.md`; + return url; + } +} + 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; + releaseNotesUrl: string, + extensionUri: vscode.Uri, + cssUriConverter: (_: vscode.Uri) => vscode.Uri +): Promise { + const text = await fetchFrom(releaseNotesUrl); // every release notes starts with that const beginning = "We're happy to announce the release of"; const notesStartIdx = text.indexOf(beginning); @@ -142,7 +151,22 @@ async function getReleaseNotesMarkdown( const md = new Remarkable({ html: true }); const renderedNotes = md.render(releaseNotes); - const notes = getHtmlContent(renderedNotes, author, title, authorUrl); + const stylesPathMainPath = vscode.Uri.joinPath( + extensionUri, + "media", + "styles.css" + ); + + // // Uri to load styles into webview + const stylesMainUri = cssUriConverter(stylesPathMainPath); + + const notes = getHtmlContent( + renderedNotes, + author, + title, + authorUrl, + stylesMainUri + ); return notes; } @@ -150,7 +174,8 @@ function getHtmlContent( renderedNotes: string, author: string, title: string, - authorURL: string + authorURL: string, + stylesUri: vscode.Uri ): string { return ` @@ -158,6 +183,7 @@ function getHtmlContent( +

${title}

diff --git a/src/util.ts b/src/util.ts index 8358d73d9..0a7bc2e93 100644 --- a/src/util.ts +++ b/src/util.ts @@ -11,6 +11,7 @@ import { TextDocumentPositionParams, } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; +import http from "https"; declare const sym: unique symbol; /** @@ -103,3 +104,18 @@ export function getJavaHomeFromConfig(): string | undefined { return javaHomePath; } } + +export async function fetchFrom( + url: string, + options?: http.RequestOptions +): Promise { + const promise = new Promise((resolve, reject) => { + http.get(url, options || {}, (resp) => { + let body = ""; + resp.on("data", (chunk) => (body += chunk)); + resp.on("end", () => resolve(body)); + resp.on("error", (e) => reject(e)); + }); + }); + return await promise; +} From e4500f9f05e9b53cca28867e26c4640b749bea41 Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sat, 11 Jun 2022 00:45:40 +0200 Subject: [PATCH 03/10] better error handling --- src/releaseNotesProvider.ts | 83 ++++++++++++++++++++++++------------- src/types.ts | 11 +++++ 2 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 src/types.ts diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index ab49381f9..ca4409d3e 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode"; import * as semver from "semver"; import { Remarkable } from "remarkable"; import { fetchFrom } from "./util"; +import { Either, makeLeft, makeRight } from "./types"; const versionKey = "metals-server-version"; @@ -16,11 +17,15 @@ export async function showReleaseNotesIfNeeded( outputChannel: vscode.OutputChannel ) { try { - context.globalState.update(versionKey, "0.11.5"); - await showReleaseNotes(context, serverVersion); + // context.globalState.update(versionKey, "0.11.5"); + const result = await showReleaseNotes(context, serverVersion); + if (result.kind === "left") { + const msg = `Release notes was not shown: ${result.value}`; + outputChannel.appendLine(msg); + } } catch (error) { outputChannel.appendLine( - `Couldn't show release notes for Metals ${serverVersion}` + `Error, couldn't show release notes for Metals ${serverVersion}` ); outputChannel.appendLine(`${error}`); } @@ -29,17 +34,30 @@ export async function showReleaseNotesIfNeeded( async function showReleaseNotes( context: ExtensionContext, currentVersion: string -): Promise { +): Promise> { const state = context.globalState; + if (isRemote()) { + const msg = "is remote environment"; + return makeLeft(msg); + } + const version = getVersion(); - if (isNotRemote() && version) { - const releaseNotesUrl = await getMarkdownLink(version); - if (releaseNotesUrl) { - await showPanel(version, releaseNotesUrl); - } + + if (version.kind === "left") { + return version; + } + + const releaseNotesUrl = await getMarkdownLink(version.value); + if (!releaseNotesUrl) { + const msg = `can't obtain release notes' url for ${version.value}`; + return makeLeft(msg); } + // actual logic starts here + await showPanel(version.value, releaseNotesUrl); + return makeRight(undefined); + async function showPanel(version: string, releaseNotesUrl: string) { const panel = vscode.window.createWebviewPanel( `scalameta.metals.whatsNew`, @@ -68,42 +86,49 @@ async function showReleaseNotes( * 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; + function isRemote(): boolean { + const isRemote = env.remoteName != null; // const isWsl = env.remoteName === "wsl"; - return isNotRemote; + return isRemote; } /** * Return version for which release notes should be displayed - * or - * undefined if notes shouldn't be displayed */ - function getVersion(): string | undefined { + function getVersion(): Either { const previousVersion: string | undefined = state.get(versionKey); - // strip version to 'major.minor.patch' + // strip version to + // in theory semver.clean can return null, but we're almost sure that currentVersion is well defined 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 (cleanVersion == null) { + const msg = `can't transform ${currentVersion} to 'major.minor.patch'`; + return makeLeft(msg); + } + if (!previousVersion) { // if there was no previous version then show release notes for current cleaned version - return currentVersion; + return makeRight(currentVersion); } + + 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 + ? makeRight(cleanVersion) + : makeLeft("do not show release notes for an older version"); } } /** - * Translate server version to link to the markdown file with release notes + * Translate server version to link to the markdown file with release notes. + * @param version clean version in major.minor.patch form + * If version has release notes return link to them, if not return nothing. */ async function getMarkdownLink(version: string): Promise { const releaseInfoUrl = `https://api.github.com/repos/scalameta/metals/releases/tags/v${version}`; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..ff4a72b76 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,11 @@ +export type Either = + | { kind: "left"; value: Left } + | { kind: "right"; value: Right }; + +export function makeLeft(t: T): Either { + return { kind: "left", value: t }; +} + +export function makeRight(t: T): Either { + return { kind: "right", value: t }; +} From bf4944089761f9e5ba6d34dedaa2f3bde57f250a Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sat, 11 Jun 2022 09:40:31 +0200 Subject: [PATCH 04/10] add more docs, tidy things up --- .eslintrc.js | 5 +- src/releaseNotesProvider.ts | 95 +++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2dffd1f23..cbd5bb85f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,10 @@ module.exports = { { ignoreRestArgs: true }, ], "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/no-unused-vars": [ + "error", + { varsIgnorePattern: "_" }, + ], "@typescript-eslint/no-non-null-assertion": "error", "guard-for-in": "error", "no-var": "error", diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index ca4409d3e..527b4c813 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -37,27 +37,27 @@ async function showReleaseNotes( ): Promise> { const state = context.globalState; - if (isRemote()) { - const msg = "is remote environment"; - return makeLeft(msg); + const remote = isRemote(); + if (remote.kind === "left") { + return remote; } const version = getVersion(); - if (version.kind === "left") { return version; } const releaseNotesUrl = await getMarkdownLink(version.value); - if (!releaseNotesUrl) { - const msg = `can't obtain release notes' url for ${version.value}`; - return makeLeft(msg); + if (releaseNotesUrl.kind === "left") { + return releaseNotesUrl; } // actual logic starts here - await showPanel(version.value, releaseNotesUrl); + await showPanel(version.value, releaseNotesUrl.value); return makeRight(undefined); + // below are helper functions + async function showPanel(version: string, releaseNotesUrl: string) { const panel = vscode.window.createWebviewPanel( `scalameta.metals.whatsNew`, @@ -67,7 +67,7 @@ async function showReleaseNotes( const releaseNotes = await getReleaseNotesMarkdown( releaseNotesUrl, - context.extensionUri, + context, (uri) => panel.webview.asWebviewUri(uri) ); @@ -86,10 +86,11 @@ async function showReleaseNotes( * Don't show panel for remote environment because it installs extension on every time. * TODO: what about wsl? */ - function isRemote(): boolean { - const isRemote = env.remoteName != null; + function isRemote(): Either { // const isWsl = env.remoteName === "wsl"; - return isRemote; + return env.remoteName == null + ? makeRight(undefined) + : makeLeft(`is a remote environment ${env.remoteName}`); } /** @@ -129,32 +130,59 @@ async function showReleaseNotes( * Translate server version to link to the markdown file with release notes. * @param version clean version in major.minor.patch form * If version has release notes return link to them, if not return nothing. + * Sample link to which we're doing request https://api.github.com/repos/scalameta/metals/releases/tags/v0.11.6. + * From such JSON obtain body property which contains link to the blogpost, but what's more important, + * contains can be converted to name of markdown file with release notes. */ -async function getMarkdownLink(version: string): Promise { +async function getMarkdownLink( + version: string +): Promise> { const releaseInfoUrl = `https://api.github.com/repos/scalameta/metals/releases/tags/v${version}`; const options = { headers: { "User-Agent": "metals", }, }; - const info = await fetchFrom(releaseInfoUrl, options); - const body = (JSON.parse(info)["body"] as string) ?? ""; - // matches (2022)/(06)/(03)/(aluminium) + const stringifiedContent = await fetchFrom(releaseInfoUrl, options); + const body = JSON.parse(stringifiedContent)["body"] as string; + + if (!body) { + const msg = `can't obtain content of ${releaseInfoUrl}`; + return makeLeft(msg); + } + + // matches (2022)/(06)/(03)/(aluminium) via capture groups const matchResult = body.match( new RegExp("(\\d\\d\\d\\d)/(\\d\\d)/(\\d\\d)/(\\w+)") ); + // whole expression + 4 capture groups = 5 entries if (matchResult?.length === 5) { - const [, ...tail] = matchResult; + // omit first entry + const [_, ...tail] = matchResult; const name = tail.join("-"); const url = `https://raw.githubusercontent.com/scalameta/metals/main/website/blog/${name}.md`; - return url; + return makeRight(url); + } else { + const msg = `can't obtain markdown file name for ${version} from ${body}`; + return makeLeft(msg); } } +/** + * + * @param releaseNotesUrl Url which server markdown with release notes + * @param context Extension context + * @param asWebviewUri + * Webviews cannot directly load resources from the workspace or local + * file system using file: uris. The asWebviewUri function takes a local + * file: uri and converts it into a uri that can be used inside of a webview + * to load the same resource. + * proxy to webview.asWebviewUri + */ async function getReleaseNotesMarkdown( releaseNotesUrl: string, - extensionUri: vscode.Uri, - cssUriConverter: (_: vscode.Uri) => vscode.Uri + context: ExtensionContext, + asWebviewUri: (_: vscode.Uri) => vscode.Uri ): Promise { const text = await fetchFrom(releaseNotesUrl); // every release notes starts with that @@ -176,32 +204,15 @@ async function getReleaseNotesMarkdown( const md = new Remarkable({ html: true }); const renderedNotes = md.render(releaseNotes); + // Uri with additional styles for webview const stylesPathMainPath = vscode.Uri.joinPath( - extensionUri, + context.extensionUri, "media", "styles.css" ); + // need to transform Uri + const stylesUri = asWebviewUri(stylesPathMainPath); - // // Uri to load styles into webview - const stylesMainUri = cssUriConverter(stylesPathMainPath); - - const notes = getHtmlContent( - renderedNotes, - author, - title, - authorUrl, - stylesMainUri - ); - return notes; -} - -function getHtmlContent( - renderedNotes: string, - author: string, - title: string, - authorURL: string, - stylesUri: vscode.Uri -): string { return ` @@ -219,7 +230,7 @@ function getHtmlContent(


-

From acccabe3794f67700dd2c9890c0fe783bdf65968 Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sat, 11 Jun 2022 09:43:14 +0200 Subject: [PATCH 05/10] leave code which sets older version for testing purposes --- src/releaseNotesProvider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index 527b4c813..8cf519bf2 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -36,6 +36,7 @@ async function showReleaseNotes( currentVersion: string ): Promise> { const state = context.globalState; + state.update(versionKey, "0.11.5"); const remote = isRemote(); if (remote.kind === "left") { From 38697f67a5a180759ab4ea4897ea26039543b1ad Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sat, 11 Jun 2022 09:57:50 +0200 Subject: [PATCH 06/10] add link to Metals blog --- src/releaseNotesProvider.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index 8cf519bf2..956fbbe86 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -228,6 +228,12 @@ async function getReleaseNotesMarkdown(

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. +
+
+ Original blogpost can be viewed at + .


From fb01b8c5c953b8123ecdc6fd8f48a4c3d6a80586 Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sun, 12 Jun 2022 11:57:23 +0200 Subject: [PATCH 07/10] fix: move remarkable to normal dependencies fix: show releas enotes on wsl --- package.json | 7 ++++--- src/releaseNotesProvider.ts | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d824db066..b2ce82660 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "ide", "scalameta" ], - "version": "1.17.2", + "version": "1.17.6", "publisher": "scalameta", "contributors": [ { @@ -876,10 +876,11 @@ "scripts": { "vscode:prepublish": "yarn compile", "compile": "tsc -p ./", + "clean": "rimraf out/", "watch": "tsc -watch -p ./", "test": "ts-mocha src/test/unit/*.test.ts", "test-extension": "rimraf out/ && tsc -p ./ && node out/test/extension/runTest.js", - "build": "vsce package --yarn", + "build": "yarn clean && vsce package --yarn", "vscode:publish": "vsce publish --yarn", "ovsx:publish": "ovsx publish", "lint": "eslint . --ext .ts --fix && yarn format", @@ -904,7 +905,6 @@ "mocha": "^10.0.0", "ovsx": "0.5.1", "prettier": "2.6.2", - "remarkable": "^2.0.1", "rimraf": "^3.0.2", "ts-mocha": "^10.0.0", "typescript": "4.7.3", @@ -915,6 +915,7 @@ "metals-languageclient": "0.5.16", "promisify-child-process": "4.1.1", "semver": "^7.3.7", + "remarkable": "^2.0.1", "vscode-languageclient": "8.0.1" }, "extensionPack": [ diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index 956fbbe86..1a32443d1 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -88,8 +88,7 @@ async function showReleaseNotes( * TODO: what about wsl? */ function isRemote(): Either { - // const isWsl = env.remoteName === "wsl"; - return env.remoteName == null + return env.remoteName == null || env.remoteName !== "wsl" ? makeRight(undefined) : makeLeft(`is a remote environment ${env.remoteName}`); } From 1f3b0519754ce4a1dddee81cdd9a4d07e11a927f Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Sun, 12 Jun 2022 19:01:15 +0200 Subject: [PATCH 08/10] fix wsl --- src/releaseNotesProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index 1a32443d1..2d620ae76 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -88,7 +88,7 @@ async function showReleaseNotes( * TODO: what about wsl? */ function isRemote(): Either { - return env.remoteName == null || env.remoteName !== "wsl" + return env.remoteName == null || env.remoteName === "wsl" ? makeRight(undefined) : makeLeft(`is a remote environment ${env.remoteName}`); } From 75ea3ff3c290d640d088e556902619f27e1fd937 Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Mon, 13 Jun 2022 09:10:37 +0200 Subject: [PATCH 09/10] feat: add 'show release notes' command --- package.json | 9 +++++++++ src/extension.ts | 26 ++++++++++++++++++++++---- src/releaseNotesProvider.ts | 23 +++++++++++++++-------- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b2ce82660..e4afc307c 100644 --- a/package.json +++ b/package.json @@ -407,6 +407,11 @@ "category": "Metals", "title": "Run doctor" }, + { + "command": "metals.show-release-notes", + "category": "Metals", + "title": "Show release notes" + }, { "command": "metals.show-tasty", "category": "Metals", @@ -641,6 +646,10 @@ "command": "metals.doctor-run", "when": "metals:enabled" }, + { + "command": "metals.show-release-notes", + "when": "metals:enabled" + }, { "command": "metals.target-info-display", "when": "metals:enabled" diff --git a/src/extension.ts b/src/extension.ts index 4154b45f5..436009a00 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -90,7 +90,7 @@ import * as workbenchCommands from "./workbenchCommands"; import { getServerVersion } from "./getServerVersion"; import { getCoursierMirrorPath } from "./mirrors"; import { DoctorProvider } from "./doctor"; -import { showReleaseNotesIfNeeded } from "./releaseNotesProvider"; +import { showReleaseNotes } from "./releaseNotesProvider"; const outputChannel = window.createOutputChannel("Metals"); const openSettingsAction = "Open settings"; @@ -132,7 +132,12 @@ export async function activate(context: ExtensionContext): Promise { try { const javaHome = await getJavaHome(getJavaHomeFromConfig()); await fetchAndLaunchMetals(context, javaHome, serverVersion); - await showReleaseNotesIfNeeded(context, serverVersion, outputChannel); + await showReleaseNotes( + "onExtensionStart", + context, + serverVersion, + outputChannel + ); } catch (err) { outputChannel.appendLine(`${err}`); showMissingJavaMessage(); @@ -264,7 +269,8 @@ function fetchAndLaunchMetals( context, classpath, serverProperties, - javaConfig + javaConfig, + serverVersion ); }, (reason) => { @@ -322,7 +328,8 @@ function launchMetals( context: ExtensionContext, metalsClasspath: string, serverProperties: string[], - javaConfig: JavaConfig + javaConfig: JavaConfig, + serverVersion: string ) { // Make editing Scala docstrings slightly nicer. enableScaladocIndentation(); @@ -509,6 +516,17 @@ function launchMetals( ) ); + registerCommand( + "metals.show-release-notes", + async () => + await showReleaseNotes( + "onUserDemand", + context, + serverVersion, + outputChannel + ) + ); + return client.start().then( () => { const doctorProvider = new DoctorProvider(client); diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index 2d620ae76..d560e6575 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -6,19 +6,25 @@ import { fetchFrom } from "./util"; import { Either, makeLeft, makeRight } from "./types"; const versionKey = "metals-server-version"; +type CalledOn = "onExtensionStart" | "onUserDemand"; /** * 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. + * + * @param calledOn determines when this function was called. + * For 'onExtensionStart' case show release notes only once (first time). + * For 'onUserDemand' show extension notes no matter if it's another time. */ -export async function showReleaseNotesIfNeeded( +export async function showReleaseNotes( + calledOn: CalledOn, context: ExtensionContext, serverVersion: string, outputChannel: vscode.OutputChannel ) { try { // context.globalState.update(versionKey, "0.11.5"); - const result = await showReleaseNotes(context, serverVersion); + const result = await showReleaseNotesImpl(calledOn, context, serverVersion); if (result.kind === "left") { const msg = `Release notes was not shown: ${result.value}`; outputChannel.appendLine(msg); @@ -31,19 +37,19 @@ export async function showReleaseNotesIfNeeded( } } -async function showReleaseNotes( +async function showReleaseNotesImpl( + calledOn: CalledOn, context: ExtensionContext, currentVersion: string ): Promise> { const state = context.globalState; - state.update(versionKey, "0.11.5"); const remote = isRemote(); if (remote.kind === "left") { return remote; } - const version = getVersion(); + const version = getVersion(calledOn); if (version.kind === "left") { return version; } @@ -96,7 +102,7 @@ async function showReleaseNotes( /** * Return version for which release notes should be displayed */ - function getVersion(): Either { + function getVersion(calledOn: CalledOn): Either { const previousVersion: string | undefined = state.get(versionKey); // strip version to // in theory semver.clean can return null, but we're almost sure that currentVersion is well defined @@ -107,8 +113,9 @@ async function showReleaseNotes( return makeLeft(msg); } - if (!previousVersion) { - // if there was no previous version then show release notes for current cleaned version + // if there was no previous version or user explicitly wants to read release notes + // show release notes for current cleaned version + if (!previousVersion || calledOn === "onUserDemand") { return makeRight(currentVersion); } From 80ee3cb8eccccfb5ab61bb43f7ff7214952745dd Mon Sep 17 00:00:00 2001 From: Kamil Podsiadlo Date: Mon, 13 Jun 2022 18:22:57 +0200 Subject: [PATCH 10/10] remove unused code --- src/releaseNotesProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/releaseNotesProvider.ts b/src/releaseNotesProvider.ts index d560e6575..f24875e31 100644 --- a/src/releaseNotesProvider.ts +++ b/src/releaseNotesProvider.ts @@ -23,7 +23,6 @@ export async function showReleaseNotes( outputChannel: vscode.OutputChannel ) { try { - // context.globalState.update(versionKey, "0.11.5"); const result = await showReleaseNotesImpl(calledOn, context, serverVersion); if (result.kind === "left") { const msg = `Release notes was not shown: ${result.value}`;