feat: added syntax check

This commit is contained in:
2026-02-23 21:38:12 +01:00
parent e68e845e70
commit 7cc2f7cab2
14 changed files with 507 additions and 10 deletions

View File

@@ -52,3 +52,9 @@
font-size: 12px;
border-radius: 4px;
}
.scripts-check-button {
padding: 4px 12px;
font-size: 12px;
border-radius: 4px;
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import MonacoEditor from '@monaco-editor/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import MonacoEditor, { type Monaco } from '@monaco-editor/react';
import type { ScriptData } from '../../../main/shared/electronApi';
import { useAppStore } from '../../store';
import { BDS_EVENT_SCRIPTS_CHANGED, dispatchWindowEvent } from '../../utils';
@@ -8,6 +8,28 @@ import { useI18n } from '../../i18n';
import { showToast } from '../Toast';
import './ScriptsView.css';
type ScriptMonacoEditor = {
getModel: () => unknown;
};
type ScriptMonacoRuntime = {
editor: {
setModelMarkers: (model: unknown, owner: string, markers: Array<{
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
message: string;
severity: number;
}>) => void;
};
MarkerSeverity: {
Error: number;
};
};
const SCRIPT_SYNTAX_MARKER_OWNER = 'scripts-python-syntax';
const UI_DATE_LOCALE: Record<string, string> = {
en: 'en-US',
de: 'de-DE',
@@ -35,6 +57,9 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isCheckingSyntax, setIsCheckingSyntax] = useState(false);
const editorRef = useRef<ScriptMonacoEditor | null>(null);
const monacoRef = useRef<ScriptMonacoRuntime | null>(null);
const buildCacheKey = (scriptMeta: Pick<ScriptData, 'id' | 'version'>, content: string): string => {
let hash = 0;
@@ -57,6 +82,87 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
return normalized || 'script';
};
const applySyntaxMarkers = useCallback((errors: Array<{
line: number;
column: number;
endLine: number;
endColumn: number;
message: string;
}>) => {
const model = editorRef.current?.getModel?.();
const monacoRuntime = monacoRef.current;
if (!model || !monacoRuntime) {
return;
}
const markers = errors.map((error) => {
const startLineNumber = Math.max(1, Math.floor(error.line || 1));
const startColumn = Math.max(1, Math.floor(error.column || 1));
const endLineNumber = Math.max(startLineNumber, Math.floor(error.endLine || startLineNumber));
const fallbackEndColumn = startColumn + 1;
const endColumn = Math.max(startColumn, Math.floor(error.endColumn || fallbackEndColumn));
return {
startLineNumber,
startColumn,
endLineNumber,
endColumn,
message: error.message,
severity: monacoRuntime.MarkerSeverity.Error,
};
});
monacoRuntime.editor.setModelMarkers(model, SCRIPT_SYNTAX_MARKER_OWNER, markers);
}, []);
const handleEditorDidMount = useCallback((editor: unknown, monaco: Monaco) => {
editorRef.current = editor as ScriptMonacoEditor;
monacoRef.current = monaco as unknown as ScriptMonacoRuntime;
}, []);
const handleCheckSyntax = useCallback(async (options: { notify: boolean } = { notify: true }): Promise<boolean> => {
if (!script || isCheckingSyntax) {
return false;
}
setIsCheckingSyntax(true);
try {
const runtimeManager = getPythonRuntimeManager();
const syntax = await runtimeManager.syntaxCheck(scriptContent, {
cacheKey: buildCacheKey(script, scriptContent),
});
applySyntaxMarkers(syntax.errors);
if (syntax.errors.length > 0) {
if (options.notify) {
showToast.error(t('scripts.syntax.invalid', { count: syntax.errors.length }));
}
return false;
}
if (options.notify) {
showToast.success(t('scripts.syntax.valid'));
}
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
appendPanelOutputEntry({
id: `output-${Date.now()}-syntax-error`,
message,
createdAt: new Date().toISOString(),
kind: 'error',
});
if (options.notify) {
showToast.error(t('scripts.syntax.checkFailed'));
}
return false;
} finally {
setIsCheckingSyntax(false);
}
}, [appendPanelOutputEntry, applySyntaxMarkers, isCheckingSyntax, script, scriptContent, t]);
useEffect(() => {
let cancelled = false;
@@ -162,6 +268,11 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
setIsSaving(true);
try {
const isSyntaxValid = await handleCheckSyntax({ notify: true });
if (!isSyntaxValid) {
return;
}
const runtimeManager = getPythonRuntimeManager();
const discoveredEntrypoints = await runtimeManager.inspectEntrypoints(scriptContent, {
cacheKey: buildCacheKey(script, scriptContent),
@@ -279,10 +390,6 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
kind: 'error',
});
} finally {
useAppStore.setState({
panelVisible: true,
panelActiveTab: 'output',
});
setIsRunning(false);
}
};
@@ -312,6 +419,16 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
>
{t('scripts.run')}
</button>
<button
type="button"
className="scripts-check-button"
onClick={() => {
void handleCheckSyntax({ notify: true });
}}
disabled={!script || isCheckingSyntax || isSaving}
>
{isCheckingSyntax ? t('scripts.syntax.checking') : t('scripts.syntax.check')}
</button>
<button
type="button"
className="secondary danger"
@@ -408,6 +525,7 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
theme="vs-dark"
value={scriptContent}
onChange={(value) => setScriptContent(value || '')}
onMount={handleEditorDidMount}
options={{
minimap: { enabled: false },
wordWrap: 'on',