diff --git a/packages/metals-vscode/icons/hot_code_replace.svg b/packages/metals-vscode/icons/hot_code_replace.svg new file mode 100644 index 000000000..3e0704763 --- /dev/null +++ b/packages/metals-vscode/icons/hot_code_replace.svg @@ -0,0 +1,13 @@ + + + + +lightning +Created with Sketch. + + + + diff --git a/packages/metals-vscode/package.json b/packages/metals-vscode/package.json index b187a3f4c..3e31b22ac 100644 --- a/packages/metals-vscode/package.json +++ b/packages/metals-vscode/package.json @@ -218,6 +218,11 @@ "configuration": { "title": "Metals", "properties": { + "metals.debug.settings.hotCodeReplace": { + "type": "boolean", + "default": false, + "markdownDescription": "Allow Hot Code Replace (HCR) while debugging" + }, "metals.serverVersion": { "type": "string", "default": "1.0.0+20-46905735-SNAPSHOT", @@ -419,6 +424,14 @@ } }, "commands": [ + { + "command": "metals.debug.hotCodeReplace", + "title": "Hot Code Replace", + "icon": { + "light": "icons/hot_code_replace.svg", + "dark": "icons/hot_code_replace.svg" + } + }, { "command": "metals.reveal-active-file", "category": "Metals", @@ -692,6 +705,13 @@ "when": "view == metalsPackages" } ], + "debug/toolBar": [ + { + "command": "metals.debug.hotCodeReplace", + "group": "navigation@100", + "when": "inDebugMode && debugType == scala && scalaHotReloadOn" + } + ], "commandPalette": [ { "command": "metals.show-tasty", @@ -1085,7 +1105,8 @@ "metals-languageclient": "file:../metals-languageclient", "promisify-child-process": "4.1.1", "semver": "^7.5.2", - "vscode-languageclient": "8.1.0" + "vscode-languageclient": "8.1.0", + "vscode-extension-telemetry-wrapper": "^0.13.3" }, "extensionPack": [ "scala-lang.scala" diff --git a/packages/metals-vscode/src/extension.ts b/packages/metals-vscode/src/extension.ts index c4d1addc2..e258cd39c 100644 --- a/packages/metals-vscode/src/extension.ts +++ b/packages/metals-vscode/src/extension.ts @@ -31,6 +31,7 @@ import { tests as vscodeTextExplorer, debug, DebugSessionCustomEvent, + DebugSession, } from "vscode"; import { LanguageClient, @@ -104,6 +105,9 @@ import { SCALA_LANGID, } from "./consts"; import { ScalaCodeLensesParams } from "./debugger/types"; +import { initializeHotCodeReplace } from "./hotCodeReplace"; +import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; +import * as vscode from "vscode"; const outputChannel = window.createOutputChannel("Metals"); const downloadJava = "Download Java"; @@ -1143,6 +1147,15 @@ function launchMetals( } ); context.subscriptions.push(decorationsRangesDidChangeDispoasable); + context.subscriptions.push( + instrumentOperationAsVsCodeCommand( + "metals.debug.hotCodeReplace", + async () => { + await applyHCR(); + } + ) + ); + initializeHotCodeReplace(context); }, (reason) => { if (reason instanceof Error) { @@ -1350,3 +1363,59 @@ function handleUserNotification(customEvent: DebugSessionCustomEvent) { window.showInformationMessage(customEvent.body.message); } } + +async function applyHCR() { + const debugSession: DebugSession | undefined = + vscode.debug.activeDebugSession; + if (!debugSession) { + return; + } + + if (debugSession.configuration.noDebug) { + vscode.window + .showWarningMessage( + "Failed to apply the changes because hot code replace is not supported by run mode, " + + "would you like to restart the program?" + ) + .then((res) => { + if (res === "Yes") { + vscode.commands.executeCommand("workbench.action.debug.restart"); + } + }); + + return; + } + + const start = new Date().getTime(); + const redefineRequest = debugSession.customRequest("redefineClasses"); + vscode.window.setStatusBarMessage( + "$(sync~spin) Applying code changes...", + redefineRequest + ); + const response = await redefineRequest; + const elapsed = new Date().getTime() - start; + const humanVisibleDelay = elapsed < 150 ? 150 : 0; + if (humanVisibleDelay) { + await new Promise((resolve) => { + setTimeout(resolve, humanVisibleDelay); + }); + } + + if (response?.errorMessage) { + vscode.window.showErrorMessage(response.errorMessage); + return; + } + + if (!response?.changedClasses?.length) { + vscode.window.showWarningMessage( + "Cannot find any changed classes for hot replace!" + ); + return; + } + + const changed = response.changedClasses.length; + vscode.window.setStatusBarMessage( + `$(check) Class${changed > 1 ? "es" : ""} successfully reloaded`, + 5 * 1000 + ); +} diff --git a/packages/metals-vscode/src/hotCodeReplace.ts b/packages/metals-vscode/src/hotCodeReplace.ts new file mode 100644 index 000000000..04ab5ebf3 --- /dev/null +++ b/packages/metals-vscode/src/hotCodeReplace.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +// Adapter from https://github.com/microsoft/vscode-java-debug/blob/main/src/hotCodeReplace.ts + +import * as vscode from "vscode"; + +import { SCALA_LANGID } from "./consts"; +import { window } from "vscode"; + +const suppressedReasons: Set = new Set(); + +export const YES_BUTTON = "Yes"; + +export const NO_BUTTON = "No"; + +const NEVER_BUTTON = "Do not show again"; + +enum HcrChangeType { + ERROR = "ERROR", + WARNING = "WARNING", + STARTING = "STARTING", + END = "END", + BUILD_COMPLETE = "BUILD_COMPLETE", +} + +export function initializeHotCodeReplace(context: vscode.ExtensionContext) { + vscode.commands.executeCommand( + "setContext", + "scalaHotReloadOn", + hotReplaceIsOn() + ); + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("metals.debug.settings.hotCodeReplace")) { + vscode.commands.executeCommand( + "setContext", + "scalaHotReloadOn", + hotReplaceIsOn() + ); + } + }); + vscode.debug.onDidStartDebugSession((session) => { + if (session?.configuration.noDebug && !vscode.debug.activeDebugSession) { + vscode.commands.executeCommand("setContext", "scalaHotReloadOn", false); + } + }); + vscode.debug.onDidChangeActiveDebugSession((session) => { + vscode.commands.executeCommand( + "setContext", + "scalaHotReloadOn", + session && !session.configuration.noDebug + ); + }); + context.subscriptions.push( + vscode.debug.onDidTerminateDebugSession((session) => { + const t = session ? session.type : undefined; + if (t === SCALA_LANGID) { + suppressedReasons.clear(); + } + }) + ); +} + +export function handleHotCodeReplaceCustomEvent( + hcrEvent: vscode.DebugSessionCustomEvent +) { + if (hcrEvent.body.changeType === HcrChangeType.BUILD_COMPLETE) { + if (hotReplaceIsOn()) { + return vscode.window.withProgress( + { location: vscode.ProgressLocation.Window }, + (progress) => { + progress.report({ message: "Applying code changes..." }); + return hcrEvent.session.customRequest("redefineClasses"); + } + ); + } + } + + if ( + hcrEvent.body.changeType === HcrChangeType.ERROR || + hcrEvent.body.changeType === HcrChangeType.WARNING + ) { + if (!suppressedReasons.has(hcrEvent.body.message)) { + window + .showWarningMessage( + `Hot code replace failed - ${hcrEvent.body.message}. Would you like to restart the debug session?` + ) + .then((res) => { + if (res === NEVER_BUTTON) { + suppressedReasons.add(hcrEvent.body.message); + } else if (res === YES_BUTTON) { + vscode.commands.executeCommand("workbench.action.debug.restart"); + } + }); + } + } + return undefined; +} + +function hotReplaceIsOn(): boolean { + return ( + vscode.workspace + .getConfiguration("metals.debug.settings") + .get("hotCodeReplace") ?? false + ); +} diff --git a/packages/metals-vscode/yarn.lock b/packages/metals-vscode/yarn.lock index 818bd0cd2..3f261037a 100644 --- a/packages/metals-vscode/yarn.lock +++ b/packages/metals-vscode/yarn.lock @@ -53,6 +53,42 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@microsoft/1ds-core-js@3.2.13", "@microsoft/1ds-core-js@^3.2.3": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz#0c105ed75091bae3f1555c0334704fa9911c58fb" + integrity sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg== + dependencies: + "@microsoft/applicationinsights-core-js" "2.8.15" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/1ds-post-js@^3.2.3": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz#560aacac8a92fdbb79e8c2ebcb293d56e19f51aa" + integrity sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA== + dependencies: + "@microsoft/1ds-core-js" "3.2.13" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/applicationinsights-core-js@2.8.15": + version "2.8.15" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz#8fa466474260e01967fe649f14dd9e5ff91dcdc8" + integrity sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ== + dependencies: + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.9" + +"@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" + integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== + +"@microsoft/dynamicproto-js@^1.1.7", "@microsoft/dynamicproto-js@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6" + integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -282,6 +318,14 @@ async "^3.2.2" semver "^7.3.5" +"@vscode/extension-telemetry@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.6.2.tgz#b86814ee680615730da94220c2b03ea9c3c14a8e" + integrity sha512-yb/wxLuaaCRcBAZtDCjNYSisAXz3FWsSqAha5nhHcYxx2ZPdQdWuZqVXGKq0ZpHVndBWWtK6XqtpCN2/HB4S1w== + dependencies: + "@microsoft/1ds-core-js" "^3.2.3" + "@microsoft/1ds-post-js" "^3.2.3" + "@vscode/test-electron@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.3.0.tgz#de0ba2f5d36546a83cd481b458cbdbb7cc0f7049" @@ -2445,6 +2489,19 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vscode-extension-telemetry-wrapper@^0.13.3: + version "0.13.3" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry-wrapper/-/vscode-extension-telemetry-wrapper-0.13.3.tgz#685fe92843b07fe785e416926721e26f867c3a69" + integrity sha512-k/PbUbH9/xqiMXI2g2RXpDg+4/v08t3NzdPc7HuDPF3A1XcYkgYwsPnS/bqsKZNymSQdbLvVuie6STMxbDX9KQ== + dependencies: + "@vscode/extension-telemetry" "^0.6.2" + uuid "^8.3.2" + vscode-jsonrpc@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94"