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 @@
+
+
+
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"