@mpeggroup/mpeg-sdl-editor 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-bun-dependencies.yml +13 -0
- package/.github/workflows/lint-pr-message.yml +13 -0
- package/.github/workflows/release-bun-webapp.yml +15 -0
- package/.github/workflows/validate-bun-webapp-pr.yml +9 -0
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/build.ts +8 -0
- package/functional_tests/README.md +25 -0
- package/functional_tests/features/environment.py +50 -0
- package/functional_tests/features/steps/helper/selenium.py +15 -0
- package/functional_tests/features/steps/webapp.py +30 -0
- package/functional_tests/features/webapp.feature +5 -0
- package/functional_tests/pip-requirements.txt +2 -0
- package/html/favicon.svg +126 -0
- package/html/index.html +14 -0
- package/html/style.css +2 -0
- package/package.json +61 -0
- package/src/App.css +3 -0
- package/src/App.tsx +266 -0
- package/src/assets/mpeg.svg +143 -0
- package/src/codemirror/ruler.ts +58 -0
- package/src/components/DesktopPanels.tsx +65 -0
- package/src/components/Editor.tsx +384 -0
- package/src/components/MobileDrawer.tsx +57 -0
- package/src/components/Navbar.tsx +226 -0
- package/src/components/ResizableLayout.tsx +69 -0
- package/src/components/Settings.tsx +170 -0
- package/src/components/StatusBar.tsx +47 -0
- package/src/components/SubNav.tsx +58 -0
- package/src/hooks/useAutoDisplayCompletions.ts +26 -0
- package/src/hooks/useDragResize.ts +104 -0
- package/src/hooks/useEnableLinting.ts +21 -0
- package/src/hooks/useFileOperations.ts +102 -0
- package/src/hooks/useMobileDetection.ts +32 -0
- package/src/hooks/usePrettier.ts +40 -0
- package/src/hooks/useRulerWidth.ts +42 -0
- package/src/hooks/useShowSemanticWarnings.ts +23 -0
- package/src/hooks/useShowSymanticErrors.ts +23 -0
- package/src/hooks/useShowSyntaxErrors.ts +23 -0
- package/src/hooks/useTheme.ts +21 -0
- package/src/hooks/useToast.ts +37 -0
- package/src/logo/getLogoSvg.ts +5 -0
- package/src/logo/svg.module.d.ts +5 -0
- package/src/main.tsx +13 -0
- package/src/sdl/sdlComplete.ts +86 -0
- package/src/sdl/sdlLanguage.ts +18 -0
- package/src/sdl/sdlLinter.ts +157 -0
- package/tailwind.config.ts +16 -0
- package/tests/Editor_test.tsx +29 -0
- package/tests/happydom.ts +3 -0
- package/tests/sdlLanguage_test.ts +39 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export const RULER_WIDTH_OPTIONS = [
|
|
4
|
+
60,
|
|
5
|
+
80,
|
|
6
|
+
100,
|
|
7
|
+
120,
|
|
8
|
+
140,
|
|
9
|
+
160,
|
|
10
|
+
180,
|
|
11
|
+
200,
|
|
12
|
+
] as const;
|
|
13
|
+
export type RulerWidth = (typeof RULER_WIDTH_OPTIONS)[number];
|
|
14
|
+
|
|
15
|
+
function isValidRulerWidth(value: number): value is RulerWidth {
|
|
16
|
+
return RULER_WIDTH_OPTIONS.includes(value as RulerWidth);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useRulerWidth(defaultWidth: RulerWidth = 80) {
|
|
20
|
+
const [rulerWidth, setRulerWidthInternal] = useState<RulerWidth>(() => {
|
|
21
|
+
const stored = localStorage.getItem("rulerWidth");
|
|
22
|
+
if (stored) {
|
|
23
|
+
const parsed = Number(stored);
|
|
24
|
+
if (isValidRulerWidth(parsed)) {
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return defaultWidth;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
localStorage.setItem("rulerWidth", String(rulerWidth));
|
|
33
|
+
}, [rulerWidth]);
|
|
34
|
+
|
|
35
|
+
const setRulerWidth = useCallback((width: number) => {
|
|
36
|
+
if (isValidRulerWidth(width)) {
|
|
37
|
+
setRulerWidthInternal(width);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return { rulerWidth, setRulerWidth };
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useShowSemanticWarnings(defaultValue: boolean = true) {
|
|
4
|
+
const [showSemanticWarnings, setShowSemanticWarningsInternal] = useState<
|
|
5
|
+
boolean
|
|
6
|
+
>(() => {
|
|
7
|
+
const stored = localStorage.getItem("showSemanticWarnings");
|
|
8
|
+
if (stored !== null) {
|
|
9
|
+
return stored === "true";
|
|
10
|
+
}
|
|
11
|
+
return defaultValue;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
localStorage.setItem("showSemanticWarnings", String(showSemanticWarnings));
|
|
16
|
+
}, [showSemanticWarnings]);
|
|
17
|
+
|
|
18
|
+
const setShowSemanticWarnings = useCallback((value: boolean) => {
|
|
19
|
+
setShowSemanticWarningsInternal(value);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
return { showSemanticWarnings, setShowSemanticWarnings };
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useShowSemanticErrors(defaultValue: boolean = true) {
|
|
4
|
+
const [showSemanticErrors, setShowSemanticErrorsInternal] = useState<boolean>(
|
|
5
|
+
() => {
|
|
6
|
+
const stored = localStorage.getItem("showSemanticErrors");
|
|
7
|
+
if (stored !== null) {
|
|
8
|
+
return stored === "true";
|
|
9
|
+
}
|
|
10
|
+
return defaultValue;
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
localStorage.setItem("showSemanticErrors", String(showSemanticErrors));
|
|
16
|
+
}, [showSemanticErrors]);
|
|
17
|
+
|
|
18
|
+
const setShowSemanticErrors = useCallback((value: boolean) => {
|
|
19
|
+
setShowSemanticErrorsInternal(value);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
return { showSemanticErrors, setShowSemanticErrors };
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useShowSyntaxErrors(defaultValue: boolean = true) {
|
|
4
|
+
const [showSyntaxErrors, setShowSyntaxErrorsInternal] = useState<boolean>(
|
|
5
|
+
() => {
|
|
6
|
+
const stored = localStorage.getItem("showSyntaxErrors");
|
|
7
|
+
if (stored !== null) {
|
|
8
|
+
return stored === "true";
|
|
9
|
+
}
|
|
10
|
+
return defaultValue;
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
localStorage.setItem("showSyntaxErrors", String(showSyntaxErrors));
|
|
16
|
+
}, [showSyntaxErrors]);
|
|
17
|
+
|
|
18
|
+
const setShowSyntaxErrors = useCallback((value: boolean) => {
|
|
19
|
+
setShowSyntaxErrorsInternal(value);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
return { showSyntaxErrors, setShowSyntaxErrors };
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type Theme = "light" | "dark";
|
|
4
|
+
|
|
5
|
+
export function useTheme(defaultTheme: Theme = "light") {
|
|
6
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
7
|
+
const storedTheme = localStorage.getItem("theme") as Theme | null;
|
|
8
|
+
return storedTheme || defaultTheme;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
13
|
+
localStorage.setItem("theme", theme);
|
|
14
|
+
}, [theme]);
|
|
15
|
+
|
|
16
|
+
const toggleTheme = () => {
|
|
17
|
+
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return { theme, toggleTheme };
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type ToastType = "success" | "warning" | "error";
|
|
4
|
+
export type ShowToastFunction = (
|
|
5
|
+
message: string,
|
|
6
|
+
type?: ToastType,
|
|
7
|
+
duration?: number,
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
interface ToastState {
|
|
11
|
+
message: string;
|
|
12
|
+
type: ToastType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useToast(): [ToastState | null, ShowToastFunction] {
|
|
16
|
+
const [toastState, setToastState] = useState<ToastState | null>(null);
|
|
17
|
+
const [toastDuration, setToastDuration] = useState<number>(2000);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (toastState) {
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
setToastState(null);
|
|
23
|
+
}, toastDuration);
|
|
24
|
+
return () => clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
}, [toastState, toastDuration]);
|
|
27
|
+
|
|
28
|
+
const showToast = useCallback(
|
|
29
|
+
(message: string, type: ToastType = "success", duration: number = 2000) => {
|
|
30
|
+
setToastState({ message, type });
|
|
31
|
+
setToastDuration(duration);
|
|
32
|
+
},
|
|
33
|
+
[],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return [toastState, showToast];
|
|
37
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createRoot } from "react-dom/client";
|
|
2
|
+
import { App } from "./App.tsx";
|
|
3
|
+
|
|
4
|
+
function start() {
|
|
5
|
+
const root = createRoot(document.getElementById("root")!);
|
|
6
|
+
root.render(<App />);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (document.readyState === "loading") {
|
|
10
|
+
document.addEventListener("DOMContentLoaded", start);
|
|
11
|
+
} else {
|
|
12
|
+
start();
|
|
13
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CompletionContext,
|
|
3
|
+
type CompletionResult,
|
|
4
|
+
} from "@codemirror/autocomplete";
|
|
5
|
+
import type { SyntaxNode } from "@lezer/common";
|
|
6
|
+
import { syntaxTree } from "@codemirror/language";
|
|
7
|
+
import { foldNodeProp } from "@codemirror/language";
|
|
8
|
+
import {
|
|
9
|
+
getPotentialSyntacticTokens,
|
|
10
|
+
TokenTypeId,
|
|
11
|
+
} from "@mpeggroup/mpeg-sdl-parser";
|
|
12
|
+
|
|
13
|
+
function isCommentCompletion(lastNode: SyntaxNode): boolean {
|
|
14
|
+
return lastNode.type.id === TokenTypeId.Comment;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isGlobalScopeCompletion(lastNode: SyntaxNode): boolean {
|
|
18
|
+
// see if the last node is a specification
|
|
19
|
+
if (lastNode.type.id === TokenTypeId.Specification) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// see if the parent of the last node is a specification
|
|
24
|
+
const parentNode = lastNode.parent;
|
|
25
|
+
|
|
26
|
+
return parentNode?.type.id === TokenTypeId.Specification;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isBlockScopeCompletion(lastNode: SyntaxNode): boolean {
|
|
30
|
+
// see if the parent of the last node is a block scoped node
|
|
31
|
+
const parentNode = lastNode.parent;
|
|
32
|
+
|
|
33
|
+
if (!parentNode) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return parentNode.type.prop(foldNodeProp) !== undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getCompletionResult(
|
|
41
|
+
completionOptions: string[],
|
|
42
|
+
from: number,
|
|
43
|
+
): CompletionResult {
|
|
44
|
+
return {
|
|
45
|
+
from,
|
|
46
|
+
options: completionOptions.map((label) => ({ label })),
|
|
47
|
+
validFor: /^\w*$/,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sdlComplete(context: CompletionContext): CompletionResult | null {
|
|
52
|
+
const parseTree = syntaxTree(context.state);
|
|
53
|
+
const lastNode = parseTree.resolveInner(context.pos, -1);
|
|
54
|
+
|
|
55
|
+
// Don't provide completions within a comment
|
|
56
|
+
if (isCommentCompletion(lastNode)) {
|
|
57
|
+
console.error("No completions in comments");
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// only provide completions at the global scope if completion was explicitly requested
|
|
62
|
+
if (isGlobalScopeCompletion(lastNode) && !context.explicit) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// only provide completions at the block scope if completion was explicitly requested
|
|
67
|
+
if (isBlockScopeCompletion(lastNode) && !context.explicit) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lastText = context.state.sliceDoc(lastNode.from, context.pos);
|
|
72
|
+
const lastTag = /^\w*$/.exec(lastText);
|
|
73
|
+
|
|
74
|
+
const completions = getPotentialSyntacticTokens(lastNode.cursor());
|
|
75
|
+
|
|
76
|
+
if (completions && (completions.length > 0)) {
|
|
77
|
+
return getCompletionResult(
|
|
78
|
+
completions,
|
|
79
|
+
lastTag ? lastNode.from + lastTag.index : context.pos,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { sdlComplete };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createLenientSdlParser } from "@mpeggroup/mpeg-sdl-parser";
|
|
2
|
+
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
|
3
|
+
import { sdlComplete } from "./sdlComplete.ts";
|
|
4
|
+
|
|
5
|
+
export const sdlLanguage = LRLanguage.define({
|
|
6
|
+
parser: createLenientSdlParser(),
|
|
7
|
+
languageData: {
|
|
8
|
+
commentTokens: { line: "//" },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const sdlCompletion = sdlLanguage.data.of({
|
|
13
|
+
autocomplete: sdlComplete,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function sdl(): LanguageSupport {
|
|
17
|
+
return new LanguageSupport(sdlLanguage, [sdlCompletion]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { syntaxTree } from "@codemirror/language";
|
|
2
|
+
import {
|
|
3
|
+
type Diagnostic,
|
|
4
|
+
forEachDiagnostic,
|
|
5
|
+
linter,
|
|
6
|
+
setDiagnostics,
|
|
7
|
+
} from "@codemirror/lint";
|
|
8
|
+
import { type Extension } from "@codemirror/state";
|
|
9
|
+
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
|
10
|
+
import {
|
|
11
|
+
buildAst,
|
|
12
|
+
collateSyntaxErrors,
|
|
13
|
+
createLenientSdlAnalyser,
|
|
14
|
+
SdlStringInput,
|
|
15
|
+
SemanticError,
|
|
16
|
+
SemanticWarning,
|
|
17
|
+
Specification,
|
|
18
|
+
SyntaxError,
|
|
19
|
+
} from "@mpeggroup/mpeg-sdl-parser";
|
|
20
|
+
|
|
21
|
+
interface SdlLinterOptions {
|
|
22
|
+
onSyntaxErrorChange: (syntaxErrors: SyntaxError[]) => void;
|
|
23
|
+
onSemanticErrorChange: (semanticErrors: SemanticError[]) => void;
|
|
24
|
+
onSemanticWarningChange: (semanticWarnings: SemanticWarning[]) => void;
|
|
25
|
+
showSyntaxErrors: boolean;
|
|
26
|
+
showSemanticErrors: boolean;
|
|
27
|
+
showSemanticWarnings: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEBOUNCE_MS = 100;
|
|
31
|
+
|
|
32
|
+
export function sdlLinter(options: SdlLinterOptions): Extension {
|
|
33
|
+
// Dispatch clear then set as two separate transactions so CodeMirror destroys
|
|
34
|
+
// and recreates the widget DOM node rather than reusing/moving it. Safari's
|
|
35
|
+
// contenteditable engine silently ignores in-place widget moves, so without
|
|
36
|
+
// the clear the indicator never visually repositions.
|
|
37
|
+
function dispatchDiagnostics(view: EditorView, diagnostics: Diagnostic[]) {
|
|
38
|
+
view.dispatch(setDiagnostics(view.state, []));
|
|
39
|
+
view.dispatch(setDiagnostics(view.state, diagnostics));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runLint(view: EditorView) {
|
|
43
|
+
const currentDoc = view.state.doc;
|
|
44
|
+
try {
|
|
45
|
+
const diagnostics: Diagnostic[] = [];
|
|
46
|
+
const sdlParseTree = syntaxTree(view.state);
|
|
47
|
+
const sdlStringInput = new SdlStringInput(view.state.doc.toString());
|
|
48
|
+
const syntaxErrors = collateSyntaxErrors(sdlParseTree, sdlStringInput);
|
|
49
|
+
const specification = buildAst(sdlParseTree, sdlStringInput, true);
|
|
50
|
+
const analyser = createLenientSdlAnalyser();
|
|
51
|
+
const analysisResult = analyser.analyse(specification as Specification);
|
|
52
|
+
|
|
53
|
+
if (options.showSyntaxErrors) {
|
|
54
|
+
for (const error of syntaxErrors) {
|
|
55
|
+
diagnostics.push({
|
|
56
|
+
from: error.location!.position,
|
|
57
|
+
to: error.location!.position,
|
|
58
|
+
severity: "error",
|
|
59
|
+
message: error.errorMessage,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (options.showSemanticErrors) {
|
|
64
|
+
for (const error of analysisResult.semanticErrors) {
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
from: error.location!.position,
|
|
67
|
+
to: error.location!.position,
|
|
68
|
+
severity: "error",
|
|
69
|
+
message: error.errorMessage,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.showSemanticWarnings) {
|
|
74
|
+
for (const warning of analysisResult.semanticWarnings) {
|
|
75
|
+
diagnostics.push({
|
|
76
|
+
from: warning.location!.position,
|
|
77
|
+
to: warning.location!.position,
|
|
78
|
+
severity: "warning",
|
|
79
|
+
message: warning.errorMessage,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options.onSyntaxErrorChange) {
|
|
85
|
+
options.onSyntaxErrorChange(syntaxErrors);
|
|
86
|
+
}
|
|
87
|
+
if (options.onSemanticErrorChange) {
|
|
88
|
+
options.onSemanticErrorChange(analysisResult.semanticErrors);
|
|
89
|
+
}
|
|
90
|
+
if (options.onSemanticWarningChange) {
|
|
91
|
+
options.onSemanticWarningChange(analysisResult.semanticWarnings);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (view.state.doc === currentDoc) {
|
|
95
|
+
dispatchDiagnostics(view, diagnostics);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
if (view.state.doc === currentDoc) {
|
|
99
|
+
view.dispatch(setDiagnostics(view.state, []));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rafLintPlugin = ViewPlugin.fromClass(
|
|
105
|
+
class {
|
|
106
|
+
private lastChangeAt = 0;
|
|
107
|
+
private rafId = 0;
|
|
108
|
+
|
|
109
|
+
constructor(private readonly view: EditorView) {
|
|
110
|
+
requestAnimationFrame(() => runLint(this.view));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
update(update: ViewUpdate) {
|
|
114
|
+
if (!update.docChanged) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Immediately remap existing diagnostics with positive bias so
|
|
119
|
+
// insertions AT a zero-width marker push it forward right away.
|
|
120
|
+
const remapped: Diagnostic[] = [];
|
|
121
|
+
forEachDiagnostic(update.startState, (d, from, to) => {
|
|
122
|
+
remapped.push({
|
|
123
|
+
...d,
|
|
124
|
+
from: update.changes.mapPos(from, 1),
|
|
125
|
+
to: update.changes.mapPos(to, 1),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
const snapDoc = update.state.doc;
|
|
129
|
+
requestAnimationFrame(() => {
|
|
130
|
+
if (this.view.state.doc === snapDoc) {
|
|
131
|
+
dispatchDiagnostics(this.view, remapped);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.lastChangeAt = Date.now();
|
|
136
|
+
cancelAnimationFrame(this.rafId);
|
|
137
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private tick() {
|
|
141
|
+
if (Date.now() - this.lastChangeAt >= DEBOUNCE_MS) {
|
|
142
|
+
runLint(this.view);
|
|
143
|
+
} else {
|
|
144
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
destroy() {
|
|
149
|
+
cancelAnimationFrame(this.rafId);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// linter(null) registers the diagnostic state field, decorations and theme
|
|
155
|
+
// without attaching a lint source — we drive everything from rafLintPlugin.
|
|
156
|
+
return [linter(null), rafLintPlugin];
|
|
157
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const tailwindConfig: import("tailwindcss").Config & { daisyui?: any } = {
|
|
2
|
+
content: [
|
|
3
|
+
"./src/**/*.{html,js,jsx,ts,tsx}",
|
|
4
|
+
],
|
|
5
|
+
theme: {
|
|
6
|
+
extend: {},
|
|
7
|
+
},
|
|
8
|
+
plugins: [
|
|
9
|
+
require("daisyui"),
|
|
10
|
+
],
|
|
11
|
+
daisyui: {
|
|
12
|
+
themes: ["light", "dark"],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default tailwindConfig;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { Editor } from "../src/components/Editor.tsx";
|
|
4
|
+
|
|
5
|
+
describe("Editor component", () => {
|
|
6
|
+
test("renders with initial value", () => {
|
|
7
|
+
const { getAllByRole } = render(
|
|
8
|
+
<Editor
|
|
9
|
+
code="int a;"
|
|
10
|
+
onCodeChange={() => {}}
|
|
11
|
+
onCursorChange={() => {}}
|
|
12
|
+
onSyntaxErrorChange={() => {}}
|
|
13
|
+
onSemanticErrorChange={() => {}}
|
|
14
|
+
onSemanticWarningChange={() => {}}
|
|
15
|
+
theme="light"
|
|
16
|
+
rulerWidth={80}
|
|
17
|
+
autoDisplayCompletions
|
|
18
|
+
enableLinting
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
const textboxes = getAllByRole("textbox");
|
|
22
|
+
|
|
23
|
+
// Check that at least one textbox contains the expected content
|
|
24
|
+
const found = textboxes.some(
|
|
25
|
+
(el) => el.textContent && el.textContent.includes("int a;"),
|
|
26
|
+
);
|
|
27
|
+
expect(found).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { sdlLanguage } from "../src/sdl/sdlLanguage.ts";
|
|
3
|
+
|
|
4
|
+
describe("sdlLanguage", () => {
|
|
5
|
+
test("parses valid SDL without error", () => {
|
|
6
|
+
const parser = sdlLanguage.parser;
|
|
7
|
+
const input = "computed const int a = 1;";
|
|
8
|
+
const tree = parser.parse(input);
|
|
9
|
+
|
|
10
|
+
// The root node should not be an error
|
|
11
|
+
expect(tree.type.isError).toBe(false);
|
|
12
|
+
|
|
13
|
+
// There should be no error nodes in the tree
|
|
14
|
+
let hasError = false;
|
|
15
|
+
tree.iterate({
|
|
16
|
+
enter: (type) => {
|
|
17
|
+
if (type.type.isError) hasError = true;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(hasError).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("parses invalid SDL with error node", () => {
|
|
25
|
+
const parser = sdlLanguage.parser;
|
|
26
|
+
const input = "invalid sdl $";
|
|
27
|
+
const tree = parser.parse(input);
|
|
28
|
+
|
|
29
|
+
// The tree should contain an error node
|
|
30
|
+
let hasError = false;
|
|
31
|
+
tree.iterate({
|
|
32
|
+
enter: (type) => {
|
|
33
|
+
if (type.type.isError) hasError = true;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(hasError).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Enable latest features
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
|
|
22
|
+
// Some stricter flags (disabled by default)
|
|
23
|
+
"noUnusedLocals": false,
|
|
24
|
+
"noUnusedParameters": false,
|
|
25
|
+
"noPropertyAccessFromIndexSignature": false
|
|
26
|
+
}
|
|
27
|
+
}
|