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
2 changes: 2 additions & 0 deletions internal-plugins/setting/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ declare global {
updatePinnedRows: (rows: number) => Promise<void>
updateClipboardConfig: (config: { retentionDays: number }) => Promise<void>
updateSearchMode: (searchMode: 'aggregate' | 'list') => Promise<void>
updateTokenSearchEnabled: (enabled: boolean) => Promise<void>
updateMatchInsideWord: (enabled: boolean) => Promise<void>
updateTabKeyFunction: (mode: 'navigate' | 'target-command') => Promise<void>
updateTabTarget: (target: string) => Promise<void>
updateSpaceOpenCommand: (enabled: boolean) => Promise<void>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ const localAppSearch = ref(true)
const recentRows = ref(2)
const pinnedRows = ref(2)
const searchMode = ref<'aggregate' | 'list'>('aggregate')
const tokenSearchEnabled = ref(false)
const matchInsideWord = ref(false)
const clipboardRetentionDays = ref(180)

// Tab 键目标指令
Expand Down Expand Up @@ -646,7 +648,6 @@ async function handlePinnedRowsChange(): Promise<void> {
}
}

// 处理搜索框模式变化
async function handleSearchModeChange(): Promise<void> {
try {
await saveSettings()
Expand All @@ -658,6 +659,28 @@ async function handleSearchModeChange(): Promise<void> {
}
}

// 处理分词搜索开关变化
async function handleTokenSearchEnabledChange(): Promise<void> {
try {
await saveSettings()
await window.ztools.internal.updateTokenSearchEnabled(tokenSearchEnabled.value)
console.log('分词搜索配置已更新:', tokenSearchEnabled.value)
} catch (error) {
console.error('保存分词搜索配置失败:', error)
}
}

// 处理匹配单词内部开关变化
async function handleMatchInsideWordChange(): Promise<void> {
try {
await saveSettings()
await window.ztools.internal.updateMatchInsideWord(matchInsideWord.value)
console.log('匹配单词内部配置已更新:', matchInsideWord.value)
} catch (error) {
console.error('保存匹配单词内部配置失败:', error)
}
}

// 处理空格打开指令变化
async function handleSpaceOpenCommandChange(): Promise<void> {
try {
Expand Down Expand Up @@ -1309,6 +1332,8 @@ async function loadSettings(): Promise<void> {
theme.value = data.theme ?? 'system'
primaryColor.value = data.primaryColor ?? 'green'
searchMode.value = data.searchMode ?? 'aggregate'
tokenSearchEnabled.value = data.tokenSearchEnabled ?? false
matchInsideWord.value = data.matchInsideWord ?? false
autoCheckUpdate.value = data.autoCheckUpdate ?? true
tabKeyFunction.value =
data.tabKeyFunction ?? (data.tabTargetCommand ? 'target-command' : 'navigate')
Expand Down Expand Up @@ -1403,6 +1428,8 @@ async function saveSettings(): Promise<void> {
recentRows: recentRows.value,
pinnedRows: pinnedRows.value,
searchMode: searchMode.value,
tokenSearchEnabled: tokenSearchEnabled.value,
matchInsideWord: matchInsideWord.value,
tabKeyFunction: tabKeyFunction.value,
tabTargetCommand: tabTargetCommand.value,
spaceOpenCommand: spaceOpenCommand.value,
Expand Down Expand Up @@ -1897,6 +1924,42 @@ onUnmounted(() => {
</div>
</div>

<div class="setting-item">
<div class="setting-label">
<span>分词模式</span>
<span class="setting-desc">开启后支持更丰富的词首匹配,例如 tasm 匹配 Task Manager</span>
</div>
<div class="setting-control">
<label class="toggle">
<input
v-model="tokenSearchEnabled"
type="checkbox"
@change="handleTokenSearchEnabledChange"
/>
<span class="toggle-slider"></span>
</label>
</div>
</div>

<div v-if="tokenSearchEnabled" class="setting-item">
<div class="setting-label">
<span>匹配单词内部</span>
<span class="setting-desc"
>开启后允许非词首匹配,例如 ps 和 shop 都可以匹配 Photoshop (噪音较多不建议开启)</span
>
</div>
<div class="setting-control">
<label class="toggle">
<input
v-model="matchInsideWord"
type="checkbox"
@change="handleMatchInsideWordChange"
/>
<span class="toggle-slider"></span>
</label>
</div>
</div>

<div class="setting-item">
<div class="setting-label">
<span>搜索框显示最近使用</span>
Expand Down
6 changes: 6 additions & 0 deletions resources/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,12 @@ window.ztools = {
// 通知主渲染进程更新搜索框模式
updateSearchMode: async (mode) =>
await electron.ipcRenderer.invoke('internal:update-search-mode', mode),
// 通知主渲染进程更新分词搜索开关
updateTokenSearchEnabled: async (enabled) =>
await electron.ipcRenderer.invoke('internal:update-token-search-enabled', enabled),
// 通知主渲染进程更新匹配单词内部开关
updateMatchInsideWord: async (enabled) =>
await electron.ipcRenderer.invoke('internal:update-match-inside-word', enabled),
// 通知主渲染进程更新 Tab 键功能配置
updateTabKeyFunction: async (mode) =>
await electron.ipcRenderer.invoke('internal:update-tab-key-function', mode),
Expand Down
18 changes: 18 additions & 0 deletions src/main/api/plugin/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,24 @@ export class InternalPluginAPI {
return { success: true }
})

// 通知主渲染进程更新分词搜索开关
ipcMain.handle('internal:update-token-search-enabled', async (event, enabled: boolean) => {
if (!requireInternalPlugin(this.pluginManager, event)) {
throw new PermissionDeniedError('internal:update-token-search-enabled')
}
this.mainWindow?.webContents.send('update-token-search-enabled', enabled)
return { success: true }
})

// 通知主渲染进程更新匹配单词内部开关
ipcMain.handle('internal:update-match-inside-word', async (event, enabled: boolean) => {
if (!requireInternalPlugin(this.pluginManager, event)) {
throw new PermissionDeniedError('internal:update-match-inside-word')
}
this.mainWindow?.webContents.send('update-match-inside-word', enabled)
return { success: true }
})

// 通知主渲染进程更新 Tab 键目标指令
ipcMain.handle('internal:update-tab-target', async (event, target: string) => {
if (!requireInternalPlugin(this.pluginManager, event)) {
Expand Down
8 changes: 8 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ const api = {
onUpdateSearchMode: (callback: (mode: string) => void) => {
ipcRenderer.on('update-search-mode', (_event, mode) => callback(mode))
},
onUpdateTokenSearchEnabled: (callback: (enabled: boolean) => void) => {
ipcRenderer.on('update-token-search-enabled', (_event, enabled) => callback(enabled))
},
onUpdateMatchInsideWord: (callback: (enabled: boolean) => void) => {
ipcRenderer.on('update-match-inside-word', (_event, enabled) => callback(enabled))
},
onUpdatePrimaryColor: (
callback: (data: { primaryColor: string; customColor?: string }) => void
) => {
Expand Down Expand Up @@ -723,6 +729,8 @@ declare global {
onUpdateSubInputPlaceholder: (
callback: (data: { pluginPath: string; placeholder: string }) => void
) => void
onUpdateTokenSearchEnabled: (callback: (enabled: boolean) => void) => void
onUpdateMatchInsideWord: (callback: (enabled: boolean) => void) => void
onUpdateSubInputVisible: (callback: (visible: boolean) => void) => void
onUpdateTabTarget: (callback: (target: string) => void) => void
onUpdateTabKeyFunction: (callback: (mode: 'navigate' | 'target-command') => void) => void
Expand Down
10 changes: 10 additions & 0 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,16 @@ onMounted(async () => {
windowStore.updateSearchMode(mode as 'aggregate' | 'list')
})

// 监听分词搜索开关更新事件
window.ztools.onUpdateTokenSearchEnabled((enabled: boolean) => {
windowStore.updateTokenSearchEnabled(enabled)
})

// 监听匹配单词内部开关更新事件
window.ztools.onUpdateMatchInsideWord((enabled: boolean) => {
windowStore.updateMatchInsideWord(enabled)
})

// 监听主题色更新事件
window.ztools.onUpdatePrimaryColor((data: { primaryColor: string; customColor?: string }) => {
console.log('更新主题色:', data)
Expand Down
27 changes: 26 additions & 1 deletion src/renderer/src/composables/useSearchResults.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { computed, ref, watch } from 'vue'
import { useCommandDataStore } from '../stores/commandDataStore'
import { matchCommand } from '../utils/tokenSearch'
import { useWindowStore } from '../stores/windowStore'

/**
Expand Down Expand Up @@ -256,7 +257,31 @@ export function useSearchResults(props: {
// 无搜索词(如仅粘贴文本)时,返回去重后的原始顺序结果
if (!query) return deduped

// 排序:完全匹配 > 前缀匹配 > 系统应用 > 其他
// 分词搜索开启时:按分词匹配分数排序,同分按使用频率降序
if (windowStore.tokenSearchEnabled) {
const matchInsideWord = windowStore.matchInsideWord
return deduped
.map((cmd) => {
const outcome = matchCommand(cmd.name, query, { matchInsideWord })
const isApp = cmd.type === 'direct' && cmd.subType === 'app'
// matchCommand 只看 name,系统应用加权在集合层补一次(对齐 tokenSearch)
const score = outcome
? outcome.pattern === 'exact'
? outcome.score
: outcome.score + (isApp ? 300 : 0)
: -1
return { cmd, score }
})
.sort((a, b) => {
if (a.score !== b.score) return b.score - a.score
const keyA = `${a.cmd.path}:${a.cmd.featureCode || ''}`
const keyB = `${b.cmd.path}:${b.cmd.featureCode || ''}`
return (usageStatsMap.value.get(keyB) || 0) - (usageStatsMap.value.get(keyA) || 0)
})
.map((item) => item.cmd)
}

// 原有排序:完全匹配 > 前缀匹配 > 系统应用 > 其他
return deduped.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ declare global {
onUpdateSpaceOpenCommand: (callback: (enabled: boolean) => void) => void
onUpdateFloatingBallDoubleClickCommand?: (callback: (command: string) => void) => void
onUpdateSearchMode: (callback: (mode: string) => void) => void
onUpdateTokenSearchEnabled: (callback: (enabled: boolean) => void) => void
onUpdateMatchInsideWord: (callback: (enabled: boolean) => void) => void
onUpdatePrimaryColor: (
callback: (data: { primaryColor: string; customColor?: string }) => void
) => void
Expand Down
119 changes: 66 additions & 53 deletions src/renderer/src/stores/commandDataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
applySpecialConfig as _applySpecialConfig,
calculateMatchScore as _calculateMatchScore
} from './commandUtils'
import { tokenSearch } from '../utils/tokenSearch'
import { useWindowStore } from './windowStore'
import {
COMMAND_ALIASES_KEY,
getLegacyDirectAppCommandId,
Expand Down Expand Up @@ -1368,72 +1370,83 @@ export const useCommandDataStore = defineStore('commandData', () => {
// 如果没有指定搜索范围,使用全局指令
const searchTarget = commandList || commands.value

if (!query || !fuse.value) {
const windowStore = useWindowStore()
const useTokenSearch = windowStore.tokenSearchEnabled

if (!query || (!fuse.value && !useTokenSearch)) {
return {
bestMatches: searchTarget.filter((cmd) => cmd.type === 'direct' && cmd.subType === 'app'), // 无搜索时只显示应用
regexMatches: []
}
}

// 1. Fuse.js 模糊搜索
// 搜索词过长时跳过 Fuse.js(应用名/指令名通常很短,超长输入走模糊搜索无意义且浪费性能
// 1. 命令搜索
// 搜索词过长时跳过(应用名/指令名通常很短,超长输入走搜索无意义且浪费性能
const FUSE_MAX_QUERY_LENGTH = 32
let bestMatches: SearchResult[] = []

if (query.length <= FUSE_MAX_QUERY_LENGTH) {
// 如果指定了搜索范围,创建临时 Fuse 实例
const searchFuse = commandList
? new Fuse(commandList, {
keys: [
{ name: 'name', weight: 2 },
{ name: 'pinyin', weight: 1.5 },
{ name: 'pinyinAbbr', weight: 1 },
{ name: 'acronym', weight: 1.5 }
],
threshold: 0,
ignoreLocation: true,
includeScore: true,
includeMatches: true
})
: fuse.value

const fuseResults = searchFuse.search(query)
const scoredMatches: SearchResultScoreMeta[] = fuseResults.map((r) => {
const displayMatches = (r.matches || []) as MatchInfo[]

// 检测匹配类型(用于前端高亮算法选择)
let matchType: 'acronym' | 'name' | 'pinyin' | 'pinyinAbbr' | undefined
if (displayMatches.length > 0) {
// 优先级:acronym > name > pinyin > pinyinAbbr
if (displayMatches.some((m) => m.key === 'acronym')) {
matchType = 'acronym'
} else if (displayMatches.some((m) => m.key === 'name')) {
matchType = 'name'
} else if (displayMatches.some((m) => m.key === 'pinyin')) {
matchType = 'pinyin'
} else if (displayMatches.some((m) => m.key === 'pinyinAbbr')) {
matchType = 'pinyinAbbr'
if (useTokenSearch) {
// 分词搜索:直接产出按分词匹配分数排序的命中项
bestMatches = tokenSearch(searchTarget, query, {
matchInsideWord: windowStore.matchInsideWord
}) as unknown as SearchResult[]
} else {
// Fuse.js 模糊搜索
// 如果指定了搜索范围,创建临时 Fuse 实例
const searchFuse = commandList
? new Fuse(commandList, {
keys: [
{ name: 'name', weight: 2 },
{ name: 'pinyin', weight: 1.5 },
{ name: 'pinyinAbbr', weight: 1 },
{ name: 'acronym', weight: 1.5 }
],
threshold: 0,
ignoreLocation: true,
includeScore: true,
includeMatches: true
})
: fuse.value!

const fuseResults = searchFuse.search(query)
const scoredMatches: SearchResultScoreMeta[] = fuseResults.map((r) => {
const displayMatches = (r.matches || []) as MatchInfo[]

// 检测匹配类型(用于前端高亮算法选择)
let matchType: 'acronym' | 'name' | 'pinyin' | 'pinyinAbbr' | undefined
if (displayMatches.length > 0) {
// 优先级:acronym > name > pinyin > pinyinAbbr
if (displayMatches.some((m) => m.key === 'acronym')) {
matchType = 'acronym'
} else if (displayMatches.some((m) => m.key === 'name')) {
matchType = 'name'
} else if (displayMatches.some((m) => m.key === 'pinyin')) {
matchType = 'pinyin'
} else if (displayMatches.some((m) => m.key === 'pinyinAbbr')) {
matchType = 'pinyinAbbr'
}
}
}

return {
result: {
...r.item,
matches: displayMatches,
matchType
},
scoreText: r.item.name,
scoreMatches: displayMatches
}
})
bestMatches = scoredMatches
.sort((a, b) => {
// 自定义排序:优先连续匹配,系统应用权重略高
const scoreA = calculateMatchScore(a.scoreText, query, a.scoreMatches, a.result)
const scoreB = calculateMatchScore(b.scoreText, query, b.scoreMatches, b.result)
return scoreB - scoreA // 分数高的排前面
return {
result: {
...r.item,
matches: displayMatches,
matchType
},
scoreText: r.item.name,
scoreMatches: displayMatches
}
})
.map((item) => item.result)
bestMatches = scoredMatches
.sort((a, b) => {
// 自定义排序:优先连续匹配,系统应用权重略高
const scoreA = calculateMatchScore(a.scoreText, query, a.scoreMatches, a.result)
const scoreB = calculateMatchScore(b.scoreText, query, b.scoreMatches, b.result)
return scoreB - scoreA // 分数高的排前面
})
.map((item) => item.result)
}

// 搜索偏好置顶:将上次选中的指令移到第一位
const prefKey = query.trim().toLowerCase()
Expand Down
Loading