diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 32271a0ae2..35b982fd13 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -55,6 +55,7 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", + "@intlify/core-base": "^11.4.6", "@element-plus/icons-vue": "^2.3.2", "@hfelix/electron-localshortcut": "^4.0.1", "@marktext/file-icons": "^1.0.6", diff --git a/packages/desktop/src/main/filesystem/encoding.ts b/packages/desktop/src/main/filesystem/encoding.ts index 6058adf3d2..cebee99838 100644 --- a/packages/desktop/src/main/filesystem/encoding.ts +++ b/packages/desktop/src/main/filesystem/encoding.ts @@ -33,6 +33,22 @@ const checkSequence = (buffer: Buffer, sequence: number[]): boolean => { return sequence.every((v, i) => v === buffer[i]) } +// `ced` occasionally misdetects a valid UTF-8 file as a legacy double-byte +// encoding (notably GBK), mojibaking multi-byte text — e.g. Greek µ/κ/α become +// CJK 碌/魏/伪 (#3151). A NUL byte signals binary / BOM-less UTF-16, not a UTF-8 +// text file. +const isLikelyUtf8 = (buffer: Buffer): boolean => { + if (buffer.includes(0)) { + return false + } + try { + new TextDecoder('utf-8', { fatal: true }).decode(buffer) + return true + } catch { + return false + } +} + /** * Guess the encoding from the buffer. */ @@ -49,6 +65,11 @@ export const guessEncoding = (buffer: Buffer, autoGuessEncoding: boolean): Encod // Auto guess encoding, otherwise use UTF-8. if (autoGuessEncoding) { + // A file that is already valid UTF-8 must be decoded as UTF-8, regardless + // of what `ced` heuristically guesses (#3151). + if (isLikelyUtf8(buffer)) { + return { encoding: 'utf8', isBom } + } encoding = ced(buffer) if (CED_ICONV_ENCODINGS[encoding]) { encoding = CED_ICONV_ENCODINGS[encoding] diff --git a/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue b/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue index 8d2d8ca244..7a0ad03e49 100644 --- a/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue +++ b/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue @@ -1044,12 +1044,20 @@ const replaceMisspelling = (payload: unknown) => { } const handleUndo = () => { + if (sourceCode.value) { + return + } + if (editor.value) { editor.value.undo() } } const handleRedo = () => { + if (sourceCode.value) { + return + } + if (editor.value) { editor.value.redo() } @@ -1912,6 +1920,10 @@ onMounted(() => { // editableHeight is the lowest cursor position(till to top) that editor allowed. const editableHeight = container.clientHeight - 100 animatedScrollTo(container, container.scrollTop + (y - editableHeight), 0) + } else if (y < 100) { + // Symmetric to #628: scroll up when the cursor rises above the top edge + // (e.g. Arrow-Up), otherwise the caret leaves the viewport (#3329). + animatedScrollTo(container, container.scrollTop + (y - 100), 0) } } diff --git a/packages/desktop/src/renderer/src/components/editorWithTabs/sourceCode.vue b/packages/desktop/src/renderer/src/components/editorWithTabs/sourceCode.vue index beef2d43e4..c685c33969 100644 --- a/packages/desktop/src/renderer/src/components/editorWithTabs/sourceCode.vue +++ b/packages/desktop/src/renderer/src/components/editorWithTabs/sourceCode.vue @@ -205,6 +205,26 @@ const handleSelectAll = () => { } } +const handleUndo = () => { + if (!sourceCode.value) { + return + } + + if (editor.value) { + editor.value.execCommand('undo') + } +} + +const handleRedo = () => { + if (!sourceCode.value) { + return + } + + if (editor.value) { + editor.value.execCommand('redo') + } +} + interface ImageActionPayload { id: string result: string @@ -340,6 +360,8 @@ onMounted(() => { bus.on('invalidate-image-cache', handleInvalidateImageCache) bus.on('file-changed', handleFileChange) bus.on('selectAll', handleSelectAll) + bus.on('undo', handleUndo) + bus.on('redo', handleRedo) bus.on('image-action', handleImageAction) bus.on('scroll-to-header', handleScrollToHeader) @@ -378,6 +400,8 @@ onBeforeUnmount(() => { bus.off('invalidate-image-cache', handleInvalidateImageCache) bus.off('file-changed', handleFileChange) bus.off('selectAll', handleSelectAll) + bus.off('undo', handleUndo) + bus.off('redo', handleRedo) bus.off('image-action', handleImageAction) bus.off('scroll-to-header', handleScrollToHeader) diff --git a/packages/desktop/src/renderer/src/components/editorWithTabs/tabs.vue b/packages/desktop/src/renderer/src/components/editorWithTabs/tabs.vue index af48d832dd..787daceedf 100644 --- a/packages/desktop/src/renderer/src/components/editorWithTabs/tabs.vue +++ b/packages/desktop/src/renderer/src/components/editorWithTabs/tabs.vue @@ -42,7 +42,7 @@