Compare commits

...

10 Commits

22 changed files with 106 additions and 644 deletions

7
.madgerc Normal file
View File

@ -0,0 +1,7 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}

View File

@ -1,223 +0,0 @@
<script lang="ts">
import InputSearch from './InputSearch.svelte'
import {
Action,
eventBus,
excerptAfter,
type ResultNote,
type SearchMatch,
} from '../globals'
import { getCtrlKeyLabel, loopIndex } from '../tools/utils'
import { onDestroy, onMount, tick } from 'svelte'
import { MarkdownView, Platform } from 'obsidian'
import ModalContainer from './ModalContainer.svelte'
import {
LocatorInFileModal,
LocatorVaultModal,
} from '../components/modals'
import ResultItemInFile from './ResultItemInFile.svelte'
import { Query } from '../search/query'
import { openNote } from '../tools/notes'
import type LocatorPlugin from '../main'
export let plugin: LocatorPlugin
export let modal: LocatorInFileModal
export let parent: LocatorVaultModal | null = null
export let singleFilePath = ''
export let previousQuery: string | undefined
let searchQuery: string
let groupedOffsets: number[] = []
let selectedIndex = 0
let note: ResultNote | undefined
let query: Query
$: searchQuery = previousQuery ?? ''
onMount(() => {
eventBus.enable('infile')
eventBus.on('infile', Action.Enter, openSelection)
eventBus.on('infile', Action.OpenInNewPane, openSelectionInNewTab)
eventBus.on('infile', Action.ArrowUp, () => moveIndex(-1))
eventBus.on('infile', Action.ArrowDown, () => moveIndex(1))
eventBus.on('infile', Action.Tab, switchToVaultModal)
})
onDestroy(() => {
eventBus.disable('infile')
})
$: (async () => {
if (searchQuery) {
query = new Query(searchQuery, {
ignoreDiacritics: plugin.settings.ignoreDiacritics,
ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics,
})
note =
(
await plugin.searchEngine.getSuggestions(query, {
singleFilePath,
})
)[0] ?? null
}
selectedIndex = 0
await scrollIntoView()
})()
$: {
if (note) {
let groups = getGroups(note.matches)
// If there are quotes in the search,
// only show results that match at least one of the quotes
const exactTerms = query.getExactTerms()
if (exactTerms.length) {
groups = groups.filter(group =>
exactTerms.every(exact =>
group.some(match => match.match.includes(exact))
)
)
}
groupedOffsets = groups.map(group => Math.round(group.first()!.offset))
}
}
/**
* Group together close matches to reduce the number of results
*/
function getGroups(matches: SearchMatch[]): SearchMatch[][] {
const groups: SearchMatch[][] = []
let lastOffset = -1
let count = 0 // Avoid infinite loops
while (++count < 100) {
const group = getGroupedMatches(matches, lastOffset, excerptAfter)
if (!group.length) break
lastOffset = group.last()!.offset
groups.push(group)
}
return groups
}
function getGroupedMatches(
matches: SearchMatch[],
offsetFrom: number,
maxLen: number
): SearchMatch[] {
const first = matches.find(m => m.offset > offsetFrom)
if (!first) return []
return matches.filter(
m => m.offset > offsetFrom && m.offset <= first.offset + maxLen
)
}
function moveIndex(dir: 1 | -1): void {
selectedIndex = loopIndex(selectedIndex + dir, groupedOffsets.length)
scrollIntoView()
}
async function scrollIntoView(): Promise<void> {
await tick()
const elem = document.querySelector(`[data-result-id="${selectedIndex}"]`)
elem?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
}
async function openSelectionInNewTab(): Promise<void> {
return openSelection(true)
}
async function openSelection(newTab = false): Promise<void> {
if (note) {
modal.close()
if (parent) parent.close()
// Open (or switch focus to) the note
const reg = plugin.textProcessor.stringsToRegex(note.foundWords)
reg.exec(note.content)
await openNote(plugin.app, note, reg.lastIndex, newTab)
// Move cursor to the match
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView)
if (!view) {
// Not an editable document, so no cursor to place
return
// throw new Error('OmniSearch - No active MarkdownView')
}
const offset = groupedOffsets[selectedIndex] ?? 0
const pos = view.editor.offsetToPos(offset)
pos.ch = 0
view.editor.setCursor(pos)
view.editor.scrollIntoView({
from: { line: pos.line - 10, ch: 0 },
to: { line: pos.line + 10, ch: 0 },
})
}
}
function switchToVaultModal(): void {
new LocatorVaultModal(plugin, searchQuery ?? previousQuery).open()
modal.close()
}
</script>
<InputSearch
plugin="{plugin}"
on:input="{e => (searchQuery = e.detail)}"
placeholder="Locator - File"
initialValue="{previousQuery}">
<div class="omnisearch-input-container__buttons">
{#if Platform.isMobile}
<button on:click="{switchToVaultModal}">Vault search</button>
{/if}
</div>
</InputSearch>
<ModalContainer>
{#if groupedOffsets.length && note}
{#each groupedOffsets as offset, i}
<ResultItemInFile
{plugin}
offset="{offset}"
note="{note}"
index="{i}"
selected="{i === selectedIndex}"
on:mousemove="{_e => (selectedIndex = i)}"
on:click="{evt => openSelection(evt.ctrlKey)}"
on:auxclick="{evt => {
if (evt.button == 1) openSelection(true)
}}" />
{/each}
{:else}
<div style="text-align: center;">
We found 0 results for your search here.
</div>
{/if}
</ModalContainer>
<div class="prompt-instructions">
<div class="prompt-instruction">
<span class="prompt-instruction-command">↑↓</span><span>to navigate</span>
</div>
<div class="prompt-instruction">
<span class="prompt-instruction-command"></span><span>to open</span>
</div>
<div class="prompt-instruction">
<span class="prompt-instruction-command">tab</span>
<span>to switch to Vault Search</span>
</div>
<div class="prompt-instruction">
<span class="prompt-instruction-command">esc</span>
{#if !!parent}
<span>to go back to Vault Search</span>
{:else}
<span>to close</span>
{/if}
</div>
<div class="prompt-instruction">
<span class="prompt-instruction-command">{getCtrlKeyLabel()}</span>
<span>to open in a new pane</span>
</div>
</div>

View File

@ -1,34 +1,33 @@
<script lang="ts"> <script lang="ts">
import { cancelable, CancelablePromise } from 'cancelable-promise'
import { debounce } from 'lodash-es'
import { MarkdownView, Notice, Platform, TFile } from 'obsidian' import { MarkdownView, Notice, Platform, TFile } from 'obsidian'
import { onDestroy, onMount, tick } from 'svelte' import { onDestroy, onMount, tick } from 'svelte'
import InputSearch from './InputSearch.svelte'
import ModalContainer from './ModalContainer.svelte'
import { import {
type LocatorVaultModal,
} from '../components/modals'
import {
Action,
eventBus, eventBus,
indexingStep, indexingStep,
IndexingStepType, IndexingStepType,
type ResultNote, type ResultNote,
SPACE_OR_PUNCTUATION, SPACE_OR_PUNCTUATION,
Action,
} from '../globals' } from '../globals'
import type LocatorPlugin from '../main'
import { Query } from '../search/query'
import { createNote, openNote } from '../tools/notes' import { createNote, openNote } from '../tools/notes'
import { import {
getCtrlKeyLabel,
getAltKeyLabel, getAltKeyLabel,
getCtrlKeyLabel,
getExtension, getExtension,
isFilePDF, isFilePDF,
loopIndex, loopIndex,
} from '../tools/utils' } from '../tools/utils'
import { import InputSearch from './InputSearch.svelte'
LocatorInFileModal,
type LocatorVaultModal,
} from '../components/modals'
import ResultItemVault from './ResultItemVault.svelte'
import { Query } from '../search/query'
import { cancelable, CancelablePromise } from 'cancelable-promise'
import { debounce } from 'lodash-es'
import type LocatorPlugin from '../main'
import LazyLoader from './lazy-loader/LazyLoader.svelte' import LazyLoader from './lazy-loader/LazyLoader.svelte'
import ModalContainer from './ModalContainer.svelte'
import ResultItemVault from './ResultItemVault.svelte'
let { let {
modal, modal,
@ -107,7 +106,6 @@
eventBus.on('vault', Action.CreateNote, createNoteAndCloseModal) eventBus.on('vault', Action.CreateNote, createNoteAndCloseModal)
eventBus.on('vault', Action.OpenInNewPane, openNoteInNewPane) eventBus.on('vault', Action.OpenInNewPane, openNoteInNewPane)
eventBus.on('vault', Action.InsertLink, insertLink) eventBus.on('vault', Action.InsertLink, insertLink)
eventBus.on('vault', Action.Tab, switchToInFileModal)
eventBus.on('vault', Action.ArrowUp, () => moveIndex(-1)) eventBus.on('vault', Action.ArrowUp, () => moveIndex(-1))
eventBus.on('vault', Action.ArrowDown, () => moveIndex(1)) eventBus.on('vault', Action.ArrowDown, () => moveIndex(1))
eventBus.on('vault', Action.PrevSearchHistory, prevSearchHistory) eventBus.on('vault', Action.PrevSearchHistory, prevSearchHistory)
@ -149,8 +147,8 @@
cancelableQuery = null cancelableQuery = null
} }
query = new Query(searchQuery, { query = new Query(searchQuery, {
ignoreDiacritics: plugin.settings.ignoreDiacritics, ignoreDiacritics: true,
ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics, ignoreArabicDiacritics: true,
}) })
cancelableQuery = cancelable( cancelableQuery = cancelable(
new Promise(resolve => { new Promise(resolve => {
@ -271,34 +269,6 @@
modal.close() modal.close()
} }
function switchToInFileModal(): void {
// Do nothing if the selectedNote is a PDF,
// or if there is 0 match (e.g indexing in progress)
if (
selectedNote &&
(isFilePDF(selectedNote?.path) || !selectedNote?.matches.length)
) {
return
}
saveCurrentQuery()
modal.close()
if (selectedNote) {
// Open in-file modal for selected search result
const file = plugin.app.vault.getAbstractFileByPath(selectedNote.path)
if (file && file instanceof TFile) {
new LocatorInFileModal(plugin, file, searchQuery).open()
}
} else {
// Open in-file modal for active file
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView)
if (view?.file) {
new LocatorInFileModal(plugin, view.file, searchQuery).open()
}
}
}
function moveIndex(dir: 1 | -1): void { function moveIndex(dir: 1 | -1): void {
selectedIndex = loopIndex(selectedIndex + dir, resultNotes.length) selectedIndex = loopIndex(selectedIndex + dir, resultNotes.length)
scrollIntoView() scrollIntoView()
@ -325,9 +295,6 @@
{#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}
<button on:click={switchToInFileModal}>In-File search</button>
{/if}
</div> </div>
</InputSearch> </InputSearch>

View File

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { showExcerpt } from '../settings/index' import { setIcon, TFile } from 'obsidian'
import { onMount } from 'svelte'
import type { ResultNote } from '../globals' import type { ResultNote } from '../globals'
import type LocatorPlugin from '../main'
import { import {
getExtension, getExtension,
isFileCanvas, isFileCanvas,
@ -10,17 +12,14 @@
pathWithoutFilename, pathWithoutFilename,
} from '../tools/utils' } from '../tools/utils'
import ResultItemContainer from './ResultItemContainer.svelte' import ResultItemContainer from './ResultItemContainer.svelte'
import type LocatorPlugin from '../main'
import { setIcon, TFile } from 'obsidian'
import { onMount } from 'svelte'
// Import icon utility functions // Import icon utility functions
import { showExcerpt } from 'src/settings/utils'
import { import {
loadIconData,
initializeIconPacks,
getIconNameForPath,
loadIconSVG,
getDefaultIconSVG, getDefaultIconSVG,
getIconNameForPath,
initializeIconPacks,
loadIconData,
loadIconSVG,
} from '../tools/icon-utils' } from '../tools/icon-utils'
export let selected = false export let selected = false
@ -148,25 +147,24 @@
</script> </script>
<ResultItemContainer <ResultItemContainer
glyph="{glyph}" {glyph}
id="{note.path}" id={note.path}
cssClass=" {note.isEmbed ? 'omnisearch-result__embed' : ''}" cssClass=" {note.isEmbed ? 'omnisearch-result__embed' : ''}"
on:auxclick on:auxclick
on:click on:click
on:mousemove on:mousemove
selected="{selected}"> {selected}>
<div> <div>
<div class="omnisearch-result__title-container"> <div class="omnisearch-result__title-container">
<span class="omnisearch-result__title"> <span class="omnisearch-result__title">
{#if note.isEmbed} {#if note.isEmbed}
<span <span
bind:this="{elEmbedIcon}" bind:this={elEmbedIcon}
title="The document above is embedded in this note"></span> title="The document above is embedded in this note" />
{:else} {:else}
<!-- File Icon --> <!-- File Icon -->
{#if fileIconSVG} {#if fileIconSVG}
<span class="omnisearch-result__icon" use:renderSVG="{fileIconSVG}" <span class="omnisearch-result__icon" use:renderSVG={fileIconSVG} />
></span>
{/if} {/if}
{/if} {/if}
<span> <span>
@ -194,8 +192,7 @@
<div class="omnisearch-result__folder-path"> <div class="omnisearch-result__folder-path">
<!-- Folder Icon --> <!-- Folder Icon -->
{#if folderIconSVG} {#if folderIconSVG}
<span class="omnisearch-result__icon" use:renderSVG="{folderIconSVG}" <span class="omnisearch-result__icon" use:renderSVG={folderIconSVG} />
></span>
{/if} {/if}
<span> <span>
{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)} {@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}
@ -218,7 +215,7 @@
<!-- Image --> <!-- Image -->
{#if imagePath} {#if imagePath}
<div class="omnisearch-result__image-container"> <div class="omnisearch-result__image-container">
<img style="width: 100px" src="{imagePath}" alt="" /> <img style="width: 100px" src={imagePath} alt="" />
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -1,10 +1,9 @@
import { MarkdownView, Modal, TFile } from 'obsidian'
import type { Modifier } from 'obsidian' import type { Modifier } from 'obsidian'
import ModalVault from './ModalVault.svelte' import { MarkdownView, Modal } from 'obsidian'
import ModalInFile from './ModalInFile.svelte' import { mount, unmount } from 'svelte'
import { Action, eventBus, EventNames, isInputComposition } from '../globals' import { Action, eventBus, EventNames, isInputComposition } from '../globals'
import type LocatorPlugin from '../main' import type LocatorPlugin from '../main'
import { mount, unmount } from 'svelte' import ModalVault from './ModalVault.svelte'
abstract class LocatorModal extends Modal { abstract class LocatorModal extends Modal {
protected constructor(plugin: LocatorPlugin) { protected constructor(plugin: LocatorPlugin) {
@ -188,36 +187,3 @@ export class LocatorVaultModal extends LocatorModal {
}) })
} }
} }
export class LocatorInFileModal extends LocatorModal {
constructor(
plugin: LocatorPlugin,
file: TFile,
searchQuery: string = '',
parent?: LocatorModal
) {
super(plugin)
const cmp = mount(ModalInFile, {
target: this.modalEl,
props: {
plugin,
modal: this,
singleFilePath: file.path,
parent: parent,
previousQuery: searchQuery,
},
})
if (parent) {
// Hide the parent vault modal, and show it back when this one is closed
parent.containerEl.toggleVisibility(false)
}
this.onClose = () => {
if (parent) {
parent.containerEl.toggleVisibility(true)
}
unmount(cmp)
}
}
}

View File

@ -11,8 +11,6 @@ export const regexExtensions = /(?:^|\s)\.(\w+)/g
export const excerptBefore = 100 export const excerptBefore = 100
export const excerptAfter = 300 export const excerptAfter = 300
export const K_DISABLE_OMNISEARCH = 'locator-disabled'
export const eventBus = new EventBus() export const eventBus = new EventBus()
export const EventNames = { export const EventNames = {

View File

@ -6,37 +6,26 @@ import {
type PluginManifest, type PluginManifest,
TFile, TFile,
} from 'obsidian' } from 'obsidian'
import { import { LocatorVaultModal } from './components/modals'
LocatorInFileModal, import { Database } from './database'
LocatorVaultModal,
} from './components/modals'
import {
getDefaultSettings,
loadSettings,
SettingsTab,
showExcerpt,
} from './settings'
import type { LocatorSettings } from './settings/utils'
import { isCacheEnabled } from './settings/utils'
import { saveSettings } from './settings/utils'
import { isPluginDisabled } from './settings/utils'
import { import {
eventBus, eventBus,
EventNames, EventNames,
indexingStep, indexingStep,
IndexingStepType, IndexingStepType,
type TextExtractorApi, type TextExtractorApi,
type AIImageAnalyzerAPI,
} from './globals' } from './globals'
import { notifyOnIndexed, registerAPI } from './tools/api'
import { Database } from './database'
import { SearchEngine } from './search/search-engine'
import { DocumentsRepository } from './repositories/documents-repository'
import { logVerbose } from './tools/utils'
import { NotesIndexer } from './notes-indexer' import { NotesIndexer } from './notes-indexer'
import { TextProcessor } from './tools/text-processing' import { DocumentsRepository } from './repositories/documents-repository'
import { EmbedsRepository } from './repositories/embeds-repository' import { EmbedsRepository } from './repositories/embeds-repository'
import { SearchEngine } from './search/search-engine'
import { SearchHistory } from './search/search-history' import { SearchHistory } from './search/search-history'
import { getDefaultSettings, loadSettings, SettingsTab } from './settings'
import type { LocatorSettings } from './settings/utils'
import { isCacheEnabled, saveSettings, showExcerpt } from './settings/utils'
import { notifyOnIndexed, registerAPI } from './tools/api'
import { TextProcessor } from './tools/text-processing'
import { logVerbose } from './tools/utils'
export default class LocatorPlugin extends Plugin { export default class LocatorPlugin extends Plugin {
// FIXME: fix the type // FIXME: fix the type
@ -70,11 +59,6 @@ export default class LocatorPlugin extends Plugin {
) )
} }
if (isPluginDisabled(this.app)) {
console.debug('Plugin disabled')
return
}
await cleanOldCacheFiles(this.app) await cleanOldCacheFiles(this.app)
await this.database.clearOldDatabases() await this.database.clearOldDatabases()
@ -100,16 +84,6 @@ export default class LocatorPlugin extends Plugin {
}, },
}) })
this.addCommand({
id: 'show-modal-infile',
name: 'In-file search',
editorCallback: (_editor, view) => {
if (view.file) {
new LocatorInFileModal(this, view.file).open()
}
},
})
const searchEngine = this.searchEngine const searchEngine = this.searchEngine
this.app.workspace.onLayoutReady(async () => { this.app.workspace.onLayoutReady(async () => {
@ -229,14 +203,6 @@ export default class LocatorPlugin extends Plugin {
return (this.app as any).plugins?.plugins?.['text-extractor']?.api return (this.app as any).plugins?.plugins?.['text-extractor']?.api
} }
/**
* Plugin dependency - Ai Image Analyzer
* @returns
*/
public getAIImageAnalyzer(): AIImageAnalyzerAPI | undefined {
return (this.app as any).plugins?.plugins?.['ai-image-analyzer']?.api
}
private async populateIndex(): Promise<void> { private async populateIndex(): Promise<void> {
console.time('Indexing total time') console.time('Indexing total time')
indexingStep.set(IndexingStepType.ReadingFiles) indexingStep.set(IndexingStepType.ReadingFiles)
@ -291,7 +257,7 @@ export default class LocatorPlugin extends Plugin {
// Disable settings.useCache while writing the cache, in case it freezes // Disable settings.useCache while writing the cache, in case it freezes
const cacheEnabled = this.settings.useCache const cacheEnabled = this.settings.useCache
if (cacheEnabled && !this.settings.DANGER_forceSaveCache) { if (cacheEnabled) {
this.settings.useCache = false this.settings.useCache = false
await saveSettings(this) await saveSettings(this)
} }

View File

@ -1,7 +1,7 @@
import type { TAbstractFile } from 'obsidian' import type { TAbstractFile } from 'obsidian'
import type { IndexedDocument } from './globals'
import type LocatorPlugin from './main' import type LocatorPlugin from './main'
import { removeAnchors } from './tools/notes' import { removeAnchors } from './tools/notes'
import type { IndexedDocument } from './globals'
import { import {
isFileCanvas, isFileCanvas,
isFileFromDataloom, isFileFromDataloom,
@ -44,17 +44,14 @@ export class NotesIndexer {
public isContentIndexable(path: string): boolean { public isContentIndexable(path: string): boolean {
const settings = this.plugin.settings const settings = this.plugin.settings
const hasTextExtractor = !!this.plugin.getTextExtractor() const hasTextExtractor = !!this.plugin.getTextExtractor()
const hasAIImageAnalyzer = !!this.plugin.getAIImageAnalyzer()
const canIndexPDF = hasTextExtractor && settings.PDFIndexing const canIndexPDF = hasTextExtractor && settings.PDFIndexing
const canIndexImages = hasTextExtractor && settings.imagesIndexing const canIndexImages = hasTextExtractor && settings.imagesIndexing
const canIndexImagesAI = hasAIImageAnalyzer && settings.aiImageIndexing
return ( return (
this.isFilePlaintext(path) || this.isFilePlaintext(path) ||
isFileCanvas(path) || isFileCanvas(path) ||
isFileFromDataloom(path) || isFileFromDataloom(path) ||
(canIndexPDF && isFilePDF(path)) || (canIndexPDF && isFilePDF(path)) ||
(canIndexImages && isFileImage(path)) || (canIndexImages && isFileImage(path))
(canIndexImagesAI && isFileImage(path))
) )
} }

View File

@ -1,5 +1,8 @@
import { normalizePath, Notice, TFile } from 'obsidian' import { normalizePath, TFile } from 'obsidian'
import type { CanvasData } from 'obsidian/canvas'
import type { IndexedDocument } from '../globals' import type { IndexedDocument } from '../globals'
import type LocatorPlugin from '../main'
import { getNonExistingNotes } from '../tools/notes'
import { import {
countError, countError,
extractHeadingsFromCache, extractHeadingsFromCache,
@ -14,9 +17,6 @@ import {
removeDiacritics, removeDiacritics,
stripMarkdownCharacters, stripMarkdownCharacters,
} from '../tools/utils' } from '../tools/utils'
import type { CanvasData } from 'obsidian/canvas'
import type LocatorPlugin from '../main'
import { getNonExistingNotes } from '../tools/notes'
export class DocumentsRepository { export class DocumentsRepository {
/** /**
@ -96,7 +96,6 @@ export class DocumentsRepository {
let content: string | null = null let content: string | null = null
const extractor = this.plugin.getTextExtractor() const extractor = this.plugin.getTextExtractor()
const aiImageAnalyzer = this.plugin.getAIImageAnalyzer()
// ** Plain text ** // ** Plain text **
// Just read the file content // Just read the file content
@ -151,10 +150,8 @@ export class DocumentsRepository {
// ** Image ** // ** Image **
else if ( else if (
isFileImage(path) && isFileImage(path) &&
((this.plugin.settings.imagesIndexing && (this.plugin.settings.imagesIndexing &&
extractor?.canFileBeExtracted(path)) || extractor?.canFileBeExtracted(path))
(this.plugin.settings.aiImageIndexing &&
aiImageAnalyzer?.canBeAnalyzed(file)))
) { ) {
if ( if (
this.plugin.settings.imagesIndexing && this.plugin.settings.imagesIndexing &&
@ -162,13 +159,6 @@ export class DocumentsRepository {
) { ) {
content = await extractor.extractText(file) content = await extractor.extractText(file)
} }
if (
this.plugin.settings.aiImageIndexing &&
aiImageAnalyzer?.canBeAnalyzed(file)
) {
content = (await aiImageAnalyzer.analyzeImage(file)) + (content ?? '')
}
} }
// ** PDF ** // ** PDF **
else if ( else if (

View File

@ -331,9 +331,7 @@ export class SearchEngine {
results.map(async result => { results.map(async result => {
const doc = await this.plugin.documentsRepository.getDocument(result.id) const doc = await this.plugin.documentsRepository.getDocument(result.id)
if (!doc) { if (!doc) {
console.warn( console.warn(`Locator - Note "${result.id}" not in the live cache`)
`Locator - Note "${result.id}" not in the live cache`
)
countError(true) countError(true)
} }
return doc return doc
@ -349,12 +347,7 @@ export class SearchEngine {
const title = document?.path.toLowerCase() ?? '' const title = document?.path.toLowerCase() ?? ''
const content = (document?.cleanedContent ?? '').toLowerCase() const content = (document?.cleanedContent ?? '').toLowerCase()
return exactTerms.every( return exactTerms.every(
q => q => content.includes(q) || removeDiacritics(title).includes(q)
content.includes(q) ||
removeDiacritics(
title,
this.plugin.settings.ignoreArabicDiacritics
).includes(q)
) )
}) })
} }
@ -524,11 +517,7 @@ export class SearchEngine {
} }
return (doc as any)[fieldName] return (doc as any)[fieldName]
}, },
processTerm: (term: string) => processTerm: (term: string) => removeDiacritics(term).toLowerCase(),
(this.plugin.settings.ignoreDiacritics
? removeDiacritics(term, this.plugin.settings.ignoreArabicDiacritics)
: term
).toLowerCase(),
idField: 'path', idField: 'path',
fields: [ fields: [
'basename', 'basename',

View File

@ -1,7 +1,7 @@
import type { QueryCombination } from 'minisearch' import type { QueryCombination } from 'minisearch'
import { BRACKETS_AND_SPACE, chsRegex, SPACE_OR_PUNCTUATION } from '../globals' import { BRACKETS_AND_SPACE, chsRegex, SPACE_OR_PUNCTUATION } from '../globals'
import { logVerbose, splitCamelCase, splitHyphens } from '../tools/utils'
import type LocatorPlugin from '../main' import type LocatorPlugin from '../main'
import { splitCamelCase, splitHyphens } from '../tools/utils'
const markdownLinkExtractor = require('markdown-link-extractor') const markdownLinkExtractor = require('markdown-link-extractor')
@ -17,26 +17,16 @@ export class Tokenizer {
public tokenizeForIndexing(text: string): string[] { public tokenizeForIndexing(text: string): string[] {
try { try {
const words = this.tokenizeWords(text) const words = this.tokenizeWords(text)
let urls: string[] = []
if (this.plugin.settings.tokenizeUrls) {
try {
urls = markdownLinkExtractor(text)
} catch (e) {
logVerbose('Error extracting urls', e)
}
}
let tokens = this.tokenizeTokens(text, { skipChs: true }) let tokens = this.tokenizeTokens(text, { skipChs: true })
tokens = [...tokens.flatMap(token => [ tokens = [
...tokens.flatMap(token => [
token, token,
...splitHyphens(token), ...splitHyphens(token),
...splitCamelCase(token), ...splitCamelCase(token),
]), ...words] ]),
...words,
// Add urls ]
if (urls.length) {
tokens = [...tokens, ...urls]
}
// Remove duplicates // Remove duplicates
tokens = [...new Set(tokens)] tokens = [...new Set(tokens)]

View File

@ -1,26 +1,15 @@
// noinspection CssUnresolvedCustomProperty // noinspection CssUnresolvedCustomProperty
import { import { App, Plugin, PluginSettingTab, Setting } from 'obsidian'
App, import { RecencyCutoff } from '../globals'
Plugin,
PluginSettingTab,
Setting,
} from 'obsidian'
import { writable } from 'svelte/store'
import { K_DISABLE_OMNISEARCH, RecencyCutoff } from '../globals'
import type LocatorPlugin from '../main' import type LocatorPlugin from '../main'
import { enableVerboseLogging } from '../tools/utils' import { enableVerboseLogging } from '../tools/utils'
import { injectSettingsIndexing } from './settings-indexing'
import { type LocatorSettings, saveSettings } from './utils'
import { injectSettingsBehavior } from './settings-behavior' import { injectSettingsBehavior } from './settings-behavior'
import { injectSettingsDanger } from './settings-danger'
import { injectSettingsHttp } from './settings-http'
import { injectSettingsIndexing } from './settings-indexing'
import { injectSettingsUserInterface } from './settings-ui' import { injectSettingsUserInterface } from './settings-ui'
import { injectSettingsWeighting } from './settings-weighting' import { injectSettingsWeighting } from './settings-weighting'
import { injectSettingsHttp } from './settings-http' import { type LocatorSettings, saveSettings, showExcerpt } from './utils'
import { injectSettingsDanger } from './settings-danger'
/**
* A store to reactively toggle the `showExcerpt` setting on the fly
*/
export const showExcerpt = writable(false)
export class SettingsTab extends PluginSettingTab { export class SettingsTab extends PluginSettingTab {
plugin: LocatorPlugin plugin: LocatorPlugin
@ -37,15 +26,9 @@ export class SettingsTab extends PluginSettingTab {
display(): void { display(): void {
const { containerEl } = this const { containerEl } = this
const database = this.plugin.database
containerEl.empty() containerEl.empty()
if (this.app.loadLocalStorage(K_DISABLE_OMNISEARCH) == '1') {
const span = containerEl.createEl('span')
span.innerHTML = `<strong style="color: var(--text-accent)">⚠️ OMNISEARCH IS DISABLED ⚠️</strong>`
}
// Settings main title // Settings main title
containerEl.createEl('h1', { text: 'Locator' }) containerEl.createEl('h1', { text: 'Locator' })
@ -100,16 +83,12 @@ export function getDefaultSettings(app: App): LocatorSettings {
hideExcluded: false, hideExcluded: false,
recencyBoost: RecencyCutoff.Disabled, recencyBoost: RecencyCutoff.Disabled,
downrankedFoldersFilters: [] as string[], downrankedFoldersFilters: [] as string[],
ignoreDiacritics: true,
ignoreArabicDiacritics: false,
indexedFileTypes: [] as string[], indexedFileTypes: [] as string[],
displayTitle: '', displayTitle: '',
PDFIndexing: false, PDFIndexing: false,
officeIndexing: false, officeIndexing: false,
imagesIndexing: false, imagesIndexing: false,
aiImageIndexing: false,
unsupportedFilesIndexing: 'default', unsupportedFilesIndexing: 'default',
splitCamelCase: false,
openInNewPane: false, openInNewPane: false,
vimLikeNavigationShortcut: app.vault.getConfig('vimMode') as boolean, vimLikeNavigationShortcut: app.vault.getConfig('vimMode') as boolean,
@ -118,10 +97,8 @@ export function getDefaultSettings(app: App): LocatorSettings {
maxEmbeds: 5, maxEmbeds: 5,
renderLineReturnInExcerpts: true, renderLineReturnInExcerpts: true,
showCreateButton: false, showCreateButton: false,
highlight: true,
showPreviousQueryResults: true, showPreviousQueryResults: true,
simpleSearch: false, simpleSearch: false,
tokenizeUrls: false,
fuzziness: '1', fuzziness: '1',
weightBasename: 10, weightBasename: 10,
@ -134,13 +111,11 @@ export function getDefaultSettings(app: App): LocatorSettings {
httpApiEnabled: false, httpApiEnabled: false,
httpApiPort: '51361', httpApiPort: '51361',
httpApiNotice: true,
welcomeMessage: '', welcomeMessage: '',
verboseLogging: false, verboseLogging: false,
DANGER_httpHost: null, DANGER_httpHost: null,
DANGER_forceSaveCache: false,
} }
} }
@ -156,9 +131,7 @@ export let settings: LocatorSettings
// return settings // return settings
// } // }
export async function loadSettings( export async function loadSettings(plugin: Plugin): Promise<LocatorSettings> {
plugin: Plugin
): Promise<LocatorSettings> {
settings = Object.assign( settings = Object.assign(
{}, {},
getDefaultSettings(plugin.app), getDefaultSettings(plugin.app),

View File

@ -70,22 +70,6 @@ export function injectSettingsBehavior(
}) })
}) })
// Split CamelCaseWords
new Setting(containerEl)
.setName('Split CamelCaseWords')
.setDesc(
htmlDescription(`Enable this if you want to be able to search for CamelCaseWords as separate words.<br/>
<span style="color: var(--text-accent)">Changing this setting will clear the cache.</span><br>
${needsARestart}`)
)
.addToggle(toggle =>
toggle.setValue(settings.splitCamelCase).onChange(async v => {
await database.clearCache()
settings.splitCamelCase = v
await saveSettings(plugin)
})
)
// Simpler search // Simpler search
new Setting(containerEl) new Setting(containerEl)
.setName('Simpler search') .setName('Simpler search')
@ -100,23 +84,6 @@ export function injectSettingsBehavior(
}) })
) )
// Extract URLs
// Crashes on iOS
if (!Platform.isIosApp) {
new Setting(containerEl)
.setName('Tokenize URLs')
.setDesc(
`Enable this if you want to be able to search for URLs as separate words.
This setting has a strong impact on indexing performance, and can crash Obsidian under certain conditions.`
)
.addToggle(toggle =>
toggle.setValue(settings.tokenizeUrls).onChange(async v => {
settings.tokenizeUrls = v
await saveSettings(plugin)
})
)
}
// Open in new pane // Open in new pane
new Setting(containerEl) new Setting(containerEl)
.setName('Open in new pane') .setName('Open in new pane')

View File

@ -1,10 +1,7 @@
import { Notice, Setting } from 'obsidian' import { Setting } from 'obsidian'
import type { LocatorSettings } from './utils'
import { isCacheEnabled } from './utils'
import { saveSettings } from './utils'
import { htmlDescription, isPluginDisabled, needsARestart } from './utils'
import type LocatorPlugin from 'src/main' import type LocatorPlugin from 'src/main'
import { K_DISABLE_OMNISEARCH } from 'src/globals' import type { LocatorSettings } from './utils'
import { htmlDescription, isCacheEnabled, needsARestart } from './utils'
export function injectSettingsDanger( export function injectSettingsDanger(
plugin: LocatorPlugin, plugin: LocatorPlugin,
@ -15,69 +12,6 @@ export function injectSettingsDanger(
new Setting(containerEl).setName('Danger Zone').setHeading() new Setting(containerEl).setName('Danger Zone').setHeading()
// Ignore diacritics
new Setting(containerEl)
.setName('Ignore diacritics')
.setDesc(
htmlDescription(`Normalize diacritics in search terms. Words like "brûlée" or "žluťoučký" will be indexed as "brulee" and "zlutoucky".<br/>
<span style="color: var(--text-accent)">You probably should <strong>NOT</strong> disable this.</span><br>
<span style="color: var(--text-accent)">Changing this setting will clear the cache.</span><br>
${needsARestart}`)
)
.addToggle(toggle =>
toggle.setValue(settings.ignoreDiacritics).onChange(async v => {
await database.clearCache()
settings.ignoreDiacritics = v
await saveSettings(plugin)
})
)
new Setting(containerEl)
.setName('Ignore Arabic diacritics (beta)')
.addToggle(toggle =>
toggle.setValue(settings.ignoreArabicDiacritics).onChange(async v => {
await database.clearCache()
settings.ignoreArabicDiacritics = v
await saveSettings(plugin)
})
)
// Disable Locator
const disableDesc = new DocumentFragment()
disableDesc.createSpan({}, span => {
span.innerHTML = `Disable Locator on this device only.<br>
${needsARestart}`
})
new Setting(containerEl)
.setName('Disable on this device')
.setDesc(disableDesc)
.addToggle(toggle =>
toggle.setValue(isPluginDisabled(plugin.app)).onChange(async v => {
if (v) {
plugin.app.saveLocalStorage(K_DISABLE_OMNISEARCH, '1')
new Notice('Locator - Disabled. Please restart Obsidian.')
} else {
plugin.app.saveLocalStorage(K_DISABLE_OMNISEARCH) // No value = unset
new Notice('Locator - Enabled. Please restart Obsidian.')
}
})
)
// Force save cache
new Setting(containerEl)
.setName('Force save the cache')
.setDesc(
htmlDescription(`Locator has a security feature that automatically disables cache writing if it cannot fully perform the operation.<br>
Use this option to force the cache to be saved, even if it causes a crash.<br>
<span style="color: var(--text-accent)">Enabling this setting could lead to crash loops</span>`)
)
.addToggle(toggle =>
toggle.setValue(settings.DANGER_forceSaveCache).onChange(async v => {
settings.DANGER_forceSaveCache = v
await saveSettings(plugin)
})
)
// Clear cache data // Clear cache data
if (isCacheEnabled()) { if (isCacheEnabled()) {
new Setting(containerEl) new Setting(containerEl)

View File

@ -50,17 +50,5 @@ export function injectSettingsHttp(
await saveSettings(plugin) await saveSettings(plugin)
}) })
}) })
new Setting(containerEl)
.setName('Show a notification when the server starts')
.setDesc(
'Will display a notification if the server is enabled, at Obsidian startup.'
)
.addToggle(toggle =>
toggle.setValue(settings.httpApiNotice).onChange(async v => {
settings.httpApiNotice = v
await saveSettings(plugin)
})
)
} }
} }

View File

@ -11,7 +11,6 @@ export function injectSettingsIndexing(
containerEl: HTMLElement containerEl: HTMLElement
) { ) {
const textExtractor = plugin.getTextExtractor() const textExtractor = plugin.getTextExtractor()
const aiImageAnalyzer = plugin.getAIImageAnalyzer()
const database = plugin.database const database = plugin.database
const clearCacheDebounced = debounce(async () => { const clearCacheDebounced = debounce(async () => {
@ -28,11 +27,6 @@ export function injectSettingsIndexing(
? `👍 You have installed <a href="https://github.com/scambier/obsidian-text-extractor">Text Extractor</a>, Locator can use it to index PDFs and images contents. ? `👍 You have installed <a href="https://github.com/scambier/obsidian-text-extractor">Text Extractor</a>, Locator can use it to index PDFs and images contents.
<br />Text extraction only works on desktop, but the cache can be synchronized with your mobile device.` <br />Text extraction only works on desktop, but the cache can be synchronized with your mobile device.`
: `⚠️ Locator requires <a href="https://github.com/scambier/obsidian-text-extractor">Text Extractor</a> to index PDFs and images.` : `⚠️ Locator requires <a href="https://github.com/scambier/obsidian-text-extractor">Text Extractor</a> to index PDFs and images.`
}
${
aiImageAnalyzer
? `<br/>👍 You have installed <a href="https://github.com/Swaggeroo/obsidian-ai-image-analyzer">AI Image Analyzer</a>, Locator can use it to index images contents with ai.`
: `<br/>⚠️ Locator requires <a href="https://github.com/Swaggeroo/obsidian-ai-image-analyzer">AI Image Analyzer</a> to index images with ai.`
}`) }`)
) )
@ -87,23 +81,6 @@ export function injectSettingsIndexing(
) )
.setDisabled(!textExtractor) .setDisabled(!textExtractor)
// AI Images Indexing
const aiIndexImagesDesc = new DocumentFragment()
aiIndexImagesDesc.createSpan({}, span => {
span.innerHTML = `Locator will use AI Image Analyzer to index the content of your images with ai.`
})
new Setting(containerEl)
.setName(`Images AI indexing ${aiImageAnalyzer ? '' : '⚠️ Disabled'}`)
.setDesc(aiIndexImagesDesc)
.addToggle(toggle =>
toggle.setValue(settings.aiImageIndexing).onChange(async v => {
await database.clearCache()
settings.aiImageIndexing = v
await saveSettings(plugin)
})
)
.setDisabled(!aiImageAnalyzer)
// Index filenames of unsupported files // Index filenames of unsupported files
new Setting(containerEl) new Setting(containerEl)
.setName('Index paths of unsupported files') .setName('Index paths of unsupported files')
@ -115,7 +92,7 @@ export function injectSettingsIndexing(
) )
.addDropdown(dropdown => { .addDropdown(dropdown => {
dropdown dropdown
.addOptions({ yes: 'Yes', no: 'No', default: 'Obsidian setting' }) .addOptions({ yes: 'Yes', no: 'No', default: 'Obsidian default' })
.setValue(settings.unsupportedFilesIndexing) .setValue(settings.unsupportedFilesIndexing)
.onChange(async v => { .onChange(async v => {
await clearCacheDebounced() await clearCacheDebounced()

View File

@ -1,9 +1,7 @@
import { Setting } from 'obsidian' import { Setting } from 'obsidian'
import type LocatorPlugin from 'src/main' import type LocatorPlugin from 'src/main'
import { showExcerpt } from '.'
import type { LocatorSettings } from './utils' import type { LocatorSettings } from './utils'
import { saveSettings } from './utils' import { htmlDescription, saveSettings, showExcerpt } from './utils'
import { htmlDescription } from './utils'
export function injectSettingsUserInterface( export function injectSettingsUserInterface(
plugin: LocatorPlugin, plugin: LocatorPlugin,
@ -82,17 +80,4 @@ export function injectSettingsUserInterface(
await saveSettings(plugin) await saveSettings(plugin)
}) })
) )
// Highlight results
new Setting(containerEl)
.setName('Highlight matching words in results')
.setDesc(
'Will highlight matching results when enabled. See README for more customization options.'
)
.addToggle(toggle =>
toggle.setValue(settings.highlight).onChange(async v => {
settings.highlight = v
await saveSettings(plugin)
})
)
} }

View File

@ -1,7 +1,13 @@
import { App, Platform, Plugin } from 'obsidian' import { Platform, Plugin } from 'obsidian'
import { K_DISABLE_OMNISEARCH, RecencyCutoff } from 'src/globals' import { RecencyCutoff } from 'src/globals'
import { writable } from 'svelte/store'
import { settings } from '.' import { settings } from '.'
/**
* A store to reactively toggle the `showExcerpt` setting on the fly
*/
export const showExcerpt = writable(false)
export function htmlDescription(innerHTML: string): DocumentFragment { export function htmlDescription(innerHTML: string): DocumentFragment {
const desc = new DocumentFragment() const desc = new DocumentFragment()
desc.createSpan({}, span => { desc.createSpan({}, span => {
@ -20,9 +26,6 @@ export interface WeightingSettings {
weightH3: number weightH3: number
weightUnmarkedTags: number weightUnmarkedTags: number
} }
export function isPluginDisabled(app: App): boolean {
return app.loadLocalStorage(K_DISABLE_OMNISEARCH) === '1'
}
export async function saveSettings(plugin: Plugin): Promise<void> { export async function saveSettings(plugin: Plugin): Promise<void> {
await plugin.saveData(settings) await plugin.saveData(settings)
@ -41,9 +44,6 @@ export interface LocatorSettings extends WeightingSettings {
recencyBoost: RecencyCutoff recencyBoost: RecencyCutoff
/** downrank files in the given folders */ /** downrank files in the given folders */
downrankedFoldersFilters: string[] downrankedFoldersFilters: string[]
/** Ignore diacritics when indexing files */
ignoreDiacritics: boolean
ignoreArabicDiacritics: boolean
/** Extensions of plain text files to index, in addition to .md */ /** Extensions of plain text files to index, in addition to .md */
indexedFileTypes: string[] indexedFileTypes: string[]
@ -55,8 +55,6 @@ export interface LocatorSettings extends WeightingSettings {
imagesIndexing: boolean imagesIndexing: boolean
/** Enable Office documents indexing */ /** Enable Office documents indexing */
officeIndexing: boolean officeIndexing: boolean
/** Enable image ai indexing */
aiImageIndexing: boolean
/** Enable indexing of unknown files */ /** Enable indexing of unknown files */
unsupportedFilesIndexing: 'yes' | 'no' | 'default' unsupportedFilesIndexing: 'yes' | 'no' | 'default'
@ -76,17 +74,12 @@ export interface LocatorSettings extends WeightingSettings {
welcomeMessage: string welcomeMessage: string
/** If a query returns 0 result, try again with more relax conditions */ /** If a query returns 0 result, try again with more relax conditions */
simpleSearch: boolean simpleSearch: boolean
tokenizeUrls: boolean
highlight: boolean
splitCamelCase: boolean
openInNewPane: boolean openInNewPane: boolean
verboseLogging: boolean verboseLogging: boolean
vimLikeNavigationShortcut: boolean vimLikeNavigationShortcut: boolean
fuzziness: '0' | '1' | '2' fuzziness: '0' | '1' | '2'
httpApiEnabled: boolean httpApiEnabled: boolean
httpApiPort: string httpApiPort: string
httpApiNotice: boolean
DANGER_httpHost: string | null DANGER_httpHost: string | null
DANGER_forceSaveCache: boolean
} }

View File

@ -48,10 +48,15 @@ export function getServer(plugin: LocatorPlugin) {
}, },
() => { () => {
console.log(`Locator - Started HTTP server on port ${port}`) console.log(`Locator - Started HTTP server on port ${port}`)
if (plugin.settings.DANGER_httpHost && plugin.settings.DANGER_httpHost !== 'localhost') { if (
new Notice(`Locator - Started non-localhost HTTP server at ${plugin.settings.DANGER_httpHost}:${port}`, 120_000) plugin.settings.DANGER_httpHost &&
} plugin.settings.DANGER_httpHost !== 'localhost'
else if (plugin.settings.httpApiNotice) { ) {
new Notice(
`Locator - Started non-localhost HTTP server at ${plugin.settings.DANGER_httpHost}:${port}`,
120_000
)
} else {
new Notice(`Locator - Started HTTP server on port ${port}`) new Notice(`Locator - Started HTTP server on port ${port}`)
} }
} }
@ -67,7 +72,7 @@ export function getServer(plugin: LocatorPlugin) {
close() { close() {
server.close() server.close()
console.log(`Locator - Terminated HTTP server`) console.log(`Locator - Terminated HTTP server`)
if (plugin.settings.httpApiEnabled && plugin.settings.httpApiNotice) { if (plugin.settings.httpApiEnabled) {
new Notice(`Locator - Terminated HTTP server`) new Notice(`Locator - Terminated HTTP server`)
} }
}, },

View File

@ -87,8 +87,8 @@ export function getApi(plugin: LocatorPlugin) {
return { return {
async search(q: string): Promise<ResultNoteApi[]> { async search(q: string): Promise<ResultNoteApi[]> {
const query = new Query(q, { const query = new Query(q, {
ignoreDiacritics: plugin.settings.ignoreDiacritics, ignoreDiacritics: true,
ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics, ignoreArabicDiacritics: true,
}) })
const raw = await plugin.searchEngine.getSuggestions(query) const raw = await plugin.searchEngine.getSuggestions(query)
return mapResults(plugin, raw) return mapResults(plugin, raw)

View File

@ -15,9 +15,7 @@ export class TextProcessor {
* @returns The html string with the matches highlighted * @returns The html string with the matches highlighted
*/ */
public highlightText(text: string, matches: SearchMatch[]): string { public highlightText(text: string, matches: SearchMatch[]): string {
const highlightClass = `suggestion-highlight locator-highlight ${ const highlightClass = 'suggestion-highlight locator-highlight locator-default-highlight'
this.plugin.settings.highlight ? 'locator-default-highlight' : ''
}`
if (!matches.length) { if (!matches.length) {
return text return text
@ -68,9 +66,7 @@ export class TextProcessor {
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'), ' ')
if (this.plugin.settings.ignoreDiacritics) { text = removeDiacritics(text)
text = removeDiacritics(text, this.plugin.settings.ignoreArabicDiacritics)
}
const startTime = new Date().getTime() const startTime = new Date().getTime()
let match: RegExpExecArray | null = null let match: RegExpExecArray | null = null
let matches: SearchMatch[] = [] let matches: SearchMatch[] = []

View File

@ -115,7 +115,7 @@ const diacriticsRegex = new RegExp(`(?!${regexpExclude})\\p{Diacritic}`, 'gu')
/** /**
* https://stackoverflow.com/a/37511463 * https://stackoverflow.com/a/37511463
*/ */
export function removeDiacritics(str: string, arabic = false): string { export function removeDiacritics(str: string, arabic = true): string {
if (str === null || str === undefined) { if (str === null || str === undefined) {
return '' return ''
} }