diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 952825ed..8cfe5a86 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -3,8 +3,9 @@ import { AutoFixHigh, Cancel, Settings, } from "@mui/icons-material"; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react"; import StanCompileResultWindow from "./StanCompileResultWindow"; import useStanc from "../Stanc/useStanc"; -import TextEditor, { ToolbarItem } from "./TextEditor"; +import TextEditor, { CodeMarker, ToolbarItem } from "./TextEditor"; import compileStanProgram from '../compileStanProgram/compileStanProgram'; +import { StancErrors } from '../Stanc/Types'; type Props = { fileName: string @@ -157,7 +158,7 @@ const StanFileEditor: FunctionComponent = ({ fileName, fileContent, onSav } return ret - }, [editedFileContent, fileContent, handleCompile, requestFormat, showLabelsOnButtons, validSyntax, compileStatus, compileMessage, readOnly]) + }, [editedFileContent, fileContent, handleCompile, requestFormat, showLabelsOnButtons, validSyntax, compileStatus, compileMessage, readOnly, hasWarnings]) const isCompiling = compileStatus === 'compiling' @@ -183,6 +184,7 @@ const StanFileEditor: FunctionComponent = ({ fileName, fileContent, onSav onSetEditedText={setEditedFileContent} readOnly={!isCompiling ? readOnly : true} toolbarItems={toolbarItems} + codeMarkers={stancErrorsToCodeMarkers(stancErrors)} /> { editedFileContent ? { return hash; } +const stancErrorsToCodeMarkers = (stancErrors: StancErrors) => { + const codeMarkers: CodeMarker[] = [] + + for (const x of stancErrors.errors || []) { + const marker = stancErrorStringToMarker(x, 'error') + if (marker) codeMarkers.push(marker) + } + for (const x of stancErrors.warnings || []) { + const marker = stancErrorStringToMarker(x, 'warning') + if (marker) codeMarkers.push(marker) + } + + return codeMarkers +} + +const stancErrorStringToMarker = (x: string, severity: 'error' | 'warning'): CodeMarker | undefined => { + if (!x) return undefined + + // Example: Syntax error in 'main.stan', line 1, column 0 to column 1, parsing error: + + let lineNumber: number | undefined = undefined + let startColumn: number | undefined = undefined + let endColumn: number | undefined = undefined + + const sections = x.split(',').map(x => x.trim()) + for (const section of sections) { + if ((section.startsWith('line ')) && (lineNumber === undefined)) { + lineNumber = parseInt(section.slice('line '.length)) + } + else if ((section.startsWith('column ')) && (startColumn === undefined)) { + const cols = section.slice('column '.length).split(' to ') + startColumn = parseInt(cols[0]) + endColumn = cols.length > 1 ? parseInt(cols[1].slice('column '.length)) : startColumn + 1 + } + } + + if ((lineNumber !== undefined) && (startColumn !== undefined) && (endColumn !== undefined)) { + return { + startLineNumber: lineNumber, + startColumn: startColumn + 1, + endLineNumber: lineNumber, + endColumn: endColumn + 1, + message: severity === 'warning' ? getWarningMessage(x) : getErrorMessage(x), + severity + } + } + else { + return undefined + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Adapted from https://github.com/WardBrian/vscode-stan-extension +function getWarningMessage(message: string) { + let warning = message.replace(/Warning.*column \d+: /s, ""); + warning = warning.replace(/\s+/gs, " "); + warning = warning.trim(); + warning = message.includes("included from") + ? "Warning in included file:\n" + warning + : warning; + return warning; +} + +function getErrorMessage(message: string) { + let error = message; + // cut off code snippet for display + if (message.includes("------\n")) { + error = error.split("------\n")[2]; + } + error = error.trim(); + error = message.includes("included from") + ? "Error in included file:\n" + error + : error; + + // only relevant to vscode-stan-extension: + // error = error.includes("given information about") + // ? error + + // "\nConsider updating the includePaths setting of vscode-stan-extension" + // : error; + + return error; +} +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// export default StanFileEditor diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index 765586ca..973249df 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -9,6 +9,16 @@ import { Hyperlink, SmallIconButton } from "@fi-sci/misc"; type Monaco = typeof monaco +// An interface for passing markers (squiggles) to the editor without depending on monaco types +export type CodeMarker = { + startLineNumber: number + startColumn: number + endLineNumber: number + endColumn: number + message: string + severity: 'error' | 'warning' | 'hint' | 'info' +} + type Props = { defaultText?: string text: string | undefined @@ -23,6 +33,7 @@ type Props = { label: string width: number height: number + codeMarkers?: CodeMarker[] } export type ToolbarItem = { @@ -38,7 +49,7 @@ export type ToolbarItem = { color?: string } -const TextEditor: FunctionComponent = ({defaultText, text, onSaveText, editedText, onSetEditedText, readOnly, wordWrap, onReload, toolbarItems, language, label, width, height}) => { +const TextEditor: FunctionComponent = ({defaultText, text, onSaveText, editedText, onSetEditedText, readOnly, wordWrap, onReload, toolbarItems, language, label, width, height, codeMarkers}) => { const handleChange = useCallback((value: string | undefined) => { onSetEditedText(value || '') }, [onSetEditedText]) @@ -60,10 +71,27 @@ const TextEditor: FunctionComponent = ({defaultText, text, onSaveText, ed if (editor.getValue() === editedText) return editor.setValue(editedText || defaultText || '') }, [editedText, editor, defaultText]) + const [monacoInstance, setMonacoInstance] = useState(undefined) + useEffect(() => { + if (!monacoInstance) return + if (codeMarkers === undefined) return + if (editor === undefined) return + const model = editor.getModel() + if (model === null) return + const modelMarkers = codeMarkers.map(marker => ({ + startLineNumber: marker.startLineNumber, + startColumn: marker.startColumn, + endLineNumber: marker.endLineNumber, + endColumn: marker.endColumn, + message: marker.message, + severity: toMonacoMarkerSeverity(marker.severity) + })) + monacoInstance.editor.setModelMarkers(model, 'stan-playground', modelMarkers) + }, [codeMarkers, monacoInstance, editor]) const handleEditorDidMount = useCallback((editor: editor.IStandaloneCodeEditor, monaco: Monaco) => { + setMonacoInstance(monaco); (async () => { if (language === 'stan') { - monaco.editor.defineTheme('vs-stan', { base: 'vs-dark', inherit: true, @@ -186,6 +214,15 @@ const TextEditor: FunctionComponent = ({defaultText, text, onSaveText, ed ) } +const toMonacoMarkerSeverity = (s: 'error' | 'warning' | 'hint' | 'info'): monaco.MarkerSeverity => { + switch (s) { + case 'error': return monaco.MarkerSeverity.Error + case 'warning': return monaco.MarkerSeverity.Warning + case 'hint': return monaco.MarkerSeverity.Hint + case 'info': return monaco.MarkerSeverity.Info + } +} + const ToolbarItemComponent: FunctionComponent<{item: ToolbarItem}> = ({item}) => { if (item.type === 'button') { const {onClick, color, label, tooltip, icon} = item