mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
feat(debug): show ghost breakpoint and tooltip on gutter hover (#9150)
* feat(debug): show ghost breakpoint and tooltip on gutter hover * fix(debug): show ghost breakpoint only on glyph margin to match click handler * revert(debug): show ghost across entire gutter, not only glyph margin * refactor(debug): use MouseTargetType enum, short-circuit hover decoration
This commit is contained in:
@@ -591,6 +591,10 @@
|
||||
let debugBreakpoints = new SvelteSet<number>()
|
||||
let breakpointDecorations: string[] = $state([])
|
||||
let currentLineDecoration: string[] = $state([])
|
||||
let hoverBreakpointDecoration: string[] = $state([])
|
||||
// Line currently showing the ghost breakpoint, used to short-circuit redundant
|
||||
// deltaDecorations calls on every mousemove event.
|
||||
let hoverBreakpointLine: number | null = null
|
||||
// Get the DAP server URL based on language
|
||||
const dapServerUrl = $derived(getDebugServerUrl((lang || 'python3') as DebugLanguage))
|
||||
const debugFilePath = $derived(`/tmp/script${getDebugFileExtension(lang || '')}`)
|
||||
@@ -623,6 +627,13 @@
|
||||
stickiness: 1
|
||||
}
|
||||
|
||||
// Ghost breakpoint shown while hovering the gutter on a line without a breakpoint
|
||||
const hoverBreakpointDecorationType: meditor.IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'debug-breakpoint-glyph-hover',
|
||||
glyphMarginHoverMessage: { value: 'Click to add a breakpoint' },
|
||||
stickiness: 1
|
||||
}
|
||||
|
||||
const currentLineDecorationType = {
|
||||
isWholeLine: true,
|
||||
className: 'debug-current-line',
|
||||
@@ -854,6 +865,35 @@
|
||||
debugBreakpoints.add(line)
|
||||
}
|
||||
updateBreakpointDecorations()
|
||||
clearHoverBreakpointDecoration()
|
||||
}
|
||||
|
||||
function updateHoverBreakpointDecoration(line: number): void {
|
||||
if (hoverBreakpointLine === line) return
|
||||
const monacoEditor = editor?.getEditor?.()
|
||||
if (!monacoEditor) return
|
||||
|
||||
const decorations = [
|
||||
{
|
||||
range: { startLineNumber: line, startColumn: 1, endLineNumber: line, endColumn: 1 },
|
||||
options: hoverBreakpointDecorationType
|
||||
}
|
||||
]
|
||||
|
||||
const oldDecorations = untrack(() => hoverBreakpointDecoration)
|
||||
hoverBreakpointDecoration = monacoEditor.deltaDecorations(oldDecorations, decorations)
|
||||
hoverBreakpointLine = line
|
||||
}
|
||||
|
||||
function clearHoverBreakpointDecoration(): void {
|
||||
if (hoverBreakpointLine === null) return
|
||||
const monacoEditor = editor?.getEditor?.()
|
||||
if (!monacoEditor) return
|
||||
const oldDecorations = untrack(() => hoverBreakpointDecoration)
|
||||
if (oldDecorations.length > 0) {
|
||||
hoverBreakpointDecoration = monacoEditor.deltaDecorations(oldDecorations, [])
|
||||
}
|
||||
hoverBreakpointLine = null
|
||||
}
|
||||
|
||||
function updateBreakpointDecorations(): void {
|
||||
@@ -1083,8 +1123,7 @@
|
||||
|
||||
// Add click handler for glyph margin (breakpoint toggle)
|
||||
const mouseDownDisposable = monacoEditor.onMouseDown((e) => {
|
||||
// MouseTargetType.GUTTER_GLYPH_MARGIN = 2
|
||||
if (e.target.type === 2) {
|
||||
if (e.target.type === meditor.MouseTargetType.GUTTER_GLYPH_MARGIN) {
|
||||
const line = e.target.position?.lineNumber
|
||||
if (line) {
|
||||
toggleBreakpoint(line)
|
||||
@@ -1092,6 +1131,27 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Show a ghost breakpoint while hovering anywhere in the gutter on an empty line.
|
||||
// Hover area is intentionally wider than the click target — clicks still only
|
||||
// toggle when landing on the glyph margin itself, but the ghost helps users find it.
|
||||
const mouseMoveDisposable = monacoEditor.onMouseMove((e) => {
|
||||
const t = e.target.type
|
||||
const isGutter =
|
||||
t === meditor.MouseTargetType.GUTTER_GLYPH_MARGIN ||
|
||||
t === meditor.MouseTargetType.GUTTER_LINE_NUMBERS ||
|
||||
t === meditor.MouseTargetType.GUTTER_LINE_DECORATIONS
|
||||
const line = e.target.position?.lineNumber
|
||||
if (isGutter && line && !debugBreakpoints.has(line)) {
|
||||
updateHoverBreakpointDecoration(line)
|
||||
} else {
|
||||
clearHoverBreakpointDecoration()
|
||||
}
|
||||
})
|
||||
|
||||
const mouseLeaveDisposable = monacoEditor.onMouseLeave(() => {
|
||||
clearHoverBreakpointDecoration()
|
||||
})
|
||||
|
||||
// Add F9 keyboard shortcut for toggling breakpoint at cursor
|
||||
monacoEditor.addCommand(120, () => {
|
||||
// KeyCode.F9 = 120
|
||||
@@ -1124,6 +1184,9 @@
|
||||
|
||||
return () => {
|
||||
mouseDownDisposable.dispose()
|
||||
mouseMoveDisposable.dispose()
|
||||
mouseLeaveDisposable.dispose()
|
||||
clearHoverBreakpointDecoration()
|
||||
// Disable glyph margin when exiting debug mode
|
||||
monacoEditor.updateOptions({ glyphMargin: false })
|
||||
}
|
||||
@@ -2163,6 +2226,18 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Ghost breakpoint shown on gutter hover before the user clicks */
|
||||
.debug-breakpoint-glyph-hover {
|
||||
background-color: #e51400;
|
||||
opacity: 0.35;
|
||||
border-radius: 50%;
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Current execution line - yellow background */
|
||||
.debug-current-line {
|
||||
background-color: rgba(255, 238, 0, 0.2);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { SvelteSet } from 'svelte/reactivity'
|
||||
import type { editor as meditor, IDisposable } from 'monaco-editor'
|
||||
import { editor as meditor, type IDisposable } from 'monaco-editor'
|
||||
import { debugState, getDAPClient, resetDAPClient, type DAPClient } from './dapClient'
|
||||
import DebugToolbar from './DebugToolbar.svelte'
|
||||
import DebugPanel from './DebugPanel.svelte'
|
||||
@@ -27,15 +27,26 @@
|
||||
let client: DAPClient | null = $state(null)
|
||||
let breakpointDecorations: string[] = $state([])
|
||||
let currentLineDecoration: string[] = $state([])
|
||||
let hoverBreakpointDecoration: string[] = $state([])
|
||||
// Line currently showing the ghost breakpoint, used to short-circuit redundant
|
||||
// deltaDecorations calls on every mousemove event.
|
||||
let hoverBreakpointLine: number | null = null
|
||||
let disposables: IDisposable[] = []
|
||||
|
||||
// Breakpoint glyph margin decoration
|
||||
const breakpointDecorationType: meditor.IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'debug-breakpoint-glyph',
|
||||
glyphMarginHoverMessage: { value: 'Breakpoint' },
|
||||
glyphMarginHoverMessage: { value: 'Breakpoint (click to remove)' },
|
||||
stickiness: 1 // NeverGrowsWhenTypingAtEdges
|
||||
}
|
||||
|
||||
// Ghost breakpoint shown while hovering the gutter on an empty line
|
||||
const hoverBreakpointDecorationType: meditor.IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'debug-breakpoint-glyph-hover',
|
||||
glyphMarginHoverMessage: { value: 'Click to add a breakpoint' },
|
||||
stickiness: 1
|
||||
}
|
||||
|
||||
// Current line decoration (yellow background when stopped)
|
||||
const currentLineDecorationType: meditor.IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
@@ -94,8 +105,7 @@
|
||||
|
||||
// Add click handler for glyph margin (breakpoint toggle)
|
||||
const mouseDownDisposable = editor.onMouseDown((e) => {
|
||||
if (e.target.type === 2) {
|
||||
// MouseTargetType.GUTTER_GLYPH_MARGIN
|
||||
if (e.target.type === meditor.MouseTargetType.GUTTER_GLYPH_MARGIN) {
|
||||
const line = e.target.position?.lineNumber
|
||||
if (line) {
|
||||
toggleBreakpoint(line)
|
||||
@@ -104,6 +114,29 @@
|
||||
})
|
||||
disposables.push(mouseDownDisposable)
|
||||
|
||||
// Show a ghost breakpoint while hovering anywhere in the gutter on an empty line.
|
||||
// Hover area is intentionally wider than the click target — clicks still only
|
||||
// toggle when landing on the glyph margin itself.
|
||||
const mouseMoveDisposable = editor.onMouseMove((e) => {
|
||||
const t = e.target.type
|
||||
const isGutter =
|
||||
t === meditor.MouseTargetType.GUTTER_GLYPH_MARGIN ||
|
||||
t === meditor.MouseTargetType.GUTTER_LINE_NUMBERS ||
|
||||
t === meditor.MouseTargetType.GUTTER_LINE_DECORATIONS
|
||||
const line = e.target.position?.lineNumber
|
||||
if (isGutter && line && !breakpoints.has(line)) {
|
||||
updateHoverBreakpointDecoration(line)
|
||||
} else {
|
||||
clearHoverBreakpointDecoration()
|
||||
}
|
||||
})
|
||||
disposables.push(mouseMoveDisposable)
|
||||
|
||||
const mouseLeaveDisposable = editor.onMouseLeave(() => {
|
||||
clearHoverBreakpointDecoration()
|
||||
})
|
||||
disposables.push(mouseLeaveDisposable)
|
||||
|
||||
// Add keyboard shortcut F9 for toggling breakpoint
|
||||
editor.addCommand(
|
||||
120, // KeyCode.F9
|
||||
@@ -128,6 +161,7 @@
|
||||
onDestroy(() => {
|
||||
disposables.forEach((d) => d.dispose())
|
||||
disposables = []
|
||||
clearHoverBreakpointDecoration()
|
||||
})
|
||||
|
||||
function toggleBreakpoint(line: number): void {
|
||||
@@ -138,9 +172,34 @@
|
||||
}
|
||||
// SvelteSet is reactive, no need to reassign
|
||||
updateBreakpointDecorations()
|
||||
// A real breakpoint now lives here (or just got cleared) — drop the ghost
|
||||
clearHoverBreakpointDecoration()
|
||||
syncBreakpointsWithServer()
|
||||
}
|
||||
|
||||
function updateHoverBreakpointDecoration(line: number): void {
|
||||
if (hoverBreakpointLine === line) return
|
||||
if (!editor) return
|
||||
|
||||
const decorations: meditor.IModelDeltaDecoration[] = [
|
||||
{
|
||||
range: { startLineNumber: line, startColumn: 1, endLineNumber: line, endColumn: 1 },
|
||||
options: hoverBreakpointDecorationType
|
||||
}
|
||||
]
|
||||
|
||||
hoverBreakpointDecoration = editor.deltaDecorations(hoverBreakpointDecoration, decorations)
|
||||
hoverBreakpointLine = line
|
||||
}
|
||||
|
||||
function clearHoverBreakpointDecoration(): void {
|
||||
if (hoverBreakpointLine === null) return
|
||||
if (editor && hoverBreakpointDecoration.length > 0) {
|
||||
hoverBreakpointDecoration = editor.deltaDecorations(hoverBreakpointDecoration, [])
|
||||
}
|
||||
hoverBreakpointLine = null
|
||||
}
|
||||
|
||||
function updateBreakpointDecorations(): void {
|
||||
if (!editor) return
|
||||
|
||||
@@ -274,7 +333,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function signDebugRequest(codeToSign: string, lang: string): Promise<{
|
||||
async function signDebugRequest(
|
||||
codeToSign: string,
|
||||
lang: string
|
||||
): Promise<{
|
||||
token: string
|
||||
code: string
|
||||
}> {
|
||||
@@ -292,7 +354,9 @@
|
||||
const errorText = await response.text()
|
||||
// Parse specific error cases for better user messages
|
||||
if (errorText.includes('not initialized')) {
|
||||
throw new Error('Debug signing is not configured on the server. Please contact your administrator.')
|
||||
throw new Error(
|
||||
'Debug signing is not configured on the server. Please contact your administrator.'
|
||||
)
|
||||
}
|
||||
throw new Error(errorText || 'Failed to authorize debug session')
|
||||
}
|
||||
@@ -447,6 +511,17 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:global(.debug-breakpoint-glyph-hover) {
|
||||
background-color: #e51400;
|
||||
opacity: 0.35;
|
||||
border-radius: 50%;
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.debug-current-line) {
|
||||
background-color: rgba(255, 238, 0, 0.2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user