Skip to content

Commit 3b2dff8

Browse files
authored
Merge pull request #58 from ShortArrow/test/kill-survivors
Kill the killable survivors of a 29-mutant sweep
2 parents baa7c6c + 2be71e8 commit 3b2dff8

8 files changed

Lines changed: 498 additions & 118 deletions

File tree

src/core.ts

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from "vscode";
2-
import { shiftHue } from "./colors";
2+
import { buildLineDecorationSpecs } from "./decorations";
33
import { visibleLineIndexes } from "./visibleLines";
44
import {
55
getColorAtCenterOfRainbow,
@@ -13,35 +13,7 @@ import {
1313
getActiveLineNumberColor,
1414
} from "./config";
1515

16-
/**
17-
* check repeating digits
18-
* @param lineNumber line number
19-
* @returns boolean if repeating digits, return true
20-
*/
21-
export function isRepeatingDigits(lineNumber: string) {
22-
return lineNumber.match(/^(\d)\1+$/) !== null;
23-
}
24-
25-
/**
26-
* check sequential digits (poker straights: 123, 543, 10)
27-
* @param lineNumber line number
28-
* @returns true when every adjacent digit steps by +1 or -1 in one direction
29-
*/
30-
export function isSequentialDigits(lineNumber: string): boolean {
31-
if (lineNumber.length < 2) {
32-
return false;
33-
}
34-
const step = Number(lineNumber[1]) - Number(lineNumber[0]);
35-
if (step !== 1 && step !== -1) {
36-
return false;
37-
}
38-
for (let i = 1; i < lineNumber.length; i++) {
39-
if (Number(lineNumber[i]) - Number(lineNumber[i - 1]) !== step) {
40-
return false;
41-
}
42-
}
43-
return true;
44-
}
16+
export { isRepeatingDigits, isSequentialDigits } from "./decorations";
4517

4618
/**
4719
* update relative line numbers
@@ -58,44 +30,34 @@ export async function updateRelativeLineNumbers(
5830
}
5931
const decorations: vscode.DecorationOptions[] = [];
6032

61-
const activeLineNumber = editor.selection.active.line;
6233
const document = editor.document;
63-
const activeLineNumberColor = getActiveLineNumberColor();
64-
const inactiveLineNumberColor = getInactiveLineNumberColor();
65-
const enableRainbow = getEnableRainbow();
66-
const enableRepeatingDigits = getEnableRepeatingDigits();
67-
const repeatingDigitsColor = getColorAtRepeatingDigits();
68-
const enableSequentialDigits = getEnableSequentialDigits();
69-
const sequentialDigitsColor = getColorAtSequentialDigits();
70-
const centerColorOfRainbow = getColorAtCenterOfRainbow();
7134
const labelWidth = document.lineCount.toString().length;
7235
const lineIndexes = visibleLineIndexes(
7336
editor.visibleRanges.map((r) => ({ startLine: r.start.line, endLine: r.end.line })),
7437
document.lineCount
7538
);
76-
for (const lineIndex of lineIndexes) {
39+
const specs = buildLineDecorationSpecs(lineIndexes, {
40+
enableRelativeLine: getEnableRelativeLine(),
41+
activeLineNumber: editor.selection.active.line,
42+
activeColor: getActiveLineNumberColor(),
43+
inactiveColor: getInactiveLineNumberColor(),
44+
enableRainbow: getEnableRainbow(),
45+
centerColorOfRainbow: getColorAtCenterOfRainbow(),
46+
enableRepeatingDigits: getEnableRepeatingDigits(),
47+
repeatingDigitsColor: getColorAtRepeatingDigits(),
48+
enableSequentialDigits: getEnableSequentialDigits(),
49+
sequentialDigitsColor: getColorAtSequentialDigits(),
50+
});
51+
for (const { lineIndex, label, color } of specs) {
7752
try {
7853
const lineRange = document.lineAt(lineIndex).range;
79-
const isCurrentLine = lineIndex === activeLineNumber;
80-
81-
const label = isCurrentLine
82-
? String(activeLineNumber + 1)
83-
: String(Math.abs(lineIndex - activeLineNumber));
8454

8555
const rangeScope = new vscode.Range(lineRange.start, lineRange.start);
8656
const lineNumberStyle = {
8757
width: `${labelWidth / 2 + 0.5}em`,
8858
align: "right",
8959
contentText: label,
90-
color: isCurrentLine
91-
? activeLineNumberColor
92-
: (enableRepeatingDigits && isRepeatingDigits(label))
93-
? repeatingDigitsColor
94-
: (enableSequentialDigits && isSequentialDigits(label))
95-
? sequentialDigitsColor
96-
: enableRainbow
97-
? shiftHue(centerColorOfRainbow, Math.abs(lineIndex - activeLineNumber))
98-
: inactiveLineNumberColor,
60+
color,
9961
textDecoration: `
10062
box-sizing: border-box;
10163
text-align: right;
@@ -116,7 +78,6 @@ export async function updateRelativeLineNumbers(
11678
console.error(error);
11779
}
11880
}
119-
const enableRelativeLine = getEnableRelativeLine();
12081

121-
editor.setDecorations(decorationType, enableRelativeLine ? decorations : []);
82+
editor.setDecorations(decorationType, decorations);
12283
}

src/decorations.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { shiftHue } from "./colors";
2+
3+
/**
4+
* A color the editor can paint a line number with.
5+
*
6+
* Production passes a vscode.ThemeColor straight through this slot; the tests
7+
* pass a plain object. Nothing here inspects a non-string color, so the module
8+
* never needs to know which of the two it is holding — which is what keeps it
9+
* free of any vscode import.
10+
*/
11+
export type LineColor = string | { themeColor?: unknown; id?: unknown };
12+
13+
/** One line's decoration, decided but not yet handed to the editor. */
14+
export interface LineDecorationSpec {
15+
lineIndex: number;
16+
label: string;
17+
color: LineColor;
18+
}
19+
20+
/** Everything the computation reads, lifted out of the configuration. */
21+
export interface DecorationSettings {
22+
enableRelativeLine: boolean;
23+
activeLineNumber: number;
24+
activeColor: LineColor;
25+
inactiveColor: LineColor;
26+
enableRainbow: boolean;
27+
centerColorOfRainbow: string;
28+
enableRepeatingDigits: boolean;
29+
repeatingDigitsColor: string;
30+
enableSequentialDigits: boolean;
31+
sequentialDigitsColor: string;
32+
}
33+
34+
/**
35+
* check repeating digits
36+
* @param lineNumber line number
37+
* @returns boolean if repeating digits, return true
38+
*/
39+
export function isRepeatingDigits(lineNumber: string) {
40+
return lineNumber.match(/^(\d)\1+$/) !== null;
41+
}
42+
43+
/**
44+
* check sequential digits (poker straights: 123, 543, 10)
45+
* @param lineNumber line number
46+
* @returns true when every adjacent digit steps by +1 or -1 in one direction
47+
*/
48+
export function isSequentialDigits(lineNumber: string): boolean {
49+
if (lineNumber.length < 2) {
50+
return false;
51+
}
52+
const step = Number(lineNumber[1]) - Number(lineNumber[0]);
53+
if (step !== 1 && step !== -1) {
54+
return false;
55+
}
56+
for (let i = 1; i < lineNumber.length; i++) {
57+
if (Number(lineNumber[i]) - Number(lineNumber[i - 1]) !== step) {
58+
return false;
59+
}
60+
}
61+
return true;
62+
}
63+
64+
/**
65+
* The label and color of every line to decorate.
66+
*
67+
* The disabled case returns nothing rather than being gated at the call site,
68+
* so the decision of what a line shows lives in one place that no editor is
69+
* needed to test.
70+
*
71+
* @param lineIndexes zero-based lines to decorate, in the order to emit them
72+
* @param settings the configured colors and modes
73+
*/
74+
export function buildLineDecorationSpecs(
75+
lineIndexes: readonly number[],
76+
settings: DecorationSettings
77+
): LineDecorationSpec[] {
78+
if (!settings.enableRelativeLine) {
79+
return [];
80+
}
81+
const specs: LineDecorationSpec[] = [];
82+
for (const lineIndex of lineIndexes) {
83+
const isCurrentLine = lineIndex === settings.activeLineNumber;
84+
const distance = Math.abs(lineIndex - settings.activeLineNumber);
85+
const label = isCurrentLine
86+
? String(settings.activeLineNumber + 1)
87+
: String(distance);
88+
const color = isCurrentLine
89+
? settings.activeColor
90+
: (settings.enableRepeatingDigits && isRepeatingDigits(label))
91+
? settings.repeatingDigitsColor
92+
: (settings.enableSequentialDigits && isSequentialDigits(label))
93+
? settings.sequentialDigitsColor
94+
: settings.enableRainbow
95+
? shiftHue(settings.centerColorOfRainbow, distance)
96+
: settings.inactiveColor;
97+
specs.push({ lineIndex, label, color });
98+
}
99+
return specs;
100+
}

src/panel.ts

Lines changed: 102 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,101 @@ function isKnownToggle(key: string) {
7070
return toggles.some((entry) => entry.key === key);
7171
}
7272

73+
/**
74+
* The effects {@link handlePanelMessage} is allowed to have.
75+
*
76+
* Injecting them is what separates deciding from doing: the webview supplies
77+
* the real configuration writes, a test supplies recorders.
78+
*/
79+
export interface PanelMessageDeps {
80+
isColorKey(key: string): boolean;
81+
isToggleKey(key: string): boolean;
82+
save(key: string, value: string | boolean, scope: string): Promise<void>;
83+
refresh(): void;
84+
postState(): void;
85+
}
86+
87+
/**
88+
* Act on one message from the settings webview.
89+
*
90+
* Unknown keys and malformed messages are dropped without any effect at all,
91+
* so a webview that has drifted from the extension cannot write a setting the
92+
* panel does not offer.
93+
*
94+
* @param message whatever the webview posted, which is not to be trusted
95+
* @param deps the key vocabulary and the effects to perform
96+
*/
97+
export async function handlePanelMessage(
98+
message: unknown,
99+
deps: PanelMessageDeps
100+
): Promise<void> {
101+
if (!message || typeof message !== "object") {
102+
return;
103+
}
104+
const panelMessage = message as PanelMessage;
105+
if (panelMessage.type === "applyAll") {
106+
for (const { key, value } of getPendingPreviews()) {
107+
if (deps.isColorKey(key) || deps.isToggleKey(key)) {
108+
await deps.save(key, value, panelMessage.scope);
109+
}
110+
}
111+
clearAllPreviews();
112+
deps.refresh();
113+
deps.postState();
114+
return;
115+
}
116+
if (panelMessage.type === "resetAll") {
117+
// Toggles go too: a pending switch is a preview like any other, and
118+
// "reset all" that left one standing would not have reset the panel.
119+
clearAllPreviews();
120+
deps.refresh();
121+
deps.postState();
122+
return;
123+
}
124+
if (panelMessage.type === "resetRow") {
125+
if (!deps.isColorKey(panelMessage.key)) {
126+
return;
127+
}
128+
clearPreview(panelMessage.key);
129+
deps.refresh();
130+
deps.postState();
131+
return;
132+
}
133+
if (
134+
panelMessage.type === "previewToggle" ||
135+
panelMessage.type === "applyToggle"
136+
) {
137+
if (!deps.isToggleKey(panelMessage.key)) {
138+
return;
139+
}
140+
const value = panelMessage.value === true;
141+
if (panelMessage.type === "previewToggle") {
142+
setPreviewToggle(panelMessage.key, value);
143+
deps.refresh();
144+
return;
145+
}
146+
clearPreview(panelMessage.key);
147+
await deps.save(panelMessage.key, value, panelMessage.scope);
148+
deps.refresh();
149+
deps.postState();
150+
return;
151+
}
152+
if (!deps.isColorKey(panelMessage.key)) {
153+
return;
154+
}
155+
if (panelMessage.type === "preview") {
156+
setPreviewColor(panelMessage.key, panelMessage.value);
157+
deps.refresh();
158+
return;
159+
}
160+
if (panelMessage.type === "apply") {
161+
clearPreview(panelMessage.key);
162+
await deps.save(panelMessage.key, panelMessage.value, panelMessage.scope);
163+
deps.refresh();
164+
deps.postState();
165+
}
166+
}
167+
73168
let resolvedHtml: string | undefined;
74169
let resolvedView: vscode.WebviewView | undefined;
75170

@@ -185,67 +280,13 @@ class ColorPanelProvider implements vscode.WebviewViewProvider {
185280
}
186281

187282
private async handle(message: PanelMessage) {
188-
if (!message) {
189-
return;
190-
}
191-
if (message.type === "applyAll") {
192-
for (const { key, value } of getPendingPreviews()) {
193-
if (isKnownKey(key) || isKnownToggle(key)) {
194-
await this.save(key, value, message.scope);
195-
}
196-
}
197-
clearAllPreviews();
198-
this.refresh();
199-
this.postState();
200-
return;
201-
}
202-
if (message.type === "resetAll") {
203-
// Toggles go too: a pending switch is a preview like any other, and
204-
// "reset all" that left one standing would not have reset the panel.
205-
clearAllPreviews();
206-
this.refresh();
207-
this.postState();
208-
return;
209-
}
210-
if (message.type === "resetRow") {
211-
if (!isKnownKey(message.key)) {
212-
return;
213-
}
214-
clearPreview(message.key);
215-
this.refresh();
216-
this.postState();
217-
return;
218-
}
219-
if (message.type === "previewToggle" || message.type === "applyToggle") {
220-
if (!isKnownToggle(message.key)) {
221-
return;
222-
}
223-
const value = message.value === true;
224-
if (message.type === "previewToggle") {
225-
setPreviewToggle(message.key, value);
226-
this.refresh();
227-
return;
228-
}
229-
clearPreview(message.key);
230-
await this.save(message.key, value, message.scope);
231-
this.refresh();
232-
this.postState();
233-
return;
234-
}
235-
if (!isKnownKey(message.key)) {
236-
return;
237-
}
238-
if (message.type === "preview") {
239-
setPreviewColor(message.key, message.value);
240-
this.refresh();
241-
return;
242-
}
243-
if (message.type === "apply") {
244-
clearPreview(message.key);
245-
await this.save(message.key, message.value, message.scope);
246-
this.refresh();
247-
this.postState();
248-
}
283+
await handlePanelMessage(message, {
284+
isColorKey: isKnownKey,
285+
isToggleKey: isKnownToggle,
286+
save: (key, value, scope) => this.save(key, value, scope),
287+
refresh: () => this.refresh(),
288+
postState: () => this.postState(),
289+
});
249290
}
250291
}
251292

0 commit comments

Comments
 (0)