Skip to content

Commit 625cd98

Browse files
Revise code structure settings. (#249)
Introduce two new options replacing L-shaped blocks: - Full, with background highlighting and borders for the active block. - Simple, with L-shaped borders. Behind the scenes there's rather more configurability than before. We might use this for future experimentation or, in time, drop it. We may revisit the distinctiveness of the active block highlight (in Full mode) and review the contrast with the syntax highlighting colors. Leaving the issue open for now to discuss this and raise new issues as required. See #207
1 parent 98a8143 commit 625cd98

File tree

14 files changed

+385
-226
lines changed

14 files changed

+385
-226
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ _steps:
3333
install_dependencies: &install_dependencies
3434
run: npm ci --cache .npm-cache && sudo npm i -g --registry https://npm.pkg.github.com/microbit-foundation @microbit-foundation/website-deploy-aws@0.2.0 @microbit-foundation/website-deploy-aws-config@0.4.2 @microbit-foundation/circleci-npm-package-versioner@1
3535
install_theme: &install_theme
36-
run: npm install --no-save --registry https://npm.pkg.github.com/microbit-foundation @microbit-foundation/python-editor-next-microbit@0.1.0-dev.45
36+
run: npm install --no-save --registry https://npm.pkg.github.com/microbit-foundation @microbit-foundation/python-editor-next-microbit@0.1.0-dev.53
3737
update_version: &update_version
3838
run: npm run ci:update-version
3939
build: &build

src/common/use-local-storage.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
import React from "react";
3+
import { useLocalStorage } from "./use-local-storage";
4+
5+
interface TestState {
6+
x: number;
7+
y: number;
8+
z: number;
9+
}
10+
11+
const validate = (o: unknown): o is TestState => true;
12+
13+
const Test = () => {
14+
const [state, setState] = useLocalStorage<TestState>("key", validate, {
15+
x: 1,
16+
y: 1,
17+
z: 1,
18+
});
19+
return (
20+
<div>
21+
<p data-testid="value">{state.x}</p>
22+
<button onClick={() => setState({ ...state, x: state.x + 1 })}>
23+
increment x
24+
</button>
25+
</div>
26+
);
27+
};
28+
29+
describe("useLocalStorage", () => {
30+
it("reads/writes to/from local storage", () => {
31+
localStorage.setItem("key", JSON.stringify({ x: 9, y: 10, z: 11 }));
32+
33+
render(<Test />);
34+
fireEvent.click(screen.getByText("increment x"));
35+
36+
expect(JSON.parse(localStorage.getItem("key")!)).toEqual({
37+
x: 10,
38+
y: 10,
39+
z: 11,
40+
});
41+
});
42+
43+
it("aligns top-level keys with default value", () => {
44+
localStorage.setItem("key", JSON.stringify({ a: 1, z: 11 }));
45+
46+
render(<Test />);
47+
fireEvent.click(screen.getByText("increment x"));
48+
49+
expect(JSON.parse(localStorage.getItem("key")!)).toEqual({
50+
x: 2,
51+
y: 1,
52+
z: 11,
53+
});
54+
});
55+
});

src/common/use-local-storage.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,21 @@ export function useLocalStorage<T>(
1717
const value = localStorage.getItem(key);
1818
if (value !== null) {
1919
try {
20-
const parsed = JSON.parse(value);
20+
let parsed = JSON.parse(value);
21+
// Ensure we get new top-level defaults.
22+
parsed = {
23+
...defaultValue,
24+
...parsed,
25+
};
26+
// Remove any top-level keys that aren't present in defaults.
27+
const unknownKeys = new Set(Object.keys(parsed));
28+
Object.keys(defaultValue).forEach((k) => unknownKeys.delete(k));
29+
unknownKeys.forEach((k) => delete parsed[k]);
30+
2131
if (!validate(parsed)) {
2232
throw new Error("Invalid data stored in local storage");
2333
}
34+
2435
return parsed;
2536
} catch (e) {
2637
// Better than exploding forever.

src/deployment/default/colors.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ const colors = {
1414
brand: gray,
1515
gray,
1616
code: {
17-
block: "rgba(52, 162, 235, 0.06)",
18-
border: theme.colors.gray[400],
17+
blockBorder: theme.colors.gray[400],
18+
blockBackground: "rgba(185, 185, 185, 0.1)",
19+
blockBackgroundActive: "lightyellow",
20+
blockBorderActive: theme.colors.blue[400],
21+
1922
comment: "gray",
2023
default: "black",
2124
keyword: "darkblue",

src/editor/EditorContainer.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
*/
66
import { useProjectFileText } from "../project/project-hooks";
7-
import { useSettings } from "../settings/settings";
7+
import { codeStructureSettings, useSettings } from "../settings/settings";
88
import { WorkbenchSelection } from "../workbench/use-selection";
99
import Editor from "./codemirror/CodeMirror";
1010

@@ -17,15 +17,15 @@ interface EditorContainerProps {
1717
* and wires it to the currently open file.
1818
*/
1919
const EditorContainer = ({ selection }: EditorContainerProps) => {
20-
const [{ fontSize, codeStructureHighlight }] = useSettings();
20+
const [settings] = useSettings();
2121
const [defaultValue, onFileChange] = useProjectFileText(selection.file);
2222
return typeof defaultValue === "undefined" ? null : (
2323
<Editor
2424
defaultValue={defaultValue}
2525
location={selection.location}
2626
onChange={onFileChange}
27-
fontSize={fontSize}
28-
codeStructureHighlight={codeStructureHighlight}
27+
fontSize={settings.fontSize}
28+
codeStructureSettings={codeStructureSettings(settings)}
2929
/>
3030
);
3131
};

src/editor/codemirror/CodeMirror.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { EditorSelection, EditorState } from "@codemirror/state";
77
import { EditorView } from "@codemirror/view";
88
import { useEffect, useMemo, useRef } from "react";
99
import { useIntl } from "react-intl";
10-
import { CodeStructureHighlight } from "../../settings/settings";
1110
import { FileLocation } from "../../workbench/use-selection";
1211
import "./CodeMirror.css";
1312
import { editorConfig, themeExtensionsCompartment } from "./config";
1413
import {
15-
structureHighlighting,
14+
codeStructure,
15+
CodeStructureSettings,
1616
structureHighlightingCompartment,
1717
} from "./structure-highlighting";
1818
import themeExtensions from "./themeExtensions";
@@ -24,7 +24,7 @@ interface CodeMirrorProps {
2424

2525
location: FileLocation;
2626
fontSize: number;
27-
codeStructureHighlight: CodeStructureHighlight;
27+
codeStructureSettings: CodeStructureSettings;
2828
}
2929

3030
/**
@@ -41,7 +41,7 @@ const CodeMirror = ({
4141
onChange,
4242
location,
4343
fontSize,
44-
codeStructureHighlight,
44+
codeStructureSettings,
4545
}: CodeMirrorProps) => {
4646
const elementRef = useRef<HTMLDivElement | null>(null);
4747
const viewRef = useRef<EditorView | null>(null);
@@ -51,9 +51,9 @@ const CodeMirror = ({
5151
const options = useMemo(
5252
() => ({
5353
fontSize,
54-
codeStructureHighlight,
54+
codeStructureSettings,
5555
}),
56-
[fontSize, codeStructureHighlight]
56+
[fontSize, codeStructureSettings]
5757
);
5858

5959
useEffect(() => {
@@ -71,7 +71,7 @@ const CodeMirror = ({
7171
editorConfig,
7272
// Extensions we enable/disable based on props.
7373
structureHighlightingCompartment.of(
74-
structureHighlighting(options.codeStructureHighlight)
74+
codeStructure(options.codeStructureSettings)
7575
),
7676
themeExtensionsCompartment.of(themeExtensions(options.fontSize)),
7777
],
@@ -101,7 +101,7 @@ const CodeMirror = ({
101101
themeExtensions(options.fontSize)
102102
),
103103
structureHighlightingCompartment.reconfigure(
104-
structureHighlighting(options.codeStructureHighlight)
104+
codeStructure(options.codeStructureSettings)
105105
),
106106
],
107107
});

src/editor/codemirror/structure-highlighting/doc-util.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@
99

1010
import { EditorState } from "@codemirror/state";
1111

12-
export const skipTrailingBlankLines = (
13-
state: EditorState,
14-
position: number
15-
) => {
12+
/**
13+
* Skips trailing comments (regardless of indent) and blank lines at the end of the body.
14+
*
15+
* @param state Document state.
16+
* @param position Document character position.
17+
* @returns End of last line we consider to be part of the body after skipping.
18+
*/
19+
export const skipBodyTrailers = (state: EditorState, position: number) => {
1620
let line = state.doc.lineAt(position);
17-
while ((line.length === 0 || /^\s+$/.test(line.text)) && line.number >= 1) {
21+
while (
22+
line.number >= 1 &&
23+
(line.length === 0 || /^\s+$/.test(line.text) || /^\s*#/.test(line.text))
24+
) {
1825
line = state.doc.line(line.number - 1);
1926
}
2027
return line.to;

src/editor/codemirror/structure-highlighting/index.ts

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,60 +8,32 @@
88
*/
99

1010
import { Compartment, Extension } from "@codemirror/state";
11-
import { CodeStructureHighlight } from "../../../settings/settings";
12-
import { baseTheme, themeTweakForBackgrounds } from "./theme";
11+
import { baseTheme } from "./theme";
1312
import { codeStructureView } from "./view";
1413

1514
export const structureHighlightingCompartment = new Compartment();
1615

16+
export type CodeStructureShape = "l-shape" | "box";
17+
export type CodeStructureBackground = "block" | "none";
18+
export type CodeStructureBorders = "borders" | "none" | "left-edge-only";
19+
1720
export interface CodeStructureSettings {
18-
shape: "l-shape" | "box";
19-
background: "block" | "none";
20-
borders: "borders" | "no-borders" | "left-edge-only";
21+
shape: CodeStructureShape;
22+
background: CodeStructureBackground;
23+
borders: CodeStructureBorders;
2124

22-
hoverBackground?: boolean;
2325
cursorBackground?: boolean;
24-
hoverBorder?: boolean;
25-
cursorBorder?: boolean;
26+
cursorBorder?: CodeStructureBorders;
2627
}
2728

28-
// We'll switch to exporting this soon, see below.
29-
const codeStructure = (settings: CodeStructureSettings): Extension => [
30-
codeStructureView(settings),
31-
baseTheme,
32-
settings.background !== "none" ||
33-
settings.cursorBackground ||
34-
settings.hoverBackground
35-
? themeTweakForBackgrounds
36-
: [],
37-
];
38-
39-
// This will go soon in favour of more fine-grained settings
40-
export const structureHighlighting = (
41-
option: CodeStructureHighlight
42-
): Extension => {
43-
switch (option) {
44-
case "l-shapes":
45-
return codeStructure({
46-
shape: "l-shape",
47-
background: "none",
48-
borders: "left-edge-only",
49-
});
50-
case "boxes":
51-
return codeStructure({
52-
shape: "box",
53-
background: "block",
54-
borders: "no-borders",
55-
});
56-
case "l-shape-boxes":
57-
return codeStructure({
58-
shape: "l-shape",
59-
background: "block",
60-
borders: "no-borders",
61-
});
62-
case "none":
63-
return [];
64-
default:
65-
return [];
66-
}
29+
/**
30+
* Creates a CodeMirror extension that provides structural highlighting
31+
* based on the CodeMirror syntax tree. The intent is to aid code comprehension
32+
* and provide clues when indentation isn't correct.
33+
*
34+
* @param settings Settings for the code structure CodeMirror extension.
35+
* @returns A appropriately configured extension.
36+
*/
37+
export const codeStructure = (settings: CodeStructureSettings): Extension => {
38+
return [codeStructureView(settings), baseTheme];
6739
};

0 commit comments

Comments
 (0)