Skip to content

Commit

Permalink
Merge pull request #100 from flatironinstitute/stan-editor-squiggles
Browse files Browse the repository at this point in the history
implement error/warning squiggles in stan editor
  • Loading branch information
magland authored Jun 27, 2024
2 parents 5e41980 + f542bbb commit 245839a
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 4 deletions.
89 changes: 87 additions & 2 deletions gui/src/app/FileEditor/StanFileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -157,7 +158,7 @@ const StanFileEditor: FunctionComponent<Props> = ({ 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'

Expand All @@ -183,6 +184,7 @@ const StanFileEditor: FunctionComponent<Props> = ({ fileName, fileContent, onSav
onSetEditedText={setEditedFileContent}
readOnly={!isCompiling ? readOnly : true}
toolbarItems={toolbarItems}
codeMarkers={stancErrorsToCodeMarkers(stancErrors)}
/>
{
editedFileContent ? <StanCompileResultWindow
Expand Down Expand Up @@ -213,5 +215,88 @@ const stringChecksum = (str: string) => {
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
41 changes: 39 additions & 2 deletions gui/src/app/FileEditor/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +33,7 @@ type Props = {
label: string
width: number
height: number
codeMarkers?: CodeMarker[]
}

export type ToolbarItem = {
Expand All @@ -38,7 +49,7 @@ export type ToolbarItem = {
color?: string
}

const TextEditor: FunctionComponent<Props> = ({defaultText, text, onSaveText, editedText, onSetEditedText, readOnly, wordWrap, onReload, toolbarItems, language, label, width, height}) => {
const TextEditor: FunctionComponent<Props> = ({defaultText, text, onSaveText, editedText, onSetEditedText, readOnly, wordWrap, onReload, toolbarItems, language, label, width, height, codeMarkers}) => {
const handleChange = useCallback((value: string | undefined) => {
onSetEditedText(value || '')
}, [onSetEditedText])
Expand All @@ -60,10 +71,27 @@ const TextEditor: FunctionComponent<Props> = ({defaultText, text, onSaveText, ed
if (editor.getValue() === editedText) return
editor.setValue(editedText || defaultText || '')
}, [editedText, editor, defaultText])
const [monacoInstance, setMonacoInstance] = useState<Monaco | undefined>(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,
Expand Down Expand Up @@ -186,6 +214,15 @@ const TextEditor: FunctionComponent<Props> = ({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
Expand Down

0 comments on commit 245839a

Please sign in to comment.