mirror of
https://github.com/quorafind/Obsidian-Float-Search.git
synced 2026-07-22 07:30:25 +00:00
Compare commits
No commits in common. "master" and "v4.3.0" have entirely different histories.
5 changed files with 14 additions and 320 deletions
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "float-search",
|
||||
"name": "Floating Search",
|
||||
"version": "4.3.0",
|
||||
"minAppVersion": "1.2.0",
|
||||
"version": "4.2.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "You can use search view in modal/leaf/popout window now.",
|
||||
"author": "Boninall",
|
||||
"authorUrl": "https://github.com/Quorafind",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "float-search",
|
||||
"version": "4.3.0",
|
||||
"version": "4.2.0",
|
||||
"description": "You can use search view in modal/leaf/popout window now.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
PaneType,
|
||||
Plugin,
|
||||
prepareFuzzySearch,
|
||||
prepareSimpleSearch,
|
||||
renderResults,
|
||||
SearchResult,
|
||||
requireApiVersion,
|
||||
|
|
@ -68,9 +67,6 @@ interface FloatSearchSettings {
|
|||
defaultViewType: searchType;
|
||||
cmdkTriggerKey: CmdkTriggerKey;
|
||||
cmdkDoubleTapInterval: number;
|
||||
cmdkQuickCreate: boolean;
|
||||
cmdkQuickCreateFolder: string;
|
||||
cmdkQuickCreateTitleFormat: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: FloatSearchSettings = {
|
||||
|
|
@ -87,9 +83,6 @@ const DEFAULT_SETTINGS: FloatSearchSettings = {
|
|||
defaultViewType: "modal",
|
||||
cmdkTriggerKey: "Shift",
|
||||
cmdkDoubleTapInterval: 300,
|
||||
cmdkQuickCreate: false,
|
||||
cmdkQuickCreateFolder: "",
|
||||
cmdkQuickCreateTitleFormat: "YYYYMMDDHHmmss",
|
||||
};
|
||||
|
||||
const allViews: viewType[] = [
|
||||
|
|
@ -1384,54 +1377,6 @@ class FloatSearchSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
containerEl.createEl("h3", { text: "Quick Create" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Enable quick create")
|
||||
.setDesc(
|
||||
"When no exact match is found in quick search, show an option to create a new note with the search text as content."
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.cmdkQuickCreate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cmdkQuickCreate = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Quick create folder")
|
||||
.setDesc(
|
||||
"Folder to create new notes in. Leave empty for vault root."
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("e.g. Inbox")
|
||||
.setValue(this.plugin.settings.cmdkQuickCreateFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cmdkQuickCreateFolder =
|
||||
value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Title format")
|
||||
.setDesc(
|
||||
"Timestamp format for the note title. Tokens: YYYY, MM, DD, HH, mm, ss."
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("YYYYMMDDHHmmss")
|
||||
.setValue(
|
||||
this.plugin.settings.cmdkQuickCreateTitleFormat
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cmdkQuickCreateTitleFormat =
|
||||
value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1951,11 +1896,7 @@ interface CmdkResult {
|
|||
pathMatch: SearchResult | null;
|
||||
heading?: string;
|
||||
headingMatch?: SearchResult | null;
|
||||
content?: string;
|
||||
contentMatch?: SearchResult | null;
|
||||
line?: number;
|
||||
type: "file" | "heading" | "content" | "create";
|
||||
createQuery?: string;
|
||||
type: "file" | "heading";
|
||||
}
|
||||
|
||||
class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
||||
|
|
@ -2057,31 +1998,8 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
return sb - sa;
|
||||
});
|
||||
|
||||
// Check if there is an exact name match (for quick-create gating)
|
||||
const queryLower = query.trim().toLowerCase();
|
||||
const hasExactMatch = fileResults.some(
|
||||
(r) => r.file.basename.toLowerCase() === queryLower
|
||||
);
|
||||
|
||||
const resultsToShow = fileResults.slice(0, this.limit);
|
||||
|
||||
// Append "quick create" option when enabled and no exact match
|
||||
if (
|
||||
this.plugin.settings.cmdkQuickCreate &&
|
||||
!hasExactMatch &&
|
||||
query.trim().length > 0
|
||||
) {
|
||||
resultsToShow.push({
|
||||
file: null as any,
|
||||
nameMatch: null,
|
||||
pathMatch: null,
|
||||
type: "create",
|
||||
createQuery: query.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// Render file results immediately
|
||||
chooser.setSuggestions(resultsToShow);
|
||||
chooser.setSuggestions(fileResults.slice(0, this.limit));
|
||||
|
||||
// Phase 2: heading search — progressive, batched via setTimeout
|
||||
if (query.trim().length >= 2) {
|
||||
|
|
@ -2091,14 +2009,6 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
chooser,
|
||||
signal
|
||||
);
|
||||
|
||||
// Phase 3: content search — progressive, async (reads file content)
|
||||
this.progressiveContentSearch(
|
||||
files,
|
||||
query,
|
||||
chooser,
|
||||
signal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2151,114 +2061,11 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
setTimeout(processBatch, 0);
|
||||
}
|
||||
|
||||
private progressiveContentSearch(
|
||||
files: TFile[],
|
||||
query: string,
|
||||
chooser: any,
|
||||
signal: AbortSignal
|
||||
) {
|
||||
const BATCH_SIZE = 50;
|
||||
const MAX_CONTENT_RESULTS = 20;
|
||||
const DURATION_LIMIT = 5; // ms before yielding
|
||||
const simpleSearch = prepareSimpleSearch(query);
|
||||
let idx = 0;
|
||||
let added = 0;
|
||||
|
||||
const processBatch = async () => {
|
||||
if (signal.aborted) return;
|
||||
const start = performance.now();
|
||||
|
||||
for (; idx < files.length; idx++) {
|
||||
if (signal.aborted || added >= MAX_CONTENT_RESULTS) return;
|
||||
|
||||
// Adaptive yielding: check time every BATCH_SIZE items
|
||||
if (idx % BATCH_SIZE === 0 && idx > 0) {
|
||||
if (performance.now() - start > DURATION_LIMIT) {
|
||||
setTimeout(processBatch, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const file = files[idx];
|
||||
if (file.extension !== "md") continue;
|
||||
|
||||
let text: string;
|
||||
try {
|
||||
text = await this.app.vault.cachedRead(file);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = simpleSearch(text);
|
||||
if (!result) continue;
|
||||
|
||||
// Compute line number and context from first match offset
|
||||
const matchStart = result.matches[0][0];
|
||||
let line = 0;
|
||||
let lineStart = 0;
|
||||
for (let i = 0; i < matchStart; i++) {
|
||||
if (text.charCodeAt(i) === 10) {
|
||||
line++;
|
||||
lineStart = i + 1;
|
||||
}
|
||||
}
|
||||
let lineEnd = text.indexOf("\n", lineStart);
|
||||
if (lineEnd === -1) lineEnd = text.length;
|
||||
const lineText = text.substring(lineStart, lineEnd).trim();
|
||||
|
||||
// Recompute match on the line for highlight offsets
|
||||
const lineMatch = simpleSearch(lineText);
|
||||
|
||||
chooser.addSuggestion({
|
||||
file,
|
||||
nameMatch: null,
|
||||
pathMatch: null,
|
||||
content: lineText,
|
||||
contentMatch: lineMatch,
|
||||
line,
|
||||
type: "content",
|
||||
} as CmdkResult);
|
||||
added++;
|
||||
if (added >= MAX_CONTENT_RESULTS) return;
|
||||
}
|
||||
};
|
||||
|
||||
// Start after heading search has a chance to render
|
||||
setTimeout(processBatch, 50);
|
||||
}
|
||||
|
||||
renderSuggestion(result: CmdkResult, el: HTMLElement) {
|
||||
el.addClass("mod-complex");
|
||||
|
||||
if (result.type === "create") {
|
||||
el.addClass("float-search-cmdk-create-item");
|
||||
const contentEl = el.createDiv("suggestion-content");
|
||||
const titleEl = contentEl.createDiv("suggestion-title");
|
||||
titleEl.setText("Create new note");
|
||||
const noteEl = contentEl.createDiv("suggestion-note");
|
||||
const folder =
|
||||
this.plugin.settings.cmdkQuickCreateFolder || "/";
|
||||
noteEl.setText(
|
||||
`"${result.createQuery}" → ${folder}`
|
||||
);
|
||||
const auxEl = el.createDiv("suggestion-aux");
|
||||
const flair = auxEl.createSpan("suggestion-flair");
|
||||
setIcon(flair, "plus");
|
||||
return;
|
||||
}
|
||||
|
||||
const contentEl = el.createDiv("suggestion-content");
|
||||
|
||||
if (result.type === "content" && result.content) {
|
||||
const titleEl = contentEl.createDiv("suggestion-title");
|
||||
if (result.contentMatch) {
|
||||
renderResults(titleEl, result.content, result.contentMatch);
|
||||
} else {
|
||||
titleEl.setText(result.content);
|
||||
}
|
||||
const noteEl = contentEl.createDiv("suggestion-note");
|
||||
noteEl.setText(result.file.path);
|
||||
} else if (result.type === "heading" && result.heading) {
|
||||
if (result.type === "heading" && result.heading) {
|
||||
const titleEl = contentEl.createDiv("suggestion-title");
|
||||
if (result.headingMatch) {
|
||||
renderResults(titleEl, result.heading, result.headingMatch);
|
||||
|
|
@ -2299,11 +2106,6 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
result: CmdkResult,
|
||||
evt: MouseEvent | KeyboardEvent
|
||||
) {
|
||||
if (result.type === "create") {
|
||||
this.quickCreateNote(result.createQuery ?? "", evt);
|
||||
return;
|
||||
}
|
||||
|
||||
const leaf = Keymap.isModEvent(evt)
|
||||
? this.app.workspace.getLeaf("tab")
|
||||
: this.app.workspace.getMostRecentLeaf() ??
|
||||
|
|
@ -2311,9 +2113,8 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
|
||||
const eState: Record<string, any> = {};
|
||||
if (result.type === "heading" && result.heading) {
|
||||
// Use subpath to jump to heading
|
||||
eState.subpath = "#" + result.heading;
|
||||
} else if (result.type === "content" && result.line != null) {
|
||||
eState.line = result.line;
|
||||
}
|
||||
|
||||
leaf.setViewState({
|
||||
|
|
@ -2321,96 +2122,28 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
state: { file: result.file.path },
|
||||
active: true,
|
||||
}).then(() => {
|
||||
if (eState.subpath || eState.line != null) {
|
||||
if (eState.subpath) {
|
||||
// Apply eState after view is ready
|
||||
leaf.setEphemeralState(eState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async quickCreateNote(
|
||||
content: string,
|
||||
evt: MouseEvent | KeyboardEvent
|
||||
) {
|
||||
const settings = this.plugin.settings;
|
||||
const fmt = settings.cmdkQuickCreateTitleFormat || "YYYYMMDDHHmmss";
|
||||
const title = this.formatTimestamp(new Date(), fmt);
|
||||
|
||||
// Resolve target folder
|
||||
let folderPath = settings.cmdkQuickCreateFolder.trim();
|
||||
if (!folderPath || folderPath === "/") {
|
||||
folderPath = "";
|
||||
}
|
||||
// Ensure folder exists
|
||||
if (folderPath) {
|
||||
const folder =
|
||||
this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = folderPath
|
||||
? `${folderPath}/${title}.md`
|
||||
: `${title}.md`;
|
||||
|
||||
const file = await this.app.vault.create(filePath, content);
|
||||
|
||||
const leaf = Keymap.isModEvent(evt)
|
||||
? this.app.workspace.getLeaf("tab")
|
||||
: this.app.workspace.getMostRecentLeaf() ??
|
||||
this.app.workspace.getLeaf();
|
||||
|
||||
await leaf.setViewState({
|
||||
type: "markdown",
|
||||
state: { file: file.path },
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
private formatTimestamp(date: Date, fmt: string): string {
|
||||
const pad = (n: number, len = 2) =>
|
||||
String(n).padStart(len, "0");
|
||||
const tokens: Record<string, string> = {
|
||||
YYYY: String(date.getFullYear()),
|
||||
YY: String(date.getFullYear()).slice(-2),
|
||||
MM: pad(date.getMonth() + 1),
|
||||
DD: pad(date.getDate()),
|
||||
HH: pad(date.getHours()),
|
||||
mm: pad(date.getMinutes()),
|
||||
ss: pad(date.getSeconds()),
|
||||
};
|
||||
let result = fmt;
|
||||
// Replace longest tokens first to avoid partial matches
|
||||
for (const token of ["YYYY", "YY", "MM", "DD", "HH", "mm", "ss"]) {
|
||||
result = result.split(token).join(tokens[token]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Called by internal Chooser on selection change
|
||||
// @ts-ignore
|
||||
onSelectedChange = debounce(
|
||||
(result: CmdkResult, _evt: Event | null) => {
|
||||
if (result?.type === "create" || !result?.file) {
|
||||
this.hidePreview();
|
||||
} else {
|
||||
this.showPreview(result);
|
||||
}
|
||||
if (result?.file) this.showPreview(result.file);
|
||||
},
|
||||
100
|
||||
);
|
||||
|
||||
private hidePreview() {
|
||||
if (this.previewEl) {
|
||||
this.previewEl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
async showPreview(result: CmdkResult) {
|
||||
async showPreview(file: TFile) {
|
||||
if (!this.previewEl) {
|
||||
this.previewEl = this.bodyEl.createDiv(
|
||||
"float-search-cmdk-preview"
|
||||
);
|
||||
this.modalEl.addClass("float-search-cmdk-expanded");
|
||||
const [leaf, view] = spawnLeafView(
|
||||
this.plugin,
|
||||
this.previewEl
|
||||
|
|
@ -2419,29 +2152,7 @@ class FloatSearchCmdkModal extends SuggestModal<CmdkResult> {
|
|||
this.fileEmbeddedView = view;
|
||||
this.fileLeaf.setPinned(true);
|
||||
}
|
||||
|
||||
this.previewEl.show();
|
||||
this.modalEl.addClass("float-search-cmdk-expanded");
|
||||
|
||||
const file = result.file;
|
||||
await this.fileLeaf!.openFile(file, { active: false });
|
||||
|
||||
const eState: Record<string, any> = {};
|
||||
if (result.type === "heading" && result.heading) {
|
||||
eState.subpath = "#" + result.heading;
|
||||
} else if (result.type === "content" && result.line != null) {
|
||||
eState.line = result.line;
|
||||
}
|
||||
|
||||
if (eState.subpath || eState.line != null) {
|
||||
this.fileLeaf!.setViewState({
|
||||
type: file.extension === "pdf" ? "pdf" : "markdown",
|
||||
state: { file: file.path },
|
||||
}).then(() => {
|
||||
this.fileLeaf?.setEphemeralState(eState);
|
||||
});
|
||||
}
|
||||
|
||||
this.inputEl.focus();
|
||||
}
|
||||
|
||||
|
|
|
|||
16
styles.css
16
styles.css
|
|
@ -213,19 +213,3 @@ body:not(.show-file-path) .search-result-file-title .search-result-file-path {
|
|||
.modal-container.float-search-cmdk-container.mod-dim {
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
/* ── Quick Create item ── */
|
||||
.float-search-cmdk-create-item {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.float-search-cmdk-create-item .suggestion-title {
|
||||
color: var(--text-accent);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.float-search-cmdk-create-item .suggestion-flair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,5 @@
|
|||
"4.0.0": "0.15.0",
|
||||
"4.1.0": "0.15.0",
|
||||
"4.1.1": "0.15.0",
|
||||
"4.2.0": "1.2.0",
|
||||
"4.3.0": "1.2.0"
|
||||
"4.2.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue