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
6 changes: 6 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1583,11 +1583,17 @@ paths:
properties:
end:
type: "number"
description: "End character position of the match within the file. Starts with 0."
start:
type: "number"
description: "Start character position of the match within the file. Starts with 0."
source:
type: "string"
description: "Where the search term matched: 'filename' or 'content'"
required:
- "start"
- "end"
- "source"
type: "object"
required:
- "match"
Expand Down
34 changes: 27 additions & 7 deletions mocks/obsidian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class DataAdapter {
_read = "";
_readBinary = new ArrayBuffer(0);
_write: [string, string];
_writeBinary : [string, ArrayBuffer];
_writeBinary: [string, ArrayBuffer];
_remove: [string];
_stat = new Stat();

Expand All @@ -42,12 +42,12 @@ class DataAdapter {
return this._readBinary;
}

async write(path: string, content: string, option?:DataWriteOptions): Promise<void> {
async write(path: string, content: string, option?: DataWriteOptions): Promise<void> {
this._write = [path, content];
}

async writeBinary(path: string, content: ArrayBuffer, option?:DataWriteOptions): Promise<void> {
this._writeBinary = [path,content]
async writeBinary(path: string, content: ArrayBuffer, option?: DataWriteOptions): Promise<void> {
this._writeBinary = [path, content]
}

async remove(path: string): Promise<void> {
Expand All @@ -72,7 +72,7 @@ export class Vault {
return this._cachedRead;
}

async createFolder(path: string): Promise<void> {}
async createFolder(path: string): Promise<void> { }

getFiles(): TFile[] {
return this._files;
Expand Down Expand Up @@ -158,14 +158,15 @@ export class FileStats {

export class TFile {
path = "somefile.md";
basename: string = "somefile";
stat: FileStats = new FileStats();
}

export class PluginManifest {
version = "";
}

export class SettingTab {}
export class SettingTab { }

export const apiVersion = "1.0.0";

Expand All @@ -177,5 +178,24 @@ export class SearchResult {
export function prepareSimpleSearch(
query: string
): (value: string) => null | SearchResult {
return null;
return (text: string) => {
const matches: [number, number][] = [];
const lowerQuery = query.toLowerCase();
const lowerText = text.toLowerCase();

let index = 0;
while ((index = lowerText.indexOf(lowerQuery, index)) !== -1) {
matches.push([index, index + query.length]);
index += query.length;
}

if (matches.length === 0) {
return null;
}

const result = new SearchResult();
result.matches = matches;
result.score = matches.length * -10;
return result;
};
}
251 changes: 251 additions & 0 deletions src/requestHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,4 +802,255 @@ describe("requestHandler", () => {
.expect(401);
});
});

describe("searchSimplePost", () => {
test("match at beginning of filename", async () => {
const testFile = new TFile();
testFile.basename = "Master Plan";
testFile.path = "Master Plan.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "Some content here";

const result = await request(server)
.post("/search/simple/?query=Master")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("Master Plan.md");
expect(result.body[0].matches).toHaveLength(1);
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(0);
expect(result.body[0].matches[0].match.end).toBe(6);
expect(result.body[0].matches[0].context).toBe("Master Plan");
});

test("match in middle of filename", async () => {
const testFile = new TFile();
testFile.basename = "1 - Master Plan";
testFile.path = "1 - Master Plan.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "Some content here";

const result = await request(server)
.post("/search/simple/?query=Master")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("1 - Master Plan.md");
expect(result.body[0].matches).toHaveLength(1);
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(4);
expect(result.body[0].matches[0].match.end).toBe(10);
expect(result.body[0].matches[0].context).toBe("1 - Master Plan");
});

test("match at end of filename", async () => {
const testFile = new TFile();
testFile.basename = "My Master Plan";
testFile.path = "My Master Plan.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "Some content here";

const result = await request(server)
.post("/search/simple/?query=Plan")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("My Master Plan.md");
expect(result.body[0].matches).toHaveLength(1);
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(10);
expect(result.body[0].matches[0].match.end).toBe(14);
expect(result.body[0].matches[0].context).toBe("My Master Plan");
});

test("match in content only", async () => {
const testFile = new TFile();
testFile.basename = "Random Note";
testFile.path = "Random Note.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "This is my master plan for the project.";

const result = await request(server)
.post("/search/simple/?query=master")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("Random Note.md");
expect(result.body[0].matches).toHaveLength(1);
expect(result.body[0].matches[0].match.source).toBe("content");
expect(result.body[0].matches[0].match.start).toBe(11);
expect(result.body[0].matches[0].match.end).toBe(17);
});

test("match in both filename and content", async () => {
const testFile = new TFile();
testFile.basename = "Master Plan";
testFile.path = "Master Plan.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "The master plan is to complete this project.";

const result = await request(server)
.post("/search/simple/?query=master")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("Master Plan.md");
expect(result.body[0].matches).toHaveLength(2);

// First match should be in filename (case-insensitive)
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(0);
expect(result.body[0].matches[0].match.end).toBe(6);
expect(result.body[0].matches[0].context).toBe("Master Plan");

// Second match should be in content
expect(result.body[0].matches[1].match.source).toBe("content");
expect(result.body[0].matches[1].match.start).toBe(4);
expect(result.body[0].matches[1].match.end).toBe(10);
});

test("multiple matches in filename", async () => {
const testFile = new TFile();
testFile.basename = "Test Test Test";
testFile.path = "Test Test Test.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "Content without the search term";

const result = await request(server)
.post("/search/simple/?query=Test")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("Test Test Test.md");
expect(result.body[0].matches).toHaveLength(3);

// All matches should be in filename
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(0);
expect(result.body[0].matches[0].match.end).toBe(4);

expect(result.body[0].matches[1].match.source).toBe("filename");
expect(result.body[0].matches[1].match.start).toBe(5);
expect(result.body[0].matches[1].match.end).toBe(9);

expect(result.body[0].matches[2].match.source).toBe("filename");
expect(result.body[0].matches[2].match.start).toBe(10);
expect(result.body[0].matches[2].match.end).toBe(14);
});

test("filename with special characters", async () => {
const testFile = new TFile();
testFile.basename = "Project (2024) - Master Plan";
testFile.path = "Project (2024) - Master Plan.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "Project details";

const result = await request(server)
.post("/search/simple/?query=2024")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].filename).toBe("Project (2024) - Master Plan.md");
expect(result.body[0].matches).toHaveLength(1);
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(9);
expect(result.body[0].matches[0].match.end).toBe(13);
expect(result.body[0].matches[0].context).toBe("Project (2024) - Master Plan");
});

test("context length for content matches", async () => {
const testFile = new TFile();
testFile.basename = "Note";
testFile.path = "Note.md";

const longContent = "A".repeat(200) + "MATCH" + "B".repeat(200);
app.vault._markdownFiles = [testFile];
app.vault._cachedRead = longContent;

const result = await request(server)
.post("/search/simple/?query=MATCH&contextLength=50")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].matches).toHaveLength(1);
expect(result.body[0].matches[0].match.source).toBe("content");

// Context should be approximately 50 chars before + match + 50 chars after
const context = result.body[0].matches[0].context;
expect(context.length).toBeLessThanOrEqual(105); // 50 + 5 + 50
expect(context).toContain("MATCH");
});

test("no matches returns empty array", async () => {
const testFile = new TFile();
testFile.basename = "Random Note";
testFile.path = "Random Note.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "Some content";

const result = await request(server)
.post("/search/simple/?query=NonExistentTerm")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(0);
});

test("case insensitive search", async () => {
const testFile = new TFile();
testFile.basename = "MASTER Plan";
testFile.path = "MASTER Plan.md";

app.vault._markdownFiles = [testFile];
app.vault._cachedRead = "master plan details";

const result = await request(server)
.post("/search/simple/?query=master")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(200);

expect(result.body).toHaveLength(1);
expect(result.body[0].matches).toHaveLength(2);

// Should match "MASTER" in filename (case-insensitive)
expect(result.body[0].matches[0].match.source).toBe("filename");
expect(result.body[0].matches[0].match.start).toBe(0);
expect(result.body[0].matches[0].match.end).toBe(6);

// Should match "master" in content
expect(result.body[0].matches[1].match.source).toBe("content");
expect(result.body[0].matches[1].match.start).toBe(0);
expect(result.body[0].matches[1].match.end).toBe(6);
});

test("unauthorized", async () => {
await request(server)
.post("/search/simple/?query=test")
.expect(401);
});

test("missing query parameter", async () => {
await request(server)
.post("/search/simple/")
.set("Authorization", `Bearer ${API_KEY}`)
.expect(400);
});
});
});
Loading