208 lines
4.4 KiB
TypeScript
208 lines
4.4 KiB
TypeScript
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',
|
|
}
|
|
}
|
|
}
|