Feature: Show runtime errors within files that are being imported (#5500)
* chore: dumping progress * chore: saving progress * fix: Got a working example of filenames piped from Rust to TS * fix: cleaning up debugging code * fix: TS type for filenames * fix: rust linter errors * fix: cargo fmt * fix: testing code, updating KCLError class for filenames * fix: auto fixes * feat: display badge in project folder if there is an error in another file * chore: skeleton ideas for badge notifications from errors in imported files * fix: more skeleton code to test some potential implementations * fix: addressing PR comments * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * fix: fixing the rust struct? * fix: cargo fmt * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * feat: skeleton workflow for showing runtime errors * chore: showBadge, adding more props * fix: new application state to reset errors from previous execution if parse fails first * fix: cleanup * fix: better UI * fix: adding comment for future * fix: revert for production * fix: removing unused comment * chore: swapping JS object to typed Map --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -130,6 +130,8 @@ export const FileMachineProvider = ({
|
|||||||
navigateToFile: ({ context, event }) => {
|
navigateToFile: ({ context, event }) => {
|
||||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
if (event.output && 'name' in event.output) {
|
if (event.output && 'name' in event.output) {
|
||||||
|
// TODO: Technically this is not the same as the FileTree Onclick even if they are in the same page
|
||||||
|
// What is "Open file?"
|
||||||
commandBarActor.send({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
@ -23,6 +23,8 @@ import { FileEntry } from 'lib/project'
|
|||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
|
import { kclErrorsByFilename, KCLError } from 'lang/errors'
|
||||||
|
|
||||||
function getIndentationCSS(level: number) {
|
function getIndentationCSS(level: number) {
|
||||||
return `calc(1rem * ${level + 1})`
|
return `calc(1rem * ${level + 1})`
|
||||||
@ -158,6 +160,7 @@ const FileTreeItem = ({
|
|||||||
level = 0,
|
level = 0,
|
||||||
treeSelection,
|
treeSelection,
|
||||||
setTreeSelection,
|
setTreeSelection,
|
||||||
|
runtimeErrors,
|
||||||
}: {
|
}: {
|
||||||
parentDir: FileEntry | undefined
|
parentDir: FileEntry | undefined
|
||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
@ -177,6 +180,7 @@ const FileTreeItem = ({
|
|||||||
level?: number
|
level?: number
|
||||||
treeSelection: FileEntry | undefined
|
treeSelection: FileEntry | undefined
|
||||||
setTreeSelection: Dispatch<React.SetStateAction<FileEntry | undefined>>
|
setTreeSelection: Dispatch<React.SetStateAction<FileEntry | undefined>>
|
||||||
|
runtimeErrors: Map<string, KCLError[]>
|
||||||
}) => {
|
}) => {
|
||||||
const { send: fileSend, context: fileContext } = useFileContext()
|
const { send: fileSend, context: fileContext } = useFileContext()
|
||||||
const { onFileOpen, onFileClose } = useLspContext()
|
const { onFileOpen, onFileClose } = useLspContext()
|
||||||
@ -186,6 +190,8 @@ const FileTreeItem = ({
|
|||||||
const isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path
|
const isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path
|
||||||
const itemRef = useRef(null)
|
const itemRef = useRef(null)
|
||||||
|
|
||||||
|
const hasRuntimeError = runtimeErrors.has(fileOrDir.path)
|
||||||
|
|
||||||
// Since every file or directory gets its own FileTreeItem, we can do this.
|
// Since every file or directory gets its own FileTreeItem, we can do this.
|
||||||
// Because subtrees only render when they are opened, that means this
|
// Because subtrees only render when they are opened, that means this
|
||||||
// only listens when they open. Because this acts like a useEffect, when
|
// only listens when they open. Because this acts like a useEffect, when
|
||||||
@ -292,7 +298,7 @@ const FileTreeItem = ({
|
|||||||
>
|
>
|
||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
<button
|
<button
|
||||||
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
className="relative flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.currentTarget.focus()
|
e.currentTarget.focus()
|
||||||
@ -300,11 +306,21 @@ const FileTreeItem = ({
|
|||||||
}}
|
}}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
>
|
>
|
||||||
|
{hasRuntimeError && (
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
'absolute m-0 p-0 bottom-3 left-6 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-red-600 rounded-full border border-red-300 dark:border-red-800 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||||
|
}
|
||||||
|
title={`Click to view notifications`}
|
||||||
|
>
|
||||||
|
<span>x</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
name={fileOrDir.name?.endsWith(FILE_EXT) ? 'kcl' : 'file'}
|
name={fileOrDir.name?.endsWith(FILE_EXT) ? 'kcl' : 'file'}
|
||||||
className="inline-block w-3 text-current"
|
className="inline-block w-3 text-current"
|
||||||
/>
|
/>
|
||||||
{fileOrDir.name}
|
<span className="pl-1">{fileOrDir.name}</span>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<RenameForm
|
<RenameForm
|
||||||
@ -414,6 +430,7 @@ const FileTreeItem = ({
|
|||||||
key={level + '-' + child.path}
|
key={level + '-' + child.path}
|
||||||
treeSelection={treeSelection}
|
treeSelection={treeSelection}
|
||||||
setTreeSelection={setTreeSelection}
|
setTreeSelection={setTreeSelection}
|
||||||
|
runtimeErrors={runtimeErrors}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@ -660,6 +677,8 @@ export const FileTreeInner = ({
|
|||||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const { send: fileSend, context: fileContext } = useFileContext()
|
const { send: fileSend, context: fileContext } = useFileContext()
|
||||||
const { send: modelingSend } = useModelingContext()
|
const { send: modelingSend } = useModelingContext()
|
||||||
|
const { errors } = useKclContext()
|
||||||
|
const runtimeErrors = kclErrorsByFilename(errors)
|
||||||
|
|
||||||
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
|
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
|
||||||
FileEntry | undefined
|
FileEntry | undefined
|
||||||
@ -769,6 +788,7 @@ export const FileTreeInner = ({
|
|||||||
key={fileOrDir.path}
|
key={fileOrDir.path}
|
||||||
treeSelection={treeSelection}
|
treeSelection={treeSelection}
|
||||||
setTreeSelection={setTreeSelection}
|
setTreeSelection={setTreeSelection}
|
||||||
|
runtimeErrors={runtimeErrors}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -18,6 +18,7 @@ import { editorManager } from 'lib/singletons'
|
|||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { FeatureTreePane } from './FeatureTreePane'
|
import { FeatureTreePane } from './FeatureTreePane'
|
||||||
|
import { kclErrorsByFilename } from 'lang/errors'
|
||||||
|
|
||||||
export type SidebarType =
|
export type SidebarType =
|
||||||
| 'code'
|
| 'code'
|
||||||
@ -30,8 +31,10 @@ export type SidebarType =
|
|||||||
| 'variables'
|
| 'variables'
|
||||||
|
|
||||||
export interface BadgeInfo {
|
export interface BadgeInfo {
|
||||||
value: (props: PaneCallbackProps) => boolean | number
|
value: (props: PaneCallbackProps) => boolean | number | string
|
||||||
onClick?: MouseEventHandler<any>
|
onClick?: MouseEventHandler<any>
|
||||||
|
className?: string
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,6 +155,25 @@ export const sidebarPanes: SidebarPane[] = [
|
|||||||
},
|
},
|
||||||
keybinding: 'Shift + F',
|
keybinding: 'Shift + F',
|
||||||
hide: ({ platform }) => platform === 'web',
|
hide: ({ platform }) => platform === 'web',
|
||||||
|
showBadge: {
|
||||||
|
value: (context) => {
|
||||||
|
// Only compute runtime errors! Compilation errors are not tracked here.
|
||||||
|
const errors = kclErrorsByFilename(context.kclContext.errors)
|
||||||
|
return errors.size > 0 ? 'x' : ''
|
||||||
|
},
|
||||||
|
onClick: (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
// TODO: When we have generic file open
|
||||||
|
// If badge is pressed
|
||||||
|
// Open the first error in the array of errors
|
||||||
|
// Then scroll to error
|
||||||
|
// Do you automatically open the project files
|
||||||
|
// editorManager.scrollToFirstErrorDiagnosticIfExists()
|
||||||
|
},
|
||||||
|
className:
|
||||||
|
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-red-600 rounded-full border border-red-300 dark:border-red-800 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
|
||||||
|
title: 'Project files have runtime errors',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'variables',
|
id: 'variables',
|
||||||
|
@ -27,8 +27,10 @@ interface ModelingSidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BadgeInfoComputed {
|
interface BadgeInfoComputed {
|
||||||
value: number | boolean
|
value: number | boolean | string
|
||||||
onClick?: MouseEventHandler<any>
|
onClick?: MouseEventHandler<any>
|
||||||
|
className?: string
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformString(): 'web' | 'desktop' {
|
function getPlatformString(): 'web' | 'desktop' {
|
||||||
@ -116,6 +118,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
acc[pane.id] = {
|
acc[pane.id] = {
|
||||||
value: pane.showBadge.value(paneCallbackProps),
|
value: pane.showBadge.value(paneCallbackProps),
|
||||||
onClick: pane.showBadge.onClick,
|
onClick: pane.showBadge.onClick,
|
||||||
|
className: pane.showBadge.className,
|
||||||
|
title: pane.showBadge.title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
@ -125,6 +129,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
// Clear any hidden panes from the `openPanes` array
|
// Clear any hidden panes from the `openPanes` array
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const panesToReset: SidebarType[] = []
|
const panesToReset: SidebarType[] = []
|
||||||
|
|
||||||
sidebarPanes.forEach((pane) => {
|
sidebarPanes.forEach((pane) => {
|
||||||
if (
|
if (
|
||||||
pane.hide === true ||
|
pane.hide === true ||
|
||||||
@ -339,22 +344,31 @@ function ModelingPaneButton({
|
|||||||
<p
|
<p
|
||||||
id={`${paneConfig.id}-badge`}
|
id={`${paneConfig.id}-badge`}
|
||||||
className={
|
className={
|
||||||
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
showBadge.className
|
||||||
|
? showBadge.className
|
||||||
|
: 'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||||
}
|
}
|
||||||
onClick={showBadge.onClick}
|
onClick={showBadge.onClick}
|
||||||
title={`Click to view ${showBadge.value} notification${
|
title={
|
||||||
|
showBadge.title
|
||||||
|
? showBadge.title
|
||||||
|
: `Click to view ${showBadge.value} notification${
|
||||||
Number(showBadge.value) > 1 ? 's' : ''
|
Number(showBadge.value) > 1 ? 's' : ''
|
||||||
}`}
|
}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="sr-only"> has </span>
|
<span className="sr-only"> has </span>
|
||||||
{typeof showBadge.value === 'number' ? (
|
{typeof showBadge.value === 'number' ||
|
||||||
|
typeof showBadge.value === 'string' ? (
|
||||||
<span>{showBadge.value}</span>
|
<span>{showBadge.value}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="sr-only">a</span>
|
<span className="sr-only">a</span>
|
||||||
)}
|
)}
|
||||||
|
{typeof showBadge.value === 'number' && (
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -293,6 +293,13 @@ export class KclManager {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GOTCHA:
|
||||||
|
// When we safeParse this is tied to execution because they clicked a new file to load
|
||||||
|
// Clear all previous errors and logs because they are old since they executed a new file
|
||||||
|
// If we decouple safeParse from execution we need to move this application logic.
|
||||||
|
this._kclErrorsCallBack([])
|
||||||
|
this._logsCallBack([])
|
||||||
|
|
||||||
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
|
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
|
||||||
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
|
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
import {
|
||||||
|
KclError,
|
||||||
|
KclError as RustKclError,
|
||||||
|
} from '../wasm-lib/kcl/bindings/KclError'
|
||||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||||
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
||||||
import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
||||||
@ -334,3 +337,34 @@ export function complilationErrorsToDiagnostics(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create an array of KCL Errors with a new formatting to
|
||||||
|
// easily map SourceRange of an error to the filename to display in the
|
||||||
|
// side bar UI. This is to indicate an error in an imported file, it isn't
|
||||||
|
// the specific code mirror error interface.
|
||||||
|
export function kclErrorsByFilename(
|
||||||
|
errors: KCLError[]
|
||||||
|
): Map<string, KCLError[]> {
|
||||||
|
const fileNameToError: Map<string, KCLError[]> = new Map()
|
||||||
|
errors.forEach((error: KCLError) => {
|
||||||
|
const filenames = error.filenames
|
||||||
|
const sourceRange: SourceRange = error.sourceRange
|
||||||
|
const fileIndex = sourceRange[2]
|
||||||
|
const modulePath: ModulePath | undefined = filenames[fileIndex]
|
||||||
|
if (modulePath) {
|
||||||
|
let stdOrLocalPath = modulePath.value
|
||||||
|
if (stdOrLocalPath) {
|
||||||
|
// Build up an array of errors per file name
|
||||||
|
const value = fileNameToError.get(stdOrLocalPath)
|
||||||
|
if (!value) {
|
||||||
|
fileNameToError.set(stdOrLocalPath, [error])
|
||||||
|
} else {
|
||||||
|
value.push(error)
|
||||||
|
fileNameToError.set(stdOrLocalPath, [error])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return fileNameToError
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user