| author | |
| committer | |
| log | a01c48f86245f2a779f9af84042f3b09fbdeb91f |
| tree | e47997cc46567f2e4ac1496f0777c8df4ea28481 |
| parent | 6947f0d9d85dd92e74b13a70bd3d97a27d899506 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 66 insertions(+), 16 deletions(-)
apps/web/src/composer-logic.test.ts+25| ... | ... | @@ -246,6 +246,31 @@ describe("composerSubmissionIntentForKey", () => { |
| 246 | 246 | }); |
| 247 | 247 | |
| 248 | 248 | describe("detectComposerTrigger", () => { |
| 249 | it.each(["Use ", "Use\t", "Use\n "])("detects inline slash skills after %j", (prefix) => { | |
| 250 | const text = `${prefix}/skill:review later`; | |
| 251 | const cursor = `${prefix}/skill:review`.length; | |
| 252 | expect(detectComposerTrigger(text, cursor)).toEqual({ | |
| 253 | kind: "slash-command", | |
| 254 | query: "skill:review", | |
| 255 | rangeStart: prefix.length, | |
| 256 | rangeEnd: cursor, | |
| 257 | }); | |
| 258 | }); | |
| 259 | ||
| 260 | it.each(["Use /", "Use /review"])("detects the inline slash token in %j", (text) => { | |
| 261 | expect(detectComposerTrigger(text, text.length)).toEqual({ | |
| 262 | kind: "slash-command", | |
| 263 | query: text.slice(5), | |
| 264 | rangeStart: 4, | |
| 265 | rangeEnd: text.length, | |
| 266 | }); | |
| 267 | }); | |
| 268 | ||
| 269 | it.each(["Use/review", "https://example.com/review", "Use /review "])( | |
| 270 | "does not trigger for %j", | |
| 271 | (text) => expect(detectComposerTrigger(text, text.length)).toBeNull(), | |
| 272 | ); | |
| 273 | ||
| 249 | 274 | it("detects @path trigger at cursor", () => { |
| 250 | 275 | const text = "Please check @src/com"; |
| 251 | 276 | const trigger = detectComposerTrigger(text, text.length); |
apps/web/src/composer-logic.ts+8-16| ... | ... | @@ -258,24 +258,16 @@ export function isCollapsedCursorAdjacentToInlineToken( |
| 258 | 258 | |
| 259 | 259 | export function detectComposerTrigger(text: string, cursorInput: number): ComposerTrigger | null { |
| 260 | 260 | const cursor = clampCursor(text, cursorInput); |
| 261 | const lineStart = text.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; | |
| 262 | const linePrefix = text.slice(lineStart, cursor); | |
| 263 | ||
| 264 | if (linePrefix.startsWith("/")) { | |
| 265 | const commandMatch = /^\/(\S*)$/.exec(linePrefix); | |
| 266 | if (commandMatch) { | |
| 267 | const commandQuery = commandMatch[1] ?? ""; | |
| 268 | return { | |
| 269 | kind: "slash-command", | |
| 270 | query: commandQuery, | |
| 271 | rangeStart: lineStart, | |
| 272 | rangeEnd: cursor, | |
| 273 | }; | |
| 274 | } | |
| 275 | } | |
| 276 | ||
| 277 | 261 | const tokenStart = tokenStartForCursor(text, cursor); |
| 278 | 262 | const token = text.slice(tokenStart, cursor); |
| 263 | if (token.startsWith("/")) { | |
| 264 | return { | |
| 265 | kind: "slash-command", | |
| 266 | query: token.slice(1), | |
| 267 | rangeStart: tokenStart, | |
| 268 | rangeEnd: cursor, | |
| 269 | }; | |
| 270 | } | |
| 279 | 271 | const pullRequestMatch = /^#([\p{L}\p{N}][\p{L}\p{N}_-]*)?$/u.exec(token); |
| 280 | 272 | if (pullRequestMatch) { |
| 281 | 273 | return { |
packages/shared/src/composerTrigger.test.ts+25| ... | ... | @@ -3,6 +3,31 @@ import { describe, expect, it } from "vite-plus/test"; |
| 3 | 3 | import { detectComposerTrigger, serializeComposerFileLink } from "./composerTrigger.ts"; |
| 4 | 4 | |
| 5 | 5 | describe("detectComposerTrigger", () => { |
| 6 | it.each(["Use ", "Use\t", "Use\n "])("detects inline slash skills after %j", (prefix) => { | |
| 7 | const text = `${prefix}/skill:review later`; | |
| 8 | const cursor = `${prefix}/skill:review`.length; | |
| 9 | expect(detectComposerTrigger(text, cursor)).toEqual({ | |
| 10 | kind: "slash-command", | |
| 11 | query: "skill:review", | |
| 12 | rangeStart: prefix.length, | |
| 13 | rangeEnd: cursor, | |
| 14 | }); | |
| 15 | }); | |
| 16 | ||
| 17 | it.each(["Use /", "Use /review"])("detects the inline slash token in %j", (text) => { | |
| 18 | expect(detectComposerTrigger(text, text.length)).toEqual({ | |
| 19 | kind: "slash-command", | |
| 20 | query: text.slice(5), | |
| 21 | rangeStart: 4, | |
| 22 | rangeEnd: text.length, | |
| 23 | }); | |
| 24 | }); | |
| 25 | ||
| 26 | it.each(["Use/review", "https://example.com/review", "Use /review "])( | |
| 27 | "does not trigger for %j", | |
| 28 | (text) => expect(detectComposerTrigger(text, text.length)).toBeNull(), | |
| 29 | ); | |
| 30 | ||
| 6 | 31 | it.each(["$", "€", "£", "¥", "₹", "₩", "₿", "𑿝"])( |
| 7 | 32 | "detects %s skill prefixes and their source range", |
| 8 | 33 | (prefix) => { |
packages/shared/src/composerTrigger.ts+8| ... | ... | @@ -100,6 +100,14 @@ export function detectComposerTrigger( |
| 100 | 100 | const tokenStart = tokenIdx + 1; |
| 101 | 101 | |
| 102 | 102 | const token = text.slice(tokenStart, cursor); |
| 103 | if (token.startsWith("/")) { | |
| 104 | return { | |
| 105 | kind: "slash-command", | |
| 106 | query: token.slice(1), | |
| 107 | rangeStart: tokenStart, | |
| 108 | rangeEnd: cursor, | |
| 109 | }; | |
| 110 | } | |
| 103 | 111 | const pullRequestMatch = /^#([\p{L}\p{N}][\p{L}\p{N}_-]*)?$/u.exec(token); |
| 104 | 112 | if (pullRequestMatch) |
| 105 | 113 | return { |