-
Notifications
You must be signed in to change notification settings - Fork 97
feat: show Metals' release notes if server version is updated #1009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
d66b766
feature: show Metals' release notes directly in vscode
kpodsiad 2157557
feat: add styles, retrieve release notes automatically
kpodsiad e4500f9
better error handling
kpodsiad bf49440
add more docs, tidy things up
kpodsiad c1d4f11
Merge branch 'main' into feat/whats-new
kpodsiad acccabe
leave code which sets older version for testing purposes
kpodsiad 38697f6
add link to Metals blog
kpodsiad fb01b8c
fix: move remarkable to normal dependencies
kpodsiad 1f3b051
fix wsl
kpodsiad 75ea3ff
feat: add 'show release notes' command
kpodsiad f813f2d
Merge branch 'main' into feat/whats-new
kpodsiad 5f9f6b9
Merge branch 'main' into feat/whats-new
kpodsiad 80ee3cb
remove unused code
kpodsiad File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>> { | ||
|
kpodsiad marked this conversation as resolved.
|
||
| 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> | ||
| `; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.