Jump to error code on badge click (#3262)

* add function to scroll to view

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* scroll into view on click

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add test for jump to code with error

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-08-04 15:59:04 -07:00
committed by GitHub
parent 21389c089d
commit 9c87b124d9
5 changed files with 502 additions and 139 deletions

View File

@ -19,6 +19,7 @@ import {
TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_START, TEST_SETTINGS_ONBOARDING_START,
TEST_CODE_GIZMO, TEST_CODE_GIZMO,
TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW,
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
} from './storageStates' } from './storageStates'
@ -8207,97 +8208,131 @@ const sketch002 = extrude(${[5, 5]} + 7, sketch002)`
await expect(page.locator('.cm-content')).toHaveText(result2.regExp) await expect(page.locator('.cm-content')).toHaveText(result2.regExp)
}) })
test('Typing KCL errors induces a badge on the error logs pane button', async ({ test.describe('Code pane and errors', () => {
page, test('Typing KCL errors induces a badge on the code pane button', async ({
}) => { page,
const u = await getUtils(page) }) => {
const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript((code) => { await page.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, bracket) }, bracket)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
// Ensure no badge is present // Ensure no badge is present
const codePaneButton = page.getByRole('button', { name: 'KCL Code pane' }) const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButton).not.toContainText('notification') await expect(codePaneButtonHolder).not.toContainText('notification')
// Delete a character to break the KCL // Delete a character to break the KCL
await u.openKclCodePanel() await u.openKclCodePanel()
await page.getByText('extrude(').click() await page.getByText('extrude(').click()
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button // Ensure that a badge appears on the button
await expect(codePaneButton).toContainText('notification') await expect(codePaneButtonHolder).toContainText('notification')
}) })
test('Opening and closing the code pane will consistently show error diagnostics', async ({ test('Opening and closing the code pane will consistently show error diagnostics', async ({
page, page,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript((code) => { await page.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, bracket) }, bracket)
await page.setViewportSize({ width: 1200, height: 900 }) await page.setViewportSize({ width: 1200, height: 900 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
// Ensure we have no errors in the gutter. // Ensure we have no errors in the gutter.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Ensure no badge is present // Ensure no badge is present
const codePaneButton = page.getByRole('button', { name: 'KCL Code pane' }) const codePaneButton = page.getByRole('button', { name: 'KCL Code pane' })
await expect(codePaneButton).not.toContainText('notification') const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).not.toContainText('notification')
// Delete a character to break the KCL
await u.openKclCodePanel() // Delete a character to break the KCL
await page.getByText('extrude(').click() await u.openKclCodePanel()
await page.keyboard.press('Backspace') await page.getByText('extrude(').click()
await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button
await expect(codePaneButton).toContainText('notification') // Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() // Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error') // error text on hover
await expect(page.getByText('Unexpected token').first()).toBeVisible() await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
// Close the code pane
u.closeKclCodePanel() // Close the code pane
codePaneButton.click()
await page.waitForTimeout(500)
await page.waitForTimeout(500)
// Ensure that a badge appears on the button
await expect(codePaneButton).toContainText('notification') // Ensure that a badge appears on the button
// Ensure we have no errors in the gutter. await expect(codePaneButtonHolder).toContainText('notification')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() // Ensure we have no errors in the gutter.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Open the code pane
u.openKclCodePanel() // Open the code pane
u.openKclCodePanel()
// Ensure that a badge appears on the button
await expect(codePaneButton).toContainText('notification') // Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() // Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error') // error text on hover
await expect(page.getByText('Unexpected token').first()).toBeVisible() await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
})
test('When error is not in view you can click the badge to scroll to it', async ({
page,
}) => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(1000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have no errors in the gutter, since error out of view.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click the badge.
const badge = page.locator('#code-badge')
await expect(badge).toBeVisible()
await badge.click()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
})
}) })

View File

@ -86,3 +86,268 @@ export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|> close(%) |> close(%)
|> extrude(5 + 7, %) |> extrude(5 + 7, %)
` `
export const TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW = `const width = 50.8
const height = 30
const thickness = 2
const keychainHoleSize = 3
const keychain = startSketchOn("XY")
|> startProfileAt([0, 0], %)
|> lineTo([width, 0], %)
|> lineTo([width, height], %)
|> lineTo([0, height], %)
|> close(%)
|> extrude(thickness, %)
// generated from /home/paultag/Downloads/zma-logomark.svg
fn svg = (surface, origin, depth) => {
let a0 = surface |> startProfileAt([origin[0] + 45.430427, origin[1] + -14.627736], %)
|> bezierCurve({
control1: [ 0, 0.764157 ],
control2: [ 0, 1.528314 ],
to: [ 0, 2.292469 ]
}, %)
|> bezierCurve({
control1: [ -3.03202, 0 ],
control2: [ -6.064039, 0 ],
to: [ -9.09606, 0 ]
}, %)
|> bezierCurve({
control1: [ 0, -1.077657 ],
control2: [ 0, -2.155312 ],
to: [ 0, -3.232969 ]
}, %)
|> bezierCurve({
control1: [ 2.741805, 0 ],
control2: [ 5.483613, 0 ],
to: [ 8.225417, 0 ]
}, %)
|> bezierCurve({
control1: [ -2.740682, -2.961815 ],
control2: [ -5.490342, -5.925794 ],
to: [ -8.225417, -8.886255 ]
}, %)
|> bezierCurve({
control1: [ 0, -0.723995 ],
control2: [ 0, -1.447988 ],
to: [ 0, -2.171981 ]
}, %)
|> bezierCurve({
control1: [ 0.712124, 0.05061 ],
control2: [ 1.511636, -0.09877 ],
to: [ 2.172096, 0.07005 ]
}, %)
|> bezierCurve({
control1: [ 0.68573, 0.740811 ],
control2: [ 1.371459, 1.481622 ],
to: [ 2.057187, 2.222436 ]
}, %)
|> bezierCurve({
control1: [ 0, -0.76416 ],
control2: [ 0, -1.52832 ],
to: [ 0, -2.29248 ]
}, %)
|> bezierCurve({
control1: [ 3.032013, 0 ],
control2: [ 6.064026, 0 ],
to: [ 9.096038, 0 ]
}, %)
|> bezierCurve({
control1: [ 0, 1.077657 ],
control2: [ 0, 2.155314 ],
to: [ 0, 3.232973 ]
}, %)
|> bezierCurve({
control1: [ -2.741312, 0 ],
control2: [ -5.482623, 0 ],
to: [ -8.223936, 0 ]
}, %)
|> bezierCurve({
control1: [ 2.741313, 2.961108 ],
control2: [ 5.482624, 5.922216 ],
to: [ 8.223936, 8.883325 ]
}, %)
|> bezierCurve({
control1: [ 0, 0.724968 ],
control2: [ 0, 1.449938 ],
to: [ 0, 2.174907 ]
}, %)
|> bezierCurve({
control1: [ -0.712656, -0.05145 ],
control2: [ -1.512554, 0.09643 ],
to: [ -2.173592, -0.07298 ]
}, %)
|> bezierCurve({
control1: [ -0.685222, -0.739834 ],
control2: [ -1.370445, -1.479669 ],
to: [ -2.055669, -2.219505 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a1 = surface |> startProfileAt([origin[0] + 57.920488, origin[1] + -15.244943], %)
|> bezierCurve({
control1: [ -2.78904, 0.106635 ],
control2: [ -5.052548, -2.969529 ],
to: [ -4.055141, -5.598369 ]
}, %)
|> bezierCurve({
control1: [ 0.841523, -0.918736 ],
control2: [ 0.439412, -1.541892 ],
to: [ -0.368488, -2.214378 ]
}, %)
|> bezierCurve({
control1: [ -0.418245, -0.448461 ],
control2: [ -0.836489, -0.896922 ],
to: [ -1.254732, -1.345384 ]
}, %)
|> bezierCurve({
control1: [ -2.76806, 2.995359 ],
control2: [ -2.32667, 8.18409 ],
to: [ 0.897655, 10.678932 ]
}, %)
|> bezierCurve({
control1: [ 2.562822, 2.186098 ],
control2: [ 6.605111, 2.28043 ],
to: [ 9.271202, 0.226476 ]
}, %)
|> bezierCurve({
control1: [ -0.743744, -0.797465 ],
control2: [ -1.487487, -1.594932 ],
to: [ -2.231232, -2.392397 ]
}, %)
|> bezierCurve({
control1: [ -0.672938, 0.421422 ],
control2: [ -1.465362, 0.646946 ],
to: [ -2.259264, 0.64512 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a2 = surface |> startProfileAt([origin[0] + 62.19406300000001, origin[1] + -19.500698999999997], %)
|> bezierCurve({
control1: [ 0.302938, 1.281141 ],
control2: [ -1.53575, 2.434288 ],
to: [ -0.10908, 3.279477 ]
}, %)
|> bezierCurve({
control1: [ 0.504637, 0.54145 ],
control2: [ 1.009273, 1.082899 ],
to: [ 1.513909, 1.624348 ]
}, %)
|> bezierCurve({
control1: [ 2.767778, -2.995425 ],
control2: [ 2.327135, -8.184384 ],
to: [ -0.897661, -10.679047 ]
}, %)
|> bezierCurve({
control1: [ -2.562947, -2.186022 ],
control2: [ -6.604089, -2.279606 ],
to: [ -9.271196, -0.227813 ]
}, %)
|> bezierCurve({
control1: [ 0.744231, 0.797952 ],
control2: [ 1.488461, 1.595904 ],
to: [ 2.232692, 2.393856 ]
}, %)
|> bezierCurve({
control1: [ 2.302377, -1.564629 ],
control2: [ 5.793126, -0.15358 ],
to: [ 6.396577, 2.547372 ]
}, %)
|> bezierCurve({
control1: [ 0.08981, 0.346302 ],
control2: [ 0.134865, 0.704078 ],
to: [ 0.13476, 1.061807 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a3 = surface |> startProfileAt([origin[0] + 74.124866, origin[1] + -15.244943], %)
|> bezierCurve({
control1: [ -2.78904, 0.106635 ],
control2: [ -5.052549, -2.969529 ],
to: [ -4.055142, -5.598369 ]
}, %)
|> bezierCurve({
control1: [ 0.841527, -0.918738 ],
control2: [ 0.43941, -1.541892 ],
to: [ -0.368497, -2.214367 ]
}, %)
|> bezierCurve({
control1: [ -0.418254, -0.448466 ],
control2: [ -0.836507, -0.896931 ],
to: [ -1.254761, -1.345395 ]
}, %)
|> bezierCurve({
control1: [ -2.768019, 2.995371 ],
control2: [ -2.326624, 8.184088 ],
to: [ 0.897678, 10.678932 ]
}, %)
|> bezierCurve({
control1: [ 2.56289, 2.186191 ],
control2: [ 6.60516, 2.280307 ],
to: [ 9.271371, 0.226476 ]
}, %)
|> bezierCurve({
control1: [ -0.743808, -0.797465 ],
control2: [ -1.487616, -1.594932 ],
to: [ -2.231424, -2.392397 ]
}, %)
|> bezierCurve({
control1: [ -0.672916, 0.421433 ],
control2: [ -1.465344, 0.646926 ],
to: [ -2.259225, 0.64512 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a4 = surface |> startProfileAt([origin[0] + 77.57333899999998, origin[1] + -16.989262999999998], %)
|> bezierCurve({
control1: [ 0.743298, 0.797463 ],
control2: [ 1.486592, 1.594926 ],
to: [ 2.229888, 2.392389 ]
}, %)
|> bezierCurve({
control1: [ 2.767827, -2.995393 ],
control2: [ 2.327103, -8.184396 ],
to: [ -0.897672, -10.679047 ]
}, %)
|> bezierCurve({
control1: [ -2.562939, -2.186037 ],
control2: [ -6.604077, -2.279589 ],
to: [ -9.271185, -0.227813 ]
}, %)
|> bezierCurve({
control1: [ 0.744243, 0.797952 ],
control2: [ 1.488486, 1.595904 ],
to: [ 2.232729, 2.393856 ]
}, %)
|> bezierCurve({
control1: [ 2.302394, -1.564623 ],
control2: [ 5.793201, -0.153598 ],
to: [ 6.396692, 2.547372 ]
}, %)
|> bezierCurve({
control1: [ 0.32074, 1.215468 ],
control2: [ 0.06159, 2.564765 ],
to: [ -0.690452, 3.573243 ]
}, %)
|> close(%)
|> extrude(depth, %)
"thing";kajsnd;akjsnd
return 0
}
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
startSketchOn(keychain, 'end')
|> circle([
width / 2,
height - (keychainHoleSize + 1.5)
], keychainHoleSize, %)
|> extrude(-thickness, %)`

View File

@ -8,12 +8,13 @@ import {
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu' import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane' import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react' import { MouseEventHandler, ReactNode } from 'react'
import { MemoryPane, MemoryPaneMenu } from './MemoryPane' import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
import { LogsPane } from './LoggingPanes' import { LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane' import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree' import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { editorManager } from 'lib/singletons'
export type SidebarType = export type SidebarType =
| 'code' | 'code'
@ -24,6 +25,11 @@ export type SidebarType =
| 'lspMessages' | 'lspMessages'
| 'variables' | 'variables'
export interface BadgeInfo {
value: (props: PaneCallbackProps) => boolean | number
onClick?: MouseEventHandler<any>
}
/** /**
* This interface can be extended as more context is needed for the panes * This interface can be extended as more context is needed for the panes
* to determine if they should show their badges or not. * to determine if they should show their badges or not.
@ -40,7 +46,7 @@ export type SidebarPane = {
Content: ReactNode | React.FC Content: ReactNode | React.FC
Menu?: ReactNode | React.FC Menu?: ReactNode | React.FC
hideOnPlatform?: 'desktop' | 'web' hideOnPlatform?: 'desktop' | 'web'
showBadge?: (props: PaneCallbackProps) => boolean | number showBadge?: BadgeInfo
} }
export const sidebarPanes: SidebarPane[] = [ export const sidebarPanes: SidebarPane[] = [
@ -51,7 +57,15 @@ export const sidebarPanes: SidebarPane[] = [
Content: KclEditorPane, Content: KclEditorPane,
keybinding: 'Shift + C', keybinding: 'Shift + C',
Menu: KclEditorMenu, Menu: KclEditorMenu,
showBadge: ({ kclContext }) => kclContext.errors.length, showBadge: {
value: ({ kclContext }) => {
return kclContext.errors.length
},
onClick: (e) => {
e.preventDefault()
editorManager.scrollToFirstDiagnosticIfExists()
},
},
}, },
{ {
id: 'files', id: 'files',

View File

@ -1,6 +1,6 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { useCallback, useMemo } from 'react' import { MouseEventHandler, useCallback, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarType, sidebarPanes } from './ModelingPanes' import { SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
@ -19,6 +19,11 @@ interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
} }
interface BadgeInfoComputed {
value: number | boolean
onClick?: MouseEventHandler<any>
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext() const kclContext = useKclContext()
@ -88,13 +93,16 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
[sidebarPanes, showDebugPanel.current] [sidebarPanes, showDebugPanel.current]
) )
const paneBadgeMap: Record<SidebarType, number | boolean> = useMemo(() => { const paneBadgeMap: Record<SidebarType, BadgeInfoComputed> = useMemo(() => {
return filteredPanes.reduce((acc, pane) => { return filteredPanes.reduce((acc, pane) => {
if (pane.showBadge) { if (pane.showBadge) {
acc[pane.id] = pane.showBadge({ kclContext }) acc[pane.id] = {
value: pane.showBadge.value({ kclContext }),
onClick: pane.showBadge.onClick,
}
} }
return acc return acc
}, {} as Record<SidebarType, number | boolean>) }, {} as Record<SidebarType, BadgeInfoComputed>)
}, [kclContext.errors]) }, [kclContext.errors])
const togglePane = useCallback( const togglePane = useCallback(
@ -229,7 +237,7 @@ interface ModelingPaneButtonProps
} }
onClick: () => void onClick: () => void
paneIsOpen?: boolean paneIsOpen?: boolean
showBadge?: boolean | number showBadge?: BadgeInfoComputed
} }
function ModelingPaneButton({ function ModelingPaneButton({
@ -244,59 +252,68 @@ function ModelingPaneButton({
}) })
return ( return (
<button <div id={paneConfig.id + '-button-holder'}>
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary" <button
onClick={onClick} className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
name={paneConfig.title} onClick={onClick}
data-testid={paneConfig.id + '-pane-button'} name={paneConfig.title}
{...props} data-testid={paneConfig.id + '-pane-button'}
> {...props}
<ActionIcon >
icon={paneConfig.icon} <ActionIcon
className={'p-1 ' + paneConfig.iconClassName || ''} icon={paneConfig.icon}
size={paneConfig.iconSize || 'sm'} className={'p-1 ' + paneConfig.iconClassName || ''}
iconClassName={ size={paneConfig.iconSize || 'sm'}
paneIsOpen iconClassName={
? ' !text-chalkboard-10' paneIsOpen
: '!text-chalkboard-80 dark:!text-chalkboard-30' ? ' !text-chalkboard-10'
} : '!text-chalkboard-80 dark:!text-chalkboard-30'
bgClassName={
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
}
/>
<span className="sr-only">
{paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
{!!showBadge && (
<p
className={
'absolute m-0 p-0 -top-1 -right-1 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'
} }
bgClassName={
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
}
/>
<span className="sr-only">
{paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<Tooltip
position="right"
contentClassName="max-w-none flex items-center gap-4"
hoverOnly
>
<span className="flex-1">
{paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<kbd className="hotkey text-xs capitalize">
{paneConfig.keybinding}
</kbd>
</Tooltip>
</button>
{!!showBadge?.value && (
<p
id={`${paneConfig.id}-badge`}
className={
'absolute m-0 p-0 top-1 right-0 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'
}
onClick={showBadge.onClick}
title={`Click to view ${showBadge.value} notification${
Number(showBadge.value) > 1 ? 's' : ''
}`}
> >
<span className="sr-only">&nbsp;has&nbsp;</span> <span className="sr-only">&nbsp;has&nbsp;</span>
{typeof showBadge === 'number' ? ( {typeof showBadge.value === 'number' ? (
<span>{showBadge}</span> <span>{showBadge.value}</span>
) : ( ) : (
<span className="sr-only">a</span> <span className="sr-only">a</span>
)} )}
<span className="sr-only"> <span className="sr-only">
&nbsp;notification{Number(showBadge) > 1 ? 's' : ''} &nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span> </span>
</p> </p>
)} )}
<Tooltip </div>
position="right"
contentClassName="max-w-none flex items-center gap-4"
hoverOnly
>
<span className="flex-1">
{paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<kbd className="hotkey text-xs capitalize">{paneConfig.keybinding}</kbd>
</Tooltip>
</button>
) )
} }

View File

@ -6,7 +6,11 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight, addLineHighlightEvent } from './highlightextension' import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
import { Diagnostic, setDiagnosticsEffect } from '@codemirror/lint' import {
Diagnostic,
forEachDiagnostic,
setDiagnosticsEffect,
} from '@codemirror/lint'
const updateOutsideEditorAnnotation = Annotation.define<boolean>() const updateOutsideEditorAnnotation = Annotation.define<boolean>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true) export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
@ -137,6 +141,34 @@ export default class EditorManager {
}) })
} }
scrollToFirstDiagnosticIfExists() {
if (!this._editorView) return
let firstDiagnosticPos: [number, number] | null = null
forEachDiagnostic(
this._editorView.state,
(d: Diagnostic, from: number, to: number) => {
if (!firstDiagnosticPos) {
firstDiagnosticPos = [from, to]
}
}
)
if (!firstDiagnosticPos) return
this._editorView.focus()
this._editorView.dispatch({
selection: EditorSelection.create([
EditorSelection.cursor(firstDiagnosticPos[0]),
]),
effects: [EditorView.scrollIntoView(firstDiagnosticPos[0])],
annotations: [
updateOutsideEditorEvent,
Transaction.addToHistory.of(false),
],
})
}
undo() { undo() {
if (this._editorView) { if (this._editorView) {
undo(this._editorView) undo(this._editorView)