mirror of
https://github.com/dnlbauer/obsidian-dataview-autocompletion.git
synced 2026-09-10 14:15:29 +00:00
feat: better regex matching for trigger
This commit is contained in:
@@ -1,19 +1,139 @@
|
||||
import { getTriggerText } from "./trigger"
|
||||
|
||||
describe("test trigger", () => {
|
||||
test("testing trigger regex on braces", () => {
|
||||
expect(getTriggerText("test string () testing", 13)).toEqual(["", 13, 13])
|
||||
expect(getTriggerText("test string (person) testing", 16)).toEqual(["person", 13, 19])
|
||||
expect(getTriggerText("test string (person:) testing", 16)).toEqual(["person:", 13, 20])
|
||||
expect(getTriggerText("test string (person::) testing", 16)).toEqual(["person::", 13, 21])
|
||||
expect(getTriggerText("test string (person::Bob) testing", 16)).toEqual(["person::Bob", 13, 24])
|
||||
expect(getTriggerText("test string (person:: Bob) testing", 16)).toEqual(["person:: Bob", 13, 25])
|
||||
expect(getTriggerText("test string (person:: Bob) testing", 16)).toEqual(["person:: Bob", 13, 27])
|
||||
describe("trigger", () => {
|
||||
describe("empty", () => {
|
||||
test("empty parantheses", () => {
|
||||
expect(getTriggerText("() testing", 1)).toEqual(["", 1, 1])
|
||||
})
|
||||
test("empty square brackets", () => {
|
||||
expect(getTriggerText("[] testing", 1)).toEqual(["", 1, 1])
|
||||
})
|
||||
test("empty square brackets in text", () => {
|
||||
expect(getTriggerText("test [] testing", 6)).toEqual(["", 6, 6])
|
||||
})
|
||||
// test("not match empty double square brackets", () => {
|
||||
// expect(getTriggerText("[[]] testing", 2)).toEqual(null)
|
||||
// })
|
||||
test("multiple empty brackets in text", () => {
|
||||
expect(getTriggerText("test [] and [] testing", 13)).toEqual(["", 13, 13])
|
||||
})
|
||||
})
|
||||
|
||||
test("test multiple regex matches in trigger", () => {
|
||||
expect(getTriggerText("test() string () testing", 5)).toEqual(["", 5, 5])
|
||||
expect(getTriggerText("test() string () testing", 15)).toEqual(["", 15, 15])
|
||||
expect(getTriggerText("test() string () testing", 17)).toEqual(null)
|
||||
describe("text in parantheses", () => {
|
||||
test("text behind", () => {
|
||||
expect(getTriggerText("(capture) testing", 1)).toEqual(["capture", 1, 8])
|
||||
})
|
||||
test("text before", () => {
|
||||
expect(getTriggerText("testing (capture)", 12)).toEqual(["capture", 9, 16])
|
||||
})
|
||||
test("text behind no whitespace", () => {
|
||||
expect(getTriggerText("(capture)testing", 1)).toEqual(["capture", 1, 8])
|
||||
})
|
||||
test("text before no whitespace", () => {
|
||||
expect(getTriggerText("test(capture)", 5)).toEqual(["capture", 5, 12])
|
||||
})
|
||||
test("text before and behind", () => {
|
||||
expect(getTriggerText("test (capture) testing", 6)).toEqual(["capture", 6, 13])
|
||||
})
|
||||
test("text and colon", () => {
|
||||
expect(getTriggerText("(capture:) testing", 1)).toEqual(["capture:", 1, 9])
|
||||
})
|
||||
test("text and double colon", () => {
|
||||
expect(getTriggerText("(capture::) testing", 1)).toEqual(["capture::", 1, 10])
|
||||
})
|
||||
test("metadata field", () => {
|
||||
expect(getTriggerText("(capture::Bob) testing", 1)).toEqual(["capture::Bob", 1, 13])
|
||||
})
|
||||
test("metadata field with whitespace", () => {
|
||||
expect(getTriggerText("(capture:: Bob) testing", 1)).toEqual(["capture:: Bob", 1, 14])
|
||||
})
|
||||
test("metadata field with more white space", () => {
|
||||
expect(getTriggerText("(capture:: Bob) testing", 1)).toEqual(["capture:: Bob", 1, 16])
|
||||
})
|
||||
test("text with markdown link", () => {
|
||||
expect(getTriggerText("([[test]]) testing", 1)).toEqual(["[[test]]", 1, 9])
|
||||
})
|
||||
test("text with aliased markdown link", () => {
|
||||
expect(getTriggerText("([[test|display]]) testing", 1)).toEqual(["[[test|display]]", 1, 17])
|
||||
})
|
||||
test("text with wiki link", () => {
|
||||
expect(getTriggerText("([[test](https://example.com)]) testing", 1)).toEqual(["[[test](https://example.com)]", 1, 30])
|
||||
})
|
||||
test("text with wiki link local", () => {
|
||||
expect(getTriggerText("([[test](test)]) testing", 1)).toEqual(["[[test](test)]", 1, 15])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("text in square brackets", () => {
|
||||
test("text behind", () => {
|
||||
expect(getTriggerText("[capture] testing", 1)).toEqual(["capture", 1, 8])
|
||||
})
|
||||
test("text before", () => {
|
||||
expect(getTriggerText("testing [capture]", 12)).toEqual(["capture", 9, 16])
|
||||
})
|
||||
test("text behind no whitespace", () => {
|
||||
expect(getTriggerText("[capture]testing", 1)).toEqual(["capture", 1, 8])
|
||||
})
|
||||
test("text before no whitespace", () => {
|
||||
expect(getTriggerText("test[capture]", 5)).toEqual(["capture", 5, 12])
|
||||
})
|
||||
test("text before and behind", () => {
|
||||
expect(getTriggerText("test [capture] testing", 6)).toEqual(["capture", 6, 13])
|
||||
})
|
||||
test("text and colon", () => {
|
||||
expect(getTriggerText("[capture:] testing", 1)).toEqual(["capture:", 1, 9])
|
||||
})
|
||||
test("text and double colon", () => {
|
||||
expect(getTriggerText("[capture::] testing", 1)).toEqual(["capture::", 1, 10])
|
||||
})
|
||||
test("metadata field", () => {
|
||||
expect(getTriggerText("[capture::Bob] testing", 1)).toEqual(["capture::Bob", 1, 13])
|
||||
})
|
||||
test("metadata field with whitespace", () => {
|
||||
expect(getTriggerText("[capture:: Bob] testing", 1)).toEqual(["capture:: Bob", 1, 14])
|
||||
})
|
||||
test("metadata field with more white space", () => {
|
||||
expect(getTriggerText("[capture:: Bob] testing", 1)).toEqual(["capture:: Bob", 1, 16])
|
||||
})
|
||||
test("text with markdown link", () => {
|
||||
expect(getTriggerText("[[[test]]] testing", 1)).toEqual(["[[test]]", 1, 9])
|
||||
})
|
||||
test("text with aliased markdown link", () => {
|
||||
expect(getTriggerText("[[[test|display]]] testing", 1)).toEqual(["[[test|display]]", 1, 17])
|
||||
})
|
||||
test("text with wiki link", () => {
|
||||
expect(getTriggerText("[[[test](https://example.com)]] testing", 1)).toEqual(["[[test](https://example.com)]", 1, 30])
|
||||
})
|
||||
test("text with wiki link local", () => {
|
||||
expect(getTriggerText("[[[test](test)]] testing", 1)).toEqual(["[[test](test)]", 1, 15])
|
||||
})
|
||||
})
|
||||
|
||||
describe("cursor position", () => {
|
||||
test("cursor in first field", () => {
|
||||
expect(getTriggerText("test (test) string (test2) testing", 9)).toEqual(["test", 6, 10])
|
||||
})
|
||||
test("cursor in second field", () => {
|
||||
expect(getTriggerText("test (test) string (test2) testing", 21)).toEqual(["test2", 20, 25])
|
||||
})
|
||||
test("ignore cursor out of field", () => {
|
||||
expect(getTriggerText("test (test) string (test2) testing", 2)).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ignore plain markdown links", () => {
|
||||
test("cursor in link text", () => {
|
||||
expect(getTriggerText("test [test](https://example.com)", 8)).toEqual(null)
|
||||
})
|
||||
test("cursor in link url", () => {
|
||||
expect(getTriggerText("test [test](https://example.com)", 15)).toEqual(null)
|
||||
})
|
||||
test("cursor in empty link text", () => {
|
||||
expect(getTriggerText("test [](https://example.com)", 6)).toEqual(null)
|
||||
})
|
||||
test("cursor in empty link url", () => {
|
||||
expect(getTriggerText("test [test]()", 12)).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -1,17 +1,93 @@
|
||||
const triggerRegex = /\((.*?)\)/g
|
||||
/**
|
||||
* Matches single square bracket pair [] or parahtheses (), but not a double square bracket pair [[]]
|
||||
* (?<!\[) # Negative lookbehind to make sure there is no second [
|
||||
* \[] # Matches []
|
||||
* (?![\]\(]) # Negative lookahead to make sure there is no second closing ] from wiki link or opening ( from a markdown link
|
||||
* | # OR
|
||||
* (?<!\]) # Negative lookbehind to make sure there is no closing ] from a markdown link
|
||||
* \(\) # Matches ()
|
||||
*/
|
||||
const emptyRegex = /(?<!\[)\[](?![\]\(])|(?<!\])\(\)/g
|
||||
|
||||
/**
|
||||
* Maches single square brackets or parantheses with some text in them
|
||||
* and captures the text
|
||||
* If the brackets start a wiki link or markdown link, they are ignored
|
||||
* but links inside another layer of brackets are returned:
|
||||
*
|
||||
* (
|
||||
* (?<=^\(|\s\() # Pattern for parantheses
|
||||
* .+?:{0,2}.*?
|
||||
* (?=\)$|\)\s)
|
||||
* |
|
||||
* (?<=^\[|\s\[) # Pattern for square brackets
|
||||
* .+?:{0,2}.*?
|
||||
* (?=\]$|\]\s)
|
||||
* )
|
||||
*/
|
||||
const filledRegex = /((?<=^\(|\s\().+?:{0,2}.*?(?=\)$|\)\s)|(?<=^\[|\s\[).+?:{0,2}.*?(?=\]$|\]\s))/g
|
||||
|
||||
/**
|
||||
* Finds the dataview metadata field the user is currently inside.
|
||||
* It returns [text, start, end] where text is the text inside the field,
|
||||
* and start and end are the cursor positions of the start and end of the field.
|
||||
* If the user is not inside a field, it returns null.
|
||||
*/
|
||||
export function getTriggerText(line: string, cursorPos: number): [string, number, number] | null {
|
||||
triggerRegex.lastIndex = 0 // resetting global regex
|
||||
|
||||
let match = null
|
||||
while ((match = triggerRegex.exec(line)) !== null) {
|
||||
const cursorInMatch = cursorPos > match.index && cursorPos <= match.index + match[1].length + 1
|
||||
|
||||
if (cursorInMatch) {
|
||||
return [match[1], match.index+1, match.index + match[1].length + 1]
|
||||
}
|
||||
// Check for empty [] or ().
|
||||
// If the user starts typing with no text in the field, this is the fastest way to find it.
|
||||
let match = getEmptyTrigger(line, cursorPos)
|
||||
if (match) {
|
||||
return match
|
||||
}
|
||||
|
||||
// If the user types inside an existing field, this is the function to find it.
|
||||
match = getTriggerTextFromRegex(line, cursorPos, filledRegex)
|
||||
if (match !== null) {
|
||||
return match
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Matches an empty [] or empty (), but not [[]]
|
||||
* Has its own function since there is no capture group resulting in a different index calculation
|
||||
*/
|
||||
function getEmptyTrigger(line: string, cursorPos: number): [string, number, number] | null {
|
||||
let matches = Array.from(line.matchAll(emptyRegex))
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (cursorPos === match.index+1) {
|
||||
return ["", match.index+1, match.index+1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a regex with a capture group, a line of text, and the users cursor position,
|
||||
* this function finds the match of the regex in the line that the user is currently inside.
|
||||
* If the user is inside the capture group, returns [text, start, end] where text is the text inside the match,
|
||||
* start is the position of the start of the match, and end is the position of the end of the match.
|
||||
* Otherwise, returns null.
|
||||
*/
|
||||
function getTriggerTextFromRegex(line: string, cursorPos: number, regex: RegExp): [string, number, number] | null {
|
||||
let matches = Array.from(line.matchAll(regex))
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const matchStart = match!.index
|
||||
const matchEnd = matchStart + match[1].length
|
||||
const cursorInMatch = cursorPos >= matchStart && cursorPos <= matchEnd
|
||||
|
||||
if (cursorInMatch) {
|
||||
return [match[1], matchStart, matchEnd]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user