[
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  parser: '@typescript-eslint/parser',\n  plugins: ['@typescript-eslint'],\n  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],\n  rules: {\n    semi: [2, 'always'],\n    '@typescript-eslint/no-unused-vars': 2,\n    '@typescript-eslint/no-explicit-any': 2,\n    '@typescript-eslint/explicit-module-boundary-types': 0,\n    '@typescript-eslint/no-non-null-assertion': 0,\n    'prefer-const': 0,\n  },\n};\n"
  },
  {
    "path": ".gitignore",
    "content": "out\nnode_modules\n*.vsix\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "// A launch configuration that compiles the extension and then opens it inside a new window\n// Use IntelliSense to learn about possible attributes.\n// Hover to view descriptions of existing attributes.\n// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n{\n\t\"version\": \"0.2.0\",\n\t\"configurations\": [\n\t\t{\n\t\t\t\"name\": \"Run Extension\",\n\t\t\t\"type\": \"extensionHost\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"runtimeExecutable\": \"${execPath}\",\n\t\t\t\"args\": [\n\t\t\t\t\"--extensionDevelopmentPath=${workspaceFolder}\"\n\t\t\t],\n\t\t\t\"outFiles\": [\n\t\t\t\t\"${workspaceFolder}/out/**/*.js\"\n\t\t\t],\n\t\t\t\"preLaunchTask\": \"npm: watch\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\t\"spellright.language\": [\n\t\t\"en\"\n\t],\n\t\"spellright.documentTypes\": [\n\t\t\"markdown\",\n\t\t\"latex\"\n\t]\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "// See https://go.microsoft.com/fwlink/?LinkId=733558\n// for the documentation about the tasks.json format\n{\n\t\"version\": \"2.0.0\",\n\t\"tasks\": [\n\t\t{\n\t\t\t\"type\": \"npm\",\n\t\t\t\"script\": \"watch\",\n\t\t\t\"problemMatcher\": \"$tsc-watch\",\n\t\t\t\"isBackground\": true,\n\t\t\t\"presentation\": {\n\t\t\t\t\"reveal\": \"never\"\n\t\t\t},\n\t\t\t\"group\": {\n\t\t\t\t\"kind\": \"build\",\n\t\t\t\t\"isDefault\": true\n\t\t\t}\n\t\t}\n\t]\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Ian Ornelas\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# HyperSnips\n\n![](./images/welcome.gif)\n\nHyperSnips is a snippet engine for vscode heavily inspired by vim's\n[UltiSnips](https://github.com/SirVer/ultisnips).\n\n## Usage\n\nTo use HyperSnips you create `.hsnips` files on a directory which depends on your platform:\n\n- Windows: `%APPDATA%\\Code\\User\\globalStorage\\draivin.hsnips\\hsnips\\(language).hsnips`\n- Mac: `$HOME/Library/Application Support/Code/User/globalStorage/draivin.hsnips/hsnips/(language).hsnips`\n- Linux: `$HOME/.config/Code/User/globalStorage/draivin.hsnips/hsnips/(language).hsnips`\n\nYou can open this directory by running the command `HyperSnips: Open snippets directory`.\nThis directory may be customized by changing the setting `hsnips.hsnipsPath`.\nIf this setting starts with `~` or `${workspaceFolder}`, then it will be replaced with\nyour home directory or the current workspace folder, respectively.\n\nThe file should be named based on the language the snippets are meant for (e.g. `latex.hsnips`\nfor snippets which will be available for LaTeX files).\nAdditionally, you can create an `all.hsnips` file for snippets that should be available on all languages.\n\n### Snippets file\n\nA snippets file is a file with the `.hsnips` extension, the file is composed of two types of blocks:\nglobal blocks and snippet blocks.\n\nGlobal blocks are JavaScript code blocks with code that is shared between all the snippets defined\nin the current file. They are defined with the `global` keyword, as follows:\n\n```lua\nglobal\n// JavaScript code\nendglobal\n```\n\nSnippet blocks are snippet definitions. They are defined with the `snippet` keyword, as follows:\n\n```lua\ncontext expression\nsnippet trigger \"description\" flags\nbody\nendsnippet\n```\n\nwhere the `trigger` field is required and the fields `description` and `flags` are optional.\n\n### Trigger\n\nA trigger can be any sequence of characters which does not contain a space, or a regular expression\nsurrounded by backticks (`` ` ``).\n\n### Flags\n\nThe flags field is a sequence of characters which modify the behavior of the snippet, the available\nflags are the following:\n\n- `A`: Automatic snippet expansion - Usually snippets are activated when the `tab` key is pressed,\n  with the `A` flag snippets will activate as soon as their trigger matches, it is specially useful\n  for regex snippets.\n\n- `i`: In-word expansion\\* - By default, a snippet trigger will only match when the trigger is\n  preceded by whitespace characters. A snippet with this option is triggered regardless of the\n  preceding character, for example, a snippet can be triggered in the middle of a word.\n\n- `w`: Word boundary\\* - With this option the snippet trigger will match when the trigger is a word\n  boundary character. Use this option, for example, to permit expansion where the trigger follows\n  punctuation without expanding suffixes of larger words.\n\n- `b`: Beginning of line expansion\\* - A snippet with this option is expanded only if the\n  tab trigger is the first word on the line. In other words, if only whitespace precedes the tab\n  trigger, expand.\n\n- `M`: Multi-line mode - By default, regex matches will only match content on the current line, when\n  this option is enabled the last `hsnips.multiLineContext` lines will be available for matching.\n\n\\*: This flag will only affect snippets which have non-regex triggers.\n\n### Snippet body\n\nThe body is the text that will replace the trigger when the snippet is expanded, as in usual\nsnippets, the tab stops `$1`, `$2`, etc. are available.\n\nThe full power of HyperSnips comes when using JavaScript interpolation: you can have code blocks\ninside your snippet delimited by two backticks (` `` `) that will run when the snippet is expanded,\nand every time the text in one of the tab stops is changed.\n\n### Code interpolation\n\nInside the code interpolation, you have access to a few special variables:\n\n- `rv`: The return value of your code block, the value of this variable will replace the code block\n  when the snippet is expanded.\n- `t`: An array containing the text within the tab stops, in the same order as the tab stops are\n  defined in the snippet block. You can use it to dynamically change the snippet content.\n- `m`: An array containing the match groups of your regular expression trigger, or an empty array if\n  the trigger is not a regular expression.\n- `w`: A URI string of the currently opened workspace, or an empty string if no workspace is open.\n- `path`: A URI string of the current document. (untitled documents have the scheme `untitled`)\n\nAdditionally, every variable defined in one code block will be available in all the subsequent code\nblocks in the snippet.\n\nThe `require` function can also be used to import NodeJS modules.\n\n### Context matching\n\nOptionally, you can have a `context` line before the snippet block, it is followed by any javascript\nexpression, and the snippet is only available if the `context` expression evaluates to `true`.\n\nInside the `context` expression you can use the `context` variable, which has the following type:\n\n```ts\ninterface Context {\n  scopes: string[];\n}\n```\nHere, `scopes` stands for the TextMate scopes at the current cursor position, which can be viewed by\nrunning the `Developer: Inspect Editor Tokens and Scopes` command in `vscode`.\n\nAs an example, here is an automatic LaTeX snippet that only expands when inside a math block:\n\n```lua\nglobal\nfunction math(context) {\n    return context.scopes.some(s => s.startsWith(\"meta.math\"));\n}\nendglobal\n\ncontext math(context)\nsnippet inv \"inverse\" Ai\n^{-1}\nendsnippet\n```\n\n## Examples\n\n- Simple snippet which greets you with the current date and time\n\n```lua\nsnippet dategreeting \"Gives you the current date!\"\nHello from your hsnip at ``rv = new Date().toDateString()``!\nendsnippet\n```\n\n- Box snippet as shown in the gif above\n\n```lua\nsnippet box \"Box\" A\n``rv = '┌' + '─'.repeat(t[0].length + 2) + '┐'``\n│ $1 │\n``rv = '└' + '─'.repeat(t[0].length + 2) + '┘'``\nendsnippet\n```\n\n- Snippet to insert the current filename\n\n```lua\nsnippet filename \"Current Filename\"\n``rv = require('path').basename(path)``\nendsnippet\n```\n"
  },
  {
    "path": "language-configuration.json",
    "content": "{\n  \"comments\": {\n    \"lineComment\": \"#\"\n  },\n  \"folding\": {\n    \"markers\": {\n      \"start\": \"^snippet\\\\b\",\n      \"end\": \"^endsnippet\\\\b\"\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"hsnips\",\n    \"displayName\": \"HyperSnips\",\n    \"icon\": \"images/hypersnips.png\",\n    \"version\": \"0.2.9\",\n    \"publisher\": \"draivin\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/draivin/hsnips\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/draivin/hsnips/issues\"\n    },\n    \"engines\": {\n        \"vscode\": \"^1.52.0\"\n    },\n    \"categories\": [\n        \"Snippets\",\n        \"Other\"\n    ],\n    \"keywords\": [\n        \"ultisnips\",\n        \"programmable snippets\",\n        \"dynamic snippets\",\n        \"snippets\"\n    ],\n    \"preview\": true,\n    \"activationEvents\": [\n        \"*\"\n    ],\n    \"contributes\": {\n        \"configuration\": [\n            {\n                \"title\": \"HyperSnips\",\n                \"properties\": {\n                    \"hsnips.multiLineContext\": {\n                        \"type\": \"number\",\n                        \"default\": 20,\n                        \"description\": \"Number of lines matched when using multi-line regex mode.\"\n                    },\n                    \"hsnips.hsnipsPath\": {\n                        \"type\": [\n                            \"string\",\n                            \"null\"\n                        ],\n                        \"default\": null,\n                        \"description\": \"Absolute path or relative path from the workspace folder to the folder containing the hsnips files.\"\n                    }\n                }\n            }\n        ],\n        \"commands\": [\n            {\n                \"category\": \"HyperSnips\",\n                \"command\": \"hsnips.openSnippetsDir\",\n                \"title\": \"Open Snippets Directory\"\n            },\n            {\n                \"category\": \"HyperSnips\",\n                \"command\": \"hsnips.openSnippetFile\",\n                \"title\": \"Open Snippet File\"\n            },\n            {\n                \"category\": \"HyperSnips\",\n                \"command\": \"hsnips.reloadSnippets\",\n                \"title\": \"Reload Snippets\"\n            }\n        ],\n        \"keybindings\": [\n            {\n                \"key\": \"tab\",\n                \"command\": \"hsnips.nextPlaceholder\",\n                \"when\": \"editorTextFocus && hasNextTabstop && inSnippetMode && !suggestWidgetVisible\"\n            },\n            {\n                \"key\": \"shift+tab\",\n                \"command\": \"hsnips.prevPlaceholder\",\n                \"when\": \"editorTextFocus && hasPrevTabstop && inSnippetMode && !suggestWidgetVisible\"\n            },\n            {\n                \"key\": \"escape\",\n                \"command\": \"hsnips.leaveSnippet\",\n                \"when\": \"editorTextFocus && inSnippetMode && !suggestWidgetVisible\"\n            }\n        ],\n        \"languages\": [\n            {\n                \"id\": \"hsnips\",\n                \"extensions\": [\n                    \".hsnips\"\n                ],\n                \"aliases\": [\n                    \"HyperSnips\"\n                ],\n                \"configuration\": \"./language-configuration.json\"\n            }\n        ],\n        \"grammars\": [\n            {\n                \"language\": \"hsnips\",\n                \"scopeName\": \"source.hsnips\",\n                \"path\": \"./syntaxes/hsnips.tmLanguage.json\",\n                \"embeddedLanguages\": {\n                    \"meta.embedded.js\": \"javascript\"\n                }\n            }\n        ]\n    },\n    \"main\": \"./out/extension.js\",\n    \"scripts\": {\n        \"vscode:prepublish\": \"npm run compile\",\n        \"compile\": \"tsc -p ./\",\n        \"lint\": \"eslint . --ext .ts,.tsx\",\n        \"watch\": \"tsc -watch -p ./\"\n    },\n    \"devDependencies\": {\n        \"@types/node\": \"^15.3.0\",\n        \"@types/vscode\": \"^1.52.0\",\n        \"@typescript-eslint/eslint-plugin\": \"^4.23.0\",\n        \"@typescript-eslint/parser\": \"^4.23.0\",\n        \"eslint\": \"^7.17.0\",\n        \"typescript\": \"^4.2.4\"\n    },\n    \"dependencies\": {\n        \"open-file-explorer\": \"^1.0.2\"\n    },\n    \"extensionDependencies\": [\n        \"draivin.hscopes\"\n    ]\n}\n"
  },
  {
    "path": "src/completion.ts",
    "content": "import * as vscode from 'vscode';\nimport { lineRange } from './utils';\nimport { HSnippet } from './hsnippet';\n\nexport class CompletionInfo {\n  range: vscode.Range;\n  completionRange: vscode.Range;\n  snippet: HSnippet;\n  label: string;\n  groups: string[];\n\n  constructor(snippet: HSnippet, label: string, range: vscode.Range, groups: string[]) {\n    this.snippet = snippet;\n    this.label = label;\n    this.range = range;\n    this.completionRange = new vscode.Range(range.start, range.start.translate(0, label.length));\n    this.groups = groups;\n  }\n\n  toCompletionItem() {\n    let completionItem = new vscode.CompletionItem(this.label);\n    completionItem.range = this.range;\n    completionItem.detail = this.snippet.description;\n    completionItem.insertText = this.label;\n    completionItem.command = {\n      command: 'hsnips.expand',\n      title: 'expand',\n      arguments: [this],\n    };\n\n    return completionItem;\n  }\n}\n\nfunction matchSuffixPrefix(context: string, trigger: string) {\n  while (trigger.length) {\n    if (context.endsWith(trigger)) return trigger;\n    trigger = trigger.substring(0, trigger.length - 1);\n  }\n\n  return null;\n}\n\nexport function getCompletions(\n  document: vscode.TextDocument,\n  position: vscode.Position,\n  snippets: HSnippet[]\n): CompletionInfo[] | CompletionInfo | undefined {\n  let line = document.getText(lineRange(0, position));\n\n  // Grab everything until previous whitespace as our matching context.\n  let match = line.match(/\\S*$/);\n  let contextRange = lineRange((match as RegExpMatchArray).index || 0, position);\n  let context = document.getText(contextRange);\n  let precedingContextRange = new vscode.Range(\n    position.line,\n    0,\n    position.line,\n    (match as RegExpMatchArray).index || 0\n  );\n  let precedingContext = document.getText(precedingContextRange);\n  let isPrecedingContextWhitespace = precedingContext.match(/^\\s*$/) != null;\n\n  let wordRange = document.getWordRangeAtPosition(position) || contextRange;\n  if (wordRange.end != position) {\n    wordRange = new vscode.Range(wordRange.start, position);\n  }\n  let wordContext = document.getText(wordRange);\n\n  let longContext = null;\n\n  let completions = [];\n  let snippetContext = { scopes: [] };\n\n  //FIXME: Plain text scope resolution should be fixed in hscopes.\n  if (document.languageId !== 'plaintext') {\n    snippetContext = {\n      scopes: vscode.extensions\n        .getExtension('draivin.hscopes')!\n        .exports.getScopeAt(document, position).scopes,\n    };\n  }\n\n  for (let snippet of snippets) {\n    if (snippet.contextFilter && !snippet.contextFilter(snippetContext)) {\n      continue;\n    }\n\n    let snippetMatches = false;\n    let snippetRange = contextRange;\n    let prefixMatches = false;\n\n    let matchGroups: string[] = [];\n    let label = snippet.trigger;\n\n    if (snippet.trigger) {\n      let matchingPrefix = null;\n\n      if (snippet.inword) {\n        snippetMatches = context.endsWith(snippet.trigger);\n        matchingPrefix = snippetMatches\n          ? snippet.trigger\n          : matchSuffixPrefix(context, snippet.trigger);\n      } else if (snippet.wordboundary) {\n        snippetMatches = wordContext == snippet.trigger;\n        matchingPrefix = snippet.trigger.startsWith(wordContext) ? wordContext : null;\n      } else if (snippet.beginningofline) {\n        snippetMatches = context.endsWith(snippet.trigger) && isPrecedingContextWhitespace;\n        matchingPrefix =\n          snippet.trigger.startsWith(context) && isPrecedingContextWhitespace ? context : null;\n      } else {\n        snippetMatches = context == snippet.trigger;\n        matchingPrefix = snippet.trigger.startsWith(context) ? context : null;\n      }\n\n      if (matchingPrefix) {\n        snippetRange = new vscode.Range(position.translate(0, -matchingPrefix.length), position);\n        prefixMatches = true;\n      }\n    } else if (snippet.regexp) {\n      let regexContext = line;\n\n      if (snippet.multiline) {\n        if (!longContext) {\n          let numberPrevLines = vscode.workspace\n            .getConfiguration('hsnips')\n            .get('multiLineContext') as number;\n\n          longContext = document\n            .getText(\n              new vscode.Range(\n                new vscode.Position(Math.max(position.line - numberPrevLines, 0), 0),\n                position\n              )\n            )\n            .replace(/\\r/g, '');\n        }\n\n        regexContext = longContext;\n      }\n\n      let match = snippet.regexp.exec(regexContext);\n      if (match) {\n        let charOffset = match.index - regexContext.lastIndexOf('\\n', match.index) - 1;\n        let lineOffset = match[0].split('\\n').length - 1;\n\n        snippetRange = new vscode.Range(\n          new vscode.Position(position.line - lineOffset, charOffset),\n          position\n        );\n        snippetMatches = true;\n        prefixMatches = true;\n        matchGroups = Array.from(match);\n        label = match[0];\n      }\n    }\n\n    let completion = new CompletionInfo(snippet, label, snippetRange, matchGroups);\n    if (snippet.automatic && snippetMatches) {\n      return completion;\n    } else if (prefixMatches) {\n      completions.push(completion);\n    }\n  }\n\n  return completions;\n}\n"
  },
  {
    "path": "src/consts.ts",
    "content": "export const COMPLETIONS_TRIGGERS = [\n  ' ',\n  '.',\n  '(',\n  ')',\n  '{',\n  '}',\n  '[',\n  ']',\n  ',',\n  ':',\n  \"'\",\n  '\"',\n  '=',\n  '<',\n  '>',\n  '/',\n  '\\\\',\n  '+',\n  '-',\n  '|',\n  '&',\n  '*',\n  '%',\n  '=',\n  '$',\n  '#',\n  '@',\n  '!',\n];\n"
  },
  {
    "path": "src/dynamicRange.ts",
    "content": "import * as vscode from 'vscode';\n\ntype PositionDelta = { characterDelta: number; lineDelta: number };\n\nexport enum GrowthType {\n  Grow,\n  FixLeft,\n  FixRight,\n}\n\nexport interface IChangeInfo {\n  change: vscode.TextDocumentContentChangeEvent;\n  growth: GrowthType;\n}\n\nfunction getRangeDelta(\n  range: vscode.Range,\n  change: vscode.TextDocumentContentChangeEvent,\n  growth: GrowthType\n): [PositionDelta, PositionDelta] {\n  let deltaStart = { characterDelta: 0, lineDelta: 0 };\n  let deltaEnd = { characterDelta: 0, lineDelta: 0 };\n\n  let textLines = change.text.split('\\n');\n  let lineDelta =\n    change.text.split('\\n').length - (change.range.end.line - change.range.start.line + 1);\n  let charDelta = textLines[textLines.length - 1].length - change.range.end.character;\n  if (lineDelta == 0) charDelta += change.range.start.character;\n\n  if (range.start.isAfterOrEqual(change.range.end)) {\n    deltaStart.lineDelta = lineDelta;\n  }\n\n  if (range.end.isAfterOrEqual(change.range.end)) {\n    deltaEnd.lineDelta = lineDelta;\n  }\n\n  if (change.range.end.line == range.start.line)\n    if (\n      (growth == GrowthType.FixRight && range.start.isEqual(change.range.end)) ||\n      range.start.isAfter(change.range.end)\n    ) {\n      deltaStart.characterDelta = charDelta;\n    }\n\n  if (change.range.end.line == range.end.line)\n    if (\n      (growth != GrowthType.FixLeft && range.end.isEqual(change.range.end)) ||\n      range.end.isAfter(change.range.end)\n    ) {\n      deltaEnd.characterDelta = charDelta;\n    }\n\n  return [deltaStart, deltaEnd];\n}\n\nexport class DynamicRange {\n  range: vscode.Range;\n\n  constructor(start: vscode.Position, end: vscode.Position) {\n    this.range = new vscode.Range(start, end);\n  }\n\n  static fromRange(range: vscode.Range) {\n    return new DynamicRange(range.start, range.end);\n  }\n\n  update(changes: IChangeInfo[]) {\n    let deltaStart = { characterDelta: 0, lineDelta: 0 };\n    let deltaEnd = { characterDelta: 0, lineDelta: 0 };\n\n    for (let { change, growth } of changes) {\n      let deltaChange = getRangeDelta(this.range, change, growth);\n\n      deltaStart.characterDelta += deltaChange[0].characterDelta;\n      deltaStart.lineDelta += deltaChange[0].lineDelta;\n      deltaEnd.characterDelta += deltaChange[1].characterDelta;\n      deltaEnd.lineDelta += deltaChange[1].lineDelta;\n    }\n\n    let [newStart, newEnd] = [this.range.start, this.range.end];\n    newStart = newStart.translate(deltaStart);\n    newEnd = newEnd.translate(deltaEnd);\n    this.range = this.range.with(newStart, newEnd);\n  }\n\n  contains(range: vscode.Range): boolean {\n    return this.range.contains(range);\n  }\n}\n"
  },
  {
    "path": "src/extension.ts",
    "content": "import * as vscode from 'vscode';\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from 'fs';\nimport * as path from 'path';\nimport openExplorer = require('open-file-explorer');\nimport { HSnippet } from './hsnippet';\nimport { HSnippetInstance } from './hsnippetInstance';\nimport { parse } from './parser';\nimport { getOldGlobalSnippetDir, getSnippetDirInfo, SnippetDirType } from './utils';\nimport { getCompletions, CompletionInfo } from './completion';\nimport { COMPLETIONS_TRIGGERS } from './consts';\n\nconst SNIPPETS_BY_LANGUAGE: Map<string, HSnippet[]> = new Map();\nconst SNIPPET_STACK: HSnippetInstance[] = [];\n\nlet insertingSnippet = false;\n\nasync function loadSnippets(context: vscode.ExtensionContext) {\n  SNIPPETS_BY_LANGUAGE.clear();\n\n  const snippetDirInfo = getSnippetDirInfo(context);\n  if (snippetDirInfo === null) {\n    return;\n  }\n\n  const snippetDirPath = snippetDirInfo.path;\n\n  if (!existsSync(snippetDirPath)) {\n    mkdirSync(snippetDirPath, { recursive: true });\n  }\n\n  for (let file of readdirSync(snippetDirPath)) {\n    if (path.extname(file).toLowerCase() != '.hsnips') continue;\n\n    let filePath = path.join(snippetDirPath, file);\n    let fileData = readFileSync(filePath, 'utf8');\n\n    let language = path.basename(file, '.hsnips').toLowerCase();\n\n    SNIPPETS_BY_LANGUAGE.set(language, parse(fileData));\n  }\n\n  let globalSnippets = SNIPPETS_BY_LANGUAGE.get('all');\n  if (globalSnippets) {\n    for (let [language, snippetList] of SNIPPETS_BY_LANGUAGE.entries()) {\n      if (language != 'all') snippetList.push(...globalSnippets);\n    }\n  }\n\n  // Sort snippets by descending priority.\n  for (let snippetList of SNIPPETS_BY_LANGUAGE.values()) {\n    snippetList.sort((a, b) => b.priority - a.priority);\n  }\n}\n\n// This function may be called after a snippet expansion, in which case the original text was\n// replaced by the snippet label, or it may be called directly, as in the case of an automatic\n// expansion. Depending on which case it is, we have to delete a different editor range before\n// triggering the real hsnip expansion.\nexport async function expandSnippet(\n  completion: CompletionInfo,\n  editor: vscode.TextEditor,\n  snippetExpansion = false\n) {\n  let snippetInstance = new HSnippetInstance(\n    completion.snippet,\n    editor,\n    completion.range.start,\n    completion.groups\n  );\n\n  let insertionRange: vscode.Range | vscode.Position = completion.range.start;\n\n  // The separate deletion is a workaround for a VsCodeVim bug, where when we trigger a snippet which\n  // has a replacement range, it will go into NORMAL mode, see issues #28 and #36.\n\n  // TODO: Go back to inserting the snippet and removing in a single command once the VsCodeVim bug\n  // is fixed.\n\n  insertingSnippet = true;\n  await editor.edit(\n    (eb) => {\n      eb.delete(snippetExpansion ? completion.completionRange : completion.range);\n    },\n    { undoStopAfter: false, undoStopBefore: !snippetExpansion }\n  );\n\n  await editor.insertSnippet(snippetInstance.snippetString, insertionRange, {\n    undoStopAfter: false,\n    undoStopBefore: false,\n  });\n\n  if (snippetInstance.selectedPlaceholder != 0) SNIPPET_STACK.unshift(snippetInstance);\n  insertingSnippet = false;\n}\n\nexport function activate(context: vscode.ExtensionContext) {\n  vscode.extensions.getExtension('draivin.hscopes')?.activate();\n\n  // migrating from the old, hardcoded directory to the new one. TODO: remove this at some point\n  const oldGlobalSnippetDir = getOldGlobalSnippetDir();\n  if (existsSync(oldGlobalSnippetDir)) {\n    // only the global directory needs to be migrated, which is why `ignoreWorkspace` is set to `true` here\n    const newSnippetDirInfo = getSnippetDirInfo(context, { ignoreWorkspace: true });\n\n    if (newSnippetDirInfo.type == SnippetDirType.Global) {\n      mkdirSync(path.dirname(newSnippetDirInfo.path), { recursive: true });\n      renameSync(oldGlobalSnippetDir, newSnippetDirInfo.path);\n    }\n  }\n\n  loadSnippets(context);\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand('hsnips.openSnippetsDir', () =>\n      openExplorer(getSnippetDirInfo(context).path)\n    )\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand('hsnips.openSnippetFile', async () => {\n      let snippetDirPath = getSnippetDirInfo(context).path;\n      let files = readdirSync(snippetDirPath);\n      let selectedFile = await vscode.window.showQuickPick(files);\n\n      if (selectedFile) {\n        let document = await vscode.workspace.openTextDocument(\n          path.join(snippetDirPath, selectedFile)\n        );\n        vscode.window.showTextDocument(document);\n      }\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand('hsnips.reloadSnippets', () => loadSnippets(context))\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand('hsnips.leaveSnippet', () => {\n      while (SNIPPET_STACK.length) SNIPPET_STACK.pop();\n      vscode.commands.executeCommand('leaveSnippet');\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand('hsnips.nextPlaceholder', () => {\n      if (SNIPPET_STACK[0] && !SNIPPET_STACK[0].nextPlaceholder()) {\n        SNIPPET_STACK.shift();\n      }\n      vscode.commands.executeCommand('jumpToNextSnippetPlaceholder');\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand('hsnips.prevPlaceholder', () => {\n      if (SNIPPET_STACK[0] && !SNIPPET_STACK[0].prevPlaceholder()) {\n        SNIPPET_STACK.shift();\n      }\n      vscode.commands.executeCommand('jumpToPrevSnippetPlaceholder');\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.workspace.onDidSaveTextDocument((document) => {\n      if (document.languageId == 'hsnips') {\n        loadSnippets(context);\n      }\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerTextEditorCommand(\n      'hsnips.expand',\n      (editor, _, completion: CompletionInfo) => {\n        expandSnippet(completion, editor, true);\n      }\n    )\n  );\n\n  // Forward all document changes so that the active snippet can update its related blocks.\n  context.subscriptions.push(\n    vscode.workspace.onDidChangeTextDocument((e) => {\n      if (SNIPPET_STACK.length && SNIPPET_STACK[0].editor.document == e.document) {\n        SNIPPET_STACK[0].update(e.contentChanges);\n      }\n\n      if (insertingSnippet) return;\n\n      if (e.contentChanges.length === 0) return;\n\n      let mainChange = e.contentChanges[0];\n\n      if (!mainChange) return;\n\n      // Let's try to detect only events that come from keystrokes.\n      if (mainChange.text.length != 1) return;\n\n      let snippets = SNIPPETS_BY_LANGUAGE.get(e.document.languageId.toLowerCase());\n      if (!snippets) snippets = SNIPPETS_BY_LANGUAGE.get('all');\n      if (!snippets) return;\n\n      let mainChangePosition = mainChange.range.start.translate(0, mainChange.text.length);\n      let completions = getCompletions(e.document, mainChangePosition, snippets);\n\n      // When an automatic completion is matched it is returned as an element, we check for this by\n      // using !isArray, and then expand the snippet.\n      if (completions && !Array.isArray(completions)) {\n        let editor = vscode.window.activeTextEditor;\n        if (editor && e.document == editor.document) {\n          expandSnippet(completions, editor);\n          return;\n        }\n      }\n    })\n  );\n\n  // Remove any stale snippet instances.\n  context.subscriptions.push(\n    vscode.window.onDidChangeVisibleTextEditors(() => {\n      while (SNIPPET_STACK.length) SNIPPET_STACK.pop();\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.window.onDidChangeTextEditorSelection((e) => {\n      while (SNIPPET_STACK.length) {\n        if (e.selections.some((s) => SNIPPET_STACK[0].range.contains(s))) {\n          break;\n        }\n        SNIPPET_STACK.shift();\n      }\n    })\n  );\n\n  context.subscriptions.push(\n    vscode.languages.registerCompletionItemProvider(\n      [{ pattern: '**' }],\n      {\n        provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {\n          let snippets = SNIPPETS_BY_LANGUAGE.get(document.languageId.toLowerCase());\n          if (!snippets) snippets = SNIPPETS_BY_LANGUAGE.get('all');\n          if (!snippets) return;\n\n          // When getCompletions returns an array it means no auto-expansion was matched for the\n          // current context, in this case show the snippet list to the user.\n          let completions = getCompletions(document, position, snippets);\n          if (completions && Array.isArray(completions)) {\n            return completions.map((c) => c.toCompletionItem());\n          }\n        },\n      },\n      ...COMPLETIONS_TRIGGERS\n    )\n  );\n}\n"
  },
  {
    "path": "src/hsnippet.ts",
    "content": "import { HSnippetUtils } from './hsnippetUtils';\n\nexport type GeneratorResult = [(string | { block: number })[], string[]];\nexport type GeneratorFunction = (\n  texts: string[],\n  matchGroups: string[],\n  workspaceUri: string,\n  fileUri: string,\n  hsnippetUtils: HSnippetUtils\n) => GeneratorResult;\n\nexport interface ContextInfo {\n  scopes: string[];\n}\n\nexport type ContextFilter = (context: ContextInfo) => boolean;\n\n// Represents a snippet template from which new instances can be created.\nexport class HSnippet {\n  trigger: string;\n  description: string;\n  generator: GeneratorFunction;\n  contextFilter?: ContextFilter;\n  regexp?: RegExp;\n  priority: number;\n\n  // UltiSnips-like options.\n  automatic = false;\n  multiline = false;\n  inword = false;\n  wordboundary = false;\n  beginningofline = false;\n\n  constructor(\n    header: IHSnippetHeader,\n    generator: GeneratorFunction,\n    contextFilter?: ContextFilter\n  ) {\n    this.description = header.description;\n    this.generator = generator;\n    this.contextFilter = contextFilter;\n    this.priority = header.priority || 0;\n\n    if (header.trigger instanceof RegExp) {\n      this.regexp = header.trigger;\n      this.trigger = '';\n    } else {\n      this.trigger = header.trigger;\n    }\n\n    if (header.flags.includes('A')) this.automatic = true;\n    if (header.flags.includes('M')) this.multiline = true;\n    if (header.flags.includes('i')) this.inword = true;\n    if (header.flags.includes('w')) this.wordboundary = true;\n    if (header.flags.includes('b')) this.beginningofline = true;\n  }\n}\n\nexport interface IHSnippetHeader {\n  trigger: string | RegExp;\n  description: string;\n  flags: string;\n  priority?: number;\n}\n"
  },
  {
    "path": "src/hsnippetInstance.ts",
    "content": "import * as vscode from 'vscode';\nimport { DynamicRange, GrowthType, IChangeInfo } from './dynamicRange';\nimport { applyOffset, getWorkspaceUri } from './utils';\nimport { HSnippet, GeneratorResult } from './hsnippet';\nimport { HSnippetUtils } from './hsnippetUtils';\n\nenum HSnippetPartType {\n  Placeholder,\n  Block,\n}\n\nclass HSnippetPart {\n  type: HSnippetPartType;\n  range: DynamicRange;\n  content: string;\n  id?: number;\n  updates: IChangeInfo[];\n\n  constructor(type: HSnippetPartType, range: DynamicRange, content: string, id?: number) {\n    this.type = type;\n    this.range = range;\n    this.content = content;\n    this.id = id;\n    this.updates = [];\n  }\n\n  updateRange() {\n    if (this.updates.length == 0) return;\n    this.range.update(this.updates);\n    this.updates = [];\n  }\n}\n\nexport class HSnippetInstance {\n  type: HSnippet;\n  matchGroups: string[];\n  editor: vscode.TextEditor;\n  range: DynamicRange;\n  placeholderIds: number[];\n  selectedPlaceholder: number;\n  parts: HSnippetPart[];\n  blockParts: HSnippetPart[];\n  blockChanged: boolean;\n  snippetString: vscode.SnippetString;\n\n  constructor(\n    type: HSnippet,\n    editor: vscode.TextEditor,\n    position: vscode.Position,\n    matchGroups: string[]\n  ) {\n    this.type = type;\n    this.editor = editor;\n    this.matchGroups = matchGroups;\n    this.selectedPlaceholder = 0;\n    this.placeholderIds = [];\n    this.blockChanged = false;\n\n    let generatorResult = this.runCodeBlocks(true);\n\n    // For a lack of creativity, I'm referring to the parts of the array that are returned by the\n    // snippet function as 'sections', and the result of the interpolated javascript in the snippets\n    // are referred to as 'blocks', as in code blocks.\n    let [sections, blocks] = generatorResult;\n\n    this.parts = [];\n    this.blockParts = [];\n\n    let start = position;\n    let snippetString = '';\n    const indentLevel = editor.document.lineAt(position.line).firstNonWhitespaceCharacterIndex;\n\n    for (let section of sections) {\n      let rawSection = section;\n\n      if (typeof rawSection != 'string') {\n        let block = blocks[rawSection.block];\n        let endPosition = applyOffset(position, block, indentLevel);\n        let range = new DynamicRange(position, endPosition);\n\n        let part = new HSnippetPart(HSnippetPartType.Block, range, block);\n        this.parts.push(part);\n        this.blockParts.push(part);\n\n        snippetString += block;\n        position = endPosition;\n        continue;\n      }\n\n      snippetString += rawSection;\n\n      // TODO: Handle snippets with default content in a placeholder.\n      let PLACEHOLDER_REGEX = /\\$(\\d+)|\\$\\{(\\d+)\\}/;\n      let match;\n      while ((match = PLACEHOLDER_REGEX.exec(rawSection))) {\n        let text = rawSection.substring(0, match.index);\n        position = applyOffset(position, text, indentLevel);\n        let range = new DynamicRange(position, position);\n\n        let placeholderId = Number(match[1] || match[2]);\n        if (!this.placeholderIds.includes(placeholderId)) this.placeholderIds.push(placeholderId);\n        this.parts.push(new HSnippetPart(HSnippetPartType.Placeholder, range, '', placeholderId));\n\n        rawSection = rawSection.substring(match.index + match[0].length);\n      }\n\n      position = applyOffset(position, rawSection, indentLevel);\n    }\n\n    this.snippetString = new vscode.SnippetString(snippetString);\n    this.range = new DynamicRange(start, position);\n\n    this.placeholderIds.sort();\n    if (this.placeholderIds[0] == 0) this.placeholderIds.shift();\n    this.placeholderIds.push(0);\n    this.selectedPlaceholder = this.placeholderIds[0];\n  }\n\n  runCodeBlocks(stripDollars = true, placeholderContents?: string[]) {\n    let generatorResult: GeneratorResult = [[], []];\n    let hsnippetUtils = new HSnippetUtils();\n\n    // TODO, update parser so only the block that threw the error does not expand, perhaps replace\n    // the block with the error message.\n    try {\n      generatorResult = this.type.generator(\n        new Proxy(placeholderContents || [], {\n          get(target, key) {\n            let index = Number(key);\n            if (target[index]) return target[index];\n            else return '';\n          },\n        }),\n        this.matchGroups,\n        getWorkspaceUri(),\n        this.editor.document.uri.toString(),\n        hsnippetUtils\n      );\n    } catch (e: unknown) {\n      if (e instanceof Error) {\n        vscode.window.showWarningMessage(\n          `Snippet ${this.type.description} failed to expand with error: ${e.message}`\n        );\n      }\n    }\n\n    generatorResult[1] = generatorResult[1].map((block) => {\n      if (stripDollars) {\n        block = block.replace(/\\$/g, '\\\\$');\n      }\n\n      block = HSnippetUtils.format(block, hsnippetUtils);\n\n      return block;\n    });\n\n    return generatorResult;\n  }\n\n  nextPlaceholder() {\n    let currentIndex = this.placeholderIds.indexOf(this.selectedPlaceholder);\n    this.selectedPlaceholder = this.placeholderIds[currentIndex + 1];\n    return this.selectedPlaceholder != undefined && this.selectedPlaceholder != 0;\n  }\n\n  prevPlaceholder() {\n    let currentIndex = this.placeholderIds.indexOf(this.selectedPlaceholder);\n    this.selectedPlaceholder = this.placeholderIds[currentIndex - 1];\n    return this.selectedPlaceholder != undefined && this.selectedPlaceholder != 0;\n  }\n\n  debugLog() {\n    let parts = this.parts;\n    for (let i = 0; i < parts.length; i++) {\n      let range = parts[i].range.range;\n      let start = range.start;\n      let end = range.end;\n      console.log(\n        `Tabstop ${i}: \"${parts[i].content}\" (${start.line}, ${start.character})..(${end.line}, ${end.character})`\n      );\n    }\n  }\n\n  // Updates the location of all the placeholder blocks and code blocks, and if any change happened\n  // to the placeholder blocks then run the generator function again with the updated values so the\n  // code blocks are updated.\n  update(changes: readonly vscode.TextDocumentContentChangeEvent[]) {\n    let ordChanges = [...changes];\n    ordChanges.sort((a, b) => {\n      if (a.range.end.isBefore(b.range.end)) return -1;\n      else if (a.range.end.isEqual(b.range.end)) return 0;\n      else return 1;\n    });\n\n    let changedPlaceholders = [];\n    let currentPart = 0;\n\n    // Expand ranges from left to right, preserving relative part positions.\n    for (let change of ordChanges) {\n      if (!change) continue;\n\n      let part = this.parts[currentPart];\n\n      while (currentPart < this.parts.length) {\n        if (part.range.range.end.isAfterOrEqual(change.range.end)) {\n          break;\n        }\n\n        currentPart++;\n        part = this.parts[currentPart];\n      }\n\n      if (currentPart >= this.parts.length) break;\n\n      while (part.range.contains(change.range)) {\n        if (\n          (part.type == HSnippetPartType.Placeholder &&\n            part.id == this.selectedPlaceholder &&\n            !this.blockChanged) ||\n          (part.type == HSnippetPartType.Block && this.blockChanged && part.content == change.text)\n        ) {\n          if (part.type == HSnippetPartType.Placeholder) changedPlaceholders.push(part);\n          part.updates.push({ change, growth: GrowthType.Grow });\n          currentPart++;\n          part = this.parts[currentPart];\n          break;\n        }\n\n        currentPart++;\n        part = this.parts[currentPart];\n      }\n\n      for (let i = currentPart; i < this.parts.length; i++) {\n        this.parts[i].updates.push({ change, growth: GrowthType.FixRight });\n      }\n    }\n\n    this.range.update(ordChanges.map((c) => ({ change: c, growth: GrowthType.Grow })));\n    this.parts.forEach((p) => p.updateRange());\n\n    if (this.blockChanged) this.blockChanged = false;\n    if (!changedPlaceholders.length) return;\n\n    changedPlaceholders.forEach((p) => (p.content = this.editor.document.getText(p.range.range)));\n    let placeholderContents = this.parts\n      .filter((p) => p.type == HSnippetPartType.Placeholder)\n      .map((p) => p.content);\n\n    let blocks = this.runCodeBlocks(false, placeholderContents)[1];\n\n    this.editor.edit((edit) => {\n      for (let i = 0; i < blocks.length; i++) {\n        let range = this.blockParts[i].range;\n        let oldContent = this.blockParts[i].content;\n        let content = blocks[i];\n\n        if (content != oldContent) {\n          edit.replace(range.range, content);\n          this.blockChanged = true;\n        }\n      }\n    });\n\n    this.blockParts.forEach((b, i) => (b.content = blocks[i]));\n  }\n}\n"
  },
  {
    "path": "src/hsnippetUtils.ts",
    "content": "function makeId(length: number) {\n  let result = '';\n  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n  for (let i = 0; i < length; i++) {\n    result += characters.charAt(Math.floor(Math.random() * characters.length));\n  }\n  return `[${result}]`;\n}\n\nexport class HSnippetUtils {\n  private placeholders: [string, string][];\n\n  constructor() {\n    this.placeholders = [];\n  }\n\n  tabstop(tabstop: number, placeholder?: string) {\n    const id = makeId(10);\n\n    let text = '';\n    if (placeholder) {\n      text = `\\${${tabstop}:${placeholder}}`;\n    } else {\n      text = `$${tabstop}`;\n    }\n    this.placeholders.push([id, text]);\n\n    return id;\n  }\n\n  static format(value: string, utils: HSnippetUtils) {\n    for (let [id, text] of utils.placeholders) {\n      value = value.replace(new RegExp(`\\\\[${id.slice(1, -1)}\\\\]`, 'g'), text);\n    }\n\n    return value;\n  }\n}\n"
  },
  {
    "path": "src/parser.ts",
    "content": "import { HSnippet, IHSnippetHeader, GeneratorFunction, ContextFilter } from './hsnippet';\n\nconst CODE_DELIMITER = '``';\nconst CODE_DELIMITER_REGEX = /``(?!`)/;\nconst HEADER_REGEXP = /^snippet ?(?:`([^`]+)`|(\\S+))?(?: \"([^\"]+)\")?(?: ([AMiwb]*))?/;\n\nfunction parseSnippetHeader(header: string): IHSnippetHeader {\n  let match = HEADER_REGEXP.exec(header);\n  if (!match) throw new Error('Invalid snippet header');\n\n  let trigger: string | RegExp = match[2];\n  if (match[1]) {\n    if (!match[1].endsWith('$')) match[1] += '$';\n    trigger = new RegExp(match[1], 'm');\n  }\n\n  return {\n    trigger,\n    description: match[3] || '',\n    flags: match[4] || '',\n  };\n}\n\ninterface IHSnippetInfo {\n  body: string;\n  contextFilter?: string;\n  header: IHSnippetHeader;\n}\n\ninterface IHSnippetParseResult {\n  contextFilter?: ContextFilter;\n  generatorFunction: GeneratorFunction;\n}\n\n// First replacement handles backslash characters, as the string will be inserted using vscode's\n// snippet engine, we should double down on every backslash, the second replacement handles double\n// quotes, as our snippet will be transformed into a javascript string surrounded by double quotes.\nfunction escapeString(string: string) {\n  return string.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nfunction parseSnippet(headerLine: string, lines: string[]): IHSnippetInfo {\n  let header = parseSnippetHeader(headerLine);\n\n  let script = [`(t, m, w, path, snip) => {`];\n  script.push(`let rv = \"\";`);\n  script.push(`let _result = [];`);\n  script.push(`let _blockResults = [];`);\n\n  let isCode = false;\n\n  while (lines.length > 0) {\n    let line = lines.shift() as string;\n\n    if (isCode) {\n      if (!line.includes(CODE_DELIMITER)) {\n        script.push(line.trim());\n      } else {\n        let [code, ...rest] = line.split(CODE_DELIMITER_REGEX);\n        script.push(code.trim());\n        lines.unshift(rest.join(CODE_DELIMITER));\n        script.push(`_result.push({block: _blockResults.length});`);\n        script.push(`_blockResults.push(String(rv));`);\n        isCode = false;\n      }\n    } else {\n      if (line.startsWith('endsnippet')) {\n        break;\n      } else if (!line.includes(CODE_DELIMITER)) {\n        script.push(`_result.push(\"${escapeString(line)}\");`);\n        script.push(`_result.push(\"\\\\n\");`);\n      } else if (isCode == false) {\n        let [text, ...rest] = line.split(CODE_DELIMITER_REGEX);\n        script.push(`_result.push(\"${escapeString(text)}\");`);\n        script.push(`rv = \"\";`);\n        lines.unshift(rest.join(CODE_DELIMITER));\n        isCode = true;\n      }\n    }\n  }\n\n  // Remove extra newline at the end.\n  script.pop();\n  script.push(`return [_result, _blockResults];`);\n  script.push(`}`);\n\n  return { body: script.join('\\n'), header };\n}\n\n// Transforms an hsnips file into a single function where the global context lives, every snippet is\n// transformed into a local function inside this and the list of all snippet functions is returned\n// so we can build the approppriate HSnippet objects.\nexport function parse(content: string): HSnippet[] {\n  let lines = content.split(/\\r?\\n/);\n\n  let snippetInfos = [];\n  let script = [];\n  let isCode = false;\n  let priority = 0;\n  let context = undefined;\n\n  while (lines.length > 0) {\n    let line = lines.shift() as string;\n\n    if (isCode) {\n      if (line.startsWith('endglobal')) {\n        isCode = false;\n      } else {\n        script.push(line);\n      }\n    } else if (line.startsWith('#')) {\n      continue;\n    } else if (line.startsWith('global')) {\n      isCode = true;\n    } else if (line.startsWith('priority ')) {\n      priority = Number(line.substring('priority '.length).trim()) || 0;\n    } else if (line.startsWith('context ')) {\n      context = line.substring('context '.length).trim() || undefined;\n    } else if (line.match(HEADER_REGEXP)) {\n      let info = parseSnippet(line, lines);\n      info.header.priority = priority;\n      info.contextFilter = context;\n      snippetInfos.push(info);\n\n      priority = 0;\n      context = undefined;\n    }\n  }\n\n  script.push(`return [`);\n  for (let snippet of snippetInfos) {\n    script.push('{');\n    if (snippet.contextFilter) {\n      script.push(`contextFilter: (context) => (${snippet.contextFilter}),`);\n    }\n    script.push(`generatorFunction: ${snippet.body}`);\n    script.push('},');\n  }\n  script.push(`]`);\n\n  // for some reason, `require` is not defined inside the snippet code blocks,\n  // so we're going to bind the it onto the function\n  let generators = new Function('require', script.join('\\n'))(require) as IHSnippetParseResult[];\n  return snippetInfos.map(\n    (s, i) => new HSnippet(s.header, generators[i].generatorFunction, generators[i].contextFilter)\n  );\n}\n"
  },
  {
    "path": "src/test/expansions/box.hsnips",
    "content": "snippet box \"Box\" A\n┌``rv = '─'.repeat(t[0].length + 2)``┐\n│ $1 │\n└``rv = '─'.repeat(t[0].length + 2)``┘\nendsnippet\n"
  },
  {
    "path": "src/test/expansions/box.input.txt",
    "content": "boxtest\n"
  },
  {
    "path": "src/test/expansions/box.output.txt",
    "content": "┌──────┐\n│ test │\n└──────┘\n"
  },
  {
    "path": "src/test/index.ts",
    "content": ""
  },
  {
    "path": "src/utils.ts",
    "content": "import * as vscode from 'vscode';\nimport * as path from 'path';\nimport * as os from 'os';\n\nexport enum SnippetDirType {\n  Global,\n  Workspace,\n}\n\nexport interface SnippetDirInfo {\n  readonly type: SnippetDirType;\n  readonly path: string;\n}\n\n/**\n * \"Expanding\" here means turning a prefix string like '~' into a string like '/home/foo'\n */\nconst pathPrefixExpanders: {\n  readonly [prefix: string]: {\n    readonly finalPathType: SnippetDirType;\n    readonly prefixExpanderFunc: () => string | null;\n  };\n} = {\n  '~': ({ finalPathType: SnippetDirType.Global, prefixExpanderFunc: os.homedir, }),\n  '${workspaceFolder}': ({ finalPathType: SnippetDirType.Workspace, prefixExpanderFunc: getWorkspaceFolderPath, }),\n};\n\nfunction getWorkspaceFolderPath(): string | null {\n  return vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || null;\n}\n\nexport function lineRange(character: number, position: vscode.Position): vscode.Range {\n  return new vscode.Range(position.line, character, position.line, position.character);\n}\n\n/**\n * The parameter `options`, can be removed after the function `getOldGlobalSnippetDir` is removed and migration from the\n * directory to the new one is not necessary anymore.\n */\nexport function getSnippetDirInfo(\n  context: vscode.ExtensionContext,\n  options: { ignoreWorkspace: boolean } = { ignoreWorkspace: false },\n): SnippetDirInfo {\n  let hsnipsPath = vscode.workspace.getConfiguration('hsnips').get('hsnipsPath') as string | null;\n\n  // only non-empty strings are taken, anything else is discarded\n  if (typeof hsnipsPath === 'string' && hsnipsPath.length > 0) {\n    // normalize to ensure that the correct platform-specific file separators are used\n    hsnipsPath = path.normalize(hsnipsPath);\n\n    let type: SnippetDirType | null = null;\n\n    // first some \"preprocessing\" is done on the configured path: expanding leading '~' and '${workspaceFolder}'\n    for (const prefix in pathPrefixExpanders) {\n      // a leading string like '~foo' is ignored, only '~' or '~/foo' values are taken\n      if (hsnipsPath !== prefix && !hsnipsPath.startsWith(prefix + path.sep)) {\n        continue;\n      }\n\n      const expandingInfo = pathPrefixExpanders[prefix];\n\n      if (options.ignoreWorkspace && expandingInfo.finalPathType == SnippetDirType.Workspace) {\n        // this expander would've resulted in a workspace folder path; skip it\n        continue;\n      }\n\n      const expandedPrefix = expandingInfo.prefixExpanderFunc();\n\n      if (expandedPrefix) {\n        hsnipsPath = expandedPrefix + hsnipsPath.substring(prefix.length);\n        type = expandingInfo.finalPathType;\n      } else {\n        // in case the prefix did match, but the expanded function wasn't able to properly expand, the entire path will\n        // be invalidated\n        // e.g.: given the string '~/foo', but the home directory could not be determined for some reason\n        hsnipsPath = null;\n        type = null;\n      }\n\n      break;\n    }\n\n    // this will only be falsy if the path was invalidated as a result of one of the expander functions failing to\n    // properly expanding a prefix\n    if (hsnipsPath) {\n      if (!options.ignoreWorkspace) {\n        const workspaceFolderPath = getWorkspaceFolderPath();\n        if (!path.isAbsolute(hsnipsPath) && workspaceFolderPath) {\n          hsnipsPath = path.join(workspaceFolderPath, hsnipsPath);\n          type = SnippetDirType.Workspace;\n        }\n      }\n\n      // at this point the path will only be relative in four cases:\n      //  * an already relative path was configured without a matching prefix to expand\n      //  * one of the expander functions messed up and returned a relative path\n      //  * the function `getWorkspaceFolderPath` messed and returned a relative path\n      //  * the path would've been a workspace path, but the parameter `ignoreWorkspace` is set to `true`\n      if (path.isAbsolute(hsnipsPath)) {\n        if (type === null) {\n          type = SnippetDirType.Global;\n        }\n\n        return {\n          type,\n          path: hsnipsPath,\n        };\n      }\n    }\n  }\n\n  const globalStoragePath = context.globalStorageUri.fsPath;\n  return {\n    type: SnippetDirType.Global,\n    path: path.join(globalStoragePath, 'hsnips'),\n  };\n}\n\n/**\n * @deprecated The paths here are hardcoded in. Only keep this function so that older users can migrate.\n */\nexport function getOldGlobalSnippetDir(): string {\n  let hsnipsPath = vscode.workspace.getConfiguration('hsnips').get('hsnipsPath') as string | null;\n\n  if (hsnipsPath && path.isAbsolute(hsnipsPath)) {\n    return hsnipsPath;\n  }\n\n  let platform = os.platform();\n\n  let APPDATA = process.env.APPDATA || '';\n  let HOME = process.env.HOME || '';\n\n  if (platform == 'win32') {\n    return path.join(APPDATA, 'Code/User/hsnips');\n  } else if (platform == 'darwin') {\n    return path.join(HOME, 'Library/Application Support/Code/User/hsnips');\n  } else {\n    return path.join(HOME, '.config/Code/User/hsnips');\n  }\n}\n\nexport function applyOffset(\n  position: vscode.Position,\n  text: string,\n  indent: number\n): vscode.Position {\n  text = text.replace('\\\\$', '$');\n  let lines = text.split('\\n');\n  let newLine = position.line + lines.length - 1;\n  let charOffset = lines[lines.length - 1].length;\n\n  let newChar = position.character + charOffset;\n  if (lines.length > 1) newChar = indent + charOffset;\n\n  return position.with(newLine, newChar);\n}\n\nexport function getWorkspaceUri(): string {\n  return vscode.workspace.workspaceFolders?.[0]?.uri?.toString() ?? '';\n}\n"
  },
  {
    "path": "syntaxes/hsnips.tmLanguage.json",
    "content": "{\n  \"scopeName\": \"source.hsnips\",\n  \"name\": \"comment\",\n  \"patterns\": [\n    {\n      \"match\": \"^#.*\",\n      \"captures\": {\n        \"0\": {\n          \"name\": \"comment\"\n        }\n      }\n    },\n    {\n      \"begin\": \"^(snippet) ?(?:(`[^`]+`)|([\\\\S]+))?(?: (\\\"[^\\\"]+\\\"))?(?: ([AMiwb]*))?.*\",\n      \"beginCaptures\": {\n        \"1\": {\n          \"name\": \"keyword.control\"\n        },\n        \"2\": {\n          \"name\": \"entity.name.function\"\n        },\n        \"3\": {\n          \"name\": \"string.regexp\"\n        },\n        \"4\": {\n          \"name\": \"string.quoted.double\"\n        },\n        \"5\": {\n          \"name\": \"constant.language\"\n        }\n      },\n      \"end\": \"^endsnippet\",\n      \"endCaptures\": {\n        \"0\": {\n          \"name\": \"keyword.control\"\n        }\n      },\n      \"patterns\": [\n        {\n          \"include\": \"#snippet\"\n        }\n      ]\n    },\n    {\n      \"begin\": \"^global\",\n      \"beginCaptures\": {\n        \"0\": {\n          \"name\": \"keyword.control\"\n        }\n      },\n      \"end\": \"^endglobal\",\n      \"endCaptures\": {\n        \"0\": {\n          \"name\": \"keyword.control\"\n        }\n      },\n      \"patterns\": [\n        {\n          \"include\": \"source.js\"\n        }\n      ]\n    },\n    {\n      \"match\": \"^(priority) ?(-?\\\\d+)?\",\n      \"captures\": {\n        \"1\": {\n          \"name\": \"keyword.control\"\n        },\n        \"2\": {\n          \"name\": \"constant.numeric\"\n        }\n      }\n    },\n    {\n      \"match\": \"^(context)(?:(.*))\",\n      \"captures\": {\n        \"1\": {\n          \"name\": \"keyword.control\"\n        },\n        \"2\": {\n          \"patterns\": [\n            {\n              \"include\": \"source.js\"\n            }\n          ]\n        }\n      }\n    }\n  ],\n  \"repository\": {\n    \"snippet\": {\n      \"patterns\": [\n        {\n          \"contentName\": \"meta.embedded.js\",\n          \"begin\": \"``\",\n          \"beginCaptures\": {\n            \"0\": {\n              \"name\": \"string.interpolated\"\n            }\n          },\n          \"end\": \"``\",\n          \"endCaptures\": {\n            \"0\": {\n              \"name\": \"string.interpolated\"\n            }\n          },\n          \"patterns\": [\n            {\n              \"include\": \"source.js\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "syntaxes/hsnips.tmLanguage.yaml",
    "content": "scopeName: 'source.hsnips'\n\nname: comment\npatterns:\n  - match: '^#.*'\n    captures:\n      0:\n        name: comment\n\n  - begin: '^(snippet) ?(?:(`[^`]+`)|([\\S]+))?(?: (\"[^\"]+\"))?(?: ([AMiwb]*))?.*'\n    beginCaptures:\n      1:\n        name: keyword.control\n      2:\n        name: entity.name.function\n      3:\n        name: string.regexp\n      4:\n        name: string.quoted.double\n      5:\n        name: constant.language\n    end: '^endsnippet'\n    endCaptures:\n      0:\n        name: keyword.control\n    patterns:\n      - include: '#snippet'\n\n  - begin: '^global'\n    beginCaptures:\n      0:\n        name: keyword.control\n    end: '^endglobal'\n    endCaptures:\n      0:\n        name: keyword.control\n    patterns:\n      - include: 'source.js'\n\n  - match: '^(priority) ?(-?\\d+)?'\n    captures:\n      1:\n        name: keyword.control\n      2:\n        name: constant.numeric\n\n  - match: '^(context)(?: (.*))'\n    captures:\n      1:\n        name: keyword.control\n      2:\n        patterns:\n          - include: 'source.js'\n\nrepository:\n  snippet:\n    patterns:\n      - contentName: meta.embedded.js\n        begin: '``'\n        beginCaptures:\n          0:\n            name: string.interpolated\n        end: '``'\n        endCaptures:\n          0:\n            name: string.interpolated\n        patterns:\n          - include: 'source.js'\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es2019\",\n    \"lib\": [\"ES2019\"],\n    \"outDir\": \"out\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"rootDir\": \"src\",\n    \"allowJs\": false\n  },\n  \"exclude\": [\"node_modules\", \".vscode-test\"]\n}\n"
  },
  {
    "path": "types/hscopes.d.ts",
    "content": "import * as vscode from 'vscode';\n\n/**\n * A grammar\n */\n\nexport interface IGrammar {\n  /**\n   * Tokenize `lineText` using previous line state `prevState`.\n   */\n  tokenizeLine(lineText: string, prevState: StackElement | null): ITokenizeLineResult;\n}\n\nexport interface ITokenizeLineResult {\n  readonly tokens: IToken[];\n  /**\n   * The `prevState` to be passed on to the next line tokenization.\n   */\n  readonly ruleStack: StackElement;\n}\n\nexport interface IToken {\n  startIndex: number;\n  readonly endIndex: number;\n  readonly scopes: string[];\n}\n\nexport interface StackElement {\n  _stackElementBrand: void;\n  readonly depth: number;\n  clone(): StackElement;\n  equals(other: StackElement): boolean;\n}\n\nexport interface Token {\n  range: vscode.Range;\n  text: string;\n  scopes: string[];\n}\n\nexport interface HScopesAPI {\n  getScopeAt(document: vscode.TextDocument, position: vscode.Position): Token | null;\n  getGrammar(scopeName: string): Promise<IGrammar | null>;\n  getScopeForLanguage(language: string): string | null;\n}\n"
  },
  {
    "path": "types/open-file-explorer.d.ts",
    "content": "declare module 'open-file-explorer' {\n  function openExplorer(path: string, callback?: (err: Error) => void): void;\n  export = openExplorer;\n}\n"
  }
]