feat: allow built-in matcher reordering

This commit is contained in:
Moy 2026-06-10 12:38:39 +08:00
parent 0bfb72a41d
commit 3e72472a4d
5 changed files with 120 additions and 16 deletions

View file

@ -1,13 +1,14 @@
import { ContextType, EasyCopySettings } from './type';
import { BuiltinCopyMatcherId, ContextType, EasyCopySettings } from './type';
export type BuiltinCopyMatcherId =
| 'bold'
| 'italic'
| 'highlight'
| 'strikethrough'
| 'inline-code'
| 'inline-latex'
| 'wiki-link';
export const DEFAULT_BUILTIN_COPY_MATCHER_IDS: BuiltinCopyMatcherId[] = [
'bold',
'italic',
'highlight',
'strikethrough',
'inline-code',
'inline-latex',
'wiki-link',
];
export interface CopyMatcher {
id: BuiltinCopyMatcherId;
@ -16,6 +17,23 @@ export interface CopyMatcher {
enabled: boolean;
}
export function normalizeBuiltinMatcherOrder(order: readonly string[] | undefined): BuiltinCopyMatcherId[] {
const knownIds = new Set<string>(DEFAULT_BUILTIN_COPY_MATCHER_IDS);
const orderedIds: BuiltinCopyMatcherId[] = [];
for (const id of order ?? []) {
if (knownIds.has(id) && !orderedIds.includes(id as BuiltinCopyMatcherId)) {
orderedIds.push(id as BuiltinCopyMatcherId);
}
}
for (const id of DEFAULT_BUILTIN_COPY_MATCHER_IDS) {
if (!orderedIds.includes(id)) orderedIds.push(id);
}
return orderedIds;
}
export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: boolean): CopyMatcher[] {
// iOS 16.4 之前不支持后视Lookbehinds但支持前视Lookaheads
// 所以针对 iOS 平台使用只带前视的正则表达式,其他平台使用完整版本
@ -25,7 +43,7 @@ export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: b
const boldRegex = /(?:\*\*([^*]+)\*\*|__([^_]+)__)/g;
return [
const matchers: CopyMatcher[] = [
{ id: 'bold', type: ContextType.BOLD, regex: boldRegex, enabled: !settings.customizeTargets || settings.enableBold },
{ id: 'italic', type: ContextType.ITALIC, regex: italicRegex, enabled: !settings.customizeTargets || settings.enableItalic },
{ id: 'highlight', type: ContextType.HIGHLIGHT, regex: /==([^=]+)==/g, enabled: !settings.customizeTargets || settings.enableHighlight },
@ -34,4 +52,7 @@ export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: b
{ id: 'inline-latex', type: ContextType.INLINELATEX, regex: /\$([^$]+)\$/g, enabled: !settings.customizeTargets || settings.enableInlineLatex },
{ id: 'wiki-link', type: ContextType.WIKILINK, regex: /\[\[([^\]]+)\]\]/g, enabled: !settings.customizeTargets || settings.enableWikiLink },
];
const matcherById = new Map(matchers.map(matcher => [matcher.id, matcher]));
return normalizeBuiltinMatcherOrder(settings.matcherOrder).map(id => matcherById.get(id)!);
}

View file

@ -49,7 +49,8 @@ export type TranslationKey =
| 'error-block-id-empty' | 'error-block-id-invalid'
| 'enable-display-name-regex' | 'enable-display-name-regex-desc'
| 'display-name-regex-from' | 'display-name-regex-from-desc'
| 'display-name-regex-to' | 'display-name-regex-to-desc';
| 'display-name-regex-to' | 'display-name-regex-to-desc'
| 'matcher-priority' | 'matcher-priority-desc' | 'move-up' | 'move-down' | 'reset-order';
// 本地化翻译字典
export const translations: Record<Language, Record<TranslationKey, string>> = {
@ -172,6 +173,11 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
'display-name-regex-from-desc': 'Regular expression pattern to match in the display text (e.g. ^\\d+\\.\\s* to remove leading numbers)',
'display-name-regex-to': 'Replace with',
'display-name-regex-to-desc': 'Replacement string, supports capture groups like $1, $2 (e.g. $1 to keep only the first captured group)',
'matcher-priority': 'Copy target priority',
'matcher-priority-desc': 'When multiple targets match the cursor, targets higher in this list are copied first.',
'move-up': 'Move up',
'move-down': 'Move down',
'reset-order': 'Reset order',
},
[Language.ZH]: {
// 复制 Block ID
@ -290,6 +296,11 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
'display-name-regex-from-desc': '用于匹配显示文本的正则表达式(如 ^\\d+\\.\\s* 可去除开头的序号)',
'display-name-regex-to': '替换为',
'display-name-regex-to-desc': '替换字符串,支持捕获组引用如 $1、$2如 $1 只保留第一个捕获组的内容)',
'matcher-priority': '复制对象优先级',
'matcher-priority-desc': '当多种复制对象同时匹配光标位置时,列表中更靠上的对象会优先生效。',
'move-up': '上移',
'move-down': '下移',
'reset-order': '重置顺序',
},
[Language.ZH_TW]: {
// 复制 Block ID
@ -409,6 +420,11 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
'display-name-regex-from-desc': '用於匹配顯示文本的正規表示式(如 ^\\d+\\.\\s* 可去除開頭的序號)',
'display-name-regex-to': '替換為',
'display-name-regex-to-desc': '替換字符串,支持捕獲組引用如 $1、$2',
'matcher-priority': '複製對象優先級',
'matcher-priority-desc': '當多種複製對象同時匹配游標位置時,列表中更靠上的對象會優先生效。',
'move-up': '上移',
'move-down': '下移',
'reset-order': '重置順序',
}
};

View file

@ -4,7 +4,7 @@ import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkForma
import { EasyCopySettingTab } from './settingTab';
import { BlockIdInputModal } from './blockIdModal';
import { detectCodeBlockFromLines } from './codeBlockDetect';
import { buildBuiltinCopyMatchers } from './copyMatcher';
import { buildBuiltinCopyMatchers, normalizeBuiltinMatcherOrder } from './copyMatcher';
import { buildHeadingLink, buildBlockLink, buildFileLink, buildExplicitPasteLink } from './linkBuilder';
import { CopyMetadata, buildBlockCopyMetadata, buildHeadingCopyMetadata, buildFileCopyMetadata } from './copyMetadata';
import { decidePasteResolution, shouldOmitAliasForSameFile, shouldRegisterPasteHandler } from './pasteResolution';
@ -161,6 +161,7 @@ export default class EasyCopy extends Plugin {
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings.matcherOrder = normalizeBuiltinMatcherOrder(this.settings.matcherOrder);
}
async saveSettings() {

View file

@ -7,12 +7,23 @@ import {
} from "obsidian";
import * as ObsidianModule from "obsidian";
import EasyCopy from "./main";
import { LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type";
import { BuiltinCopyMatcherId, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type";
import { DEFAULT_BUILTIN_COPY_MATCHER_IDS, normalizeBuiltinMatcherOrder } from "./copyMatcher";
interface SettingsContainer {
addSetting(cb: (setting: Setting) => void): void;
}
const BUILTIN_MATCHER_LABEL_KEYS: Record<BuiltinCopyMatcherId, 'enable-bold' | 'enable-italic' | 'enable-highlight' | 'enable-strikethrough' | 'enable-inline-code' | 'enable-inline-latex' | 'enable-wikilink'> = {
'bold': 'enable-bold',
'italic': 'enable-italic',
'highlight': 'enable-highlight',
'strikethrough': 'enable-strikethrough',
'inline-code': 'enable-inline-code',
'inline-latex': 'enable-inline-latex',
'wiki-link': 'enable-wikilink',
};
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
@ -345,6 +356,8 @@ export class EasyCopySettingTab extends PluginSettingTab {
// 只有当自定义复制对象选项开启时才显示具体的复制对象选项
if (this.plugin.settings.customizeTargets) {
this.plugin.settings.matcherOrder = normalizeBuiltinMatcherOrder(this.plugin.settings.matcherOrder);
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-inline-code'))
.setDesc(this.plugin.t('enable-inline-code-desc'))
@ -424,8 +437,38 @@ export class EasyCopySettingTab extends PluginSettingTab {
this.plugin.settings.enableWikiLink = value;
void this.plugin.saveSettings();
this.display(); // 切换后刷新界面以显示/隐藏下方选项
})));
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('matcher-priority'))
.setDesc(this.plugin.t('matcher-priority-desc'))
.addButton(button => button
.setButtonText(this.plugin.t('reset-order'))
.onClick(() => {
this.plugin.settings.matcherOrder = [...DEFAULT_BUILTIN_COPY_MATCHER_IDS];
void this.plugin.saveSettings();
this.display();
})));
this.plugin.settings.matcherOrder.forEach((matcherId, index) => {
targetGroup.addSetting(setting => setting
.setName(this.plugin.t(BUILTIN_MATCHER_LABEL_KEYS[matcherId]))
.addButton(button => button
.setButtonText('↑')
.setTooltip(this.plugin.t('move-up'))
.setDisabled(index === 0)
.onClick(() => {
this.moveMatcher(matcherId, -1);
}))
.addButton(button => button
.setButtonText('↓')
.setTooltip(this.plugin.t('move-down'))
.setDisabled(index === this.plugin.settings.matcherOrder.length - 1)
.onClick(() => {
this.moveMatcher(matcherId, 1);
})));
});
}
targetGroup.addSetting(setting => setting
@ -514,4 +557,16 @@ export class EasyCopySettingTab extends PluginSettingTab {
})));
}
}
private moveMatcher(matcherId: BuiltinCopyMatcherId, direction: -1 | 1): void {
const matcherOrder = normalizeBuiltinMatcherOrder(this.plugin.settings.matcherOrder);
const currentIndex = matcherOrder.indexOf(matcherId);
const nextIndex = currentIndex + direction;
if (nextIndex < 0 || nextIndex >= matcherOrder.length) return;
[matcherOrder[currentIndex], matcherOrder[nextIndex]] = [matcherOrder[nextIndex], matcherOrder[currentIndex]];
this.plugin.settings.matcherOrder = matcherOrder;
void this.plugin.saveSettings();
this.display();
}
}

View file

@ -34,6 +34,15 @@ export enum ContextType {
CODEBLOCK = 'code-block', // 光标在代码块内
}
export type BuiltinCopyMatcherId =
| 'bold'
| 'italic'
| 'highlight'
| 'strikethrough'
| 'inline-code'
| 'inline-latex'
| 'wiki-link';
export interface ContextData {
type: ContextType;
curLine: string;
@ -62,6 +71,7 @@ export interface EasyCopySettings {
enableInlineLatex: boolean;
enableLink: boolean;
enableWikiLink: boolean; // 是否启用 Wiki 链接复制
matcherOrder: BuiltinCopyMatcherId[]; // 复制对象匹配优先级
keepWikiBrackets: boolean; // 复制 wiki-link 时保留 [[ ]]
autoEmbedBlockLink: boolean; // 复制块链接时自动添加 !(嵌入块)
enableCalloutCopy: boolean; // 是否启用复制 Callout 内文本
@ -99,6 +109,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = {
enableInlineLatex: true,
enableLink: true,
enableWikiLink: true,
matcherOrder: ['bold', 'italic', 'highlight', 'strikethrough', 'inline-code', 'inline-latex', 'wiki-link'],
keepWikiBrackets: true,
autoEmbedBlockLink: false,
enableCalloutCopy: true,
@ -113,4 +124,4 @@ export const DEFAULT_SETTINGS: EasyCopySettings = {
enableDisplayNameRegex: false,
displayNameRegexFrom: '',
displayNameRegexTo: '',
}
}