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

@@ -49,6 +49,7 @@
"hsl-matcher": "^1.2.4",
"lezer": "^0.13.5",
"pinia": "^3.0.3",
"prettier": "^3.5.3",
"sass": "^1.89.2",
"uuid": "^11.1.0",
"vue": "^3.5.17",
@@ -4288,6 +4289,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",

View File

@@ -53,6 +53,7 @@
"hsl-matcher": "^1.2.4",
"lezer": "^0.13.5",
"pinia": "^3.0.3",
"prettier": "^3.5.3",
"sass": "^1.89.2",
"uuid": "^11.1.0",
"vue": "^3.5.17",

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,14 +142,19 @@ 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">
@@ -79,20 +163,37 @@ watch(isLoaded, async (newLoaded) => {
</span>
<!-- 块语言选择器 -->
<BlockLanguageSelector />
<BlockLanguageSelector/>
<!-- 窗口置顶图标按钮 -->
<!-- 格式化提示按钮 - 只在支持的语言块中显示不可点击 -->
<div
class="pin-button"
:class="{ 'active': configStore.config.general.alwaysOnTop }"
:title="t('toolbar.alwaysOnTop')"
@click="toggleAlwaysOnTop"
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">
@@ -146,7 +247,6 @@ watch(isLoaded, async (newLoaded) => {
}
/* 窗口置顶图标按钮样式 */
.pin-button {
cursor: pointer;
@@ -181,6 +281,42 @@ watch(isLoaded, async (newLoaded) => {
}
.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;

View File

@@ -23,7 +23,9 @@ export default {
alwaysOnTop: 'Always on Top',
blockLanguage: 'Block Language',
searchLanguage: 'Search language...',
noLanguageFound: 'No language found'
noLanguageFound: 'No language found',
autoDetected: 'Auto-detected',
formatHint: 'Current block supports formatting, use Ctrl+Shift+F shortcut for formatting',
},
config: {
loadSuccess: 'Configuration loaded successfully',

View File

@@ -23,7 +23,9 @@ export default {
alwaysOnTop: '窗口置顶',
blockLanguage: '块语言',
searchLanguage: '搜索语言...',
noLanguageFound: '未找到匹配的语言'
noLanguageFound: '未找到匹配的语言',
autoDetected: '自动检测',
formatHint: '当前区块支持格式化,使用 Ctrl+Shift+F 进行格式化',
},
config: {
loadSuccess: '配置加载成功',

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { computed, watch } from 'vue';
import { computed } from 'vue';
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
import { useConfigStore } from './configStore';

View File

@@ -5,7 +5,8 @@
import { EditorSelection } from "@codemirror/state";
import { Command } from "@codemirror/view";
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./state";
import { Block, EditorOptions } from "./types";
import { Block, EditorOptions, DELIMITER_REGEX } from "./types";
import { formatBlockContent } from "./formatCode";
/**
* 获取块分隔符
@@ -143,17 +144,24 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) {
if (state.readOnly) return false;
const delimRegex = /^\n∞∞∞[a-z]+?(-a)?\n/g;
if (state.doc.sliceString(block.delimiter.from, block.delimiter.to).match(delimRegex)) {
dispatch(state.update({
const currentDelimiter = state.doc.sliceString(block.delimiter.from, block.delimiter.to);
// 重置正则表达式的 lastIndex
DELIMITER_REGEX.lastIndex = 0;
if (currentDelimiter.match(DELIMITER_REGEX)) {
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
dispatch({
changes: {
from: block.delimiter.from,
to: block.delimiter.to,
insert: `\n∞∞∞${language}${auto ? '-a' : ''}\n`,
insert: newDelimiter,
},
}));
});
return true;
} else {
throw new Error("Invalid delimiter: " + state.doc.sliceString(block.delimiter.from, block.delimiter.to));
return false;
}
}
@@ -162,13 +170,17 @@ export function changeLanguageTo(state: any, dispatch: any, block: Block, langua
*/
export function changeCurrentBlockLanguage(state: any, dispatch: any, language: string | null, auto: boolean) {
const block = getActiveNoteBlock(state);
if (!block) return;
if (!block) {
console.warn("No active block found");
return false;
}
// 如果 language 为 null我们只想更改自动检测标志
if (language === null) {
language = block.language.name;
}
changeLanguageTo(state, dispatch, block, language, auto);
return changeLanguageTo(state, dispatch, block, language, auto);
}
// 选择和移动辅助函数
@@ -353,3 +365,10 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
return true;
}
/**
* 格式化当前块
*/
export const formatCurrentBlock: Command = (view) => {
return formatBlockContent(view);
}

View File

@@ -0,0 +1,96 @@
import { EditorSelection } from "@codemirror/state"
import * as prettier from "prettier/standalone"
import { getActiveNoteBlock } from "./state"
import { getLanguage } from "./lang-parser/languages"
import { SupportedLanguage } from "./types"
export const formatBlockContent = (view) => {
const state = view.state
if (state.readOnly)
return false
const block = getActiveNoteBlock(state)
if (!block) {
return false
}
const language = getLanguage(block.language.name as SupportedLanguage)
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)
let useFormat = false
if (cursorPos == block.content.from || cursorPos == block.content.to) {
useFormat = true
}
// 执行异步格式化,但在回调中获取最新状态
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)
}
}
} 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) {
console.warn('Block not found after formatting')
return false
}
view.dispatch(currentState.update({
changes: {
from: currentBlock.content.from,
to: currentBlock.content.to,
insert: formattedContent.formatted,
},
selection: EditorSelection.cursor(currentBlock.content.from + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
}, {
userEvent: "input",
scrollIntoView: true,
}))
return true;
} catch (error) {
console.error('Failed to apply formatting changes:', error);
return false;
}
}
// 执行异步格式化
performFormat()
return true // 立即返回 true表示命令已开始执行
}

View File

@@ -214,6 +214,13 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
run: transposeChars,
preventDefault: true
},
// 代码格式化命令
{
key: 'Mod-Shift-f', // 格式化代码
run: commands.formatCurrentBlock,
preventDefault: true
},
])
];
@@ -249,6 +256,9 @@ export {
// 命令
export * from './commands';
// 格式化功能
export { formatBlockContent } from './formatCode';
// 选择功能
export {
selectAll,

View File

@@ -13,9 +13,10 @@ BlockDelimiter {
}
BlockLanguage {
"text" | "math" | "json" | "python" | "html" | "sql" | "markdown" |
"java" | "php" | "css" | "xml" | "cpp" | "rust" | "ruby" | "shell" |
"yaml" | "go" | "javascript" | "typescript"
"text" | "json" | "py" | "html" | "sql" | "md" | "java" | "php" |
"css" | "xml" | "cpp" | "rs" | "cs" | "rb" | "sh" | "yaml" | "toml" |
"go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" |
"ps1" | "dart" | "scala"
}
@tokens {

View File

@@ -29,9 +29,17 @@ import { groovy } from "@codemirror/legacy-modes/mode/groovy";
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
import { scala } from "@codemirror/legacy-modes/mode/clike";
import { toml } from "@codemirror/legacy-modes/mode/toml";
import { elixir } from "codemirror-lang-elixir";
import { SupportedLanguage } from '../types';
import typescriptPlugin from "prettier/plugins/typescript"
import babelPrettierPlugin from "prettier/plugins/babel"
import htmlPrettierPlugin from "prettier/plugins/html"
import cssPrettierPlugin from "prettier/plugins/postcss"
import markdownPrettierPlugin from "prettier/plugins/markdown"
import yamlPrettierPlugin from "prettier/plugins/yaml"
import * as prettierPluginEstree from "prettier/plugins/estree";
/**
* 语言信息类
*/
@@ -39,7 +47,11 @@ export class LanguageInfo {
constructor(
public token: SupportedLanguage,
public name: string,
public parser: any
public parser: any,
public prettier?: {
parser: string;
plugins: any[];
}
) {}
}
@@ -48,28 +60,49 @@ export class LanguageInfo {
*/
export const LANGUAGES: LanguageInfo[] = [
new LanguageInfo("text", "Plain Text", null),
new LanguageInfo("json", "JSON", jsonLanguage.parser),
new LanguageInfo("json", "JSON", jsonLanguage.parser, {
parser: "json",
plugins: [babelPrettierPlugin, prettierPluginEstree]
}),
new LanguageInfo("py", "Python", pythonLanguage.parser),
new LanguageInfo("html", "HTML", htmlLanguage.parser),
new LanguageInfo("html", "HTML", htmlLanguage.parser, {
parser: "html",
plugins: [htmlPrettierPlugin]
}),
new LanguageInfo("sql", "SQL", StandardSQL.language.parser),
new LanguageInfo("md", "Markdown", markdownLanguage.parser),
new LanguageInfo("md", "Markdown", markdownLanguage.parser, {
parser: "markdown",
plugins: [markdownPrettierPlugin]
}),
new LanguageInfo("java", "Java", javaLanguage.parser),
new LanguageInfo("php", "PHP", phpLanguage.configure({top:"Program"}).parser),
new LanguageInfo("css", "CSS", cssLanguage.parser),
new LanguageInfo("css", "CSS", cssLanguage.parser, {
parser: "css",
plugins: [cssPrettierPlugin]
}),
new LanguageInfo("xml", "XML", xmlLanguage.parser),
new LanguageInfo("cpp", "C++", cppLanguage.parser),
new LanguageInfo("rs", "Rust", rustLanguage.parser),
new LanguageInfo("cs", "C#", StreamLanguage.define(csharp).parser),
new LanguageInfo("rb", "Ruby", StreamLanguage.define(ruby).parser),
new LanguageInfo("sh", "Shell", StreamLanguage.define(shell).parser),
new LanguageInfo("yaml", "YAML", yamlLanguage.parser),
new LanguageInfo("yaml", "YAML", yamlLanguage.parser, {
parser: "yaml",
plugins: [yamlPrettierPlugin]
}),
new LanguageInfo("toml", "TOML", StreamLanguage.define(toml).parser),
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser),
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
new LanguageInfo("ex", "Elixir", null), // 暂无解析器
new LanguageInfo("ex", "Elixir", elixir().language.parser),
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser),
new LanguageInfo("ts", "TypeScript", typescriptLanguage.parser),
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser, {
parser: "babel",
plugins: [babelPrettierPlugin, prettierPluginEstree]
}),
new LanguageInfo("ts", "TypeScript", typescriptLanguage.parser, {
parser: "typescript",
plugins: [typescriptPlugin, prettierPluginEstree]
}),
new LanguageInfo("swift", "Swift", StreamLanguage.define(swift).parser),
new LanguageInfo("kt", "Kotlin", StreamLanguage.define(kotlin).parser),
new LanguageInfo("groovy", "Groovy", StreamLanguage.define(groovy).parser),

View File

@@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr"
import {blockContent} from "./external-tokens.js"
export const parser = LRParser.deserialize({
version: 14,
states: "!jQQOQOOOVOQO'#C`O!dOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO!iOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO!qOSO1G.fOOOP7+$Q7+$Q",
stateData: "!v~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTO~OPVO~OUYOmXO~OmZO~O",
states: "!jQQOQOOOVOQO'#C`O#SOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#XOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO#aOSO1G.fOOOP7+$Q7+$Q",
stateData: "#f~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTO~OPVO~OUYOwXO~OwZO~O",
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
maxTerm: 29,
maxTerm: 39,
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: ",k~R]YZz}!O!e#V#W!p#Z#[#a#[#]#l#^#_$T#a#b%x#d#e'X#f#g([#g#h)R#h#i*O#l#m+q#m#n,SR!PPmQ%&x%&y!SP!VP%&x%&y!YP!]P%&x%&y!`P!eOXP~!hP#T#U!k~!pOU~~!sQ#d#e!y#g#h#U~!|P#d#e#P~#UOe~~#XP#g#h#[~#aOc~~#dP#c#d#g~#lOj~~#oP#h#i#r~#uP#a#b#x~#{P#`#a$O~$TO^~~$WQ#T#U$^#g#h%g~$aP#j#k$d~$gP#T#U$j~$oPa~#g#h$r~$uP#V#W$x~${P#f#g%O~%RP#]#^%U~%XP#d#e%[~%_P#h#i%b~%gOk~~%jP#c#d%m~%pP#b#c%s~%xO[~~%{P#T#U&O~&RQ#f#g&X#h#i&|~&[P#_#`&_~&bP#W#X&e~&hP#c#d&k~&nP#k#l&q~&tP#b#c&w~&|O`~~'PP#[#]'S~'XOZ~~'[Q#[#]'b#m#n'm~'eP#d#e'h~'mOb~~'pP#h#i's~'vP#[#]'y~'|P#c#d(P~(SP#b#c(V~([O]~~(_P#i#j(b~(eQ#U#V(k#g#h(v~(nP#m#n(q~(vOg~~(yP#h#i(|~)ROf~~)UQ#[#])[#e#f)s~)_P#X#Y)b~)eP#`#a)h~)kP#`#a)n~)sOh~~)vP#`#a)y~*OO_~~*RQ#X#Y*X#m#n*j~*[P#l#m*_~*bP#h#i*e~*jOY~~*mP#d#e*p~*sP#X#Y*v~*yP#g#h*|~+PP#V#W+S~+VP#f#g+Y~+]P#]#^+`~+cP#d#e+f~+iP#h#i+l~+qOl~~+tP#a#b+w~+zP#`#a+}~,SOd~~,VP#T#U,Y~,]P#a#b,`~,cP#`#a,f~,kOi~",
tokenData: ",s~R`YZ!T}!O!n#V#W!y#W#X#z#X#Y$c#Z#[$|#[#]%y#^#_&b#_#`'a#a#b'l#d#e'w#f#g(p#g#h)T#h#i*t#l#m+y#m#n,[R!YPwQ%&x%&y!]P!`P%&x%&y!cP!fP%&x%&y!iP!nOXP~!qP#T#U!t~!yOU~~!|R#`#a#V#d#e#b#g#h#m~#YP#^#_#]~#bOl~~#eP#d#e#h~#mOd~~#rPf~#g#h#u~#zOb~~#}P#T#U$Q~$TP#f#g$W~$ZP#h#i$^~$cOu~~$fQ#f#g$l#l#m$w~$oP#`#a$r~$wOn~~$|Om~~%PQ#c#d%V#f#g%[~%[Ok~~%_P#c#d%b~%eP#c#d%h~%kP#j#k%n~%qP#m#n%t~%yOs~~%|P#h#i&P~&SP#a#b&V~&YP#`#a&]~&bO]~~&eQ#T#U&k#g#h&|~&nP#j#k&q~&tP#T#U&w~&|O`~~'RPo~#c#d'U~'XP#b#c'[~'aOZ~~'dP#h#i'g~'lOr~~'oP#W#X'r~'wO_~~'zR#[#](T#g#h(`#m#n(k~(WP#d#e(Z~(`Oa~~(cP!R!S(f~(kOt~~(pO[~~(sQ#U#V(y#g#h)O~)OOg~~)TOe~~)WS#V#W)d#[#]){#e#f*Q#k#l*]~)gP#T#U)j~)mP#`#a)p~)sP#T#U)v~){Ov~~*QOh~~*TP#`#a*W~*]O^~~*`P#]#^*c~*fP#Y#Z*i~*lP#h#i*o~*tOq~~*wR#X#Y+Q#c#d+c#g#h+t~+TP#l#m+W~+ZP#h#i+^~+cOY~~+fP#a#b+i~+lP#`#a+o~+tOj~~+yOp~~+|P#a#b,P~,SP#`#a,V~,[Oc~~,_P#T#U,b~,eP#a#b,h~,kP#`#a,n~,sOi~",
tokenizers: [blockContent, 0, 1],
topRules: {"Document":[0,2]},
tokenPrec: 0

View File

@@ -4,10 +4,12 @@
import { EditorState, Annotation } from '@codemirror/state';
import { EditorView, ViewPlugin } from '@codemirror/view';
import { blockState } from '../state';
import { redoDepth } from '@codemirror/commands';
import { blockState, getActiveNoteBlock } from '../state';
import { levenshteinDistance } from './levenshtein';
import { LANGUAGES } from '../lang-parser/languages';
import { SupportedLanguage, Block } from '../types';
import { changeLanguageTo } from '../commands';
// ===== 类型定义 =====
@@ -100,27 +102,6 @@ function cancelIdleCallbackCompat(id: number): void {
*/
const languageChangeAnnotation = Annotation.define<boolean>();
/**
* 更新代码块语言
*/
function updateBlockLanguage(
state: EditorState,
dispatch: (transaction: any) => void,
block: Block,
newLanguage: SupportedLanguage
): void {
const newDelimiter = `\n∞∞∞${newLanguage}-a\n`;
const transaction = state.update({
changes: {
from: block.delimiter.from,
to: block.delimiter.to,
insert: newDelimiter,
},
annotations: [languageChangeAnnotation.of(true)]
});
dispatch(transaction);
}
// ===== Web Worker 管理器 =====
/**
@@ -237,22 +218,18 @@ export function createLanguageDetection(config: LanguageDetectionConfig = {}): V
}
private performDetection(state: EditorState): void {
const selection = state.selection.asSingle().ranges[0];
const blocks = state.field(blockState);
const block = blocks.find(b =>
b.content.from <= selection.from && b.content.to >= selection.from
);
const block = getActiveNoteBlock(state);
if (!block || !block.language.auto) return;
const blocks = state.field(blockState);
const blockIndex = blocks.indexOf(block);
const content = state.doc.sliceString(block.content.from, block.content.to);
// 内容为空时重置为默认语言
if (content === "") {
if (content === "" && redoDepth(state) === 0) {
if (block.language.name !== finalConfig.defaultLanguage) {
updateBlockLanguage(state, this.view.dispatch, block, finalConfig.defaultLanguage);
changeLanguageTo(state, this.view.dispatch, block, finalConfig.defaultLanguage, true);
}
contentCache.delete(blockIndex);
return;
@@ -281,7 +258,10 @@ export function createLanguageDetection(config: LanguageDetectionConfig = {}): V
SUPPORTED_LANGUAGES.has(result.language) &&
LANGUAGE_MAP.has(result.language)) {
updateBlockLanguage(state, this.view.dispatch, block, result.language);
// 只有在用户没有撤销操作时才更改语言
if (redoDepth(state) === 0) {
changeLanguageTo(state, this.view.dispatch, block, result.language, true);
}
}
contentCache.set(blockIndex, content);

View File

@@ -24,6 +24,7 @@ export interface Block {
* 支持的语言类型
*/
export type SupportedLanguage =
| 'auto' // 自动检测
| 'text'
| 'json'
| 'py' // Python
@@ -58,6 +59,7 @@ export type SupportedLanguage =
* 支持的语言列表
*/
export const SUPPORTED_LANGUAGES: SupportedLanguage[] = [
'auto',
'text',
'json',
'py',