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
|
id: git-cliff
|
||||||
with:
|
with:
|
||||||
config: cliff.toml
|
config: cliff.toml
|
||||||
args: --verbose
|
args: -vv --latest --strip header
|
||||||
env:
|
env:
|
||||||
GITHUB_REPO: ${{ github.repository }}
|
GITHUB_REPO: ${{ github.repository }}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ Please read this document before beginning work on a Pull Request.
|
|||||||
## Preface
|
## 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.
|
- 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.
|
- ❗ 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
|
|
||||||
|
|
||||||
## "Good First Issue"
|
## "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
|
## 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.
|
- 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.
|
- Use Svelte for all UI needs.
|
||||||
- Try to not shoehorn your code into existing functions or components.
|
- 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"
|
- .ts files must be formatted with "Prettier ESLint"
|
||||||
- .svelte files must be formatted with "Svelte for VS Code"
|
- .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).
|
- 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.**
|
- 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, ...
|
> [!INFO] Chinese users
|
||||||
> If you have notes in a CJK language, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch)
|
> If you have notes in Chinese, you should install [this additional plugin](https://github.com/aidenlx/cm-chs-patch) for better search results.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -22,8 +22,9 @@ Omnisearch is licensed under [GPL-3](https://tldrlegal.com/license/gnu-general-p
|
|||||||
|
|
||||||
## Thanks
|
## 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)" :)
|
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;
|
white-space: normal;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
/* justify-content: space-between; */
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +59,11 @@
|
|||||||
margin-inline-start: 0.5em;
|
margin-inline-start: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.omnisearch-result__embed {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.omnisearch-result__image-container {
|
.omnisearch-result__image-container {
|
||||||
flex-basis: 20%;
|
flex-basis: 20%;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
@@ -82,6 +87,25 @@
|
|||||||
gap: 5px;
|
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) {
|
@media only screen and (max-width: 600px) {
|
||||||
.omnisearch-input-container {
|
.omnisearch-input-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "omnisearch",
|
"id": "omnisearch",
|
||||||
"name": "Omnisearch",
|
"name": "Omnisearch",
|
||||||
"version": "1.25.0-beta.4",
|
"version": "1.25.0-beta.2",
|
||||||
"minAppVersion": "1.3.0",
|
"minAppVersion": "1.3.0",
|
||||||
"description": "A search engine that just works",
|
"description": "A search engine that just works",
|
||||||
"author": "Simon Cambier",
|
"author": "Simon Cambier",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "omnisearch",
|
"id": "omnisearch",
|
||||||
"name": "Omnisearch",
|
"name": "Omnisearch",
|
||||||
"version": "1.24.1",
|
"version": "1.24.1",
|
||||||
"minAppVersion": "1.3.0",
|
"minAppVersion": "1.7.2",
|
||||||
"description": "A search engine that just works",
|
"description": "A search engine that just works",
|
||||||
"author": "Simon Cambier",
|
"author": "Simon Cambier",
|
||||||
"authorUrl": "https://github.com/scambier/obsidian-omnisearch",
|
"authorUrl": "https://github.com/scambier/obsidian-omnisearch",
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "scambier.obsidian-search",
|
"name": "scambier.obsidian-search",
|
||||||
"version": "1.24.1",
|
"version": "1.25.0-beta.4",
|
||||||
"description": "A search engine for Obsidian",
|
"description": "A search engine for Obsidian",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
"author": "Simon Cambier",
|
"author": "Simon Cambier",
|
||||||
"license": "GPL-3",
|
"license": "GPL-3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.25.4",
|
"@babel/preset-env": "^7.25.8",
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.25.7",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@tsconfig/svelte": "^3.0.0",
|
"@tsconfig/svelte": "^3.0.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^16.18.108",
|
"@types/node": "^16.18.113",
|
||||||
"@types/pako": "^2.0.3",
|
"@types/pako": "^2.0.3",
|
||||||
"babel-jest": "^27.5.1",
|
"babel-jest": "^27.5.1",
|
||||||
"builtin-modules": "^3.3.0",
|
"builtin-modules": "^3.3.0",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"esbuild-plugin-copy": "1.3.0",
|
"esbuild-plugin-copy": "1.3.0",
|
||||||
"esbuild-svelte": "0.7.1",
|
"esbuild-svelte": "0.7.1",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"obsidian": "1.5.7-1",
|
"obsidian": "1.7.2",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
"svelte": "^3.59.2",
|
"svelte": "^3.59.2",
|
||||||
@@ -37,14 +37,14 @@
|
|||||||
"svelte-preprocess": "^4.10.7",
|
"svelte-preprocess": "^4.10.7",
|
||||||
"tslib": "2.3.1",
|
"tslib": "2.3.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^3.2.10"
|
"vite": "^3.2.11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cancelable-promise": "^4.3.1",
|
"cancelable-promise": "^4.3.1",
|
||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"markdown-link-extractor": "^4.0.2",
|
"markdown-link-extractor": "^4.0.2",
|
||||||
"minisearch": "github:scambier/minisearch#async-load-json",
|
"minisearch": "7.1.0",
|
||||||
"pure-md5": "^0.1.14",
|
"pure-md5": "^0.1.14",
|
||||||
"search-query-parser": "^1.6.0"
|
"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(() => {
|
const debouncedOnInput = debounce(() => {
|
||||||
// If typing a query and not executing it,
|
// If typing a query and not executing it,
|
||||||
// the next time we open the modal, the search field will be empty
|
// the next time we open the modal, the search field will be empty
|
||||||
plugin.cacheManager.addToSearchHistory('')
|
plugin.searchHistory.addToHistory('')
|
||||||
dispatch('input', value)
|
dispatch('input', value)
|
||||||
}, 300)
|
}, 300)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
|
|
||||||
async function prevSearchHistory() {
|
async function prevSearchHistory() {
|
||||||
// Filter out the empty string, if it's there
|
// 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
|
s => s
|
||||||
)
|
)
|
||||||
if (++historySearchIndex >= history.length) {
|
if (++historySearchIndex >= history.length) {
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function nextSearchHistory() {
|
async function nextSearchHistory() {
|
||||||
const history = (await plugin.cacheManager.getSearchHistory()).filter(
|
const history = (await plugin.searchHistory.getHistory()).filter(
|
||||||
s => s
|
s => s
|
||||||
)
|
)
|
||||||
if (--historySearchIndex < 0) {
|
if (--historySearchIndex < 0) {
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
function saveCurrentQuery() {
|
function saveCurrentQuery() {
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
plugin.cacheManager.addToSearchHistory(searchQuery)
|
plugin.searchHistory.addToHistory(searchQuery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
export let id: string
|
export let id: string
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let glyph = false
|
export let glyph = false
|
||||||
|
export let cssClass = ''
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-result-id={id}
|
data-result-id={id}
|
||||||
class="suggestion-item omnisearch-result"
|
class="suggestion-item omnisearch-result {cssClass}"
|
||||||
class:is-selected={selected}
|
class:is-selected={selected}
|
||||||
on:mousemove
|
on:mousemove
|
||||||
on:click
|
on:click
|
||||||
|
|||||||
@@ -3,14 +3,25 @@
|
|||||||
import type { ResultNote } from '../globals'
|
import type { ResultNote } from '../globals'
|
||||||
import {
|
import {
|
||||||
getExtension,
|
getExtension,
|
||||||
isFileCanvas, isFileExcalidraw,
|
isFileCanvas,
|
||||||
|
isFileExcalidraw,
|
||||||
isFileImage,
|
isFileImage,
|
||||||
isFilePDF,
|
isFilePDF,
|
||||||
pathWithoutFilename,
|
pathWithoutFilename,
|
||||||
} from '../tools/utils'
|
} from '../tools/utils'
|
||||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||||
import { TFile, setIcon } from 'obsidian'
|
|
||||||
import type OmnisearchPlugin from '../main'
|
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 selected = false
|
||||||
export let note: ResultNote
|
export let note: ResultNote
|
||||||
@@ -19,8 +30,77 @@
|
|||||||
let imagePath: string | null = null
|
let imagePath: string | null = null
|
||||||
let title = ''
|
let title = ''
|
||||||
let notePath = ''
|
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 elFolderPathIcon: HTMLElement
|
||||||
let elFilePathIcon: HTMLElement
|
let elFilePathIcon: HTMLElement
|
||||||
|
let elEmbedIcon: HTMLElement
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
imagePath = null
|
imagePath = null
|
||||||
@@ -31,9 +111,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
|
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
|
||||||
$: matchesNotePath = plugin.textProcessor.getMatches(notePath, note.foundWords)
|
$: matchesNotePath = plugin.textProcessor.getMatches(
|
||||||
$: cleanedContent = plugin.textProcessor.makeExcerpt(note.content, note.matches[0]?.offset ?? -1)
|
notePath,
|
||||||
|
note.foundWords
|
||||||
|
)
|
||||||
|
$: cleanedContent = plugin.textProcessor.makeExcerpt(
|
||||||
|
note.content,
|
||||||
|
note.matches[0]?.offset ?? -1
|
||||||
|
)
|
||||||
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
|
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
|
||||||
$: {
|
$: {
|
||||||
title = note.displayTitle || note.basename
|
title = note.displayTitle || note.basename
|
||||||
@@ -46,23 +133,24 @@
|
|||||||
if (elFilePathIcon) {
|
if (elFilePathIcon) {
|
||||||
if (isFileImage(note.path)) {
|
if (isFileImage(note.path)) {
|
||||||
setIcon(elFilePathIcon, 'image')
|
setIcon(elFilePathIcon, 'image')
|
||||||
}
|
} else if (isFilePDF(note.path)) {
|
||||||
else if (isFilePDF(note.path)) {
|
|
||||||
setIcon(elFilePathIcon, 'file-text')
|
setIcon(elFilePathIcon, 'file-text')
|
||||||
}
|
} else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
|
||||||
else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
|
|
||||||
setIcon(elFilePathIcon, 'layout-dashboard')
|
setIcon(elFilePathIcon, 'layout-dashboard')
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
setIcon(elFilePathIcon, 'file')
|
setIcon(elFilePathIcon, 'file')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (elEmbedIcon) {
|
||||||
|
setIcon(elEmbedIcon, 'corner-down-right')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ResultItemContainer
|
<ResultItemContainer
|
||||||
glyph="{glyph}"
|
glyph="{glyph}"
|
||||||
id="{note.path}"
|
id="{note.path}"
|
||||||
|
cssClass=" {note.isEmbed ? 'omnisearch-result__embed' : ''}"
|
||||||
on:auxclick
|
on:auxclick
|
||||||
on:click
|
on:click
|
||||||
on:mousemove
|
on:mousemove
|
||||||
@@ -70,8 +158,19 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="omnisearch-result__title-container">
|
<div class="omnisearch-result__title-container">
|
||||||
<span class="omnisearch-result__title">
|
<span class="omnisearch-result__title">
|
||||||
<span bind:this="{elFilePathIcon}"></span>
|
{#if note.isEmbed}
|
||||||
<span>{@html plugin.textProcessor.highlightText(title, matchesTitle)}</span>
|
<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">
|
<span class="omnisearch-result__extension">
|
||||||
.{getExtension(note.path)}
|
.{getExtension(note.path)}
|
||||||
</span>
|
</span>
|
||||||
@@ -90,15 +189,25 @@
|
|||||||
<!-- Folder path -->
|
<!-- Folder path -->
|
||||||
{#if notePath}
|
{#if notePath}
|
||||||
<div class="omnisearch-result__folder-path">
|
<div class="omnisearch-result__folder-path">
|
||||||
<span bind:this="{elFolderPathIcon}"></span>
|
<!-- Folder Icon -->
|
||||||
<span>{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}</span>
|
{#if folderIconSVG}
|
||||||
|
<span class="omnisearch-result__icon" use:renderSVG="{folderIconSVG}"></span>
|
||||||
|
{/if}
|
||||||
|
<span>
|
||||||
|
{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Do not display the excerpt for embedding references -->
|
||||||
|
{#if !note.isEmbed}
|
||||||
<div style="display: flex; flex-direction: row;">
|
<div style="display: flex; flex-direction: row;">
|
||||||
{#if $showExcerpt}
|
{#if $showExcerpt}
|
||||||
<div class="omnisearch-result__body">
|
<div class="omnisearch-result__body">
|
||||||
{@html plugin.textProcessor.highlightText(cleanedContent, note.matches)}
|
{@html plugin.textProcessor.highlightText(
|
||||||
|
cleanedContent,
|
||||||
|
note.matches
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -109,5 +218,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ResultItemContainer>
|
</ResultItemContainer>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export class OmnisearchVaultModal extends OmnisearchModal {
|
|||||||
.getActiveViewOfType(MarkdownView)
|
.getActiveViewOfType(MarkdownView)
|
||||||
?.editor.getSelection()
|
?.editor.getSelection()
|
||||||
|
|
||||||
plugin.cacheManager.getSearchHistory().then(history => {
|
plugin.searchHistory.getHistory().then(history => {
|
||||||
// Previously searched query (if enabled in settings)
|
// Previously searched query (if enabled in settings)
|
||||||
const previous = plugin.settings.showPreviousQueryResults
|
const previous = plugin.settings.showPreviousQueryResults
|
||||||
? history[0]
|
? history[0]
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import Dexie from 'dexie'
|
import Dexie from 'dexie'
|
||||||
import type MiniSearch from 'minisearch'
|
|
||||||
import type { AsPlainObject } from 'minisearch'
|
import type { AsPlainObject } from 'minisearch'
|
||||||
import type { DocumentRef } from './globals'
|
import type { DocumentRef } from './globals'
|
||||||
import { Notice } from 'obsidian'
|
import { Notice } from 'obsidian'
|
||||||
import type OmnisearchPlugin from './main'
|
import type OmnisearchPlugin from './main'
|
||||||
|
|
||||||
export class Database extends Dexie {
|
export class Database extends Dexie {
|
||||||
public static readonly dbVersion = 8
|
public static readonly dbVersion = 10
|
||||||
searchHistory!: Dexie.Table<{ id?: number; query: string }, number>
|
searchHistory!: Dexie.Table<{ id?: number; query: string }, number>
|
||||||
minisearch!: Dexie.Table<
|
minisearch!: Dexie.Table<
|
||||||
{
|
{
|
||||||
@@ -16,6 +15,7 @@ export class Database extends Dexie {
|
|||||||
},
|
},
|
||||||
string
|
string
|
||||||
>
|
>
|
||||||
|
embeds!: Dexie.Table<{ embedded: string; referencedBy: string[] }, string>
|
||||||
|
|
||||||
constructor(private plugin: OmnisearchPlugin) {
|
constructor(private plugin: OmnisearchPlugin) {
|
||||||
super(Database.getDbName(plugin.app.appId))
|
super(Database.getDbName(plugin.app.appId))
|
||||||
@@ -23,6 +23,7 @@ export class Database extends Dexie {
|
|||||||
this.version(Database.dbVersion).stores({
|
this.version(Database.dbVersion).stores({
|
||||||
searchHistory: '++id',
|
searchHistory: '++id',
|
||||||
minisearch: 'date',
|
minisearch: 'date',
|
||||||
|
embeds: 'embedded',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,17 +50,15 @@ export class Database extends Dexie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async writeMinisearchCache(
|
public async writeMinisearchCache(): Promise<void> {
|
||||||
minisearch: MiniSearch,
|
const minisearchJson = this.plugin.searchEngine.getSerializedMiniSearch()
|
||||||
indexed: Map<string, number>
|
const paths = this.plugin.searchEngine.getSerializedIndexedDocuments()
|
||||||
): Promise<void> {
|
|
||||||
const paths = Array.from(indexed).map(([k, v]) => ({ path: k, mtime: v }))
|
|
||||||
const database = this.plugin.database
|
const database = this.plugin.database
|
||||||
await database.minisearch.clear()
|
await database.minisearch.clear()
|
||||||
await database.minisearch.add({
|
await database.minisearch.add({
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
paths,
|
paths,
|
||||||
data: minisearch.toJSON(),
|
data: minisearchJson,
|
||||||
})
|
})
|
||||||
console.log('Omnisearch - Search cache written')
|
console.log('Omnisearch - Search cache written')
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,8 @@ export class Database extends Dexie {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async clearCache() {
|
public async clearCache() {
|
||||||
new Notice('Omnisearch - Cache cleared. Please restart Obsidian.')
|
|
||||||
await this.minisearch.clear()
|
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
|
content: string
|
||||||
foundWords: string[]
|
foundWords: string[]
|
||||||
matches: SearchMatch[]
|
matches: SearchMatch[]
|
||||||
|
isEmbed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let inComposition = false
|
let inComposition = false
|
||||||
|
|||||||
28
src/main.ts
28
src/main.ts
@@ -24,30 +24,33 @@ import {
|
|||||||
import { notifyOnIndexed, registerAPI } from './tools/api'
|
import { notifyOnIndexed, registerAPI } from './tools/api'
|
||||||
import { Database } from './database'
|
import { Database } from './database'
|
||||||
import { SearchEngine } from './search/search-engine'
|
import { SearchEngine } from './search/search-engine'
|
||||||
import { CacheManager } from './cache-manager'
|
import { DocumentsRepository } from './repositories/documents-repository'
|
||||||
import { logDebug } from './tools/utils'
|
import { logDebug } from './tools/utils'
|
||||||
import { NotesIndexer } from './notes-indexer'
|
import { NotesIndexer } from './notes-indexer'
|
||||||
import { TextProcessor } from './tools/text-processing'
|
import { TextProcessor } from './tools/text-processing'
|
||||||
|
import { EmbedsRepository } from './repositories/embeds-repository'
|
||||||
|
import { SearchHistory } from "./search/search-history";
|
||||||
|
|
||||||
export default class OmnisearchPlugin extends Plugin {
|
export default class OmnisearchPlugin extends Plugin {
|
||||||
// FIXME: fix the type
|
// FIXME: fix the type
|
||||||
public apiHttpServer: null | any = null
|
public apiHttpServer: null | any = null
|
||||||
public settings: OmnisearchSettings = getDefaultSettings(this.app)
|
public settings: OmnisearchSettings = getDefaultSettings(this.app)
|
||||||
|
|
||||||
// FIXME: merge cache and cacheManager, or find other names
|
public readonly documentsRepository: DocumentsRepository
|
||||||
public readonly cacheManager: CacheManager
|
public readonly embedsRepository = new EmbedsRepository(this)
|
||||||
public readonly database = new Database(this)
|
public readonly database = new Database(this)
|
||||||
|
|
||||||
public readonly notesIndexer = new NotesIndexer(this)
|
public readonly notesIndexer = new NotesIndexer(this)
|
||||||
public readonly textProcessor = new TextProcessor(this)
|
public readonly textProcessor = new TextProcessor(this)
|
||||||
public readonly searchEngine = new SearchEngine(this)
|
public readonly searchEngine = new SearchEngine(this)
|
||||||
|
public readonly searchHistory = new SearchHistory(this)
|
||||||
|
|
||||||
private ribbonButton?: HTMLElement
|
private ribbonButton?: HTMLElement
|
||||||
private refreshIndexCallback?: () => void
|
private refreshIndexCallback?: () => void
|
||||||
|
|
||||||
constructor(app: App, manifest: PluginManifest) {
|
constructor(app: App, manifest: PluginManifest) {
|
||||||
super(app, manifest)
|
super(app, manifest)
|
||||||
this.cacheManager = new CacheManager(this)
|
this.documentsRepository = new DocumentsRepository(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async onload(): Promise<void> {
|
async onload(): Promise<void> {
|
||||||
@@ -109,14 +112,16 @@ export default class OmnisearchPlugin extends Plugin {
|
|||||||
if (this.notesIndexer.isFileIndexable(file.path)) {
|
if (this.notesIndexer.isFileIndexable(file.path)) {
|
||||||
logDebug('Indexing new file', file.path)
|
logDebug('Indexing new file', file.path)
|
||||||
searchEngine.addFromPaths([file.path])
|
searchEngine.addFromPaths([file.path])
|
||||||
|
this.embedsRepository.refreshEmbedsForNote(file.path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.vault.on('delete', file => {
|
this.app.vault.on('delete', file => {
|
||||||
logDebug('Removing file', file.path)
|
logDebug('Removing file', file.path)
|
||||||
this.cacheManager.removeFromLiveCache(file.path)
|
this.documentsRepository.removeDocument(file.path)
|
||||||
searchEngine.removeFromPaths([file.path])
|
searchEngine.removeFromPaths([file.path])
|
||||||
|
this.embedsRepository.removeFile(file.path)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
@@ -124,16 +129,20 @@ export default class OmnisearchPlugin extends Plugin {
|
|||||||
if (this.notesIndexer.isFileIndexable(file.path)) {
|
if (this.notesIndexer.isFileIndexable(file.path)) {
|
||||||
this.notesIndexer.flagNoteForReindex(file)
|
this.notesIndexer.flagNoteForReindex(file)
|
||||||
}
|
}
|
||||||
|
this.embedsRepository.refreshEmbedsForNote(file.path)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.vault.on('rename', async (file, oldPath) => {
|
this.app.vault.on('rename', async (file, oldPath) => {
|
||||||
if (this.notesIndexer.isFileIndexable(file.path)) {
|
if (this.notesIndexer.isFileIndexable(file.path)) {
|
||||||
logDebug('Renaming file', file.path)
|
logDebug('Renaming file', file.path)
|
||||||
this.cacheManager.removeFromLiveCache(oldPath)
|
this.documentsRepository.removeDocument(oldPath)
|
||||||
await this.cacheManager.addToLiveCache(file.path)
|
await this.documentsRepository.addDocument(file.path)
|
||||||
|
|
||||||
searchEngine.removeFromPaths([oldPath])
|
searchEngine.removeFromPaths([oldPath])
|
||||||
await searchEngine.addFromPaths([file.path])
|
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 }))
|
files.map(f => ({ path: f.path, mtime: f.stat.mtime }))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -281,7 +290,8 @@ export default class OmnisearchPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the cache
|
// Write the cache
|
||||||
await searchEngine.writeToCache()
|
await this.database.writeMinisearchCache()
|
||||||
|
await this.embedsRepository.writeToCache()
|
||||||
|
|
||||||
// Re-enable settings.caching
|
// Re-enable settings.caching
|
||||||
if (cacheEnabled) {
|
if (cacheEnabled) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class NotesIndexer {
|
|||||||
public async refreshIndex(): Promise<void> {
|
public async refreshIndex(): Promise<void> {
|
||||||
for (const file of this.notesToReindex) {
|
for (const file of this.notesToReindex) {
|
||||||
logDebug('Updating file', file.path)
|
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)
|
const paths = [...this.notesToReindex].map(n => n.path)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TFile } from 'obsidian'
|
import { normalizePath, Notice, TFile } from 'obsidian'
|
||||||
import type { IndexedDocument } from './globals'
|
import type { IndexedDocument } from '../globals'
|
||||||
import {
|
import {
|
||||||
extractHeadingsFromCache,
|
extractHeadingsFromCache,
|
||||||
getAliasesFromMetadata,
|
getAliasesFromMetadata,
|
||||||
@@ -12,30 +12,33 @@ import {
|
|||||||
logDebug,
|
logDebug,
|
||||||
removeDiacritics,
|
removeDiacritics,
|
||||||
stripMarkdownCharacters,
|
stripMarkdownCharacters,
|
||||||
} from './tools/utils'
|
} from '../tools/utils'
|
||||||
import type { CanvasData } from 'obsidian/canvas'
|
import type { CanvasData } from 'obsidian/canvas'
|
||||||
import type OmnisearchPlugin from './main'
|
import type OmnisearchPlugin from '../main'
|
||||||
import { getNonExistingNotes } from './tools/notes'
|
import { getNonExistingNotes } from '../tools/notes'
|
||||||
|
|
||||||
export class CacheManager {
|
|
||||||
/**
|
|
||||||
* Show an empty input field next time the user opens Omnisearch modal
|
|
||||||
*/
|
|
||||||
private nextQueryIsEmpty = false
|
|
||||||
|
|
||||||
|
export class DocumentsRepository {
|
||||||
/**
|
/**
|
||||||
* The "live cache", containing all indexed vault files
|
* The "live cache", containing all indexed vault files
|
||||||
* in the form of IndexedDocuments
|
* in the form of IndexedDocuments
|
||||||
*/
|
*/
|
||||||
private documents: Map<string, IndexedDocument> = new Map()
|
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.
|
* Set or update the live cache with the content of the given file.
|
||||||
* @param path
|
* @param path
|
||||||
*/
|
*/
|
||||||
public async addToLiveCache(path: string): Promise<void> {
|
public async addDocument(path: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const doc = await this.getAndMapIndexedDocument(path)
|
const doc = await this.getAndMapIndexedDocument(path)
|
||||||
if (!doc.path) {
|
if (!doc.path) {
|
||||||
@@ -45,14 +48,16 @@ export class CacheManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.documents.set(path, doc)
|
this.documents.set(path, doc)
|
||||||
|
this.plugin.embedsRepository.refreshEmbedsForNote(path)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e)
|
console.warn(`Omnisearch: Error while adding "${path}" to live cache`, e)
|
||||||
// Shouldn't be needed, but...
|
// 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)
|
this.documents.delete(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,38 +66,10 @@ export class CacheManager {
|
|||||||
return this.documents.get(path)!
|
return this.documents.get(path)!
|
||||||
}
|
}
|
||||||
logDebug('Generating IndexedDocument from', path)
|
logDebug('Generating IndexedDocument from', path)
|
||||||
await this.addToLiveCache(path)
|
await this.addDocument(path)
|
||||||
return this.documents.get(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
|
* This function is responsible for extracting the text from a file and
|
||||||
* returning it as an `IndexedDocument` object.
|
* returning it as an `IndexedDocument` object.
|
||||||
@@ -101,6 +78,7 @@ export class CacheManager {
|
|||||||
private async getAndMapIndexedDocument(
|
private async getAndMapIndexedDocument(
|
||||||
path: string
|
path: string
|
||||||
): Promise<IndexedDocument> {
|
): Promise<IndexedDocument> {
|
||||||
|
path = normalizePath(path)
|
||||||
const app = this.plugin.app
|
const app = this.plugin.app
|
||||||
const file = app.vault.getAbstractFileByPath(path)
|
const file = app.vault.getAbstractFileByPath(path)
|
||||||
if (!file) throw new Error(`Invalid file path: "${path}"`)
|
if (!file) throw new Error(`Invalid file path: "${path}"`)
|
||||||
@@ -167,12 +145,18 @@ export class CacheManager {
|
|||||||
(this.plugin.settings.aiImageIndexing &&
|
(this.plugin.settings.aiImageIndexing &&
|
||||||
aiImageAnalyzer?.canBeAnalyzed(file)))
|
aiImageAnalyzer?.canBeAnalyzed(file)))
|
||||||
) {
|
) {
|
||||||
if (this.plugin.settings.imagesIndexing && extractor?.canFileBeExtracted(path)){
|
if (
|
||||||
|
this.plugin.settings.imagesIndexing &&
|
||||||
|
extractor?.canFileBeExtracted(path)
|
||||||
|
) {
|
||||||
content = await extractor.extractText(file)
|
content = await extractor.extractText(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.plugin.settings.aiImageIndexing && aiImageAnalyzer?.canBeAnalyzed(file)) {
|
if (
|
||||||
content = await aiImageAnalyzer.analyzeImage(file) + (content ?? '')
|
this.plugin.settings.aiImageIndexing &&
|
||||||
|
aiImageAnalyzer?.canBeAnalyzed(file)
|
||||||
|
) {
|
||||||
|
content = (await aiImageAnalyzer.analyzeImage(file)) + (content ?? '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ** PDF **
|
// ** 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)
|
const tags = getTagsFromMetadata(metadata)
|
||||||
return {
|
return {
|
||||||
basename: file.basename,
|
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 type { DocumentRef, IndexedDocument, ResultNote } from '../globals'
|
||||||
|
|
||||||
import { chunkArray, logDebug, removeDiacritics } from '../tools/utils'
|
import { chunkArray, logDebug, removeDiacritics } from '../tools/utils'
|
||||||
@@ -13,6 +17,7 @@ export class SearchEngine {
|
|||||||
private minisearch: MiniSearch
|
private minisearch: MiniSearch
|
||||||
/** Map<path, mtime> */
|
/** Map<path, mtime> */
|
||||||
private indexedDocuments: Map<string, number> = new Map()
|
private indexedDocuments: Map<string, number> = new Map()
|
||||||
|
|
||||||
// private previousResults: SearchResult[] = []
|
// private previousResults: SearchResult[] = []
|
||||||
// private previousQuery: Query | null = null
|
// private previousQuery: Query | null = null
|
||||||
|
|
||||||
@@ -25,6 +30,7 @@ export class SearchEngine {
|
|||||||
* Return true if the cache is valid
|
* Return true if the cache is valid
|
||||||
*/
|
*/
|
||||||
async loadCache(): Promise<boolean> {
|
async loadCache(): Promise<boolean> {
|
||||||
|
await this.plugin.embedsRepository.loadFromCache()
|
||||||
const cache = await this.plugin.database.getMinisearchCache()
|
const cache = await this.plugin.database.getMinisearchCache()
|
||||||
if (cache) {
|
if (cache) {
|
||||||
this.minisearch = await MiniSearch.loadJSAsync(
|
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
|
* @param docs
|
||||||
*/
|
*/
|
||||||
getDiff(docs: DocumentRef[]): {
|
getDocumentsToReindex(docs: DocumentRef[]): {
|
||||||
toAdd: DocumentRef[]
|
toAdd: DocumentRef[]
|
||||||
toRemove: DocumentRef[]
|
toRemove: DocumentRef[]
|
||||||
} {
|
} {
|
||||||
@@ -72,7 +79,7 @@ export class SearchEngine {
|
|||||||
let documents = (
|
let documents = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
paths.map(
|
paths.map(
|
||||||
async path => await this.plugin.cacheManager.getDocument(path)
|
async path => await this.plugin.documentsRepository.getDocument(path)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).filter(d => !!d?.path)
|
).filter(d => !!d?.path)
|
||||||
@@ -167,7 +174,7 @@ export class SearchEngine {
|
|||||||
tokenize: text => [text],
|
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)
|
// Filter query results to only keep files that match query.query.ext (if any)
|
||||||
if (query.query.ext?.length) {
|
if (query.query.ext?.length) {
|
||||||
@@ -264,9 +271,9 @@ export class SearchEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boost custom properties
|
|
||||||
const metadata = this.plugin.app.metadataCache.getCache(path)
|
const metadata = this.plugin.app.metadataCache.getCache(path)
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
|
// Boost custom properties
|
||||||
for (const { name, weight } of settings.weightCustomProperties) {
|
for (const { name, weight } of settings.weightCustomProperties) {
|
||||||
const values = metadata?.frontmatter?.[name]
|
const values = metadata?.frontmatter?.[name]
|
||||||
if (values && result.terms.some(t => values.includes(t))) {
|
if (values && result.terms.some(t => values.includes(t))) {
|
||||||
@@ -288,11 +295,13 @@ export class SearchEngine {
|
|||||||
// Sort results and keep the 50 best
|
// Sort results and keep the 50 best
|
||||||
results = results.sort((a, b) => b.score - a.score).slice(0, 50)
|
results = results.sort((a, b) => b.score - a.score).slice(0, 50)
|
||||||
|
|
||||||
|
logDebug('Filtered results:', results)
|
||||||
|
|
||||||
if (results.length) logDebug('First result:', results[0])
|
if (results.length) logDebug('First result:', results[0])
|
||||||
|
|
||||||
const documents = await Promise.all(
|
const documents = await Promise.all(
|
||||||
results.map(
|
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(
|
const documents = await Promise.all(
|
||||||
results.map(
|
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
|
// Map the raw results to get usable suggestions
|
||||||
const resultNotes = results.map(result => {
|
const resultNotes = results.map(result => {
|
||||||
logDebug('Locating matches for', result.id)
|
logDebug('Locating matches for', result.id)
|
||||||
@@ -407,23 +443,37 @@ export class SearchEngine {
|
|||||||
foundWords,
|
foundWords,
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
logDebug(`Matches for ${note.basename}`, matches)
|
logDebug(`Matches for note "${note.path}"`, matches)
|
||||||
const resultNote: ResultNote = {
|
const resultNote: ResultNote = {
|
||||||
score: result.score,
|
score: result.score,
|
||||||
foundWords,
|
foundWords,
|
||||||
matches,
|
matches,
|
||||||
|
isEmbed: result.isEmbed,
|
||||||
...note,
|
...note,
|
||||||
}
|
}
|
||||||
return resultNote
|
return resultNote
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logDebug('Suggestions:', resultNotes)
|
||||||
|
|
||||||
return resultNotes
|
return resultNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
public async writeToCache(): Promise<void> {
|
/**
|
||||||
await this.plugin.database.writeMinisearchCache(
|
* For cache saving
|
||||||
this.minisearch,
|
*/
|
||||||
this.indexedDocuments
|
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> {
|
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
|
ribbonIcon: boolean
|
||||||
/** Display the small contextual excerpt in search results */
|
/** Display the small contextual excerpt in search results */
|
||||||
showExcerpt: boolean
|
showExcerpt: boolean
|
||||||
|
/** Number of embeds references to display in search results */
|
||||||
|
maxEmbeds: number
|
||||||
/** Render line returns with <br> in excerpts */
|
/** Render line returns with <br> in excerpts */
|
||||||
renderLineReturnInExcerpts: boolean
|
renderLineReturnInExcerpts: boolean
|
||||||
/** Enable a "create note" button in the Vault Search modal */
|
/** 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
|
// Keep line returns in excerpts
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Render line return in excerpts')
|
.setName('Render line return in excerpts')
|
||||||
@@ -761,7 +781,7 @@ export class SettingsTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void {
|
weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void {
|
||||||
cb.setLimits(1, 5, 0.1)
|
cb.setLimits(1, 10, 0.5)
|
||||||
.setValue(settings[key])
|
.setValue(settings[key])
|
||||||
.setDynamicTooltip()
|
.setDynamicTooltip()
|
||||||
.onChange(async v => {
|
.onChange(async v => {
|
||||||
@@ -791,6 +811,7 @@ export function getDefaultSettings(app: App): OmnisearchSettings {
|
|||||||
|
|
||||||
ribbonIcon: true,
|
ribbonIcon: true,
|
||||||
showExcerpt: true,
|
showExcerpt: true,
|
||||||
|
maxEmbeds: 5,
|
||||||
renderLineReturnInExcerpts: true,
|
renderLineReturnInExcerpts: true,
|
||||||
showCreateButton: false,
|
showCreateButton: false,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
@@ -799,12 +820,12 @@ export function getDefaultSettings(app: App): OmnisearchSettings {
|
|||||||
tokenizeUrls: false,
|
tokenizeUrls: false,
|
||||||
fuzziness: '1',
|
fuzziness: '1',
|
||||||
|
|
||||||
weightBasename: 3,
|
weightBasename: 10,
|
||||||
weightDirectory: 2,
|
weightDirectory: 7,
|
||||||
weightH1: 1.5,
|
weightH1: 6,
|
||||||
weightH2: 1.3,
|
weightH2: 5,
|
||||||
weightH3: 1.1,
|
weightH3: 4,
|
||||||
weightUnmarkedTags: 1.1,
|
weightUnmarkedTags: 2,
|
||||||
weightCustomProperties: [] as { name: string; weight: number }[],
|
weightCustomProperties: [] as { name: string; weight: number }[],
|
||||||
|
|
||||||
httpApiEnabled: false,
|
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.
|
* 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.
|
* 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
|
return html
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
.replaceAll('<', '<')
|
.replaceAll('<', '<')
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function getCtrlKeyLabel(): 'ctrl' | '⌘' {
|
|||||||
|
|
||||||
export function isFileImage(path: string): boolean {
|
export function isFileImage(path: string): boolean {
|
||||||
const ext = getExtension(path)
|
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 {
|
export function isFilePDF(path: string): boolean {
|
||||||
|
|||||||
@@ -149,7 +149,5 @@
|
|||||||
"1.24.0": "1.3.0",
|
"1.24.0": "1.3.0",
|
||||||
"1.24.1": "1.3.0",
|
"1.24.1": "1.3.0",
|
||||||
"1.25.0-beta.1": "1.3.0",
|
"1.25.0-beta.1": "1.3.0",
|
||||||
"1.25.0-beta.2": "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"
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user