From 4ac623b1bdae5b4b4f12c800901c115e41f33721 Mon Sep 17 00:00:00 2001 From: Andre Light <7719153+lightmotive@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:35:51 -0600 Subject: [PATCH] feat: opt-in toggle, editor-paste cooperation, and orphaned-setting fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #35 maintainer feedback: gate paste-time resolution behind a default-off toggle with 5-min TTL on lastCopyMeta. Switch from DOM paste (capture phase) to editor-paste workspace event with documented defaultPrevented yield protocol; register only when active. Also fixes a pre-existing bug where simplifiedHeadingToNoteLink was never read — the simplification fired unconditionally when filename matched heading. Designed, carefully reviewed, and edited by @lightmotive in collaboration with Claude Code. --- src/copyMetadata.test.ts | 27 +++++++ src/copyMetadata.ts | 4 + src/i18n.ts | 7 ++ src/linkBuilder.test.ts | 68 +++++++++++++++++ src/linkBuilder.ts | 7 +- src/main.ts | 85 ++++++++++++++++------ src/pasteResolution.test.ts | 141 ++++++++++++++++++++++++++++++++++++ src/pasteResolution.ts | 37 ++++++++++ src/settingTab.ts | 17 +++++ src/type.ts | 2 + 10 files changed, 372 insertions(+), 23 deletions(-) create mode 100644 src/pasteResolution.test.ts create mode 100644 src/pasteResolution.ts diff --git a/src/copyMetadata.test.ts b/src/copyMetadata.test.ts index cd76a2c..be3e0b0 100644 --- a/src/copyMetadata.test.ts +++ b/src/copyMetadata.test.ts @@ -70,6 +70,14 @@ describe('buildBlockCopyMetadata', () => { expect(buildBlockCopyMetadata({ ...base, autoEmbedBlockLink: true }).isEmbed).toBe(true); expect(buildBlockCopyMetadata({ ...base, autoEmbedBlockLink: false }).isEmbed).toBe(false); }); + + it('sets timestamp to current time', () => { + const before = Date.now(); + const meta = buildBlockCopyMetadata(base); + const after = Date.now(); + expect(meta.timestamp).toBeGreaterThanOrEqual(before); + expect(meta.timestamp).toBeLessThanOrEqual(after); + }); }); // --------------------------------------------------------------------------- @@ -174,6 +182,14 @@ describe('buildHeadingCopyMetadata', () => { it('isEmbed is always false for headings', () => { expect(buildHeadingCopyMetadata(base).isEmbed).toBe(false); }); + + it('sets timestamp to current time', () => { + const before = Date.now(); + const meta = buildHeadingCopyMetadata(base); + const after = Date.now(); + expect(meta.timestamp).toBeGreaterThanOrEqual(before); + expect(meta.timestamp).toBeLessThanOrEqual(after); + }); }); // --------------------------------------------------------------------------- @@ -225,4 +241,15 @@ describe('buildFileCopyMetadata', () => { expect(meta.clipboardText).toBe('[[a/b/c]]'); expect(meta.sourceFilePath).toBe('a/b/c.md'); }); + + it('sets timestamp to current time', () => { + const before = Date.now(); + const meta = buildFileCopyMetadata({ + clipboardText: '[[note]]', + sourceFilePath: 'notes/note.md', + }); + const after = Date.now(); + expect(meta.timestamp).toBeGreaterThanOrEqual(before); + expect(meta.timestamp).toBeLessThanOrEqual(after); + }); }); diff --git a/src/copyMetadata.ts b/src/copyMetadata.ts index 270c9f6..3b1efca 100644 --- a/src/copyMetadata.ts +++ b/src/copyMetadata.ts @@ -6,6 +6,7 @@ export interface CopyMetadata { subpath: string; alias: string; isEmbed: boolean; + timestamp: number; } export interface BuildBlockCopyMetadataInput { @@ -47,6 +48,7 @@ export function buildBlockCopyMetadata(input: BuildBlockCopyMetadataInput): Copy subpath: `#^${blockId}`, alias, isEmbed: autoEmbedBlockLink, + timestamp: Date.now(), }; } @@ -94,6 +96,7 @@ export function buildHeadingCopyMetadata(input: BuildHeadingCopyMetadataInput): subpath: isNoteLink ? '' : `#${sanitizeHeadingForLink(heading)}`, alias, isEmbed: false, + timestamp: Date.now(), }; } @@ -110,5 +113,6 @@ export function buildFileCopyMetadata(input: BuildFileCopyMetadataInput): CopyMe subpath: '', alias: input.displayText || '', isEmbed: false, + timestamp: Date.now(), }; } diff --git a/src/i18n.ts b/src/i18n.ts index 6e497f5..bc01344 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -18,6 +18,7 @@ export type TranslationKey = | 'strict-heading-match' | 'strict-heading-match-desc' | 'use-frontmatter-as-display' | 'use-frontmatter-as-display-desc' | 'frontmatter-key' | 'frontmatter-key-desc' | 'link-format'| 'link-format-desc' | 'link-format-obsidian' | 'markdown-link' | 'wiki-link' | 'contextual-copy' + | 'resolve-link-path-on-paste' | 'resolve-link-path-on-paste-desc' | 'copy-current-file-link' | 'file-link-copied' | 'target' | 'customize-targets' | 'customize-targets-desc' | 'enable-bold' | 'enable-bold-desc' @@ -107,6 +108,8 @@ export const translations: Record> = { 'link-format-obsidian': 'Follow Obsidian settings', 'markdown-link': 'Markdown link', 'wiki-link': 'Wiki link', + 'resolve-link-path-on-paste': 'Resolve link path on paste', + 'resolve-link-path-on-paste-desc': 'When using "Follow Obsidian settings", regenerate the link path (shortest/relative/absolute) at paste time based on the destination file. Won\'t interfere with other paste-handling plugins; if one handles the paste first, this feature may be bypassed depending on how that plugin handles the paste.', 'customize-targets': 'Customize targets', 'customize-targets-desc': 'Enable to customize which elements can be copied (disable to copy all elements)', 'enable-inline-code': 'Enable inline code', @@ -205,6 +208,8 @@ export const translations: Record> = { 'link-format-obsidian': '跟随 Obsidian 设置', 'markdown-link': 'Markdown链接', 'wiki-link': 'Wiki链接', + 'resolve-link-path-on-paste': '粘贴时解析链接路径', + 'resolve-link-path-on-paste-desc': '在使用"跟随 Obsidian 设置"时,根据目标文件在粘贴时重新生成链接路径(最短/相对/绝对)。不会干扰其他处理粘贴的插件;若有其他插件先处理粘贴,此功能可能被绕过,具体取决于该插件的处理方式。', 'customize-targets': '自定义复制对象', 'customize-targets-desc': '启用后可以自定义哪些元素可以被复制(不启用则默认可复制所有元素)', 'enable-inline-code': '启用行内代码', @@ -313,6 +318,8 @@ export const translations: Record> = { 'link-format-obsidian': '跟隨 Obsidian 設定', 'markdown-link': 'Markdown連結', 'wiki-link': 'Wiki連結', + 'resolve-link-path-on-paste': '貼上時解析連結路徑', + 'resolve-link-path-on-paste-desc': '在使用「跟隨 Obsidian 設定」時,根據目標檔案在貼上時重新生成連結路徑(最短/相對/絕對)。不會干擾其他處理貼上的外掛;若有其他外掛先處理貼上,此功能可能被略過,具體取決於該外掛的處理方式。', 'customize-targets': '自定義複製對象', 'customize-targets-desc': '啟用後可以自定義哪些元素可以被複製(不啟用則默认可複製所有元素)', 'enable-inline-code': '啟用行內代碼', diff --git a/src/linkBuilder.test.ts b/src/linkBuilder.test.ts index 6f04940..44a597f 100644 --- a/src/linkBuilder.test.ts +++ b/src/linkBuilder.test.ts @@ -77,6 +77,7 @@ describe('buildHeadingLink', () => { linkFormat: LinkFormat.WIKILINK, useHeadingAsDisplayText: true, headingLinkSeparator: '#', + simplifiedHeadingToNoteLink: true, }; // -- Wiki format --------------------------------------------------------- @@ -364,6 +365,73 @@ describe('buildHeadingLink', () => { }); }); + // -- simplifiedHeadingToNoteLink: false --------------------------------- + + describe('when simplifiedHeadingToNoteLink is false', () => { + const noSimplify = { ...defaults, simplifiedHeadingToNoteLink: false }; + + it('keeps full heading link when filename equals heading exactly (wiki)', () => { + const result = buildHeadingLink({ + ...noSimplify, + heading: 'MyNote', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote#MyNote|MyNote]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('keeps full heading link on case-insensitive match (wiki)', () => { + const result = buildHeadingLink({ + ...noSimplify, + heading: 'mynote', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote#mynote|mynote]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('keeps full heading link on space-removed match (wiki)', () => { + const result = buildHeadingLink({ + ...noSimplify, + heading: 'Some Thing', + filename: 'SomeThing', + }); + expect(result.link).toBe('[[SomeThing#Some Thing|Some Thing]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('keeps full heading link on filename-includes-heading match (wiki)', () => { + const result = buildHeadingLink({ + ...noSimplify, + heading: 'note', + filename: '260422_note', + }); + expect(result.link).toBe('[[260422_note#note|note]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('does not affect non-matching filename/heading (sanity check)', () => { + const result = buildHeadingLink({ + ...noSimplify, + heading: 'Getting Started', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote#Getting Started|Getting Started]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('markdown format always includes heading regardless (sanity check)', () => { + const result = buildHeadingLink({ + ...noSimplify, + linkFormat: LinkFormat.MDLINK, + heading: 'MyNote', + filename: 'MyNote', + }); + expect(result.link).toBe('[MyNote](MyNote#MyNote)'); + expect(result.isNoteLink).toBe(false); + }); + }); + // -- Combinatorial -------------------------------------------------------- describe('combinatorial', () => { diff --git a/src/linkBuilder.ts b/src/linkBuilder.ts index f5a0091..e68fd16 100644 --- a/src/linkBuilder.ts +++ b/src/linkBuilder.ts @@ -35,6 +35,7 @@ export interface BuildHeadingLinkOptions { useHeadingAsDisplayText: boolean; headingLinkSeparator: string; strictHeadingMatch?: boolean; + simplifiedHeadingToNoteLink: boolean; } export interface BuildHeadingLinkResult { @@ -43,7 +44,7 @@ export interface BuildHeadingLinkResult { } export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeadingLinkResult { - const { filename, linkFormat, useHeadingAsDisplayText, headingLinkSeparator, strictHeadingMatch } = options; + const { filename, linkFormat, useHeadingAsDisplayText, headingLinkSeparator, strictHeadingMatch, simplifiedHeadingToNoteLink } = options; const filenameOrTitle = options.frontmatterTitle || filename; // 提取标题文本和级别 @@ -72,7 +73,9 @@ export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeading // 特殊情况:如果文件名包含标题,则不添加指向标题的 # 部分 // 我自己的情况——会把 SomeThing 给拆成 Some Thing 来做标题,所以也考虑空格替换的部分 + // Gated on simplifiedHeadingToNoteLink so users can opt out and preserve the heading fragment. if ( + simplifiedHeadingToNoteLink && selectedHeading && (filename === selectedHeading || compareIgnoreCase(filename, selectedHeading) || @@ -87,7 +90,7 @@ export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeading // 根据设置选择链接格式 if (linkFormat === LinkFormat.WIKILINK) { // Wiki链接格式 - if (filename === selectedHeading) { + if (simplifiedHeadingToNoteLink && filename === selectedHeading) { // 特殊情况:当文件名与标题相同时,直接链接到文件 link = `[[${filename}]]`; isNoteLink = true; diff --git a/src/main.ts b/src/main.ts index 816ccf9..2fc3276 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,19 @@ -import { Editor, MarkdownView, Notice, Plugin, Menu, Platform, MarkdownFileInfo, TFile } from 'obsidian'; +import { Editor, EventRef, MarkdownView, Notice, Plugin, Menu, Platform, MarkdownFileInfo, TFile } from 'obsidian'; import { Language, TranslationKey, I18n } from './i18n'; import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkFormat, BlockIdInsertPosition } from './type'; import { EasyCopySettingTab } from './settingTab'; import { BlockIdInputModal } from './blockIdModal'; import { buildHeadingLink, buildBlockLink, buildFileLink } from './linkBuilder'; import { CopyMetadata, buildBlockCopyMetadata, buildHeadingCopyMetadata, buildFileCopyMetadata } from './copyMetadata'; +import { decidePasteResolution, shouldRegisterPasteHandler } from './pasteResolution'; + +const LAST_COPY_META_TTL_MS = 5 * 60 * 1000; export default class EasyCopy extends Plugin { settings: EasyCopySettings; i18n: I18n; private lastCopyMeta: CopyMetadata | null = null; + private pasteEventRef: EventRef | null = null; async onload() { await this.loadSettings(); @@ -118,13 +122,37 @@ export default class EasyCopy extends Plugin { this.lastCopyMeta = null; }); - // Paste-time path resolution: when "Follow Obsidian settings" is active, - // intercept paste to regenerate the link with correct path format - // (shortest/relative/absolute) based on the destination file. - // Uses DOM capture phase to intercept before Obsidian processes the paste. - this.registerDomEvent(document, 'paste', (evt: ClipboardEvent) => { - this.handlePaste(evt); - }, true); + // Paste-time path resolution: when enabled and "Follow Obsidian settings" + // is active, intercept paste to regenerate the link with correct path + // format (shortest/relative/absolute) based on the destination file. + // Uses Obsidian's editor-paste workspace event so we cooperate with + // other plugins (registration order, defaultPrevented handshake). + // Only registered when both conditions hold, so we're absent from the + // plugin chain entirely when the user has the feature off. + this.syncPasteHandlerRegistration(); + } + + syncPasteHandlerRegistration(): void { + if (shouldRegisterPasteHandler(this.settings)) { + this.registerPasteHandler(); + } else { + this.unregisterPasteHandler(); + } + } + + private registerPasteHandler(): void { + if (this.pasteEventRef) return; + this.pasteEventRef = this.app.workspace.on('editor-paste', (evt, editor, info) => { + this.handlePaste(evt, editor, info); + }); + this.registerEvent(this.pasteEventRef); + } + + private unregisterPasteHandler(): void { + if (!this.pasteEventRef) return; + this.app.workspace.offref(this.pasteEventRef); + this.pasteEventRef = null; + this.lastCopyMeta = null; } onunload() { @@ -143,38 +171,52 @@ export default class EasyCopy extends Plugin { * Intercept paste when the clipboard contains an Easy Copy link. * Uses Obsidian's generateMarkdownLink() to resolve the correct path * format (shortest/relative/absolute) for the destination file. + * + * Per the editor-paste contract (obsidian.d.ts): yield if another handler + * has already preventDefault'd, and call preventDefault to claim the event. */ - private handlePaste(evt: ClipboardEvent): void { - if (!this.lastCopyMeta) return; - if (this.settings.linkFormat !== LinkFormat.OBSIDIAN) return; - + private handlePaste(evt: ClipboardEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo): void { const clipboardText = evt.clipboardData?.getData('text/plain'); - if (clipboardText !== this.lastCopyMeta.clipboardText) { + const decision = decidePasteResolution({ + defaultPrevented: evt.defaultPrevented, + resolveLinkPathOnPaste: this.settings.resolveLinkPathOnPaste, + linkFormat: this.settings.linkFormat, + lastCopyMeta: this.lastCopyMeta, + clipboardText, + now: Date.now(), + ttlMs: LAST_COPY_META_TTL_MS, + }); + + if (decision === 'reset-and-skip') { this.lastCopyMeta = null; return; } + if (decision === 'skip') return; - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (!view?.file) return; + // decision === 'rewrite' — lastCopyMeta and clipboardText are non-null here. + const meta = this.lastCopyMeta!; - const sourceFile = this.app.vault.getAbstractFileByPath(this.lastCopyMeta.sourceFilePath); + const destFile = info.file; + if (!destFile) return; + + const sourceFile = this.app.vault.getAbstractFileByPath(meta.sourceFilePath); if (!(sourceFile instanceof TFile)) return; try { let link = this.app.fileManager.generateMarkdownLink( sourceFile, - view.file.path, - this.lastCopyMeta.subpath || undefined, - this.lastCopyMeta.alias || undefined, + destFile.path, + meta.subpath || undefined, + meta.alias || undefined, ); - if (this.lastCopyMeta.isEmbed) { + if (meta.isEmbed) { link = '!' + link; } if (link !== clipboardText) { evt.preventDefault(); - view.editor.replaceSelection(link); + editor.replaceSelection(link); } } catch { // generateMarkdownLink failed — let normal paste proceed @@ -644,6 +686,7 @@ export default class EasyCopy extends Plugin { useHeadingAsDisplayText: this.settings.useHeadingAsDisplayText, headingLinkSeparator: this.settings.headingLinkSeparator, strictHeadingMatch: this.settings.strictHeadingMatch, + simplifiedHeadingToNoteLink: this.settings.simplifiedHeadingToNoteLink, }); navigator.clipboard.writeText(link); diff --git a/src/pasteResolution.test.ts b/src/pasteResolution.test.ts new file mode 100644 index 0000000..d3ddeb5 --- /dev/null +++ b/src/pasteResolution.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect } from 'vitest'; +import { decidePasteResolution, PasteResolutionInput, shouldRegisterPasteHandler } from './pasteResolution'; +import { CopyMetadata } from './copyMetadata'; +import { LinkFormat } from './type'; + +describe('shouldRegisterPasteHandler', () => { + it('returns true when toggle is on AND linkFormat is OBSIDIAN', () => { + expect(shouldRegisterPasteHandler({ + resolveLinkPathOnPaste: true, + linkFormat: LinkFormat.OBSIDIAN, + })).toBe(true); + }); + + it('returns false when toggle is off', () => { + expect(shouldRegisterPasteHandler({ + resolveLinkPathOnPaste: false, + linkFormat: LinkFormat.OBSIDIAN, + })).toBe(false); + }); + + it('returns false when linkFormat is MDLINK even if toggle is on', () => { + expect(shouldRegisterPasteHandler({ + resolveLinkPathOnPaste: true, + linkFormat: LinkFormat.MDLINK, + })).toBe(false); + }); + + it('returns false when linkFormat is WIKILINK even if toggle is on', () => { + expect(shouldRegisterPasteHandler({ + resolveLinkPathOnPaste: true, + linkFormat: LinkFormat.WIKILINK, + })).toBe(false); + }); + + it('returns false when both conditions fail', () => { + expect(shouldRegisterPasteHandler({ + resolveLinkPathOnPaste: false, + linkFormat: LinkFormat.MDLINK, + })).toBe(false); + }); +}); + +const META: CopyMetadata = { + clipboardText: '[[note#Heading]]', + sourceFilePath: 'notes/note.md', + subpath: '#Heading', + alias: 'Heading', + isEmbed: false, + timestamp: 1_000_000, +}; + +const TTL = 5 * 60 * 1000; + +const baseInput: PasteResolutionInput = { + defaultPrevented: false, + resolveLinkPathOnPaste: true, + linkFormat: LinkFormat.OBSIDIAN, + lastCopyMeta: META, + clipboardText: META.clipboardText, + now: META.timestamp + 1000, + ttlMs: TTL, +}; + +describe('decidePasteResolution', () => { + it('returns rewrite when all guards pass', () => { + expect(decidePasteResolution(baseInput)).toBe('rewrite'); + }); + + it('skips when another handler already preventDefault\'d', () => { + expect(decidePasteResolution({ ...baseInput, defaultPrevented: true })).toBe('skip'); + }); + + it('skips when the toggle is off', () => { + expect(decidePasteResolution({ ...baseInput, resolveLinkPathOnPaste: false })).toBe('skip'); + }); + + it('skips when linkFormat is not OBSIDIAN', () => { + expect(decidePasteResolution({ ...baseInput, linkFormat: LinkFormat.MDLINK })).toBe('skip'); + expect(decidePasteResolution({ ...baseInput, linkFormat: LinkFormat.WIKILINK })).toBe('skip'); + }); + + it('skips when there is no lastCopyMeta', () => { + expect(decidePasteResolution({ ...baseInput, lastCopyMeta: null })).toBe('skip'); + }); + + it('resets and skips when meta is older than TTL', () => { + expect(decidePasteResolution({ + ...baseInput, + now: META.timestamp + TTL + 1, + })).toBe('reset-and-skip'); + }); + + it('does not consider meta stale at exactly the TTL boundary', () => { + expect(decidePasteResolution({ + ...baseInput, + now: META.timestamp + TTL, + })).toBe('rewrite'); + }); + + it('resets and skips when clipboard text differs from copied text', () => { + expect(decidePasteResolution({ + ...baseInput, + clipboardText: 'something else entirely', + })).toBe('reset-and-skip'); + }); + + it('resets and skips when clipboard text is undefined', () => { + expect(decidePasteResolution({ + ...baseInput, + clipboardText: undefined, + })).toBe('reset-and-skip'); + }); + + it('defaultPrevented takes precedence over toggle and other state', () => { + expect(decidePasteResolution({ + ...baseInput, + defaultPrevented: true, + resolveLinkPathOnPaste: false, + lastCopyMeta: null, + })).toBe('skip'); + }); + + it('toggle takes precedence over linkFormat and meta state', () => { + expect(decidePasteResolution({ + ...baseInput, + resolveLinkPathOnPaste: false, + linkFormat: LinkFormat.MDLINK, + lastCopyMeta: null, + })).toBe('skip'); + }); + + it('TTL check runs before clipboard match', () => { + // Stale meta with mismatched clipboard text — TTL should fire first, + // resulting in reset-and-skip either way, but staleness is the cause. + expect(decidePasteResolution({ + ...baseInput, + now: META.timestamp + TTL + 1, + clipboardText: 'mismatch', + })).toBe('reset-and-skip'); + }); +}); diff --git a/src/pasteResolution.ts b/src/pasteResolution.ts new file mode 100644 index 0000000..70cf57f --- /dev/null +++ b/src/pasteResolution.ts @@ -0,0 +1,37 @@ +import { CopyMetadata } from './copyMetadata'; +import { EasyCopySettings, LinkFormat } from './type'; + +/** + * Whether the editor-paste handler should be registered for the given settings. + * The bridge between settings state and "is Easy Copy in the paste plugin chain." + */ +export function shouldRegisterPasteHandler( + settings: Pick, +): boolean { + return settings.resolveLinkPathOnPaste && settings.linkFormat === LinkFormat.OBSIDIAN; +} + +export type PasteResolutionAction = + | 'skip' // do nothing, leave lastCopyMeta as-is + | 'reset-and-skip' // null out lastCopyMeta, do nothing else + | 'rewrite'; // proceed to regenerate and substitute the link + +export interface PasteResolutionInput { + defaultPrevented: boolean; + resolveLinkPathOnPaste: boolean; + linkFormat: LinkFormat; + lastCopyMeta: CopyMetadata | null; + clipboardText: string | undefined; + now: number; + ttlMs: number; +} + +export function decidePasteResolution(input: PasteResolutionInput): PasteResolutionAction { + if (input.defaultPrevented) return 'skip'; + if (!input.resolveLinkPathOnPaste) return 'skip'; + if (input.linkFormat !== LinkFormat.OBSIDIAN) return 'skip'; + if (!input.lastCopyMeta) return 'skip'; + if (input.now - input.lastCopyMeta.timestamp > input.ttlMs) return 'reset-and-skip'; + if (input.clipboardText !== input.lastCopyMeta.clipboardText) return 'reset-and-skip'; + return 'rewrite'; +} diff --git a/src/settingTab.ts b/src/settingTab.ts index dba644e..bf09b9d 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -119,8 +119,25 @@ export class EasyCopySettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.linkFormat = value as LinkFormat; await this.plugin.saveSettings(); + this.plugin.syncPasteHandlerRegistration(); + this.display(); }))); + // Only relevant when linkFormat is OBSIDIAN; the resolver intercepts paste + // to regenerate the path (shortest/relative/absolute) for the destination file. + if (this.plugin.settings.linkFormat === LinkFormat.OBSIDIAN) { + formatGroup.addSetting(setting => setting + .setName(this.plugin.t('resolve-link-path-on-paste')) + .setDesc(this.plugin.t('resolve-link-path-on-paste-desc')) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.resolveLinkPathOnPaste) + .onChange(async (value) => { + this.plugin.settings.resolveLinkPathOnPaste = value; + await this.plugin.saveSettings(); + this.plugin.syncPasteHandlerRegistration(); + }))); + } + formatGroup.addSetting(setting => setting .setName(this.plugin.t('use-heading-as-display')) .setDesc(this.plugin.t('use-heading-as-display-desc')) diff --git a/src/type.ts b/src/type.ts index 85ebef2..df7268f 100644 --- a/src/type.ts +++ b/src/type.ts @@ -44,6 +44,7 @@ export interface EasyCopySettings { simplifiedHeadingToNoteLink: boolean; // 是否简化标题到笔记链接 strictHeadingMatch: boolean; // 标题与文件名简化匹配时是否使用严格匹配 linkFormat: LinkFormat; + resolveLinkPathOnPaste: boolean; // 当 linkFormat 为 OBSIDIAN 时,粘贴时根据目标文件重新解析链接路径 customizeTargets: boolean; enableInlineCode: boolean; enableBold: boolean; @@ -76,6 +77,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = { simplifiedHeadingToNoteLink: true, strictHeadingMatch: false, linkFormat: LinkFormat.OBSIDIAN, + resolveLinkPathOnPaste: false, customizeTargets: false, enableInlineCode: true, enableBold: true,