Add synchronized editor spelling and tab width preferences

This commit is contained in:
eezd committed 2026-10-20 19:08:43 +08:00
1 parent f9cfb138ea
commit f188f0fc32
4 files changed
+50 -6

No files matched your search

+18 -1
View File
@@ -5,7 +5,7 @@ import { allCommands, formatKeys, runCommand } from './commands'
import Icon, { type IconName } from './Icon.vue'
import AppearanceSettings from './AppearanceSettings.vue'
import PluginsPanel from './PluginsPanel.vue'
import { CONTENT_WIDTHS, FONT_SIZES, loadSettings, settings, updateSettings, type Settings } from './settings'
import { CONTENT_WIDTHS, FONT_SIZES, TAB_SIZES, loadSettings, settings, updateSettings, type Settings } from './settings'
import SharePanel from './SharePanel.vue'
import TokensPanel from './TokensPanel.vue'
import TrashPanel from './TrashPanel.vue'
@@ -13,6 +13,7 @@ import { toast, ws, type SettingsSection } from './workspace'
const SECTIONS: { id: SettingsSection; title: string; icon: IconName }[] = [
{ id: 'appearance', title: '外观', icon: 'palette' },
{ id: 'editor', title: '编辑器', icon: 'edit' },
{ id: 'plugins', title: '插件', icon: 'command' },
{ id: 'account', title: '账户', icon: 'user' },
{ id: 'shortcuts', title: '快捷键', icon: 'command' },
@@ -135,6 +136,22 @@ async function changePassword() {
<p class="rm-share-note">设置保存在服务器上,所有设备共用。</p>
</template>
<template v-else-if="section === 'editor'">
<div class="rm-setting">
<label for="rm-spellcheck" class="rm-setting-label">拼写检查</label>
<input id="rm-spellcheck" type="checkbox" :checked="settings.spellcheck" @change="change({ spellcheck: ($event.target as HTMLInputElement).checked })" />
</div>
<p class="rm-share-note">使用浏览器的拼写检查,检查语言取决于浏览器设置。</p>
<div class="rm-setting">
<label for="rm-tab-size" class="rm-setting-label">制表符宽度</label>
<select id="rm-tab-size" :value="settings.tabSize" @change="change({ tabSize: Number(($event.target as HTMLSelectElement).value) })">
<option v-for="n in TAB_SIZES" :key="n" :value="n">{{ n }} 列</option>
</select>
</div>
<p class="rm-share-note">仅调整 Tab 字符的显示宽度,不改写正文。Tab 键仍按两个空格缩进。</p>
<p class="rm-share-note">设置保存在服务器上,所有设备共用。</p>
</template>
<template v-else-if="section === 'account'">
<form class="rm-auth-card rm-settings-form" @submit.prevent="changePassword">
<h3>修改登录密码</h3>
+4 -1
View File
@@ -5,6 +5,7 @@ import { api } from './api'
import { applyAppearance, appearancePaused, sanitizeAppearance, type Appearance } from '../themes/appearance'
export const FONT_SIZES = [14, 15, 16, 17, 18, 20, 22]
export const TAB_SIZES = [2, 4, 8]
// 0 为全宽
export const CONTENT_WIDTHS = [
{ label: '窄', value: 680 },
@@ -15,7 +16,7 @@ export const CONTENT_WIDTHS = [
export const SIDEBAR_WIDTH = { min: 200, max: 480 }
const DEFAULTS = { fontSize: 16, contentWidth: 760, sidebarWidth: 260 }
const DEFAULTS = { fontSize: 16, contentWidth: 760, sidebarWidth: 260, spellcheck: false, tabSize: 4 }
export type Settings = typeof DEFAULTS & Appearance
const CACHE_KEY = 'rawmark.settings'
@@ -27,6 +28,8 @@ function sanitize(raw: Record<string, unknown>): Settings {
const pick = <T>(v: unknown, allowed: T[], def: T) => (allowed.includes(v as T) ? (v as T) : def)
return {
fontSize: pick(raw.fontSize, FONT_SIZES, DEFAULTS.fontSize),
spellcheck: typeof raw.spellcheck === 'boolean' ? raw.spellcheck : DEFAULTS.spellcheck,
tabSize: pick(raw.tabSize, TAB_SIZES, DEFAULTS.tabSize),
...sanitizeAppearance(raw),
contentWidth: pick(
raw.contentWidth,
+20 -3
View File
@@ -1,9 +1,9 @@
// 工作区状态与操作:目录树、标签页、保存队列、冲突处理、实时同步。
// 文件操作一律先更新界面再请求服务器,失败时回滚,保证"操作即时生效"。
import { Annotation, Transaction, type EditorState } from '@codemirror/state'
import { Annotation, EditorState, Transaction } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { nextTick, reactive, watch } from 'vue'
import { createEditorState, insertImageFiles, pasteImages, languageCompartment, pluginCompartment, pluginEditorExtensions, pluginLanguage } from '../editor'
import { createEditorState, insertImageFiles, pasteImages, languageCompartment, pluginCompartment, pluginEditorExtensions, pluginLanguage, preferencesCompartment } from '../editor'
import { handleShortcut, registerCommand } from './commands'
import { api, ApiError, type Entry } from './api'
import { drafts } from './drafts'
@@ -30,7 +30,7 @@ export interface Tab {
conflictRev?: string
}
export type SettingsSection = 'appearance' | 'account' | 'shortcuts' | 'shares' | 'tokens' | 'trash' | 'plugins'
export type SettingsSection = 'appearance' | 'editor' | 'account' | 'shortcuts' | 'shares' | 'tokens' | 'trash' | 'plugins'
export interface Toast {
id: number
@@ -176,6 +176,8 @@ function buildState(path: string, doc: string): EditorState {
noteRefs.set(path, ref)
return createEditorState({
doc,
spellcheck: settings.spellcheck,
tabSize: settings.tabSize,
resolveImage: (src) => resolveAssetUrl(ref.path, src),
onChange: (u) => {
editorStates.set(ref.path, u.state)
@@ -944,6 +946,21 @@ export async function startWorkspace() {
() => [settings.fontSize, settings.contentWidth, settings.sidebarWidth],
() => view?.requestMeasure(),
)
watch(
() => [settings.spellcheck, settings.tabSize],
() => {
const effects = preferencesCompartment.reconfigure([
EditorState.tabSize.of(settings.tabSize),
EditorView.contentAttributes.of({ spellcheck: String(settings.spellcheck) }),
])
for (const [path, state] of editorStates) {
if (path === ws.active && view) {
view.dispatch({ effects })
editorStates.set(path, view.state)
} else editorStates.set(path, state.update({ effects }).state)
}
},
)
window.addEventListener('rawmark-appearance', () => requestAnimationFrame(() => view?.requestMeasure()))
window.addEventListener('rawmark-plugin-notice', (event) => toast((event as CustomEvent<string>).detail))
// 页面隐藏/关闭前尽力发出待保存的修改(部分浏览器关闭标签页时只触发 pagehide)
+8 -1
View File
@@ -20,6 +20,7 @@ export { insertImageFiles, pasteImages } from './paste'
// 沿用其语言数据,markdownKeymap 中的命令据此识别 Markdown(isActiveAt 按语言数据判断)
export const pluginCompartment = new Compartment()
export const languageCompartment = new Compartment()
export const preferencesCompartment = new Compartment()
export function pluginLanguage() { return new Language(
markdownLanguage.data,
(markdownLanguage.parser as MarkdownParser).configure([typoraExtensions, ...[...markdownSyntax.values()].map((entry) => entry.value)]),
@@ -30,6 +31,8 @@ export function pluginEditorExtensions(): Extension[] { return [...editorExtensi
export interface EditorStateOptions {
doc: string
spellcheck?: boolean
tabSize?: number
resolveImage?: (src: string) => string
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
onChange?: (update: ViewUpdate) => void
@@ -65,7 +68,11 @@ export function createEditorState(opts: EditorStateOptions): EditorState {
// CodeMirror 只在 Linux/Mac 上把 Ctrl/Cmd+Shift+Z 绑定为重做,Windows 上会退化成撤销;
// 与 Typora、VS Code 保持一致,所有平台都支持
keymap.of([indentWithTab, { key: 'Mod-Shift-z', run: redo, preventDefault: true }, ...defaultKeymap, ...historyKeymap]),
EditorView.contentAttributes.of({ spellcheck: 'false', autocapitalize: 'sentences' }),
preferencesCompartment.of([
EditorState.tabSize.of(opts.tabSize ?? 4),
EditorView.contentAttributes.of({ spellcheck: String(opts.spellcheck ?? false) }),
]),
EditorView.contentAttributes.of({ autocapitalize: 'sentences' }),
]
if (opts.resolveImage) extensions.push(resolveImageUrl.of(opts.resolveImage))
if (opts.onChange) {