From c5ee33ea47037207ad72830e9acdc0eaabc2a110 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Wed, 26 Jun 2024 15:56:57 -0400 Subject: [PATCH 01/11] collapsible side panel --- .../SPAnalysis/SPAnalysisContextProvider.tsx | 4 +- gui/src/app/pages/HomePage/HomePage.tsx | 41 ++++++++++++++++--- gui/src/app/pages/HomePage/LeftPanel.tsx | 39 +++++++++++++++++- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx b/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx index eb5c15e7..7498e7cf 100644 --- a/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx +++ b/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx @@ -41,7 +41,9 @@ const SPAnalysisContextProvider: FunctionComponent = ({ width, height }) => { } const HomePageChild: FunctionComponent = ({ width, height }) => { - - const { data, update } = useContext(SPAnalysisContext) const setSamplingOpts = useCallback((opts: SamplingOpts) => { update({ type: 'setSamplingOpts', opts }) @@ -39,14 +37,32 @@ const HomePageChild: FunctionComponent = ({ width, height }) => { const [compiledMainJsUrl, setCompiledMainJsUrl] = useState('') - const leftPanelWidth = Math.max(250, Math.min(340, width * 0.2)) + const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(determineShouldBeInitiallyCollapsed(width)) + const expandedLeftPanelWidth = determineLeftPanelWidth(width) // what the width would be if expanded + const leftPanelWidth = leftPanelCollapsed ? 20 : expandedLeftPanelWidth // the actual width + + // We automatically collapse the panel if user has resized the window to be + // too small but we only want to do this right when we cross the threshold, + // not every time we resize by a pixel. Similar for expanding the panel when + // we cross the threshold in the other direction. + const lastWidth = useRef(width) + useEffect(() => { + if (!determineShouldBeInitiallyCollapsed(lastWidth.current) && determineShouldBeInitiallyCollapsed(width)) { + lastWidth.current = width + setLeftPanelCollapsed(true) + } + else if (determineShouldBeInitiallyCollapsed(lastWidth.current) && !determineShouldBeInitiallyCollapsed(width)) { + lastWidth.current = width + setLeftPanelCollapsed(false) + } + }, [width]) + const topBarHeight = 25 useEffect(() => { document.title = "Stan Playground - " + data.meta.title; }, [data.meta.title]) - return (
@@ -58,6 +74,8 @@ const HomePageChild: FunctionComponent = ({ width, height }) => {
= ({ width, height }) => { ) } +// the width of the left panel when it is expanded based on the overall width +const determineLeftPanelWidth = (width: number) => { + const minWidth = 250 + const maxWidth = 500 + return Math.min(maxWidth, Math.max(minWidth, width / 4)) +} + +// whether the left panel should be collapsed initially based on the overall +// width +const determineShouldBeInitiallyCollapsed = (width: number) => { + return width < 800 +} + type RightViewProps = { width: number height: number diff --git a/gui/src/app/pages/HomePage/LeftPanel.tsx b/gui/src/app/pages/HomePage/LeftPanel.tsx index 554758ce..30c7a362 100644 --- a/gui/src/app/pages/HomePage/LeftPanel.tsx +++ b/gui/src/app/pages/HomePage/LeftPanel.tsx @@ -1,18 +1,21 @@ -import { Hyperlink } from "@fi-sci/misc" +import { Hyperlink, SmallIconButton } from "@fi-sci/misc" import ModalWindow, { useModalWindow } from "@fi-sci/modal-window" import { FunctionComponent, useCallback, useContext } from "react" import examplesStanies, { Stanie } from "../../exampleStanies/exampleStanies" import { SPAnalysisContext } from "../../SPAnalysis/SPAnalysisContextProvider" import ExportWindow from "./ExportWindow" import ImportWindow from "./ImportWindow" +import { ChevronLeft, ChevronRight } from "@mui/icons-material" type LeftPanelProps = { width: number height: number hasUnsavedChanges: boolean + collapsed: boolean + onSetCollapsed: (collapsed: boolean) => void } -const LeftPanel: FunctionComponent = ({ width, height, hasUnsavedChanges }) => { +const LeftPanel: FunctionComponent = ({ width, height, hasUnsavedChanges, collapsed, onSetCollapsed }) => { // note: this is close enough to pass in directly if we wish const { update } = useContext(SPAnalysisContext) @@ -22,9 +25,21 @@ const LeftPanel: FunctionComponent = ({ width, height, hasUnsave const { visible: exportVisible, handleOpen: exportOpen, handleClose: exportClose } = useModalWindow() const { visible: importVisible, handleOpen: importOpen, handleClose: importClose } = useModalWindow() + + if (collapsed) { + return ( +
+
+ onSetCollapsed(false)} /> +
+
+ ) + } + return (
+ onSetCollapsed(true)} />

Examples

{ examplesStanies.map((stanie, i) => ( @@ -85,4 +100,24 @@ const LeftPanel: FunctionComponent = ({ width, height, hasUnsave ) } +const ExpandButton: FunctionComponent<{ onClick: () => void }> = ({ onClick }) => { + return ( + } + onClick={onClick} + title="Expand" + /> + ) +} + +const CollapseButton: FunctionComponent<{ onClick: () => void }> = ({ onClick }) => { + return ( + } + onClick={onClick} + title="Collapse" + /> + ) +} + export default LeftPanel \ No newline at end of file From dac2c783a0acb682f357724a354969839ec3fadf Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Wed, 26 Jun 2024 16:06:01 -0400 Subject: [PATCH 02/11] adjust max width of left panel --- gui/src/app/pages/HomePage/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index 9a127ab0..453a527f 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -121,7 +121,7 @@ const HomePageChild: FunctionComponent = ({ width, height }) => { // the width of the left panel when it is expanded based on the overall width const determineLeftPanelWidth = (width: number) => { const minWidth = 250 - const maxWidth = 500 + const maxWidth = 400 return Math.min(maxWidth, Math.max(minWidth, width / 4)) } From 62a74e99547a191a18233580aa01b81ab3e0c2e6 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 26 Jun 2024 20:01:34 +0000 Subject: [PATCH 03/11] Enable back button, clears url on load --- .../SPAnalysis/SPAnalysisContextProvider.tsx | 19 +++++++++++++------ .../app/SPAnalysis/SPAnalysisQueryLoading.ts | 15 ++------------- gui/src/app/SPAnalysis/SPAnalysisReducer.ts | 11 ++--------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx b/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx index f18fa591..62d28843 100644 --- a/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx +++ b/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx @@ -2,7 +2,8 @@ import { initialDataModel, SPAnalysisDataModel } from "./SPAnalysisDataModel" import { createContext, FunctionComponent, PropsWithChildren, useEffect, useReducer } from "react" import { SPAnalysisReducer, SPAnalysisReducerAction, SPAnalysisReducerType } from "./SPAnalysisReducer" import { deserializeAnalysisFromLocalStorage, serializeAnalysisToLocalStorage } from "./SPAnalysisSerialization" -import { fetchRemoteAnalysis, queryStringHasParameters, useQueryParams } from "./SPAnalysisQueryLoading" +import { fetchRemoteAnalysis, queryStringHasParameters, fromQueryParams } from "./SPAnalysisQueryLoading" +import { useSearchParams } from "react-router-dom" type SPAnalysisContextType = { data: SPAnalysisDataModel @@ -19,10 +20,10 @@ export const SPAnalysisContext = createContext({ const SPAnalysisContextProvider: FunctionComponent> = ({ children }) => { + const [data, update] = useReducer(SPAnalysisReducer, initialDataModel) - const { queries, clearSearchParams } = useQueryParams(); + const [searchParams, setSearchParams] = useSearchParams(); - const [data, update] = useReducer(SPAnalysisReducer(clearSearchParams), initialDataModel) useEffect(() => { // as user reloads the page or closes the tab, save state to local storage @@ -38,11 +39,17 @@ const SPAnalysisContextProvider: FunctionComponent { - if (data != initialDataModel) return; + if (searchParams.size === 0 && data !== initialDataModel) return; + const queries = fromQueryParams(searchParams) if (queryStringHasParameters(queries)) { fetchRemoteAnalysis(queries).then((data) => { - update({ type: 'loadInitialData', state: data }) + update({ type: 'loadInitialData', state: data }); + + // set title so that history is better preserved in the browser + document.title = "Stan Playground - " + data.meta.title; + // clear search parameters now that load is complete + setSearchParams(new URLSearchParams()); }) } else { // load the saved state on first load @@ -52,7 +59,7 @@ const SPAnalysisContextProvider: FunctionComponent diff --git a/gui/src/app/SPAnalysis/SPAnalysisQueryLoading.ts b/gui/src/app/SPAnalysis/SPAnalysisQueryLoading.ts index 9282c58a..c7351e26 100644 --- a/gui/src/app/SPAnalysis/SPAnalysisQueryLoading.ts +++ b/gui/src/app/SPAnalysis/SPAnalysisQueryLoading.ts @@ -1,6 +1,4 @@ -import { useSearchParams } from "react-router-dom"; import { SPAnalysisDataModel, initialDataModel, persistStateToEphemera } from "./SPAnalysisDataModel"; -import { useCallback } from "react"; enum QueryParamKeys { @@ -19,16 +17,7 @@ type QueryParams = { [key in QueryParamKeys]: string | null } -export const useQueryParams = () => { - const [searchParams, setSearchParams] = useSearchParams(); - - const clearSearchParams = useCallback(() => { - // whenever the data state is 'dirty', we want to - // clear the URL bar as to indiciate that the viewed content is - // no longer what the link would point to - if (searchParams.size !== 0) - setSearchParams(new URLSearchParams()) - }, [searchParams, setSearchParams]); +export const fromQueryParams = (searchParams: URLSearchParams) => { for (const key of searchParams.keys()) { // warn on unknown keys @@ -49,7 +38,7 @@ export const useQueryParams = () => { seed: searchParams.get(QueryParamKeys.SOSeed), } - return { queries, clearSearchParams } + return queries; } export const queryStringHasParameters = (query: QueryParams) => { diff --git a/gui/src/app/SPAnalysis/SPAnalysisReducer.ts b/gui/src/app/SPAnalysis/SPAnalysisReducer.ts index 8e2ab806..4577f399 100644 --- a/gui/src/app/SPAnalysis/SPAnalysisReducer.ts +++ b/gui/src/app/SPAnalysis/SPAnalysisReducer.ts @@ -34,14 +34,7 @@ export type SPAnalysisReducerAction = { type: 'clear' } -export const SPAnalysisReducer = (onDirty: () => void) => (s: SPAnalysisDataModel, a: SPAnalysisReducerAction) => { - if (a.type !== "loadInitialData") { - // TextEditor seems to trigger occasional spurious edits where nothing changes - if (a.type !== "editFile" || s[a.filename] != a.content) { - onDirty(); - } - } - +export const SPAnalysisReducer = (s: SPAnalysisDataModel, a: SPAnalysisReducerAction) => { switch (a.type) { case "loadStanie": { const dataFileContent = JSON.stringify(a.stanie.data, null, 2); @@ -123,4 +116,4 @@ const loadFromProjectFiles = (data: SPAnalysisDataModel, files: Partial loadFileFromString(currData, currField, files[currField] ?? ''), newData) newData = persistStateToEphemera(newData) return newData -} \ No newline at end of file +} From dd667611aa0f7bb6ba846191033d4eae594b1815 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 27 Jun 2024 13:35:30 +0000 Subject: [PATCH 04/11] Add a build job to CI --- .github/workflows/tests.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a28835c0..86ab4dd6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,3 +31,20 @@ jobs: run: cd gui; yarn - name: Test run: cd gui; yarn test + + frontend-build: + name: yarn build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + cache-dependency-path: gui/yarn.lock + - name: Install dependencies + run: cd gui; yarn + - name: Build + run: cd gui; yarn build From 14a049ad028d3b21738e19bd673bf7e2f350d3fc Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 27 Jun 2024 12:38:03 -0400 Subject: [PATCH 05/11] fix editor toolbar styling --- gui/src/app/FileEditor/TextEditor.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index a47f0b3c..765586ca 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -140,8 +140,13 @@ const TextEditor: FunctionComponent = ({defaultText, text, onSaveText, ed return (
-
- {label} +
+ + {label} +     {!readOnly && ( } title="Save file" disabled={text === editedText} label="save" /> From 7fbbb93a0a56d75339af78437b54554e7f9828d1 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 27 Jun 2024 13:46:07 -0400 Subject: [PATCH 06/11] implement error/warning squiggles in stan editor --- gui/src/app/FileEditor/StanFileEditor.tsx | 44 +++++++++++++++++++++-- gui/src/app/FileEditor/TextEditor.tsx | 40 +++++++++++++++++++-- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 952825ed..73a822f8 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 cm: CodeMarker[] = [] + const errorsAndWarnings = [...(stancErrors.errors || []), ...(stancErrors.warnings || [])] + for (const x of errorsAndWarnings) { + if (!x) continue + + // 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 = parseInt(section.slice('line '.length)) + } + else if (section.startsWith('column ')) { + 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)) { + cm.push({ + startLineNumber: lineNumber, + startColumn: startColumn + 1, + endLineNumber: lineNumber, + endColumn: endColumn + 1, + message: x, + severity: x.toLowerCase().startsWith('warning') ? 'warning' : 'error' + }) + } + } + return cm +} + export default StanFileEditor diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index a47f0b3c..7e6acb58 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,35 @@ 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 toSeverity = (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 modelMarkers = codeMarkers.map(marker => ({ + startLineNumber: marker.startLineNumber, + startColumn: marker.startColumn, + endLineNumber: marker.endLineNumber, + endColumn: marker.endColumn, + message: marker.message, + severity: toSeverity(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, From de0dc7c00be43265f748158bed336e7b8352e617 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 27 Jun 2024 13:48:52 -0400 Subject: [PATCH 07/11] Add check to stancErrorsToCodeMarkers --- gui/src/app/FileEditor/StanFileEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 73a822f8..988cc852 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -229,10 +229,10 @@ const stancErrorsToCodeMarkers = (stancErrors: StancErrors) => { const sections = x.split(',').map(x => x.trim()) for (const section of sections) { - if (section.startsWith('line ')) { + if ((section.startsWith('line ')) && (lineNumber === undefined)) { lineNumber = parseInt(section.slice('line '.length)) } - else if (section.startsWith('column ')) { + 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 From 4d236c40053e125561feebd0dc8ed5047409c2af Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 27 Jun 2024 15:25:13 -0400 Subject: [PATCH 08/11] Format error/warning messages --- gui/src/app/FileEditor/StanFileEditor.tsx | 37 +++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 988cc852..ceb39d32 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -240,18 +240,51 @@ const stancErrorsToCodeMarkers = (stancErrors: StancErrors) => { } if ((lineNumber !== undefined) && (startColumn !== undefined) && (endColumn !== undefined)) { + const isWarning = x.toLowerCase().startsWith('warning') cm.push({ startLineNumber: lineNumber, startColumn: startColumn + 1, endLineNumber: lineNumber, endColumn: endColumn + 1, - message: x, - severity: x.toLowerCase().startsWith('warning') ? 'warning' : 'error' + message: isWarning ? getWarningMessage(x) : getErrorMessage(x), + severity: isWarning ? 'warning' : 'error' }) } } return cm } +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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 From 26ee51e7fc3b27240577ba3e7ef3ad879e82a7d0 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 27 Jun 2024 20:14:47 +0000 Subject: [PATCH 09/11] Avoid conditional in useEffect --- gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx b/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx index 62d28843..ddad1ed7 100644 --- a/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx +++ b/gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx @@ -24,7 +24,6 @@ const SPAnalysisContextProvider: FunctionComponent { // as user reloads the page or closes the tab, save state to local storage const handleBeforeUnload = () => { @@ -39,8 +38,6 @@ const SPAnalysisContextProvider: FunctionComponent { - if (searchParams.size === 0 && data !== initialDataModel) return; - const queries = fromQueryParams(searchParams) if (queryStringHasParameters(queries)) { fetchRemoteAnalysis(queries).then((data) => { @@ -58,8 +55,10 @@ const SPAnalysisContextProvider: FunctionComponent From f542bbb1c8a3d103834703978ff7e0bc1001e505 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 27 Jun 2024 16:30:32 -0400 Subject: [PATCH 10/11] code style improvements for stanc errors to markers conversion --- gui/src/app/FileEditor/StanFileEditor.tsx | 70 +++++++++++++---------- gui/src/app/FileEditor/TextEditor.tsx | 19 +++--- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index ceb39d32..8cfe5a86 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -216,42 +216,54 @@ const stringChecksum = (str: string) => { } const stancErrorsToCodeMarkers = (stancErrors: StancErrors) => { - const cm: CodeMarker[] = [] - const errorsAndWarnings = [...(stancErrors.errors || []), ...(stancErrors.warnings || [])] - for (const x of errorsAndWarnings) { - if (!x) continue + const codeMarkers: CodeMarker[] = [] - // Example: Syntax error in 'main.stan', line 1, column 0 to column 1, parsing error: + 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 +} - let lineNumber: number | undefined = undefined - let startColumn: number | undefined = undefined - let endColumn: number | undefined = undefined +const stancErrorStringToMarker = (x: string, severity: 'error' | 'warning'): CodeMarker | undefined => { + if (!x) return 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 - } + // 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)) { - const isWarning = x.toLowerCase().startsWith('warning') - cm.push({ - startLineNumber: lineNumber, - startColumn: startColumn + 1, - endLineNumber: lineNumber, - endColumn: endColumn + 1, - message: isWarning ? getWarningMessage(x) : getErrorMessage(x), - severity: isWarning ? 'warning' : 'error' - }) + 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 } } - return cm + else { + return undefined + } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index 7e6acb58..696693bf 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -78,21 +78,13 @@ const TextEditor: FunctionComponent = ({defaultText, text, onSaveText, ed if (editor === undefined) return const model = editor.getModel() if (model === null) return - const toSeverity = (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 modelMarkers = codeMarkers.map(marker => ({ startLineNumber: marker.startLineNumber, startColumn: marker.startColumn, endLineNumber: marker.endLineNumber, endColumn: marker.endColumn, message: marker.message, - severity: toSeverity(marker.severity) + severity: toMonacoMarkerSeverity(marker.severity) })) monacoInstance.editor.setModelMarkers(model, 'stan-playground', modelMarkers) }, [codeMarkers, monacoInstance, editor]) @@ -217,6 +209,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 From 6c9ca69a596961144a79e4c6449efdc0d6481689 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Fri, 28 Jun 2024 08:36:00 -0400 Subject: [PATCH 11/11] adjust panel collapse logic --- gui/src/app/pages/HomePage/HomePage.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index 453a527f..cbb57fb5 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -45,15 +45,12 @@ const HomePageChild: FunctionComponent = ({ width, height }) => { // too small but we only want to do this right when we cross the threshold, // not every time we resize by a pixel. Similar for expanding the panel when // we cross the threshold in the other direction. - const lastWidth = useRef(width) + const lastShouldBeCollapsed = useRef(determineShouldBeInitiallyCollapsed(width)) useEffect(() => { - if (!determineShouldBeInitiallyCollapsed(lastWidth.current) && determineShouldBeInitiallyCollapsed(width)) { - lastWidth.current = width - setLeftPanelCollapsed(true) - } - else if (determineShouldBeInitiallyCollapsed(lastWidth.current) && !determineShouldBeInitiallyCollapsed(width)) { - lastWidth.current = width - setLeftPanelCollapsed(false) + const shouldBeCollapsed = determineShouldBeInitiallyCollapsed(width) + if (shouldBeCollapsed !== lastShouldBeCollapsed.current) { + lastShouldBeCollapsed.current = shouldBeCollapsed + setLeftPanelCollapsed(shouldBeCollapsed) } }, [width])