update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
2025-10-22 02:33:08 +08:00
parent 0d600382d3
commit 4306e555bb
4 changed files with 785 additions and 40 deletions

View File

@@ -2,8 +2,8 @@
import { cpp } from "@codemirror/lang-cpp" import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python" import { python } from "@codemirror/lang-python"
import { EditorState } from "@codemirror/state" import { EditorState } from "@codemirror/state"
import { EditorView } from "@codemirror/view" import { EditorView, Decoration, DecorationSet } from "@codemirror/view"
import { Icon } from "@iconify/vue" import { StateField, StateEffect } from "@codemirror/state"
import { useDark } from "@vueuse/core" import { useDark } from "@vueuse/core"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { Codemirror } from "vue-codemirror" import { Codemirror } from "vue-codemirror"
@@ -19,6 +19,10 @@ interface Props {
fontSize?: number fontSize?: number
readonly?: boolean readonly?: boolean
placeholder?: string placeholder?: string
currentLine?: number
nextLine?: number
currentLineText?: string
nextLineText?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -31,6 +35,92 @@ const props = withDefaults(defineProps<Props>(), {
const code = ref(props.modelValue) const code = ref(props.modelValue)
const isDark = useDark() const isDark = useDark()
const editorView = ref<EditorView>()
// 定义高亮效果
const setHighlight = StateEffect.define<{
currentLine?: number
nextLine?: number
currentLineText?: string
nextLineText?: string
}>()
// 高亮状态字段
const highlightField = StateField.define<DecorationSet>({
create() {
return Decoration.none
},
update(decorations, tr) {
decorations = decorations.map(tr.changes)
for (let effect of tr.effects) {
if (effect.is(setHighlight)) {
decorations = Decoration.none
if (effect.value.currentLine || effect.value.nextLine) {
const decorations_array: any[] = []
// 当前行高亮(绿色)
if (effect.value.currentLine) {
try {
const line = tr.state.doc.line(effect.value.currentLine)
decorations_array.push(
Decoration.line({
class: "cm-current-line",
}).range(line.from),
)
// 在当前行添加文字 - 使用行装饰而不是Widget
if (effect.value.currentLineText) {
decorations_array.push(
Decoration.line({
class: "cm-current-line-with-text",
attributes: {
"data-text": effect.value.currentLineText
}
}).range(line.from),
)
}
} catch (e) {
console.warn("Invalid line number for current line:", effect.value.currentLine)
}
}
// 下一步行高亮(红色)
if (effect.value.nextLine) {
try {
const line = tr.state.doc.line(effect.value.nextLine)
decorations_array.push(
Decoration.line({
class: "cm-next-line",
}).range(line.from),
)
// 在下一步行添加文字 - 使用行装饰而不是Widget
if (effect.value.nextLineText) {
decorations_array.push(
Decoration.line({
class: "cm-next-line-with-text",
attributes: {
"data-text": effect.value.nextLineText
}
}).range(line.from),
)
}
} catch (e) {
console.warn("Invalid line number for next line:", effect.value.nextLine)
}
}
// 确保装饰按位置排序,避免重复
decorations_array.sort((a, b) => a.from - b.from)
decorations = Decoration.set(decorations_array)
}
}
}
return decorations
},
provide: (f) => EditorView.decorations.from(f),
})
const styleTheme = EditorView.baseTheme({ const styleTheme = EditorView.baseTheme({
"& .cm-scroller": { "& .cm-scroller": {
"font-family": "Monaco", "font-family": "Monaco",
@@ -41,6 +131,54 @@ const styleTheme = EditorView.baseTheme({
"&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": { "&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": {
"font-family": "Monaco", "font-family": "Monaco",
}, },
// 当前行高亮样式(绿色)
"& .cm-current-line": {
"background-color": "rgba(0, 255, 0, 0.2)",
"border-left": "3px solid #00ff00",
},
// 下一步行高亮样式(红色)
"& .cm-next-line": {
"background-color": "rgba(255, 0, 0, 0.2)",
"border-left": "3px solid #ff0000",
},
// 当前行带文字样式
"& .cm-current-line-with-text": {
position: "relative",
"&::after": {
content: "attr(data-text)",
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
"background-color": "rgba(0, 255, 0, 0.8)",
color: "#000",
padding: "2px 6px",
"border-radius": "3px",
"font-size": "12px",
"font-weight": "bold",
"white-space": "nowrap",
"z-index": "10",
},
},
// 下一步行带文字样式
"& .cm-next-line-with-text": {
position: "relative",
"&::after": {
content: "attr(data-text)",
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
"background-color": "rgba(255, 0, 0, 0.8)",
color: "#fff",
padding: "2px 6px",
"border-radius": "3px",
"font-size": "12px",
"font-weight": "bold",
"white-space": "nowrap",
"z-index": "10",
},
},
}) })
const emit = defineEmits(["update:modelValue", "ready"]) const emit = defineEmits(["update:modelValue", "ready"])
@@ -67,37 +205,59 @@ function onReady(payload: {
state: EditorState state: EditorState
container: HTMLDivElement container: HTMLDivElement
}) { }) {
editorView.value = payload.view
emit("ready", payload.view) emit("ready", payload.view)
// Editor 准备好后立即设置高亮
updateHighlight()
} }
// 更新高亮的函数
function updateHighlight() {
if (editorView.value) {
console.log(
"Updating highlight - currentLine:",
props.currentLine,
"nextLine:",
props.nextLine,
)
// 如果当前行和下一步相同,只高亮当前行,不显示下一步
const nextLine =
props.currentLine === props.nextLine ? undefined : props.nextLine
editorView.value.dispatch({
effects: setHighlight.of({
currentLine: props.currentLine,
nextLine: nextLine,
currentLineText: props.currentLineText,
nextLineText: props.nextLineText,
}),
})
}
}
// 监听 props 变化并更新高亮
watch(
() => [props.currentLine, props.nextLine, props.currentLineText, props.nextLineText],
() => {
updateHighlight()
},
)
</script> </script>
<template> <template>
<n-flex align="center" class="header" v-if="props.label">
<Icon v-if="icon" :icon="icon" :width="24" :height="24"></Icon>
<span class="title">{{ label }}</span>
<slot name="actions"></slot>
</n-flex>
<Codemirror <Codemirror
v-model="code" v-model="code"
indentWithTab indentWithTab
:extensions="[styleTheme, lang, isDark ? oneDark : smoothy]" :extensions="[styleTheme, lang, highlightField, isDark ? oneDark : smoothy]"
:disabled="props.readonly" :disabled="props.readonly"
:tabSize="4" :tabSize="4"
:placeholder="props.placeholder" :placeholder="props.placeholder"
:style="{ :style="{
height: !!props.label ? 'calc(100% - 60px)' : '100%', height: 'calc(100% - 60px)',
fontSize: props.fontSize + 'px', fontSize: props.fontSize + 'px',
}" }"
@change="onChange" @change="onChange"
@ready="onReady" @ready="onReady"
/> />
</template> </template>
<style scoped>
.header {
padding: 12px 20px;
height: 60px;
box-sizing: border-box;
}
.title {
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,198 @@
<script lang="ts" setup>
import { computed } from "vue"
import { Icon } from "@iconify/vue"
interface Props {
visible: boolean
variables?: Record<string, any>
output?: string
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
variables: () => ({}),
output: "",
})
const emit = defineEmits(["close"])
// 格式化变量显示
const formattedVariables = computed(() => {
if (!props.variables || Object.keys(props.variables).length === 0) {
return []
}
return Object.entries(props.variables).map(([key, value]) => {
// 处理特殊类型
let displayValue = ""
let displayType = typeof value
if (Array.isArray(value) && value.length === 2 &&
value[0] === "IMPORTED_FAUX_PRIMITIVE" &&
value[1] === "imported object") {
displayValue = ""
displayType = "function"
} else if (typeof value === "object" && value !== null) {
displayValue = JSON.stringify(value, null, 2)
} else {
displayValue = String(value)
}
return {
name: key,
value: displayValue,
type: displayType,
}
})
})
// 格式化输出显示
const formattedOutput = computed(() => {
if (!props.output) return ""
return props.output
})
// 计算输出行数
const outputLines = computed(() => {
if (!props.output) return 0
return props.output.split("\n").filter((line) => line !== "").length
})
function closePanel() {
emit("close")
}
</script>
<template>
<n-card v-if="visible" class="floating-panel" :bordered="true" size="small">
<template #header>
<n-flex justify="space-between" align="center">
<n-flex align="center" :gap="8">
<n-icon>
<Icon icon="mdi:bug" :width="16" :height="16" />
</n-icon>
<n-text strong>调试信息</n-text>
</n-flex>
<n-button quaternary circle size="small" @click="closePanel">
<template #icon>
<n-icon>
<Icon icon="mdi:close" :width="16" :height="16" />
</n-icon>
</template>
</n-button>
</n-flex>
</template>
<n-space vertical :size="16" class="panel-content">
<!-- 变量部分 -->
<n-collapse :default-expanded-names="['variables']">
<n-collapse-item title="变量" name="variables">
<template #header>
<n-flex align="center" :gap="8">
<n-icon>
<Icon icon="mdi:variable" :width="14" :height="14" />
</n-icon>
<n-text>变量</n-text>
</n-flex>
</template>
<div v-if="formattedVariables.length === 0">
<n-text
type="info"
class="no-variables-text"
>
暂无变量
</n-text>
</div>
<n-space v-else vertical :size="12">
<n-card
v-for="variable in formattedVariables"
:key="variable.name"
size="small"
:bordered="true"
>
<n-flex
justify="space-between"
align="center"
class="variable-header"
>
<n-text strong :type="'primary'">{{ variable.name }}</n-text>
<n-tag size="small" type="info">{{ variable.type }}</n-tag>
</n-flex>
<n-text
code
class="variable-value"
>
{{ variable.value }}
</n-text>
</n-card>
</n-space>
</n-collapse-item>
</n-collapse>
<!-- 输出部分 -->
<n-collapse v-if="formattedOutput" :default-expanded-names="['output']">
<n-collapse-item :title="`输出 (${outputLines} 行)`" name="output">
<template #header>
<n-flex align="center" :gap="8">
<n-icon>
<Icon icon="mdi:console" :width="14" :height="14" />
</n-icon>
<n-text>输出 ({{ outputLines }} )</n-text>
</n-flex>
</template>
<n-card size="small" :bordered="true">
<n-text
code
class="output-text"
>
{{ formattedOutput }}
</n-text>
</n-card>
</n-collapse-item>
</n-collapse>
</n-space>
</n-card>
</template>
<style scoped>
.floating-panel {
font-family: "Monaco", "Consolas", monospace;
width: 300px;
position: absolute;
top: 20px;
right: 120px;
z-index: 100;
}
.output-text {
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
display: block;
}
.panel-content {
padding: 16px;
}
.no-variables-text {
font-style: italic;
text-align: center;
display: block;
padding: 20px;
}
.variable-header {
margin-bottom: 8px;
}
.variable-value {
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@@ -2,10 +2,141 @@
import copyTextToClipboard from "copy-text-to-clipboard" import copyTextToClipboard from "copy-text-to-clipboard"
import { useMessage } from "naive-ui" import { useMessage } from "naive-ui"
import DebugEditor from "../components/DebugEditor.vue" import DebugEditor from "../components/DebugEditor.vue"
import { code, input, reset, size } from "../composables/code" import FloatingPanel from "../components/FloatingPanel.vue"
import { code, input, reset, size, output } from "../composables/code"
import { debug } from "../api" import { debug } from "../api"
import { ref, computed } from "vue"
import { Icon } from "@iconify/vue"
const message = useMessage() const message = useMessage()
const showDebug = ref(false)
const debugData = ref<any>(null)
const currentStep = ref(0)
const showFloatingPanel = ref(false)
const isAutoRunning = ref(false)
const autoRunInterval = ref<number | null>(null)
// 计算当前行和下一步行
const currentLine = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
const line = debugData.value.trace[currentStep.value].line
console.log(`Step ${currentStep.value}: currentLine = ${line}`)
// 确保行号有效
return line && line > 0 ? line : undefined
}
return undefined
})
const nextLine = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value + 1]
) {
const line = debugData.value.trace[currentStep.value + 1].line
console.log(`Step ${currentStep.value}: nextLine = ${line}`)
// 确保行号有效且与当前行不同
return line && line > 0 && line !== currentLine.value ? line : undefined
}
console.log(`Step ${currentStep.value}: nextLine = undefined (no next step)`)
return undefined
})
// 计算当前步骤的变量
const currentVariables = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
return debugData.value.trace[currentStep.value].globals || {}
}
return {}
})
// 计算当前行文字
const currentLineText = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
const step = debugData.value.trace[currentStep.value]
const isLastStep = currentStep.value === debugData.value.trace.length - 1
const eventText = isLastStep ? "" : getEventText(step.event)
const stepText = isLastStep
? "最后一步"
: `当前第${currentStep.value + 1}`
return `${stepText}${eventText}`
}
return undefined
})
// 计算下一步行文字
const nextLineText = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value + 1]
) {
const step = debugData.value.trace[currentStep.value + 1]
const isNextLastStep =
currentStep.value + 1 === debugData.value.trace.length - 1
const eventText = isNextLastStep ? "" : getEventText(step.event)
const stepText = isNextLastStep ? "最后一步" : `下一步`
return `${stepText}${eventText}`
}
return undefined
})
// 检测是否为最后一步且事件为 raw_input
const isLastStepRawInput = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace.length > 0
) {
const lastStep = debugData.value.trace[debugData.value.trace.length - 1]
return lastStep.event === "raw_input"
}
return false
})
// 获取事件类型的中文描述
function getEventText(event: string): string {
switch (event) {
case "step_line":
return "" // 普通执行不显示额外文字
case "call":
return "(调用函数)"
case "return":
return "(函数返回)" // 更准确地表示函数返回
case "exception":
return "(异常)"
case "uncaught_exception":
return "(未捕获异常)"
case "raw_input":
return "(等待输入)"
default:
return event || ""
}
}
// 计算当前步骤的输出
const currentOutput = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
return debugData.value.trace[currentStep.value].stdout || ""
}
return output.value || ""
})
function copy() { function copy() {
copyTextToClipboard(code.value) copyTextToClipboard(code.value)
@@ -13,31 +144,273 @@ function copy() {
} }
async function handleDebug() { async function handleDebug() {
showDebug.value = true
const inputs = input.value ? input.value.split("\n") : [] const inputs = input.value ? input.value.split("\n") : []
const res = await debug(code.value, inputs) const res = await debug(code.value, inputs)
console.log(res.data) debugData.value = res.data
currentStep.value = 0
// 检查步骤数量并显示提醒
if (res.data.trace && res.data.trace.length > 5000) {
message.warning(`超过 5000 步,请优化代码或减少循环次数`)
}
// 检查最后一步是否为 raw_input 事件
if (res.data.trace && res.data.trace.length > 0) {
const lastStep = res.data.trace[res.data.trace.length - 1]
if (lastStep.event === "raw_input") {
message.info("程序正在等待输入,请在输入框输入内容后重新点击调试按钮")
}
}
// 显示前几个 trace 条目的行号
if (res.data.trace) {
console.log("First few trace entries:")
res.data.trace.slice(0, 5).forEach((entry: any, index: number) => {
console.log(` Step ${index}: line ${entry.line}, event: ${entry.event}`)
})
}
}
function firstStep() {
if (debugData.value && debugData.value.trace) {
currentStep.value = 0
}
}
function prevStep() {
if (debugData.value && debugData.value.trace && currentStep.value > 0) {
currentStep.value--
}
}
function nextStep() {
if (
debugData.value &&
debugData.value.trace &&
currentStep.value < debugData.value.trace.length - 1
) {
currentStep.value++
}
}
function lastStep() {
if (debugData.value && debugData.value.trace) {
currentStep.value = debugData.value.trace.length - 1
// 如果最后一步是 raw_input显示提醒
const lastStep = debugData.value.trace[debugData.value.trace.length - 1]
if (lastStep.event === "raw_input") {
message.info("程序正在等待输入,请在输入区域输入内容后重新调试")
}
}
}
function closeFloatingPanel() {
showFloatingPanel.value = false
}
function closeDebug() {
showDebug.value = false
showFloatingPanel.value = false
debugData.value = null
currentStep.value = 0
// 停止自动运行
if (autoRunInterval.value) {
clearInterval(autoRunInterval.value)
autoRunInterval.value = null
}
isAutoRunning.value = false
}
function autoRun() {
if (!debugData.value || !debugData.value.trace) return
if (isAutoRunning.value) {
// 停止自动运行
if (autoRunInterval.value) {
clearInterval(autoRunInterval.value)
autoRunInterval.value = null
}
isAutoRunning.value = false
} else {
// 开始自动运行
isAutoRunning.value = true
autoRunInterval.value = setInterval(() => {
if (currentStep.value < debugData.value.trace.length - 1) {
currentStep.value++
} else {
// 到达最后一步,停止自动运行
if (autoRunInterval.value) {
clearInterval(autoRunInterval.value)
autoRunInterval.value = null
}
isAutoRunning.value = false
}
}, 500) // 每500毫秒执行一步
}
} }
</script> </script>
<template> <template>
<DebugEditor <n-flex align="center" class="header">
label="代码区" <Icon icon="streamline-emojis:lemon" :width="24" :height="24"></Icon>
icon="streamline-emojis:lemon" <span class="title">代码区</span>
:font-size="size" <n-button
v-model="code.value" quaternary
:language="code.language" type="error"
> :disabled="!code.value"
<template #actions> @click="handleDebug"
<n-button >
quaternary 调试
type="error" </n-button>
:disabled="!code.value" <template v-if="showDebug">
@click="handleDebug" <n-tooltip>
> <template #trigger>
调试 <n-button text @click="firstStep">
</n-button> <template #icon>
<n-button quaternary type="primary" @click="copy">复制</n-button> <Icon icon="tabler:player-skip-back" />
<n-button quaternary @click="reset">清空</n-button> </template>
</n-button>
</template>
第一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button text type="primary" @click="prevStep">
<template #icon>
<Icon icon="tabler:chevron-left" />
</template>
</n-button>
</template>
上一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button
:type="isAutoRunning ? 'warning' : 'success'"
text
@click="autoRun"
>
<template #icon>
<Icon
:icon="
isAutoRunning ? 'tabler:player-pause' : 'tabler:player-play'
"
/>
</template>
</n-button>
</template>
{{ isAutoRunning ? "暂停自动运行" : "开始自动运行" }}
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button type="error" text @click="nextStep">
<template #icon>
<Icon icon="tabler:chevron-right" />
</template>
</n-button>
</template>
下一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button text @click="lastStep">
<template #icon>
<Icon icon="tabler:player-skip-forward" />
</template>
</n-button>
</template>
最后一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button text type="error" @click="closeDebug">
<template #icon>
<Icon icon="tabler:x" />
</template>
</n-button>
</template>
关闭调试
</n-tooltip>
</template> </template>
</DebugEditor> <n-button quaternary type="primary" @click="copy">复制</n-button>
<n-button quaternary @click="reset">清空</n-button>
<n-button
v-if="showDebug"
quaternary
type="info"
@click="showFloatingPanel = !showFloatingPanel"
>
{{ showFloatingPanel ? "隐藏面板" : "显示面板" }}
</n-button>
<!-- 调试进度条 -->
<n-flex
align="center"
v-if="showDebug && debugData && debugData.trace"
class="progress-section"
>
<n-slider
v-model:value="currentStep"
:min="0"
:max="debugData.trace.length - 1"
:step="1"
:tooltip="false"
class="debug-progress"
/>
<span class="progress-text">
步骤: {{ currentStep + 1 }} / {{ debugData.trace.length }}
</span>
</n-flex>
</n-flex>
<div class="debug-container">
<DebugEditor
v-model="code.value"
:font-size="size"
:language="code.language"
:current-line="currentLine"
:next-line="nextLine"
:current-line-text="currentLineText"
:next-line-text="nextLineText"
/>
<!-- 浮动面板 -->
<FloatingPanel
:visible="showFloatingPanel"
:variables="currentVariables"
:output="currentOutput"
@close="closeFloatingPanel"
/>
</div>
</template> </template>
<style scoped>
.header {
padding: 12px 20px;
height: 60px;
box-sizing: border-box;
}
.title {
font-size: 16px;
}
.debug-container {
position: relative;
}
.progress-section {
flex: 1;
margin-left: 12px;
}
.debug-progress {
flex: 1;
max-width: 200px;
}
.progress-text {
font-size: 12px;
color: var(--n-text-color-disabled);
white-space: nowrap;
}
</style>

View File

@@ -1,6 +1,9 @@
import { addAPIProvider } from "@iconify/vue" import { addAPIProvider } from "@iconify/vue"
import { import {
NButton, NButton,
NCard,
NCollapse,
NCollapseItem,
NConfigProvider, NConfigProvider,
NDropdown, NDropdown,
NFlex, NFlex,
@@ -14,11 +17,15 @@ import {
NModal, NModal,
NPopover, NPopover,
NSelect, NSelect,
NSpace,
NSplit, NSplit,
NTabPane, NTabPane,
NTabs, NTabs,
NTag, NTag,
NSpin, NSpin,
NText,
NTooltip,
NSlider,
create, create,
} from "naive-ui" } from "naive-ui"
import "normalize.css" import "normalize.css"
@@ -28,6 +35,9 @@ import App from "./App.vue"
const naive = create({ const naive = create({
components: [ components: [
NButton, NButton,
NCard,
NCollapse,
NCollapseItem,
NConfigProvider, NConfigProvider,
NMessageProvider, NMessageProvider,
NLayout, NLayout,
@@ -47,6 +57,10 @@ const naive = create({
NTabPane, NTabPane,
NDropdown, NDropdown,
NSpin, NSpin,
NSpace,
NText,
NTooltip,
NSlider,
], ],
}) })