🐛 Fixed block formatting issue
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
Reference in New Issue
Block a user