Skip to content

Commit 8a8dec0

Browse files
committed
feat(settings): add search functionality with fuzzy matching
Implement comprehensive settings search feature with: - Real-time fuzzy search using Obsidian's prepareFuzzySearch API - Intelligent navigation to specific settings with scroll and highlight - High-performance indexing of 100+ settings with lazy loading - Keyboard navigation support (arrow keys, enter, escape) - Minimal design following Obsidian UI principles - Multi-language search support with translation integration Components added: - SettingsSearchComponent: Main search interface with dropdown results - SettingsIndexer: High-performance search indexing with <5ms build time - Static metadata for all settings with keywords and categories - CSS styling for search interface with responsive design The search appears at the top of settings page and allows users to quickly find and jump to any setting across all tabs and categories.
1 parent 1bf838a commit 8a8dec0

File tree

9 files changed

+1759
-4
lines changed

9 files changed

+1759
-4
lines changed

src/components/settings/ProgressSettingsTab.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ export function renderProgressSettingsTab(
1515
"You can customize the progress bar behind the parent task(usually at the end of the task). You can also customize the progress bar for the task below the heading."
1616
)
1717
)
18-
.setHeading();
18+
.setHeading()
19+
.settingEl.setAttribute("data-setting-id", "progress-bar-main");
1920

20-
new Setting(containerEl)
21+
const progressDisplaySetting = new Setting(containerEl)
2122
.setName(t("Progress display mode"))
2223
.setDesc(t("Choose how to display task progress"))
2324
.addDropdown((dropdown) =>
@@ -33,6 +34,7 @@ export function renderProgressSettingsTab(
3334
settingTab.display();
3435
})
3536
);
37+
progressDisplaySetting.settingEl.setAttribute("data-setting-id", "progress-display-mode");
3638

3739
// Only show these options if some form of progress bar is enabled
3840
if (settingTab.plugin.settings.progressBarDisplayMode !== "none") {
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { prepareFuzzySearch } from "obsidian";
2+
import { SETTINGS_METADATA } from "../../data/settings-metadata";
3+
import { SettingsSearchIndex, SettingSearchItem, SearchResult } from "../../types/SettingsSearch";
4+
import { t } from "../../translations/helper";
5+
6+
/**
7+
* 高性能设置项索引器
8+
* 提供快速的设置项搜索和导航功能
9+
*/
10+
export class SettingsIndexer {
11+
private index: SettingsSearchIndex;
12+
private isInitialized: boolean = false;
13+
14+
constructor() {
15+
this.index = {
16+
items: [],
17+
keywordMap: new Map(),
18+
tabMap: new Map()
19+
};
20+
}
21+
22+
/**
23+
* 初始化索引 - 懒加载模式
24+
*/
25+
public initialize(): void {
26+
if (this.isInitialized) {
27+
return;
28+
}
29+
30+
const startTime = performance.now();
31+
32+
// 构建索引
33+
this.buildIndex();
34+
35+
const endTime = performance.now();
36+
console.log(`Settings index built in ${(endTime - startTime).toFixed(2)}ms`);
37+
38+
this.isInitialized = true;
39+
}
40+
41+
/**
42+
* 构建设置项索引
43+
*/
44+
private buildIndex(): void {
45+
// 处理每个设置项
46+
for (const item of SETTINGS_METADATA) {
47+
// 翻译设置项名称和描述
48+
const translatedItem: SettingSearchItem = {
49+
...item,
50+
name: t(item.translationKey),
51+
description: item.descriptionKey ? t(item.descriptionKey) : item.description
52+
};
53+
54+
this.index.items.push(translatedItem);
55+
56+
// 构建关键词映射
57+
for (const keyword of item.keywords) {
58+
const normalizedKeyword = keyword.toLowerCase();
59+
if (!this.index.keywordMap.has(normalizedKeyword)) {
60+
this.index.keywordMap.set(normalizedKeyword, []);
61+
}
62+
this.index.keywordMap.get(normalizedKeyword)!.push(item.id);
63+
}
64+
65+
// 构建标签页映射
66+
if (!this.index.tabMap.has(item.tabId)) {
67+
this.index.tabMap.set(item.tabId, []);
68+
}
69+
this.index.tabMap.get(item.tabId)!.push(translatedItem);
70+
}
71+
}
72+
73+
/**
74+
* 搜索设置项
75+
* @param query 搜索查询
76+
* @param maxResults 最大结果数量
77+
* @returns 搜索结果数组
78+
*/
79+
public search(query: string, maxResults: number = 10): SearchResult[] {
80+
if (!this.isInitialized) {
81+
this.initialize();
82+
}
83+
84+
if (!query || query.trim().length === 0) {
85+
return [];
86+
}
87+
88+
const normalizedQuery = query.toLowerCase().trim();
89+
const results: SearchResult[] = [];
90+
const seenIds = new Set<string>();
91+
92+
// 使用 Obsidian 的模糊搜索
93+
const fuzzySearch = prepareFuzzySearch(normalizedQuery);
94+
95+
// 搜索设置项名称
96+
for (const item of this.index.items) {
97+
if (seenIds.has(item.id)) continue;
98+
99+
const nameMatch = fuzzySearch(item.name.toLowerCase());
100+
if (nameMatch) {
101+
results.push({
102+
item,
103+
score: this.calculateScore(normalizedQuery, item.name, 'name'),
104+
matchType: 'name'
105+
});
106+
seenIds.add(item.id);
107+
}
108+
}
109+
110+
// 搜索设置项描述
111+
for (const item of this.index.items) {
112+
if (seenIds.has(item.id) || !item.description) continue;
113+
114+
const descMatch = fuzzySearch(item.description.toLowerCase());
115+
if (descMatch) {
116+
results.push({
117+
item,
118+
score: this.calculateScore(normalizedQuery, item.description, 'description'),
119+
matchType: 'description'
120+
});
121+
seenIds.add(item.id);
122+
}
123+
}
124+
125+
// 搜索关键词
126+
for (const item of this.index.items) {
127+
if (seenIds.has(item.id)) continue;
128+
129+
for (const keyword of item.keywords) {
130+
const keywordMatch = fuzzySearch(keyword.toLowerCase());
131+
if (keywordMatch) {
132+
results.push({
133+
item,
134+
score: this.calculateScore(normalizedQuery, keyword, 'keyword'),
135+
matchType: 'keyword'
136+
});
137+
seenIds.add(item.id);
138+
break; // 只需要一个关键词匹配即可
139+
}
140+
}
141+
}
142+
143+
// 按分数排序并限制结果数量
144+
return results
145+
.sort((a, b) => b.score - a.score)
146+
.slice(0, maxResults);
147+
}
148+
149+
/**
150+
* 计算匹配分数
151+
* @param query 查询字符串
152+
* @param target 目标字符串
153+
* @param matchType 匹配类型
154+
* @returns 匹配分数
155+
*/
156+
private calculateScore(query: string, target: string, matchType: 'name' | 'description' | 'keyword'): number {
157+
const lowerTarget = target.toLowerCase();
158+
const lowerQuery = query.toLowerCase();
159+
160+
let score = 0;
161+
162+
// 基础分数根据匹配类型
163+
const baseScores = {
164+
name: 100,
165+
description: 60,
166+
keyword: 80
167+
};
168+
score += baseScores[matchType];
169+
170+
// 精确匹配加分
171+
if (lowerTarget.includes(lowerQuery)) {
172+
score += 50;
173+
174+
// 开头匹配额外加分
175+
if (lowerTarget.startsWith(lowerQuery)) {
176+
score += 30;
177+
}
178+
}
179+
180+
// 长度相似性加分
181+
const lengthRatio = Math.min(query.length / target.length, 1);
182+
score += lengthRatio * 20;
183+
184+
return score;
185+
}
186+
187+
/**
188+
* 根据标签页ID获取设置项
189+
* @param tabId 标签页ID
190+
* @returns 设置项数组
191+
*/
192+
public getItemsByTab(tabId: string): SettingSearchItem[] {
193+
if (!this.isInitialized) {
194+
this.initialize();
195+
}
196+
197+
return this.index.tabMap.get(tabId) || [];
198+
}
199+
200+
/**
201+
* 根据设置项ID获取设置项
202+
* @param itemId 设置项ID
203+
* @returns 设置项或undefined
204+
*/
205+
public getItemById(itemId: string): SettingSearchItem | undefined {
206+
if (!this.isInitialized) {
207+
this.initialize();
208+
}
209+
210+
return this.index.items.find(item => item.id === itemId);
211+
}
212+
213+
/**
214+
* 获取所有可用的标签页ID
215+
* @returns 标签页ID数组
216+
*/
217+
public getAllTabIds(): string[] {
218+
if (!this.isInitialized) {
219+
this.initialize();
220+
}
221+
222+
return Array.from(this.index.tabMap.keys());
223+
}
224+
225+
/**
226+
* 获取索引统计信息
227+
* @returns 索引统计
228+
*/
229+
public getStats(): { itemCount: number; tabCount: number; keywordCount: number } {
230+
if (!this.isInitialized) {
231+
this.initialize();
232+
}
233+
234+
return {
235+
itemCount: this.index.items.length,
236+
tabCount: this.index.tabMap.size,
237+
keywordCount: this.index.keywordMap.size
238+
};
239+
}
240+
}

0 commit comments

Comments
 (0)