Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/

# Build artifacts.
build/
dist/

# Various editor preferences.
.vscode
Expand All @@ -18,4 +19,4 @@ build/

docs/root/api/**/*.*
!docs/root/api/index.md
.docuaurus/
.docusaurus/
9 changes: 8 additions & 1 deletion src/api/local-api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IndexQuery } from "index/types/index-query";
import { Indexable } from "index/types/indexable";
import { MarkdownPage } from "index/types/markdown";
import { App } from "obsidian";
import { useFileMetadata, useFullQuery, useIndexUpdates, useInterning, useQuery } from "ui/hooks";
import { useFileMetadata, useFullQuery, useIndexUpdates, useInterning, useQuery, useLastOpenedFiles } from "ui/hooks";
import * as luxon from "luxon";
import * as preact from "preact";
import * as hooks from "preact/hooks";
Expand Down Expand Up @@ -182,6 +182,13 @@ export class DatacoreLocalApi {
return useFileMetadata(this.core, path, settings)!;
}

/** Get the list of opened files. Defaults the 10 most recent files. Can be overridden by setting `settings.limit` to
* the desired amount.
*/
public useLastOpenedFiles(settings?: { limit?: number; debounce?: number }): Indexable[] {
return useLastOpenedFiles(this.core, settings);
}

/** Automatically refresh the view whenever the index updates; returns the latest index revision ID. */
public useIndexUpdates(settings?: { debounce?: number }): number {
return useIndexUpdates(this.core, settings);
Expand Down
19 changes: 18 additions & 1 deletion src/index/datacore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { deferred, Deferred } from "utils/deferred";
import { Datastore, Substorer } from "index/datastore";
import { LocalStorageCache } from "index/persister";
import { Indexable, INDEXABLE_EXTENSIONS } from "index/types/indexable";
import { Indexable, INDEXABLE_EXTENSIONS, isFile } from "index/types/indexable";
import { FileImporter, ImportThrottle } from "index/web-worker/importer";
import { ImportResult } from "index/web-worker/message";
import { App, Component, EventRef, Events, MetadataCache, TAbstractFile, TFile, Vault } from "obsidian";
Expand Down Expand Up @@ -69,6 +69,23 @@ export class Datacore extends Component {
// Metadata cache handles markdown file updates.
this.registerEvent(this.metadataCache.on("resolve", (file) => this.reload(file)));

this.registerEvent(
this.app.workspace.on("file-open", (file) => {
if (file instanceof TFile) {
const maybeMetadata: Indexable | undefined = this.datastore.load(file.path);
if (isFile(maybeMetadata)) {
maybeMetadata.$atime = DateTime.now();
// we just update the top-level store, we don't need to recursively re-store everything.
this.datastore.store(maybeMetadata);
if (maybeMetadata instanceof MarkdownPage || maybeMetadata instanceof Canvas) {
this.persister.storeFile(maybeMetadata.$path, maybeMetadata.json());
}
this.trigger("update", this.revision);
}
}
})
);

// Renames do not set off the metadata cache; catch these explicitly.
this.registerEvent(this.vault.on("rename", this.rename, this));

Expand Down
3 changes: 3 additions & 0 deletions src/index/types/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable,
$types: string[] = Canvas.TYPES;
$typename: string = "Canvas";

$atime?: DateTime;
$ctime: DateTime;
$mtime: DateTime;

Expand Down Expand Up @@ -75,6 +76,7 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable,
$cards: this.$cards.map((x) => x.json()) as JsonCanvasCard[],
$ctime: this.$ctime.toMillis(),
$mtime: this.$mtime.toMillis(),
$atime: this.$atime?.toMillis(),
$size: this.$size,
$links: this.$links,
$path: this.$path,
Expand All @@ -101,6 +103,7 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable,
$cards: cards,
$ctime: DateTime.fromMillis(raw.$ctime),
$mtime: DateTime.fromMillis(raw.$mtime),
$atime: raw.$atime ? DateTime.fromMillis(raw.$atime) : undefined,
$size: raw.$size,
$extension: "canvas",
$path: raw.$path,
Expand Down
5 changes: 4 additions & 1 deletion src/index/types/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ export class GenericFile implements File, Indexable, Fieldbearing, Linkable {
$ctime: DateTime;
/** Obsidian-provided date this page was modified. */
$mtime: DateTime;
/** Timestamp of last file access, as determined by inspecting `file-open` workspace events */
$atime?: DateTime;
/** Obsidian-provided size of this page in bytes. */
$size: number;
/** The extension of the file. */
$extension: string;

public constructor(path: string, ctime: DateTime, mtime: DateTime, size: number) {
public constructor(path: string, ctime: DateTime, mtime: DateTime, size: number, atime?: DateTime) {
this.$path = path;
this.$ctime = ctime;
this.$mtime = mtime;
this.$atime = atime;
this.$size = size;

const lastDot = path.lastIndexOf(".");
Expand Down
34 changes: 34 additions & 0 deletions src/index/types/indexable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface Linkable {
$link: Link;
}

export function isLinkable(obj: any): obj is Linkable {
if (obj && obj.$types !== undefined && Array.isArray(obj.$types) && obj.$types.contains(LINKABLE_TYPE)) {
return true;
}

return false;
}

/** General metadata for any file. */
export const FILE_TYPE = "file";
/**
Expand All @@ -44,12 +52,22 @@ export interface File extends Linkable {
$ctime: DateTime;
/** Obsidian-provided date this page was modified. */
$mtime: DateTime;
/** Timestamp of last file access, as determined by inspecting `file-open` workspace events */
$atime?: DateTime;
/** Obsidian-provided size of this page in bytes. */
$size: number;
/** The extension of the file. */
$extension: string;
}

export function isFile(obj: any): obj is File {
if (obj && obj.$types !== undefined && Array.isArray(obj.$types) && obj.$types.contains(FILE_TYPE)) {
return true;
}

return false;
}

/** Metadata for taggable objects. */
export const TAGGABLE_TYPE = "taggable";
/**
Expand All @@ -60,6 +78,14 @@ export interface Taggable {
$tags: string[];
}

export function isTaggable(obj: any): obj is Taggable {
if (obj && obj.$types !== undefined && Array.isArray(obj.$types) && obj.$types.contains(TAGGABLE_TYPE)) {
return true;
}

return false;
}

/** Metadata for objects which can link to other things. */
export const LINKBEARING_TYPE = "links";
/**
Expand All @@ -70,6 +96,14 @@ export interface Linkbearing {
$links: Link[];
}

export function isLinkbearing(obj: any): obj is Linkbearing {
if (obj && obj.$types !== undefined && Array.isArray(obj.$types) && obj.$types.contains(LINKBEARING_TYPE)) {
return true;
}

return false;
}

/**
* All supported extensions. This should probably become a dynamic lookup table and not just
* a fixed list at some point, especially if we add the ability to turn indexing on/off.
Expand Down
2 changes: 2 additions & 0 deletions src/index/types/json/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface JsonCanvas {
$ctime: number;
/** Last modified time as a UNIX epoch time in milliseconds. */
$mtime: number;
/** Last access time as a UNIX epoch time in milliseconds. */
$atime?: number;
/** All tags in the canvas. */
$tags: string[];
/** All links in the canvas. */
Expand Down
2 changes: 2 additions & 0 deletions src/index/types/json/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface JsonMarkdownPage {
$ctime: number;
/** Obsidian-provided date this page was modified. */
$mtime: number;
/** Timestamp of last file access, as determined by inspecting `file-open` workspace events */
$atime?: number;
/** The extension; for markdown files, almost always '.md'. */
$extension: string;
/** Obsidian-provided size of this page in bytes. */
Expand Down
4 changes: 4 additions & 0 deletions src/index/types/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie
$ctime: DateTime;
/** Obsidian-provided date this page was modified. */
$mtime: DateTime;
/** Timestamp of last file access, as determined by inspecting `file-open` workspace events */
$atime?: DateTime;
/** The extension; for markdown files, almost always '.md'. */
$extension: string;
/** Obsidian-provided size of this page in bytes. */
Expand Down Expand Up @@ -89,6 +91,7 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie
$infields: mapObjectValues(raw.$infields, (field) => normalizeLinks(valueInlineField(field), normalizer)),
$ctime: DateTime.fromMillis(raw.$ctime),
$mtime: DateTime.fromMillis(raw.$mtime),
$atime: raw.$atime ? DateTime.fromMillis(raw.$atime) : undefined,
$extension: raw.$extension,
$size: raw.$size,
$position: raw.$position,
Expand Down Expand Up @@ -140,6 +143,7 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie
$infields: mapObjectValues(this.$infields, jsonInlineField),
$ctime: this.$ctime.toMillis(),
$mtime: this.$mtime.toMillis(),
$atime: this.$atime?.toMillis(),
$extension: this.$extension,
$size: this.$size,
$position: this.$position,
Expand Down
34 changes: 33 additions & 1 deletion src/ui/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import { Datacore } from "index/datacore";
import { debounce } from "obsidian";
import { IndexQuery } from "index/types/index-query";
import { Indexable } from "index/types/indexable";
import { Indexable, File, isFile } from "index/types/indexable";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { SearchResult } from "index/datastore";
import { Literals } from "expression/literal";
import { Result } from "api/result";
import { QUERY } from "expression/parser";

/** Hook that updates the view whenever the revision updates, returning the newest revision.
* @group Hooks
Expand All @@ -26,6 +27,37 @@ export function useIndexUpdates(datacore: Datacore, settings?: { debounce?: numb
return revision;
}

export function useLastOpenedFiles(datacore: Datacore, settings?: { limit?: number; debounce?: number }): Indexable[] {
const limit = settings?.limit ?? 10;
const indexRevision = useIndexUpdates(datacore, settings);
const query = useRef(QUERY.query.tryParse("@file AND exists($atime)"));

return useMemo(() => {
return datacore.datastore
.search(query.current)
.map(
(result) =>
(result.results.filter((file) => isFile(file) && file.$atime !== undefined) as unknown as File[])
.sort((left, right) => {
return right.$atime!.toMillis() - left.$atime!.toMillis();
})
.slice(0, limit !== 0 ? limit : undefined) as unknown as Indexable[]
)
.orElse(
datacore.app.workspace
.getLastOpenFiles()
.reduce((indexables: Indexable[], path: string) => {
const file = datacore.datastore.load(path);
if (file) {
indexables.push(file);
}
return indexables;
}, [])
.slice(0, limit !== 0 ? limit : undefined)
);
}, [settings?.limit, indexRevision]);
}

/** A hook which updates whenever file metadata for a specific file updates.
* @group Hooks
*/
Expand Down
Loading