diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js
index 060136b47f..c0f608923d 100644
--- a/src/extensionsIntegrated/Terminal/main.js
+++ b/src/extensionsIntegrated/Terminal/main.js
@@ -93,6 +93,8 @@ define(function (require, exports, module) {
let originalDefaultShellName = null; // System-detected default shell name
let _focusToastShown = false; // Show focus hint toast only once per session
let _clearHintShown = false; // Show clear buffer hint toast only once per session
+ let _projectPath = null;
+ let _restartingTerminals = false;
let $panel, $contentArea, $shellDropdown, $flyoutList;
/**
@@ -186,7 +188,7 @@ define(function (require, exports, module) {
const shells = ShellProfiles.getShells();
const defaultShell = ShellProfiles.getDefaultShell();
$shellDropdown.empty();
- for (const shell of shells) {
+ shells.forEach(function (shell) {
const isSelected = defaultShell && defaultShell.name === shell.name;
const $check = $('');
if (isSelected) {
@@ -197,6 +199,9 @@ define(function (require, exports, module) {
.append($check)
.append($('').text(shell.name));
$item.on("click", function () {
+ if (_restartingTerminals) {
+ return;
+ }
_hideShellDropdown();
ShellProfiles.setDefaultShell(shell.name);
_populateShellDropdown();
@@ -209,7 +214,7 @@ define(function (require, exports, module) {
_createNewTerminalWithShell(shell);
});
$shellDropdown.append($item);
- }
+ });
}
/**
@@ -272,6 +277,9 @@ define(function (require, exports, module) {
* Create a new terminal with the default shell
*/
async function _createNewTerminal(cwdOverride) {
+ if (_restartingTerminals) {
+ return;
+ }
const shell = ShellProfiles.getDefaultShell();
return _createNewTerminalWithShell(shell, cwdOverride);
}
@@ -286,17 +294,12 @@ define(function (require, exports, module) {
if (cwd.startsWith(tauriPrefix)) {
cwd = Phoenix.fs.getTauriPlatformPath(cwd);
}
- if (cwd.length > 1 && (cwd.endsWith("/") || cwd.endsWith("\\"))) {
+ if (cwd.length > 1 && !/^[a-z]:[\\/]$/i.test(cwd) && (cwd.endsWith("/") || cwd.endsWith("\\"))) {
cwd = cwd.slice(0, -1);
}
return cwd;
}
- /**
- * Create a new terminal with a specific shell profile
- * @param {Object} shell - Shell profile to use
- * @param {string} [cwdOverride] - Optional VFS path to use as cwd instead of project root
- */
/**
* Map an OS shell name (e.g. "powershell.exe", "bash.exe") to a short
* family label so the metrics server's per-event length budget stays
@@ -315,7 +318,14 @@ define(function (require, exports, module) {
return n || "unknown";
}
- async function _createNewTerminalWithShell(shell, cwdOverride) {
+ /**
+ * Create a terminal using a shell profile and an optional VFS directory.
+ * @param {Object} shell Shell profile to use.
+ * @param {string} [cwdOverride] Directory to use instead of the project root.
+ * @param {string} [projectPath] Owning project when restarting during a project switch.
+ * @return {Promise} The new terminal, if a shell is available.
+ */
+ async function _createNewTerminalWithShell(shell, cwdOverride, projectPath) {
if (!shell) {
console.error("Terminal: No shell available");
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new", "noShell");
@@ -326,11 +336,11 @@ define(function (require, exports, module) {
_shellMetricLabel(shell.name));
// Get cwd: use override if provided, otherwise fall back to project root
+ const projectRoot = ProjectManager.getProjectRoot();
let cwd;
if (cwdOverride) {
cwd = _toNativePath(cwdOverride);
} else {
- const projectRoot = ProjectManager.getProjectRoot();
if (projectRoot) {
cwd = _toNativePath(projectRoot.fullPath);
}
@@ -338,6 +348,8 @@ define(function (require, exports, module) {
// Create instance
const instance = new TerminalInstance(nodeConnector, shell, cwd);
+ // Project ownership is independent of directories the user visits in the shell.
+ instance.projectPath = projectPath || (projectRoot ? projectRoot.fullPath : null);
// Set up callbacks
instance.onTitleChanged = _onTerminalTitleChanged;
@@ -376,6 +388,120 @@ define(function (require, exports, module) {
// Spawn PTY process
await instance.spawn();
+ return instance;
+ }
+
+ /** Remove the project-switch notice without changing any terminal session. */
+ function _hideProjectBanner() {
+ if ($contentArea) {
+ $contentArea.find(".terminal-project-banner").remove();
+ }
+ }
+
+ /** Show one notice for all bottom-panel terminals, naming the current project. */
+ function _showProjectBanner() {
+ _hideProjectBanner();
+ const root = ProjectManager.getProjectRoot();
+ if (!root || !terminalInstances.some(inst => inst.projectPath !== root.fullPath)) {
+ return;
+ }
+ const $banner = $('');
+ $banner.append($('')
+ .text(StringUtils.format(Strings.TERMINAL_PROJECT_CHANGED, root.name)));
+ const path = _toNativePath(root.fullPath);
+ $banner.append($('')
+ .text(StringUtils.format(Strings.TERMINAL_PROJECT_RESTART_PATH, path)).attr("title", path));
+ const $actions = $('');
+ $actions.append($('')
+ .text(Strings.TERMINAL_PROJECT_KEEP).on("click", function () {
+ _hideProjectBanner();
+ const active = _getActiveTerminal();
+ if (active) {
+ active.focus();
+ }
+ }));
+ $actions.append($('')
+ .text(Strings.TERMINAL_PROJECT_RESTART).attr("title", Strings.TERMINAL_PROJECT_RESTART_WARNING)
+ .on("click", _restartTerminalsInProject));
+ $actions.find("button").prop("disabled", _restartingTerminals);
+ $banner.append($actions);
+ $contentArea.append($banner);
+ }
+
+ /** Notify on actual project changes; shell navigation and panel toggles leave sessions alone. */
+ function _onProjectOpen() {
+ const root = ProjectManager.getProjectRoot();
+ const path = root ? root.fullPath : null;
+ if (path !== _projectPath) {
+ _projectPath = path;
+ _showProjectBanner();
+ }
+ }
+
+ /**
+ * Query child processes using the same platform-specific lookup as terminal tabs.
+ * @return {Promise} Active child process names.
+ */
+ async function _getActiveProcesses() {
+ const results = await Promise.all(terminalInstances.filter(inst => inst.isAlive).map(function (inst) {
+ return nodeConnector.execPeer("getTerminalProcess", {id: inst.id})
+ .catch(function () { return {process: ""}; });
+ }));
+ return results.filter(result => result.process && !_isShellProcess(result.process))
+ .map(result => result.process);
+ }
+
+ /** Restart all bottom-panel tabs in the selected project, preserving shells, order and selection. */
+ async function _restartTerminalsInProject() {
+ const root = ProjectManager.getProjectRoot();
+ if (_restartingTerminals || !root || !terminalInstances.length) {
+ return;
+ }
+ const path = root.fullPath;
+ _restartingTerminals = true;
+ $contentArea.find(".terminal-project-actions button").prop("disabled", true);
+ try {
+ const activeProcesses = await _getActiveProcesses();
+ if (activeProcesses.length) {
+ const dialog = Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_INFO,
+ Strings.TERMINAL_RESTART_CONFIRM_TITLE,
+ Strings.TERMINAL_RESTART_CONFIRM_MSG, [
+ {className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL, text: Strings.CANCEL},
+ {className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK,
+ text: Strings.TERMINAL_PROJECT_RESTART}
+ ]);
+ if (await dialog.getPromise() !== Dialogs.DIALOG_BTN_OK) {
+ return;
+ }
+ }
+ // A project can change while process lookup or confirmation is pending.
+ const currentRoot = ProjectManager.getProjectRoot();
+ if (!currentRoot || currentRoot.fullPath !== path) {
+ return;
+ }
+ const profiles = terminalInstances.map(inst => inst.shellProfile);
+ const activeIndex = terminalInstances.findIndex(inst => inst.id === activeTerminalId);
+ await _disposeAllAsync();
+ activeTerminalId = null;
+ _updateFlyout();
+ const replacements = [];
+ for (const profile of profiles) {
+ replacements.push(await _createNewTerminalWithShell(profile, path, path));
+ }
+ if (replacements[activeIndex]) {
+ _activateTerminal(replacements[activeIndex].id);
+ }
+ // Keep a notice for a newer project selected during the restart.
+ if (_projectPath !== path) {
+ _showProjectBanner();
+ }
+ } catch (err) {
+ console.error("Terminal: Failed to restart terminals:", err);
+ _showProjectBanner();
+ } finally {
+ _restartingTerminals = false;
+ $contentArea.find(".terminal-project-actions button").prop("disabled", false);
+ }
}
/**
@@ -401,6 +527,9 @@ define(function (require, exports, module) {
* Close a terminal instance, confirming first if a child process is running
*/
async function _closeTerminal(id) {
+ if (_restartingTerminals) {
+ return;
+ }
const idx = terminalInstances.findIndex(t => t.id === id);
if (idx === -1) {
return;
@@ -433,6 +562,9 @@ define(function (require, exports, module) {
instance.dispose();
terminalInstances.splice(idx, 1);
delete processInfo[id];
+ if ($contentArea.find(".terminal-project-banner").length) {
+ _showProjectBanner();
+ }
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "close", "user");
// If we closed the active terminal, activate another
@@ -447,6 +579,7 @@ define(function (require, exports, module) {
// If no terminals left, hide the panel
if (terminalInstances.length === 0) {
+ _hideProjectBanner();
panel.hide();
}
@@ -708,22 +841,23 @@ define(function (require, exports, module) {
return false;
}
const el = document.activeElement;
- if (!el || !$contentArea[0].contains(el)) {
+ if (!el || !$(el).closest(".terminal-instance-container").length) {
return false;
}
+ const inBottomPanel = $contentArea[0].contains(el);
const ctrlOrMeta = event.ctrlKey || event.metaKey;
const key = event.key.toLowerCase();
// Ctrl+K (Cmd+K on mac): clear terminal scrollback
- if (ctrlOrMeta && !event.shiftKey && key === "k") {
+ if (inBottomPanel && ctrlOrMeta && !event.shiftKey && key === "k") {
event.preventDefault();
_clearActiveTerminal();
return true;
}
// Show clear buffer hint on Ctrl+L
- if (ctrlOrMeta && !event.shiftKey && key === "l") {
+ if (inBottomPanel && ctrlOrMeta && !event.shiftKey && key === "l") {
_showClearBufferHintToast();
}
@@ -800,6 +934,7 @@ define(function (require, exports, module) {
}
terminalInstances = [];
processInfo = {};
+ _hideProjectBanner();
}
/**
@@ -922,23 +1057,18 @@ define(function (require, exports, module) {
_initNodeConnector();
_createPanel();
_createToolbarButton();
+ const root = ProjectManager.getProjectRoot();
+ _projectPath = root ? root.fullPath : null;
+ ProjectManager.on("projectOpen.terminal", _onProjectOpen);
// Gate user-initiated panel close (X button): confirm if needed, then
// dispose all terminals. Programmatic hide() just collapses the panel
// without disposing terminals.
panel.registerOnCloseRequestedHandler(async function () {
- // Query all terminals in parallel to avoid sequential 2s waits on Windows
- const aliveInstances = terminalInstances.filter(inst => inst.isAlive);
- const results = await Promise.all(aliveInstances.map(function (inst) {
- return nodeConnector.execPeer("getTerminalProcess", {id: inst.id})
- .catch(function () { return {process: ""}; });
- }));
- const activeProcesses = [];
- for (const result of results) {
- if (result.process && !_isShellProcess(result.process)) {
- activeProcesses.push(result.process);
- }
+ if (_restartingTerminals) {
+ return false;
}
+ const activeProcesses = await _getActiveProcesses();
let title, message, confirmText;
const count = terminalInstances.length;
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index e22cc483c0..fd86c62978 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -2230,6 +2230,13 @@ define({
"TERMINAL_FOCUS_HINT": "Press {0} to switch between editor and terminal",
"TERMINAL_CLEAR": "Clear Terminal",
"TERMINAL_CLEAR_BUFFER_HINT": "💡 Press {0} to clear terminal buffer",
+ "TERMINAL_PROJECT_CHANGED": "Project changed to {0}. Existing terminals have kept their locations.",
+ "TERMINAL_PROJECT_RESTART_PATH": "Restart in: {0}",
+ "TERMINAL_PROJECT_RESTART_WARNING": "Restarting opens every terminal in this folder, stops running processes and clears terminal output.",
+ "TERMINAL_PROJECT_KEEP": "Keep Terminals",
+ "TERMINAL_PROJECT_RESTART": "Restart All in This Project",
+ "TERMINAL_RESTART_CONFIRM_TITLE": "Restart All Terminals?",
+ "TERMINAL_RESTART_CONFIRM_MSG": "Terminals have active processes. Restarting will stop them and clear terminal output. Continue?",
"EXTENDED_COMMIT_MESSAGE": "EXTENDED",
"GETTING_STAGED_DIFF_PROGRESS": "Getting diff of staged files\u2026",
"GIT_COMMIT": "Git commit\u2026",
diff --git a/src/styles/Extn-Terminal.less b/src/styles/Extn-Terminal.less
index ed54b2dd50..83524a34ae 100644
--- a/src/styles/Extn-Terminal.less
+++ b/src/styles/Extn-Terminal.less
@@ -365,6 +365,40 @@
overflow: hidden;
}
+.terminal-project-banner {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 5;
+ box-sizing: border-box;
+ max-height: 100%;
+ overflow: auto;
+ padding: 8px 10px;
+ background: var(--terminal-toolbar-bg);
+ color: var(--terminal-foreground);
+ border-top: 1px solid var(--terminal-border);
+ border-left: 3px solid var(--terminal-ansi-yellow);
+ box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.2);
+ white-space: normal;
+
+ .terminal-project-path {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin: 4px 0;
+ font-weight: 600;
+ }
+
+ .terminal-project-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 8px;
+ }
+}
+
.terminal-instance-container {
position: absolute;
top: 0;
diff --git a/test/spec/Terminal-integ-test.js b/test/spec/Terminal-integ-test.js
index 9ebca685f8..a0a94f098c 100644
--- a/test/spec/Terminal-integ-test.js
+++ b/test/spec/Terminal-integ-test.js
@@ -18,7 +18,7 @@
*
*/
-/*global describe, it, expect, beforeAll, afterAll, afterEach, awaitsFor, spyOn */
+/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, spyOn */
define(function (require, exports, module) {
@@ -28,6 +28,7 @@ define(function (require, exports, module) {
const SpecRunnerUtils = require("spec/SpecRunnerUtils");
const Strings = require("strings");
+ const StringUtils = require("utils/StringUtils");
const IS_WINDOWS = Phoenix.platform === "win";
const IS_MAC = Phoenix.platform === "mac";
@@ -597,6 +598,276 @@ define(function (require, exports, module) {
});
});
+ describe("Project-switch banner", function () {
+ let termModule, secondProjectPath;
+
+ beforeAll(async function () {
+ termModule = testWindow.brackets.getModule("extensionsIntegrated/Terminal/main");
+ await SpecRunnerUtils.createTempDirectory();
+ secondProjectPath = SpecRunnerUtils.getTempDirectory();
+ });
+
+ beforeEach(async function () {
+ await termModule._disposeAll();
+ WorkspaceManager.getPanelForID(PANEL_ID).hide();
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ }, 30000);
+
+ afterEach(async function () {
+ if (isDialogOpen()) {
+ __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_CANCEL);
+ await __PR.waitForModalDialogClosed();
+ }
+ await termModule._disposeAll();
+ WorkspaceManager.getPanelForID(PANEL_ID).hide();
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ }, 30000);
+
+ afterAll(async function () {
+ await SpecRunnerUtils.removeTempDirectory();
+ });
+
+ /**
+ * Open a real shell and wait for its first prompt output.
+ * @return {Promise} The ready terminal.
+ */
+ async function openReadyTerminal() {
+ await openTerminal();
+ await waitForShellReady();
+ const instance = termModule._getActiveTerminal();
+ await instance.firstDataReceived;
+ return instance;
+ }
+
+ /**
+ * Verify the shell's actual directory without depending on its prompt format.
+ * @param {TerminalInstance} instance The shell to query.
+ * @param {string} path Expected native directory.
+ */
+ async function expectWorkingDirectory(instance, path) {
+ const shell = instance.shellProfile.path.split(/[\\/]/).pop().toLowerCase();
+ const command = shell === "cmd.exe" ? "cd"
+ : /^(powershell|pwsh)(\.exe)?$/.test(shell) ? "(Get-Location).Path" : "pwd";
+ await instance.firstDataReceived;
+ await termModule.getNodeConnector().execPeer("writeTerminal", {id: instance.id, data: command + "\r"});
+ await awaitsFor(function () {
+ const buffer = instance.terminal.buffer.active;
+ let text = "";
+ for (let i = 0; i < buffer.length; i++) {
+ text += buffer.getLine(i).translateToString();
+ }
+ return text.includes(path);
+ }, "shell to report the new project directory", 10000);
+ }
+
+ /**
+ * Report a busy terminal while keeping real PTY creation, input and disposal.
+ * @return {jasmine.Spy} Connector spy for checking restart calls.
+ */
+ function reportActiveProcess() {
+ const connector = termModule.getNodeConnector();
+ const execPeer = connector.execPeer.bind(connector);
+ return spyOn(connector, "execPeer").and.callFake(function (method, params) {
+ if (method === "getTerminalProcess") {
+ return Promise.resolve({process: "test-running-task"});
+ }
+ return execPeer(method, params);
+ });
+ }
+
+ it("does not show a banner when no terminals exist", async function () {
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ const instance = await openReadyTerminal();
+ expect(instance.cwd).toBe(getNativeProjectPath());
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ }, 30000);
+
+ it("keeps sessions and dismisses the notice until the next project switch", async function () {
+ const instance = await openReadyTerminal();
+ await writeToTerminal("cd ..\r");
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ expect(testWindow.$(".terminal-project-banner").is(":visible")).toBeTrue();
+ expect(testWindow.$(".terminal-project-path").text())
+ .toBe(StringUtils.format(Strings.TERMINAL_PROJECT_RESTART_PATH, getNativeProjectPath()));
+ testWindow.$(".terminal-project-keep").click();
+
+ const panel = WorkspaceManager.getPanelForID(PANEL_ID);
+ panel.hide();
+ panel.show();
+ testWindow.brackets.test.ProjectManager.trigger("projectOpen");
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ expect(termModule._getActiveTerminal()).toBe(instance);
+ expect(instance.isAlive).toBeTrue();
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ expect(testWindow.$(".terminal-project-banner").is(":visible")).toBeTrue();
+ }, 30000);
+
+ it("clears the banner when returning to the original project without restarting", async function () {
+ const instance = await openReadyTerminal();
+ await writeToTerminal("cd ..\r");
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ expect(testWindow.$(".terminal-project-banner").is(":visible")).toBeTrue();
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ expect(termModule._getActiveTerminal()).toBe(instance);
+ expect(instance.isAlive).toBeTrue();
+ }, 30000);
+
+ it("keeps the banner while tabs from another project remain", async function () {
+ const first = await openReadyTerminal();
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ await __PR.execCommand(termModule.CMD_NEW_TERMINAL);
+ const second = termModule._getActiveTerminal();
+ await second.firstDataReceived;
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ expect(testWindow.$(".terminal-project-banner").is(":visible")).toBeTrue();
+ testWindow.$('.terminal-flyout-item[data-terminal-id="' + second.id + '"] .terminal-flyout-close')
+ .click();
+ await awaitsFor(function () {
+ return second._disposed && getTerminalCount() === 1;
+ }, "the other project's terminal to close", 10000);
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ expect(termModule._getActiveTerminal()).toBe(first);
+ expect(first.isAlive).toBeTrue();
+ }, 30000);
+
+ it("restarts every tab in the new project and preserves its shell and selection", async function () {
+ const first = await openReadyTerminal();
+ const ShellProfiles = testWindow.brackets.getModule("extensionsIntegrated/Terminal/ShellProfiles");
+ // Distinct profiles using an installed shell keep this portable to machines with only one shell.
+ const profileSpy = spyOn(ShellProfiles, "getDefaultShell").and.returnValue(
+ Object.assign({}, first.shellProfile, {name: "Secondary test shell"})
+ );
+ await __PR.execCommand(termModule.CMD_NEW_TERMINAL);
+ profileSpy.and.callThrough();
+ const second = termModule._getActiveTerminal();
+ await second.firstDataReceived;
+ testWindow.$('.terminal-flyout-item[data-terminal-id="' + first.id + '"]').click();
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ const path = getNativeProjectPath();
+ testWindow.$(".terminal-project-restart").click();
+ await awaitsFor(function () {
+ const active = termModule._getActiveTerminal();
+ return getTerminalCount() === 2 && active && active.isAlive && active.id !== first.id
+ && testWindow.$(".terminal-flyout-item.active").index() === 0;
+ }, "both terminals to restart and the first tab to remain selected", 15000);
+
+ expect(first._disposed).toBeTrue();
+ expect(second._disposed).toBeTrue();
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ const replacements = testWindow.$(".terminal-flyout-item").map(function () {
+ return testWindow.$(this).attr("data-terminal-id");
+ }).get();
+ const originals = [first, second];
+ for (let i = 0; i < replacements.length; i++) {
+ testWindow.$('.terminal-flyout-item[data-terminal-id="' + replacements[i] + '"]').click();
+ const instance = termModule._getActiveTerminal();
+ expect(instance.id).not.toBe(originals[i].id);
+ expect(instance.shellProfile).toEqual(originals[i].shellProfile);
+ expect(instance.cwd).toBe(path);
+ await expectWorkingDirectory(instance, path);
+ }
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ expect(testWindow.$(".terminal-project-banner").is(":visible")).toBeTrue();
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ }, 30000);
+
+ it("confirms active processes and leaves sessions untouched when canceled", async function () {
+ const instance = await openReadyTerminal();
+ const connectorSpy = reportActiveProcess();
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ testWindow.$(".terminal-project-restart").click();
+ await __PR.waitForModalDialog();
+ expect(getDialogTitle()).toBe(Strings.TERMINAL_RESTART_CONFIRM_TITLE);
+ __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_CANCEL);
+ await __PR.waitForModalDialogClosed();
+ await awaitsFor(function () {
+ return !testWindow.$(".terminal-project-restart").prop("disabled");
+ }, "restart action to be available again", 3000);
+ expect(termModule._getActiveTerminal()).toBe(instance);
+ expect(instance.isAlive).toBeTrue();
+ expect(connectorSpy.calls.allArgs().some(args => args[0] === "killTerminal")).toBeFalse();
+
+ testWindow.$(".terminal-project-restart").click();
+ await __PR.waitForModalDialog();
+ __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_OK);
+ await __PR.waitForModalDialogClosed();
+ await awaitsFor(function () {
+ const active = termModule._getActiveTerminal();
+ return active && active.id !== instance.id && active.isAlive;
+ }, "confirmed restart to replace the terminal", 10000);
+ expect(instance._disposed).toBeTrue();
+ expect(termModule._getActiveTerminal().cwd).toBe(getNativeProjectPath());
+ }, 30000);
+
+ it("does not restart into a stale project if the project changes during confirmation", async function () {
+ const instance = await openReadyTerminal();
+ const connectorSpy = reportActiveProcess();
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ testWindow.$(".terminal-project-restart").click();
+ await __PR.waitForModalDialog();
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_OK);
+ await __PR.waitForModalDialogClosed();
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ expect(termModule._getActiveTerminal()).toBe(instance);
+ expect(connectorSpy.calls.allArgs().some(args => args[0] === "killTerminal")).toBeFalse();
+ }, 30000);
+
+ it("retains the notice while hidden and removes it when all terminals close", async function () {
+ await openReadyTerminal();
+ const panel = WorkspaceManager.getPanelForID(PANEL_ID);
+ panel.hide();
+ await SpecRunnerUtils.loadProjectInTestWindow(secondProjectPath);
+ expect(panel.isVisible()).toBeFalse();
+ panel.show();
+ expect(testWindow.$(".terminal-project-banner").is(":visible")).toBeTrue();
+ await termModule._disposeAll();
+ expect(testWindow.$(".terminal-project-banner").length).toBe(0);
+ }, 30000);
+ });
+
+ it("should forward Alt+Up to the terminal instead of a Phoenix command", async function () {
+ const termModule = testWindow.brackets.getModule("extensionsIntegrated/Terminal/main");
+ let inputListener;
+ try {
+ await openTerminal();
+ await waitForShellReady();
+ const instance = termModule._getActiveTerminal();
+ const KeyBindingManager = testWindow.brackets.getModule("command/KeyBindingManager");
+ const binding = KeyBindingManager.getKeymap()["Alt-Up"];
+ const command = testWindow.brackets.test.CommandManager.get(binding.commandID);
+ const execute = spyOn(command, "execute")
+ .and.returnValue(testWindow.$.Deferred().resolve().promise());
+ const input = [];
+ inputListener = instance.terminal.onData(function (data) { input.push(data); });
+ instance.focus();
+ await awaitsFor(function () {
+ return testWindow.document.activeElement === instance.terminal.textarea;
+ }, "terminal input to have focus", 3000);
+
+ const key = {key: "ArrowUp", code: "ArrowUp", keyCode: 38, which: 38,
+ altKey: true, bubbles: true, cancelable: true};
+ instance.terminal.textarea.dispatchEvent(new testWindow.KeyboardEvent("keydown", key));
+ instance.terminal.textarea.dispatchEvent(new testWindow.KeyboardEvent("keyup", key));
+ await awaitsFor(function () {
+ return input.includes("\x1b[1;3A");
+ }, "Alt+Up escape sequence to reach the terminal", 3000);
+ expect(execute).not.toHaveBeenCalled();
+ } finally {
+ if (inputListener) {
+ inputListener.dispose();
+ }
+ await termModule._disposeAll();
+ WorkspaceManager.getPanelForID(PANEL_ID).hide();
+ }
+ });
+
describe("Context menu commands", function () {
let CommandManager;
@@ -632,6 +903,43 @@ define(function (require, exports, module) {
testWindow.brackets.test.CommandManager;
});
+ it("should open Copy, Paste and Clear Terminal on right-click",
+ async function () {
+ const Menus = testWindow.brackets.test.Menus;
+ const ctxMenu = Menus.getContextMenu("terminal-context-menu");
+ const termModule = testWindow.brackets.getModule("extensionsIntegrated/Terminal/main");
+ const commandStates = ["terminal.copy", "terminal.paste", "terminal.clear"].map(function (id) {
+ const command = CommandManager.get(id);
+ return {command, enabled: command.getEnabled()};
+ });
+ try {
+ await openTerminal();
+ await waitForShellReady();
+ const active = getActiveTerminal();
+ active.terminal.clearSelection();
+ active.$container.find(".xterm-screen").trigger(testWindow.$.Event("contextmenu", {
+ pageX: 100,
+ pageY: 100
+ }));
+ await awaitsFor(function () {
+ return testWindow.$("#terminal-context-menu.open > .dropdown-menu").is(":visible");
+ }, "terminal context menu to open on right-click", 3000);
+
+ const labels = testWindow.$("#terminal-context-menu .menu-name").map(function () {
+ return testWindow.$(this).text();
+ }).get();
+ expect(labels).toEqual([Strings.CMD_COPY, Strings.CMD_PASTE, Strings.TERMINAL_CLEAR]);
+ expect(CommandManager.get("terminal.copy").getEnabled()).toBeFalse();
+ expect(CommandManager.get("terminal.paste").getEnabled()).toBeTrue();
+ expect(CommandManager.get("terminal.clear").getEnabled()).toBeTrue();
+ } finally {
+ ctxMenu.close();
+ commandStates.forEach(function (state) { state.command.setEnabled(state.enabled); });
+ await termModule._disposeAll();
+ WorkspaceManager.getPanelForID(PANEL_ID).hide();
+ }
+ });
+
it("should clear the terminal screen",
async function () {
await openTerminal();
diff --git a/tracking-repos.json b/tracking-repos.json
index d2a173a22c..ed1a8bc1b9 100644
--- a/tracking-repos.json
+++ b/tracking-repos.json
@@ -1,5 +1,5 @@
{
"phoenixPro": {
- "commitID": "ac067fcd943e145b0ba3f92782678f661371b9a1"
+ "commitID": "9e1ee1d4f8e5eb181a63329977cabebb3caec4d9"
}
}