Add synchronized editor spelling and tab width preferences
This commit is contained in:
1 parent
f9cfb138ea
commit
f188f0fc32
4 files changed
+50
-6
No files matched your search
@@ -5,7 +5,7 @@ import { allCommands, formatKeys, runCommand } from './commands'
|
|||||||
import Icon, { type IconName } from './Icon.vue'
|
import Icon, { type IconName } from './Icon.vue'
|
||||||
import AppearanceSettings from './AppearanceSettings.vue'
|
import AppearanceSettings from './AppearanceSettings.vue'
|
||||||
import PluginsPanel from './PluginsPanel.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 SharePanel from './SharePanel.vue'
|
||||||
import TokensPanel from './TokensPanel.vue'
|
import TokensPanel from './TokensPanel.vue'
|
||||||
import TrashPanel from './TrashPanel.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 }[] = [
|
const SECTIONS: { id: SettingsSection; title: string; icon: IconName }[] = [
|
||||||
{ id: 'appearance', title: '外观', icon: 'palette' },
|
{ id: 'appearance', title: '外观', icon: 'palette' },
|
||||||
|
{ id: 'editor', title: '编辑器', icon: 'edit' },
|
||||||
{ id: 'plugins', title: '插件', icon: 'command' },
|
{ id: 'plugins', title: '插件', icon: 'command' },
|
||||||
{ id: 'account', title: '账户', icon: 'user' },
|
{ id: 'account', title: '账户', icon: 'user' },
|
||||||
{ id: 'shortcuts', title: '快捷键', icon: 'command' },
|
{ id: 'shortcuts', title: '快捷键', icon: 'command' },
|
||||||
@@ -135,6 +136,22 @@ async function changePassword() {
|
|||||||
<p class="rm-share-note">设置保存在服务器上,所有设备共用。</p>
|
<p class="rm-share-note">设置保存在服务器上,所有设备共用。</p>
|
||||||
</template>
|
</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'">
|
<template v-else-if="section === 'account'">
|
||||||
<form class="rm-auth-card rm-settings-form" @submit.prevent="changePassword">
|
<form class="rm-auth-card rm-settings-form" @submit.prevent="changePassword">
|
||||||
<h3>修改登录密码</h3>
|
<h3>修改登录密码</h3>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { api } from './api'
|
|||||||
import { applyAppearance, appearancePaused, sanitizeAppearance, type Appearance } from '../themes/appearance'
|
import { applyAppearance, appearancePaused, sanitizeAppearance, type Appearance } from '../themes/appearance'
|
||||||
|
|
||||||
export const FONT_SIZES = [14, 15, 16, 17, 18, 20, 22]
|
export const FONT_SIZES = [14, 15, 16, 17, 18, 20, 22]
|
||||||
|
export const TAB_SIZES = [2, 4, 8]
|
||||||
// 0 为全宽
|
// 0 为全宽
|
||||||
export const CONTENT_WIDTHS = [
|
export const CONTENT_WIDTHS = [
|
||||||
{ label: '窄', value: 680 },
|
{ label: '窄', value: 680 },
|
||||||
@@ -15,7 +16,7 @@ export const CONTENT_WIDTHS = [
|
|||||||
|
|
||||||
export const SIDEBAR_WIDTH = { min: 200, max: 480 }
|
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
|
export type Settings = typeof DEFAULTS & Appearance
|
||||||
|
|
||||||
const CACHE_KEY = 'rawmark.settings'
|
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)
|
const pick = <T>(v: unknown, allowed: T[], def: T) => (allowed.includes(v as T) ? (v as T) : def)
|
||||||
return {
|
return {
|
||||||
fontSize: pick(raw.fontSize, FONT_SIZES, DEFAULTS.fontSize),
|
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),
|
...sanitizeAppearance(raw),
|
||||||
contentWidth: pick(
|
contentWidth: pick(
|
||||||
raw.contentWidth,
|
raw.contentWidth,
|
||||||
|
|||||||
@@ -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 { EditorView } from '@codemirror/view'
|
||||||
import { nextTick, reactive, watch } from 'vue'
|
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 { handleShortcut, registerCommand } from './commands'
|
||||||
import { api, ApiError, type Entry } from './api'
|
import { api, ApiError, type Entry } from './api'
|
||||||
import { drafts } from './drafts'
|
import { drafts } from './drafts'
|
||||||
@@ -30,7 +30,7 @@ export interface Tab {
|
|||||||
conflictRev?: string
|
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 {
|
export interface Toast {
|
||||||
id: number
|
id: number
|
||||||
@@ -176,6 +176,8 @@ function buildState(path: string, doc: string): EditorState {
|
|||||||
noteRefs.set(path, ref)
|
noteRefs.set(path, ref)
|
||||||
return createEditorState({
|
return createEditorState({
|
||||||
doc,
|
doc,
|
||||||
|
spellcheck: settings.spellcheck,
|
||||||
|
tabSize: settings.tabSize,
|
||||||
resolveImage: (src) => resolveAssetUrl(ref.path, src),
|
resolveImage: (src) => resolveAssetUrl(ref.path, src),
|
||||||
onChange: (u) => {
|
onChange: (u) => {
|
||||||
editorStates.set(ref.path, u.state)
|
editorStates.set(ref.path, u.state)
|
||||||
@@ -944,6 +946,21 @@ export async function startWorkspace() {
|
|||||||
() => [settings.fontSize, settings.contentWidth, settings.sidebarWidth],
|
() => [settings.fontSize, settings.contentWidth, settings.sidebarWidth],
|
||||||
() => view?.requestMeasure(),
|
() => 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-appearance', () => requestAnimationFrame(() => view?.requestMeasure()))
|
||||||
window.addEventListener('rawmark-plugin-notice', (event) => toast((event as CustomEvent<string>).detail))
|
window.addEventListener('rawmark-plugin-notice', (event) => toast((event as CustomEvent<string>).detail))
|
||||||
// 页面隐藏/关闭前尽力发出待保存的修改(部分浏览器关闭标签页时只触发 pagehide)
|
// 页面隐藏/关闭前尽力发出待保存的修改(部分浏览器关闭标签页时只触发 pagehide)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export { insertImageFiles, pasteImages } from './paste'
|
|||||||
// 沿用其语言数据,markdownKeymap 中的命令据此识别 Markdown(isActiveAt 按语言数据判断)
|
// 沿用其语言数据,markdownKeymap 中的命令据此识别 Markdown(isActiveAt 按语言数据判断)
|
||||||
export const pluginCompartment = new Compartment()
|
export const pluginCompartment = new Compartment()
|
||||||
export const languageCompartment = new Compartment()
|
export const languageCompartment = new Compartment()
|
||||||
|
export const preferencesCompartment = new Compartment()
|
||||||
export function pluginLanguage() { return new Language(
|
export function pluginLanguage() { return new Language(
|
||||||
markdownLanguage.data,
|
markdownLanguage.data,
|
||||||
(markdownLanguage.parser as MarkdownParser).configure([typoraExtensions, ...[...markdownSyntax.values()].map((entry) => entry.value)]),
|
(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 {
|
export interface EditorStateOptions {
|
||||||
doc: string
|
doc: string
|
||||||
|
spellcheck?: boolean
|
||||||
|
tabSize?: number
|
||||||
resolveImage?: (src: string) => string
|
resolveImage?: (src: string) => string
|
||||||
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
|
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
|
||||||
onChange?: (update: ViewUpdate) => void
|
onChange?: (update: ViewUpdate) => void
|
||||||
@@ -65,7 +68,11 @@ export function createEditorState(opts: EditorStateOptions): EditorState {
|
|||||||
// CodeMirror 只在 Linux/Mac 上把 Ctrl/Cmd+Shift+Z 绑定为重做,Windows 上会退化成撤销;
|
// CodeMirror 只在 Linux/Mac 上把 Ctrl/Cmd+Shift+Z 绑定为重做,Windows 上会退化成撤销;
|
||||||
// 与 Typora、VS Code 保持一致,所有平台都支持
|
// 与 Typora、VS Code 保持一致,所有平台都支持
|
||||||
keymap.of([indentWithTab, { key: 'Mod-Shift-z', run: redo, preventDefault: true }, ...defaultKeymap, ...historyKeymap]),
|
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.resolveImage) extensions.push(resolveImageUrl.of(opts.resolveImage))
|
||||||
if (opts.onChange) {
|
if (opts.onChange) {
|
||||||
|
|||||||
Reference in new issue
Block a user