grammar.ts 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. /**
  2. * Browser-safe `@file` token grammar shared by terminal and web clients.
  3. *
  4. * @module @deepseek-ai/dsh-file-reference/grammar
  5. */
  6. import type { FileReferenceCandidate } from './types.ts'
  7. /** Active `@` token ending at the editor cursor. */
  8. export interface ActiveAtToken {
  9. /** Complete token replaced when the user accepts a completion. */
  10. prefix: string
  11. /** Path query after `@` or `@"`. */
  12. query: string
  13. /** Whether the user opened a quoted path. */
  14. quoted: boolean
  15. }
  16. /**
  17. * Extract an `@path` or `@"path with spaces` token at the cursor. An `@`
  18. * inside another token, such as an email address, is not a completion trigger.
  19. * @param line - current editor line.
  20. * @param cursorCol - cursor column within that line.
  21. * @returns the active token, or `undefined` outside an `@` token.
  22. */
  23. export function activeAtToken(line: string, cursorCol: number): ActiveAtToken | undefined {
  24. const beforeCursor = line.slice(0, cursorCol)
  25. const quoted = /(?:^|\s)(@"([^"]*))$/u.exec(beforeCursor)
  26. if (quoted?.[1] !== undefined && quoted[2] !== undefined) {
  27. return { prefix: quoted[1], query: quoted[2], quoted: true }
  28. }
  29. const plain = /(?:^|\s)(@([^\s]*))$/u.exec(beforeCursor)
  30. if (plain?.[1] === undefined || plain[2] === undefined) return undefined
  31. return { prefix: plain[1], query: plain[2], quoted: false }
  32. }
  33. /**
  34. * Format a selected path as prompt text. Whitespace uses the quoted
  35. * `@"path"` grammar; a quoted directory keeps that quote open after its
  36. * trailing slash so completion can descend another level.
  37. * @param candidate - selected file or directory.
  38. * @param preserveQuote - retain an explicitly opened quote even when unnecessary.
  39. * @returns the insertion value, or `undefined` for a path the editor grammar cannot represent safely.
  40. */
  41. export function formatFileMention(
  42. candidate: FileReferenceCandidate,
  43. preserveQuote: boolean,
  44. ): string | undefined {
  45. const path = candidate.kind === 'directory' ? `${candidate.path}/` : candidate.path
  46. if (/[\u0000-\u001f\u007f-\u009f"]/u.test(path)) return undefined
  47. const quoted = preserveQuote || /\s/u.test(path)
  48. if (!quoted) return `@${path}`
  49. if (candidate.kind === 'directory') return `@"${path}`
  50. return `@"${path}"`
  51. }