Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const localAppSearch = ref(true)
const recentRows = ref(2)
const pinnedRows = ref(2)
const searchMode = ref<'aggregate' | 'list'>('aggregate')
const wordTokenEnabled = ref(true)
const clipboardRetentionDays = ref(180)
// Tab 键目标指令
Expand Down Expand Up @@ -548,6 +549,15 @@ async function handleLocalAppSearchChange(): Promise<void> {
}
}
// 处理词 token 搜索开关变化
async function handleWordTokenEnabledChange(): Promise<void> {
try {
await window.ztools.internal.setWordTokenEnabled(wordTokenEnabled.value)
console.log('词 token 搜索开关已更新:', wordTokenEnabled.value)
} catch (error) {
console.error('保存词 token 搜索开关失败:', error)
}
}
// 处理最近使用行数变化
async function handleRecentRowsChange(): Promise<void> {
try {
Expand Down Expand Up @@ -1211,6 +1221,8 @@ async function loadSettings(): Promise<void> {
theme.value = data.theme ?? 'system'
primaryColor.value = data.primaryColor ?? 'blue'
searchMode.value = data.searchMode ?? 'aggregate'
wordTokenEnabled.value =
(await window.ztools.internal.dbGet('search.wordTokenEnabled')) !== false
autoCheckUpdate.value = data.autoCheckUpdate ?? true
tabKeyFunction.value =
data.tabKeyFunction ?? (data.tabTargetCommand ? 'target-command' : 'navigate')
Expand Down Expand Up @@ -1741,6 +1753,23 @@ onUnmounted(() => {
</div>
</div>

<div class="setting-item">
<div class="setting-label">
<span>分词搜索</span>
<span class="setting-desc">支持跨词缩写搜索,如输入 tas man 匹配 Task Manager</span>
</div>
<div class="setting-control">
<label class="toggle">
<input
v-model="wordTokenEnabled"
type="checkbox"
@change="handleWordTokenEnabledChange"
/>
<span class="toggle-slider"></span>
</label>
</div>
</div>

<div class="setting-item">
<div class="setting-label">
<span>空格打开指令</span>
Expand Down
2 changes: 2 additions & 0 deletions resources/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,8 @@ window.ztools = {
// 通知主渲染进程更新搜索框模式
updateSearchMode: async (mode) =>
await electron.ipcRenderer.invoke('internal:update-search-mode', mode),
setWordTokenEnabled: async (enabled) =>
await electron.ipcRenderer.invoke('internal:set-word-token-enabled', enabled),
// 通知主渲染进程更新 Tab 键功能配置
updateTabKeyFunction: async (mode) =>
await electron.ipcRenderer.invoke('internal:update-tab-key-function', mode),
Expand Down
10 changes: 10 additions & 0 deletions src/main/api/plugin/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,16 @@ export class InternalPluginAPI {
return { success: true }
})

// 词 token 搜索开关:写库 + 即时通知主渲染进程
ipcMain.handle('internal:set-word-token-enabled', async (event, enabled: boolean) => {
if (!requireInternalPlugin(this.pluginManager, event)) {
throw new PermissionDeniedError('internal:set-word-token-enabled')
}
databaseAPI.dbPut('search.wordTokenEnabled', enabled)
this.mainWindow?.webContents.send('word-token-enabled-changed', enabled)
return { success: true }
})

// 通知主渲染进程更新 Tab 键目标指令
ipcMain.handle('internal:update-tab-target', async (event, target: string) => {
if (!requireInternalPlugin(this.pluginManager, event)) {
Expand Down
29 changes: 8 additions & 21 deletions src/main/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { app } from 'electron'
import { fileURLToPath } from 'url'
import { WindowManager } from '../core/native/index.js'
import { tokenize } from '../../shared/tokenizer'

/**
* 睡眠指定毫秒数
Expand All @@ -23,30 +24,16 @@ export function shuffleArray<T>(arr: readonly T[]): T[] {
}

/**
* 提取英文名称的首字母缩写
* 支持两种模式(优先级从高到低):
* 1. 空格分隔的单词首字母:Visual Studio Code → vsc
* 2. 驼峰命名首字母:VisualStudioCode → vsc
* 提取名称的首字母缩写:取每个分词 token 的首字母拼接。
* 分词规则由 tokenize 统一提供(含空格 / 驼峰 / 数字 / 汉字逐字等边界)。
* 例:Visual Studio Code → [visual, studio, code] → vsc
* @param name 应用名称
* @returns 首字母缩写字符串
* @returns 首字母缩写字符串(小写)
*/
export function extractAcronym(name: string): string {
// 方式1:空格分隔的单词首字母(优先)
// "Visual Studio Code" → "vsc"
const words = name.split(/\s+/).filter((w) => w.length > 0)
if (words.length > 1) {
return words.map((w) => w[0].toLowerCase()).join('')
}

// 方式2:驼峰命名首字母
// "VisualStudioCode" → "vsc"
const capitals = name.match(/[A-Z]/g)
if (capitals && capitals.length > 1) {
return capitals.map((c) => c.toLowerCase()).join('')
}

// 无法提取首字母缩写
return ''
return tokenize(name)
.map((t) => t[0])
.join('')
}

interface ExplorerFolderWindowInfo {
Expand Down
4 changes: 4 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ const api = {
onHistoryChanged: (callback: () => void) => {
ipcRenderer.on('history-changed', callback)
},
onWordTokenEnabledChanged: (callback: (enabled: boolean) => void) => {
ipcRenderer.on('word-token-enabled-changed', (_event, enabled: boolean) => callback(enabled))
},
onPinnedChanged: (callback: () => void) => {
ipcRenderer.on('pinned-changed', callback)
},
Expand Down Expand Up @@ -640,6 +643,7 @@ declare global {
onShowSettings: (callback: () => void) => void
onAppLaunched: (callback: () => void) => void
onHistoryChanged: (callback: () => void) => void
onWordTokenEnabledChanged: (callback: (enabled: boolean) => void) => void
onPinnedChanged: (callback: () => void) => void
onSuperPanelPinnedChanged: (callback: () => void) => void
onIpcLaunch: (
Expand Down
71 changes: 70 additions & 1 deletion src/renderer/src/composables/useSearchResults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { computed, ref, watch } from 'vue'
import { NO_MATCH, DEFAULT_CONFIG, type PatternMode } from '@shared/tokenSearch'
import { useCommandDataStore } from '../stores/commandDataStore'
import { useWindowStore } from '../stores/windowStore'

Expand All @@ -24,6 +25,66 @@ export function deduplicateResults<
/**
* 根据使用统计对匹配指令结果排序(useCount 降序)
*/
/**
* 列表模式档位序,与引擎 modeTier 权重表一致:
* 完整项(1000) > 连续词首(900) > 全词(800) > 词首子串(600) > 非连续词首(400)
* _tokenMode 取自 tokenSearch 引擎(语义化枚举:multiTokensExactitude 等)。
* query 需预先 toLowerCase。
*/
export function listModeRank(
item: { name: string; _tokenMode?: PatternMode | typeof NO_MATCH },
query: string
): number {
// name 完全匹配等价于 multiTokensExactitude(最高优先)
if (item.name.toLowerCase() === query) return DEFAULT_CONFIG.modeTiers.multiTokensExactitude
// _tokenMode 缺失(OFF 路径结果)落到最低档
const mode = item._tokenMode
if (!mode || mode === NO_MATCH) return 0
return DEFAULT_CONFIG.modeTiers[mode]
}

/** 列表模式排序上下文 */
export interface ListModeSortCtx {
query: string
usageMap: Map<string, number>
}

/**
* 列表模式纯函数排序(软加权,沿用 1554a27 原设计)。
* 偏好置顶由 store.search 保证(偏好项已在 bestSearchResults 首位,
* 合并去重后仍居前),本函数仅按 token 档位排序:
* 完全匹配 > 跨词词首 > 单token > 其他
* 同档位内 tiebreaker:系统应用软加权(direct+app)→ 频率(useCount 降序)
* 返回新数组,不修改入参。
*/

export function sortListModeResults<
T extends {
name: string
type?: string
subType?: string
path: string
featureCode?: string
_tokenMode?: PatternMode | typeof NO_MATCH
}
>(items: T[], ctx: ListModeSortCtx): T[] {
const usageKey = (item: { path: string; featureCode?: string }): string =>
`${item.path}:${item.featureCode || ''}`
return [...items].sort((a, b) => {
// token 档位(与 modeTier 权重表一致,大的优先)
const rankA = listModeRank(a, ctx.query)
const rankB = listModeRank(b, ctx.query)
if (rankA !== rankB) return rankB - rankA
// 同档位内:系统应用软加权(对齐原设计 calculateMatchScore +300)
const isAppA = a.type === 'direct' && a.subType === 'app'
const isAppB = b.type === 'direct' && b.subType === 'app'
if (isAppA !== isAppB) return isAppA ? -1 : 1
// 频率 tiebreaker
const countA = ctx.usageMap.get(usageKey(a)) || 0
const countB = ctx.usageMap.get(usageKey(b)) || 0
return countB - countA
})
}
function sortByUsage<T extends { path: string; featureCode?: string }>(
results: T[],
statsMap: Map<string, number>
Expand Down Expand Up @@ -254,8 +315,16 @@ export function useSearchResults(props: {

// 无搜索词(如仅粘贴文本)时,返回去重后的原始顺序结果
if (!query) return deduped
// 列表模式排序(软加权,沿用 1554a27 原设计)。
// 主排序:search-preference 置顶(store.search 已将偏好项放在 bestSearchResults 首位,
// 合并去重后仍居前,无需在此重复处理),然后按 token 档位排序。
// 同档位内 tiebreaker:系统应用软加权 → 频率。
// 开关 OFF: 旧比较器(完全匹配 → 前缀 → 系统应用 → 频率)
if (commandDataStore.wordTokenEnabled) {
// ON: sortListModeResults 软加权(偏好置顶由 store.search 保证)
return sortListModeResults(deduped, { query, usageMap: usageStatsMap.value })
}

// 排序:完全匹配 > 前缀匹配 > 系统应用 > 其他
return deduped.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ declare global {
onLocalShortcutsChanged: (callback: () => void) => void
onCommandAliasesChanged: (callback: () => void) => void
onHistoryChanged: (callback: () => void) => void
onWordTokenEnabledChanged: (callback: (enabled: boolean) => void) => void
onPinnedChanged: (callback: () => void) => void
onSuperPanelPinnedChanged: (callback: () => void) => void
onDisabledCommandsChanged: (callback: () => void) => void
Expand Down
Loading