🐛 Fixed block formatting issue

This commit is contained in:
2025-07-01 23:55:15 +08:00
parent 1ccee779ae
commit 25e1a98932
3 changed files with 141 additions and 129 deletions

View File

@@ -429,7 +429,7 @@ onUnmounted(() => {
margin-left: 2px; margin-left: 2px;
transition: transform 0.2s ease; transition: transform 0.2s ease;
&:not(.open) { &.open {
transform: rotate(180deg); transform: rotate(180deg);
} }
} }

View File

@@ -11,6 +11,7 @@ import BlockLanguageSelector from './BlockLanguageSelector.vue';
import DocumentSelector from './DocumentSelector.vue'; import DocumentSelector from './DocumentSelector.vue';
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages'; import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const configStore = useConfigStore(); const configStore = useConfigStore();
@@ -19,124 +20,140 @@ const documentStore = useDocumentStore();
const {t} = useI18n(); const {t} = useI18n();
const router = useRouter(); const router = useRouter();
// 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false);
// 设置窗口置顶 // 窗口置顶状态管理
const setWindowAlwaysOnTop = async (isTop: boolean) => { const setWindowAlwaysOnTop = async (isTop: boolean) => {
await runtime.Window.SetAlwaysOnTop(isTop); await runtime.Window.SetAlwaysOnTop(isTop);
}; };
// 切换窗口置顶
const toggleAlwaysOnTop = async () => { const toggleAlwaysOnTop = async () => {
await configStore.toggleAlwaysOnTop(); await configStore.toggleAlwaysOnTop();
// 使用Window.SetAlwaysOnTop方法设置窗口置顶状态
await runtime.Window.SetAlwaysOnTop(configStore.config.general.alwaysOnTop); await runtime.Window.SetAlwaysOnTop(configStore.config.general.alwaysOnTop);
}; };
// 跳转到设置页面 // 跳转到设置页面
const goToSettings = () => { const goToSettings = () => {
const currentDocId = documentStore.currentDocumentId;
router.push({ router.push({
path: '/settings', 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 = () => { const updateFormatButtonState = () => {
if (!editorStore.editorView) { // 安全检查
const view = editorStore.editorView;
if (!view) {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
return; return;
} }
try { try {
const state = editorStore.editorView.state; // 获取活动块和语言信息
const state = view.state;
const activeBlock = getActiveNoteBlock(state as any); const activeBlock = getActiveNoteBlock(state as any);
if (!activeBlock) {
canFormatCurrentBlock.value = false; // 检查块和语言格式化支持
return; canFormatCurrentBlock.value = !!(
} activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier
const language = getLanguage(activeBlock.language.name as any); );
canFormatCurrentBlock.value = !!(language && language.prettier);
} catch (error) { } catch (error) {
console.warn('Error checking format capability:', error);
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
} }
}; };
// 编辑器事件监听器引用,用于清理 // 创建带300ms防抖的更新函数
let editorEventListeners: (() => void)[] = []; 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( watch(
() => editorStore.editorView, () => editorStore.editorView,
(newView, oldView) => { (newView) => {
// 清理旧监听器 // 清理旧监听器
editorEventListeners.forEach(cleanup => cleanup()); cleanupListeners.forEach(cleanup => cleanup());
editorEventListeners = []; cleanupListeners = [];
if (newView) { if (newView) {
updateFormatButtonState(); // 初始更新状态
updateFormatButtonState();
// 添加点击监听器(用于检测光标位置变化) // 设置新监听器
const clickListener = () => { cleanupListeners = setupEditorListeners(newView);
setTimeout(updateFormatButtonState, 0); } else {
}; canFormatCurrentBlock.value = false;
newView.dom.addEventListener('click', clickListener); }
editorEventListeners.push(() => newView.dom.removeEventListener('click', clickListener)); },
{ immediate: true }
// 添加键盘监听器(用于检测内容和光标变化)
const keyupListener = () => {
setTimeout(updateFormatButtonState, 0);
};
newView.dom.addEventListener('keyup', keyupListener);
editorEventListeners.push(() => newView.dom.removeEventListener('keyup', keyupListener));
} else {
canFormatCurrentBlock.value = false;
}
},
{immediate: true}
); );
// 定期更新格式化按钮状态 // 组件生命周期
let formatButtonUpdateTimer: number | null = null;
const isLoaded = ref(false); const isLoaded = ref(false);
onMounted(() => { onMounted(() => {
isLoaded.value = true; isLoaded.value = true;
// 降低定时器频率,主要作为备用机制 // 首次更新格式化状态
formatButtonUpdateTimer = setInterval(updateFormatButtonState, 2000) as any; updateFormatButtonState();
}); });
onUnmounted(() => { onUnmounted(() => {
// 清理定时 // 清理所有事件监听
if (formatButtonUpdateTimer) { cleanupListeners.forEach(cleanup => cleanup());
clearInterval(formatButtonUpdateTimer); cleanupListeners = [];
formatButtonUpdateTimer = null;
}
// 清理编辑器事件监听器
editorEventListeners.forEach(cleanup => cleanup());
editorEventListeners = [];
}); });
// 监听置顶设置变化 // 监听置顶设置变化
watch( watch(
() => configStore.config.general.alwaysOnTop, () => configStore.config.general.alwaysOnTop,
async (newValue) => { async (newValue) => {
if (!isLoaded.value) return; if (isLoaded.value) {
await runtime.Window.SetAlwaysOnTop(newValue); await runtime.Window.SetAlwaysOnTop(newValue);
} }
}
); );
// 组件加载完成后应用置顶设置 // 组件加载后应用置顶设置
watch(isLoaded, async (newLoaded) => { watch(isLoaded, async (loaded) => {
if (newLoaded && configStore.config.general.alwaysOnTop) { if (loaded && configStore.config.general.alwaysOnTop) {
await setWindowAlwaysOnTop(true); await setWindowAlwaysOnTop(true);
} }
}); });
@@ -171,11 +188,12 @@ watch(isLoaded, async (newLoaded) => {
<!-- 块语言选择器 --> <!-- 块语言选择器 -->
<BlockLanguageSelector/> <BlockLanguageSelector/>
<!-- 格式化提示按钮 - 只在支持的语言块中显示不可点击 --> <!-- 格式化按钮 - 支持点击操作 -->
<div <div
v-if="canFormatCurrentBlock" v-if="canFormatCurrentBlock"
class="format-button" class="format-button"
:title="t('toolbar.formatHint')" :title="t('toolbar.formatHint')"
@click="formatCurrentBlock"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
@@ -336,6 +354,7 @@ watch(isLoaded, async (newLoaded) => {
.format-button { .format-button {
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -345,20 +364,10 @@ watch(isLoaded, async (newLoaded) => {
border-radius: 3px; border-radius: 3px;
transition: all 0.2s ease; transition: all 0.2s ease;
//&:not(.disabled) { &:hover {
// cursor: pointer; background-color: var(--border-color);
// opacity: 0.8;
// &:hover { }
// background-color: var(--border-color);
// opacity: 0.8;
// }
//}
//
//&.disabled {
// cursor: not-allowed;
// opacity: 0.5;
// background-color: rgba(128, 128, 128, 0.1);
//}
svg { svg {
width: 14px; width: 14px;

View File

@@ -6,65 +6,63 @@ import { getLanguage } from "./lang-parser/languages"
import { SupportedLanguage } from "./types" import { SupportedLanguage } from "./types"
export const formatBlockContent = (view) => { export const formatBlockContent = (view) => {
const state = view.state if (!view || view.state.readOnly)
if (state.readOnly)
return false return false
const block = getActiveNoteBlock(state)
// 获取初始信息但不缓存state对象
const initialState = view.state
const block = getActiveNoteBlock(initialState)
if (!block) { if (!block) {
return false 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) { if (!language || !language.prettier) {
return false return false
} }
// get current cursor position // 获取初始需要的信息
const cursorPos = state.selection.asSingle().ranges[0].head const cursorPos = initialState.selection.asSingle().ranges[0].head
// get block content const content = initialState.sliceDoc(blockFrom, blockTo)
const content = state.sliceDoc(block.content.from, block.content.to) const tabSize = initialState.tabSize
let useFormat = false // 检查光标是否在块的开始或结束
if (cursorPos == block.content.from || cursorPos == block.content.to) { const cursorAtEdge = cursorPos == blockFrom || cursorPos == blockTo
useFormat = true
}
// 执行异步格式化,但在回调中获取最新状态 // 执行异步格式化
const performFormat = async () => { const performFormat = async () => {
let formattedContent let formattedContent
try { try {
if (useFormat) { // 格式化代码
formattedContent = { const formatted = await prettier.format(content, {
formatted: await prettier.format(content, { parser: language.prettier!.parser,
parser: language.prettier!.parser, plugins: language.prettier!.plugins,
plugins: language.prettier!.plugins, tabWidth: tabSize,
tabWidth: state.tabSize, })
}),
} // 计算新光标位置
formattedContent.cursorOffset = cursorPos == block.content.from ? 0 : formattedContent.formatted.length const cursorOffset = cursorAtEdge
} else { ? (cursorPos == blockFrom ? 0 : formatted.length)
// formatWithCursor 有性能问题,改用简单格式化 + 光标位置计算 : Math.min(cursorPos - blockFrom, formatted.length)
const formatted = await prettier.format(content, {
parser: language.prettier!.parser, formattedContent = {
plugins: language.prettier!.plugins, formatted,
tabWidth: state.tabSize, cursorOffset
})
formattedContent = {
formatted: formatted,
cursorOffset: Math.min(cursorPos - block.content.from, formatted.length)
}
} }
} catch (e) { } 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 return false
} }
try { try {
// 重新获取当前状态和块信息,确保状态一致 // 格式化完成后再次获取最新状态
const currentState = view.state const currentState = view.state
// 重新获取当前块的位置
const currentBlock = getActiveNoteBlock(currentState) const currentBlock = getActiveNoteBlock(currentState)
if (!currentBlock) { if (!currentBlock) {
@@ -72,17 +70,22 @@ export const formatBlockContent = (view) => {
return false return false
} }
view.dispatch(currentState.update({ // 使用当前块的实际位置
const currentBlockFrom = currentBlock.content.from
const currentBlockTo = currentBlock.content.to
// 基于最新状态创建更新
view.dispatch({
changes: { changes: {
from: currentBlock.content.from, from: currentBlockFrom,
to: currentBlock.content.to, to: currentBlockTo,
insert: formattedContent.formatted, insert: formattedContent.formatted,
}, },
selection: EditorSelection.cursor(currentBlock.content.from + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)), selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
}, {
userEvent: "input",
scrollIntoView: true, scrollIntoView: true,
})) userEvent: "input"
})
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to apply formatting changes:', error); console.error('Failed to apply formatting changes:', error);