Skip to content

Commit

Permalink
feat: show Metals' release notes if server version was updated (scala…
Browse files Browse the repository at this point in the history
  • Loading branch information
kpodsiad authored Jun 13, 2022
1 parent 78824f6 commit 3ebbea7
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 6 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions media/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
h2,
h3,
h4,
h5,
h6 {
margin-top: 2em;
margin-bottom: 0em;
}
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@
"category": "Metals",
"title": "Run doctor"
},
{
"command": "metals.show-release-notes",
"category": "Metals",
"title": "Show release notes"
},
{
"command": "metals.scalafix-run",
"category": "Metals",
Expand Down Expand Up @@ -660,6 +665,10 @@
"command": "metals.doctor-run",
"when": "metals:enabled"
},
{
"command": "metals.show-release-notes",
"when": "metals:enabled"
},
{
"command": "metals.scalafix-run",
"when": "metals:enabled"
Expand Down Expand Up @@ -899,10 +908,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",
Expand All @@ -913,6 +923,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.1",
"@typescript-eslint/parser": "^5.27.1",
Expand All @@ -934,6 +946,8 @@
"ansicolor": "^1.1.100",
"metals-languageclient": "0.5.15",
"promisify-child-process": "4.1.1",
"semver": "^7.3.7",
"remarkable": "^2.0.1",
"vscode-languageclient": "7.0.0"
},
"extensionPack": [
Expand Down
26 changes: 22 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import * as workbenchCommands from "./workbenchCommands";
import { getServerVersion } from "./getServerVersion";
import { getCoursierMirrorPath } from "./mirrors";
import { DoctorProvider } from "./doctor";
import { showReleaseNotes } from "./releaseNotesProvider";

const outputChannel = window.createOutputChannel("Metals");
const openSettingsAction = "Open settings";
Expand Down Expand Up @@ -130,7 +131,13 @@ export async function activate(context: ExtensionContext): Promise<void> {
commands.executeCommand("setContext", "metals:enabled", true);
try {
const javaHome = await getJavaHome(getJavaHomeFromConfig());
return fetchAndLaunchMetals(context, javaHome, serverVersion);
await fetchAndLaunchMetals(context, javaHome, serverVersion);
await showReleaseNotes(
"onExtensionStart",
context,
serverVersion,
outputChannel
);
} catch (err) {
outputChannel.appendLine(`${err}`);
showMissingJavaMessage();
Expand Down Expand Up @@ -262,7 +269,8 @@ function fetchAndLaunchMetals(
context,
classpath,
serverProperties,
javaConfig
javaConfig,
serverVersion
);
},
(reason) => {
Expand Down Expand Up @@ -320,7 +328,8 @@ function launchMetals(
context: ExtensionContext,
metalsClasspath: string,
serverProperties: string[],
javaConfig: JavaConfig
javaConfig: JavaConfig,
serverVersion: string
) {
// Make editing Scala docstrings slightly nicer.
enableScaladocIndentation();
Expand Down Expand Up @@ -507,7 +516,16 @@ function launchMetals(
)
);

context.subscriptions.push(client.start());
registerCommand(
"metals.show-release-notes",
async () =>
await showReleaseNotes(
"onUserDemand",
context,
serverVersion,
outputChannel
)
);

return client.onReady().then(
() => {
Expand Down
254 changes: 254 additions & 0 deletions src/releaseNotesProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { env, ExtensionContext } from "vscode";
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";
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 showReleaseNotes(
calledOn: CalledOn,
context: ExtensionContext,
serverVersion: string,
outputChannel: vscode.OutputChannel
) {
try {
const result = await showReleaseNotesImpl(calledOn, context, serverVersion);
if (result.kind === "left") {
const msg = `Release notes was not shown: ${result.value}`;
outputChannel.appendLine(msg);
}
} catch (error) {
outputChannel.appendLine(
`Error, couldn't show release notes for Metals ${serverVersion}`
);
outputChannel.appendLine(`${error}`);
}
}

async function showReleaseNotesImpl(
calledOn: CalledOn,
context: ExtensionContext,
currentVersion: string
): Promise<Either<string, void>> {
const state = context.globalState;

const remote = isRemote();
if (remote.kind === "left") {
return remote;
}

const version = getVersion(calledOn);
if (version.kind === "left") {
return version;
}

const releaseNotesUrl = await getMarkdownLink(version.value);
if (releaseNotesUrl.kind === "left") {
return releaseNotesUrl;
}

// actual logic starts here
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`,
`Metals ${version} release notes`,
vscode.ViewColumn.One
);

const releaseNotes = await getReleaseNotesMarkdown(
releaseNotesUrl,
context,
(uri) => panel.webview.asWebviewUri(uri)
);

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, version);
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 isRemote(): Either<string, void> {
return env.remoteName == null || env.remoteName === "wsl"
? makeRight(undefined)
: makeLeft(`is a remote environment ${env.remoteName}`);
}

/**
* Return version for which release notes should be displayed
*/
function getVersion(calledOn: CalledOn): Either<string, string> {
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
const cleanVersion = semver.clean(currentVersion);

if (cleanVersion == null) {
const msg = `can't transform ${currentVersion} to 'major.minor.patch'`;
return makeLeft(msg);
}

// 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);
}

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.
* @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<Either<string, string>> {
const releaseInfoUrl = `https://api.github.com/repos/scalameta/metals/releases/tags/v${version}`;
const options = {
headers: {
"User-Agent": "metals",
},
};
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) {
// omit first entry
const [_, ...tail] = matchResult;
const name = tail.join("-");
const url = `https://raw.githubusercontent.com/scalameta/metals/main/website/blog/${name}.md`;
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,
context: ExtensionContext,
asWebviewUri: (_: vscode.Uri) => vscode.Uri
): Promise<string> {
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);
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);

// Uri with additional styles for webview
const stylesPathMainPath = vscode.Uri.joinPath(
context.extensionUri,
"media",
"styles.css"
);
// need to transform Uri
const stylesUri = asWebviewUri(stylesPathMainPath);

return `
<!DOCTYPE html>
<html lang="en" style="height: 100%; width: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<h1>${title}</h1>
<hr>
<p>
Showing Metals' release notes embedded in vscode is an experimental feature, in case of any issues report them at
<a href="https://github.com/scalameta/metals-vscode">https://github.com/scalameta/metals-vscode</a>.
<br/>
<br/>
Original blogpost can be viewed at
<a href="https://scalameta.org/metals/blog/" target="_blank" itemprop="url">
<span itemprop="name">Metals blog</span>
</a>.
</p>
<hr>
<p>
<a href="${authorUrl}" target="_blank" itemprop="url">
<span itemprop="name">${author}</span>
</a>
</p>
<hr>
${renderedNotes}
</body>
</html>
`;
}
Loading

0 comments on commit 3ebbea7

Please sign in to comment.