From 7cc2f7cab2a360e944e616607a2809683a09b2a8 Mon Sep 17 00:00:00 2001 From: hugo Date: Mon, 23 Feb 2026 21:38:12 +0100 Subject: [PATCH] feat: added syntax check --- .../components/ScriptsView/ScriptsView.css | 6 + .../components/ScriptsView/ScriptsView.tsx | 130 +++++++++++++++++- src/renderer/i18n/locales/de.json | 5 + src/renderer/i18n/locales/en.json | 5 + src/renderer/i18n/locales/es.json | 5 + src/renderer/i18n/locales/fr.json | 5 + src/renderer/i18n/locales/it.json | 5 + src/renderer/python/PythonRuntimeManager.ts | 54 +++++++- src/renderer/python/pythonRuntime.worker.ts | 39 ++++++ src/renderer/python/pythonSyntaxCheck.ts | 73 ++++++++++ src/renderer/python/runtimeProtocol.ts | 15 ++ .../renderer/components/ScriptsView.test.tsx | 91 +++++++++++- .../python/PythonRuntimeManager.test.ts | 41 ++++++ .../pythonSyntaxCheck.integration.test.ts | 43 ++++++ 14 files changed, 507 insertions(+), 10 deletions(-) create mode 100644 src/renderer/python/pythonSyntaxCheck.ts create mode 100644 tests/renderer/python/pythonSyntaxCheck.integration.test.ts diff --git a/src/renderer/components/ScriptsView/ScriptsView.css b/src/renderer/components/ScriptsView/ScriptsView.css index 5f89456..d50808c 100644 --- a/src/renderer/components/ScriptsView/ScriptsView.css +++ b/src/renderer/components/ScriptsView/ScriptsView.css @@ -52,3 +52,9 @@ font-size: 12px; border-radius: 4px; } + +.scripts-check-button { + padding: 4px 12px; + font-size: 12px; + border-radius: 4px; +} diff --git a/src/renderer/components/ScriptsView/ScriptsView.tsx b/src/renderer/components/ScriptsView/ScriptsView.tsx index 3d58642..c0e0a01 100644 --- a/src/renderer/components/ScriptsView/ScriptsView.tsx +++ b/src/renderer/components/ScriptsView/ScriptsView.tsx @@ -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 = { en: 'en-US', de: 'de-DE', @@ -35,6 +57,9 @@ export const ScriptsView: React.FC = ({ scriptId }) => { const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false); const [isRunning, setIsRunning] = useState(false); const [isSaving, setIsSaving] = useState(false); + const [isCheckingSyntax, setIsCheckingSyntax] = useState(false); + const editorRef = useRef(null); + const monacoRef = useRef(null); const buildCacheKey = (scriptMeta: Pick, content: string): string => { let hash = 0; @@ -57,6 +82,87 @@ export const ScriptsView: React.FC = ({ 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 => { + 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 = ({ 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 = ({ scriptId }) => { kind: 'error', }); } finally { - useAppStore.setState({ - panelVisible: true, - panelActiveTab: 'output', - }); setIsRunning(false); } }; @@ -312,6 +419,16 @@ export const ScriptsView: React.FC = ({ scriptId }) => { > {t('scripts.run')} +