This commit is contained in:
@@ -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>
|
|
||||||
|
|||||||
198
src/components/FloatingPanel.vue
Normal file
198
src/components/FloatingPanel.vue
Normal 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>
|
||||||
@@ -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,21 +144,119 @@ 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"
|
|
||||||
v-model="code.value"
|
|
||||||
:language="code.language"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<n-button
|
<n-button
|
||||||
quaternary
|
quaternary
|
||||||
type="error"
|
type="error"
|
||||||
@@ -36,8 +265,152 @@ async function handleDebug() {
|
|||||||
>
|
>
|
||||||
调试
|
调试
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<template v-if="showDebug">
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button text @click="firstStep">
|
||||||
|
<template #icon>
|
||||||
|
<Icon icon="tabler:player-skip-back" />
|
||||||
|
</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>
|
||||||
<n-button quaternary type="primary" @click="copy">复制</n-button>
|
<n-button quaternary type="primary" @click="copy">复制</n-button>
|
||||||
<n-button quaternary @click="reset">清空</n-button>
|
<n-button quaternary @click="reset">清空</n-button>
|
||||||
</template>
|
<n-button
|
||||||
</DebugEditor>
|
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>
|
||||||
|
|||||||
14
src/main.ts
14
src/main.ts
@@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user