Add formatting method

This commit is contained in:
2025-06-19 20:23:20 +08:00
parent 25858cb42b
commit 13072a00a1
15 changed files with 596 additions and 106 deletions

View File

@@ -1,9 +1,13 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useEditorStore } from '@/stores/editorStore';
import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '@/views/editor/extensions/codeblock/types';
import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
const { t } = useI18n();
const editorStore = useEditorStore();
// 组件状态
const showLanguageMenu = ref(false);
@@ -12,6 +16,7 @@ const searchInputRef = ref<HTMLInputElement>();
// 语言别名映射
const LANGUAGE_ALIASES: Record<SupportedLanguage, string> = {
auto: 'auto',
text: 'txt',
json: 'JSON',
py: 'python',
@@ -45,6 +50,7 @@ const LANGUAGE_ALIASES: Record<SupportedLanguage, string> = {
// 语言显示名称映射
const LANGUAGE_NAMES: Record<SupportedLanguage, string> = {
auto: 'Auto',
text: 'Plain Text',
json: 'JSON',
py: 'Python',
@@ -76,8 +82,117 @@ const LANGUAGE_NAMES: Record<SupportedLanguage, string> = {
scala: 'Scala'
};
// 当前选中的语言
const currentLanguage = ref<SupportedLanguage>('text');
// 当前活动块的语言信息
const currentBlockLanguage = ref<{ name: SupportedLanguage; auto: boolean }>({
name: 'text',
auto: false
});
// 事件监听器引用
const eventListeners = ref<{
updateListener?: () => void;
selectionUpdateListener?: () => void;
}>({});
// 更新当前块语言信息
const updateCurrentBlockLanguage = () => {
if (!editorStore.editorView) {
currentBlockLanguage.value = { name: 'text', auto: false };
return;
}
try {
const state = editorStore.editorView.state;
const activeBlock = getActiveNoteBlock(state as any);
if (activeBlock) {
const newLanguage = {
name: activeBlock.language.name as SupportedLanguage,
auto: activeBlock.language.auto
};
// 只有当语言信息实际发生变化时才更新
if (currentBlockLanguage.value.name !== newLanguage.name ||
currentBlockLanguage.value.auto !== newLanguage.auto) {
currentBlockLanguage.value = newLanguage;
}
} else {
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto !== false) {
currentBlockLanguage.value = { name: 'text', auto: false };
}
}
} catch (error) {
console.warn('Failed to get active block language:', error);
currentBlockLanguage.value = { name: 'text', auto: false };
}
};
// 清理事件监听器
const cleanupEventListeners = () => {
if (editorStore.editorView?.dom && eventListeners.value.updateListener) {
const dom = editorStore.editorView.dom;
dom.removeEventListener('click', eventListeners.value.updateListener);
dom.removeEventListener('keyup', eventListeners.value.updateListener);
dom.removeEventListener('keydown', eventListeners.value.updateListener);
dom.removeEventListener('focus', eventListeners.value.updateListener);
dom.removeEventListener('mouseup', eventListeners.value.updateListener);
if (eventListeners.value.selectionUpdateListener) {
dom.removeEventListener('selectionchange', eventListeners.value.selectionUpdateListener);
}
}
eventListeners.value = {};
};
// 设置事件监听器
const setupEventListeners = (view: any) => {
cleanupEventListeners();
// 监听编辑器状态更新
const updateListener = () => {
// 使用 requestAnimationFrame 确保在下一帧更新,性能更好
requestAnimationFrame(() => {
updateCurrentBlockLanguage();
});
};
// 监听选择变化
const selectionUpdateListener = () => {
requestAnimationFrame(() => {
updateCurrentBlockLanguage();
});
};
// 保存监听器引用
eventListeners.value = { updateListener, selectionUpdateListener };
// 监听关键事件:光标位置变化、文档变化、焦点变化
view.dom.addEventListener('click', updateListener);
view.dom.addEventListener('keyup', updateListener);
view.dom.addEventListener('keydown', updateListener);
view.dom.addEventListener('focus', updateListener);
view.dom.addEventListener('mouseup', updateListener); // 鼠标选择结束
// 监听编辑器的选择变化事件
if (view.dom.addEventListener) {
view.dom.addEventListener('selectionchange', selectionUpdateListener);
}
// 立即更新一次当前状态
updateCurrentBlockLanguage();
};
// 监听编辑器状态变化
watch(
() => editorStore.editorView,
(newView) => {
if (newView) {
setupEventListeners(newView);
} else {
cleanupEventListeners();
}
},
{ immediate: true }
);
// 过滤后的语言列表
const filteredLanguages = computed(() => {
@@ -98,6 +213,11 @@ const filteredLanguages = computed(() => {
// 切换语言选择器显示状态
const toggleLanguageMenu = () => {
showLanguageMenu.value = !showLanguageMenu.value;
// 如果菜单打开,滚动到当前语言
if (showLanguageMenu.value) {
scrollToCurrentLanguage();
}
};
// 关闭语言选择器
@@ -108,10 +228,42 @@ const closeLanguageMenu = () => {
// 选择语言
const selectLanguage = (languageId: SupportedLanguage) => {
currentLanguage.value = languageId;
if (!editorStore.editorView) {
closeLanguageMenu();
return;
}
try {
const view = editorStore.editorView;
const state = view.state;
const dispatch = view.dispatch;
let targetLanguage: string;
let autoDetect: boolean;
if (languageId === 'auto') {
// 设置为自动检测
targetLanguage = 'text';
autoDetect = true;
} else {
// 设置为指定语言,关闭自动检测
targetLanguage = languageId;
autoDetect = false;
}
// 使用修复后的函数来更改语言
const success = changeCurrentBlockLanguage(state as any, dispatch, targetLanguage, autoDetect);
if (success) {
// 立即更新当前语言状态
updateCurrentBlockLanguage();
}
} catch (error) {
console.warn('Failed to change block language:', error);
}
closeLanguageMenu();
// TODO: 这里后续需要调用实际的语言设置功能
console.log('Selected language:', languageId);
};
// 点击外部关闭
@@ -132,17 +284,56 @@ const handleKeydown = (event: KeyboardEvent) => {
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleKeydown);
// 立即更新一次当前语言状态
updateCurrentBlockLanguage();
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleKeydown);
cleanupEventListeners();
});
// 获取当前语言的显示名称
const getCurrentLanguageName = computed(() => {
return LANGUAGE_NAMES[currentLanguage.value] || currentLanguage.value;
const lang = currentBlockLanguage.value;
if (lang.auto) {
return `${lang.name} (auto)`;
}
return lang.name;
});
// 获取当前显示的语言选项
const getCurrentDisplayLanguage = computed(() => {
const lang = currentBlockLanguage.value;
if (lang.auto) {
return 'auto';
}
return lang.name;
});
// 滚动到当前选择的语言
const scrollToCurrentLanguage = () => {
nextTick(() => {
const currentLang = getCurrentDisplayLanguage.value;
const selectorElement = document.querySelector('.block-language-selector');
if (!selectorElement) return;
const languageList = selectorElement.querySelector('.language-list') as HTMLElement;
const activeOption = selectorElement.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
if (languageList && activeOption) {
// 使用 scrollIntoView 进行平滑滚动
activeOption.scrollIntoView({
behavior: 'auto',
block: 'nearest',
inline: 'nearest'
});
}
});
};
</script>
<template>
@@ -185,7 +376,8 @@ const getCurrentLanguageName = computed(() => {
v-for="language in filteredLanguages"
:key="language"
class="language-option"
:class="{ 'active': currentLanguage === language }"
:class="{ 'active': getCurrentDisplayLanguage === language }"
:data-language="language"
@click="selectLanguage(language)"
>
<span class="language-name">{{ LANGUAGE_NAMES[language] || language }}</span>
@@ -228,7 +420,7 @@ const getCurrentLanguageName = computed(() => {
}
.language-name {
max-width: 60px;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -1,21 +1,22 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, watch } from 'vue';
import { useConfigStore } from '@/stores/configStore';
import { useEditorStore } from '@/stores/editorStore';
import { useErrorHandler } from '@/utils/errorHandler';
import {useI18n} from 'vue-i18n';
import {onMounted, onUnmounted, ref, watch} from 'vue';
import {useConfigStore} from '@/stores/configStore';
import {useEditorStore} from '@/stores/editorStore';
import {useErrorHandler} from '@/utils/errorHandler';
import * as runtime from '@wailsio/runtime';
import { useRouter } from 'vue-router';
import {useRouter} from 'vue-router';
import BlockLanguageSelector from './BlockLanguageSelector.vue';
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
const editorStore = useEditorStore();
const configStore = useConfigStore();
const { safeCall } = useErrorHandler();
const { t } = useI18n();
const {safeCall} = useErrorHandler();
const {t} = useI18n();
const router = useRouter();
// 设置窗口置顶
const setWindowAlwaysOnTop = async (isTop: boolean) => {
await safeCall(async () => {
@@ -37,19 +38,97 @@ const goToSettings = () => {
router.push('/settings');
};
// 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false);
// 更新格式化按钮状态
const updateFormatButtonState = () => {
if (!editorStore.editorView) {
canFormatCurrentBlock.value = false;
return;
}
try {
const state = editorStore.editorView.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);
} catch (error) {
canFormatCurrentBlock.value = false;
}
};
// 编辑器事件监听器引用,用于清理
let editorEventListeners: (() => 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}
);
// 定期更新格式化按钮状态(作为备用机制)
let formatButtonUpdateTimer: number | null = null;
const isLoaded = ref(false);
onMounted(() => {
isLoaded.value = true;
// 降低定时器频率,主要作为备用机制
formatButtonUpdateTimer = setInterval(updateFormatButtonState, 2000) as any;
});
onUnmounted(() => {
// 清理定时器
if (formatButtonUpdateTimer) {
clearInterval(formatButtonUpdateTimer);
formatButtonUpdateTimer = null;
}
// 清理编辑器事件监听器
editorEventListeners.forEach(cleanup => cleanup());
editorEventListeners = [];
});
// 监听置顶设置变化
watch(
() => configStore.config.general.alwaysOnTop,
async (newValue) => {
if (!isLoaded.value) return;
await runtime.Window.SetAlwaysOnTop(newValue);
}
() => configStore.config.general.alwaysOnTop,
async (newValue) => {
if (!isLoaded.value) return;
await runtime.Window.SetAlwaysOnTop(newValue);
}
);
// 在组件加载完成后应用置顶设置
@@ -63,38 +142,60 @@ watch(isLoaded, async (newLoaded) => {
<template>
<div class="toolbar-container">
<div class="statistics">
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span class="stat-value">{{
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span
class="stat-value">{{
editorStore.documentStats.lines
}}</span></span>
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span class="stat-value">{{
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span
class="stat-value">{{
editorStore.documentStats.characters
}}</span></span>
<span class="stat-item" :title="t('toolbar.editor.selected')" v-if="editorStore.documentStats.selectedCharacters > 0">
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{ editorStore.documentStats.selectedCharacters }}</span>
<span class="stat-item" :title="t('toolbar.editor.selected')"
v-if="editorStore.documentStats.selectedCharacters > 0">
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{
editorStore.documentStats.selectedCharacters
}}</span>
</span>
</div>
<div class="actions">
<span class="font-size" :title="t('toolbar.fontSizeTooltip')" @click="() => configStore.resetFontSize()">
{{ configStore.config.editing.fontSize }}px
</span>
<!-- 块语言选择器 -->
<BlockLanguageSelector />
<!-- 窗口置顶图标按钮 -->
<div
class="pin-button"
:class="{ 'active': configStore.config.general.alwaysOnTop }"
:title="t('toolbar.alwaysOnTop')"
@click="toggleAlwaysOnTop"
<!-- 块语言选择器 -->
<BlockLanguageSelector/>
<!-- 格式化提示按钮 - 只在支持的语言块中显示不可点击 -->
<div
v-if="canFormatCurrentBlock"
class="format-button"
:title="t('toolbar.formatHint')"
>
<svg class="pin-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M557.44 104.96l361.6 361.6-60.16 64-26.88-33.92-181.12 181.12L617.6 832l-60.16 60.16-181.12-184.32-211.2 211.2-60.16-60.16 211.2-211.2-181.12-181.12 60.16-60.16 151.04-30.08 181.12-181.12-30.72-30.08 64-60.16zM587.52 256L387.84 455.04l-120.32 23.68 277.76 277.76 23.68-120.32L768 436.48z" />
<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">
<path
d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
<path d="M5 3v4"/>
<path d="M19 17v4"/>
<path d="M3 5h4"/>
<path d="M17 19h4"/>
</svg>
</div>
<!-- 窗口置顶图标按钮 -->
<div
class="pin-button"
:class="{ 'active': configStore.config.general.alwaysOnTop }"
:title="t('toolbar.alwaysOnTop')"
@click="toggleAlwaysOnTop"
>
<svg class="pin-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path
d="M557.44 104.96l361.6 361.6-60.16 64-26.88-33.92-181.12 181.12L617.6 832l-60.16 60.16-181.12-184.32-211.2 211.2-60.16-60.16 211.2-211.2-181.12-181.12 60.16-60.16 151.04-30.08 181.12-181.12-30.72-30.08 64-60.16zM587.52 256L387.84 455.04l-120.32 23.68 277.76 277.76 23.68-120.32L768 436.48z"/>
</svg>
</div>
<button class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -146,7 +247,6 @@ watch(isLoaded, async (newLoaded) => {
}
/* 窗口置顶图标按钮样式 */
.pin-button {
cursor: pointer;
@@ -158,20 +258,20 @@ watch(isLoaded, async (newLoaded) => {
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
&.active {
background-color: rgba(181, 206, 168, 0.2);
.pin-icon {
fill: #b5cea8;
}
}
.pin-icon {
width: 14px;
height: 14px;
@@ -179,9 +279,45 @@ watch(isLoaded, async (newLoaded) => {
transition: fill 0.2s ease;
}
}
.format-button {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 2px;
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);
//}
svg {
width: 14px;
height: 14px;
stroke: var(--text-muted);
transition: stroke 0.2s ease;
}
&:hover svg {
stroke: var(--text-secondary);
}
}
.settings-btn {
background: none;
border: none;