diff --git a/frontend/src/components/toolbar/DocumentSelector.vue b/frontend/src/components/toolbar/DocumentSelector.vue index 4c901da..6add2ff 100644 --- a/frontend/src/components/toolbar/DocumentSelector.vue +++ b/frontend/src/components/toolbar/DocumentSelector.vue @@ -429,7 +429,7 @@ onUnmounted(() => { margin-left: 2px; transition: transform 0.2s ease; - &:not(.open) { + &.open { transform: rotate(180deg); } } diff --git a/frontend/src/components/toolbar/Toolbar.vue b/frontend/src/components/toolbar/Toolbar.vue index 1f99fa3..fa71554 100644 --- a/frontend/src/components/toolbar/Toolbar.vue +++ b/frontend/src/components/toolbar/Toolbar.vue @@ -11,6 +11,7 @@ import BlockLanguageSelector from './BlockLanguageSelector.vue'; import DocumentSelector from './DocumentSelector.vue'; import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages'; +import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode'; const editorStore = useEditorStore(); const configStore = useConfigStore(); @@ -19,124 +20,140 @@ const documentStore = useDocumentStore(); const {t} = useI18n(); const router = useRouter(); +// 当前块是否支持格式化的响应式状态 +const canFormatCurrentBlock = ref(false); -// 设置窗口置顶 +// 窗口置顶状态管理 const setWindowAlwaysOnTop = async (isTop: boolean) => { await runtime.Window.SetAlwaysOnTop(isTop); }; -// 切换窗口置顶 const toggleAlwaysOnTop = async () => { await configStore.toggleAlwaysOnTop(); - // 使用Window.SetAlwaysOnTop方法设置窗口置顶状态 await runtime.Window.SetAlwaysOnTop(configStore.config.general.alwaysOnTop); }; // 跳转到设置页面 const goToSettings = () => { - const currentDocId = documentStore.currentDocumentId; router.push({ path: '/settings', - query: { documentId: currentDocId || undefined } + query: { documentId: documentStore.currentDocumentId || undefined } }); }; +// 执行格式化 +const formatCurrentBlock = () => { + if (!canFormatCurrentBlock.value || !editorStore.editorView) return; + formatBlockContent(editorStore.editorView); +}; -// 当前块是否支持格式化的响应式状态 -const canFormatCurrentBlock = ref(false); - -// 更新格式化按钮状态 +// 格式化按钮状态更新 const updateFormatButtonState = () => { - if (!editorStore.editorView) { + // 安全检查 + const view = editorStore.editorView; + if (!view) { canFormatCurrentBlock.value = false; return; } try { - const state = editorStore.editorView.state; + // 获取活动块和语言信息 + const state = view.state; const activeBlock = getActiveNoteBlock(state as any); - if (!activeBlock) { - canFormatCurrentBlock.value = false; - return; - } - - const language = getLanguage(activeBlock.language.name as any); - canFormatCurrentBlock.value = !!(language && language.prettier); + + // 检查块和语言格式化支持 + canFormatCurrentBlock.value = !!( + activeBlock && + getLanguage(activeBlock.language.name as any)?.prettier + ); } catch (error) { + console.warn('Error checking format capability:', error); canFormatCurrentBlock.value = false; } }; -// 编辑器事件监听器引用,用于清理 -let editorEventListeners: (() => void)[] = []; +// 创建带300ms防抖的更新函数 +const debouncedUpdateFormatButton = (() => { + let timeout: number | null = null; + + return () => { + if (timeout) clearTimeout(timeout); + timeout = window.setTimeout(() => { + updateFormatButtonState(); + timeout = null; + }, 300); + }; +})(); + +// 编辑器事件管理 +const setupEditorListeners = (view: any) => { + if (!view?.dom) return []; + + const events = [ + { type: 'click', handler: updateFormatButtonState }, + { type: 'keyup', handler: debouncedUpdateFormatButton }, + { type: 'focus', handler: updateFormatButtonState } + ]; + + // 注册所有事件 + events.forEach(event => view.dom.addEventListener(event.type, event.handler)); + + // 返回清理函数数组 + return events.map(event => + () => view.dom.removeEventListener(event.type, event.handler) + ); +}; + +// 监听编辑器视图变化 +let cleanupListeners: (() => void)[] = []; -// 监听编辑器初始化 watch( - () => editorStore.editorView, - (newView, oldView) => { - // 清理旧的监听器 - editorEventListeners.forEach(cleanup => cleanup()); - editorEventListeners = []; - - if (newView) { - updateFormatButtonState(); - - // 添加点击监听器(用于检测光标位置变化) - const clickListener = () => { - setTimeout(updateFormatButtonState, 0); - }; - newView.dom.addEventListener('click', clickListener); - editorEventListeners.push(() => newView.dom.removeEventListener('click', clickListener)); - - // 添加键盘监听器(用于检测内容和光标变化) - const keyupListener = () => { - setTimeout(updateFormatButtonState, 0); - }; - newView.dom.addEventListener('keyup', keyupListener); - editorEventListeners.push(() => newView.dom.removeEventListener('keyup', keyupListener)); - - } else { - canFormatCurrentBlock.value = false; - } - }, - {immediate: true} + () => editorStore.editorView, + (newView) => { + // 清理旧监听器 + cleanupListeners.forEach(cleanup => cleanup()); + cleanupListeners = []; + + if (newView) { + // 初始更新状态 + updateFormatButtonState(); + // 设置新监听器 + cleanupListeners = setupEditorListeners(newView); + } else { + canFormatCurrentBlock.value = false; + } + }, + { immediate: true } ); -// 定期更新格式化按钮状态 -let formatButtonUpdateTimer: number | null = null; - +// 组件生命周期 const isLoaded = ref(false); onMounted(() => { isLoaded.value = true; - // 降低定时器频率,主要作为备用机制 - formatButtonUpdateTimer = setInterval(updateFormatButtonState, 2000) as any; + // 首次更新格式化状态 + updateFormatButtonState(); }); onUnmounted(() => { - // 清理定时器 - if (formatButtonUpdateTimer) { - clearInterval(formatButtonUpdateTimer); - formatButtonUpdateTimer = null; - } - - // 清理编辑器事件监听器 - editorEventListeners.forEach(cleanup => cleanup()); - editorEventListeners = []; + // 清理所有事件监听器 + cleanupListeners.forEach(cleanup => cleanup()); + cleanupListeners = []; }); // 监听置顶设置变化 watch( - () => configStore.config.general.alwaysOnTop, - async (newValue) => { - if (!isLoaded.value) return; + () => configStore.config.general.alwaysOnTop, + async (newValue) => { + if (isLoaded.value) { await runtime.Window.SetAlwaysOnTop(newValue); } + } ); -// 在组件加载完成后应用置顶设置 -watch(isLoaded, async (newLoaded) => { - if (newLoaded && configStore.config.general.alwaysOnTop) { +// 组件加载后应用置顶设置 +watch(isLoaded, async (loaded) => { + if (loaded && configStore.config.general.alwaysOnTop) { await setWindowAlwaysOnTop(true); } }); @@ -171,11 +188,12 @@ watch(isLoaded, async (newLoaded) => { - +
@@ -336,6 +354,7 @@ watch(isLoaded, async (newLoaded) => { .format-button { + cursor: pointer; display: flex; align-items: center; justify-content: center; @@ -345,20 +364,10 @@ watch(isLoaded, async (newLoaded) => { border-radius: 3px; transition: all 0.2s ease; - //&:not(.disabled) { - // cursor: pointer; - // - // &:hover { - // background-color: var(--border-color); - // opacity: 0.8; - // } - //} - // - //&.disabled { - // cursor: not-allowed; - // opacity: 0.5; - // background-color: rgba(128, 128, 128, 0.1); - //} + &:hover { + background-color: var(--border-color); + opacity: 0.8; + } svg { width: 14px; diff --git a/frontend/src/views/editor/extensions/codeblock/formatCode.ts b/frontend/src/views/editor/extensions/codeblock/formatCode.ts index caee8fd..a923471 100644 --- a/frontend/src/views/editor/extensions/codeblock/formatCode.ts +++ b/frontend/src/views/editor/extensions/codeblock/formatCode.ts @@ -6,65 +6,63 @@ import { getLanguage } from "./lang-parser/languages" import { SupportedLanguage } from "./types" export const formatBlockContent = (view) => { - const state = view.state - if (state.readOnly) + if (!view || view.state.readOnly) return false - const block = getActiveNoteBlock(state) + + // 获取初始信息,但不缓存state对象 + const initialState = view.state + const block = getActiveNoteBlock(initialState) if (!block) { return false } - const language = getLanguage(block.language.name as SupportedLanguage) + const blockFrom = block.content.from + const blockTo = block.content.to + const blockLanguageName = block.language.name as SupportedLanguage + + const language = getLanguage(blockLanguageName) if (!language || !language.prettier) { return false } - // get current cursor position - const cursorPos = state.selection.asSingle().ranges[0].head - // get block content - const content = state.sliceDoc(block.content.from, block.content.to) + // 获取初始需要的信息 + const cursorPos = initialState.selection.asSingle().ranges[0].head + const content = initialState.sliceDoc(blockFrom, blockTo) + const tabSize = initialState.tabSize - let useFormat = false - if (cursorPos == block.content.from || cursorPos == block.content.to) { - useFormat = true - } + // 检查光标是否在块的开始或结束 + const cursorAtEdge = cursorPos == blockFrom || cursorPos == blockTo - // 执行异步格式化,但在回调中获取最新状态 + // 执行异步格式化 const performFormat = async () => { let formattedContent try { - if (useFormat) { - formattedContent = { - formatted: await prettier.format(content, { - parser: language.prettier!.parser, - plugins: language.prettier!.plugins, - tabWidth: state.tabSize, - }), - } - formattedContent.cursorOffset = cursorPos == block.content.from ? 0 : formattedContent.formatted.length - } else { - // formatWithCursor 有性能问题,改用简单格式化 + 光标位置计算 - const formatted = await prettier.format(content, { - parser: language.prettier!.parser, - plugins: language.prettier!.plugins, - tabWidth: state.tabSize, - }) - formattedContent = { - formatted: formatted, - cursorOffset: Math.min(cursorPos - block.content.from, formatted.length) - } + // 格式化代码 + const formatted = await prettier.format(content, { + parser: language.prettier!.parser, + plugins: language.prettier!.plugins, + tabWidth: tabSize, + }) + + // 计算新光标位置 + const cursorOffset = cursorAtEdge + ? (cursorPos == blockFrom ? 0 : formatted.length) + : Math.min(cursorPos - blockFrom, formatted.length) + + formattedContent = { + formatted, + cursorOffset } } catch (e) { - const hyphens = "----------------------------------------------------------------------------" - const errorMessage = (e as Error).message; - console.log(`Error when trying to format block:\n${hyphens}\n${errorMessage}\n${hyphens}`) return false } try { - // 重新获取当前状态和块信息,确保状态一致 + // 格式化完成后再次获取最新状态 const currentState = view.state + + // 重新获取当前块的位置 const currentBlock = getActiveNoteBlock(currentState) if (!currentBlock) { @@ -72,17 +70,22 @@ export const formatBlockContent = (view) => { return false } - view.dispatch(currentState.update({ + // 使用当前块的实际位置 + const currentBlockFrom = currentBlock.content.from + const currentBlockTo = currentBlock.content.to + + // 基于最新状态创建更新 + view.dispatch({ changes: { - from: currentBlock.content.from, - to: currentBlock.content.to, + from: currentBlockFrom, + to: currentBlockTo, insert: formattedContent.formatted, }, - selection: EditorSelection.cursor(currentBlock.content.from + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)), - }, { - userEvent: "input", + selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)), scrollIntoView: true, - })) + userEvent: "input" + }) + return true; } catch (error) { console.error('Failed to apply formatting changes:', error);