Skip to content

Commit ab01a7e

Browse files
committed
feat(terminal): name the tabs, and let the user rename them
Every tab carried the same terminal icon, which told them apart not at all: open three shells and the strip showed three identical glyphs. The name each tab already knew was hidden in the narrow strip, and shown only when the panel was wide enough. So the icon gives up its slot — the strip widens a little and always shows the name. The name is the running process, which says nothing while several shells sit idle at a prompt, all reading "zsh". A pencil on hover lets the user name a tab themselves; the name shows in italics so it reads as theirs, and emptying the field hands the tab back to the process label. Names are kept globally, so one means the same thing wherever the user is working, with no project switching to reason about. They are stored as a list in tab order: terminal ids are handed out afresh on every run and again on every restart, so nothing identifies a tab across them, while a list is exactly what says how many tabs to bring back and what to call them. Opening an empty panel restores the lot at once — the panel is usually shut when a window starts, and bringing tabs back one press of + at a time would not be bringing them back at all. Restarting the terminals for a new project carries the names onto the replacements, and closing a named tab drops it from the list, which is how the user says not to bring it back. Nothing but the tabs and their names returns: the shells are new, and with nothing named the panel opens the single terminal it always did. Three details the implementation turns on. The pencil's click is bound to the pencil rather than delegated, so it runs before the row's own handler and can stop it there: let it through and the row activates the terminal, which takes focus straight back off the field — the same reason a double click could never work. A refresh will not rebuild the rows while a field is open, because emptying the list tears the field out, the removal fires blur, and blur commits, landing a half-typed name nobody confirmed. And the tick that finishes an edit commits on mousedown, because a click would let the field blur and finish first, taking the button away between the press and the release. Hovering trades the cwd for the controls rather than reserving room for them, which leaves the name more space, not less, and spares every future control the margin arithmetic that went stale the moment a second one appeared. The field leaves its background and text colour to the theme so it reads like every other input, and sets all four margins, the theme's own bottom margin for form inputs having pushed it off the row's centre.
1 parent bf993f4 commit ab01a7e

4 files changed

Lines changed: 246 additions & 40 deletions

File tree

‎src/extensionsIntegrated/Terminal/main.js‎

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,6 @@ define(function (require, exports, module) {
356356
const instance = new TerminalInstance(nodeConnector, shell, cwd);
357357
// Project ownership is independent of directories the user visits in the shell.
358358
instance.projectPath = projectPath || (projectRoot ? projectRoot.fullPath : null);
359-
// a name given to this slot in an earlier run comes back with it
360-
instance.customName = _loadTabNames()[instance.id] || null;
361359

362360
// Set up callbacks
363361
instance.onTitleChanged = _onTerminalTitleChanged;
@@ -490,15 +488,26 @@ define(function (require, exports, module) {
490488
if (!currentRoot || currentRoot.fullPath !== path) {
491489
return;
492490
}
493-
const profiles = terminalInstances.map(inst => inst.shellProfile);
491+
// A restart replaces every instance, and the new ones are issued
492+
// fresh ids, so a name looked up by id would not find them. Carry
493+
// the names across with the shells: the tabs are the same tabs to
494+
// the user, only pointed at the new project.
495+
const carried = terminalInstances.map(inst => ({
496+
profile: inst.shellProfile,
497+
customName: inst.customName
498+
}));
494499
const activeIndex = terminalInstances.findIndex(inst => inst.id === activeTerminalId);
495500
await _disposeAllAsync();
496501
activeTerminalId = null;
497502
_updateFlyout();
498503
const replacements = [];
499-
for (const profile of profiles) {
500-
replacements.push(await _createNewTerminalWithShell(profile, path, path));
504+
for (const item of carried) {
505+
const replacement = await _createNewTerminalWithShell(item.profile, path, path);
506+
replacement.customName = item.customName;
507+
replacements.push(replacement);
501508
}
509+
// the names were set after each tab rendered, so draw them again
510+
_updateFlyout();
502511
if (replacements[activeIndex]) {
503512
_activateTerminal(replacements[activeIndex].id);
504513
}
@@ -573,6 +582,8 @@ define(function (require, exports, module) {
573582
instance.dispose();
574583
terminalInstances.splice(idx, 1);
575584
delete processInfo[id];
585+
// closing a named tab is how the user says not to bring it back
586+
_saveTabNames();
576587
if ($contentArea.find(".terminal-project-banner").length) {
577588
_showProjectBanner();
578589
}
@@ -699,38 +710,47 @@ define(function (require, exports, module) {
699710
}
700711

701712
/**
702-
* Names the user has given terminal tabs, kept globally rather than per
703-
* project so a name means the same thing wherever they are working.
713+
* The names the user has given terminal tabs, in strip order, kept globally
714+
* rather than per project so a name means the same thing wherever they are
715+
* working.
704716
*
705-
* Keyed by terminal id, which counts up from term_1 in creation order and
706-
* starts over each run: a name therefore belongs to a tab's place in the
707-
* strip rather than to one shell, and the second terminal opened next run
708-
* gets the name the second terminal had. That is as close to "remember my
709-
* terminals" as this can get until the panel restores a session at all —
710-
* it recreates nothing on boot today, so there is no terminal to hand a
711-
* name back to. Names are kept when a tab closes for the same reason:
712-
* dropping them would leave nothing to remember by the next run.
717+
* A list rather than a map off terminal ids: ids are handed out afresh each
718+
* run and again on every restart, so nothing identifies a shell across
719+
* them. What the user is naming is the tab, and the list is what says how
720+
* many tabs to bring back and what to call them.
713721
*
714-
* @return {Object} id -> name, empty when nothing has been named
722+
* @return {Array<string>} names in order, empty when nothing has been named
715723
*/
716724
function _loadTabNames() {
717725
const stored = StateManager.get(STATE_TAB_NAMES);
718-
return (stored && typeof stored === "object") ? stored : {};
726+
if (Array.isArray(stored)) {
727+
return stored.filter(function (n) { return typeof n === "string" && n; });
728+
}
729+
// an earlier build kept these against terminal ids; order them by the
730+
// number in the id rather than the text of it, or term_10 sorts before
731+
// term_2 and the tabs come back shuffled
732+
if (stored && typeof stored === "object") {
733+
const idOrder = function (k) {
734+
const digits = /(\d+)/.exec(k);
735+
return digits ? parseInt(digits[1], 10) : 0;
736+
};
737+
return Object.keys(stored)
738+
.sort(function (a, b) { return idOrder(a) - idOrder(b); })
739+
.map(function (k) { return stored[k]; })
740+
.filter(Boolean);
741+
}
742+
return [];
719743
}
720744

721745
/**
722-
* Record or forget one tab's name.
723-
* @param {string} id - terminal id
724-
* @param {?string} name - the name, or null to go back to the process label
746+
* Write the current tabs' names back, so the set that returns next run is
747+
* the set on screen now. Unnamed tabs are left out: naming one is what asks
748+
* for it to come back.
725749
*/
726-
function _saveTabName(id, name) {
727-
const names = _loadTabNames();
728-
if (name) {
729-
names[id] = name;
730-
} else {
731-
delete names[id];
732-
}
733-
StateManager.set(STATE_TAB_NAMES, names);
750+
function _saveTabNames() {
751+
StateManager.set(STATE_TAB_NAMES, terminalInstances
752+
.map(function (inst) { return inst.customName; })
753+
.filter(Boolean));
734754
}
735755

736756
/**
@@ -756,9 +776,12 @@ define(function (require, exports, module) {
756776
}
757777
const $input = $('<input type="text" class="terminal-flyout-rename">')
758778
.val(inst.customName || $title.text().trim());
779+
const $done = $('<span class="terminal-flyout-rename-done" title="' +
780+
Strings.TERMINAL_RENAME_DONE + '"><i class="fa-solid fa-check"></i></span>');
759781
$item.addClass("renaming");
760782
renamingTerminalId = inst.id;
761783
$title.hide().after($input);
784+
$input.after($done);
762785
$input.trigger("focus").trigger("select");
763786

764787
let settled = false;
@@ -770,11 +793,12 @@ define(function (require, exports, module) {
770793
const name = $input.val().trim();
771794
// an emptied field means "go back to naming it after the process"
772795
inst.customName = name || null;
773-
_saveTabName(inst.id, inst.customName);
796+
_saveTabNames();
774797
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "rename",
775798
name ? "set" : "clear");
776799
}
777800
$input.remove();
801+
$done.remove();
778802
$title.show();
779803
$item.removeClass("renaming");
780804
_updateFlyout();
@@ -788,6 +812,15 @@ define(function (require, exports, module) {
788812
$input.on("blur", function () { finish(true); });
789813
// a double click inside the field shouldn't re-enter renaming
790814
$input.on("dblclick click", function (e) { e.stopPropagation(); });
815+
// Committed on mousedown, before the field can lose focus: letting the
816+
// blur land first would finish the edit and take this button away
817+
// between press and release, so the click would never arrive.
818+
$done.on("mousedown", function (e) {
819+
e.preventDefault();
820+
e.stopPropagation();
821+
finish(true);
822+
});
823+
$done.on("click", function (e) { e.stopPropagation(); });
791824
}
792825

793826
function _updateFlyout() {
@@ -878,6 +911,39 @@ define(function (require, exports, module) {
878911
* @param {string} [options.shellCommand] - A shell command to execute in a new terminal.
879912
* When provided, always creates a fresh terminal and types the command into it.
880913
*/
914+
/**
915+
* Open the tabs the user named last time, all of them, in the order they
916+
* were in.
917+
*
918+
* The panel is often shut when a window starts, so this runs the first time
919+
* it opens with nothing in it. Bringing the tabs back one at a time as the
920+
* user presses + would not be bringing them back at all — the point of
921+
* naming a terminal is that it is waiting where it was left.
922+
*
923+
* Shells are new: nothing of what ran in them is restored, only the tabs
924+
* and what they are called. With nothing saved this is the plain single
925+
* terminal the panel has always opened with.
926+
*/
927+
async function _restoreSavedTerminals() {
928+
const names = _loadTabNames();
929+
if (!names.length) {
930+
await _createNewTerminal();
931+
return;
932+
}
933+
for (const name of names) {
934+
await _createNewTerminal();
935+
const restored = terminalInstances[terminalInstances.length - 1];
936+
if (restored) {
937+
restored.customName = name;
938+
}
939+
}
940+
// each tab rendered before its name was put back
941+
_updateFlyout();
942+
if (terminalInstances.length) {
943+
_activateTerminal(terminalInstances[0].id);
944+
}
945+
}
946+
881947
async function _showTerminal(options) {
882948
if (options && options.shellCommand) {
883949
await _createNewTerminal();
@@ -893,7 +959,7 @@ define(function (require, exports, module) {
893959
return;
894960
}
895961
if (terminalInstances.length === 0) {
896-
await _createNewTerminal();
962+
await _restoreSavedTerminals();
897963
return;
898964
}
899965
const active = _getActiveTerminal();

‎src/nls/root/strings.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,6 +2229,7 @@ define({
22292229
"ERROR_SAVE_FIRST": "Save the document first!",
22302230
"ERROR_TERMINAL_NOT_FOUND": "Terminal was not found for your OS, you can define a custom Terminal command in the settings",
22312231
"TERMINAL_RENAME_TAB": "Rename this terminal",
2232+
"TERMINAL_RENAME_DONE": "Done",
22322233
"TERMINAL_CLOSE_CONFIRM_TITLE": "Active Process Running",
22332234
"TERMINAL_CLOSE_CONFIRM_MSG": "Terminal has an active process running: <b>{0}</b>.<br>Are you sure you want to close it?",
22342235
"TERMINAL_CLOSE_SINGLE_TITLE": "Close Terminal?",

‎src/styles/Extn-Terminal.less‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,16 @@ input[type="text"].terminal-flyout-rename {
291291
flex: 1;
292292
min-width: 0;
293293
height: 20px;
294-
margin-left: 6px;
295-
margin-right: 18px;
294+
/* all four sides, because the theme gives form inputs a bottom margin of
295+
their own and that pushes the field off the row's centre line */
296+
margin: 0 22px 0 6px;
296297
padding: 0 4px;
297298
font-size: 12px;
298299
line-height: 18px;
299-
color: var(--terminal-tab-active-text);
300-
background: rgba(0, 0, 0, 0.35);
300+
/* background and text colour are left to the theme's own input styling, so
301+
this field reads like every other one — white on a light theme rather
302+
than a dark patch on a light strip. Only the border is ours, to mark the
303+
row as being edited. */
301304
border: 1px solid #007acc;
302305
border-radius: 2px;
303306
outline: none;
@@ -310,6 +313,28 @@ input[type="text"].terminal-flyout-rename {
310313
display: none;
311314
}
312315

316+
/* Confirm button, in the slot the pencil and close vacate while editing */
317+
.terminal-flyout-rename-done {
318+
position: absolute;
319+
right: 0;
320+
top: 0;
321+
bottom: 0;
322+
width: 20px;
323+
display: flex;
324+
align-items: center;
325+
justify-content: center;
326+
font-size: 10px;
327+
color: var(--terminal-tab-text);
328+
background: transparent;
329+
z-index: 2;
330+
cursor: pointer;
331+
}
332+
333+
.terminal-flyout-rename-done:hover {
334+
color: var(--terminal-tab-active-text);
335+
background: rgba(255, 255, 255, 0.1);
336+
}
337+
313338

314339
/* ─── Flyout bottom actions ─── */
315340
.terminal-flyout-actions {

0 commit comments

Comments
 (0)