mirror of
https://github.com/moyf/easy-copy.git
synced 2026-07-22 05:43:47 +00:00
Previously the paste handler only fired when linkFormat was "Follow Obsidian settings". Users on explicit Wiki or Markdown got no paste-time cleanup — pasting a heading into the same file produced the verbose [[Filename#Heading|Heading]] instead of [[#Heading]]. Drop the OBSIDIAN-only gate in shouldRegisterPasteHandler and decidePasteResolution. handlePaste branches by linkFormat: OBSIDIAN keeps using app.fileManager.generateMarkdownLink (full newLinkFormat support); explicit Wiki/Markdown uses app.metadataCache.fileToLinktext (shortest-unique paths only). The same-file alias-redundancy fix from the previous commit applies to both branches. Add buildExplicitPasteLink in linkBuilder.ts as a pure string-producer for the explicit-format branch — caller decides omitAlias via shouldOmitAliasForSameFile, formatter consumes the boolean. Toggle now shown for all three linkFormat options; description updated to reflect format-aware behavior across en/zh/zh-tw. Tests: new buildExplicitPasteLink suite covering wiki/md, same-file vs cross-file, block links, and encoding behavior. pasteResolution tests updated for the broadened gate (linkFormat field removed from PasteResolutionInput; OBSIDIAN-only assertions dropped). Trade-off: explicit Wiki/Markdown get shortest-path-only resolution; users wanting relative/absolute path styles keep using "Follow Obsidian settings" (which honors the vault's newLinkFormat config). Designed, carefully reviewed, and edited by @lightmotive in collaboration with Claude Code.
457 lines
17 KiB
TypeScript
457 lines
17 KiB
TypeScript
import {
|
||
App,
|
||
PluginSettingTab,
|
||
Setting,
|
||
requireApiVersion,
|
||
} from "obsidian";
|
||
import * as ObsidianModule from "obsidian";
|
||
import EasyCopy from "./main";
|
||
import { LinkFormat, BlockIdInsertPosition } from "./type";
|
||
|
||
interface SettingsContainer {
|
||
addSetting(cb: (setting: Setting) => void): void;
|
||
}
|
||
|
||
function createSettingsGroup(containerEl: HTMLElement, heading?: string): SettingsContainer {
|
||
// Check if SettingGroup is available (API 1.11.0+)
|
||
// requireApiVersion is the official Obsidian API method for version checking
|
||
if (requireApiVersion('1.11.0')) {
|
||
// Use SettingGroup - it's guaranteed to exist if requireApiVersion returns true
|
||
// Access SettingGroup via type assertion since it may not be in type definitions
|
||
// for older TypeScript versions, but exists at runtime in Obsidian 1.11.0+
|
||
// Using unknown instead of any to satisfy eslint while maintaining type safety
|
||
const SettingGroupClass = (ObsidianModule as unknown as { SettingGroup?: new (containerEl: HTMLElement) => {
|
||
setHeading(heading: string): {
|
||
addSetting(cb: (setting: Setting) => void): void;
|
||
};
|
||
addSetting(cb: (setting: Setting) => void): void;
|
||
} }).SettingGroup;
|
||
|
||
if (SettingGroupClass) {
|
||
const group = heading
|
||
? new SettingGroupClass(containerEl).setHeading(heading)
|
||
: new SettingGroupClass(containerEl);
|
||
return {
|
||
addSetting(cb: (setting: Setting) => void) {
|
||
group.addSetting(cb);
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
// Fallback path (either API < 1.11.0 or SettingGroup not found)
|
||
{
|
||
// Fallback: Create a heading manually for older API versions
|
||
// Note: While best practice prefers Setting.setHeading(), the fallback path
|
||
// is for versions that may not support it, so manual heading is appropriate here
|
||
if (heading) {
|
||
const headingEl = containerEl.createDiv('setting-group-heading');
|
||
headingEl.createEl('h3', { text: heading });
|
||
}
|
||
|
||
return {
|
||
addSetting(cb: (setting: Setting) => void) {
|
||
const setting = new Setting(containerEl);
|
||
cb(setting);
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
|
||
export class EasyCopySettingTab extends PluginSettingTab {
|
||
plugin: EasyCopy;
|
||
|
||
icon: string = 'copy-plus';
|
||
|
||
constructor(app: App, plugin: EasyCopy) {
|
||
super(app, plugin);
|
||
this.plugin = plugin;
|
||
}
|
||
|
||
display(): void {
|
||
const {containerEl} = this;
|
||
|
||
containerEl.empty();
|
||
|
||
const generalGroup = createSettingsGroup(containerEl);
|
||
|
||
generalGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('add-to-menu'))
|
||
.setDesc(this.plugin.t('add-to-menu-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.addToMenu)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.addToMenu = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
generalGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('add-extra-commands'))
|
||
.setDesc(this.plugin.t('add-extra-commands-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.addExtraCommands)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.addExtraCommands = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
generalGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('show-notice'))
|
||
.setDesc(this.plugin.t('show-notice-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.showNotice)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.showNotice = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
const formatGroup = createSettingsGroup(containerEl, this.plugin.t('format'));
|
||
|
||
formatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('link-format'))
|
||
.setDesc(this.plugin.t('link-format-desc'))
|
||
.addDropdown(dropdown => dropdown
|
||
.addOption(LinkFormat.OBSIDIAN, this.plugin.t('link-format-obsidian'))
|
||
.addOption(LinkFormat.MDLINK, this.plugin.t('markdown-link'))
|
||
.addOption(LinkFormat.WIKILINK, this.plugin.t('wiki-link'))
|
||
.setValue(this.plugin.settings.linkFormat)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.linkFormat = value as LinkFormat;
|
||
await this.plugin.saveSettings();
|
||
this.plugin.syncPasteHandlerRegistration();
|
||
this.display();
|
||
})));
|
||
|
||
// The resolver intercepts paste to regenerate the link target for the
|
||
// destination. Under "Follow Obsidian settings" it uses the vault's
|
||
// path style (shortest/relative/absolute); under explicit Wiki/Markdown
|
||
// it falls back to shortest-unique paths only.
|
||
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'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.useHeadingAsDisplayText)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.useHeadingAsDisplayText = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})));
|
||
|
||
// 新增:标题链接连接符设置,仅在禁用"使用标题作为显示文本"时显示
|
||
if (!this.plugin.settings.useHeadingAsDisplayText) {
|
||
formatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('heading-link-separator'))
|
||
.setDesc(this.plugin.t('heading-link-separator-desc'))
|
||
.addText(text => text
|
||
.setPlaceholder('#')
|
||
.setValue(this.plugin.settings.headingLinkSeparator)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.headingLinkSeparator = value || '#';
|
||
await this.plugin.saveSettings();
|
||
})
|
||
));
|
||
}
|
||
|
||
// 后续新增:文件名包含标题时,简化为复制文件链接(通常用于复制一级标题时)
|
||
// Under OBSIDIAN, defer format/path choice to Obsidian's vault config —
|
||
// Obsidian's own generateMarkdownLink never auto-collapses heading links
|
||
// to file links, so this opt-in is hidden+inert there. Stored values
|
||
// persist across format toggles (matches resolveLinkPathOnPaste).
|
||
if (this.plugin.settings.linkFormat !== LinkFormat.OBSIDIAN) {
|
||
formatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('simplified-heading-to-note-link'))
|
||
.setDesc(this.plugin.t('simplified-heading-to-note-link-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.simplifiedHeadingToNoteLink)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.simplifiedHeadingToNoteLink = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})));
|
||
|
||
if (this.plugin.settings.simplifiedHeadingToNoteLink) {
|
||
formatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('strict-heading-match'))
|
||
.setDesc(this.plugin.t('strict-heading-match-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.strictHeadingMatch)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.strictHeadingMatch = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
}
|
||
}
|
||
|
||
|
||
// 新增:是否使用 frontmatter 属性作为显示文本
|
||
formatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('use-frontmatter-as-display'))
|
||
.setDesc(this.plugin.t('use-frontmatter-as-display-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.useFrontmatterAsDisplay)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.useFrontmatterAsDisplay = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})
|
||
));
|
||
|
||
// 新增:自定义 frontmatter 属性名,仅在上方开启时显示
|
||
if (this.plugin.settings.useFrontmatterAsDisplay) {
|
||
formatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('frontmatter-key'))
|
||
.setDesc(this.plugin.t('frontmatter-key-desc'))
|
||
.addText(text => text
|
||
.setPlaceholder('title')
|
||
.setValue(this.plugin.settings.frontmatterKey)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.frontmatterKey = value || 'title';
|
||
await this.plugin.saveSettings();
|
||
})
|
||
));
|
||
}
|
||
|
||
|
||
const blockIdGroup = createSettingsGroup(containerEl, this.plugin.t('block-id'));
|
||
|
||
blockIdGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('auto-add-block-id'))
|
||
.setDesc(this.plugin.t('auto-add-block-id-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.autoAddBlockId)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.autoAddBlockId = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})));
|
||
|
||
if (this.plugin.settings.autoAddBlockId) {
|
||
// 新增:块ID插入位置设置
|
||
blockIdGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('block-id-insert-position'))
|
||
.setDesc(this.plugin.t('block-id-insert-position-desc'))
|
||
.addDropdown(dropdown => dropdown
|
||
.addOption(BlockIdInsertPosition.END_OF_BLOCK, this.plugin.t('block-id-end-of-block'))
|
||
.addOption(BlockIdInsertPosition.NEXT_LINE, this.plugin.t('block-id-next-line'))
|
||
.setValue(this.plugin.settings.blockIdInsertPosition)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.blockIdInsertPosition = value as BlockIdInsertPosition;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
}
|
||
|
||
if (this.plugin.settings.autoAddBlockId) {
|
||
blockIdGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('manual-block-id'))
|
||
.setDesc(this.plugin.t('manual-block-id-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.allowManualBlockId)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.allowManualBlockId = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
}
|
||
|
||
// 新增:自动为 Block 链接添加显示文本
|
||
blockIdGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('auto-block-display-text'))
|
||
.setDesc(this.plugin.t('auto-block-display-text-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.autoBlockDisplayText)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.autoBlockDisplayText = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})
|
||
));
|
||
|
||
// 新增:块显示文本限制设置,仅在启用 autoBlockDisplayText 时显示
|
||
if (this.plugin.settings.autoBlockDisplayText) {
|
||
blockIdGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('block-display-word-limit'))
|
||
.setDesc(this.plugin.t('block-display-word-limit-desc'))
|
||
.addText(text => text
|
||
.setPlaceholder('3')
|
||
.setValue(String(this.plugin.settings.blockDisplayWordLimit))
|
||
.onChange(async (value) => {
|
||
const numValue = parseInt(value) || 3;
|
||
this.plugin.settings.blockDisplayWordLimit = Math.max(1, numValue);
|
||
await this.plugin.saveSettings();
|
||
})
|
||
));
|
||
|
||
blockIdGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('block-display-char-limit'))
|
||
.setDesc(this.plugin.t('block-display-char-limit-desc'))
|
||
.addText(text => text
|
||
.setPlaceholder('5')
|
||
.setValue(String(this.plugin.settings.blockDisplayCharLimit))
|
||
.onChange(async (value) => {
|
||
const numValue = parseInt(value) || 5;
|
||
this.plugin.settings.blockDisplayCharLimit = Math.max(1, numValue);
|
||
await this.plugin.saveSettings();
|
||
})
|
||
));
|
||
}
|
||
|
||
const targetGroup = createSettingsGroup(containerEl, this.plugin.t('target'));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('customize-targets'))
|
||
.setDesc(this.plugin.t('customize-targets-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.customizeTargets)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.customizeTargets = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})));
|
||
|
||
// 只有当自定义复制对象选项开启时才显示具体的复制对象选项
|
||
if (this.plugin.settings.customizeTargets) {
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-inline-code'))
|
||
.setDesc(this.plugin.t('enable-inline-code-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableInlineCode)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableInlineCode = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-bold'))
|
||
.setDesc(this.plugin.t('enable-bold-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableBold)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableBold = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-highlight'))
|
||
.setDesc(this.plugin.t('enable-highlight-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableHighlight)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableHighlight = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-italic'))
|
||
.setDesc(this.plugin.t('enable-italic-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableItalic)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableItalic = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-strikethrough'))
|
||
.setDesc(this.plugin.t('enable-strikethrough-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableStrikethrough)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableStrikethrough = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-inline-latex'))
|
||
.setDesc(this.plugin.t('enable-inline-latex-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableInlineLatex)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableInlineLatex = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-link'))
|
||
.setDesc(this.plugin.t('enable-link-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableLink)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableLink = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-wikilink'))
|
||
.setDesc(this.plugin.t('enable-wikilink-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableWikiLink ?? true)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableWikiLink = value;
|
||
await this.plugin.saveSettings();
|
||
this.display(); // 切换后刷新界面以显示/隐藏下方选项
|
||
})));
|
||
|
||
}
|
||
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('enable-callout-copy'))
|
||
.setDesc(this.plugin.t('enable-callout-copy-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableCalloutCopy ?? true)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableCalloutCopy = value;
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
})));
|
||
// 优先复制 Callout 内容
|
||
if (this.plugin.settings.enableCalloutCopy) {
|
||
targetGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('callout-copy-priority'))
|
||
.setDesc(this.plugin.t('callout-copy-priority-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.calloutCopyPriority ?? true)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calloutCopyPriority = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
}
|
||
|
||
|
||
const specialFormatGroup = createSettingsGroup(containerEl, this.plugin.t('special-format'));
|
||
|
||
// 块链接特殊格式选项
|
||
specialFormatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('auto-embed-block-link'))
|
||
.setDesc(this.plugin.t('auto-embed-block-link-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.autoEmbedBlockLink ?? false)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.autoEmbedBlockLink = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
|
||
// 仅当启用 Wiki 链接复制时显示
|
||
if (this.plugin.settings.enableWikiLink) {
|
||
specialFormatGroup.addSetting(setting => setting
|
||
.setName(this.plugin.t('keep-wiki-brackets'))
|
||
.setDesc(this.plugin.t('keep-wiki-brackets-desc'))
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.keepWikiBrackets ?? true)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.keepWikiBrackets = value;
|
||
await this.plugin.saveSettings();
|
||
})));
|
||
}
|
||
}
|
||
}
|