Merge branch 'develop'
# Conflicts: # manifest-beta.json # versions.json
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
id: git-cliff
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --verbose
|
||||
args: -vv --latest --strip header
|
||||
env:
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ Please read this document before beginning work on a Pull Request.
|
||||
## Preface
|
||||
|
||||
- Omnisearch is a personal hobby project. I'm happy to discuss about your ideas and additions, but ultimately it is my code to grow and maintain.
|
||||
- Always file an issue/feature request before working on a PR, to make sure we're aligned and no-one is making useless work.
|
||||
- Omnisearch is still in its infancy: some important features are missing, and there will be architectural changes.
|
||||
- As such, I may refuse your PR simply because it will have to be refactored in a short-ish term
|
||||
- ❗ Always file an issue/feature request before working on a PR, to make sure we're aligned and no-one is making useless work.
|
||||
|
||||
## "Good First Issue"
|
||||
|
||||
@@ -21,7 +19,7 @@ If you wish to work on one of these issues, leave a comment and I'll assign it t
|
||||
|
||||
## Code guidelines
|
||||
|
||||
- Respect the existing style
|
||||
- ❗ By default, start your fork from the `develop` branch. If the `develop` branch is behind `master`, then use `master`. When in doubt, ask :)
|
||||
- Don't add npm dependencies if you can avoid it. If a new dependency is unavoidable, be mindful of its size, freshness and added value.
|
||||
- Use Svelte for all UI needs.
|
||||
- Try to not shoehorn your code into existing functions or components.
|
||||
@@ -46,3 +44,4 @@ Always respect those UI & UX points:
|
||||
|
||||
- .ts files must be formatted with "Prettier ESLint"
|
||||
- .svelte files must be formatted with "Svelte for VS Code"
|
||||
- All CSS code **must** go into styles.css, and all classes should be properly named for easy customization. Do **not** use `<style>` tags in Svelte components
|
||||
@@ -5,8 +5,8 @@
|
||||
- Omnisearch is available on [the official Community Plugins repository](https://obsidian.md/plugins?search=Omnisearch).
|
||||
- Beta releases can be installed through [BRAT](https://github.com/TfTHacker/obsidian42-brat). **Be advised that those versions can be buggy and break things.**
|
||||
|
||||
> [!INFO] Chinese, Japanese, Korean, ...
|
||||
> If you have notes in a CJK language, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch)
|
||||
> [!INFO] Chinese users
|
||||
> If you have notes in Chinese, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch) for better search results.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -22,8 +22,9 @@ Omnisearch is licensed under [GPL-3](https://tldrlegal.com/license/gnu-general-p
|
||||
|
||||
## Thanks
|
||||
|
||||
❤ To all people who donate through [Ko-Fi](https://ko-fi.com/scambier)or [Github Sponsors](https://github.com/sponsors/scambier), to code contributors, and to Obsidian who graciously provides this Publish space ❤
|
||||
❤ To all people who donate through [Ko-Fi](https://ko-fi.com/scambier) or [Github Sponsors](https://github.com/sponsors/scambier), to code contributors, and to the Obsidian team who graciously provides this Publish space ❤
|
||||
|
||||
If you wish to get involved in Omnisearch's development, there are [open issues](https://github.com/scambier/obsidian-omnisearch/issues) that need to be solved, and probably several of them tagged as "[good first issue](https://github.com/scambier/obsidian-omnisearch/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)" :)
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -5,7 +5,7 @@
|
||||
white-space: normal;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
/* justify-content: space-between; */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
margin-inline-start: 0.5em;
|
||||
}
|
||||
|
||||
.omnisearch-result__embed {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
.omnisearch-result__image-container {
|
||||
flex-basis: 20%;
|
||||
text-align: end;
|
||||
@@ -82,6 +87,25 @@
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.omnisearch-result__icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.omnisearch-result__icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.omnisearch-result__icon--emoji {
|
||||
font-size: 16px;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.omnisearch-input-container {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "omnisearch",
|
||||
"name": "Omnisearch",
|
||||
"version": "1.25.0-beta.4",
|
||||
"version": "1.25.0-beta.2",
|
||||
"minAppVersion": "1.3.0",
|
||||
"description": "A search engine that just works",
|
||||
"author": "Simon Cambier",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "omnisearch",
|
||||
"name": "Omnisearch",
|
||||
"version": "1.24.1",
|
||||
"minAppVersion": "1.3.0",
|
||||
"minAppVersion": "1.7.2",
|
||||
"description": "A search engine that just works",
|
||||
"author": "Simon Cambier",
|
||||
"authorUrl": "https://github.com/scambier/obsidian-omnisearch",
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scambier.obsidian-search",
|
||||
"version": "1.24.1",
|
||||
"version": "1.25.0-beta.4",
|
||||
"description": "A search engine for Obsidian",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
@@ -14,13 +14,13 @@
|
||||
"author": "Simon Cambier",
|
||||
"license": "GPL-3",
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@babel/preset-env": "^7.25.8",
|
||||
"@babel/preset-typescript": "^7.25.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^16.18.108",
|
||||
"@types/node": "^16.18.113",
|
||||
"@types/pako": "^2.0.3",
|
||||
"babel-jest": "^27.5.1",
|
||||
"builtin-modules": "^3.3.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"esbuild-plugin-copy": "1.3.0",
|
||||
"esbuild-svelte": "0.7.1",
|
||||
"jest": "^27.5.1",
|
||||
"obsidian": "1.5.7-1",
|
||||
"obsidian": "1.7.2",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^3.59.2",
|
||||
@@ -37,14 +37,14 @@
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "2.3.1",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^3.2.10"
|
||||
"vite": "^3.2.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"dexie": "^3.2.7",
|
||||
"lodash-es": "4.17.21",
|
||||
"markdown-link-extractor": "^4.0.2",
|
||||
"minisearch": "github:scambier/minisearch#async-load-json",
|
||||
"minisearch": "7.1.0",
|
||||
"pure-md5": "^0.1.14",
|
||||
"search-query-parser": "^1.6.0"
|
||||
},
|
||||
|
||||
1843
pnpm-lock.yaml
generated
1843
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@
|
||||
const debouncedOnInput = debounce(() => {
|
||||
// If typing a query and not executing it,
|
||||
// the next time we open the modal, the search field will be empty
|
||||
plugin.cacheManager.addToSearchHistory('')
|
||||
plugin.searchHistory.addToHistory('')
|
||||
dispatch('input', value)
|
||||
}, 300)
|
||||
</script>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
async function prevSearchHistory() {
|
||||
// Filter out the empty string, if it's there
|
||||
const history = (await plugin.cacheManager.getSearchHistory()).filter(
|
||||
const history = (await plugin.searchHistory.getHistory()).filter(
|
||||
s => s
|
||||
)
|
||||
if (++historySearchIndex >= history.length) {
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
|
||||
async function nextSearchHistory() {
|
||||
const history = (await plugin.cacheManager.getSearchHistory()).filter(
|
||||
const history = (await plugin.searchHistory.getHistory()).filter(
|
||||
s => s
|
||||
)
|
||||
if (--historySearchIndex < 0) {
|
||||
@@ -192,7 +192,7 @@
|
||||
|
||||
function saveCurrentQuery() {
|
||||
if (searchQuery) {
|
||||
plugin.cacheManager.addToSearchHistory(searchQuery)
|
||||
plugin.searchHistory.addToHistory(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
export let id: string
|
||||
export let selected = false
|
||||
export let glyph = false
|
||||
export let cssClass = ''
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-result-id={id}
|
||||
class="suggestion-item omnisearch-result"
|
||||
class="suggestion-item omnisearch-result {cssClass}"
|
||||
class:is-selected={selected}
|
||||
on:mousemove
|
||||
on:click
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
import type { ResultNote } from '../globals'
|
||||
import {
|
||||
getExtension,
|
||||
isFileCanvas, isFileExcalidraw,
|
||||
isFileCanvas,
|
||||
isFileExcalidraw,
|
||||
isFileImage,
|
||||
isFilePDF,
|
||||
pathWithoutFilename,
|
||||
} from '../tools/utils'
|
||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||
import { TFile, setIcon } from 'obsidian'
|
||||
import type OmnisearchPlugin from '../main'
|
||||
import { setIcon, TFile } from 'obsidian'
|
||||
import { onMount, SvelteComponent } from 'svelte'
|
||||
|
||||
// Import icon utility functions
|
||||
import {
|
||||
loadIconData,
|
||||
initializeIconPacks,
|
||||
getIconNameForPath,
|
||||
loadIconSVG,
|
||||
getDefaultIconSVG,
|
||||
} from '../tools/icon-utils'
|
||||
|
||||
export let selected = false
|
||||
export let note: ResultNote
|
||||
@@ -19,8 +30,77 @@
|
||||
let imagePath: string | null = null
|
||||
let title = ''
|
||||
let notePath = ''
|
||||
let iconData = {}
|
||||
let folderIconSVG: string | null = null
|
||||
let fileIconSVG: string | null = null
|
||||
let prefixToIconPack: { [prefix: string]: string } = {}
|
||||
let iconsPath: string
|
||||
let iconDataLoaded = false // Flag to indicate iconData is loaded
|
||||
|
||||
// Initialize icon data and icon packs once when the component mounts
|
||||
onMount(async () => {
|
||||
iconData = await loadIconData(plugin)
|
||||
const iconPacks = await initializeIconPacks(plugin)
|
||||
prefixToIconPack = iconPacks.prefixToIconPack
|
||||
iconsPath = iconPacks.iconsPath
|
||||
iconDataLoaded = true // Set the flag after iconData is loaded
|
||||
})
|
||||
|
||||
// Reactive statement to call loadIcons() whenever the note changes and iconData is loaded
|
||||
$: if (note && note.path && iconDataLoaded) {
|
||||
;(async () => {
|
||||
// Update title and notePath before loading icons
|
||||
title = note.displayTitle || note.basename
|
||||
notePath = pathWithoutFilename(note.path)
|
||||
await loadIcons()
|
||||
})()
|
||||
}
|
||||
|
||||
async function loadIcons() {
|
||||
// Load folder icon
|
||||
const folderIconName = getIconNameForPath(notePath, iconData)
|
||||
if (folderIconName) {
|
||||
folderIconSVG = await loadIconSVG(
|
||||
folderIconName,
|
||||
plugin,
|
||||
iconsPath,
|
||||
prefixToIconPack
|
||||
)
|
||||
} else {
|
||||
// Fallback to default folder icon
|
||||
folderIconSVG = getDefaultIconSVG('folder', plugin)
|
||||
}
|
||||
|
||||
// Load file icon
|
||||
const fileIconName = getIconNameForPath(note.path, iconData)
|
||||
if (fileIconName) {
|
||||
fileIconSVG = await loadIconSVG(
|
||||
fileIconName,
|
||||
plugin,
|
||||
iconsPath,
|
||||
prefixToIconPack
|
||||
)
|
||||
} else {
|
||||
// Fallback to default icons based on file type
|
||||
fileIconSVG = getDefaultIconSVG(note.path, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
// Svelte action to render SVG content with dynamic updates
|
||||
function renderSVG(node: HTMLElement, svgContent: string) {
|
||||
node.innerHTML = svgContent
|
||||
return {
|
||||
update(newSvgContent: string) {
|
||||
node.innerHTML = newSvgContent
|
||||
},
|
||||
destroy() {
|
||||
node.innerHTML = ''
|
||||
},
|
||||
}
|
||||
}
|
||||
let elFolderPathIcon: HTMLElement
|
||||
let elFilePathIcon: HTMLElement
|
||||
let elEmbedIcon: HTMLElement
|
||||
|
||||
$: {
|
||||
imagePath = null
|
||||
@@ -31,9 +111,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
|
||||
$: matchesNotePath = plugin.textProcessor.getMatches(notePath, note.foundWords)
|
||||
$: cleanedContent = plugin.textProcessor.makeExcerpt(note.content, note.matches[0]?.offset ?? -1)
|
||||
$: matchesNotePath = plugin.textProcessor.getMatches(
|
||||
notePath,
|
||||
note.foundWords
|
||||
)
|
||||
$: cleanedContent = plugin.textProcessor.makeExcerpt(
|
||||
note.content,
|
||||
note.matches[0]?.offset ?? -1
|
||||
)
|
||||
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
|
||||
$: {
|
||||
title = note.displayTitle || note.basename
|
||||
@@ -46,23 +133,24 @@
|
||||
if (elFilePathIcon) {
|
||||
if (isFileImage(note.path)) {
|
||||
setIcon(elFilePathIcon, 'image')
|
||||
}
|
||||
else if (isFilePDF(note.path)) {
|
||||
} else if (isFilePDF(note.path)) {
|
||||
setIcon(elFilePathIcon, 'file-text')
|
||||
}
|
||||
else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
|
||||
} else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
|
||||
setIcon(elFilePathIcon, 'layout-dashboard')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
setIcon(elFilePathIcon, 'file')
|
||||
}
|
||||
}
|
||||
if (elEmbedIcon) {
|
||||
setIcon(elEmbedIcon, 'corner-down-right')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ResultItemContainer
|
||||
glyph="{glyph}"
|
||||
id="{note.path}"
|
||||
cssClass=" {note.isEmbed ? 'omnisearch-result__embed' : ''}"
|
||||
on:auxclick
|
||||
on:click
|
||||
on:mousemove
|
||||
@@ -70,8 +158,19 @@
|
||||
<div>
|
||||
<div class="omnisearch-result__title-container">
|
||||
<span class="omnisearch-result__title">
|
||||
<span bind:this="{elFilePathIcon}"></span>
|
||||
<span>{@html plugin.textProcessor.highlightText(title, matchesTitle)}</span>
|
||||
{#if note.isEmbed}
|
||||
<span
|
||||
bind:this="{elEmbedIcon}"
|
||||
title="The document above is embedded in this note"></span>
|
||||
{:else}
|
||||
<!-- File Icon -->
|
||||
{#if fileIconSVG}
|
||||
<span class="omnisearch-result__icon" use:renderSVG="{fileIconSVG}"></span>
|
||||
{/if}
|
||||
{/if}
|
||||
<span>
|
||||
{@html plugin.textProcessor.highlightText(title, matchesTitle)}
|
||||
</span>
|
||||
<span class="omnisearch-result__extension">
|
||||
.{getExtension(note.path)}
|
||||
</span>
|
||||
@@ -90,15 +189,25 @@
|
||||
<!-- Folder path -->
|
||||
{#if notePath}
|
||||
<div class="omnisearch-result__folder-path">
|
||||
<span bind:this="{elFolderPathIcon}"></span>
|
||||
<span>{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}</span>
|
||||
<!-- Folder Icon -->
|
||||
{#if folderIconSVG}
|
||||
<span class="omnisearch-result__icon" use:renderSVG="{folderIconSVG}"></span>
|
||||
{/if}
|
||||
<span>
|
||||
{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Do not display the excerpt for embedding references -->
|
||||
{#if !note.isEmbed}
|
||||
<div style="display: flex; flex-direction: row;">
|
||||
{#if $showExcerpt}
|
||||
<div class="omnisearch-result__body">
|
||||
{@html plugin.textProcessor.highlightText(cleanedContent, note.matches)}
|
||||
{@html plugin.textProcessor.highlightText(
|
||||
cleanedContent,
|
||||
note.matches
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -109,5 +218,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ResultItemContainer>
|
||||
|
||||
@@ -163,7 +163,7 @@ export class OmnisearchVaultModal extends OmnisearchModal {
|
||||
.getActiveViewOfType(MarkdownView)
|
||||
?.editor.getSelection()
|
||||
|
||||
plugin.cacheManager.getSearchHistory().then(history => {
|
||||
plugin.searchHistory.getHistory().then(history => {
|
||||
// Previously searched query (if enabled in settings)
|
||||
const previous = plugin.settings.showPreviousQueryResults
|
||||
? history[0]
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import Dexie from 'dexie'
|
||||
import type MiniSearch from 'minisearch'
|
||||
import type { AsPlainObject } from 'minisearch'
|
||||
import type { DocumentRef } from './globals'
|
||||
import { Notice } from 'obsidian'
|
||||
import type OmnisearchPlugin from './main'
|
||||
|
||||
export class Database extends Dexie {
|
||||
public static readonly dbVersion = 8
|
||||
public static readonly dbVersion = 10
|
||||
searchHistory!: Dexie.Table<{ id?: number; query: string }, number>
|
||||
minisearch!: Dexie.Table<
|
||||
{
|
||||
@@ -16,6 +15,7 @@ export class Database extends Dexie {
|
||||
},
|
||||
string
|
||||
>
|
||||
embeds!: Dexie.Table<{ embedded: string; referencedBy: string[] }, string>
|
||||
|
||||
constructor(private plugin: OmnisearchPlugin) {
|
||||
super(Database.getDbName(plugin.app.appId))
|
||||
@@ -23,6 +23,7 @@ export class Database extends Dexie {
|
||||
this.version(Database.dbVersion).stores({
|
||||
searchHistory: '++id',
|
||||
minisearch: 'date',
|
||||
embeds: 'embedded',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,17 +50,15 @@ export class Database extends Dexie {
|
||||
}
|
||||
}
|
||||
|
||||
public async writeMinisearchCache(
|
||||
minisearch: MiniSearch,
|
||||
indexed: Map<string, number>
|
||||
): Promise<void> {
|
||||
const paths = Array.from(indexed).map(([k, v]) => ({ path: k, mtime: v }))
|
||||
public async writeMinisearchCache(): Promise<void> {
|
||||
const minisearchJson = this.plugin.searchEngine.getSerializedMiniSearch()
|
||||
const paths = this.plugin.searchEngine.getSerializedIndexedDocuments()
|
||||
const database = this.plugin.database
|
||||
await database.minisearch.clear()
|
||||
await database.minisearch.add({
|
||||
date: new Date().toISOString(),
|
||||
paths,
|
||||
data: minisearch.toJSON(),
|
||||
data: minisearchJson,
|
||||
})
|
||||
console.log('Omnisearch - Search cache written')
|
||||
}
|
||||
@@ -85,7 +84,8 @@ export class Database extends Dexie {
|
||||
}
|
||||
|
||||
public async clearCache() {
|
||||
new Notice('Omnisearch - Cache cleared. Please restart Obsidian.')
|
||||
await this.minisearch.clear()
|
||||
await this.embeds.clear()
|
||||
new Notice('Omnisearch - Cache cleared. Please restart Obsidian.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export type ResultNote = {
|
||||
content: string
|
||||
foundWords: string[]
|
||||
matches: SearchMatch[]
|
||||
isEmbed: boolean
|
||||
}
|
||||
|
||||
let inComposition = false
|
||||
|
||||
28
src/main.ts
28
src/main.ts
@@ -24,30 +24,33 @@ import {
|
||||
import { notifyOnIndexed, registerAPI } from './tools/api'
|
||||
import { Database } from './database'
|
||||
import { SearchEngine } from './search/search-engine'
|
||||
import { CacheManager } from './cache-manager'
|
||||
import { DocumentsRepository } from './repositories/documents-repository'
|
||||
import { logDebug } from './tools/utils'
|
||||
import { NotesIndexer } from './notes-indexer'
|
||||
import { TextProcessor } from './tools/text-processing'
|
||||
import { EmbedsRepository } from './repositories/embeds-repository'
|
||||
import { SearchHistory } from "./search/search-history";
|
||||
|
||||
export default class OmnisearchPlugin extends Plugin {
|
||||
// FIXME: fix the type
|
||||
public apiHttpServer: null | any = null
|
||||
public settings: OmnisearchSettings = getDefaultSettings(this.app)
|
||||
|
||||
// FIXME: merge cache and cacheManager, or find other names
|
||||
public readonly cacheManager: CacheManager
|
||||
public readonly documentsRepository: DocumentsRepository
|
||||
public readonly embedsRepository = new EmbedsRepository(this)
|
||||
public readonly database = new Database(this)
|
||||
|
||||
public readonly notesIndexer = new NotesIndexer(this)
|
||||
public readonly textProcessor = new TextProcessor(this)
|
||||
public readonly searchEngine = new SearchEngine(this)
|
||||
public readonly searchHistory = new SearchHistory(this)
|
||||
|
||||
private ribbonButton?: HTMLElement
|
||||
private refreshIndexCallback?: () => void
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest)
|
||||
this.cacheManager = new CacheManager(this)
|
||||
this.documentsRepository = new DocumentsRepository(this)
|
||||
}
|
||||
|
||||
async onload(): Promise<void> {
|
||||
@@ -109,14 +112,16 @@ export default class OmnisearchPlugin extends Plugin {
|
||||
if (this.notesIndexer.isFileIndexable(file.path)) {
|
||||
logDebug('Indexing new file', file.path)
|
||||
searchEngine.addFromPaths([file.path])
|
||||
this.embedsRepository.refreshEmbedsForNote(file.path)
|
||||
}
|
||||
})
|
||||
)
|
||||
this.registerEvent(
|
||||
this.app.vault.on('delete', file => {
|
||||
logDebug('Removing file', file.path)
|
||||
this.cacheManager.removeFromLiveCache(file.path)
|
||||
this.documentsRepository.removeDocument(file.path)
|
||||
searchEngine.removeFromPaths([file.path])
|
||||
this.embedsRepository.removeFile(file.path)
|
||||
})
|
||||
)
|
||||
this.registerEvent(
|
||||
@@ -124,16 +129,20 @@ export default class OmnisearchPlugin extends Plugin {
|
||||
if (this.notesIndexer.isFileIndexable(file.path)) {
|
||||
this.notesIndexer.flagNoteForReindex(file)
|
||||
}
|
||||
this.embedsRepository.refreshEmbedsForNote(file.path)
|
||||
})
|
||||
)
|
||||
this.registerEvent(
|
||||
this.app.vault.on('rename', async (file, oldPath) => {
|
||||
if (this.notesIndexer.isFileIndexable(file.path)) {
|
||||
logDebug('Renaming file', file.path)
|
||||
this.cacheManager.removeFromLiveCache(oldPath)
|
||||
await this.cacheManager.addToLiveCache(file.path)
|
||||
this.documentsRepository.removeDocument(oldPath)
|
||||
await this.documentsRepository.addDocument(file.path)
|
||||
|
||||
searchEngine.removeFromPaths([oldPath])
|
||||
await searchEngine.addFromPaths([file.path])
|
||||
|
||||
this.embedsRepository.renameFile(oldPath, file.path)
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -240,7 +249,7 @@ export default class OmnisearchPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
const diff = searchEngine.getDiff(
|
||||
const diff = searchEngine.getDocumentsToReindex(
|
||||
files.map(f => ({ path: f.path, mtime: f.stat.mtime }))
|
||||
)
|
||||
|
||||
@@ -281,7 +290,8 @@ export default class OmnisearchPlugin extends Plugin {
|
||||
}
|
||||
|
||||
// Write the cache
|
||||
await searchEngine.writeToCache()
|
||||
await this.database.writeMinisearchCache()
|
||||
await this.embedsRepository.writeToCache()
|
||||
|
||||
// Re-enable settings.caching
|
||||
if (cacheEnabled) {
|
||||
|
||||
@@ -26,7 +26,7 @@ export class NotesIndexer {
|
||||
public async refreshIndex(): Promise<void> {
|
||||
for (const file of this.notesToReindex) {
|
||||
logDebug('Updating file', file.path)
|
||||
await this.plugin.cacheManager.addToLiveCache(file.path)
|
||||
await this.plugin.documentsRepository.addDocument(file.path)
|
||||
}
|
||||
|
||||
const paths = [...this.notesToReindex].map(n => n.path)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TFile } from 'obsidian'
|
||||
import type { IndexedDocument } from './globals'
|
||||
import { normalizePath, Notice, TFile } from 'obsidian'
|
||||
import type { IndexedDocument } from '../globals'
|
||||
import {
|
||||
extractHeadingsFromCache,
|
||||
getAliasesFromMetadata,
|
||||
@@ -12,30 +12,33 @@ import {
|
||||
logDebug,
|
||||
removeDiacritics,
|
||||
stripMarkdownCharacters,
|
||||
} from './tools/utils'
|
||||
} from '../tools/utils'
|
||||
import type { CanvasData } from 'obsidian/canvas'
|
||||
import type OmnisearchPlugin from './main'
|
||||
import { getNonExistingNotes } from './tools/notes'
|
||||
|
||||
export class CacheManager {
|
||||
/**
|
||||
* Show an empty input field next time the user opens Omnisearch modal
|
||||
*/
|
||||
private nextQueryIsEmpty = false
|
||||
import type OmnisearchPlugin from '../main'
|
||||
import { getNonExistingNotes } from '../tools/notes'
|
||||
|
||||
export class DocumentsRepository {
|
||||
/**
|
||||
* The "live cache", containing all indexed vault files
|
||||
* in the form of IndexedDocuments
|
||||
*/
|
||||
private documents: Map<string, IndexedDocument> = new Map()
|
||||
private errorsCount = 0
|
||||
private errorsWarned = false
|
||||
|
||||
constructor(private plugin: OmnisearchPlugin) {}
|
||||
constructor(private plugin: OmnisearchPlugin) {
|
||||
setInterval(() => {
|
||||
if (this.errorsCount > 0) {
|
||||
--this.errorsCount
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update the live cache with the content of the given file.
|
||||
* @param path
|
||||
*/
|
||||
public async addToLiveCache(path: string): Promise<void> {
|
||||
public async addDocument(path: string): Promise<void> {
|
||||
try {
|
||||
const doc = await this.getAndMapIndexedDocument(path)
|
||||
if (!doc.path) {
|
||||
@@ -45,14 +48,16 @@ export class CacheManager {
|
||||
return
|
||||
}
|
||||
this.documents.set(path, doc)
|
||||
this.plugin.embedsRepository.refreshEmbedsForNote(path)
|
||||
} catch (e) {
|
||||
console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e)
|
||||
// Shouldn't be needed, but...
|
||||
this.removeFromLiveCache(path)
|
||||
this.removeDocument(path)
|
||||
this.countError()
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromLiveCache(path: string): void {
|
||||
public removeDocument(path: string): void {
|
||||
this.documents.delete(path)
|
||||
}
|
||||
|
||||
@@ -61,38 +66,10 @@ export class CacheManager {
|
||||
return this.documents.get(path)!
|
||||
}
|
||||
logDebug('Generating IndexedDocument from', path)
|
||||
await this.addToLiveCache(path)
|
||||
await this.addDocument(path)
|
||||
return this.documents.get(path)!
|
||||
}
|
||||
|
||||
public async addToSearchHistory(query: string): Promise<void> {
|
||||
if (!query) {
|
||||
this.nextQueryIsEmpty = true
|
||||
return
|
||||
}
|
||||
this.nextQueryIsEmpty = false
|
||||
const database = this.plugin.database
|
||||
let history = await database.searchHistory.toArray()
|
||||
history = history.filter(s => s.query !== query).reverse()
|
||||
history.unshift({ query })
|
||||
history = history.slice(0, 10)
|
||||
await database.searchHistory.clear()
|
||||
await database.searchHistory.bulkAdd(history)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The search history, in reverse chronological order
|
||||
*/
|
||||
public async getSearchHistory(): Promise<ReadonlyArray<string>> {
|
||||
const data = (await this.plugin.database.searchHistory.toArray())
|
||||
.reverse()
|
||||
.map(o => o.query)
|
||||
if (this.nextQueryIsEmpty) {
|
||||
data.unshift('')
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is responsible for extracting the text from a file and
|
||||
* returning it as an `IndexedDocument` object.
|
||||
@@ -101,6 +78,7 @@ export class CacheManager {
|
||||
private async getAndMapIndexedDocument(
|
||||
path: string
|
||||
): Promise<IndexedDocument> {
|
||||
path = normalizePath(path)
|
||||
const app = this.plugin.app
|
||||
const file = app.vault.getAbstractFileByPath(path)
|
||||
if (!file) throw new Error(`Invalid file path: "${path}"`)
|
||||
@@ -167,12 +145,18 @@ export class CacheManager {
|
||||
(this.plugin.settings.aiImageIndexing &&
|
||||
aiImageAnalyzer?.canBeAnalyzed(file)))
|
||||
) {
|
||||
if (this.plugin.settings.imagesIndexing && extractor?.canFileBeExtracted(path)){
|
||||
if (
|
||||
this.plugin.settings.imagesIndexing &&
|
||||
extractor?.canFileBeExtracted(path)
|
||||
) {
|
||||
content = await extractor.extractText(file)
|
||||
}
|
||||
|
||||
if (this.plugin.settings.aiImageIndexing && aiImageAnalyzer?.canBeAnalyzed(file)) {
|
||||
content = await aiImageAnalyzer.analyzeImage(file) + (content ?? '')
|
||||
if (
|
||||
this.plugin.settings.aiImageIndexing &&
|
||||
aiImageAnalyzer?.canBeAnalyzed(file)
|
||||
) {
|
||||
content = (await aiImageAnalyzer.analyzeImage(file)) + (content ?? '')
|
||||
}
|
||||
}
|
||||
// ** PDF **
|
||||
@@ -230,7 +214,8 @@ export class CacheManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
const displayTitle = metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? ''
|
||||
const displayTitle =
|
||||
metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? ''
|
||||
const tags = getTagsFromMetadata(metadata)
|
||||
return {
|
||||
basename: file.basename,
|
||||
@@ -255,4 +240,13 @@ export class CacheManager {
|
||||
: '',
|
||||
}
|
||||
}
|
||||
|
||||
private countError(): void {
|
||||
if (++this.errorsCount > 5 && !this.errorsWarned) {
|
||||
this.errorsWarned = true
|
||||
new Notice(
|
||||
'Omnisearch ⚠️ There might be an issue with your cache. You should clean it in Omnisearch settings and restart Obsidian.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/repositories/embeds-repository.ts
Normal file
106
src/repositories/embeds-repository.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { getLinkpath, Notice } from 'obsidian'
|
||||
import type OmnisearchPlugin from '../main'
|
||||
import { logDebug } from '../tools/utils'
|
||||
|
||||
export class EmbedsRepository {
|
||||
/** Map<embedded file, notes where the embed is referenced> */
|
||||
private embeds: Map<string, Set<string>> = new Map()
|
||||
|
||||
constructor(private plugin: OmnisearchPlugin) {}
|
||||
|
||||
public addEmbed(embed: string, notePath: string): void {
|
||||
if (!this.embeds.has(embed)) {
|
||||
this.embeds.set(embed, new Set())
|
||||
}
|
||||
this.embeds.get(embed)!.add(notePath)
|
||||
}
|
||||
|
||||
public removeFile(filePath: string): void {
|
||||
// If the file is embedded
|
||||
this.embeds.delete(filePath)
|
||||
// If the file is a note referencing other files
|
||||
this.refreshEmbedsForNote(filePath)
|
||||
}
|
||||
|
||||
public renameFile(oldPath: string, newPath: string): void {
|
||||
// If the file is embedded
|
||||
if (this.embeds.has(oldPath)) {
|
||||
this.embeds.set(newPath, this.embeds.get(oldPath)!)
|
||||
this.embeds.delete(oldPath)
|
||||
}
|
||||
// If the file is a note referencing other files
|
||||
this.embeds.forEach((referencedBy, key) => {
|
||||
if (referencedBy.has(oldPath)) {
|
||||
referencedBy.delete(oldPath)
|
||||
referencedBy.add(newPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public refreshEmbedsForNote(filePath: string): void {
|
||||
this.embeds.forEach((referencedBy, key) => {
|
||||
if (referencedBy.has(filePath)) {
|
||||
referencedBy.delete(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
this.addEmbedsForNote(filePath)
|
||||
}
|
||||
|
||||
public getEmbeds(pathEmbedded: string): string[] {
|
||||
const embeds = this.embeds.has(pathEmbedded)
|
||||
? [...this.embeds.get(pathEmbedded)!]
|
||||
: []
|
||||
return embeds
|
||||
}
|
||||
|
||||
public async writeToCache(): Promise<void> {
|
||||
logDebug('Writing embeds to cache')
|
||||
const database = this.plugin.database
|
||||
const data: { embedded: string; referencedBy: string[] }[] = []
|
||||
for (const [path, embedsList] of this.embeds) {
|
||||
data.push({ embedded: path, referencedBy: [...embedsList] })
|
||||
}
|
||||
await database.embeds.clear()
|
||||
await database.embeds.bulkAdd(data)
|
||||
}
|
||||
|
||||
public async loadFromCache(): Promise<void> {
|
||||
try {
|
||||
const database = this.plugin.database
|
||||
if (!database.embeds) {
|
||||
logDebug('No embeds in cache')
|
||||
return
|
||||
}
|
||||
logDebug('Loading embeds from cache')
|
||||
const embedsArr = await database.embeds.toArray()
|
||||
for (const { embedded: path, referencedBy: embeds } of embedsArr) {
|
||||
for (const embed of embeds) {
|
||||
this.addEmbed(path, embed)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.plugin.database.clearCache()
|
||||
console.error('Omnisearch - Error while loading embeds cache')
|
||||
new Notice('Omnisearch - There was an error while loading the cache. Please restart Obsidian.')
|
||||
}
|
||||
}
|
||||
|
||||
private addEmbedsForNote(notePath: string): void {
|
||||
// Get all embeds from the note
|
||||
// and map them to TFiles to get the real path
|
||||
const embeds = (
|
||||
this.plugin.app.metadataCache.getCache(notePath)?.embeds ?? []
|
||||
)
|
||||
.map(embed =>
|
||||
this.plugin.app.metadataCache.getFirstLinkpathDest(
|
||||
getLinkpath(embed.link),
|
||||
notePath
|
||||
)
|
||||
)
|
||||
.filter(o => !!o)
|
||||
for (const embed of embeds) {
|
||||
this.addEmbed(embed!.path, notePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import MiniSearch, { type Options, type SearchResult } from 'minisearch'
|
||||
import MiniSearch, {
|
||||
type AsPlainObject,
|
||||
type Options,
|
||||
type SearchResult,
|
||||
} from 'minisearch'
|
||||
import type { DocumentRef, IndexedDocument, ResultNote } from '../globals'
|
||||
|
||||
import { chunkArray, logDebug, removeDiacritics } from '../tools/utils'
|
||||
@@ -13,6 +17,7 @@ export class SearchEngine {
|
||||
private minisearch: MiniSearch
|
||||
/** Map<path, mtime> */
|
||||
private indexedDocuments: Map<string, number> = new Map()
|
||||
|
||||
// private previousResults: SearchResult[] = []
|
||||
// private previousQuery: Query | null = null
|
||||
|
||||
@@ -25,6 +30,7 @@ export class SearchEngine {
|
||||
* Return true if the cache is valid
|
||||
*/
|
||||
async loadCache(): Promise<boolean> {
|
||||
await this.plugin.embedsRepository.loadFromCache()
|
||||
const cache = await this.plugin.database.getMinisearchCache()
|
||||
if (cache) {
|
||||
this.minisearch = await MiniSearch.loadJSAsync(
|
||||
@@ -39,10 +45,11 @@ export class SearchEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of documents that need to be reindexed
|
||||
* Returns the list of documents that need to be reindexed or removed,
|
||||
* either because they are new, have been modified, or have been deleted
|
||||
* @param docs
|
||||
*/
|
||||
getDiff(docs: DocumentRef[]): {
|
||||
getDocumentsToReindex(docs: DocumentRef[]): {
|
||||
toAdd: DocumentRef[]
|
||||
toRemove: DocumentRef[]
|
||||
} {
|
||||
@@ -72,7 +79,7 @@ export class SearchEngine {
|
||||
let documents = (
|
||||
await Promise.all(
|
||||
paths.map(
|
||||
async path => await this.plugin.cacheManager.getDocument(path)
|
||||
async path => await this.plugin.documentsRepository.getDocument(path)
|
||||
)
|
||||
)
|
||||
).filter(d => !!d?.path)
|
||||
@@ -167,7 +174,7 @@ export class SearchEngine {
|
||||
tokenize: text => [text],
|
||||
})
|
||||
|
||||
logDebug('Found', results.length, 'results')
|
||||
logDebug(`Found ${results.length} results`, results)
|
||||
|
||||
// Filter query results to only keep files that match query.query.ext (if any)
|
||||
if (query.query.ext?.length) {
|
||||
@@ -264,9 +271,9 @@ export class SearchEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Boost custom properties
|
||||
const metadata = this.plugin.app.metadataCache.getCache(path)
|
||||
if (metadata) {
|
||||
// Boost custom properties
|
||||
for (const { name, weight } of settings.weightCustomProperties) {
|
||||
const values = metadata?.frontmatter?.[name]
|
||||
if (values && result.terms.some(t => values.includes(t))) {
|
||||
@@ -288,11 +295,13 @@ export class SearchEngine {
|
||||
// Sort results and keep the 50 best
|
||||
results = results.sort((a, b) => b.score - a.score).slice(0, 50)
|
||||
|
||||
logDebug('Filtered results:', results)
|
||||
|
||||
if (results.length) logDebug('First result:', results[0])
|
||||
|
||||
const documents = await Promise.all(
|
||||
results.map(
|
||||
async result => await this.plugin.cacheManager.getDocument(result.id)
|
||||
async result => await this.plugin.documentsRepository.getDocument(result.id)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -368,10 +377,37 @@ export class SearchEngine {
|
||||
|
||||
const documents = await Promise.all(
|
||||
results.map(
|
||||
async result => await this.plugin.cacheManager.getDocument(result.id)
|
||||
async result => await this.plugin.documentsRepository.getDocument(result.id)
|
||||
)
|
||||
)
|
||||
|
||||
// Inject embeds for images, documents, and PDFs
|
||||
let total = documents.length
|
||||
for (let i = 0; i < total; i++) {
|
||||
const doc = documents[i]
|
||||
if (!doc) continue
|
||||
|
||||
const embeds = this.plugin.embedsRepository
|
||||
.getEmbeds(doc.path)
|
||||
.slice(0, this.plugin.settings.maxEmbeds)
|
||||
|
||||
// Inject embeds in the results
|
||||
for (const embed of embeds) {
|
||||
total++
|
||||
const newDoc = await this.plugin.documentsRepository.getDocument(embed)
|
||||
documents.splice(i + 1, 0, newDoc)
|
||||
results.splice(i + 1, 0, {
|
||||
id: newDoc.path,
|
||||
score: 0,
|
||||
terms: [],
|
||||
queryTerms: [],
|
||||
match: {},
|
||||
isEmbed: true,
|
||||
})
|
||||
i++ // Increment i to skip the newly inserted document
|
||||
}
|
||||
}
|
||||
|
||||
// Map the raw results to get usable suggestions
|
||||
const resultNotes = results.map(result => {
|
||||
logDebug('Locating matches for', result.id)
|
||||
@@ -407,23 +443,37 @@ export class SearchEngine {
|
||||
foundWords,
|
||||
query
|
||||
)
|
||||
logDebug(`Matches for ${note.basename}`, matches)
|
||||
logDebug(`Matches for note "${note.path}"`, matches)
|
||||
const resultNote: ResultNote = {
|
||||
score: result.score,
|
||||
foundWords,
|
||||
matches,
|
||||
isEmbed: result.isEmbed,
|
||||
...note,
|
||||
}
|
||||
return resultNote
|
||||
})
|
||||
|
||||
logDebug('Suggestions:', resultNotes)
|
||||
|
||||
return resultNotes
|
||||
}
|
||||
|
||||
public async writeToCache(): Promise<void> {
|
||||
await this.plugin.database.writeMinisearchCache(
|
||||
this.minisearch,
|
||||
this.indexedDocuments
|
||||
)
|
||||
/**
|
||||
* For cache saving
|
||||
*/
|
||||
public getSerializedMiniSearch(): AsPlainObject {
|
||||
return this.minisearch.toJSON()
|
||||
}
|
||||
|
||||
/**
|
||||
* For cache saving
|
||||
*/
|
||||
public getSerializedIndexedDocuments(): { path: string; mtime: number }[] {
|
||||
return Array.from(this.indexedDocuments).map(([path, mtime]) => ({
|
||||
path,
|
||||
mtime,
|
||||
}))
|
||||
}
|
||||
|
||||
private getOptions(): Options<IndexedDocument> {
|
||||
|
||||
38
src/search/search-history.ts
Normal file
38
src/search/search-history.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type OmnisearchPlugin from '../main'
|
||||
|
||||
export class SearchHistory {
|
||||
/**
|
||||
* Show an empty input field next time the user opens Omnisearch modal
|
||||
*/
|
||||
private nextQueryIsEmpty = false
|
||||
|
||||
constructor(private plugin: OmnisearchPlugin) {}
|
||||
|
||||
public async addToHistory(query: string): Promise<void> {
|
||||
if (!query) {
|
||||
this.nextQueryIsEmpty = true
|
||||
return
|
||||
}
|
||||
this.nextQueryIsEmpty = false
|
||||
const database = this.plugin.database
|
||||
let history = await database.searchHistory.toArray()
|
||||
history = history.filter(s => s.query !== query).reverse()
|
||||
history.unshift({ query })
|
||||
history = history.slice(0, 10)
|
||||
await database.searchHistory.clear()
|
||||
await database.searchHistory.bulkAdd(history)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The search history, in reverse chronological order
|
||||
*/
|
||||
public async getHistory(): Promise<ReadonlyArray<string>> {
|
||||
const data = (await this.plugin.database.searchHistory.toArray())
|
||||
.reverse()
|
||||
.map(o => o.query)
|
||||
if (this.nextQueryIsEmpty) {
|
||||
data.unshift('')
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ export interface OmnisearchSettings extends WeightingSettings {
|
||||
ribbonIcon: boolean
|
||||
/** Display the small contextual excerpt in search results */
|
||||
showExcerpt: boolean
|
||||
/** Number of embeds references to display in search results */
|
||||
maxEmbeds: number
|
||||
/** Render line returns with <br> in excerpts */
|
||||
renderLineReturnInExcerpts: boolean
|
||||
/** Enable a "create note" button in the Vault Search modal */
|
||||
@@ -465,6 +467,24 @@ export class SettingsTab extends PluginSettingTab {
|
||||
})
|
||||
)
|
||||
|
||||
// Show embeds
|
||||
new Setting(containerEl)
|
||||
.setName('Show embed references')
|
||||
.setDesc(
|
||||
htmlDescription(`Some results are <a href="https://help.obsidian.md/Linking+notes+and+files/Embed+files">embedded</a> in other notes.<br>
|
||||
This setting controls the maximum number of embeds to show in the search results. Set to 0 to disable.<br>
|
||||
Also works with Text Extractor for embedded images and documents.`)
|
||||
)
|
||||
.addSlider(cb => {
|
||||
cb.setLimits(0, 10, 1)
|
||||
.setValue(settings.maxEmbeds)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async v => {
|
||||
settings.maxEmbeds = v
|
||||
await saveSettings(this.plugin)
|
||||
})
|
||||
})
|
||||
|
||||
// Keep line returns in excerpts
|
||||
new Setting(containerEl)
|
||||
.setName('Render line return in excerpts')
|
||||
@@ -761,7 +781,7 @@ export class SettingsTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void {
|
||||
cb.setLimits(1, 5, 0.1)
|
||||
cb.setLimits(1, 10, 0.5)
|
||||
.setValue(settings[key])
|
||||
.setDynamicTooltip()
|
||||
.onChange(async v => {
|
||||
@@ -791,6 +811,7 @@ export function getDefaultSettings(app: App): OmnisearchSettings {
|
||||
|
||||
ribbonIcon: true,
|
||||
showExcerpt: true,
|
||||
maxEmbeds: 5,
|
||||
renderLineReturnInExcerpts: true,
|
||||
showCreateButton: false,
|
||||
highlight: true,
|
||||
@@ -799,12 +820,12 @@ export function getDefaultSettings(app: App): OmnisearchSettings {
|
||||
tokenizeUrls: false,
|
||||
fuzziness: '1',
|
||||
|
||||
weightBasename: 3,
|
||||
weightDirectory: 2,
|
||||
weightH1: 1.5,
|
||||
weightH2: 1.3,
|
||||
weightH3: 1.1,
|
||||
weightUnmarkedTags: 1.1,
|
||||
weightBasename: 10,
|
||||
weightDirectory: 7,
|
||||
weightH1: 6,
|
||||
weightH2: 5,
|
||||
weightH3: 4,
|
||||
weightUnmarkedTags: 2,
|
||||
weightCustomProperties: [] as { name: string; weight: number }[],
|
||||
|
||||
httpApiEnabled: false,
|
||||
|
||||
188
src/tools/icon-utils.ts
Normal file
188
src/tools/icon-utils.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { TFile, getIcon, normalizePath } from 'obsidian'
|
||||
import type OmnisearchPlugin from '../main'
|
||||
import {
|
||||
isFileImage,
|
||||
isFilePDF,
|
||||
isFileCanvas,
|
||||
isFileExcalidraw,
|
||||
warnDebug,
|
||||
} from './utils'
|
||||
import { escapeHTML } from './text-processing'
|
||||
|
||||
export interface IconPacks {
|
||||
prefixToIconPack: { [prefix: string]: string }
|
||||
iconsPath: string
|
||||
}
|
||||
|
||||
export async function loadIconData(plugin: OmnisearchPlugin): Promise<any> {
|
||||
const app = plugin.app
|
||||
|
||||
// Check if the 'obsidian-icon-folder' plugin is installed and enabled
|
||||
// Casting 'app' to 'any' here to avoid TypeScript errors since 'plugins' might not be defined on 'App'
|
||||
const iconFolderPlugin = (app as any).plugins.getPlugin(
|
||||
'obsidian-icon-folder'
|
||||
)
|
||||
if (!iconFolderPlugin) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const dataJsonPath = `${app.vault.configDir}/plugins/obsidian-icon-folder/data.json`
|
||||
try {
|
||||
const dataJsonContent = await app.vault.adapter.read(dataJsonPath)
|
||||
const rawIconData = JSON.parse(dataJsonContent)
|
||||
// Normalize keys
|
||||
const iconData: any = {}
|
||||
for (const key in rawIconData) {
|
||||
const normalizedKey = normalizePath(key)
|
||||
iconData[normalizedKey] = rawIconData[key]
|
||||
}
|
||||
return iconData
|
||||
} catch (e) {
|
||||
warnDebug('Failed to read data.json:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeIconPacks(
|
||||
plugin: OmnisearchPlugin
|
||||
): Promise<IconPacks> {
|
||||
// Add 'Li' prefix for Lucide icons
|
||||
const prefixToIconPack: { [prefix: string]: string } = { Li: 'lucide-icons' }
|
||||
let iconsPath = 'icons'
|
||||
|
||||
const app = plugin.app
|
||||
|
||||
// Access the obsidian-icon-folder plugin
|
||||
const iconFolderPlugin = (app as any).plugins.getPlugin(
|
||||
'obsidian-icon-folder'
|
||||
)
|
||||
|
||||
if (iconFolderPlugin) {
|
||||
// Get the icons path from the plugin's settings
|
||||
const iconFolderSettings = iconFolderPlugin.settings
|
||||
iconsPath = iconFolderSettings?.iconPacksPath || 'icons'
|
||||
const iconsDir = `${app.vault.configDir}/${iconsPath}`
|
||||
|
||||
try {
|
||||
const iconPackDirs = await app.vault.adapter.list(iconsDir)
|
||||
if (iconPackDirs.folders && iconPackDirs.folders.length > 0) {
|
||||
for (const folderPath of iconPackDirs.folders) {
|
||||
const pathParts = folderPath.split('/')
|
||||
const iconPackName = pathParts[pathParts.length - 1]
|
||||
const prefix = createIconPackPrefix(iconPackName)
|
||||
prefixToIconPack[prefix] = iconPackName
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
warnDebug('Failed to list icon packs:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return { prefixToIconPack, iconsPath }
|
||||
}
|
||||
|
||||
function createIconPackPrefix(iconPackName: string): string {
|
||||
if (iconPackName.includes('-')) {
|
||||
const splitted = iconPackName.split('-')
|
||||
let result = splitted[0].charAt(0).toUpperCase()
|
||||
for (let i = 1; i < splitted.length; i++) {
|
||||
result += splitted[i].charAt(0).toLowerCase()
|
||||
}
|
||||
return result
|
||||
}
|
||||
return (
|
||||
iconPackName.charAt(0).toUpperCase() + iconPackName.charAt(1).toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
export function getIconNameForPath(path: string, iconData: any): string | null {
|
||||
const normalizedPath = normalizePath(path)
|
||||
const iconEntry = iconData[normalizedPath]
|
||||
if (iconEntry) {
|
||||
if (typeof iconEntry === 'string') {
|
||||
return iconEntry
|
||||
} else if (typeof iconEntry === 'object' && iconEntry.iconName) {
|
||||
return iconEntry.iconName
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function parseIconName(iconName: string): {
|
||||
prefix: string
|
||||
name: string
|
||||
} {
|
||||
const prefixMatch = iconName.match(/^[A-Z][a-z]*/)
|
||||
if (prefixMatch) {
|
||||
const prefix = prefixMatch[0]
|
||||
const name = iconName.substring(prefix.length)
|
||||
return { prefix, name }
|
||||
} else {
|
||||
// No prefix, treat the entire iconName as the name
|
||||
return { prefix: '', name: iconName }
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadIconSVG(
|
||||
iconName: string,
|
||||
plugin: OmnisearchPlugin,
|
||||
iconsPath: string,
|
||||
prefixToIconPack: { [prefix: string]: string }
|
||||
): Promise<string | null> {
|
||||
const parsed = parseIconName(iconName)
|
||||
const { prefix, name } = parsed
|
||||
|
||||
if (!prefix) {
|
||||
// No prefix, assume it's an emoji or text
|
||||
return `<span class="omnisearch-result__icon--emoji">${escapeHTML(name)}</span>`
|
||||
}
|
||||
|
||||
const iconPackName = prefixToIconPack[prefix]
|
||||
|
||||
if (!iconPackName) {
|
||||
warnDebug(`No icon pack found for prefix: ${prefix}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (iconPackName === 'lucide-icons') {
|
||||
// Convert CamelCase to dash-case for Lucide icons
|
||||
const dashedName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
const iconEl = getIcon(dashedName)
|
||||
if (iconEl) {
|
||||
return iconEl.outerHTML
|
||||
} else {
|
||||
warnDebug(`Lucide icon not found: ${dashedName}`)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
if (!iconsPath) {
|
||||
warnDebug('Icons path is not set. Cannot load icon SVG.')
|
||||
return null
|
||||
}
|
||||
const iconPath = `${plugin.app.vault.configDir}/${iconsPath}/${iconPackName}/${name}.svg`
|
||||
try {
|
||||
const svgContent = await plugin.app.vault.adapter.read(iconPath)
|
||||
return svgContent
|
||||
} catch (e) {
|
||||
warnDebug(`Failed to load icon SVG for ${iconName} at ${iconPath}:`, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultIconSVG(
|
||||
notePath: string,
|
||||
plugin: OmnisearchPlugin
|
||||
): string {
|
||||
// Return SVG content for default icons based on file type
|
||||
let iconName = 'file'
|
||||
if (isFileImage(notePath)) {
|
||||
iconName = 'image'
|
||||
} else if (isFilePDF(notePath)) {
|
||||
iconName = 'file-text'
|
||||
} else if (isFileCanvas(notePath) || isFileExcalidraw(notePath)) {
|
||||
iconName = 'layout-dashboard'
|
||||
}
|
||||
const iconEl = getIcon(iconName)
|
||||
return iconEl ? iconEl.outerHTML : ''
|
||||
}
|
||||
@@ -78,15 +78,6 @@ export class TextProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
escapeHTML(html: string): string {
|
||||
return html
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of strings to a list of words, using the \b word boundary.
|
||||
* Used to find excerpts in a note body, or select which words to highlight.
|
||||
@@ -200,7 +191,7 @@ export class TextProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHTML(html: string): string {
|
||||
export function escapeHTML(html: string): string {
|
||||
return html
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
|
||||
@@ -151,7 +151,7 @@ export function getCtrlKeyLabel(): 'ctrl' | '⌘' {
|
||||
|
||||
export function isFileImage(path: string): boolean {
|
||||
const ext = getExtension(path)
|
||||
return ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp'
|
||||
return ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp' || ext === 'gif'
|
||||
}
|
||||
|
||||
export function isFilePDF(path: string): boolean {
|
||||
|
||||
@@ -149,7 +149,5 @@
|
||||
"1.24.0": "1.3.0",
|
||||
"1.24.1": "1.3.0",
|
||||
"1.25.0-beta.1": "1.3.0",
|
||||
"1.25.0-beta.2": "1.3.0",
|
||||
"1.25.0-beta.3": "1.3.0",
|
||||
"1.25.0-beta.4": "1.3.0"
|
||||
"1.25.0-beta.2": "1.3.0"
|
||||
}
|
||||
Reference in New Issue
Block a user