Merge branch 'develop'

This commit is contained in:
Simon Cambier
2025-05-25 12:35:54 +02:00
12 changed files with 592 additions and 559 deletions

View File

@@ -7,7 +7,6 @@ module.exports = {
singleQuote: true, singleQuote: true,
arrowParens: 'avoid', arrowParens: 'avoid',
bracketSameLine: true, bracketSameLine: true,
svelteStrictMode: true,
svelteBracketNewLine: false, svelteBracketNewLine: false,
svelteAllowShorthand: true, svelteAllowShorthand: true,
svelteIndentScriptAndStyle: true, svelteIndentScriptAndStyle: true,

View File

@@ -1,10 +1,10 @@
import { build } from 'esbuild' import esbuild from 'esbuild'
import sveltePlugin from 'esbuild-svelte'
import sveltePreprocess from 'svelte-preprocess'
import { copy } from 'esbuild-plugin-copy'
import process from 'process' import process from 'process'
import builtins from 'builtin-modules' import builtins from 'builtin-modules'
import esbuildSvelte from 'esbuild-svelte'
import { sveltePreprocess } from 'svelte-preprocess'
import path from 'path' import path from 'path'
import { copy } from 'esbuild-plugin-copy'
const banner = `/* const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@@ -14,7 +14,7 @@ if you want to view the source, please visit the github repository of this plugi
const prod = process.argv[2] === 'production' const prod = process.argv[2] === 'production'
build({ const context = await esbuild.context({
banner: { banner: {
js: banner, js: banner,
}, },
@@ -24,54 +24,42 @@ build({
'obsidian', 'obsidian',
'electron', 'electron',
'@codemirror/autocomplete', '@codemirror/autocomplete',
'@codemirror/closebrackets',
'@codemirror/collab', '@codemirror/collab',
'@codemirror/commands', '@codemirror/commands',
'@codemirror/comment',
'@codemirror/fold',
'@codemirror/gutter',
'@codemirror/highlight',
'@codemirror/history',
'@codemirror/language', '@codemirror/language',
'@codemirror/lint', '@codemirror/lint',
'@codemirror/matchbrackets',
'@codemirror/panel',
'@codemirror/rangeset',
'@codemirror/rectangular-selection',
'@codemirror/search', '@codemirror/search',
'@codemirror/state', '@codemirror/state',
'@codemirror/stream-parser',
'@codemirror/text',
'@codemirror/tooltip',
'@codemirror/view', '@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins, ...builtins,
], ],
outfile: path.join('./dist', 'main.js'), outfile: path.join('./dist', 'main.js'),
plugins: [ plugins: [
sveltePlugin({ esbuildSvelte({
compilerOptions: { css: 'injected' },
preprocess: sveltePreprocess(), preprocess: sveltePreprocess(),
}), }),
copy({ copy({
assets:{ assets:{
from: ['./assets/styles.css', 'manifest.json'], from: ['manifest.json','./assets/styles.css'],
to: ['./'], to: ['./']
}, }
}), })
{
name: 'resolve-minisearch',
setup(build) {
build.onResolve({ filter: /^minisearch$/ }, () => {
return { path: path.resolve('node_modules/minisearch/src/MiniSearch.ts') };
});
},
},
], ],
format: 'cjs', format: 'cjs',
watch: !prod,
target: 'chrome98', target: 'chrome98',
logLevel: 'info', logLevel: 'info',
sourcemap: prod ? false : 'inline', sourcemap: prod ? false : 'inline',
treeShaking: true, treeShaking: true,
minify: prod, minify: prod,
legalComments: 'none', })
}).catch(() => process.exit(1))
if (prod) {
await context.rebuild()
process.exit(0)
} else {
await context.watch()
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "scambier.obsidian-search", "name": "scambier.obsidian-search",
"version": "1.26.1", "version": "1.27.0-beta.1",
"description": "A search engine for Obsidian", "description": "A search engine for Obsidian",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
@@ -24,19 +24,19 @@
"@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",
"esbuild": "0.14.0", "esbuild": "0.17.19",
"esbuild-plugin-copy": "1.3.0", "esbuild-plugin-copy": "1.3.0",
"esbuild-svelte": "0.7.1", "esbuild-svelte": "^0.9.2",
"jest": "^27.5.1", "jest": "^27.5.1",
"obsidian": "1.7.2", "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": "^5.23.2",
"svelte-check": "^2.10.3", "svelte-check": "^4.1.5",
"svelte-jester": "^2.3.2", "svelte-jester": "^2.3.2",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^6.0.3",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "^4.9.5", "typescript": "^5.8.2",
"vite": "^3.2.11" "vite": "^3.2.11"
}, },
"dependencies": { "dependencies": {
@@ -46,7 +46,8 @@
"markdown-link-extractor": "^4.0.2", "markdown-link-extractor": "^4.0.2",
"minisearch": "7.1.0", "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",
"svelte-multiselect": "github:janosh/svelte-multiselect"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {

824
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -191,7 +191,7 @@
{/each} {/each}
{:else} {:else}
<div style="text-align: center;"> <div style="text-align: center;">
We found 0 result for your search here. We found 0 results for your search here.
</div> </div>
{/if} {/if}
</ModalContainer> </ModalContainer>

View File

@@ -30,27 +30,34 @@
import type OmnisearchPlugin from '../main' import type OmnisearchPlugin from '../main'
import LazyLoader from './lazy-loader/LazyLoader.svelte' import LazyLoader from './lazy-loader/LazyLoader.svelte'
export let modal: OmnisearchVaultModal let {
export let previousQuery: string | undefined modal,
export let plugin: OmnisearchPlugin previousQuery,
plugin,
}: {
modal: OmnisearchVaultModal
previousQuery?: string | undefined
plugin: OmnisearchPlugin
} = $props()
let selectedIndex = 0 let selectedIndex = $state(0)
let historySearchIndex = 0 let historySearchIndex = 0
let searchQuery: string | undefined let searchQuery = $state(previousQuery ?? '')
let resultNotes: ResultNote[] = [] let resultNotes: ResultNote[] = $state([])
let query: Query let query: Query
let indexingStepDesc = '' let indexingStepDesc = $state('')
let searching = true let searching = $state(true)
let refInput: InputSearch | undefined let refInput: InputSearch | undefined
let openInNewPaneKey: string let openInNewPaneKey: string = $state('')
let openInCurrentPaneKey: string let openInCurrentPaneKey: string = $state('')
let createInNewPaneKey: string let createInNewPaneKey: string = $state('')
let createInCurrentPaneKey: string let createInCurrentPaneKey: string = $state('')
let openInNewLeafKey: string = getCtrlKeyLabel() + getAltKeyLabel() + '' let openInNewLeafKey: string = `${getCtrlKeyLabel()} ${getAltKeyLabel()}`
$: selectedNote = resultNotes[selectedIndex] const selectedNote = $derived(resultNotes[selectedIndex])
$: searchQuery = searchQuery ?? previousQuery
$: if (plugin.settings.openInNewPane) { $effect(() => {
if (plugin.settings.openInNewPane) {
openInNewPaneKey = '↵' openInNewPaneKey = '↵'
openInCurrentPaneKey = getCtrlKeyLabel() + ' ↵' openInCurrentPaneKey = getCtrlKeyLabel() + ' ↵'
createInNewPaneKey = 'Shift ↵' createInNewPaneKey = 'Shift ↵'
@@ -61,13 +68,17 @@
createInNewPaneKey = getCtrlKeyLabel() + ' Shift ↵' createInNewPaneKey = getCtrlKeyLabel() + ' Shift ↵'
createInCurrentPaneKey = 'Shift ↵' createInCurrentPaneKey = 'Shift ↵'
} }
$: if (searchQuery) { })
$effect(() => {
if (searchQuery) {
updateResultsDebounced() updateResultsDebounced()
} else { } else {
searching = false searching = false
resultNotes = [] resultNotes = []
} }
$: { })
$effect(() => {
switch ($indexingStep) { switch ($indexingStep) {
case IndexingStepType.LoadingCache: case IndexingStepType.LoadingCache:
indexingStepDesc = 'Loading cache...' indexingStepDesc = 'Loading cache...'
@@ -87,7 +98,7 @@
indexingStepDesc = '' indexingStepDesc = ''
break break
} }
} })
onMount(async () => { onMount(async () => {
eventBus.enable('vault') eventBus.enable('vault')
@@ -305,17 +316,17 @@
</script> </script>
<InputSearch <InputSearch
bind:this="{refInput}" bind:this={refInput}
plugin="{plugin}" {plugin}
initialValue="{searchQuery}" initialValue={searchQuery}
on:input="{e => (searchQuery = e.detail)}" on:input={e => (searchQuery = e.detail)}
placeholder="Omnisearch - Vault"> placeholder="Omnisearch - Vault">
<div class="omnisearch-input-container__buttons"> <div class="omnisearch-input-container__buttons">
{#if plugin.settings.showCreateButton} {#if plugin.settings.showCreateButton}
<button on:click="{onClickCreateNote}">Create note</button> <button on:click={onClickCreateNote}>Create note</button>
{/if} {/if}
{#if Platform.isMobile} {#if Platform.isMobile}
<button on:click="{switchToInFileModal}">In-File search</button> <button on:click={switchToInFileModal}>In-File search</button>
{/if} {/if}
</div> </div>
</InputSearch> </InputSearch>
@@ -329,24 +340,24 @@
<ModalContainer> <ModalContainer>
{#each resultNotes as result, i} {#each resultNotes as result, i}
<LazyLoader <LazyLoader
height="{100}" height={100}
offset="{500}" offset={500}
keep="{true}" keep={true}
fadeOption="{{ delay: 0, duration: 0 }}"> fadeOption={{ delay: 0, duration: 0 }}>
<ResultItemVault <ResultItemVault
plugin="{plugin}" {plugin}
selected="{i === selectedIndex}" selected={i === selectedIndex}
note="{result}" note={result}
on:mousemove="{_ => (selectedIndex = i)}" on:mousemove={_ => (selectedIndex = i)}
on:click="{onClick}" on:click={onClick}
on:auxclick="{evt => { on:auxclick={evt => {
if (evt.button == 1) openNoteInNewPane() if (evt.button == 1) openNoteInNewPane()
}}" /> }} />
</LazyLoader> </LazyLoader>
{/each} {/each}
<div style="text-align: center;"> <div style="text-align: center;">
{#if !resultNotes.length && searchQuery && !searching} {#if !resultNotes.length && searchQuery && !searching}
We found 0 result for your search here. We found 0 results for your search here.
{#if plugin.settings.simpleSearch && searchQuery {#if plugin.settings.simpleSearch && searchQuery
.split(SPACE_OR_PUNCTUATION) .split(SPACE_OR_PUNCTUATION)
.some(w => w.length < 3)} .some(w => w.length < 3)}
@@ -408,7 +419,7 @@
<span>to insert a link</span> <span>to insert a link</span>
</div> </div>
<div class="prompt-instruction"> <div class="prompt-instruction">
<span class="prompt-instruction-command">Ctrl g</span> <span class="prompt-instruction-command">{getCtrlKeyLabel()} g</span>
<span>to toggle excerpts</span> <span>to toggle excerpts</span>
</div> </div>
<div class="prompt-instruction"> <div class="prompt-instruction">

View File

@@ -4,6 +4,7 @@ import ModalVault from './ModalVault.svelte'
import ModalInFile from './ModalInFile.svelte' import ModalInFile from './ModalInFile.svelte'
import { Action, eventBus, EventNames, isInputComposition } from '../globals' import { Action, eventBus, EventNames, isInputComposition } from '../globals'
import type OmnisearchPlugin from '../main' import type OmnisearchPlugin from '../main'
import { mount, unmount } from 'svelte'
abstract class OmnisearchModal extends Modal { abstract class OmnisearchModal extends Modal {
protected constructor(plugin: OmnisearchPlugin) { protected constructor(plugin: OmnisearchPlugin) {
@@ -119,7 +120,7 @@ abstract class OmnisearchModal extends Modal {
}) })
// Open in background // Open in background
this.scope.register(['Ctrl'], 'O', e => { this.scope.register(['Mod'], 'O', e => {
if (!isInputComposition()) { if (!isInputComposition()) {
// Check if the user is still typing // Check if the user is still typing
e.preventDefault() e.preventDefault()
@@ -143,7 +144,7 @@ abstract class OmnisearchModal extends Modal {
}) })
// Context // Context
this.scope.register(['Ctrl'], 'G', _e => { this.scope.register(['Mod'], 'G', _e => {
eventBus.emit(EventNames.ToggleExcerpts) eventBus.emit(EventNames.ToggleExcerpts)
}) })
} }
@@ -170,7 +171,7 @@ export class OmnisearchVaultModal extends OmnisearchModal {
: null : null
// Instantiate and display the Svelte component // Instantiate and display the Svelte component
const cmp = new ModalVault({ const cmp = mount(ModalVault, {
target: this.modalEl, target: this.modalEl,
props: { props: {
plugin, plugin,
@@ -178,10 +179,11 @@ export class OmnisearchVaultModal extends OmnisearchModal {
previousQuery: query || selectedText || previous || '', previousQuery: query || selectedText || previous || '',
}, },
}) })
this.onClose = () => { this.onClose = () => {
// Since the component is manually created, // Since the component is manually created,
// we also need to manually destroy it // we also need to manually destroy it
cmp.$destroy() unmount(cmp)
} }
}) })
} }
@@ -196,7 +198,7 @@ export class OmnisearchInFileModal extends OmnisearchModal {
) { ) {
super(plugin) super(plugin)
const cmp = new ModalInFile({ const cmp = mount(ModalInFile, {
target: this.modalEl, target: this.modalEl,
props: { props: {
plugin, plugin,
@@ -215,7 +217,7 @@ export class OmnisearchInFileModal extends OmnisearchModal {
if (parent) { if (parent) {
parent.containerEl.toggleVisibility(true) parent.containerEl.toggleVisibility(true)
} }
cmp.$destroy() unmount(cmp)
} }
} }
} }

View File

@@ -53,7 +53,7 @@ export default class OmnisearchPlugin extends Plugin {
public readonly searchHistory = new SearchHistory(this) public readonly searchHistory = new SearchHistory(this)
private ribbonButton?: HTMLElement private ribbonButton?: HTMLElement
private refreshIndexCallback?: () => void private refreshIndexCallback?: (ev: FocusEvent) => any
constructor(app: App, manifest: PluginManifest) { constructor(app: App, manifest: PluginManifest) {
super(app, manifest) super(app, manifest)
@@ -116,10 +116,8 @@ export default class OmnisearchPlugin extends Plugin {
// Listeners to keep the search index up-to-date // Listeners to keep the search index up-to-date
this.registerEvent( this.registerEvent(
this.app.vault.on('create', file => { this.app.vault.on('create', file => {
if ( if (!(file instanceof TFile)) return
file instanceof TFile && if (this.notesIndexer.isFileIndexable(file.path)) {
this.notesIndexer.isFileIndexable(file.path)
) {
logVerbose('Indexing new file', file.path) logVerbose('Indexing new file', file.path)
searchEngine.addFromPaths([file.path]) searchEngine.addFromPaths([file.path])
this.embedsRepository.refreshEmbedsForNote(file.path) this.embedsRepository.refreshEmbedsForNote(file.path)
@@ -128,6 +126,7 @@ export default class OmnisearchPlugin extends Plugin {
) )
this.registerEvent( this.registerEvent(
this.app.vault.on('delete', file => { this.app.vault.on('delete', file => {
if (!(file instanceof TFile)) return
logVerbose('Removing file', file.path) logVerbose('Removing file', file.path)
this.documentsRepository.removeDocument(file.path) this.documentsRepository.removeDocument(file.path)
searchEngine.removeFromPaths([file.path]) searchEngine.removeFromPaths([file.path])
@@ -136,6 +135,7 @@ export default class OmnisearchPlugin extends Plugin {
) )
this.registerEvent( this.registerEvent(
this.app.vault.on('modify', async file => { this.app.vault.on('modify', async file => {
if (!(file instanceof TFile)) return
if (this.notesIndexer.isFileIndexable(file.path)) { if (this.notesIndexer.isFileIndexable(file.path)) {
this.notesIndexer.flagNoteForReindex(file) this.notesIndexer.flagNoteForReindex(file)
} }
@@ -144,6 +144,7 @@ export default class OmnisearchPlugin extends Plugin {
) )
this.registerEvent( this.registerEvent(
this.app.vault.on('rename', async (file, oldPath) => { this.app.vault.on('rename', async (file, oldPath) => {
if (!(file instanceof TFile)) return
if (this.notesIndexer.isFileIndexable(file.path)) { if (this.notesIndexer.isFileIndexable(file.path)) {
logVerbose('Renaming file', file.path) logVerbose('Renaming file', file.path)
this.documentsRepository.removeDocument(oldPath) this.documentsRepository.removeDocument(oldPath)
@@ -160,7 +161,7 @@ export default class OmnisearchPlugin extends Plugin {
this.refreshIndexCallback = this.notesIndexer.refreshIndex.bind( this.refreshIndexCallback = this.notesIndexer.refreshIndex.bind(
this.notesIndexer this.notesIndexer
) )
addEventListener('blur', this.refreshIndexCallback) addEventListener('blur', this.refreshIndexCallback!)
removeEventListener removeEventListener
await this.executeFirstLaunchTasks() await this.executeFirstLaunchTasks()

View File

@@ -67,7 +67,16 @@ export class DocumentsRepository {
} }
logVerbose('Generating IndexedDocument from', path) logVerbose('Generating IndexedDocument from', path)
await this.addDocument(path) await this.addDocument(path)
return this.documents.get(path)! const document = this.documents.get(path)
// Only happens if the cache is corrupted
if (!document) {
console.error('Omnisearch', path, 'cannot be read')
this.countError()
}
// The document might be undefined, but this shouldn't stop the search from mostly working
return document!
} }
/** /**
@@ -219,7 +228,8 @@ export class DocumentsRepository {
if (this.plugin.settings.displayTitle === '#heading') { if (this.plugin.settings.displayTitle === '#heading') {
displayTitle = metadata?.headings?.find(h => h.level === 1)?.heading ?? '' displayTitle = metadata?.headings?.find(h => h.level === 1)?.heading ?? ''
} else { } else {
displayTitle = metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? '' displayTitle =
metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? ''
} }
const tags = getTagsFromMetadata(metadata) const tags = getTagsFromMetadata(metadata)
return { return {
@@ -247,10 +257,11 @@ export class DocumentsRepository {
} }
private countError(): void { private countError(): void {
if (++this.errorsCount > 5 && !this.errorsWarned) { if (++this.errorsCount >= 3 && !this.errorsWarned) {
this.errorsWarned = true this.errorsWarned = true
new Notice( new Notice(
'Omnisearch ⚠️ There might be an issue with your cache. You should clean it in Omnisearch settings and restart Obsidian.' 'Omnisearch ⚠️ There might be an issue with your cache. You should clean it in Omnisearch settings and restart Obsidian.',
0
) )
} }
} }

View File

@@ -76,7 +76,7 @@ export class SettingsTab extends PluginSettingTab {
new Setting(containerEl) new Setting(containerEl)
.setName('Enable verbose logging') .setName('Enable verbose logging')
.setDesc( .setDesc(
"Adds a LOT of logs for debugging purposes. Don't forget to disable it." 'Adds a LOT of logs for debugging purposes. You also need to enable "Verbose" logging in the console to see these logs.'
) )
.addToggle(toggle => .addToggle(toggle =>
toggle.setValue(settings.verboseLogging).onChange(async v => { toggle.setValue(settings.verboseLogging).onChange(async v => {

View File

@@ -23,55 +23,13 @@ export class TextProcessor {
return text return text
} }
try { try {
// Text to highlight return text.replace(
const smartMatches = new RegExp(
matches
.map(
// This regex will match the word (with \b word boundary)
// \b doesn't detect non-alphabetical character's word boundary, so we need to escape it
matchItem => {
const escaped = escapeRegExp(matchItem.match)
return `\\b${escaped}\\b${
!/[a-zA-Z]/.test(matchItem.match) ? `|${escaped}` : ''
}`
}
)
.join('|'),
'giu'
)
// Replacer function that will highlight the matches
const replacer = (match: string) => {
const matchInfo = matches.find(info =>
match.match(
new RegExp( new RegExp(
`\\b${escapeRegExp(info.match)}\\b${ `(${matches.map(item => escapeRegExp(item.match)).join('|')})`,
!/[a-zA-Z]/.test(info.match)
? `|${escapeRegExp(info.match)}`
: ''
}`,
'giu' 'giu'
),
`<span class="${highlightClass}">$1</span>`
) )
)
)
if (matchInfo) {
return `<span class="${highlightClass}">${match}</span>`
}
return match
}
// Effectively highlight the text
let newText = text.replace(smartMatches, replacer)
// If the text didn't change (= nothing to highlight), re-run the regex but just replace the matches without the word boundary
if (newText === text) {
const dumbMatches = new RegExp(
matches.map(matchItem => escapeRegExp(matchItem.match)).join('|'),
'giu'
)
newText = text.replace(dumbMatches, replacer)
}
return newText
} catch (e) { } catch (e) {
console.error('Omnisearch - Error in highlightText()', e) console.error('Omnisearch - Error in highlightText()', e)
return text return text
@@ -101,7 +59,12 @@ export class TextProcessor {
* @param reg * @param reg
* @param query * @param query
*/ */
public getMatches(text: string, words: string[], query?: Query): SearchMatch[] { public getMatches(
text: string,
words: string[],
query?: Query
): SearchMatch[] {
words = words.map(escapeHTML)
const reg = this.stringsToRegex(words) const reg = this.stringsToRegex(words)
const originalText = text const originalText = text
// text = text.toLowerCase().replace(new RegExp(SEPARATORS, 'gu'), ' ') // text = text.toLowerCase().replace(new RegExp(SEPARATORS, 'gu'), ' ')
@@ -199,4 +162,3 @@ export function escapeHTML(html: string): string {
.replaceAll('"', '&quot;') .replaceAll('"', '&quot;')
.replaceAll("'", '&#039;') .replaceAll("'", '&#039;')
} }

View File

@@ -1,31 +1,27 @@
{ {
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": [ "verbatimModuleSyntax": true,
"svelte", "skipLibCheck": true,
"node",
"jest"
],
"strict": true,
"noUncheckedIndexedAccess": false,
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext", "module": "ESNext",
"target": "ES2021", "target": "ES2021",
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true, "isolatedModules": true,
"strictNullChecks": true,
"lib": [ "lib": [
"DOM", "DOM",
"ES2021" "ES2021"
], ]
"paths": {
"minisearch": ["node_modules/minisearch/src/MiniSearch.ts"]
}
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
"src/__tests__/event-bus-tests.mts" "**/*.svelte"
] ]
} }