Merge branch 'develop'

This commit is contained in:
Simon Cambier
2023-11-11 10:08:17 +01:00
15 changed files with 194 additions and 89 deletions

View File

@@ -25,7 +25,6 @@
}
.omnisearch-result__title > span {
}
.omnisearch-result__folder-path {
@@ -37,9 +36,9 @@
}
.omnisearch-result__extension {
font-size: 0.7rem;
color: var(--text-muted);
}
font-size: 0.7rem;
color: var(--text-muted);
}
.omnisearch-result__counter {
font-size: 0.7rem;
@@ -62,7 +61,7 @@
.omnisearch-result__image-container {
flex-basis: 20%;
text-align: right
text-align: right;
}
.omnisearch-highlight {
@@ -78,6 +77,7 @@
.omnisearch-input-container {
display: flex;
align-items: center;
flex-direction: row;
gap: 5px;
}
@@ -86,6 +86,23 @@
.omnisearch-input-container {
flex-direction: column;
}
.omnisearch-input-container__buttons {
display: flex;
flex-direction: row;
width: 100%;
padding: 0 1em 0 1em;
gap: 1em;
}
.omnisearch-input-container__buttons > button {
flex-grow: 1;
}
}
@media only screen and (min-width: 600px) {
.omnisearch-input-container__buttons {
margin-right: 1em;
}
}
.omnisearch-input-field {

View File

@@ -1,6 +1,6 @@
{
"name": "scambier.obsidian-search",
"version": "1.18.1",
"version": "1.19.0-beta.1",
"description": "A search engine for Obsidian",
"main": "dist/main.js",
"scripts": {

View File

@@ -217,6 +217,9 @@ class CacheManager {
await database.searchHistory.bulkAdd(history)
}
/**
* @returns The search history, in reverse chronological order
*/
public async getSearchHistory(): Promise<ReadonlyArray<string>> {
const data = (await database.searchHistory.toArray())
.reverse()

View File

@@ -59,4 +59,4 @@
type="text" />
</div>
<slot />
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import InputSearch from './InputSearch.svelte'
import {
Action,
Action,
eventBus,
excerptAfter,
type ResultNote,
@@ -9,7 +9,7 @@
} from 'src/globals'
import { getCtrlKeyLabel, loopIndex } from 'src/tools/utils'
import { onDestroy, onMount, tick } from 'svelte'
import { MarkdownView } from 'obsidian'
import { MarkdownView, App, Platform } from 'obsidian'
import ModalContainer from './ModalContainer.svelte'
import {
OmnisearchInFileModal,
@@ -24,6 +24,7 @@
export let parent: OmnisearchVaultModal | null = null
export let singleFilePath = ''
export let previousQuery: string | undefined
export let app: App
let searchQuery: string
let groupedOffsets: number[] = []
@@ -150,7 +151,13 @@
<InputSearch
on:input="{e => (searchQuery = e.detail)}"
placeholder="Omnisearch - File"
initialValue="{previousQuery}" />
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}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { MarkdownView, Notice, TFile } from 'obsidian'
import { App, MarkdownView, Notice, Platform, TFile } from 'obsidian'
import { onDestroy, onMount, tick } from 'svelte'
import InputSearch from './InputSearch.svelte'
import ModalContainer from './ModalContainer.svelte'
@@ -29,9 +29,12 @@
import { cacheManager } from '../cache-manager'
import { searchEngine } from 'src/search/omnisearch'
import { cancelable, CancelablePromise } from 'cancelable-promise'
import { debounce } from 'lodash-es'
export let modal: OmnisearchVaultModal
export let previousQuery: string | undefined
export let app: App
let selectedIndex = 0
let historySearchIndex = 0
let searchQuery: string | undefined
@@ -59,10 +62,7 @@
createInCurrentPaneKey = 'shift ↵'
}
$: if (searchQuery) {
searching = true
updateResults().then(() => {
searching = false
})
updateResultsDebounced()
} else {
searching = false
resultNotes = []
@@ -79,11 +79,11 @@
indexingStepDesc = 'Indexing files...'
break
case IndexingStepType.WritingCache:
updateResults()
updateResultsDebounced()
indexingStepDesc = 'Updating cache...'
break
default:
updateResults()
updateResultsDebounced()
indexingStepDesc = ''
break
}
@@ -102,9 +102,7 @@
eventBus.on('vault', Action.PrevSearchHistory, prevSearchHistory)
eventBus.on('vault', Action.NextSearchHistory, nextSearchHistory)
await NotesIndex.refreshIndex()
if (settings.showPreviousQueryResults) {
previousQuery = (await cacheManager.getSearchHistory())[0]
}
await updateResultsDebounced()
})
onDestroy(() => {
@@ -132,6 +130,7 @@
let cancelableQuery: CancelablePromise<ResultNote[]> | null = null
async function updateResults() {
searching = true
// If search is already in progress, cancel it and start a new one
if (cancelableQuery) {
cancelableQuery.cancel()
@@ -146,8 +145,12 @@
resultNotes = await cancelableQuery
selectedIndex = 0
await scrollIntoView()
searching = false
}
// Debounce this function to avoid multiple calls caused by Svelte reactivity
const updateResultsDebounced = debounce(updateResults, 0)
function onClick(evt?: MouseEvent | KeyboardEvent) {
if (!selectedNote) return
if (evt?.ctrlKey) {
@@ -282,9 +285,14 @@
initialValue="{searchQuery}"
on:input="{e => (searchQuery = e.detail)}"
placeholder="Omnisearch - Vault">
{#if settings.showCreateButton}
<button on:click="{onClickCreateNote}">Create note</button>
{/if}
<div class="omnisearch-input-container__buttons">
{#if settings.showCreateButton}
<button on:click="{onClickCreateNote}">Create note</button>
{/if}
{#if Platform.isMobile}
<button on:click="{switchToInFileModal}">In-File search</button>
{/if}
</div>
</InputSearch>
{#if indexingStepDesc}
@@ -296,6 +304,7 @@
<ModalContainer>
{#each resultNotes as result, i}
<ResultItemVault
app="{app}"
selected="{i === selectedIndex}"
note="{result}"
on:mousemove="{_ => (selectedIndex = i)}"

View File

@@ -10,12 +10,13 @@
removeDiacritics,
} from '../tools/utils'
import ResultItemContainer from './ResultItemContainer.svelte'
import { setIcon } from 'obsidian'
import { TFile, setIcon, App } from 'obsidian'
import { cloneDeep } from 'lodash-es'
import { stringsToRegex, getMatches, makeExcerpt, highlightText } from 'src/tools/text-processing'
export let selected = false
export let note: ResultNote
export let app: App
let imagePath: string | null = null
let title = ''
@@ -26,10 +27,8 @@
$: {
imagePath = null
if (isFileImage(note.path)) {
// @ts-ignore
const file = app.vault.getFiles().find(f => f.path === note.path)
if (file) {
// @ts-ignore
const file = app.vault.getAbstractFileByPath(note.path)
if (file instanceof TFile) {
imagePath = app.vault.getResourcePath(file)
}
}

View File

@@ -4,6 +4,7 @@ import ModalVault from './ModalVault.svelte'
import ModalInFile from './ModalInFile.svelte'
import { Action, eventBus, EventNames, isInputComposition } from '../globals'
import { settings } from '../settings'
import { cacheManager } from 'src/cache-manager'
abstract class OmnisearchModal extends Modal {
protected constructor(app: App) {
@@ -142,25 +143,38 @@ abstract class OmnisearchModal extends Modal {
}
export class OmnisearchVaultModal extends OmnisearchModal {
/**
* Instanciate the Omnisearch vault modal
* @param app
* @param query The query to pre-fill the search field with
*/
constructor(app: App, query?: string) {
super(app)
// Get selected text
const selection = app.workspace.getActiveViewOfType(MarkdownView)?.editor.getSelection()
// Selected text in the editor
const selectedText = app.workspace
.getActiveViewOfType(MarkdownView)
?.editor.getSelection()
const cmp = new ModalVault({
target: this.modalEl,
props: {
modal: this,
previousQuery: selection ?? query,
},
cacheManager.getSearchHistory().then(history => {
// Previously searched query (if enabled in settings)
const previous = settings.showPreviousQueryResults ? history[0] : null
// Instantiate and display the Svelte component
const cmp = new ModalVault({
target: this.modalEl,
props: {
app,
modal: this,
previousQuery: query || selectedText || previous || '',
},
})
this.onClose = () => {
// Since the component is manually created,
// we also need to manually destroy it
cmp.$destroy()
}
})
this.onClose = () => {
// Since the component is manually created,
// we also need to manually destroy it
cmp.$destroy()
}
}
}
@@ -176,6 +190,7 @@ export class OmnisearchInFileModal extends OmnisearchModal {
const cmp = new ModalInFile({
target: this.modalEl,
props: {
app,
modal: this,
singleFilePath: file.path,
parent: parent,

View File

@@ -125,3 +125,4 @@ const separators =
.slice(1, -1)
export const SPACE_OR_PUNCTUATION_UNIQUE = new RegExp(`${separators}`, 'u')
export const SPACE_OR_PUNCTUATION = new RegExp(`${separators}+`, 'u')
export const BRACKETS_AND_SPACE = /[|\[\]\(\)<>\{\} \t\n\r]/u

View File

@@ -1,4 +1,4 @@
import { Notice, Platform, Plugin } from 'obsidian'
import { App, Notice, Platform, Plugin } from 'obsidian'
import {
OmnisearchInFileModal,
OmnisearchVaultModal,
@@ -46,7 +46,7 @@ export default class OmnisearchPlugin extends Plugin {
return
}
await cleanOldCacheFiles()
await cleanOldCacheFiles(this.app)
await OmnisearchCache.clearOldDatabases()
registerAPI(this)
@@ -66,7 +66,7 @@ export default class OmnisearchPlugin extends Plugin {
id: 'show-modal',
name: 'Vault search',
callback: () => {
new OmnisearchVaultModal(app).open()
new OmnisearchVaultModal(this.app).open()
},
})
@@ -75,12 +75,12 @@ export default class OmnisearchPlugin extends Plugin {
name: 'In-file search',
editorCallback: (_editor, view) => {
if (view.file) {
new OmnisearchInFileModal(app, view.file).open()
new OmnisearchInFileModal(this.app, view.file).open()
}
},
})
app.workspace.onLayoutReady(async () => {
this.app.workspace.onLayoutReady(async () => {
// Listeners to keep the search index up-to-date
this.registerEvent(
this.app.vault.on('create', file => {
@@ -155,7 +155,7 @@ export default class OmnisearchPlugin extends Plugin {
addRibbonButton(): void {
this.ribbonButton = this.addRibbonIcon('search', 'Omnisearch', _evt => {
new OmnisearchVaultModal(app).open()
new OmnisearchVaultModal(this.app).open()
})
}
@@ -168,7 +168,7 @@ export default class OmnisearchPlugin extends Plugin {
private async populateIndex(): Promise<void> {
console.time('Omnisearch - Indexing total time')
indexingStep.set(IndexingStepType.ReadingFiles)
const files = app.vault.getFiles().filter(f => isFileIndexable(f.path))
const files = this.app.vault.getFiles().filter(f => isFileIndexable(f.path))
console.log(`Omnisearch - ${files.length} files total`)
console.log(
`Omnisearch - Cache is ${isCacheEnabled() ? 'enabled' : 'disabled'}`
@@ -243,7 +243,7 @@ export default class OmnisearchPlugin extends Plugin {
* Read the files and feed them to Minisearch
*/
async function cleanOldCacheFiles() {
async function cleanOldCacheFiles(app: App) {
const toDelete = [
`${app.vault.configDir}/plugins/omnisearch/searchIndex.json`,
`${app.vault.configDir}/plugins/omnisearch/notesCache.json`,
@@ -264,12 +264,12 @@ async function cleanOldCacheFiles() {
function registerAPI(plugin: OmnisearchPlugin): void {
// Url scheme for obsidian://omnisearch?query=foobar
plugin.registerObsidianProtocolHandler('omnisearch', params => {
new OmnisearchVaultModal(app, params.query).open()
new OmnisearchVaultModal(plugin.app, params.query).open()
})
// Public api
// @ts-ignore
globalThis['omnisearch'] = api
// Deprecated
;(app as any).plugins.plugins.omnisearch.api = api
;(plugin.app as any).plugins.plugins.omnisearch.api = api
}

View File

@@ -1,6 +1,11 @@
import MiniSearch, { type Options, type SearchResult } from 'minisearch'
import type { DocumentRef, IndexedDocument, ResultNote } from '../globals'
import { chsRegex, getChsSegmenter, SPACE_OR_PUNCTUATION } from '../globals'
import {
BRACKETS_AND_SPACE,
chsRegex,
getChsSegmenter,
SPACE_OR_PUNCTUATION,
} from '../globals'
import { settings } from '../settings'
import {
chunkArray,
@@ -17,6 +22,8 @@ import { sortBy } from 'lodash-es'
import { getMatches, stringsToRegex } from 'src/tools/text-processing'
const tokenize = (text: string): string[] => {
const words = text.split(BRACKETS_AND_SPACE)
let tokens = text.split(SPACE_OR_PUNCTUATION)
// Split hyphenated tokens
@@ -25,15 +32,22 @@ const tokenize = (text: string): string[] => {
// Split camelCase tokens into "camel" and "case
tokens = [...tokens, ...tokens.flatMap(splitCamelCase)]
// Add whole words (aka "not tokens")
tokens = [...tokens, ...words]
// When enabled, we only use the chsSegmenter,
// and not the other custom tokenizers
const chsSegmenter = getChsSegmenter()
if (chsSegmenter) {
tokens = tokens.flatMap(word =>
const chs = tokens.flatMap(word =>
chsRegex.test(word) ? chsSegmenter.cut(word) : [word]
)
tokens = [...tokens, ...chs]
}
// Remove duplicates
tokens = [...new Set(tokens)]
return tokens
}

View File

@@ -61,6 +61,7 @@ export interface OmnisearchSettings extends WeightingSettings {
fuzziness: '0' | '1' | '2'
httpApiEnabled: boolean
httpApiPort: string
httpApiNotice: boolean
}
/**
@@ -74,7 +75,7 @@ export class SettingsTab extends PluginSettingTab {
plugin: OmnisearchPlugin
constructor(plugin: OmnisearchPlugin) {
super(app, plugin)
super(plugin.app, plugin)
this.plugin = plugin
showExcerpt.subscribe(async v => {
@@ -87,7 +88,7 @@ export class SettingsTab extends PluginSettingTab {
const { containerEl } = this
containerEl.empty()
if (app.loadLocalStorage(K_DISABLE_OMNISEARCH) == '1') {
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>`
}
@@ -220,6 +221,17 @@ export class SettingsTab extends PluginSettingTab {
})
)
// Show previous query results
new Setting(containerEl)
.setName('Show previous query results')
.setDesc('Re-executes the previous query when opening Omnisearch.')
.addToggle(toggle =>
toggle.setValue(settings.showPreviousQueryResults).onChange(async v => {
settings.showPreviousQueryResults = v
await saveSettings(this.plugin)
})
)
// Respect excluded files
new Setting(containerEl)
.setName('Respect Obsidian\'s "Excluded Files"')
@@ -367,17 +379,6 @@ export class SettingsTab extends PluginSettingTab {
})
)
// Show previous query results
new Setting(containerEl)
.setName('Show previous query results')
.setDesc('Re-executes the previous query when opening Omnisearch.')
.addToggle(toggle =>
toggle.setValue(settings.showPreviousQueryResults).onChange(async v => {
settings.showPreviousQueryResults = v
await saveSettings(this.plugin)
})
)
// Show "Create note" button
const createBtnDesc = new DocumentFragment()
createBtnDesc.createSpan({}, span => {
@@ -504,6 +505,18 @@ export class SettingsTab extends PluginSettingTab {
await saveSettings(this.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(this.plugin)
})
)
}
//#endregion HTTP Server
@@ -543,9 +556,9 @@ export class SettingsTab extends PluginSettingTab {
.addToggle(toggle =>
toggle.setValue(isPluginDisabled()).onChange(async v => {
if (v) {
app.saveLocalStorage(K_DISABLE_OMNISEARCH, '1')
this.app.saveLocalStorage(K_DISABLE_OMNISEARCH, '1')
} else {
app.saveLocalStorage(K_DISABLE_OMNISEARCH) // No value = unset
this.app.saveLocalStorage(K_DISABLE_OMNISEARCH) // No value = unset
}
new Notice('Omnisearch - Disabled. Please restart Obsidian.')
})
@@ -613,6 +626,7 @@ export const DEFAULT_SETTINGS: OmnisearchSettings = {
httpApiEnabled: false,
httpApiPort: '51361',
httpApiNotice: true,
welcomeMessage: '',
verboseLogging: false,

View File

@@ -2,7 +2,7 @@ import * as http from 'http'
import * as url from 'url'
import api from './api'
import { Notice } from 'obsidian'
import { saveSettings, settings } from 'src/settings'
import { settings } from 'src/settings'
export function getServer() {
const server = http.createServer(async function (req, res) {
@@ -47,7 +47,9 @@ export function getServer() {
},
() => {
console.log(`Omnisearch - Started HTTP server on port ${port}`)
new Notice(`Omnisearch - Started HTTP server on port ${port}`)
if (settings.httpApiNotice) {
new Notice(`Omnisearch - Started HTTP server on port ${port}`)
}
}
)
@@ -61,7 +63,9 @@ export function getServer() {
close() {
server.close()
console.log(`Omnisearch - Terminated HTTP server`)
new Notice(`Omnisearch - Terminated HTTP server`)
if (settings.httpApiNotice) {
new Notice(`Omnisearch - Terminated HTTP server`)
}
},
}
}

View File

@@ -38,7 +38,7 @@ export async function openNote(
return
}
const pos = view.editor.offsetToPos(offset)
pos.ch = 0
// pos.ch = 0
view.editor.setCursor(pos)
view.editor.scrollIntoView({

View File

@@ -29,24 +29,46 @@ export function highlighterGroups(_substring: string, ...args: any[]) {
* @returns The html string with the matches highlighted
*/
export function highlightText(text: string, matches: SearchMatch[]): string {
if (!matches.length) {
return text
}
const chsSegmenter = getChsSegmenter()
try {
return text.replace(
new RegExp(
matches
.map(matchInfo => `\\b${escapeRegExp(matchInfo.match)}\\b`)
.join('|'),
'giu'
),
match => {
const matchInfo = matches.find(info =>
match.match(new RegExp(`\\b${escapeRegExp(info.match)}\\b`, 'giu'))
// Text to highlight
const src = new RegExp(
matches
.map(
// This regex will match the word (with \b word boundary)
// and, if ChsSegmenter is active, the simple string (without word boundary)
matchItem =>
`\\b${escapeRegExp(matchItem.match)}\\b${
chsSegmenter ? `|${escapeRegExp(matchItem.match)}` : ''
}`
)
if (matchInfo) {
return `<span class="${highlightClass}">${match}</span>`
}
return match
}
.join('|'),
'giu'
)
// Replacer function that will highlight the matches
const replacer = (match: string) => {
const matchInfo = matches.find(info =>
match.match(
new RegExp(
`\\b${escapeRegExp(info.match)}\\b${
chsSegmenter ? `|${escapeRegExp(info.match)}` : ''
}`,
'giu'
)
)
)
if (matchInfo) {
return `<span class="${highlightClass}">${match}</span>`
}
return match
}
// Effectively highlight the text
return text.replace(src, replacer)
} catch (e) {
console.error('Omnisearch - Error in highlightText()', e)
return text
@@ -93,7 +115,7 @@ export function stringsToRegex(strings: string[]): RegExp {
')' +
`(${strings.map(s => escapeRegExp(s)).join('|')})`
return new RegExp(`${joined}`, 'gu')
return new RegExp(`${joined}`, 'gui')
}
export function getMatches(