Skip to content

Commit 7529911

Browse files
Make Jacdac modules readonly
Remove Jacdac modules from project when user code no longer references them. We now auto add and auto remove.
1 parent c244538 commit 7529911

8 files changed

Lines changed: 90 additions & 29 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@codemirror/view": "^6.26.3",
1717
"@emotion/react": "^11.14.0",
1818
"@emotion/styled": "^11.14.1",
19-
"@microbit/microbit-connection": "0.0.0-worker.jacdac.600",
19+
"@microbit/microbit-connection": "0.0.0-worker.jacdac.601",
2020
"@microbit/microbit-fs": "^0.10.0",
2121
"@sanity/block-content-to-react": "^3.0.0",
2222
"@sanity/image-url": "^1.0.1",

src/editor/EditorContainer.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@ const EditorContainer = ({ selection }: EditorContainerProps) => {
2929
return null;
3030
}
3131

32-
return fileInfo.isThirdPartyModule &&
33-
!settings.allowEditingThirdPartyModules ? (
34-
<ModuleOverlay moduleData={fileInfo.moduleData} />
35-
) : (
32+
// Jacdac modules are managed by the editor and always shown read-only (the
33+
// user can read them but not edit), regardless of the third-party module
34+
// setting, which is why they're handled before the overlay branch.
35+
if (
36+
!fileInfo.isJacdacModule &&
37+
fileInfo.isThirdPartyModule &&
38+
!settings.allowEditingThirdPartyModules
39+
) {
40+
return <ModuleOverlay moduleData={fileInfo.moduleData} />;
41+
}
42+
return (
3643
<Editor
3744
defaultValue={fileInfo.initialValue}
3845
selection={selection}
@@ -42,6 +49,7 @@ const EditorContainer = ({ selection }: EditorContainerProps) => {
4249
parameterHelpOption={settings.parameterHelp}
4350
warnOnV2OnlyFeatures={settings.warnForApiUnsupportedByDevice}
4451
disableV2OnlyFeaturesWarning={disableV2OnlyFeaturesWarning}
52+
readOnly={fileInfo.isJacdacModule}
4553
/>
4654
);
4755
};

src/editor/codemirror/CodeMirror.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ interface CodeMirrorProps {
5353
parameterHelpOption: ParameterHelpOption;
5454
warnOnV2OnlyFeatures: boolean;
5555
disableV2OnlyFeaturesWarning: () => void;
56+
/** Show the document but disallow edits (e.g. for managed Jacdac modules). */
57+
readOnly?: boolean;
5658
}
5759

5860
/**
@@ -73,6 +75,7 @@ const CodeMirror = ({
7375
parameterHelpOption,
7476
warnOnV2OnlyFeatures,
7577
disableV2OnlyFeaturesWarning,
78+
readOnly = false,
7679
}: CodeMirrorProps) => {
7780
// Really simple model for now as we only have one editor at a time.
7881
const [, setActiveEditor] = useActiveEditorActionsState();
@@ -125,6 +128,8 @@ const CodeMirror = ({
125128
extensions: [
126129
notify,
127130
editorConfig,
131+
EditorState.readOnly.of(readOnly),
132+
EditorView.editable.of(!readOnly),
128133
// Extension requires external state.
129134
dndSupport({ sessionSettings, setSessionSettings }),
130135
// Extensions only relevant for editing:
@@ -186,6 +191,7 @@ const CodeMirror = ({
186191
apiReferenceMap,
187192
device,
188193
disableV2OnlyFeaturesWarning,
194+
readOnly,
189195
]);
190196
useEffect(() => {
191197
// Do this separately as we don't want to destroy the view whenever options needed for initialization change.

src/jacdac/jacdac-hooks.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from "react";
2727
import { MAIN_FILE, VersionAction } from "../fs/fs";
2828
import { useFileSystem } from "../fs/fs-hooks";
29+
import { extractModuleData } from "../fs/fs-util";
2930
import { parseRoles, ParsedRole } from "./parse-roles";
3031
import { JACDAC_MODULES } from "./python/module-source";
3132
import { SupportedService, supportedServiceByClass } from "./supported-services";
@@ -127,11 +128,16 @@ export const useJacdacIdentify = (): ((deviceId: string) => Promise<void>) => {
127128
};
128129

129130
/**
130-
* Ensure each Jacdac sensor module (JacdacButton.py, etc.) is a project file
131-
* whenever the user's code uses that class, so the import resolves at runtime
132-
* (in the simulator) and the user can read the module. Only the modules
133-
* actually used are added, keeping the flat filesystem lean. Added as micro:bit
134-
* module files.
131+
* Keep the Jacdac sensor modules (JacdacButton.py, etc.) in sync with the user's
132+
* code: add a module file whenever the code uses that class (so the import
133+
* resolves in the simulator and the user can read it), and remove it again once
134+
* the code no longer references the class. Only used modules are present, keeping
135+
* the flat filesystem lean. Added/removed as micro:bit module files.
136+
*
137+
* Removal is guarded by the magic module comment: we only delete a file we
138+
* recognise as our own module (matching class name), so a user's own same-named
139+
* file is left untouched. This does mean edits to an added module file are lost
140+
* if the last reference is removed.
135141
*/
136142
export const useEnsureJacdacModules = (): void => {
137143
const fs = useFileSystem();
@@ -149,8 +155,16 @@ export const useEnsureJacdacModules = (): void => {
149155
}
150156
const used = new RegExp(`\\b${module.className}\\b`).test(text);
151157
const filename = `${module.className}.py`;
152-
if (used && !(await fs.exists(filename))) {
158+
const exists = await fs.exists(filename);
159+
if (used && !exists) {
153160
await fs.write(filename, module.source, VersionAction.INCREMENT);
161+
} else if (!used && exists) {
162+
const existing = new TextDecoder().decode(
163+
(await fs.read(filename)).data
164+
);
165+
if (extractModuleData(existing)?.name === module.className) {
166+
await fs.remove(filename);
167+
}
154168
}
155169
}
156170
} catch {

src/jacdac/python/module-source.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* (the micro:bit filesystem is flat and small). Each module's source feeds both
1010
* the language-server stub and the project file, so the two never drift.
1111
*/
12+
import { extractModuleData } from "../../fs/fs-util";
1213
import { JacdacRoleType } from "../parse-roles";
1314
import jacdacButton from "./JacdacButton.py?raw";
1415
import jacdacRotaryEncoder from "./JacdacRotaryEncoder.py?raw";
@@ -30,3 +31,22 @@ export const JACDAC_MODULES: JacdacModule[] = [
3031
},
3132
{ className: "JacdacSlider", type: "slider", source: jacdacSlider },
3233
];
34+
35+
/**
36+
* True if the given source is one of our Jacdac modules, identified by its magic
37+
* module comment's name (not just the filename), so a user's own same-named file
38+
* is not treated as ours. Jacdac modules are managed by the editor and shown
39+
* read-only.
40+
*/
41+
export const isJacdacModuleSource = (source: string): boolean => {
42+
const name = extractModuleData(source)?.name;
43+
return !!name && JACDAC_MODULES.some((m) => m.className === name);
44+
};
45+
46+
/**
47+
* True if the filename is one of our managed Jacdac module files (e.g.
48+
* "JacdacButton.py"). Used where only the name is available (the project file
49+
* list); prefer isJacdacModuleSource when the content is available.
50+
*/
51+
export const isJacdacModuleFilename = (filename: string): boolean =>
52+
JACDAC_MODULES.some((m) => `${m.className}.py` === filename);

src/project/FileRow.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RiDeleteBin2Line, RiDownload2Line, RiEdit2Line } from "react-icons/ri";
1919
import { FormattedMessage, useIntl } from "react-intl";
2020
import { zIndexProjectAreaMenu } from "../common/zIndex";
2121
import { FileVersion, MAIN_FILE } from "../fs/fs";
22+
import { isJacdacModuleFilename } from "../jacdac/python/module-source";
2223
import { useProjectActions } from "./project-hooks";
2324
import { isEditableFile } from "./project-utils";
2425

@@ -34,6 +35,9 @@ interface FileRowProps extends BoxProps {
3435
const FileRow = ({ projectName, value, onEdit, ...props }: FileRowProps) => {
3536
const { name } = value;
3637
const isMainFile = name === MAIN_FILE;
38+
// Jacdac modules are managed by the editor and read-only, so they can't be
39+
// edited or deleted from here (only saved/downloaded).
40+
const isJacdacModule = isJacdacModuleFilename(name);
3741
const actions = useProjectActions();
3842
const intl = useIntl();
3943

@@ -63,26 +67,30 @@ const FileRow = ({ projectName, value, onEdit, ...props }: FileRowProps) => {
6367
/>
6468
<Portal>
6569
<MenuList zIndex={zIndexProjectAreaMenu}>
66-
<MenuItem
67-
icon={<RiEdit2Line />}
68-
isDisabled={!isEditableFile(name)}
69-
onClick={onEdit}
70-
>
71-
<FormattedMessage id="edit-file-action" values={{ name }} />
72-
</MenuItem>
70+
{!isJacdacModule && (
71+
<MenuItem
72+
icon={<RiEdit2Line />}
73+
isDisabled={!isEditableFile(name)}
74+
onClick={onEdit}
75+
>
76+
<FormattedMessage id="edit-file-action" values={{ name }} />
77+
</MenuItem>
78+
)}
7379
<MenuItem
7480
icon={<RiDownload2Line />}
7581
onClick={() => actions.saveFile(name)}
7682
>
7783
<FormattedMessage id="save-file-action" values={{ name }} />
7884
</MenuItem>
79-
<MenuItem
80-
icon={<RiDeleteBin2Line />}
81-
onClick={() => actions.deleteFile(name)}
82-
isDisabled={isMainFile}
83-
>
84-
<FormattedMessage id="delete-file-action" values={{ name }} />
85-
</MenuItem>
85+
{!isJacdacModule && (
86+
<MenuItem
87+
icon={<RiDeleteBin2Line />}
88+
onClick={() => actions.deleteFile(name)}
89+
isDisabled={isMainFile}
90+
>
91+
<FormattedMessage id="delete-file-action" values={{ name }} />
92+
</MenuItem>
93+
)}
8694
</MenuList>
8795
</Portal>
8896
</Menu>

src/project/project-hooks.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isPythonMicrobitModule,
1717
ModuleData,
1818
} from "../fs/fs-util";
19+
import { isJacdacModuleSource } from "../jacdac/python/module-source";
1920
import { useLanguageServerClient } from "../language-server/language-server-hooks";
2021
import { useLogging } from "../logging/logging-hooks";
2122
import { useSessionSettings } from "../settings/session-settings";
@@ -102,6 +103,9 @@ export const useProject = (): DefaultedProject => {
102103

103104
interface ProjectTextFileInfo {
104105
isThirdPartyModule: boolean;
106+
// A Jacdac module managed by the editor: shown read-only rather than via the
107+
// third-party module overlay, so the user can read (but not edit) it.
108+
isJacdacModule: boolean;
105109
initialValue: string;
106110
moduleData: ModuleData | undefined;
107111
}
@@ -128,6 +132,7 @@ export const useProjectFileText = (
128132
// We don't change this value if the text is edited to become a module
129133
// as that would abruptly prevent it being edited further.
130134
isThirdPartyModule: isPythonMicrobitModule(text),
135+
isJacdacModule: isJacdacModuleSource(text),
131136
moduleData: extractModuleData(text),
132137
});
133138
}

0 commit comments

Comments
 (0)