Skip to content

Commit 3224f9c

Browse files
authored
Merge pull request #54 from ShortArrow/feat/panel-apply-all
Stage toggle changes and add Apply all to the panel
2 parents da0084d + 9f3350c commit 3224f9c

9 files changed

Lines changed: 219 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ All notable changes to the "ShortArrow.line-number-deco" extension will be docum
6262
- Releases are built by GitHub Actions on tag push: tests on three OSes, a packaged VSIX with build provenance, and an installation smoke test (#29)
6363
- The VSIX no longer ships tests, the generator, or CI files
6464
- Package manager is pnpm instead of Yarn
65-
- A settings panel in the activity bar toggles each decoration and previews colors live, saving per workspace or user (#21)
65+
- A settings panel in the activity bar previews toggles and colors live, with per-row Apply and Apply all per workspace or user (#21)
6666
- List settings and commands in `README.md` (#20), add an `init.lua` example (#31), and trim the recommended plugin list to maintained extensions (#26)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ The `ForUser` variants write to your user settings; the others write to the curr
8686

8787
## Settings panel
8888

89-
The LineNumberDeco entry in the activity bar opens a panel with a switch for each decoration above every color setting with its saved value and a color picker. Flipping a switch turns that decoration on or off straight away, writing to the scope the radio at the top selects. Dragging a picker previews that color in the open editors without saving it; Apply writes it to the workspace or to your user settings, whichever the radio at the top selects. Closing the panel discards any preview that was not applied.
89+
The LineNumberDeco entry in the activity bar opens a panel with a switch for each decoration above every color setting with its saved value and a color picker. Flipping a switch and dragging a picker both preview in the open editors without saving anything, and the row is marked until it is saved. The Apply beside a row saves that one setting; Apply all at the bottom saves everything still pending. Either writes to the workspace or to your user settings, whichever the radio at the top selects. Closing the panel discards every preview that was not applied.
9090

9191
## Calling commands from init.lua
9292

src/config.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from "vscode";
2-
import { getPreviewColor } from "./preview";
2+
import { getPreviewColor, getPreviewToggle } from "./preview";
33

44
export const nameOfExtension = "LineNumberDeco";
55
export const defaultCenterColorOfRainbow = "#8888ff";
@@ -56,11 +56,17 @@ export function getColorAtInactiveRowNumber() {
5656
}
5757

5858
export function getEnableRainbow() {
59-
return getConfig<boolean>("enableRainbow", false);
59+
return (
60+
getPreviewToggle("enableRainbow") ??
61+
getConfig<boolean>("enableRainbow", false)
62+
);
6063
}
6164

6265
export function getEnableRepeatingDigits() {
63-
return getConfig<boolean>("enableRepeatingDigits", false);
66+
return (
67+
getPreviewToggle("enableRepeatingDigits") ??
68+
getConfig<boolean>("enableRepeatingDigits", false)
69+
);
6470
}
6571

6672
export function getColorAtRepeatingDigits() {
@@ -71,7 +77,10 @@ export function getColorAtRepeatingDigits() {
7177
}
7278

7379
export function getEnableSequentialDigits() {
74-
return getConfig<boolean>("enableSequentialDigits", false);
80+
return (
81+
getPreviewToggle("enableSequentialDigits") ??
82+
getConfig<boolean>("enableSequentialDigits", false)
83+
);
7584
}
7685

7786
export function getColorAtSequentialDigits() {
@@ -82,5 +91,8 @@ export function getColorAtSequentialDigits() {
8291
}
8392

8493
export function getEnableRelativeLine() {
85-
return getConfig<boolean>("enableRelativeLine", true);
94+
return (
95+
getPreviewToggle("enableRelativeLine") ??
96+
getConfig<boolean>("enableRelativeLine", true)
97+
);
8698
}

src/panel.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import * as crypto from "crypto";
22
import * as vscode from "vscode";
33
import { getConfig, nameOfExtension } from "./config";
44
import { PanelRow, PanelToggle, renderPanelHtml } from "./panelHtml";
5-
import { clearAllPreviews, clearPreviewColor, setPreviewColor } from "./preview";
5+
import {
6+
clearAllPreviews,
7+
clearPreview,
8+
getPendingPreviews,
9+
setPreviewColor,
10+
setPreviewToggle,
11+
} from "./preview";
612
import { updateUserConfig, updateWorkspaceConfig } from "./ui";
713

814
const viewId = "lineNumberDeco.settings";
@@ -48,7 +54,9 @@ function currentRows(): PanelRow[] {
4854
type PanelMessage =
4955
| { type: "preview"; key: string; value: string }
5056
| { type: "apply"; key: string; value: string; scope: string }
51-
| { type: "toggle"; key: string; value: boolean; scope: string };
57+
| { type: "previewToggle"; key: string; value: boolean }
58+
| { type: "applyToggle"; key: string; value: boolean; scope: string }
59+
| { type: "applyAll"; scope: string };
5260

5361
function isKnownKey(key: string) {
5462
return labels.some((entry) => entry.key === key);
@@ -109,21 +117,44 @@ class ColorPanelProvider implements vscode.WebviewViewProvider {
109117
});
110118
}
111119

120+
/** Save one setting where the scope radio points. */
121+
private async save(key: string, value: string | boolean, scope: string) {
122+
if (scope === "user") {
123+
await updateUserConfig(key, value);
124+
} else {
125+
await updateWorkspaceConfig(key, value);
126+
}
127+
}
128+
112129
private async handle(message: PanelMessage) {
113130
if (!message) {
114131
return;
115132
}
116-
if (message.type === "toggle") {
133+
if (message.type === "applyAll") {
134+
for (const { key, value } of getPendingPreviews()) {
135+
if (isKnownKey(key) || isKnownToggle(key)) {
136+
await this.save(key, value, message.scope);
137+
}
138+
}
139+
clearAllPreviews();
140+
this.refresh();
141+
this.postState();
142+
return;
143+
}
144+
if (message.type === "previewToggle" || message.type === "applyToggle") {
117145
if (!isKnownToggle(message.key)) {
118146
return;
119147
}
120148
const value = message.value === true;
121-
if (message.scope === "user") {
122-
await updateUserConfig(message.key, value);
123-
} else {
124-
await updateWorkspaceConfig(message.key, value);
149+
if (message.type === "previewToggle") {
150+
setPreviewToggle(message.key, value);
151+
this.refresh();
152+
return;
125153
}
154+
clearPreview(message.key);
155+
await this.save(message.key, value, message.scope);
126156
this.refresh();
157+
this.postState();
127158
return;
128159
}
129160
if (!isKnownKey(message.key)) {
@@ -135,12 +166,8 @@ class ColorPanelProvider implements vscode.WebviewViewProvider {
135166
return;
136167
}
137168
if (message.type === "apply") {
138-
clearPreviewColor(message.key);
139-
if (message.scope === "user") {
140-
await updateUserConfig(message.key, message.value);
141-
} else {
142-
await updateWorkspaceConfig(message.key, message.value);
143-
}
169+
clearPreview(message.key);
170+
await this.save(message.key, message.value, message.scope);
144171
this.refresh();
145172
this.postState();
146173
}

src/panelHtml.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,22 @@ function pickerValue(savedColor: string) {
3030

3131
function renderToggle(toggle: PanelToggle) {
3232
const key = escapeHtml(toggle.key);
33-
return ` <label class="toggle">
34-
<span class="label">${escapeHtml(toggle.label)}</span>
35-
<span class="switch">
36-
<input type="checkbox" data-toggle="${key}"${toggle.value ? " checked" : ""} />
37-
<span class="slider"></span>
38-
</span>
39-
</label>`;
33+
return ` <div class="row toggle-row" data-row="${key}">
34+
<label class="toggle">
35+
<span class="label">${escapeHtml(toggle.label)}</span>
36+
<span class="switch">
37+
<input type="checkbox" data-toggle="${key}"${toggle.value ? " checked" : ""} />
38+
<span class="slider"></span>
39+
</span>
40+
</label>
41+
<button data-apply-toggle="${key}">Apply</button>
42+
</div>`;
4043
}
4144

4245
function renderRow(row: PanelRow) {
4346
const key = escapeHtml(row.key);
4447
const saved = escapeHtml(row.savedColor);
45-
return ` <div class="row">
48+
return ` <div class="row" data-row="${key}">
4649
<div class="label">${escapeHtml(row.label)}</div>
4750
<div class="controls">
4851
<span class="swatch" data-swatch="${key}" style="background:${saved}" title="${saved}"></span>
@@ -57,18 +60,25 @@ const script = ` const vscode = acquireVsCodeApi();
5760
const checked = document.querySelector('input[name="scope"]:checked');
5861
return checked ? checked.value : 'workspace';
5962
}
63+
function markPending(key, pending) {
64+
const row = document.querySelector('[data-row="' + key + '"]');
65+
if (row) {
66+
row.classList.toggle('pending', pending);
67+
}
68+
}
6069
document.querySelectorAll('input[type="checkbox"][data-toggle]').forEach((input) => {
6170
input.addEventListener('change', () => {
71+
markPending(input.dataset.toggle, true);
6272
vscode.postMessage({
63-
type: 'toggle',
73+
type: 'previewToggle',
6474
key: input.dataset.toggle,
6575
value: input.checked,
66-
scope: scope(),
6776
});
6877
});
6978
});
7079
document.querySelectorAll('input[type="color"]').forEach((input) => {
7180
input.addEventListener('input', () => {
81+
markPending(input.dataset.key, true);
7282
vscode.postMessage({ type: 'preview', key: input.dataset.key, value: input.value });
7383
});
7484
});
@@ -79,6 +89,23 @@ const script = ` const vscode = acquireVsCodeApi();
7989
vscode.postMessage({ type: 'apply', key: key, value: input.value, scope: scope() });
8090
});
8191
});
92+
document.querySelectorAll('button[data-apply-toggle]').forEach((button) => {
93+
button.addEventListener('click', () => {
94+
const key = button.dataset.applyToggle;
95+
const box = document.querySelector('input[data-toggle="' + key + '"]');
96+
vscode.postMessage({
97+
type: 'applyToggle',
98+
key: key,
99+
value: box.checked,
100+
scope: scope(),
101+
});
102+
});
103+
});
104+
document.querySelectorAll('button[data-apply-all]').forEach((button) => {
105+
button.addEventListener('click', () => {
106+
vscode.postMessage({ type: 'applyAll', scope: scope() });
107+
});
108+
});
82109
window.addEventListener('message', (event) => {
83110
const message = event.data;
84111
if (!message || message.type !== 'state') {
@@ -89,8 +116,10 @@ const script = ` const vscode = acquireVsCodeApi();
89116
if (box) {
90117
box.checked = toggle.value;
91118
}
119+
markPending(toggle.key, false);
92120
});
93121
message.rows.forEach((row) => {
122+
markPending(row.key, false);
94123
const swatch = document.querySelector('[data-swatch="' + row.key + '"]');
95124
if (swatch) {
96125
swatch.style.background = row.savedColor;
@@ -107,6 +136,10 @@ const style = ` body { font-family: var(--vscode-font-family); font-size: v
107136
.scope { display: flex; gap: 12px; margin-bottom: 12px; }
108137
.scope label { display: flex; align-items: center; gap: 4px; }
109138
.row { margin-bottom: 10px; }
139+
.row.pending .label::after { content: " ●"; color: var(--vscode-charts-orange, var(--vscode-button-background)); }
140+
.footer { border-top: 1px solid var(--vscode-panel-border); padding-top: 10px; }
141+
.toggle-row { display: flex; align-items: center; gap: 8px; }
142+
.toggle-row .toggle { flex: 1; margin-bottom: 0; }
110143
.label { margin-bottom: 4px; }
111144
.controls { display: flex; align-items: center; gap: 6px; }
112145
.swatch { width: 18px; height: 18px; border: 1px solid var(--vscode-panel-border); display: inline-block; }
@@ -131,6 +164,10 @@ const style = ` body { font-family: var(--vscode-font-family); font-size: v
131164
* carrying this nonce, and each toggle and row keeps its configuration key in a
132165
* data attribute so the messages back to the extension need no other lookup
133166
* table. The switches come first: they decide whether a color is drawn at all.
167+
*
168+
* Flipping a switch or dragging a picker only previews; a row marks itself
169+
* pending until a state message says its value was saved. The footer button
170+
* commits every pending row at once.
134171
*/
135172
export function renderPanelHtml(
136173
toggles: PanelToggle[],
@@ -163,6 +200,9 @@ ${toggles.map(renderToggle).join("\n")}
163200
<h2>Colors</h2>
164201
${rows.map(renderRow).join("\n")}
165202
</div>
203+
<div class="footer">
204+
<button data-apply-all="1">Apply all</button>
205+
</div>
166206
<script nonce="${safeNonce}">
167207
${script}
168208
</script>

src/preview.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,50 @@
1+
/** One setting a control is currently proposing, before anything is saved. */
2+
export interface PendingPreview {
3+
key: string;
4+
value: string | boolean;
5+
}
6+
17
/**
2-
* Colors a picker is currently proposing, keyed by configuration name.
8+
* Settings the panel is currently proposing, keyed by configuration name.
39
*
4-
* The color panel writes here while one of its pickers is being dragged, and the
5-
* config getters read an override before the configuration itself, so the editors
6-
* render the candidate color without anything being written to settings. Hiding
7-
* the panel clears the overrides and the configured colors take over again.
10+
* The panel writes here while a picker is being dragged or a switch is flipped,
11+
* and the config getters read an override before the configuration itself, so
12+
* the editors render the candidate without anything being written to settings.
13+
* Applying one row, applying all of them, or hiding the panel clears what is
14+
* pending and the configured values take over again.
815
*/
9-
const previews = new Map<string, string>();
16+
const previews = new Map<string, string | boolean>();
1017

1118
export function setPreviewColor(key: string, value: string) {
1219
previews.set(key, value);
1320
}
1421

15-
export function clearPreviewColor(key: string) {
22+
export function setPreviewToggle(key: string, value: boolean) {
23+
previews.set(key, value);
24+
}
25+
26+
export function clearPreview(key: string) {
1627
previews.delete(key);
1728
}
1829

30+
/** The name the color rows have always used for {@link clearPreview}. */
31+
export const clearPreviewColor = clearPreview;
32+
1933
export function clearAllPreviews() {
2034
previews.clear();
2135
}
2236

2337
export function getPreviewColor(key: string) {
24-
return previews.get(key);
38+
const value = previews.get(key);
39+
return typeof value === "string" ? value : undefined;
40+
}
41+
42+
export function getPreviewToggle(key: string) {
43+
const value = previews.get(key);
44+
return typeof value === "boolean" ? value : undefined;
45+
}
46+
47+
/** Everything pending, so one action can commit all of it. */
48+
export function getPendingPreviews(): PendingPreview[] {
49+
return [...previews].map(([key, value]) => ({ key, value }));
2550
}

src/test/panelHtml.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,23 @@ describe('Test render the color panel html', () => {
9797
);
9898
assert.ok(!html.includes('<b>'));
9999
});
100+
101+
it('Must offer an apply button per toggle row', () => {
102+
const html = renderPanelHtml(toggles, rows, 'n0nce', 'vscode-resource:');
103+
assert.ok(html.includes('data-apply-toggle="enableRainbow"'));
104+
});
105+
106+
it('Must offer exactly one apply all control', () => {
107+
const html = renderPanelHtml(toggles, rows, 'n0nce', 'vscode-resource:');
108+
const occurrences = html.split('data-apply-all=').length - 1;
109+
assert.strictEqual(occurrences, 1);
110+
});
111+
112+
it('Must draw the apply all control below the color rows', () => {
113+
const html = renderPanelHtml(toggles, rows, 'n0nce', 'vscode-resource:');
114+
const lastApply = html.lastIndexOf('data-apply=');
115+
const applyAll = html.indexOf('data-apply-all=');
116+
assert.ok(lastApply >= 0, 'no color row apply button');
117+
assert.ok(applyAll > lastApply, 'the apply all control is not below the colors');
118+
});
100119
});

src/test/panelView.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,9 @@ describe('Test color panel view', () => {
3939
`resolved panel html has no switch for ${key}`
4040
);
4141
}
42+
assert.ok(
43+
(html as string).includes('data-apply-all='),
44+
'resolved panel html has no apply all control'
45+
);
4246
});
4347
});

0 commit comments

Comments
 (0)