feat(题目页): 手机上写代码顺手多了:键盘上面多一排常用符号(括号、引号、冒号、缩进…点一下就进去,括号自动配对),代码字号改小、长行折行不再看不见右半截;输入输出说明上下排、例子不再被左边两列标签挤窄;题单里那条不再超出屏幕
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3dbdc7cb1b
commit
7e68e11371
13 files changed
+262
-16
No files matched your search
@@ -10,7 +10,7 @@ import { explainRuntimeError } from "oj/problem/utils/runtimeError"
|
|||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { mainWrapExtensions, wrapMain } from "../wrapMain"
|
import { mainWrapExtensions, wrapMain } from "../wrapMain"
|
||||||
import { hangWrapExtensions } from "../hangIndent"
|
import { hangWrapExtensions } from "shared/extensions/hangIndent"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
|
||||||
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import ExerciseResult from "./ExerciseResult.vue"
|
import ExerciseResult from "./ExerciseResult.vue"
|
||||||
import type { Exercise, ExerciseDebugData } from "utils/types"
|
import type { Exercise, ExerciseDebugData } from "utils/types"
|
||||||
import { highlightLines } from "shared/highlight"
|
import { highlightLines } from "shared/highlight"
|
||||||
import { hangStyle } from "../hangIndent"
|
import { hangStyle } from "shared/extensions/hangIndent"
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import ExerciseResult from "./ExerciseResult.vue"
|
import ExerciseResult from "./ExerciseResult.vue"
|
||||||
import type { Exercise, ExerciseFillData } from "utils/types"
|
import type { Exercise, ExerciseFillData } from "utils/types"
|
||||||
import { highlightWithHoles } from "shared/highlight"
|
import { highlightWithHoles } from "shared/highlight"
|
||||||
import { hangStyle } from "../hangIndent"
|
import { hangStyle } from "shared/extensions/hangIndent"
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Exercise, ExerciseSortData } from "utils/types"
|
|||||||
import { shuffle } from "../composables/useShuffle"
|
import { shuffle } from "../composables/useShuffle"
|
||||||
import { highlightLines } from "shared/highlight"
|
import { highlightLines } from "shared/highlight"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { hangStyle } from "../hangIndent"
|
import { hangStyle } from "shared/extensions/hangIndent"
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -311,7 +311,8 @@ onSolvedHere(() => {
|
|||||||
<router-link :to="setBar.home" class="back set-back" :title="`回到题单:${setBar.title}`">
|
<router-link :to="setBar.home" class="back set-back" :title="`回到题单:${setBar.title}`">
|
||||||
‹ {{ setBar.title }}
|
‹ {{ setBar.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div v-if="setBar.joined" class="set-progress" aria-hidden="true">
|
<!-- 手机上一排放不下题单名 + 进度条 + 做完几道 + 两个大箭头,进度条让掉,「做完 x/y」还在 -->
|
||||||
|
<div v-if="setBar.joined && !isMobile" class="set-progress" aria-hidden="true">
|
||||||
<div class="set-progress-fill" :style="{ width: `${setBar.percentage}%` }" />
|
<div class="set-progress-fill" :style="{ width: `${setBar.percentage}%` }" />
|
||||||
</div>
|
</div>
|
||||||
<span v-if="setBar.joined" class="meta">
|
<span v-if="setBar.joined" class="meta">
|
||||||
@@ -319,7 +320,9 @@ onSolvedHere(() => {
|
|||||||
<template v-if="setBar.position"> · 第 {{ setBar.position }} 题</template>
|
<template v-if="setBar.position"> · 第 {{ setBar.position }} 题</template>
|
||||||
</span>
|
</span>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="meta">还没加入,在这里做对了不算进度</span>
|
<span class="meta">{{
|
||||||
|
isMobile ? "没加入,不算进度" : "还没加入,在这里做对了不算进度"
|
||||||
|
}}</span>
|
||||||
<n-button size="tiny" type="primary" :loading="joiningSet" @click="joinHere"
|
<n-button size="tiny" type="primary" :loading="joiningSet" @click="joinHere"
|
||||||
>加入题单</n-button
|
>加入题单</n-button
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useCodeStore } from "oj/store/code"
|
|||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import SubmitCode from "./SubmitCode.vue"
|
import SubmitCode from "./SubmitCode.vue"
|
||||||
|
import { editorFocused } from "../utils/symbolKeys"
|
||||||
|
|
||||||
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
|
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ const canRunSamples = computed(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mobile-actions">
|
<!-- 写代码时(键盘弹着)让位给符号键:两条都贴底会叠在一起,安卓上还会被键盘顶上来 -->
|
||||||
|
<div v-show="!editorFocused" class="mobile-actions">
|
||||||
<n-button
|
<n-button
|
||||||
v-if="canRunSamples"
|
v-if="canRunSamples"
|
||||||
size="large"
|
size="large"
|
||||||
|
|||||||
@@ -154,10 +154,16 @@ const sampleTextWidth = computed(() => {
|
|||||||
|
|
||||||
const sampleTable = useTemplateRef<HTMLElement>("sampleTable")
|
const sampleTable = useTemplateRef<HTMLElement>("sampleTable")
|
||||||
const { width: sampleTableWidth } = useElementSize(sampleTable)
|
const { width: sampleTableWidth } = useElementSize(sampleTable)
|
||||||
|
/**
|
||||||
|
* 窄(手机,或桌面左栏拖到最窄):「例子 N」不占一列,改成每个例子头上一行小字。
|
||||||
|
* 390 宽的手机上那一列连同间距要吃掉 64px,框只剩一半多
|
||||||
|
*/
|
||||||
|
const sampleNarrow = computed(() => sampleTableWidth.value > 0 && sampleTableWidth.value < 480)
|
||||||
/** 摆成表时,半格里留给文字的宽度 */
|
/** 摆成表时,半格里留给文字的宽度 */
|
||||||
const sampleHalfWidth = computed(
|
const sampleHalfWidth = computed(
|
||||||
() =>
|
() =>
|
||||||
(sampleTableWidth.value - SAMPLE_NO_W - SAMPLE_GAP * 2) / 2 -
|
(sampleTableWidth.value - (sampleNarrow.value ? 0 : SAMPLE_NO_W + SAMPLE_GAP) - SAMPLE_GAP) /
|
||||||
|
2 -
|
||||||
SAMPLE_PAD_L -
|
SAMPLE_PAD_L -
|
||||||
SAMPLE_PAD_R -
|
SAMPLE_PAD_R -
|
||||||
SAMPLE_COPY_W,
|
SAMPLE_COPY_W,
|
||||||
@@ -179,7 +185,7 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="problem">
|
<div v-if="problem" class="problem-content">
|
||||||
<header class="problem-head">
|
<header class="problem-head">
|
||||||
<div class="title-row">
|
<div class="title-row">
|
||||||
<span class="pid">{{ problem._id }}</span>
|
<span class="pid">{{ problem._id }}</span>
|
||||||
@@ -329,12 +335,12 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
<div
|
<div
|
||||||
ref="sampleTable"
|
ref="sampleTable"
|
||||||
class="sample-table"
|
class="sample-table"
|
||||||
:class="{ stacked: samplesStacked }"
|
:class="{ stacked: samplesStacked, narrow: sampleNarrow }"
|
||||||
role="table"
|
role="table"
|
||||||
aria-label="例子"
|
aria-label="例子"
|
||||||
>
|
>
|
||||||
<div v-if="!samplesStacked" class="sample-row" role="row">
|
<div v-if="!samplesStacked" class="sample-row" role="row">
|
||||||
<span role="columnheader"></span>
|
<span v-if="!sampleNarrow" role="columnheader"></span>
|
||||||
<span role="columnheader" class="sample-label">输入</span>
|
<span role="columnheader" class="sample-label">输入</span>
|
||||||
<span role="columnheader" class="sample-label">输出</span>
|
<span role="columnheader" class="sample-label">输出</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -437,6 +443,10 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.problem-content {
|
||||||
|
container: problem-content / inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
.block-link {
|
.block-link {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
@@ -460,6 +470,13 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 窄的时候输入、输出上下排:并排的话长一点的说明挤成一字两行的窄条,旁边一栏空着 */
|
||||||
|
@container problem-content (max-width: 480px) {
|
||||||
|
.io {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 语法要求:一行的浅绿框,硬性的判定条件,放在最前面 */
|
/* 语法要求:一行的浅绿框,硬性的判定条件,放在最前面 */
|
||||||
.requirements {
|
.requirements {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -584,6 +601,26 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 窄:「例子 N」占满一行当小标题,下面输入、输出(并排或上下排) */
|
||||||
|
.narrow .sample-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow.stacked .sample-row {
|
||||||
|
grid-template-columns: 32px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow .sample-no,
|
||||||
|
.narrow.stacked .sample-no {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: auto;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-table.narrow {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.sample-cell {
|
.sample-cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import storage from "utils/storage"
|
|||||||
import type { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
|
import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
|
||||||
import { errorMarkExtensions } from "oj/problem/utils/errorMark"
|
import { errorMarkExtensions } from "oj/problem/utils/errorMark"
|
||||||
|
import { editorFocusTracker } from "oj/problem/utils/symbolKeys"
|
||||||
|
import { hangWrapExtensions } from "shared/extensions/hangIndent"
|
||||||
import EditorToolbar from "./EditorToolbar.vue"
|
import EditorToolbar from "./EditorToolbar.vue"
|
||||||
import { useDraftKey } from "../composables/draftKey"
|
import { useDraftKey } from "../composables/draftKey"
|
||||||
import { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
|
import { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
|
||||||
@@ -85,8 +87,11 @@ watch(
|
|||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
// 拼一次、留同一个数组:每次渲染新建会让编辑器反复重配扩展
|
// 拼一次、留同一个数组:每次渲染新建会让编辑器反复重配扩展。
|
||||||
const editorExtensions = [...editTraceExtensions, ...errorMarkExtensions]
|
// 手机上另加折行(20px 一行只放得下二十来个字,长行看不见右半截)
|
||||||
|
const desktopExtensions = [...editTraceExtensions, ...errorMarkExtensions, editorFocusTracker]
|
||||||
|
const mobileExtensions = [...desktopExtensions, ...hangWrapExtensions]
|
||||||
|
const editorExtensions = computed(() => (isDesktop.value ? desktopExtensions : mobileExtensions))
|
||||||
|
|
||||||
const contestID = route.params.contestID || null
|
const contestID = route.params.contestID || null
|
||||||
// 草稿存在哪个键下(题单入口单独一份),和「重置代码」共用一份算法
|
// 草稿存在哪个键下(题单入口单独一份),和「重置代码」共用一份算法
|
||||||
@@ -223,6 +228,7 @@ onBeforeUnmount(() => flowchartStore.attachEditor(null))
|
|||||||
:collab="ctx.collab"
|
:collab="ctx.collab"
|
||||||
:height="editorHeight"
|
:height="editorHeight"
|
||||||
:extra-extensions="editorExtensions"
|
:extra-extensions="editorExtensions"
|
||||||
|
:font-size="isDesktop ? 20 : 15"
|
||||||
@update:model-value="changeCode"
|
@update:model-value="changeCode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
|
import { useCodeStore } from "oj/store/code"
|
||||||
|
import { editorFocused, SYMBOL_KEYS, typeSymbol } from "../utils/symbolKeys"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机上贴着键盘顶的一排符号键,编辑器有焦点(键盘弹着)时才出来。
|
||||||
|
*
|
||||||
|
* 位置按 visualViewport 算:iOS 弹键盘时布局视口不变,position: fixed; bottom: 0 的东西
|
||||||
|
* 会被键盘盖住,只有可视视口的底边才是键盘的上沿。
|
||||||
|
* 按下时 preventDefault:不抢编辑器的焦点,键盘不收起来。
|
||||||
|
*/
|
||||||
|
const theme = useThemeVars()
|
||||||
|
const codeStore = useCodeStore()
|
||||||
|
|
||||||
|
const keys = computed(() => {
|
||||||
|
const language = codeStore.code.language
|
||||||
|
if (language === "SQL") return SYMBOL_KEYS.sql
|
||||||
|
if (language === "Python") return SYMBOL_KEYS.python
|
||||||
|
if (language === "C" || language === "C++") return SYMBOL_KEYS.c
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
const BAR_HEIGHT = 44
|
||||||
|
const top = ref<number | null>(null)
|
||||||
|
|
||||||
|
function place() {
|
||||||
|
const viewport = window.visualViewport
|
||||||
|
top.value = viewport ? viewport.offsetTop + viewport.height - BAR_HEIGHT : null
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener(() => window.visualViewport, "resize", place)
|
||||||
|
useEventListener(() => window.visualViewport, "scroll", place)
|
||||||
|
watch(editorFocused, (focused) => focused && place(), { immediate: true })
|
||||||
|
// 键盘弹着时点了别处换题、离开页面,编辑器直接卸载,不一定等得到失焦
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editorFocused.value = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="editorFocused && keys.length"
|
||||||
|
class="symbol-keys"
|
||||||
|
:style="top === null ? { bottom: '0px' } : { top: `${top}px` }"
|
||||||
|
role="toolbar"
|
||||||
|
aria-label="常用符号"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="key in keys"
|
||||||
|
:key="key.label"
|
||||||
|
type="button"
|
||||||
|
class="key"
|
||||||
|
:class="{ wide: key.label.length > 1 }"
|
||||||
|
@pointerdown.prevent
|
||||||
|
@mousedown.prevent
|
||||||
|
@click="typeSymbol(key)"
|
||||||
|
>
|
||||||
|
{{ key.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.symbol-keys {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 20;
|
||||||
|
height: 44px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 5px 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
background-color: v-bind("theme.bodyColor");
|
||||||
|
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol-keys::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
flex: none;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 1px solid rgba(128, 128, 128, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(128, 128, 128, 0.06);
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
font-size: 17px;
|
||||||
|
color: inherit;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key.wide {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key:active {
|
||||||
|
background-color: rgba(24, 160, 88, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -34,6 +34,7 @@ const ProblemContent = defineAsyncComponent(() => import("./components/ProblemCo
|
|||||||
const ProblemDrawer = defineAsyncComponent(() => import("./components/ProblemDrawer.vue"))
|
const ProblemDrawer = defineAsyncComponent(() => import("./components/ProblemDrawer.vue"))
|
||||||
const ResultPane = defineAsyncComponent(() => import("./components/ResultPane.vue"))
|
const ResultPane = defineAsyncComponent(() => import("./components/ResultPane.vue"))
|
||||||
const MobileActionBar = defineAsyncComponent(() => import("./components/MobileActionBar.vue"))
|
const MobileActionBar = defineAsyncComponent(() => import("./components/MobileActionBar.vue"))
|
||||||
|
const SymbolKeys = defineAsyncComponent(() => import("./components/SymbolKeys.vue"))
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
problemID: string
|
problemID: string
|
||||||
@@ -386,6 +387,7 @@ onBeforeUnmount(() => {
|
|||||||
</n-tabs>
|
</n-tabs>
|
||||||
<ProblemDrawer v-model="drawer" />
|
<ProblemDrawer v-model="drawer" />
|
||||||
<MobileActionBar />
|
<MobileActionBar />
|
||||||
|
<SymbolKeys />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<NotFound
|
<NotFound
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ const theme = EditorView.baseTheme({
|
|||||||
|
|
||||||
export const errorMarkExtensions = [markField, punctuationField, tracker, theme]
|
export const errorMarkExtensions = [markField, punctuationField, tracker, theme]
|
||||||
|
|
||||||
|
/** 题目页主编辑器(没挂着就是 null)。手机上的符号键往它里面插字 */
|
||||||
|
export function currentEditorView(): EditorView | null {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
/** 标出这些中文标点(位置是整篇代码里的偏移,来自 findChinesePunctuation) */
|
/** 标出这些中文标点(位置是整篇代码里的偏移,来自 findChinesePunctuation) */
|
||||||
export function showPunctuationMarks(fixes: PunctuationFix[]) {
|
export function showPunctuationMarks(fixes: PunctuationFix[]) {
|
||||||
current?.dispatch({ effects: setPunctuation.of(fixes) })
|
current?.dispatch({ effects: setPunctuation.of(fixes) })
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { insertBracket } from "@codemirror/autocomplete"
|
||||||
|
import { EditorView } from "@codemirror/view"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { currentEditorView } from "./errorMark"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机上写代码的符号键(SymbolKeys.vue)。括号、引号、冒号在手机键盘上都要切到符号页,
|
||||||
|
* 一行 print(int(input())) 来回切七八次。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 题目页主编辑器有没有焦点 —— 有焦点就是键盘弹着,这时才摆符号键 */
|
||||||
|
export const editorFocused = ref(false)
|
||||||
|
|
||||||
|
export const editorFocusTracker = EditorView.focusChangeEffect.of((_state, focusing) => {
|
||||||
|
editorFocused.value = focusing
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface SymbolKey {
|
||||||
|
label: string
|
||||||
|
/** 插进去的字;不写就是 label */
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const INDENT: SymbolKey = { label: "缩进", text: " " }
|
||||||
|
|
||||||
|
/** 按语言给一排:最常用的在前,放不下的往右滑 */
|
||||||
|
export const SYMBOL_KEYS: Record<"python" | "c" | "sql", SymbolKey[]> = {
|
||||||
|
python: [
|
||||||
|
INDENT,
|
||||||
|
...["(", ")", '"', ":", "=", "[", "]", ",", "'", "+", "-", "*", "/", "%", "<", ">", "#"].map(
|
||||||
|
(label) => ({ label }),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
c: [
|
||||||
|
INDENT,
|
||||||
|
...[
|
||||||
|
"(",
|
||||||
|
")",
|
||||||
|
";",
|
||||||
|
'"',
|
||||||
|
"{",
|
||||||
|
"}",
|
||||||
|
"=",
|
||||||
|
"%",
|
||||||
|
"&",
|
||||||
|
"[",
|
||||||
|
"]",
|
||||||
|
",",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
"*",
|
||||||
|
"/",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
].map((label) => ({ label })),
|
||||||
|
],
|
||||||
|
sql: ["(", ")", "'", ",", "*", "=", ";", "<", ">", "%"].map((label) => ({ label })),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在光标处插一个键。括号、引号走 CodeMirror 自己的配对规则(和键盘上敲是一样的:
|
||||||
|
* 敲「(」补上「)」、光标前正好是「)」时敲「)」是跳过去),别的原样插。
|
||||||
|
*/
|
||||||
|
export function typeSymbol(key: SymbolKey) {
|
||||||
|
const view = currentEditorView()
|
||||||
|
if (!view) return
|
||||||
|
const text = key.text ?? key.label
|
||||||
|
const bracket = text.length === 1 ? insertBracket(view.state, text) : null
|
||||||
|
view.dispatch(
|
||||||
|
bracket ?? view.state.update(view.state.replaceSelection(text), { userEvent: "input.type" }),
|
||||||
|
)
|
||||||
|
view.dispatch({ effects: EditorView.scrollIntoView(view.state.selection.main.head) })
|
||||||
|
view.focus()
|
||||||
|
}
|
||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
} from "@codemirror/view"
|
} from "@codemirror/view"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手机上代码折行。原来是横着滚,可没有滚动条也看不出右边还有,
|
* 手机上代码折行(自学的代码块和练一练、题目页的编辑器)。原来是横着滚,可没有滚动条
|
||||||
* 综合练习那种长 print 整整半行看不见。折下来的那截缩到这一行的代码下面再进两格,
|
* 也看不出右边还有,自学综合练习那种长 print 整整半行看不见。折下来的那截缩到这一行的
|
||||||
* 一眼看得出还是同一行。
|
* 代码下面再进两格,一眼看得出还是同一行。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 行首空白占几格,tab 按 4 */
|
/** 行首空白占几格,tab 按 4 */
|
||||||
@@ -37,6 +37,8 @@ export function hangStyle(text: string): Record<string, string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const glue = Decoration.mark({ attributes: { style: "white-space: nowrap" } })
|
||||||
|
|
||||||
function hangDecorations(view: EditorView): DecorationSet {
|
function hangDecorations(view: EditorView): DecorationSet {
|
||||||
const builder = new RangeSetBuilder<Decoration>()
|
const builder = new RangeSetBuilder<Decoration>()
|
||||||
for (const { from, to } of view.visibleRanges) {
|
for (const { from, to } of view.visibleRanges) {
|
||||||
@@ -53,6 +55,12 @@ function hangDecorations(view: EditorView): DecorationSet {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
// 行首缩进后面紧跟一长串没空格的(printf("%d",a*100+b*10+c);),浏览器会在缩进后面折,
|
||||||
|
// 第一行只剩一截空白。把缩进和第一个字粘成不折的一段,长串就接在缩进后面再折
|
||||||
|
const indent = line.text.match(/^[ \t]+/)?.[0].length ?? 0
|
||||||
|
if (indent > 0 && indent < line.length) {
|
||||||
|
builder.add(line.from, line.from + indent + 1, glue)
|
||||||
|
}
|
||||||
pos = line.to + 1
|
pos = line.to + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user