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.
+
+
+
+
+ ${author}
+
+
+
+ ${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(
-
+
${author}
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
+
+ Metals blog
+ .
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}`;