♻️ Refactored translation extension

This commit is contained in:
2025-11-23 18:45:49 +08:00
parent 4b0f39d747
commit ad24d3a140
16 changed files with 1101 additions and 1536 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"app:generate": "cd .. && wails3 generate bindings -ts" "app:generate": "cd .. && wails3 generate bindings -ts"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.19.1", "@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0", "@codemirror/commands": "^6.10.0",
"@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
@@ -50,7 +50,7 @@
"@codemirror/lint": "^6.9.2", "@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11", "@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.6", "@codemirror/view": "^6.38.8",
"@cospaia/prettier-plugin-clojure": "^0.0.2", "@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.3", "@lezer/lr": "^1.4.3",
@@ -72,37 +72,37 @@
"linguist-languages": "^9.1.0", "linguist-languages": "^9.1.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"mermaid": "^11.12.1", "mermaid": "^11.12.1",
"npm": "^11.6.2", "npm": "^11.6.3",
"php-parser": "^3.2.5", "php-parser": "^3.2.5",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.94.0", "sass": "^1.94.2",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.2.1",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@types/node": "^24.9.2", "@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.2",
"@wailsio/runtime": "latest", "@wailsio/runtime": "latest",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-vue": "^10.5.1", "eslint-plugin-vue": "^10.6.0",
"globals": "^16.5.0", "globals": "^16.5.0",
"happy-dom": "^20.0.10", "happy-dom": "^20.0.10",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.47.0",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "npm:rolldown-vite@latest", "vite": "npm:rolldown-vite@latest",
"vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12", "vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.8", "vitest": "^4.0.13",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.3" "vue-tsc": "^3.1.4"
}, },
"overrides": { "overrides": {
"vite": "npm:rolldown-vite@latest" "vite": "npm:rolldown-vite@latest"

View File

@@ -1,45 +1,3 @@
/**
* 默认翻译配置
*/
export const DEFAULT_TRANSLATION_CONFIG = {
minSelectionLength: 2,
maxTranslationLength: 5000,
} as const;
/**
* 翻译相关的错误消息
*/
export const TRANSLATION_ERRORS = {
NO_TEXT: 'no text to translate',
TRANSLATION_FAILED: 'translation failed',
} as const;
/**
* 翻译结果接口
*/
export interface TranslationResult {
translatedText: string;
error?: string;
}
/**
* 语言信息接口
*/
export interface LanguageInfo {
Code: string; // 语言代码
Name: string; // 语言名称
}
/**
* 翻译器扩展配置
*/
export interface TranslatorConfig {
/** 最小选择字符数才显示翻译按钮 */
minSelectionLength: number;
/** 最大翻译字符数 */
maxTranslationLength: number;
}
/** /**
* 翻译图标SVG * 翻译图标SVG
*/ */

View File

@@ -1,7 +1,28 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {ref} from 'vue'; import {ref} from 'vue';
import {TranslationService} from '@/../bindings/voidraft/internal/services'; import {TranslationService} from '@/../bindings/voidraft/internal/services';
import {LanguageInfo, TRANSLATION_ERRORS, TranslationResult} from '@/common/constant/translation'; /**
* 翻译结果接口
*/
export interface TranslationResult {
translatedText: string;
error?: string;
}
/**
* 语言信息接口
*/
export interface LanguageInfo {
Code: string; // 语言代码
Name: string; // 语言名称
}
/**
* 翻译相关的错误消息
*/
export const TRANSLATION_ERRORS = {
NO_TEXT: 'no text to translate',
TRANSLATION_FAILED: 'translation failed',
} as const;
export const useTranslationStore = defineStore('translation', () => { export const useTranslationStore = defineStore('translation', () => {
// 基础状态 // 基础状态

View File

@@ -9,6 +9,8 @@ import LoadingScreen from '@/components/loading/LoadingScreen.vue';
import { useTabStore } from '@/stores/tabStore'; import { useTabStore } from '@/stores/tabStore';
import ContextMenu from './contextMenu/ContextMenu.vue'; import ContextMenu from './contextMenu/ContextMenu.vue';
import { contextMenuManager } from './contextMenu/manager'; import { contextMenuManager } from './contextMenu/manager';
import TranslatorDialog from './extensions/translator/TranslatorDialog.vue';
import { translatorManager } from './extensions/translator/manager';
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
@@ -34,17 +36,24 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
contextMenuManager.destroy(); contextMenuManager.destroy();
translatorManager.destroy();
}); });
</script> </script>
<template> <template>
<div class="editor-container"> <div class="editor-container">
<!-- 加载动画 -->
<transition name="loading-fade"> <transition name="loading-fade">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT" /> <LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT" />
</transition> </transition>
<!-- 编辑器区域 -->
<div ref="editorElement" class="editor"></div> <div ref="editorElement" class="editor"></div>
<!-- 工具栏 -->
<Toolbar /> <Toolbar />
<!-- 右键菜单 -->
<ContextMenu :portal-target="editorElement" /> <ContextMenu :portal-target="editorElement" />
<!-- 翻译器弹窗 -->
<TranslatorDialog :portal-target="editorElement" />
</div> </div>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'; import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
import { contextMenuManager } from './manager'; import { contextMenuManager } from './manager';
import type { RenderMenuItem } from './menuSchema'; import type { RenderMenuItem } from './menuSchema';
@@ -30,9 +30,19 @@ watch(
watch(isVisible, (visible) => { watch(isVisible, (visible) => {
if (visible) { if (visible) {
nextTick(adjustMenuWithinViewport); nextTick(adjustMenuWithinViewport);
// 显示时添加 outside 点击监听
document.addEventListener('mousedown', handleClickOutside);
} else {
// 隐藏时移除监听
document.removeEventListener('mousedown', handleClickOutside);
} }
}); });
// 清理
onUnmounted(() => {
document.removeEventListener('mousedown', handleClickOutside);
});
const menuStyle = computed(() => ({ const menuStyle = computed(() => ({
left: `${adjustedPosition.value.x}px`, left: `${adjustedPosition.value.x}px`,
top: `${adjustedPosition.value.y}px` top: `${adjustedPosition.value.y}px`
@@ -65,26 +75,24 @@ function handleItemClick(item: RenderMenuItem) {
contextMenuManager.runCommand(item); contextMenuManager.runCommand(item);
} }
function handleOverlayMouseDown() { function handleClickOutside(event: MouseEvent) {
// 如果点击在菜单内部,不关闭
if (menuRef.value?.contains(event.target as Node)) {
return;
}
contextMenuManager.hide(); contextMenuManager.hide();
} }
function stopPropagation(event: MouseEvent) {
event.stopPropagation();
}
</script> </script>
<template> <template>
<Teleport :to="teleportTarget"> <Teleport :to="teleportTarget">
<template v-if="isVisible"> <template v-if="isVisible">
<div class="cm-context-overlay" @mousedown="handleOverlayMouseDown" />
<div <div
ref="menuRef" ref="menuRef"
class="cm-context-menu show" class="cm-context-menu show"
:style="menuStyle" :style="menuStyle"
role="menu" role="menu"
@contextmenu.prevent @contextmenu.prevent
@mousedown="stopPropagation"
> >
<template v-for="item in items" :key="item.id"> <template v-for="item in items" :key="item.id">
<div v-if="item.type === 'separator'" class="cm-context-menu-divider" /> <div v-if="item.type === 'separator'" class="cm-context-menu-divider" />
@@ -110,13 +118,6 @@ function stopPropagation(event: MouseEvent) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.cm-context-overlay {
position: absolute;
inset: 0;
z-index: 9000;
background: transparent;
}
.cm-context-menu { .cm-context-menu {
position: fixed; position: fixed;
min-width: 180px; min-width: 180px;

View File

@@ -27,6 +27,26 @@ class ContextMenuManager {
} }
show(view: EditorView, clientX: number, clientY: number, items: RenderMenuItem[]): void { show(view: EditorView, clientX: number, clientY: number, items: RenderMenuItem[]): void {
const currentState = this.state.value;
// 如果菜单已经显示且位置很接近20px范围内则只更新内容避免闪烁
if (currentState.visible) {
const dx = Math.abs(currentState.position.x - clientX);
const dy = Math.abs(currentState.position.y - clientY);
const isSamePosition = dx < 20 && dy < 20;
if (isSamePosition) {
// 只更新items和view保持visible状态和位置
this.state.value = {
...currentState,
items,
view
};
return;
}
}
// 否则正常显示菜单
this.state.value = { this.state.value = {
visible: true, visible: true,
position: { x: clientX, y: clientY }, position: { x: clientX, y: clientY },

View File

@@ -0,0 +1,481 @@
<script setup lang="ts">
import {computed, nextTick, onUnmounted, ref, watch} from 'vue';
import {translatorManager} from './manager';
import {useTranslationStore} from '@/stores/translationStore';
const props = defineProps<{
portalTarget?: HTMLElement | null;
}>();
const state = translatorManager.useState();
const translationStore = useTranslationStore();
const dialogRef = ref<HTMLDivElement | null>(null);
const adjustedPosition = ref({ x: 0, y: 0 });
const isVisible = computed(() => state.value.visible);
const sourceText = computed(() => state.value.sourceText);
const position = computed(() => state.value.position);
const teleportTarget = computed<HTMLElement | string>(() => props.portalTarget ?? 'body');
const sourceLangSelector = ref('');
const targetLangSelector = ref('');
const translatorSelector = ref('');
const translatedText = ref('');
const isLoading = ref(false);
const isDragging = ref(false);
const dragStart = ref({ x: 0, y: 0 });
// 监听可见性变化
watch(isVisible, async (visible) => {
if (visible) {
adjustedPosition.value = { ...position.value };
await nextTick();
adjustDialogPosition();
await initializeTranslation();
await nextTick();
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
isDragging.value = false;
}
});
// 清理
onUnmounted(() => {
document.removeEventListener('mousedown', handleClickOutside);
});
const dialogStyle = computed(() => ({
left: `${adjustedPosition.value.x}px`,
top: `${adjustedPosition.value.y}px`
}));
const availableLanguages = computed(() => {
const languageMap = translationStore.translatorLanguages[translatorSelector.value];
if (!languageMap) return [];
return Object.entries(languageMap).map(([code, info]: [string, any]) => ({
code,
name: info.Name || info.name || code
}));
});
const availableTranslators = computed(() => translationStore.translators);
function adjustDialogPosition() {
const dialogEl = dialogRef.value;
const container = props.portalTarget;
if (!dialogEl || !container) return;
const containerRect = container.getBoundingClientRect();
const dialogRect = dialogEl.getBoundingClientRect();
let x = adjustedPosition.value.x;
let y = adjustedPosition.value.y;
// 限制在容器范围内
x = Math.max(containerRect.left, Math.min(x, containerRect.right - dialogRect.width - 8));
y = Math.max(containerRect.top, Math.min(y, containerRect.bottom - dialogRect.height - 8));
adjustedPosition.value = { x, y };
}
function clampPosition(x: number, y: number) {
const container = props.portalTarget;
const dialogEl = dialogRef.value;
if (!container || !dialogEl) return { x, y };
const containerRect = container.getBoundingClientRect();
const dialogRect = dialogEl.getBoundingClientRect();
return {
x: Math.max(containerRect.left, Math.min(x, containerRect.right - dialogRect.width)),
y: Math.max(containerRect.top, Math.min(y, containerRect.bottom - dialogRect.height))
};
}
async function initializeTranslation() {
isLoading.value = true;
translatedText.value = '';
try {
await loadTranslators();
await translate();
} catch (error) {
console.error('Failed to initialize translation:', error);
isLoading.value = false;
}
}
async function loadTranslators() {
const translators = translationStore.translators;
if (translators.length > 0) {
translatorSelector.value = translators[0];
}
resetLanguageSelectors();
}
function resetLanguageSelectors() {
const languageMap = translationStore.translatorLanguages[translatorSelector.value];
if (!languageMap) return;
const languages = Object.keys(languageMap);
if (languages.length > 0) {
sourceLangSelector.value = languages[0];
targetLangSelector.value = languages[0];
}
}
function handleTranslatorChange() {
resetLanguageSelectors();
translate();
}
function swapLanguages() {
const temp = sourceLangSelector.value;
sourceLangSelector.value = targetLangSelector.value;
targetLangSelector.value = temp;
translate();
}
async function translate() {
const sourceLang = sourceLangSelector.value;
const targetLang = targetLangSelector.value;
const translatorType = translatorSelector.value;
if (!sourceLang || !targetLang || !translatorType) {
return;
}
isLoading.value = true;
translatedText.value = '';
try {
const result = await translationStore.translateText(
sourceText.value,
sourceLang,
targetLang,
translatorType
);
translatedText.value = result.translatedText || result.error || '';
} catch (err) {
console.error('Translation failed:', err);
translatedText.value = 'Translation failed';
} finally {
isLoading.value = false;
}
}
function startDrag(e: MouseEvent) {
const target = e.target as HTMLElement;
if (target.closest('select, button')) return;
e.preventDefault();
e.stopPropagation();
const rect = dialogRef.value!.getBoundingClientRect();
dragStart.value = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
isDragging.value = true;
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', endDrag);
}
function onDrag(e: MouseEvent) {
adjustedPosition.value = clampPosition(
e.clientX - dragStart.value.x,
e.clientY - dragStart.value.y
);
}
function endDrag(e: MouseEvent) {
e.stopPropagation();
isDragging.value = false;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', endDrag);
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(translatedText.value);
} catch (error) {
console.error('Failed to copy text:', error);
}
}
function handleClickOutside(e: MouseEvent) {
if (isDragging.value) return;
if (dialogRef.value?.contains(e.target as Node)) return;
translatorManager.hide();
}
</script>
<template>
<Teleport :to="teleportTarget">
<template v-if="isVisible">
<div
ref="dialogRef"
class="cm-translation-tooltip"
:class="{ 'cm-translation-dragging': isDragging }"
:style="dialogStyle"
@mousedown="startDrag"
@keydown.esc="translatorManager.hide"
@contextmenu.prevent
tabindex="-1"
>
<div class="cm-translation-header">
<div class="cm-translation-controls">
<select
v-model="sourceLangSelector"
class="cm-translation-select"
@change="translate"
@mousedown.stop
>
<option v-for="lang in availableLanguages" :key="lang.code" :value="lang.code">
{{ lang.name }}
</option>
</select>
<button class="cm-translation-swap" @click="swapLanguages" @mousedown.stop title="交换语言">
<svg viewBox="0 0 24 24" width="11" height="11">
<path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/>
</svg>
</button>
<select
v-model="targetLangSelector"
class="cm-translation-select"
@change="translate"
@mousedown.stop
>
<option v-for="lang in availableLanguages" :key="lang.code" :value="lang.code">
{{ lang.name }}
</option>
</select>
<select
v-model="translatorSelector"
class="cm-translation-select"
@change="handleTranslatorChange"
@mousedown.stop
>
<option v-for="translator in availableTranslators" :key="translator" :value="translator">
{{ translator }}
</option>
</select>
</div>
</div>
<div class="cm-translation-scroll-container">
<div v-if="isLoading" class="cm-translation-loading">
Translation...
</div>
<div v-else class="cm-translation-result">
<div class="cm-translation-result-wrapper">
<button
v-if="translatedText"
class="cm-translation-copy-btn"
@click="copyToClipboard"
@mousedown.stop
title="复制"
>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
<div class="cm-translation-target">{{ translatedText }}</div>
</div>
</div>
</div>
</div>
</template>
</Teleport>
</template>
<style scoped>
.cm-translation-tooltip {
position: fixed;
background: var(--settings-card-bg, #fff);
color: var(--text-primary, #333);
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.2);
padding: 6px;
max-width: 240px;
max-height: 180px;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: var(--voidraft-font-mono, system-ui, -apple-system, sans-serif), serif;
font-size: 10px;
user-select: none;
cursor: grab;
z-index: 10000;
outline: none;
}
.cm-translation-dragging {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.2);
z-index: 10001;
cursor: grabbing !important;
}
.cm-translation-header {
margin-bottom: 6px;
flex-shrink: 0;
}
.cm-translation-controls {
display: flex;
align-items: center;
gap: 3px;
flex-wrap: nowrap;
}
.cm-translation-select {
padding: 2px 3px;
border-radius: 4px;
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.12));
background: var(--bg-primary, #f8f8f8);
font-size: 10px;
color: var(--text-primary, #333);
flex: 1;
min-width: 0;
max-width: 65px;
height: 20px;
cursor: pointer;
}
.cm-translation-select:focus {
outline: none;
border-color: var(--border-color, rgba(66, 133, 244, 0.5));
}
.cm-translation-swap {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.12));
background: var(--bg-primary, transparent);
color: var(--text-muted, #666);
cursor: pointer;
padding: 0;
flex-shrink: 0;
transition: all 0.15s ease;
}
.cm-translation-swap:hover {
background: var(--bg-hover, rgba(66, 133, 244, 0.08));
border-color: var(--border-color, rgba(66, 133, 244, 0.3));
}
.cm-translation-scroll-container {
overflow-y: auto;
flex: 1;
min-height: 0;
}
.cm-translation-scroll-container::-webkit-scrollbar {
width: 4px;
}
.cm-translation-scroll-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 2px;
}
.cm-translation-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
.cm-translation-result {
display: flex;
flex-direction: column;
}
.cm-translation-result-wrapper {
position: relative;
width: 100%;
}
.cm-translation-copy-btn {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 3px;
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
background: var(--bg-primary, rgba(255, 255, 255, 0.9));
color: var(--text-muted, #666);
cursor: pointer;
padding: 0;
position: absolute;
top: 3px;
right: 3px;
z-index: 2;
opacity: 0.6;
transition: all 0.15s ease;
}
.cm-translation-copy-btn:hover {
background: var(--bg-hover, rgba(66, 133, 244, 0.1));
opacity: 1;
transform: scale(1.05);
}
.cm-translation-copy-btn svg {
width: 11px;
height: 11px;
}
.cm-translation-target {
padding: 5px;
padding-right: 24px;
background: var(--bg-primary, rgba(66, 133, 244, 0.03));
color: var(--text-primary, #333);
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.4;
min-height: 32px;
}
.cm-translation-loading {
padding: 6px;
text-align: center;
color: var(--text-muted, #666);
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
min-height: 32px;
}
.cm-translation-loading::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--text-muted, rgba(0, 0, 0, 0.2));
border-top-color: var(--text-muted, #666);
animation: cm-translation-spin 0.8s linear infinite;
}
@keyframes cm-translation-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -1,355 +1,84 @@
import { Extension, StateField, StateEffect, StateEffectType } from '@codemirror/state'; import { Extension, StateField } from '@codemirror/state';
import { EditorView, showTooltip, Tooltip } from '@codemirror/view'; import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
import { createTranslationTooltip } from './tooltip'; import { translatorManager } from './manager';
import { import { TRANSLATION_ICON_SVG } from '@/common/constant/translation';
TranslatorConfig,
DEFAULT_TRANSLATION_CONFIG,
TRANSLATION_ICON_SVG
} from '@/common/constant/translation';
function TranslationTooltips(state: any): readonly Tooltip[] {
const selection = state.selection.main;
if (selection.empty) return [];
const selectedText = state.sliceDoc(selection.from, selection.to);
if (!selectedText.trim()) return [];
return [{
pos: selection.to,
above: false,
strictSide: true,
arrow: false,
create: (view) => {
const dom = document.createElement('div');
dom.className = 'cm-translator-button';
dom.innerHTML = TRANSLATION_ICON_SVG;
class TranslatorExtension { dom.addEventListener('mousedown', (e) => {
private config: TranslatorConfig; e.preventDefault();
private setTranslationTooltip: StateEffectType<Tooltip | null>; e.stopPropagation();
private translationTooltipField: StateField<readonly Tooltip[]>; showTranslatorDialog(view);
private translationButtonField: StateField<readonly Tooltip[]>; });
constructor(config?: Partial<TranslatorConfig>) { return { dom };
// 初始化配置
this.config = {
minSelectionLength: DEFAULT_TRANSLATION_CONFIG.minSelectionLength,
maxTranslationLength: DEFAULT_TRANSLATION_CONFIG.maxTranslationLength,
...config
};
// 初始化状态效果
this.setTranslationTooltip = StateEffect.define<Tooltip | null>();
// 初始化翻译气泡状态字段
this.translationTooltipField = StateField.define<readonly Tooltip[]>({
create: () => [],
update: (tooltips, tr) => {
// 检查是否有特定的状态效果来更新tooltips
for (const effect of tr.effects) {
if (effect.is(this.setTranslationTooltip)) {
return effect.value ? [effect.value] : [];
}
}
// 如果文档或选择变化,隐藏气泡
if (tr.docChanged || tr.selection) {
return [];
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
// 初始化翻译按钮状态字段
this.translationButtonField = StateField.define<readonly Tooltip[]>({
create: (state) => this.getTranslationButtonTooltips(state),
update: (tooltips, tr) => {
// 如果文档或选择变化重新计算tooltip
if (tr.docChanged || tr.selection) {
return this.getTranslationButtonTooltips(tr.state);
}
// 检查是否有翻译气泡显示,如果有则不显示按钮
if (tr.state.field(this.translationTooltipField).length > 0) {
return [];
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
}
/**
* 根据当前选择获取翻译按钮tooltip
*/
private getTranslationButtonTooltips(state: any): readonly Tooltip[] {
// 如果气泡已显示,则不显示按钮
if (state.field(this.translationTooltipField).length > 0) return [];
const selection = state.selection.main;
// 如果没有选中文本,不显示按钮
if (selection.empty) return [];
// 获取选中的文本
const selectedText = state.sliceDoc(selection.from, selection.to);
// 检查文本是否只包含空格
if (!selectedText.trim()) {
return [];
} }
}];
// 检查文本长度条件
if (selectedText.length < this.config.minSelectionLength ||
selectedText.length > this.config.maxTranslationLength) {
return [];
}
// 返回翻译按钮tooltip配置
return [{
pos: selection.to,
above: false,
strictSide: true,
arrow: false,
create: (view) => {
// 创建按钮DOM
const dom = document.createElement('div');
dom.className = 'cm-translator-button';
dom.innerHTML = TRANSLATION_ICON_SVG;
// 点击事件
dom.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
// 显示翻译气泡
this.showTranslationTooltip(view);
});
return { dom };
}
}];
}
/**
* 显示翻译气泡
*/
private showTranslationTooltip(view: EditorView) {
// 直接从当前选择获取文本
const selection = view.state.selection.main;
if (selection.empty) return;
const selectedText = view.state.sliceDoc(selection.from, selection.to);
if (!selectedText.trim()) return;
// 创建翻译气泡
const tooltip = createTranslationTooltip(view, selectedText);
// 更新状态以显示气泡
view.dispatch({
effects: this.setTranslationTooltip.of(tooltip)
});
}
/**
* 创建扩展
*/
createExtension(): Extension {
return [
// 翻译按钮tooltip
this.translationButtonField,
// 翻译气泡tooltip
this.translationTooltipField,
// 添加基础样式
EditorView.baseTheme({
".cm-translator-button": {
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
background: "var(--bg-secondary, transparent)",
color: "var(--text-muted, #4285f4)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
padding: "2px",
width: "24px",
height: "24px",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
userSelect: "none",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
},
// 翻译气泡样式
".cm-translation-tooltip": {
background: "var(--bg-secondary, #fff)",
color: "var(--text-primary, #333)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
padding: "8px",
maxWidth: "300px",
maxHeight: "200px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
fontSize: "11px",
userSelect: "none",
cursor: "grab"
},
// 拖拽状态样式
".cm-translation-dragging": {
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)",
zIndex: "1000",
cursor: "grabbing !important"
},
".cm-translation-header": {
marginBottom: "8px",
flexShrink: "0"
},
".cm-translation-controls": {
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "nowrap"
},
".cm-translation-select": {
padding: "2px 4px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, #f5f5f5)",
fontSize: "11px",
color: "var(--text-primary, #333)",
flex: "1",
minWidth: "0",
maxWidth: "80px"
},
".cm-translation-swap": {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "16px",
height: "16px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, transparent)",
color: "var(--text-muted, #666)",
cursor: "pointer",
padding: "0",
flexShrink: "0",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
},
// 滚动容器
".cm-translation-scroll-container": {
overflowY: "auto",
flex: "1",
minHeight: "0"
},
".cm-translation-result": {
display: "flex",
flexDirection: "column"
},
".cm-translation-result-header": {
display: "flex",
justifyContent: "flex-end",
marginBottom: "4px"
},
".cm-translation-result-wrapper": {
position: "relative",
width: "100%"
},
".cm-translation-copy-btn": {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "20px",
height: "20px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, transparent)",
color: "var(--text-muted, #666)",
cursor: "pointer",
padding: "0",
position: "absolute",
top: "4px",
right: "4px",
zIndex: "2",
opacity: "0.7",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
opacity: "1"
},
"&.copied": {
background: "var(--bg-success, #4caf50)",
color: "white",
border: "1px solid var(--bg-success, #4caf50)",
opacity: "1"
}
},
".cm-translation-target": {
padding: "6px",
paddingRight: "28px", // 为复制按钮留出空间
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
color: "var(--text-primary, #333)",
borderRadius: "3px",
whiteSpace: "pre-wrap",
wordBreak: "break-word"
},
".cm-translation-notice": {
fontSize: "10px",
color: "var(--text-muted, #888)",
padding: "2px 0",
fontStyle: "italic",
textAlign: "center",
marginBottom: "2px"
},
".cm-translation-error": {
color: "var(--text-danger, #d32f2f)",
fontStyle: "italic"
},
".cm-translation-loading": {
padding: "8px",
textAlign: "center",
color: "var(--text-muted, #666)",
fontSize: "11px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "6px"
},
".cm-translation-loading::before": {
content: "''",
display: "inline-block",
width: "12px",
height: "12px",
borderRadius: "50%",
border: "2px solid var(--text-muted, #666)",
borderTopColor: "transparent",
animation: "cm-translation-spin 1s linear infinite"
},
"@keyframes cm-translation-spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
}
})
];
}
} }
/** function showTranslatorDialog(view: EditorView) {
* 创建翻译扩展 const selection = view.state.selection.main;
*/ if (selection.empty) return;
export function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
const translatorExtension = new TranslatorExtension(config); const selectedText = view.state.sliceDoc(selection.from, selection.to);
return translatorExtension.createExtension(); if (!selectedText.trim()) return;
const coords = view.coordsAtPos(selection.to);
if (!coords) return;
translatorManager.show(view, coords.left, coords.bottom + 5, selectedText);
}
const translationButtonField = StateField.define<readonly Tooltip[]>({
create: (state) => TranslationTooltips(state),
update: (tooltips, tr) => {
if (tr.docChanged || tr.selection) {
return TranslationTooltips(tr.state);
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
export function createTranslatorExtension(): Extension {
return [
translationButtonField,
EditorView.baseTheme({
".cm-translator-button": {
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
background: "var(--bg-secondary, transparent)",
color: "var(--text-muted, #4285f4)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
padding: "2px",
width: "24px",
height: "24px",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
userSelect: "none",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
}
})
];
} }
export default createTranslatorExtension; export default createTranslatorExtension;

View File

@@ -0,0 +1,66 @@
import type { EditorView } from '@codemirror/view';
import { readonly, shallowRef, type ShallowRef } from 'vue';
interface TranslatorPosition {
x: number;
y: number;
}
interface TranslatorState {
visible: boolean;
position: TranslatorPosition;
sourceText: string;
view: EditorView | null;
}
class TranslatorManager {
private state: ShallowRef<TranslatorState> = shallowRef({
visible: false,
position: { x: 0, y: 0 },
sourceText: '',
view: null
});
useState() {
return readonly(this.state);
}
show(view: EditorView, clientX: number, clientY: number, text: string): void {
this.state.value = {
visible: true,
position: { x: clientX, y: clientY },
sourceText: text,
view
};
}
hide(): void {
if (!this.state.value.visible) {
return;
}
const view = this.state.value.view;
this.state.value = {
visible: false,
position: { x: 0, y: 0 },
sourceText: '',
view: null
};
if (view) {
view.focus();
}
}
destroy(): void {
this.state.value = {
visible: false,
position: { x: 0, y: 0 },
sourceText: '',
view: null
};
}
}
export const translatorManager = new TranslatorManager();

View File

@@ -1,598 +0,0 @@
import {EditorView, Tooltip, TooltipView} from '@codemirror/view';
import {useTranslationStore} from '@/stores/translationStore';
/**
* 翻译气泡弹窗类
* 提供文本翻译功能的交互式界面
*/
export class TranslationTooltip implements TooltipView {
// ===== 核心属性 =====
dom!: HTMLElement;
sourceText: string;
translationStore: ReturnType<typeof useTranslationStore>;
// ===== UI 元素 =====
private translatorSelector!: HTMLSelectElement;
private sourceLangSelector!: HTMLSelectElement;
private targetLangSelector!: HTMLSelectElement;
private resultContainer!: HTMLDivElement;
private loadingIndicator!: HTMLDivElement;
private swapButton!: HTMLButtonElement;
// ===== 状态管理 =====
private translatedText: string = '';
private eventListeners: Array<{element: HTMLElement | Document, event: string, handler: EventListener}> = [];
// ===== 拖拽状态 =====
private isDragging: boolean = false;
private dragOffset: { x: number; y: number } = { x: 0, y: 0 };
constructor(_view: EditorView, text: string) {
this.sourceText = text;
this.translationStore = useTranslationStore();
this.initializeDOM();
this.setupEventListeners();
this.initializeTranslation();
}
// ===== DOM 初始化 =====
/**
* 初始化DOM结构
*/
private initializeDOM(): void {
this.dom = this.createElement('div', 'cm-translation-tooltip');
// 设置为绝对定位,允许拖拽移动
this.dom.style.position = 'absolute';
const header = this.createHeader();
const scrollContainer = this.createScrollContainer();
this.dom.appendChild(header);
this.dom.appendChild(scrollContainer);
}
/**
* 创建头部控制区域
*/
private createHeader(): HTMLElement {
const header = this.createElement('div', 'cm-translation-header');
const controlsContainer = this.createElement('div', 'cm-translation-controls');
// 创建所有控制元素
this.sourceLangSelector = this.createSelector('cm-translation-select');
this.swapButton = this.createSwapButton();
this.targetLangSelector = this.createSelector('cm-translation-select');
this.translatorSelector = this.createTranslatorSelector();
// 添加到控制容器
controlsContainer.appendChild(this.sourceLangSelector);
controlsContainer.appendChild(this.swapButton);
controlsContainer.appendChild(this.targetLangSelector);
controlsContainer.appendChild(this.translatorSelector);
header.appendChild(controlsContainer);
return header;
}
/**
* 创建滚动容器
*/
private createScrollContainer(): HTMLElement {
const scrollContainer = this.createElement('div', 'cm-translation-scroll-container');
this.loadingIndicator = this.createElement('div', 'cm-translation-loading') as HTMLDivElement;
this.loadingIndicator.textContent = 'Translation...';
this.loadingIndicator.style.display = 'none';
this.resultContainer = this.createElement('div', 'cm-translation-result') as HTMLDivElement;
scrollContainer.appendChild(this.loadingIndicator);
scrollContainer.appendChild(this.resultContainer);
return scrollContainer;
}
/**
* 创建选择器元素
*/
private createSelector(className: string): HTMLSelectElement {
const select = this.createElement('select', className) as HTMLSelectElement;
return select;
}
/**
* 创建语言交换按钮
*/
private createSwapButton(): HTMLButtonElement {
const button = this.createElement('button', 'cm-translation-swap') as HTMLButtonElement;
button.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/></svg>`;
return button;
}
/**
* 创建翻译器选择器
*/
private createTranslatorSelector(): HTMLSelectElement {
const select = this.createSelector('cm-translation-select');
const tempOption = this.createElement('option') as HTMLOptionElement;
tempOption.textContent = 'Loading...';
select.appendChild(tempOption);
return select;
}
/**
* 通用DOM元素创建方法
*/
private createElement(tag: string, className?: string): HTMLElement {
const element = document.createElement(tag);
if (className) {
element.className = className;
}
return element;
}
// ===== 事件管理 =====
/**
* 设置事件监听器
*/
private setupEventListeners(): void {
this.addEventListenerWithCleanup(this.sourceLangSelector, 'change', () => {
this.handleLanguageChange();
});
this.addEventListenerWithCleanup(this.targetLangSelector, 'change', () => {
this.handleLanguageChange();
});
this.addEventListenerWithCleanup(this.swapButton, 'click', () => {
this.swapLanguages();
});
// 添加拖拽事件监听器
this.setupDragListeners();
}
/**
* 添加事件监听器并记录以便清理
*/
private addEventListenerWithCleanup(element: HTMLElement | Document, event: string, handler: EventListener): void {
element.addEventListener(event, handler);
this.eventListeners.push({ element, event, handler });
}
/**
* 清理所有事件监听器
*/
private cleanupEventListeners(): void {
this.eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.eventListeners = [];
}
// ===== 初始化和生命周期 =====
/**
* 初始化翻译功能
*/
private async initializeTranslation(): Promise<void> {
this.showLoading();
this.resultContainer.innerHTML = '<div class="cm-translation-loading">Loading...</div>';
try {
await this.loadTranslators();
await this.translate();
} catch (error) {
console.error('Failed to initialize translation:', error);
this.hideLoading();
}
}
// ===== 语言管理 =====
/**
* 设置拖拽事件监听器
*/
private setupDragListeners(): void {
// 在整个翻译框上监听鼠标按下事件
this.addEventListenerWithCleanup(this.dom, 'mousedown', (e: Event) => {
const mouseEvent = e as MouseEvent;
const target = mouseEvent.target as HTMLElement;
// 如果点击的是交互元素(按钮、选择框等),不启动拖拽
if (target.tagName === 'SELECT' || target.tagName === 'BUTTON' ||
target.tagName === 'OPTION' || target.closest('select') || target.closest('button')) {
return;
}
this.startDrag(mouseEvent);
});
// 鼠标移动
this.addEventListenerWithCleanup(document, 'mousemove', (e: Event) => {
const mouseEvent = e as MouseEvent;
this.onDrag(mouseEvent);
});
// 鼠标释放结束拖拽
this.addEventListenerWithCleanup(document, 'mouseup', () => {
this.endDrag();
});
}
/**
* 开始拖拽
*/
private startDrag(e: MouseEvent): void {
e.preventDefault();
this.isDragging = true;
const rect = this.dom.getBoundingClientRect();
this.dragOffset = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// 添加拖拽状态样式
this.dom.classList.add('cm-translation-dragging');
this.dom.style.cursor = 'grabbing';
}
/**
* 拖拽过程中
*/
private onDrag(e: MouseEvent): void {
if (!this.isDragging) return;
e.preventDefault();
const newX = e.clientX - this.dragOffset.x;
const newY = e.clientY - this.dragOffset.y;
// 确保不会拖拽到视窗外
const maxX = window.innerWidth - this.dom.offsetWidth;
const maxY = window.innerHeight - this.dom.offsetHeight;
const clampedX = Math.max(0, Math.min(newX, maxX));
const clampedY = Math.max(0, Math.min(newY, maxY));
this.dom.style.left = `${clampedX}px`;
this.dom.style.top = `${clampedY}px`;
}
/**
* 结束拖拽
*/
private endDrag(): void {
if (!this.isDragging) return;
this.isDragging = false;
// 移除拖拽状态样式
this.dom.classList.remove('cm-translation-dragging');
this.dom.style.cursor = 'default';
}
/**
* 处理语言变更
*/
private handleLanguageChange(): void {
// 语言变更后重新翻译具体的语言限制逻辑在store中处理
this.translate();
}
/**
* 交换源语言和目标语言
*/
private swapLanguages(): void {
const temp = this.sourceLangSelector.value;
this.sourceLangSelector.value = this.targetLangSelector.value;
this.targetLangSelector.value = temp;
this.translate();
}
// ===== 翻译器管理 =====
/**
* 加载翻译器选项
*/
private async loadTranslators(): Promise<boolean> {
try {
this.clearSelectOptions(this.translatorSelector);
const translators = this.translationStore.translators;
this.populateTranslatorOptions(translators);
// 添加翻译器变更事件监听
this.addEventListenerWithCleanup(this.translatorSelector, 'change', () => {
this.handleTranslatorChange();
});
await this.updateLanguageSelectors();
return true;
} catch (error) {
console.error('Failed to load translators:', error);
this.loadDefaultTranslators();
await this.updateLanguageSelectors();
return false;
}
}
/**
* 填充翻译器选项
*/
private populateTranslatorOptions(translators: string[]): void {
translators.forEach((translator, index) => {
const option = this.createElement('option') as HTMLOptionElement;
option.value = translator;
option.textContent = translator;
option.selected = index === 0; // 选择第一个翻译器
this.translatorSelector.appendChild(option);
});
}
/**
* 加载默认翻译器
*/
private loadDefaultTranslators(): void {
this.clearSelectOptions(this.translatorSelector);
// 使用从后端获取的翻译器列表
const translators = this.translationStore.translators;
this.populateTranslatorOptions(translators);
this.addEventListenerWithCleanup(this.translatorSelector, 'change', () => {
this.handleTranslatorChange();
});
}
/**
* 处理翻译器选择变化
*/
private async handleTranslatorChange(): Promise<void> {
await this.updateLanguageSelectors();
this.translate();
}
// ===== 语言选择器管理 =====
/**
* 更新语言选择器
*/
private async updateLanguageSelectors(): Promise<void> {
const currentTranslator = this.translatorSelector.value;
// 保存当前选中的语言
const currentSourceLang = this.sourceLangSelector.value || '';
const currentTargetLang = this.targetLangSelector.value;
// 清空选择器
this.clearSelectOptions(this.sourceLangSelector);
this.clearSelectOptions(this.targetLangSelector);
// 直接使用预加载的语言映射
const languageMap = this.translationStore.translatorLanguages[currentTranslator];
if (!languageMap || Object.keys(languageMap).length === 0) {
return;
}
// 添加语言选项
Object.entries(languageMap).forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
// 恢复之前的语言选择
this.restoreLanguageSelection(currentSourceLang, currentTargetLang);
}
/**
* 清空选择器选项
*/
private clearSelectOptions(selector: HTMLSelectElement): void {
while (selector.firstChild) {
selector.removeChild(selector.firstChild);
}
}
/**
* 添加语言选项到选择器
*/
private addLanguageOption(code: string, langInfo: any): void {
const displayName = langInfo.Name || langInfo.name || code;
// 添加源语言选项
const sourceOption = this.createElement('option') as HTMLOptionElement;
sourceOption.value = code;
sourceOption.textContent = displayName;
this.sourceLangSelector.appendChild(sourceOption);
// 添加目标语言选项
const targetOption = this.createElement('option') as HTMLOptionElement;
targetOption.value = code;
targetOption.textContent = displayName;
this.targetLangSelector.appendChild(targetOption);
}
/**
* 恢复语言选择
*/
private restoreLanguageSelection(sourceLang: string, targetLang: string): void {
// 设置源语言
if (sourceLang && this.hasLanguageOption(this.sourceLangSelector, sourceLang)) {
this.sourceLangSelector.value = sourceLang;
} else if (this.sourceLangSelector.options.length > 0) {
this.sourceLangSelector.selectedIndex = 0;
}
// 设置目标语言
if (targetLang && this.hasLanguageOption(this.targetLangSelector, targetLang)) {
this.targetLangSelector.value = targetLang;
} else if (this.targetLangSelector.options.length > 0) {
this.targetLangSelector.selectedIndex = 0;
}
// 确保源语言和目标语言不同
this.handleLanguageChange();
}
/**
* 检查选择器是否有指定语言选项
*/
private hasLanguageOption(selector: HTMLSelectElement, langCode: string): boolean {
return Array.from(selector.options).some(option => option.value === langCode);
}
// ===== 翻译功能 =====
/**
* 执行翻译
*/
private async translate(): Promise<void> {
const sourceLang = this.sourceLangSelector.value;
const targetLang = this.targetLangSelector.value;
const translatorType = this.translatorSelector.value;
this.showLoading();
this.resultContainer.innerHTML = '';
try {
const result = await this.translationStore.translateText(
this.sourceText,
sourceLang,
targetLang,
translatorType
);
this.displayTranslationResult(result);
} catch (err) {
console.error('Translation failed:', err);
this.displayError('Translation failed');
} finally {
this.hideLoading();
}
}
// ===== UI 状态管理 =====
/**
* 显示加载状态
*/
private showLoading(): void {
this.loadingIndicator.style.display = 'block';
}
/**
* 隐藏加载状态
*/
private hideLoading(): void {
this.loadingIndicator.style.display = 'none';
}
/**
* 显示错误信息
*/
private displayError(message: string): void {
this.resultContainer.innerHTML = '';
this.translatedText = '';
const errorElement = this.createElement('div', 'cm-translation-error');
errorElement.textContent = message;
this.resultContainer.appendChild(errorElement);
}
// ===== 结果显示 =====
/**
* 显示翻译结果
*/
private displayTranslationResult(result: any): void {
this.resultContainer.innerHTML = '';
const resultWrapper = this.createElement('div', 'cm-translation-result-wrapper');
const translatedTextElem = this.createElement('div', 'cm-translation-target');
if (result.error) {
translatedTextElem.classList.add('cm-translation-error');
translatedTextElem.textContent = result.error;
this.translatedText = '';
} else {
this.translatedText = result.translatedText || '';
translatedTextElem.textContent = this.translatedText;
}
// 添加复制按钮
if (this.translatedText) {
const copyButton = this.createCopyButton();
resultWrapper.appendChild(copyButton);
}
resultWrapper.appendChild(translatedTextElem);
this.resultContainer.appendChild(resultWrapper);
}
/**
* 创建复制按钮
*/
private createCopyButton(): HTMLButtonElement {
const copyButton = this.createElement('button', 'cm-translation-copy-btn') as HTMLButtonElement;
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
this.addEventListenerWithCleanup(copyButton, 'click', () => {
this.copyToClipboard(copyButton);
});
return copyButton;
}
/**
* 复制文本到剪贴板
*/
private async copyToClipboard(button: HTMLButtonElement): Promise<void> {
try {
await navigator.clipboard.writeText(this.translatedText);
this.showCopySuccess(button);
} catch (error) {
console.error('Failed to copy text:', error);
}
}
/**
* 显示复制成功状态
*/
private showCopySuccess(button: HTMLButtonElement): void {
const originalHTML = button.innerHTML;
button.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`;
button.classList.add('copied');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('copied');
}, 1500);
}
// ===== 生命周期管理 =====
/**
* 销毁组件时的清理工作
*/
destroy(): void {
this.cleanupEventListeners();
}
}
// 创建翻译气泡
export function createTranslationTooltip(view: EditorView, text: string): Tooltip {
return {
pos: view.state.selection.main.to, // 紧贴文本末尾
above: false,
strictSide: false,
arrow: true,
create: () => new TranslationTooltip(view, text)
};
}

View File

@@ -26,7 +26,7 @@ import {deleteLineCommand} from '../extensions/codeblock/deleteLine';
import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines'; import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines';
import {transposeChars} from '../extensions/codeblock'; import {transposeChars} from '../extensions/codeblock';
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste'; import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste';
import {textHighlightToggleCommand} from '../extensions/textHighlight/textHighlightExtension'; import {textHighlightToggleCommand} from '../extensions/textHighlight';
import { import {
copyLineDown, copyLineDown,
copyLineUp, copyLineUp,

View File

@@ -4,7 +4,7 @@ import i18n from '@/i18n';
import {ExtensionDefinition} from './types'; import {ExtensionDefinition} from './types';
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'; import {createTextHighlighter} from '../extensions/textHighlight';
import {color} from '../extensions/colorSelector'; import {color} from '../extensions/colorSelector';
import {hyperLink} from '../extensions/hyperlink'; import {hyperLink} from '../extensions/hyperlink';
import {minimap} from '../extensions/minimap'; import {minimap} from '../extensions/minimap';
@@ -43,13 +43,7 @@ const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
descriptionKey: 'extensions.colorSelector.description' descriptionKey: 'extensions.colorSelector.description'
}, },
[ExtensionID.ExtensionTranslator]: { [ExtensionID.ExtensionTranslator]: {
definition: defineExtension((config: any) => createTranslatorExtension({ definition: defineExtension(() => createTranslatorExtension()),
minSelectionLength: config?.minSelectionLength ?? 2,
maxTranslationLength: config?.maxTranslationLength ?? 5000
}), {
minSelectionLength: 2,
maxTranslationLength: 5000
}),
displayNameKey: 'extensions.translator.name', displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description' descriptionKey: 'extensions.translator.description'
}, },

14
go.mod
View File

@@ -10,11 +10,11 @@ require (
github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/providers/structs v1.0.0
github.com/knadh/koanf/v2 v2.3.0 github.com/knadh/koanf/v2 v2.3.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.40 github.com/wailsapp/wails/v3 v3.0.0-alpha.41
golang.org/x/net v0.47.0 golang.org/x/net v0.47.0
golang.org/x/sys v0.38.0 golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0 golang.org/x/text v0.31.0
modernc.org/sqlite v1.40.0 modernc.org/sqlite v1.40.1
resty.dev/v3 v3.0.0-beta.3 resty.dev/v3 v3.0.0-beta.3
) )
@@ -29,7 +29,7 @@ require (
github.com/adrg/xdg v0.5.3 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -42,7 +42,7 @@ require (
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
@@ -73,19 +73,19 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect github.com/skeema/knownhosts v1.3.2 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect github.com/xanzy/go-gitlab v0.115.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
golang.org/x/image v0.33.0 // indirect golang.org/x/image v0.33.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.67.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )

48
go.sum
View File

@@ -25,8 +25,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk= github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM= github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -62,8 +62,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -87,6 +87,8 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
@@ -162,12 +164,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.40 h1:LY0hngVwihlSXveshL5LM8ivjLTHAN6VDjOSF6szI9k= github.com/wailsapp/wails/v3 v3.0.0-alpha.41 h1:DYcC1/vtO862sxnoyCOMfLLypbzpFWI257fR6zDYY+Y=
github.com/wailsapp/wails/v3 v3.0.0-alpha.40/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw= github.com/wailsapp/wails/v3 v3.0.0-alpha.41/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M= github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -178,12 +180,12 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -220,8 +222,8 @@ golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -236,18 +238,20 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= modernc.org/libc v1.67.0 h1:QzL4IrKab2OFmxA3/vRYl0tLXrIamwrhD6CKD4WBVjQ=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/libc v1.67.0/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -256,8 +260,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ= modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=