Skip to content

Commit 7853ef5

Browse files
committed
feat(editor): show outline boxes around active snippet tab-stops
TabstopManager now draws a subdued box around every remaining tab-stop in an active session, with a bolder box layered on top of whichever stop is currently selected - matching the visual language RenameIdentifier.js already uses for its rename boxes. Benefits every consumer of the shared tab-stop engine (LSP completions, DocComment hints, Custom Snippets), not just one feature. Uses a new @bc-editor-decoration-neutral variable (no light/dark split) for the subdued box, since it decorates the code editor canvas whose background depends on the user's code theme, not the app's light/dark UI mode - same reasoning as .cm-matchhighlight. The active box reuses the existing @bc-primary-btn-border/@dark-bc-primary-btn-border pair split by .dark& for consistency with rename's own accent color. Adds unit coverage for marker class assignment, active-marker swap on navigation, zero-width stops correctly getting no box, and both markers clearing on session end.
1 parent 2bb0ea6 commit 7853ef5

5 files changed

Lines changed: 181 additions & 6 deletions

File tree

src/editor/Editor.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,37 @@ define(function (require, exports, module) {
13961396
};
13971397
}
13981398

1399+
/**
1400+
* Mark option for a subdued outline box, used to show every remaining stop of an active
1401+
* snippet/tab-stop session (see editor/TabstopManager.js) so the user can see at a glance how
1402+
* many fields are left and where, even for the ones they haven't tabbed to yet.
1403+
*/
1404+
function getMarkOptionTabstopOutline() {
1405+
return {
1406+
className: "editor-text-tabstop-outline",
1407+
startStyle: "editor-text-tabstop-outline-left",
1408+
endStyle: "editor-text-tabstop-outline-right",
1409+
clearWhenEmpty: false,
1410+
inclusiveLeft: true,
1411+
inclusiveRight: true
1412+
};
1413+
}
1414+
1415+
/**
1416+
* Mark option for the bold/active variant of the above, layered on top of it for whichever stop
1417+
* is currently selected in an active snippet/tab-stop session.
1418+
*/
1419+
function getMarkOptionTabstopOutlineActive() {
1420+
return {
1421+
className: "editor-text-tabstop-outline-active",
1422+
startStyle: "editor-text-tabstop-outline-active-left",
1423+
endStyle: "editor-text-tabstop-outline-active-right",
1424+
clearWhenEmpty: false,
1425+
inclusiveLeft: true,
1426+
inclusiveRight: true
1427+
};
1428+
}
1429+
13991430
/**
14001431
* Mark option to underline errors.
14011432
*/
@@ -1430,6 +1461,8 @@ define(function (require, exports, module) {
14301461
* Mark option for renaming outlines.
14311462
*/
14321463
Editor.getMarkOptionRenameOutline = getMarkOptionRenameOutline;
1464+
Editor.getMarkOptionTabstopOutline = getMarkOptionTabstopOutline;
1465+
Editor.getMarkOptionTabstopOutlineActive = getMarkOptionTabstopOutlineActive;
14331466

14341467
/**
14351468
* Can be used to mark a range of text with a specific CSS class name. cursorFrom and cursorTo should be {line, ch}

src/editor/TabstopManager.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,19 @@
3535
* stop, and (when there is more than one stop) starts a Tab-navigable session backed by markers
3636
* so the stops follow any later edits (e.g. an auto-import line inserted above).
3737
*
38-
* NOTE: this is currently wired only into the LSP completion path (languageTools/DefaultProviders).
39-
* The Emmet expander (HTMLCodeHints) and the custom-snippets feature have their own stable cursor
40-
* handling and were intentionally left untouched; they can migrate onto this manager in future.
38+
* Used by the LSP completion path (languageTools/DefaultProviders), DocCommentHints, and Custom
39+
* Snippets (extensionsIntegrated/CustomSnippets/snippetCursorManager.js). The Emmet expander
40+
* (HTMLCodeHints) still has its own separate cursor handling.
41+
*
42+
* While a Tab-navigable session is active, every remaining stop gets a subdued outline box (see
43+
* Editor.getMarkOptionTabstopOutline) so the user can see at a glance how many fields are left and
44+
* where, with the currently-selected one getting a bolder "active" outline layered on top (see
45+
* Editor.getMarkOptionTabstopOutlineActive) - matches the visual language RenameIdentifier.js already
46+
* uses for its own outline box. Zero-width stops (a bare `$N`/`$0` with no default text - just a
47+
* caret position, no marker range) don't get an outline, since there's no span to box.
4148
*/
4249
define(function (require, exports, module) {
50+
const Editor = require("editor/Editor").Editor;
4351

4452
/**
4553
* Expand an LSP snippet into plain text plus the list of tab-stops.
@@ -174,7 +182,7 @@ define(function (require, exports, module) {
174182

175183
// ---- Tab-navigation session ----------------------------------------------------------------
176184

177-
var _session = null; // { editor, markers: [marker], index, keymap }
185+
var _session = null; // { editor, markers: [marker], index, keymap, activeOutlineMarker }
178186

179187
function _clearSession() {
180188
if (!_session) {
@@ -185,6 +193,9 @@ define(function (require, exports, module) {
185193
session.markers.forEach(function (m) {
186194
m.clear();
187195
});
196+
if (session.activeOutlineMarker) {
197+
session.activeOutlineMarker.clear();
198+
}
188199
session.editor._codeMirror.removeKeyMap(session.keymap);
189200
session.editor.off(".tabstop");
190201
}
@@ -251,6 +262,17 @@ define(function (require, exports, module) {
251262
}
252263
_session.index = index;
253264
_session.editor.setSelection(range.from, range.to);
265+
266+
// swap the bold "active" outline onto whichever stop we just landed on - only meaningful for
267+
// a real span (a bare $N/$0 with no default text is a zero-width caret, nothing to box)
268+
if (_session.activeOutlineMarker) {
269+
_session.activeOutlineMarker.clear();
270+
_session.activeOutlineMarker = null;
271+
}
272+
if (range.from.line !== range.to.line || range.from.ch !== range.to.ch) {
273+
_session.activeOutlineMarker = _session.editor.markText(
274+
"tabstop-active", range.from, range.to, Editor.getMarkOptionTabstopOutlineActive());
275+
}
254276
return true;
255277
}
256278

@@ -332,6 +354,11 @@ define(function (require, exports, module) {
332354
}
333355

334356
// Multiple stops: lay down markers and start a Tab-navigable session.
357+
// the subdued outline (visual only) is layered onto the SAME functional tracking options
358+
// below by className/startStyle/endStyle alone - deliberately not spreading the whole helper
359+
// object in, since its own inclusiveLeft/clearWhenEmpty differ from what marker TRACKING here
360+
// actually needs (inclusiveLeft: false is what makes typing at a stop's start not stick to it).
361+
var outlineOption = Editor.getMarkOptionTabstopOutline();
335362
var markers = parsed.stops.map(function (stop) {
336363
var ms = posFromOffset(stop.start),
337364
me = posFromOffset(stop.end);
@@ -341,7 +368,10 @@ define(function (require, exports, module) {
341368
return editor.markText("tabstop", ms, me, {
342369
clearWhenEmpty: false,
343370
inclusiveLeft: false,
344-
inclusiveRight: true
371+
inclusiveRight: true,
372+
className: outlineOption.className,
373+
startStyle: outlineOption.startStyle,
374+
endStyle: outlineOption.endStyle
345375
});
346376
});
347377

@@ -358,7 +388,7 @@ define(function (require, exports, module) {
358388
}
359389
};
360390

361-
_session = { editor: editor, markers: markers, index: -1, keymap: keymap };
391+
_session = { editor: editor, markers: markers, index: -1, keymap: keymap, activeOutlineMarker: null };
362392
editor._codeMirror.addKeyMap(keymap);
363393
// End the session if the editor it belongs to is destroyed (file closed), or the user moves
364394
// on (cursor leaves the snippet's lines, or a multi-cursor selection is made). Namespaced so

src/styles/brackets.less

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,58 @@ html, body {
190190
box-shadow: inset 1px 0 0 0 @bc-primary-btn-border, inset -1px 0 0 0 @bc-primary-btn-border; /* Left, right shadow */
191191
}
192192

193+
// Subdued outline shown for every remaining stop of an active snippet/tab-stop session (see
194+
// editor/TabstopManager.js) - a neutral border color, distinct from the bold "active" variant below.
195+
// Uses @bc-editor-decoration-neutral (no .dark & split - see its own definition for why editor-canvas
196+
// decorations need one universal mid-tone value instead of a light/dark UI-chrome color pair).
197+
.editor-text-tabstop-outline {
198+
border-top: 1px @bc-editor-decoration-neutral solid;
199+
border-bottom: 1px @bc-editor-decoration-neutral solid;
200+
}
201+
.editor-text-tabstop-outline-left {
202+
box-shadow: inset 1px 0 0 0 @bc-editor-decoration-neutral; /* Left shadow */
203+
}
204+
.editor-text-tabstop-outline-right {
205+
box-shadow: inset -1px 0 0 0 @bc-editor-decoration-neutral; /* right shadow */
206+
}
207+
.editor-text-tabstop-outline-left.editor-text-tabstop-outline-right {
208+
box-shadow: inset 1px 0 0 0 @bc-editor-decoration-neutral, inset -1px 0 0 0 @bc-editor-decoration-neutral; /* Left, right shadow */
209+
}
210+
211+
// Bold/active outline for whichever stop is currently selected, layered on top of the subdued one
212+
// above (same accent color rename-outline uses, for a consistent "you're actively editing here"
213+
// visual language across features).
214+
.editor-text-tabstop-outline-active {
215+
border-top: 1px @bc-primary-btn-border solid;
216+
border-bottom: 1px @bc-primary-btn-border solid;
217+
218+
.dark & {
219+
border-top: 1px @dark-bc-primary-btn-border solid;
220+
border-bottom: 1px @dark-bc-primary-btn-border solid;
221+
}
222+
}
223+
.editor-text-tabstop-outline-active-left {
224+
box-shadow: inset 1px 0 0 0 @bc-primary-btn-border;
225+
226+
.dark & {
227+
box-shadow: inset 1px 0 0 0 @dark-bc-primary-btn-border;
228+
}
229+
}
230+
.editor-text-tabstop-outline-active-right {
231+
box-shadow: inset -1px 0 0 0 @bc-primary-btn-border;
232+
233+
.dark & {
234+
box-shadow: inset -1px 0 0 0 @dark-bc-primary-btn-border;
235+
}
236+
}
237+
.editor-text-tabstop-outline-active-left.editor-text-tabstop-outline-active-right {
238+
box-shadow: inset 1px 0 0 0 @bc-primary-btn-border, inset -1px 0 0 0 @bc-primary-btn-border;
239+
240+
.dark & {
241+
box-shadow: inset 1px 0 0 0 @dark-bc-primary-btn-border, inset -1px 0 0 0 @dark-bc-primary-btn-border;
242+
}
243+
}
244+
193245
// error class has highest visual precedence, followed by warning, spell error and info.
194246
// .error.error: This selector has 4x the class name, which increases its specificity compared to single class
195247
// selectors like .warning or .info. Even if error, warning, and info are used on the same div in any order,

src/styles/brackets_core_ui_variables.less

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@
6565
@bc-error: #f74687;
6666
@bc-modal-backdrop-opacity: 0.4;
6767
@bc-spinner: #78b2f2;
68+
// Deliberately NOT theme-split (no @dark- pair): this decorates the EDITOR CANVAS, whose background
69+
// depends on the user's chosen code theme (not just the app's light/dark UI mode - see
70+
// .cm-matchhighlight in brackets_codemirror_override.less for the same reasoning), so it needs one
71+
// mid-tone value that reads reasonably against both very light and very dark editor backgrounds,
72+
// rather than a UI-chrome color pair tuned only for the app's own near-white/near-black panels.
73+
@bc-editor-decoration-neutral: #808080;
6874

6975
// Highlights and Shadows
7076
@bc-highlight: rgba(255, 255, 255, 0.12);

test/spec/TabstopManager-test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,60 @@ define(function (require, exports, module) {
387387
expect(myEditor.getSelectedText()).toBe("a"); // two backward moves: c -> b -> a
388388
});
389389
});
390+
391+
// Visual boxing while a session is active - see Editor.getMarkOptionTabstopOutline/
392+
// getMarkOptionTabstopOutlineActive and brackets.less .editor-text-tabstop-outline*.
393+
describe("visual outline markers", function () {
394+
function outlineMarks(className) {
395+
return myEditor._codeMirror.getAllMarks().filter(function (m) {
396+
return m.className === className;
397+
});
398+
}
399+
400+
it("should give every range-based stop a subdued outline marker on insertion", function () {
401+
createTestEditor("");
402+
TabstopManager.insertSnippet(myEditor, "${1:a} ${2:b} $0", ORIGIN, ORIGIN);
403+
// 2 range stops (a, b) get the subdued outline; $0 (zero-width, no default) does not
404+
expect(outlineMarks("editor-text-tabstop-outline").length).toBe(2);
405+
});
406+
407+
it("should give only the CURRENTLY selected stop the bold active outline", function () {
408+
createTestEditor("");
409+
TabstopManager.insertSnippet(myEditor, "${1:a} ${2:b} $0", ORIGIN, ORIGIN);
410+
expect(outlineMarks("editor-text-tabstop-outline-active").length).toBe(1);
411+
412+
pressTab();
413+
expect(outlineMarks("editor-text-tabstop-outline-active").length).toBe(1);
414+
});
415+
416+
it("should move the active outline as the user tabs, keeping the subdued ones in place",
417+
function () {
418+
createTestEditor("");
419+
TabstopManager.insertSnippet(myEditor, "${1:a} ${2:b} $0", ORIGIN, ORIGIN);
420+
expect(outlineMarks("editor-text-tabstop-outline-active")[0].find().from.ch).toBe(0); // "a"
421+
422+
pressTab();
423+
expect(outlineMarks("editor-text-tabstop-outline-active")[0].find().from.ch).toBe(2); // "b"
424+
// both subdued outlines (a and b) are still there, untouched
425+
expect(outlineMarks("editor-text-tabstop-outline").length).toBe(2);
426+
});
427+
428+
it("should not give a zero-width stop (no default text) any active outline", function () {
429+
createTestEditor("");
430+
TabstopManager.insertSnippet(myEditor, "${1:a} $2 $0", ORIGIN, ORIGIN);
431+
pressTab(); // -> $2, a bare zero-width stop
432+
expect(myEditor.getSelectedText()).toBe("");
433+
expect(outlineMarks("editor-text-tabstop-outline-active").length).toBe(0);
434+
});
435+
436+
it("should clear all outline markers when the session ends", function () {
437+
createTestEditor("");
438+
TabstopManager.insertSnippet(myEditor, "${1:a} ${2:b} $0", ORIGIN, ORIGIN);
439+
TabstopManager.endSession();
440+
expect(outlineMarks("editor-text-tabstop-outline").length).toBe(0);
441+
expect(outlineMarks("editor-text-tabstop-outline-active").length).toBe(0);
442+
});
443+
});
390444
});
391445
});
392446
});

0 commit comments

Comments
 (0)