init
This commit is contained in:
207
src/utils/fuzzy-search.ts
Normal file
207
src/utils/fuzzy-search.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import fuzzysort from 'fuzzysort'
|
||||
import { App, TFile, TFolder } from 'obsidian'
|
||||
|
||||
import {
|
||||
MentionableFile,
|
||||
MentionableFolder,
|
||||
MentionableVault,
|
||||
} from '../types/mentionable'
|
||||
|
||||
import { calculateFileDistance, getOpenFiles } from './obsidian'
|
||||
|
||||
export type SearchableMentionable =
|
||||
| MentionableFile
|
||||
| MentionableFolder
|
||||
| MentionableVault
|
||||
|
||||
type VaultSearchItem = {
|
||||
type: 'vault'
|
||||
path: string
|
||||
}
|
||||
|
||||
type FileWithMetadata = {
|
||||
type: 'file'
|
||||
path: string
|
||||
name: string
|
||||
file: TFile
|
||||
opened: boolean
|
||||
distance: number | null
|
||||
daysSinceLastModified: number
|
||||
}
|
||||
|
||||
type FolderWithMetadata = {
|
||||
type: 'folder'
|
||||
path: string
|
||||
name: string
|
||||
folder: TFolder
|
||||
distance: number | null
|
||||
}
|
||||
|
||||
type SearchItem = FolderWithMetadata | FileWithMetadata | VaultSearchItem
|
||||
|
||||
function scoreFnWithBoost({
|
||||
searchItem,
|
||||
pathScore,
|
||||
nameScore,
|
||||
}: {
|
||||
searchItem: SearchItem
|
||||
pathScore: number
|
||||
nameScore: number
|
||||
}): number {
|
||||
const score = Math.max(pathScore, nameScore)
|
||||
|
||||
let boost = 1
|
||||
switch (searchItem.type) {
|
||||
case 'file': {
|
||||
const { opened, distance, daysSinceLastModified } = searchItem
|
||||
|
||||
// Boost for open files
|
||||
if (opened) boost = Math.max(boost, 3)
|
||||
|
||||
// Boost for recently modified files
|
||||
if (daysSinceLastModified < 30) {
|
||||
const recentBoost = 1 + 2 / (daysSinceLastModified + 2)
|
||||
boost = Math.max(boost, recentBoost)
|
||||
}
|
||||
|
||||
// Boost for nearby files
|
||||
if (distance !== null && distance > 0 && distance <= 5) {
|
||||
const nearbyBoost = 1 + 0.5 / Math.max(distance - 1, 1)
|
||||
boost = Math.max(boost, nearbyBoost)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'folder': {
|
||||
const { distance } = searchItem
|
||||
|
||||
// Boost for nearby folders
|
||||
if (distance !== null && distance > 0 && distance <= 5) {
|
||||
const nearbyBoost = 1 + 0.5 / Math.max(distance - 1, 1)
|
||||
boost = Math.max(boost, nearbyBoost)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'vault': {
|
||||
if (score === 1) {
|
||||
boost = 3
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the boost
|
||||
const normalizedScore =
|
||||
boost > 1 ? Math.log(boost * score + 1) / Math.log(boost + 1) : score
|
||||
return normalizedScore
|
||||
}
|
||||
|
||||
function getEmptyQueryResult(
|
||||
searchItems: SearchItem[],
|
||||
limit: number,
|
||||
): SearchableMentionable[] {
|
||||
// Sort files based on a custom scoring function
|
||||
const sortedFiles = searchItems.sort((a, b) => {
|
||||
const scoreA = scoreFnWithBoost({
|
||||
searchItem: a,
|
||||
pathScore: 0.5, // Use 0.5 as a base score
|
||||
nameScore: 0.5,
|
||||
})
|
||||
const scoreB = scoreFnWithBoost({
|
||||
searchItem: b,
|
||||
pathScore: 0.5,
|
||||
nameScore: 0.5,
|
||||
})
|
||||
return scoreB - scoreA // Sort in descending order
|
||||
})
|
||||
|
||||
// Return only the top 'limit' files
|
||||
return sortedFiles
|
||||
.slice(0, limit)
|
||||
.map((item) => searchItemToMentionable(item))
|
||||
}
|
||||
|
||||
export function fuzzySearch(app: App, query: string): SearchableMentionable[] {
|
||||
const currentFile = app.workspace.getActiveFile()
|
||||
const openFiles = getOpenFiles(app)
|
||||
|
||||
const allSupportedFiles = app.vault.getFiles().filter((file) => {
|
||||
const extension = file.extension
|
||||
return extension === 'md'
|
||||
})
|
||||
|
||||
const allFilesWithMetadata: SearchItem[] = allSupportedFiles.map((file) => ({
|
||||
type: 'file',
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
file,
|
||||
opened: openFiles.some((f) => f.path === file.path),
|
||||
distance: currentFile
|
||||
? currentFile === file
|
||||
? null
|
||||
: calculateFileDistance(currentFile, file)
|
||||
: null,
|
||||
daysSinceLastModified:
|
||||
(Date.now() - file.stat.mtime) / (1000 * 60 * 60 * 24),
|
||||
}))
|
||||
|
||||
const allFolders = app.vault.getAllFolders()
|
||||
const allFoldersWithMetadata: SearchItem[] = allFolders.map((folder) => ({
|
||||
type: 'folder',
|
||||
path: folder.path,
|
||||
name: folder.name,
|
||||
folder,
|
||||
distance: currentFile ? calculateFileDistance(currentFile, folder) : null,
|
||||
}))
|
||||
|
||||
const vaultItem: VaultSearchItem = {
|
||||
type: 'vault',
|
||||
path: 'vault',
|
||||
}
|
||||
|
||||
const searchItems: SearchItem[] = [
|
||||
...allFilesWithMetadata,
|
||||
...allFoldersWithMetadata,
|
||||
vaultItem,
|
||||
]
|
||||
|
||||
if (!query) {
|
||||
return getEmptyQueryResult(searchItems, 20)
|
||||
}
|
||||
|
||||
const results = fuzzysort.go(query, searchItems, {
|
||||
keys: ['path', 'name'],
|
||||
threshold: 0.2,
|
||||
limit: 20,
|
||||
all: true,
|
||||
scoreFn: (result) =>
|
||||
scoreFnWithBoost({
|
||||
searchItem: result.obj,
|
||||
pathScore: result[0].score,
|
||||
nameScore: result[1].score,
|
||||
}),
|
||||
})
|
||||
|
||||
return results.map((result) => searchItemToMentionable(result.obj))
|
||||
}
|
||||
|
||||
function searchItemToMentionable(item: SearchItem): SearchableMentionable {
|
||||
switch (item.type) {
|
||||
case 'file':
|
||||
return {
|
||||
type: 'file',
|
||||
file: item.file,
|
||||
}
|
||||
case 'folder':
|
||||
return {
|
||||
type: 'folder',
|
||||
folder: item.folder,
|
||||
}
|
||||
case 'vault':
|
||||
return {
|
||||
type: 'vault',
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user