Skip to content

Commit

Permalink
Add tests and better search results
Browse files Browse the repository at this point in the history
  • Loading branch information
catacgc committed Sep 17, 2024
1 parent 66e4cb5 commit 41268df
Show file tree
Hide file tree
Showing 16 changed files with 1,709 additions and 99 deletions.
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,30 @@ Consider the following example: you have daily/weekly notes where you jolt down
in `2026-01-01.md` you have the following:

```markdown
- [[MyArea]]
- [[MyProject]]
- https://my-project-reference #bookmark
- [[Project1]]
- [An external link to remember](https://company-reference) #bookmark
```

in another day, you decide you want to add a note to `My Project` in `2026-01-02.md`. Most natural way to do so in Obsidian
is to quickly nest it in your otline
As your work progresses you add more notes, nested with a hierarchy that evolves as your work evolves.
Obsidian is good at linking these notes together easily. You start adding more important links

```markdown
- [[MyProject]]
- [[MyProject Task]] with some optional inline context
- [[Project1]]
- [[Project1 Task]] with some optional inline context
- https://task-tracker/item - this is something I should keep track of temporarily

- [[Project2]]
- https://my-project-himepage #bookmark/important
```

yet another day comes and you start ading some references tot the area you're working on in `2026-01-03.md`
Not let's decide you want to start organizing a bit and organize your more important projects [[ImportantProjects]] page

```markdown
- [[MyArea]]
- https://some-web-reference #bookmark/important
- [[Project1]]
- [[Project2]]
```

TreeSearch let's explore these connections quickly and easily.
TreeSearch let's search and explore these connections across your vault, easily.

![img.png](docs/img/img.png)

Expand All @@ -39,6 +42,9 @@ TreeSearch let's explore these connections quickly and easily.
The plugin adds a new command `TreeSearch: Search` that opens a new pane with a search bar.
You can search for a note and see all the notes (and links) that are nested under it.

The search algo is very simple and only supports exact matches for now. There are only two operators you should care about:
- `>` - searches down in the tee

## Funding URL

You can include funding URLs where people who use your plugin can financially support it.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.13",
"@types/markdown-it": "^14.1.2",
"@types/node": "^16.11.6",
"@types/react": "^18.3.5",
Expand Down
8 changes: 4 additions & 4 deletions src/obsidian-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { App } from "obsidian";

export async function openFileAndHighlightLine(app: App, path: string, lineNumber: number) {
export async function openFileAndHighlightLine(app: App, path: string, start: { line: number, col: number }, end: { line: number, col: number }) {
const file = app.vault.getAbstractFileByPath(path);
if (file) {
const leaf = this.app.workspace.getLeaf();
Expand All @@ -9,9 +9,9 @@ export async function openFileAndHighlightLine(app: App, path: string, lineNumbe
// Set timeout to ensure the file is loaded
setTimeout(() => {
const editor = this.app.workspace.activeLeaf.view.sourceMode.cmEditor as CodeMirror.Editor;
editor.setCursor(lineNumber, 0);
editor.setCursor(start.line, 0);
editor.focus();
editor.setSelection({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: 40 });
editor.setSelection({ line: start.line, ch: start.col }, { line: end.line, ch: end.col });
}, 100);
}
}
}
55 changes: 55 additions & 0 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export type SearchExpr = {
operator: "and" | "or" | "not" | "value",
children: SearchExpr[],
value: string
};

function matchesExpr(text: string, expr: SearchExpr): boolean {
if (expr.operator === "and") {
return expr.children.every(c => matchesExpr(text, c));
} else if (expr.operator === "or") {
return expr.children.some(c => matchesExpr(text, c));
} else if (expr.operator === "not") {
return !matchesExpr(text, expr.children[0]);
} else {
return text.includes(expr.value);
}
}

export function parseQuery(query: string): SearchExpr {
const words = query.toLowerCase().split(" ")
.map(it => it.trim())
.filter(it => it.length > 0);
const stack: SearchExpr[] = [];

for (const word of words) {
if (word === "and") {
stack.push({ operator: "and", children: [], value: "" });
} else if (word === "or") {
stack.push({ operator: "or", children: [], value: "" });
} else if (word.startsWith("-")) {
stack.push({ operator: "not", children: [{ operator: "value", children: [], value: word.substring(1) }], value: "" });
} else {
stack.push({ operator: "value", children: [], value: word });
}
}

const root: SearchExpr = { operator: "and", children: [], value: "" };
let current: SearchExpr = root;

for (const node of stack) {
if (node.operator === "and" || node.operator === "or") {
const newNode: SearchExpr = { operator: node.operator, children: [], value: "" };
current.children.push(newNode);
current = newNode;
} else {
current.children.push(node);
}
}

return root;
}

export function matchQuery(text: string, parsedQuery: SearchExpr): boolean {
return matchesExpr(text, parsedQuery);
}
48 changes: 25 additions & 23 deletions src/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {EdgeAttributes, GraphAttributes, Index, NodeAttributes} from "./tree-builder";
import {DirectedGraphOfNotes, EdgeAttributes, GraphAttributes, Index, NodeAttributes} from "./tree-builder";
import Graph from "graphology";
import {matchQuery, parseQuery, SearchExpr} from "./query";

export type ResultNode = {
value: string,
Expand All @@ -8,20 +9,18 @@ export type ResultNode = {
parents: string[]
}

type GF = Graph<NodeAttributes, EdgeAttributes, GraphAttributes>

function filterTreeByWord(node: ResultNode, word: string): ResultNode | null {
// Check if the current node contains the word
if (node.value.includes(word)) {
function filterTreeByWord(node: ResultNode, expr: SearchExpr): ResultNode | null {
// Check if the current node contains the expr
if (matchQuery(node.value, expr)) {
return node;
}

// Recursively filter the children
const filteredChildren = node.children
.map(child => filterTreeByWord(child, word))
.map(child => filterTreeByWord(child, expr))
.filter(child => child !== null) as ResultNode[];

// If no children contain the word, return null
// If no children contain the expr, return null
if (filteredChildren.length === 0) {
return null;
}
Expand All @@ -33,19 +32,19 @@ function filterTreeByWord(node: ResultNode, word: string): ResultNode | null {
};
}

function filterDown(results: ResultNode[], words: string[]): ResultNode[] {
if (words.length === 0) return results
function filterDown(results: ResultNode[], search: SearchExpr[]): ResultNode[] {
if (search.length === 0) return results

const word = words[0]
const expr = search[0]

const filtered = results
.map(r => filterTreeByWord(r, word))
.map(r => filterTreeByWord(r, expr))
.filter(child => child !== null) as ResultNode[]

return filterDown(filtered, words.slice(1))
return filterDown(filtered, search.slice(1))
}

function traverseChildren(graph: GF, node: ResultNode, depth: number, allChildren: Set<string>) {
function traverseChildren(graph: DirectedGraphOfNotes, node: ResultNode, depth: number, allChildren: Set<string>) {
if (depth > 2) return

const neighbours = graph.outboundNeighborEntries(node.value)
Expand Down Expand Up @@ -74,30 +73,33 @@ function getParents(graph: Graph, node: string) {
return parents;
}


export function searchIndex(index: Index, qs: string): ResultNode[] {
export function searchIndex(graph: DirectedGraphOfNotes, qs: string): ResultNode[] {
if (qs.length < 3) return []

const words = qs.split(">")
const treeWords = qs.split(">")
.map(w => w.toLowerCase().trim())
const anchor = words.length > 0 ? words[0] : ""
.map(it => parseQuery(it))

const filtered: ResultNode[] = []

const nodes = index.graph.filterNodes(n => n.toLowerCase().includes(anchor))
const nodes = graph.filterNodes(n => matchQuery(n, treeWords[0]))
const allChildren = new Set<string>()

for (const node of nodes) {
const attrs = index.graph.getNodeAttributes(node)
const attrs = graph.getNodeAttributes(node)
const newNode = {
value: node,
children: [],
attrs: attrs,
parents: getParents(index.graph, node)
parents: getParents(graph, node)
}

filtered.push(newNode)
traverseChildren(index.graph, newNode, 0, allChildren)
traverseChildren(graph, newNode, 0, allChildren)
}

return filterDown(filtered, words.slice(1)).filter(f => !allChildren.has(f.value))
return filterDown(filtered, treeWords.slice(1)).filter(f => !allChildren.has(f.value))
.sort((a, b) => b.children.length - a.children.length)
}


Loading

0 comments on commit 41268df

Please sign in to comment.