Repository: draivin/hsnips
Branch: master
Commit: 1180529cdb1d
Files: 27
Total size: 54.4 KB
Directory structure:
gitextract_wqaxspsi/
├── .eslintrc.js
├── .gitignore
├── .vscode/
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── LICENSE
├── README.md
├── language-configuration.json
├── package.json
├── src/
│ ├── completion.ts
│ ├── consts.ts
│ ├── dynamicRange.ts
│ ├── extension.ts
│ ├── hsnippet.ts
│ ├── hsnippetInstance.ts
│ ├── hsnippetUtils.ts
│ ├── parser.ts
│ ├── test/
│ │ ├── expansions/
│ │ │ ├── box.hsnips
│ │ │ ├── box.input.txt
│ │ │ └── box.output.txt
│ │ └── index.ts
│ └── utils.ts
├── syntaxes/
│ ├── hsnips.tmLanguage.json
│ └── hsnips.tmLanguage.yaml
├── tsconfig.json
└── types/
├── hscopes.d.ts
└── open-file-explorer.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
rules: {
semi: [2, 'always'],
'@typescript-eslint/no-unused-vars': 2,
'@typescript-eslint/no-explicit-any': 2,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-non-null-assertion': 0,
'prefer-const': 0,
},
};
================================================
FILE: .gitignore
================================================
out
node_modules
*.vsix
================================================
FILE: .vscode/launch.json
================================================
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"spellright.language": [
"en"
],
"spellright.documentTypes": [
"markdown",
"latex"
]
}
================================================
FILE: .vscode/tasks.json
================================================
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Ian Ornelas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# HyperSnips

HyperSnips is a snippet engine for vscode heavily inspired by vim's
[UltiSnips](https://github.com/SirVer/ultisnips).
## Usage
To use HyperSnips you create `.hsnips` files on a directory which depends on your platform:
- Windows: `%APPDATA%\Code\User\globalStorage\draivin.hsnips\hsnips\(language).hsnips`
- Mac: `$HOME/Library/Application Support/Code/User/globalStorage/draivin.hsnips/hsnips/(language).hsnips`
- Linux: `$HOME/.config/Code/User/globalStorage/draivin.hsnips/hsnips/(language).hsnips`
You can open this directory by running the command `HyperSnips: Open snippets directory`.
This directory may be customized by changing the setting `hsnips.hsnipsPath`.
If this setting starts with `~` or `${workspaceFolder}`, then it will be replaced with
your home directory or the current workspace folder, respectively.
The file should be named based on the language the snippets are meant for (e.g. `latex.hsnips`
for snippets which will be available for LaTeX files).
Additionally, you can create an `all.hsnips` file for snippets that should be available on all languages.
### Snippets file
A snippets file is a file with the `.hsnips` extension, the file is composed of two types of blocks:
global blocks and snippet blocks.
Global blocks are JavaScript code blocks with code that is shared between all the snippets defined
in the current file. They are defined with the `global` keyword, as follows:
```lua
global
// JavaScript code
endglobal
```
Snippet blocks are snippet definitions. They are defined with the `snippet` keyword, as follows:
```lua
context expression
snippet trigger "description" flags
body
endsnippet
```
where the `trigger` field is required and the fields `description` and `flags` are optional.
### Trigger
A trigger can be any sequence of characters which does not contain a space, or a regular expression
surrounded by backticks (`` ` ``).
### Flags
The flags field is a sequence of characters which modify the behavior of the snippet, the available
flags are the following:
- `A`: Automatic snippet expansion - Usually snippets are activated when the `tab` key is pressed,
with the `A` flag snippets will activate as soon as their trigger matches, it is specially useful
for regex snippets.
- `i`: In-word expansion\* - By default, a snippet trigger will only match when the trigger is
preceded by whitespace characters. A snippet with this option is triggered regardless of the
preceding character, for example, a snippet can be triggered in the middle of a word.
- `w`: Word boundary\* - With this option the snippet trigger will match when the trigger is a word
boundary character. Use this option, for example, to permit expansion where the trigger follows
punctuation without expanding suffixes of larger words.
- `b`: Beginning of line expansion\* - A snippet with this option is expanded only if the
tab trigger is the first word on the line. In other words, if only whitespace precedes the tab
trigger, expand.
- `M`: Multi-line mode - By default, regex matches will only match content on the current line, when
this option is enabled the last `hsnips.multiLineContext` lines will be available for matching.
\*: This flag will only affect snippets which have non-regex triggers.
### Snippet body
The body is the text that will replace the trigger when the snippet is expanded, as in usual
snippets, the tab stops `$1`, `$2`, etc. are available.
The full power of HyperSnips comes when using JavaScript interpolation: you can have code blocks
inside your snippet delimited by two backticks (` `` `) that will run when the snippet is expanded,
and every time the text in one of the tab stops is changed.
### Code interpolation
Inside the code interpolation, you have access to a few special variables:
- `rv`: The return value of your code block, the value of this variable will replace the code block
when the snippet is expanded.
- `t`: An array containing the text within the tab stops, in the same order as the tab stops are
defined in the snippet block. You can use it to dynamically change the snippet content.
- `m`: An array containing the match groups of your regular expression trigger, or an empty array if
the trigger is not a regular expression.
- `w`: A URI string of the currently opened workspace, or an empty string if no workspace is open.
- `path`: A URI string of the current document. (untitled documents have the scheme `untitled`)
Additionally, every variable defined in one code block will be available in all the subsequent code
blocks in the snippet.
The `require` function can also be used to import NodeJS modules.
### Context matching
Optionally, you can have a `context` line before the snippet block, it is followed by any javascript
expression, and the snippet is only available if the `context` expression evaluates to `true`.
Inside the `context` expression you can use the `context` variable, which has the following type:
```ts
interface Context {
scopes: string[];
}
```
Here, `scopes` stands for the TextMate scopes at the current cursor position, which can be viewed by
running the `Developer: Inspect Editor Tokens and Scopes` command in `vscode`.
As an example, here is an automatic LaTeX snippet that only expands when inside a math block:
```lua
global
function math(context) {
return context.scopes.some(s => s.startsWith("meta.math"));
}
endglobal
context math(context)
snippet inv "inverse" Ai
^{-1}
endsnippet
```
## Examples
- Simple snippet which greets you with the current date and time
```lua
snippet dategreeting "Gives you the current date!"
Hello from your hsnip at ``rv = new Date().toDateString()``!
endsnippet
```
- Box snippet as shown in the gif above
```lua
snippet box "Box" A
``rv = '┌' + '─'.repeat(t[0].length + 2) + '┐'``
│ $1 │
``rv = '└' + '─'.repeat(t[0].length + 2) + '┘'``
endsnippet
```
- Snippet to insert the current filename
```lua
snippet filename "Current Filename"
``rv = require('path').basename(path)``
endsnippet
```
================================================
FILE: language-configuration.json
================================================
{
"comments": {
"lineComment": "#"
},
"folding": {
"markers": {
"start": "^snippet\\b",
"end": "^endsnippet\\b"
}
}
}
================================================
FILE: package.json
================================================
{
"name": "hsnips",
"displayName": "HyperSnips",
"icon": "images/hypersnips.png",
"version": "0.2.9",
"publisher": "draivin",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/draivin/hsnips"
},
"bugs": {
"url": "https://github.com/draivin/hsnips/issues"
},
"engines": {
"vscode": "^1.52.0"
},
"categories": [
"Snippets",
"Other"
],
"keywords": [
"ultisnips",
"programmable snippets",
"dynamic snippets",
"snippets"
],
"preview": true,
"activationEvents": [
"*"
],
"contributes": {
"configuration": [
{
"title": "HyperSnips",
"properties": {
"hsnips.multiLineContext": {
"type": "number",
"default": 20,
"description": "Number of lines matched when using multi-line regex mode."
},
"hsnips.hsnipsPath": {
"type": [
"string",
"null"
],
"default": null,
"description": "Absolute path or relative path from the workspace folder to the folder containing the hsnips files."
}
}
}
],
"commands": [
{
"category": "HyperSnips",
"command": "hsnips.openSnippetsDir",
"title": "Open Snippets Directory"
},
{
"category": "HyperSnips",
"command": "hsnips.openSnippetFile",
"title": "Open Snippet File"
},
{
"category": "HyperSnips",
"command": "hsnips.reloadSnippets",
"title": "Reload Snippets"
}
],
"keybindings": [
{
"key": "tab",
"command": "hsnips.nextPlaceholder",
"when": "editorTextFocus && hasNextTabstop && inSnippetMode && !suggestWidgetVisible"
},
{
"key": "shift+tab",
"command": "hsnips.prevPlaceholder",
"when": "editorTextFocus && hasPrevTabstop && inSnippetMode && !suggestWidgetVisible"
},
{
"key": "escape",
"command": "hsnips.leaveSnippet",
"when": "editorTextFocus && inSnippetMode && !suggestWidgetVisible"
}
],
"languages": [
{
"id": "hsnips",
"extensions": [
".hsnips"
],
"aliases": [
"HyperSnips"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "hsnips",
"scopeName": "source.hsnips",
"path": "./syntaxes/hsnips.tmLanguage.json",
"embeddedLanguages": {
"meta.embedded.js": "javascript"
}
}
]
},
"main": "./out/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"lint": "eslint . --ext .ts,.tsx",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/node": "^15.3.0",
"@types/vscode": "^1.52.0",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"eslint": "^7.17.0",
"typescript": "^4.2.4"
},
"dependencies": {
"open-file-explorer": "^1.0.2"
},
"extensionDependencies": [
"draivin.hscopes"
]
}
================================================
FILE: src/completion.ts
================================================
import * as vscode from 'vscode';
import { lineRange } from './utils';
import { HSnippet } from './hsnippet';
export class CompletionInfo {
range: vscode.Range;
completionRange: vscode.Range;
snippet: HSnippet;
label: string;
groups: string[];
constructor(snippet: HSnippet, label: string, range: vscode.Range, groups: string[]) {
this.snippet = snippet;
this.label = label;
this.range = range;
this.completionRange = new vscode.Range(range.start, range.start.translate(0, label.length));
this.groups = groups;
}
toCompletionItem() {
let completionItem = new vscode.CompletionItem(this.label);
completionItem.range = this.range;
completionItem.detail = this.snippet.description;
completionItem.insertText = this.label;
completionItem.command = {
command: 'hsnips.expand',
title: 'expand',
arguments: [this],
};
return completionItem;
}
}
function matchSuffixPrefix(context: string, trigger: string) {
while (trigger.length) {
if (context.endsWith(trigger)) return trigger;
trigger = trigger.substring(0, trigger.length - 1);
}
return null;
}
export function getCompletions(
document: vscode.TextDocument,
position: vscode.Position,
snippets: HSnippet[]
): CompletionInfo[] | CompletionInfo | undefined {
let line = document.getText(lineRange(0, position));
// Grab everything until previous whitespace as our matching context.
let match = line.match(/\S*$/);
let contextRange = lineRange((match as RegExpMatchArray).index || 0, position);
let context = document.getText(contextRange);
let precedingContextRange = new vscode.Range(
position.line,
0,
position.line,
(match as RegExpMatchArray).index || 0
);
let precedingContext = document.getText(precedingContextRange);
let isPrecedingContextWhitespace = precedingContext.match(/^\s*$/) != null;
let wordRange = document.getWordRangeAtPosition(position) || contextRange;
if (wordRange.end != position) {
wordRange = new vscode.Range(wordRange.start, position);
}
let wordContext = document.getText(wordRange);
let longContext = null;
let completions = [];
let snippetContext = { scopes: [] };
//FIXME: Plain text scope resolution should be fixed in hscopes.
if (document.languageId !== 'plaintext') {
snippetContext = {
scopes: vscode.extensions
.getExtension('draivin.hscopes')!
.exports.getScopeAt(document, position).scopes,
};
}
for (let snippet of snippets) {
if (snippet.contextFilter && !snippet.contextFilter(snippetContext)) {
continue;
}
let snippetMatches = false;
let snippetRange = contextRange;
let prefixMatches = false;
let matchGroups: string[] = [];
let label = snippet.trigger;
if (snippet.trigger) {
let matchingPrefix = null;
if (snippet.inword) {
snippetMatches = context.endsWith(snippet.trigger);
matchingPrefix = snippetMatches
? snippet.trigger
: matchSuffixPrefix(context, snippet.trigger);
} else if (snippet.wordboundary) {
snippetMatches = wordContext == snippet.trigger;
matchingPrefix = snippet.trigger.startsWith(wordContext) ? wordContext : null;
} else if (snippet.beginningofline) {
snippetMatches = context.endsWith(snippet.trigger) && isPrecedingContextWhitespace;
matchingPrefix =
snippet.trigger.startsWith(context) && isPrecedingContextWhitespace ? context : null;
} else {
snippetMatches = context == snippet.trigger;
matchingPrefix = snippet.trigger.startsWith(context) ? context : null;
}
if (matchingPrefix) {
snippetRange = new vscode.Range(position.translate(0, -matchingPrefix.length), position);
prefixMatches = true;
}
} else if (snippet.regexp) {
let regexContext = line;
if (snippet.multiline) {
if (!longContext) {
let numberPrevLines = vscode.workspace
.getConfiguration('hsnips')
.get('multiLineContext') as number;
longContext = document
.getText(
new vscode.Range(
new vscode.Position(Math.max(position.line - numberPrevLines, 0), 0),
position
)
)
.replace(/\r/g, '');
}
regexContext = longContext;
}
let match = snippet.regexp.exec(regexContext);
if (match) {
let charOffset = match.index - regexContext.lastIndexOf('\n', match.index) - 1;
let lineOffset = match[0].split('\n').length - 1;
snippetRange = new vscode.Range(
new vscode.Position(position.line - lineOffset, charOffset),
position
);
snippetMatches = true;
prefixMatches = true;
matchGroups = Array.from(match);
label = match[0];
}
}
let completion = new CompletionInfo(snippet, label, snippetRange, matchGroups);
if (snippet.automatic && snippetMatches) {
return completion;
} else if (prefixMatches) {
completions.push(completion);
}
}
return completions;
}
================================================
FILE: src/consts.ts
================================================
export const COMPLETIONS_TRIGGERS = [
' ',
'.',
'(',
')',
'{',
'}',
'[',
']',
',',
':',
"'",
'"',
'=',
'<',
'>',
'/',
'\\',
'+',
'-',
'|',
'&',
'*',
'%',
'=',
'$',
'#',
'@',
'!',
];
================================================
FILE: src/dynamicRange.ts
================================================
import * as vscode from 'vscode';
type PositionDelta = { characterDelta: number; lineDelta: number };
export enum GrowthType {
Grow,
FixLeft,
FixRight,
}
export interface IChangeInfo {
change: vscode.TextDocumentContentChangeEvent;
growth: GrowthType;
}
function getRangeDelta(
range: vscode.Range,
change: vscode.TextDocumentContentChangeEvent,
growth: GrowthType
): [PositionDelta, PositionDelta] {
let deltaStart = { characterDelta: 0, lineDelta: 0 };
let deltaEnd = { characterDelta: 0, lineDelta: 0 };
let textLines = change.text.split('\n');
let lineDelta =
change.text.split('\n').length - (change.range.end.line - change.range.start.line + 1);
let charDelta = textLines[textLines.length - 1].length - change.range.end.character;
if (lineDelta == 0) charDelta += change.range.start.character;
if (range.start.isAfterOrEqual(change.range.end)) {
deltaStart.lineDelta = lineDelta;
}
if (range.end.isAfterOrEqual(change.range.end)) {
deltaEnd.lineDelta = lineDelta;
}
if (change.range.end.line == range.start.line)
if (
(growth == GrowthType.FixRight && range.start.isEqual(change.range.end)) ||
range.start.isAfter(change.range.end)
) {
deltaStart.characterDelta = charDelta;
}
if (change.range.end.line == range.end.line)
if (
(growth != GrowthType.FixLeft && range.end.isEqual(change.range.end)) ||
range.end.isAfter(change.range.end)
) {
deltaEnd.characterDelta = charDelta;
}
return [deltaStart, deltaEnd];
}
export class DynamicRange {
range: vscode.Range;
constructor(start: vscode.Position, end: vscode.Position) {
this.range = new vscode.Range(start, end);
}
static fromRange(range: vscode.Range) {
return new DynamicRange(range.start, range.end);
}
update(changes: IChangeInfo[]) {
let deltaStart = { characterDelta: 0, lineDelta: 0 };
let deltaEnd = { characterDelta: 0, lineDelta: 0 };
for (let { change, growth } of changes) {
let deltaChange = getRangeDelta(this.range, change, growth);
deltaStart.characterDelta += deltaChange[0].characterDelta;
deltaStart.lineDelta += deltaChange[0].lineDelta;
deltaEnd.characterDelta += deltaChange[1].characterDelta;
deltaEnd.lineDelta += deltaChange[1].lineDelta;
}
let [newStart, newEnd] = [this.range.start, this.range.end];
newStart = newStart.translate(deltaStart);
newEnd = newEnd.translate(deltaEnd);
this.range = this.range.with(newStart, newEnd);
}
contains(range: vscode.Range): boolean {
return this.range.contains(range);
}
}
================================================
FILE: src/extension.ts
================================================
import * as vscode from 'vscode';
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from 'fs';
import * as path from 'path';
import openExplorer = require('open-file-explorer');
import { HSnippet } from './hsnippet';
import { HSnippetInstance } from './hsnippetInstance';
import { parse } from './parser';
import { getOldGlobalSnippetDir, getSnippetDirInfo, SnippetDirType } from './utils';
import { getCompletions, CompletionInfo } from './completion';
import { COMPLETIONS_TRIGGERS } from './consts';
const SNIPPETS_BY_LANGUAGE: Map<string, HSnippet[]> = new Map();
const SNIPPET_STACK: HSnippetInstance[] = [];
let insertingSnippet = false;
async function loadSnippets(context: vscode.ExtensionContext) {
SNIPPETS_BY_LANGUAGE.clear();
const snippetDirInfo = getSnippetDirInfo(context);
if (snippetDirInfo === null) {
return;
}
const snippetDirPath = snippetDirInfo.path;
if (!existsSync(snippetDirPath)) {
mkdirSync(snippetDirPath, { recursive: true });
}
for (let file of readdirSync(snippetDirPath)) {
if (path.extname(file).toLowerCase() != '.hsnips') continue;
let filePath = path.join(snippetDirPath, file);
let fileData = readFileSync(filePath, 'utf8');
let language = path.basename(file, '.hsnips').toLowerCase();
SNIPPETS_BY_LANGUAGE.set(language, parse(fileData));
}
let globalSnippets = SNIPPETS_BY_LANGUAGE.get('all');
if (globalSnippets) {
for (let [language, snippetList] of SNIPPETS_BY_LANGUAGE.entries()) {
if (language != 'all') snippetList.push(...globalSnippets);
}
}
// Sort snippets by descending priority.
for (let snippetList of SNIPPETS_BY_LANGUAGE.values()) {
snippetList.sort((a, b) => b.priority - a.priority);
}
}
// This function may be called after a snippet expansion, in which case the original text was
// replaced by the snippet label, or it may be called directly, as in the case of an automatic
// expansion. Depending on which case it is, we have to delete a different editor range before
// triggering the real hsnip expansion.
export async function expandSnippet(
completion: CompletionInfo,
editor: vscode.TextEditor,
snippetExpansion = false
) {
let snippetInstance = new HSnippetInstance(
completion.snippet,
editor,
completion.range.start,
completion.groups
);
let insertionRange: vscode.Range | vscode.Position = completion.range.start;
// The separate deletion is a workaround for a VsCodeVim bug, where when we trigger a snippet which
// has a replacement range, it will go into NORMAL mode, see issues #28 and #36.
// TODO: Go back to inserting the snippet and removing in a single command once the VsCodeVim bug
// is fixed.
insertingSnippet = true;
await editor.edit(
(eb) => {
eb.delete(snippetExpansion ? completion.completionRange : completion.range);
},
{ undoStopAfter: false, undoStopBefore: !snippetExpansion }
);
await editor.insertSnippet(snippetInstance.snippetString, insertionRange, {
undoStopAfter: false,
undoStopBefore: false,
});
if (snippetInstance.selectedPlaceholder != 0) SNIPPET_STACK.unshift(snippetInstance);
insertingSnippet = false;
}
export function activate(context: vscode.ExtensionContext) {
vscode.extensions.getExtension('draivin.hscopes')?.activate();
// migrating from the old, hardcoded directory to the new one. TODO: remove this at some point
const oldGlobalSnippetDir = getOldGlobalSnippetDir();
if (existsSync(oldGlobalSnippetDir)) {
// only the global directory needs to be migrated, which is why `ignoreWorkspace` is set to `true` here
const newSnippetDirInfo = getSnippetDirInfo(context, { ignoreWorkspace: true });
if (newSnippetDirInfo.type == SnippetDirType.Global) {
mkdirSync(path.dirname(newSnippetDirInfo.path), { recursive: true });
renameSync(oldGlobalSnippetDir, newSnippetDirInfo.path);
}
}
loadSnippets(context);
context.subscriptions.push(
vscode.commands.registerCommand('hsnips.openSnippetsDir', () =>
openExplorer(getSnippetDirInfo(context).path)
)
);
context.subscriptions.push(
vscode.commands.registerCommand('hsnips.openSnippetFile', async () => {
let snippetDirPath = getSnippetDirInfo(context).path;
let files = readdirSync(snippetDirPath);
let selectedFile = await vscode.window.showQuickPick(files);
if (selectedFile) {
let document = await vscode.workspace.openTextDocument(
path.join(snippetDirPath, selectedFile)
);
vscode.window.showTextDocument(document);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('hsnips.reloadSnippets', () => loadSnippets(context))
);
context.subscriptions.push(
vscode.commands.registerCommand('hsnips.leaveSnippet', () => {
while (SNIPPET_STACK.length) SNIPPET_STACK.pop();
vscode.commands.executeCommand('leaveSnippet');
})
);
context.subscriptions.push(
vscode.commands.registerCommand('hsnips.nextPlaceholder', () => {
if (SNIPPET_STACK[0] && !SNIPPET_STACK[0].nextPlaceholder()) {
SNIPPET_STACK.shift();
}
vscode.commands.executeCommand('jumpToNextSnippetPlaceholder');
})
);
context.subscriptions.push(
vscode.commands.registerCommand('hsnips.prevPlaceholder', () => {
if (SNIPPET_STACK[0] && !SNIPPET_STACK[0].prevPlaceholder()) {
SNIPPET_STACK.shift();
}
vscode.commands.executeCommand('jumpToPrevSnippetPlaceholder');
})
);
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((document) => {
if (document.languageId == 'hsnips') {
loadSnippets(context);
}
})
);
context.subscriptions.push(
vscode.commands.registerTextEditorCommand(
'hsnips.expand',
(editor, _, completion: CompletionInfo) => {
expandSnippet(completion, editor, true);
}
)
);
// Forward all document changes so that the active snippet can update its related blocks.
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((e) => {
if (SNIPPET_STACK.length && SNIPPET_STACK[0].editor.document == e.document) {
SNIPPET_STACK[0].update(e.contentChanges);
}
if (insertingSnippet) return;
if (e.contentChanges.length === 0) return;
let mainChange = e.contentChanges[0];
if (!mainChange) return;
// Let's try to detect only events that come from keystrokes.
if (mainChange.text.length != 1) return;
let snippets = SNIPPETS_BY_LANGUAGE.get(e.document.languageId.toLowerCase());
if (!snippets) snippets = SNIPPETS_BY_LANGUAGE.get('all');
if (!snippets) return;
let mainChangePosition = mainChange.range.start.translate(0, mainChange.text.length);
let completions = getCompletions(e.document, mainChangePosition, snippets);
// When an automatic completion is matched it is returned as an element, we check for this by
// using !isArray, and then expand the snippet.
if (completions && !Array.isArray(completions)) {
let editor = vscode.window.activeTextEditor;
if (editor && e.document == editor.document) {
expandSnippet(completions, editor);
return;
}
}
})
);
// Remove any stale snippet instances.
context.subscriptions.push(
vscode.window.onDidChangeVisibleTextEditors(() => {
while (SNIPPET_STACK.length) SNIPPET_STACK.pop();
})
);
context.subscriptions.push(
vscode.window.onDidChangeTextEditorSelection((e) => {
while (SNIPPET_STACK.length) {
if (e.selections.some((s) => SNIPPET_STACK[0].range.contains(s))) {
break;
}
SNIPPET_STACK.shift();
}
})
);
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
[{ pattern: '**' }],
{
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
let snippets = SNIPPETS_BY_LANGUAGE.get(document.languageId.toLowerCase());
if (!snippets) snippets = SNIPPETS_BY_LANGUAGE.get('all');
if (!snippets) return;
// When getCompletions returns an array it means no auto-expansion was matched for the
// current context, in this case show the snippet list to the user.
let completions = getCompletions(document, position, snippets);
if (completions && Array.isArray(completions)) {
return completions.map((c) => c.toCompletionItem());
}
},
},
...COMPLETIONS_TRIGGERS
)
);
}
================================================
FILE: src/hsnippet.ts
================================================
import { HSnippetUtils } from './hsnippetUtils';
export type GeneratorResult = [(string | { block: number })[], string[]];
export type GeneratorFunction = (
texts: string[],
matchGroups: string[],
workspaceUri: string,
fileUri: string,
hsnippetUtils: HSnippetUtils
) => GeneratorResult;
export interface ContextInfo {
scopes: string[];
}
export type ContextFilter = (context: ContextInfo) => boolean;
// Represents a snippet template from which new instances can be created.
export class HSnippet {
trigger: string;
description: string;
generator: GeneratorFunction;
contextFilter?: ContextFilter;
regexp?: RegExp;
priority: number;
// UltiSnips-like options.
automatic = false;
multiline = false;
inword = false;
wordboundary = false;
beginningofline = false;
constructor(
header: IHSnippetHeader,
generator: GeneratorFunction,
contextFilter?: ContextFilter
) {
this.description = header.description;
this.generator = generator;
this.contextFilter = contextFilter;
this.priority = header.priority || 0;
if (header.trigger instanceof RegExp) {
this.regexp = header.trigger;
this.trigger = '';
} else {
this.trigger = header.trigger;
}
if (header.flags.includes('A')) this.automatic = true;
if (header.flags.includes('M')) this.multiline = true;
if (header.flags.includes('i')) this.inword = true;
if (header.flags.includes('w')) this.wordboundary = true;
if (header.flags.includes('b')) this.beginningofline = true;
}
}
export interface IHSnippetHeader {
trigger: string | RegExp;
description: string;
flags: string;
priority?: number;
}
================================================
FILE: src/hsnippetInstance.ts
================================================
import * as vscode from 'vscode';
import { DynamicRange, GrowthType, IChangeInfo } from './dynamicRange';
import { applyOffset, getWorkspaceUri } from './utils';
import { HSnippet, GeneratorResult } from './hsnippet';
import { HSnippetUtils } from './hsnippetUtils';
enum HSnippetPartType {
Placeholder,
Block,
}
class HSnippetPart {
type: HSnippetPartType;
range: DynamicRange;
content: string;
id?: number;
updates: IChangeInfo[];
constructor(type: HSnippetPartType, range: DynamicRange, content: string, id?: number) {
this.type = type;
this.range = range;
this.content = content;
this.id = id;
this.updates = [];
}
updateRange() {
if (this.updates.length == 0) return;
this.range.update(this.updates);
this.updates = [];
}
}
export class HSnippetInstance {
type: HSnippet;
matchGroups: string[];
editor: vscode.TextEditor;
range: DynamicRange;
placeholderIds: number[];
selectedPlaceholder: number;
parts: HSnippetPart[];
blockParts: HSnippetPart[];
blockChanged: boolean;
snippetString: vscode.SnippetString;
constructor(
type: HSnippet,
editor: vscode.TextEditor,
position: vscode.Position,
matchGroups: string[]
) {
this.type = type;
this.editor = editor;
this.matchGroups = matchGroups;
this.selectedPlaceholder = 0;
this.placeholderIds = [];
this.blockChanged = false;
let generatorResult = this.runCodeBlocks(true);
// For a lack of creativity, I'm referring to the parts of the array that are returned by the
// snippet function as 'sections', and the result of the interpolated javascript in the snippets
// are referred to as 'blocks', as in code blocks.
let [sections, blocks] = generatorResult;
this.parts = [];
this.blockParts = [];
let start = position;
let snippetString = '';
const indentLevel = editor.document.lineAt(position.line).firstNonWhitespaceCharacterIndex;
for (let section of sections) {
let rawSection = section;
if (typeof rawSection != 'string') {
let block = blocks[rawSection.block];
let endPosition = applyOffset(position, block, indentLevel);
let range = new DynamicRange(position, endPosition);
let part = new HSnippetPart(HSnippetPartType.Block, range, block);
this.parts.push(part);
this.blockParts.push(part);
snippetString += block;
position = endPosition;
continue;
}
snippetString += rawSection;
// TODO: Handle snippets with default content in a placeholder.
let PLACEHOLDER_REGEX = /\$(\d+)|\$\{(\d+)\}/;
let match;
while ((match = PLACEHOLDER_REGEX.exec(rawSection))) {
let text = rawSection.substring(0, match.index);
position = applyOffset(position, text, indentLevel);
let range = new DynamicRange(position, position);
let placeholderId = Number(match[1] || match[2]);
if (!this.placeholderIds.includes(placeholderId)) this.placeholderIds.push(placeholderId);
this.parts.push(new HSnippetPart(HSnippetPartType.Placeholder, range, '', placeholderId));
rawSection = rawSection.substring(match.index + match[0].length);
}
position = applyOffset(position, rawSection, indentLevel);
}
this.snippetString = new vscode.SnippetString(snippetString);
this.range = new DynamicRange(start, position);
this.placeholderIds.sort();
if (this.placeholderIds[0] == 0) this.placeholderIds.shift();
this.placeholderIds.push(0);
this.selectedPlaceholder = this.placeholderIds[0];
}
runCodeBlocks(stripDollars = true, placeholderContents?: string[]) {
let generatorResult: GeneratorResult = [[], []];
let hsnippetUtils = new HSnippetUtils();
// TODO, update parser so only the block that threw the error does not expand, perhaps replace
// the block with the error message.
try {
generatorResult = this.type.generator(
new Proxy(placeholderContents || [], {
get(target, key) {
let index = Number(key);
if (target[index]) return target[index];
else return '';
},
}),
this.matchGroups,
getWorkspaceUri(),
this.editor.document.uri.toString(),
hsnippetUtils
);
} catch (e: unknown) {
if (e instanceof Error) {
vscode.window.showWarningMessage(
`Snippet ${this.type.description} failed to expand with error: ${e.message}`
);
}
}
generatorResult[1] = generatorResult[1].map((block) => {
if (stripDollars) {
block = block.replace(/\$/g, '\\$');
}
block = HSnippetUtils.format(block, hsnippetUtils);
return block;
});
return generatorResult;
}
nextPlaceholder() {
let currentIndex = this.placeholderIds.indexOf(this.selectedPlaceholder);
this.selectedPlaceholder = this.placeholderIds[currentIndex + 1];
return this.selectedPlaceholder != undefined && this.selectedPlaceholder != 0;
}
prevPlaceholder() {
let currentIndex = this.placeholderIds.indexOf(this.selectedPlaceholder);
this.selectedPlaceholder = this.placeholderIds[currentIndex - 1];
return this.selectedPlaceholder != undefined && this.selectedPlaceholder != 0;
}
debugLog() {
let parts = this.parts;
for (let i = 0; i < parts.length; i++) {
let range = parts[i].range.range;
let start = range.start;
let end = range.end;
console.log(
`Tabstop ${i}: "${parts[i].content}" (${start.line}, ${start.character})..(${end.line}, ${end.character})`
);
}
}
// Updates the location of all the placeholder blocks and code blocks, and if any change happened
// to the placeholder blocks then run the generator function again with the updated values so the
// code blocks are updated.
update(changes: readonly vscode.TextDocumentContentChangeEvent[]) {
let ordChanges = [...changes];
ordChanges.sort((a, b) => {
if (a.range.end.isBefore(b.range.end)) return -1;
else if (a.range.end.isEqual(b.range.end)) return 0;
else return 1;
});
let changedPlaceholders = [];
let currentPart = 0;
// Expand ranges from left to right, preserving relative part positions.
for (let change of ordChanges) {
if (!change) continue;
let part = this.parts[currentPart];
while (currentPart < this.parts.length) {
if (part.range.range.end.isAfterOrEqual(change.range.end)) {
break;
}
currentPart++;
part = this.parts[currentPart];
}
if (currentPart >= this.parts.length) break;
while (part.range.contains(change.range)) {
if (
(part.type == HSnippetPartType.Placeholder &&
part.id == this.selectedPlaceholder &&
!this.blockChanged) ||
(part.type == HSnippetPartType.Block && this.blockChanged && part.content == change.text)
) {
if (part.type == HSnippetPartType.Placeholder) changedPlaceholders.push(part);
part.updates.push({ change, growth: GrowthType.Grow });
currentPart++;
part = this.parts[currentPart];
break;
}
currentPart++;
part = this.parts[currentPart];
}
for (let i = currentPart; i < this.parts.length; i++) {
this.parts[i].updates.push({ change, growth: GrowthType.FixRight });
}
}
this.range.update(ordChanges.map((c) => ({ change: c, growth: GrowthType.Grow })));
this.parts.forEach((p) => p.updateRange());
if (this.blockChanged) this.blockChanged = false;
if (!changedPlaceholders.length) return;
changedPlaceholders.forEach((p) => (p.content = this.editor.document.getText(p.range.range)));
let placeholderContents = this.parts
.filter((p) => p.type == HSnippetPartType.Placeholder)
.map((p) => p.content);
let blocks = this.runCodeBlocks(false, placeholderContents)[1];
this.editor.edit((edit) => {
for (let i = 0; i < blocks.length; i++) {
let range = this.blockParts[i].range;
let oldContent = this.blockParts[i].content;
let content = blocks[i];
if (content != oldContent) {
edit.replace(range.range, content);
this.blockChanged = true;
}
}
});
this.blockParts.forEach((b, i) => (b.content = blocks[i]));
}
}
================================================
FILE: src/hsnippetUtils.ts
================================================
function makeId(length: number) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return `[${result}]`;
}
export class HSnippetUtils {
private placeholders: [string, string][];
constructor() {
this.placeholders = [];
}
tabstop(tabstop: number, placeholder?: string) {
const id = makeId(10);
let text = '';
if (placeholder) {
text = `\${${tabstop}:${placeholder}}`;
} else {
text = `$${tabstop}`;
}
this.placeholders.push([id, text]);
return id;
}
static format(value: string, utils: HSnippetUtils) {
for (let [id, text] of utils.placeholders) {
value = value.replace(new RegExp(`\\[${id.slice(1, -1)}\\]`, 'g'), text);
}
return value;
}
}
================================================
FILE: src/parser.ts
================================================
import { HSnippet, IHSnippetHeader, GeneratorFunction, ContextFilter } from './hsnippet';
const CODE_DELIMITER = '``';
const CODE_DELIMITER_REGEX = /``(?!`)/;
const HEADER_REGEXP = /^snippet ?(?:`([^`]+)`|(\S+))?(?: "([^"]+)")?(?: ([AMiwb]*))?/;
function parseSnippetHeader(header: string): IHSnippetHeader {
let match = HEADER_REGEXP.exec(header);
if (!match) throw new Error('Invalid snippet header');
let trigger: string | RegExp = match[2];
if (match[1]) {
if (!match[1].endsWith('$')) match[1] += '$';
trigger = new RegExp(match[1], 'm');
}
return {
trigger,
description: match[3] || '',
flags: match[4] || '',
};
}
interface IHSnippetInfo {
body: string;
contextFilter?: string;
header: IHSnippetHeader;
}
interface IHSnippetParseResult {
contextFilter?: ContextFilter;
generatorFunction: GeneratorFunction;
}
// First replacement handles backslash characters, as the string will be inserted using vscode's
// snippet engine, we should double down on every backslash, the second replacement handles double
// quotes, as our snippet will be transformed into a javascript string surrounded by double quotes.
function escapeString(string: string) {
return string.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
function parseSnippet(headerLine: string, lines: string[]): IHSnippetInfo {
let header = parseSnippetHeader(headerLine);
let script = [`(t, m, w, path, snip) => {`];
script.push(`let rv = "";`);
script.push(`let _result = [];`);
script.push(`let _blockResults = [];`);
let isCode = false;
while (lines.length > 0) {
let line = lines.shift() as string;
if (isCode) {
if (!line.includes(CODE_DELIMITER)) {
script.push(line.trim());
} else {
let [code, ...rest] = line.split(CODE_DELIMITER_REGEX);
script.push(code.trim());
lines.unshift(rest.join(CODE_DELIMITER));
script.push(`_result.push({block: _blockResults.length});`);
script.push(`_blockResults.push(String(rv));`);
isCode = false;
}
} else {
if (line.startsWith('endsnippet')) {
break;
} else if (!line.includes(CODE_DELIMITER)) {
script.push(`_result.push("${escapeString(line)}");`);
script.push(`_result.push("\\n");`);
} else if (isCode == false) {
let [text, ...rest] = line.split(CODE_DELIMITER_REGEX);
script.push(`_result.push("${escapeString(text)}");`);
script.push(`rv = "";`);
lines.unshift(rest.join(CODE_DELIMITER));
isCode = true;
}
}
}
// Remove extra newline at the end.
script.pop();
script.push(`return [_result, _blockResults];`);
script.push(`}`);
return { body: script.join('\n'), header };
}
// Transforms an hsnips file into a single function where the global context lives, every snippet is
// transformed into a local function inside this and the list of all snippet functions is returned
// so we can build the approppriate HSnippet objects.
export function parse(content: string): HSnippet[] {
let lines = content.split(/\r?\n/);
let snippetInfos = [];
let script = [];
let isCode = false;
let priority = 0;
let context = undefined;
while (lines.length > 0) {
let line = lines.shift() as string;
if (isCode) {
if (line.startsWith('endglobal')) {
isCode = false;
} else {
script.push(line);
}
} else if (line.startsWith('#')) {
continue;
} else if (line.startsWith('global')) {
isCode = true;
} else if (line.startsWith('priority ')) {
priority = Number(line.substring('priority '.length).trim()) || 0;
} else if (line.startsWith('context ')) {
context = line.substring('context '.length).trim() || undefined;
} else if (line.match(HEADER_REGEXP)) {
let info = parseSnippet(line, lines);
info.header.priority = priority;
info.contextFilter = context;
snippetInfos.push(info);
priority = 0;
context = undefined;
}
}
script.push(`return [`);
for (let snippet of snippetInfos) {
script.push('{');
if (snippet.contextFilter) {
script.push(`contextFilter: (context) => (${snippet.contextFilter}),`);
}
script.push(`generatorFunction: ${snippet.body}`);
script.push('},');
}
script.push(`]`);
// for some reason, `require` is not defined inside the snippet code blocks,
// so we're going to bind the it onto the function
let generators = new Function('require', script.join('\n'))(require) as IHSnippetParseResult[];
return snippetInfos.map(
(s, i) => new HSnippet(s.header, generators[i].generatorFunction, generators[i].contextFilter)
);
}
================================================
FILE: src/test/expansions/box.hsnips
================================================
snippet box "Box" A
┌``rv = '─'.repeat(t[0].length + 2)``┐
│ $1 │
└``rv = '─'.repeat(t[0].length + 2)``┘
endsnippet
================================================
FILE: src/test/expansions/box.input.txt
================================================
boxtest
================================================
FILE: src/test/expansions/box.output.txt
================================================
┌──────┐
│ test │
└──────┘
================================================
FILE: src/test/index.ts
================================================
================================================
FILE: src/utils.ts
================================================
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
export enum SnippetDirType {
Global,
Workspace,
}
export interface SnippetDirInfo {
readonly type: SnippetDirType;
readonly path: string;
}
/**
* "Expanding" here means turning a prefix string like '~' into a string like '/home/foo'
*/
const pathPrefixExpanders: {
readonly [prefix: string]: {
readonly finalPathType: SnippetDirType;
readonly prefixExpanderFunc: () => string | null;
};
} = {
'~': ({ finalPathType: SnippetDirType.Global, prefixExpanderFunc: os.homedir, }),
'${workspaceFolder}': ({ finalPathType: SnippetDirType.Workspace, prefixExpanderFunc: getWorkspaceFolderPath, }),
};
function getWorkspaceFolderPath(): string | null {
return vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || null;
}
export function lineRange(character: number, position: vscode.Position): vscode.Range {
return new vscode.Range(position.line, character, position.line, position.character);
}
/**
* The parameter `options`, can be removed after the function `getOldGlobalSnippetDir` is removed and migration from the
* directory to the new one is not necessary anymore.
*/
export function getSnippetDirInfo(
context: vscode.ExtensionContext,
options: { ignoreWorkspace: boolean } = { ignoreWorkspace: false },
): SnippetDirInfo {
let hsnipsPath = vscode.workspace.getConfiguration('hsnips').get('hsnipsPath') as string | null;
// only non-empty strings are taken, anything else is discarded
if (typeof hsnipsPath === 'string' && hsnipsPath.length > 0) {
// normalize to ensure that the correct platform-specific file separators are used
hsnipsPath = path.normalize(hsnipsPath);
let type: SnippetDirType | null = null;
// first some "preprocessing" is done on the configured path: expanding leading '~' and '${workspaceFolder}'
for (const prefix in pathPrefixExpanders) {
// a leading string like '~foo' is ignored, only '~' or '~/foo' values are taken
if (hsnipsPath !== prefix && !hsnipsPath.startsWith(prefix + path.sep)) {
continue;
}
const expandingInfo = pathPrefixExpanders[prefix];
if (options.ignoreWorkspace && expandingInfo.finalPathType == SnippetDirType.Workspace) {
// this expander would've resulted in a workspace folder path; skip it
continue;
}
const expandedPrefix = expandingInfo.prefixExpanderFunc();
if (expandedPrefix) {
hsnipsPath = expandedPrefix + hsnipsPath.substring(prefix.length);
type = expandingInfo.finalPathType;
} else {
// in case the prefix did match, but the expanded function wasn't able to properly expand, the entire path will
// be invalidated
// e.g.: given the string '~/foo', but the home directory could not be determined for some reason
hsnipsPath = null;
type = null;
}
break;
}
// this will only be falsy if the path was invalidated as a result of one of the expander functions failing to
// properly expanding a prefix
if (hsnipsPath) {
if (!options.ignoreWorkspace) {
const workspaceFolderPath = getWorkspaceFolderPath();
if (!path.isAbsolute(hsnipsPath) && workspaceFolderPath) {
hsnipsPath = path.join(workspaceFolderPath, hsnipsPath);
type = SnippetDirType.Workspace;
}
}
// at this point the path will only be relative in four cases:
// * an already relative path was configured without a matching prefix to expand
// * one of the expander functions messed up and returned a relative path
// * the function `getWorkspaceFolderPath` messed and returned a relative path
// * the path would've been a workspace path, but the parameter `ignoreWorkspace` is set to `true`
if (path.isAbsolute(hsnipsPath)) {
if (type === null) {
type = SnippetDirType.Global;
}
return {
type,
path: hsnipsPath,
};
}
}
}
const globalStoragePath = context.globalStorageUri.fsPath;
return {
type: SnippetDirType.Global,
path: path.join(globalStoragePath, 'hsnips'),
};
}
/**
* @deprecated The paths here are hardcoded in. Only keep this function so that older users can migrate.
*/
export function getOldGlobalSnippetDir(): string {
let hsnipsPath = vscode.workspace.getConfiguration('hsnips').get('hsnipsPath') as string | null;
if (hsnipsPath && path.isAbsolute(hsnipsPath)) {
return hsnipsPath;
}
let platform = os.platform();
let APPDATA = process.env.APPDATA || '';
let HOME = process.env.HOME || '';
if (platform == 'win32') {
return path.join(APPDATA, 'Code/User/hsnips');
} else if (platform == 'darwin') {
return path.join(HOME, 'Library/Application Support/Code/User/hsnips');
} else {
return path.join(HOME, '.config/Code/User/hsnips');
}
}
export function applyOffset(
position: vscode.Position,
text: string,
indent: number
): vscode.Position {
text = text.replace('\\$', '$');
let lines = text.split('\n');
let newLine = position.line + lines.length - 1;
let charOffset = lines[lines.length - 1].length;
let newChar = position.character + charOffset;
if (lines.length > 1) newChar = indent + charOffset;
return position.with(newLine, newChar);
}
export function getWorkspaceUri(): string {
return vscode.workspace.workspaceFolders?.[0]?.uri?.toString() ?? '';
}
================================================
FILE: syntaxes/hsnips.tmLanguage.json
================================================
{
"scopeName": "source.hsnips",
"name": "comment",
"patterns": [
{
"match": "^#.*",
"captures": {
"0": {
"name": "comment"
}
}
},
{
"begin": "^(snippet) ?(?:(`[^`]+`)|([\\S]+))?(?: (\"[^\"]+\"))?(?: ([AMiwb]*))?.*",
"beginCaptures": {
"1": {
"name": "keyword.control"
},
"2": {
"name": "entity.name.function"
},
"3": {
"name": "string.regexp"
},
"4": {
"name": "string.quoted.double"
},
"5": {
"name": "constant.language"
}
},
"end": "^endsnippet",
"endCaptures": {
"0": {
"name": "keyword.control"
}
},
"patterns": [
{
"include": "#snippet"
}
]
},
{
"begin": "^global",
"beginCaptures": {
"0": {
"name": "keyword.control"
}
},
"end": "^endglobal",
"endCaptures": {
"0": {
"name": "keyword.control"
}
},
"patterns": [
{
"include": "source.js"
}
]
},
{
"match": "^(priority) ?(-?\\d+)?",
"captures": {
"1": {
"name": "keyword.control"
},
"2": {
"name": "constant.numeric"
}
}
},
{
"match": "^(context)(?:(.*))",
"captures": {
"1": {
"name": "keyword.control"
},
"2": {
"patterns": [
{
"include": "source.js"
}
]
}
}
}
],
"repository": {
"snippet": {
"patterns": [
{
"contentName": "meta.embedded.js",
"begin": "``",
"beginCaptures": {
"0": {
"name": "string.interpolated"
}
},
"end": "``",
"endCaptures": {
"0": {
"name": "string.interpolated"
}
},
"patterns": [
{
"include": "source.js"
}
]
}
]
}
}
}
================================================
FILE: syntaxes/hsnips.tmLanguage.yaml
================================================
scopeName: 'source.hsnips'
name: comment
patterns:
- match: '^#.*'
captures:
0:
name: comment
- begin: '^(snippet) ?(?:(`[^`]+`)|([\S]+))?(?: ("[^"]+"))?(?: ([AMiwb]*))?.*'
beginCaptures:
1:
name: keyword.control
2:
name: entity.name.function
3:
name: string.regexp
4:
name: string.quoted.double
5:
name: constant.language
end: '^endsnippet'
endCaptures:
0:
name: keyword.control
patterns:
- include: '#snippet'
- begin: '^global'
beginCaptures:
0:
name: keyword.control
end: '^endglobal'
endCaptures:
0:
name: keyword.control
patterns:
- include: 'source.js'
- match: '^(priority) ?(-?\d+)?'
captures:
1:
name: keyword.control
2:
name: constant.numeric
- match: '^(context)(?: (.*))'
captures:
1:
name: keyword.control
2:
patterns:
- include: 'source.js'
repository:
snippet:
patterns:
- contentName: meta.embedded.js
begin: '``'
beginCaptures:
0:
name: string.interpolated
end: '``'
endCaptures:
0:
name: string.interpolated
patterns:
- include: 'source.js'
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"lib": ["ES2019"],
"outDir": "out",
"sourceMap": true,
"strict": true,
"rootDir": "src",
"allowJs": false
},
"exclude": ["node_modules", ".vscode-test"]
}
================================================
FILE: types/hscopes.d.ts
================================================
import * as vscode from 'vscode';
/**
* A grammar
*/
export interface IGrammar {
/**
* Tokenize `lineText` using previous line state `prevState`.
*/
tokenizeLine(lineText: string, prevState: StackElement | null): ITokenizeLineResult;
}
export interface ITokenizeLineResult {
readonly tokens: IToken[];
/**
* The `prevState` to be passed on to the next line tokenization.
*/
readonly ruleStack: StackElement;
}
export interface IToken {
startIndex: number;
readonly endIndex: number;
readonly scopes: string[];
}
export interface StackElement {
_stackElementBrand: void;
readonly depth: number;
clone(): StackElement;
equals(other: StackElement): boolean;
}
export interface Token {
range: vscode.Range;
text: string;
scopes: string[];
}
export interface HScopesAPI {
getScopeAt(document: vscode.TextDocument, position: vscode.Position): Token | null;
getGrammar(scopeName: string): Promise<IGrammar | null>;
getScopeForLanguage(language: string): string | null;
}
================================================
FILE: types/open-file-explorer.d.ts
================================================
declare module 'open-file-explorer' {
function openExplorer(path: string, callback?: (err: Error) => void): void;
export = openExplorer;
}
gitextract_wqaxspsi/
├── .eslintrc.js
├── .gitignore
├── .vscode/
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── LICENSE
├── README.md
├── language-configuration.json
├── package.json
├── src/
│ ├── completion.ts
│ ├── consts.ts
│ ├── dynamicRange.ts
│ ├── extension.ts
│ ├── hsnippet.ts
│ ├── hsnippetInstance.ts
│ ├── hsnippetUtils.ts
│ ├── parser.ts
│ ├── test/
│ │ ├── expansions/
│ │ │ ├── box.hsnips
│ │ │ ├── box.input.txt
│ │ │ └── box.output.txt
│ │ └── index.ts
│ └── utils.ts
├── syntaxes/
│ ├── hsnips.tmLanguage.json
│ └── hsnips.tmLanguage.yaml
├── tsconfig.json
└── types/
├── hscopes.d.ts
└── open-file-explorer.d.ts
SYMBOL INDEX (66 symbols across 10 files)
FILE: src/completion.ts
class CompletionInfo (line 5) | class CompletionInfo {
method constructor (line 12) | constructor(snippet: HSnippet, label: string, range: vscode.Range, gro...
method toCompletionItem (line 20) | toCompletionItem() {
function matchSuffixPrefix (line 35) | function matchSuffixPrefix(context: string, trigger: string) {
function getCompletions (line 44) | function getCompletions(
FILE: src/consts.ts
constant COMPLETIONS_TRIGGERS (line 1) | const COMPLETIONS_TRIGGERS = [
FILE: src/dynamicRange.ts
type PositionDelta (line 3) | type PositionDelta = { characterDelta: number; lineDelta: number };
type GrowthType (line 5) | enum GrowthType {
type IChangeInfo (line 11) | interface IChangeInfo {
function getRangeDelta (line 16) | function getRangeDelta(
class DynamicRange (line 57) | class DynamicRange {
method constructor (line 60) | constructor(start: vscode.Position, end: vscode.Position) {
method fromRange (line 64) | static fromRange(range: vscode.Range) {
method update (line 68) | update(changes: IChangeInfo[]) {
method contains (line 87) | contains(range: vscode.Range): boolean {
FILE: src/extension.ts
constant SNIPPETS_BY_LANGUAGE (line 12) | const SNIPPETS_BY_LANGUAGE: Map<string, HSnippet[]> = new Map();
constant SNIPPET_STACK (line 13) | const SNIPPET_STACK: HSnippetInstance[] = [];
function loadSnippets (line 17) | async function loadSnippets(context: vscode.ExtensionContext) {
function expandSnippet (line 59) | async function expandSnippet(
function activate (line 96) | function activate(context: vscode.ExtensionContext) {
FILE: src/hsnippet.ts
type GeneratorResult (line 3) | type GeneratorResult = [(string | { block: number })[], string[]];
type GeneratorFunction (line 4) | type GeneratorFunction = (
type ContextInfo (line 12) | interface ContextInfo {
type ContextFilter (line 16) | type ContextFilter = (context: ContextInfo) => boolean;
class HSnippet (line 19) | class HSnippet {
method constructor (line 34) | constructor(
type IHSnippetHeader (line 59) | interface IHSnippetHeader {
FILE: src/hsnippetInstance.ts
type HSnippetPartType (line 7) | enum HSnippetPartType {
class HSnippetPart (line 12) | class HSnippetPart {
method constructor (line 19) | constructor(type: HSnippetPartType, range: DynamicRange, content: stri...
method updateRange (line 27) | updateRange() {
class HSnippetInstance (line 34) | class HSnippetInstance {
method constructor (line 46) | constructor(
method runCodeBlocks (line 119) | runCodeBlocks(stripDollars = true, placeholderContents?: string[]) {
method nextPlaceholder (line 160) | nextPlaceholder() {
method prevPlaceholder (line 166) | prevPlaceholder() {
method debugLog (line 172) | debugLog() {
method update (line 187) | update(changes: readonly vscode.TextDocumentContentChangeEvent[]) {
FILE: src/hsnippetUtils.ts
function makeId (line 1) | function makeId(length: number) {
class HSnippetUtils (line 10) | class HSnippetUtils {
method constructor (line 13) | constructor() {
method tabstop (line 17) | tabstop(tabstop: number, placeholder?: string) {
method format (line 31) | static format(value: string, utils: HSnippetUtils) {
FILE: src/parser.ts
constant CODE_DELIMITER (line 3) | const CODE_DELIMITER = '``';
constant CODE_DELIMITER_REGEX (line 4) | const CODE_DELIMITER_REGEX = /``(?!`)/;
constant HEADER_REGEXP (line 5) | const HEADER_REGEXP = /^snippet ?(?:`([^`]+)`|(\S+))?(?: "([^"]+)")?(?: ...
function parseSnippetHeader (line 7) | function parseSnippetHeader(header: string): IHSnippetHeader {
type IHSnippetInfo (line 24) | interface IHSnippetInfo {
type IHSnippetParseResult (line 30) | interface IHSnippetParseResult {
function escapeString (line 38) | function escapeString(string: string) {
function parseSnippet (line 42) | function parseSnippet(headerLine: string, lines: string[]): IHSnippetInfo {
function parse (line 93) | function parse(content: string): HSnippet[] {
FILE: src/utils.ts
type SnippetDirType (line 5) | enum SnippetDirType {
type SnippetDirInfo (line 10) | interface SnippetDirInfo {
function getWorkspaceFolderPath (line 28) | function getWorkspaceFolderPath(): string | null {
function lineRange (line 32) | function lineRange(character: number, position: vscode.Position): vscode...
function getSnippetDirInfo (line 40) | function getSnippetDirInfo(
function getOldGlobalSnippetDir (line 122) | function getOldGlobalSnippetDir(): string {
function applyOffset (line 143) | function applyOffset(
function getWorkspaceUri (line 159) | function getWorkspaceUri(): string {
FILE: types/hscopes.d.ts
type IGrammar (line 7) | interface IGrammar {
type ITokenizeLineResult (line 14) | interface ITokenizeLineResult {
type IToken (line 22) | interface IToken {
type StackElement (line 28) | interface StackElement {
type Token (line 35) | interface Token {
type HScopesAPI (line 41) | interface HScopesAPI {
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (60K chars).
[
{
"path": ".eslintrc.js",
"chars": 450,
"preview": "module.exports = {\n root: true,\n parser: '@typescript-eslint/parser',\n plugins: ['@typescript-eslint'],\n extends: ['"
},
{
"path": ".gitignore",
"chars": 24,
"preview": "out\nnode_modules\n*.vsix\n"
},
{
"path": ".vscode/launch.json",
"chars": 621,
"preview": "// A launch configuration that compiles the extension and then opens it inside a new window\n// Use IntelliSense to learn"
},
{
"path": ".vscode/settings.json",
"chars": 98,
"preview": "{\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",
"chars": 365,
"preview": "// See https://go.microsoft.com/fwlink/?LinkId=733558\n// for the documentation about the tasks.json format\n{\n\t\"version\":"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2019 Ian Ornelas\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 6055,
"preview": "# HyperSnips\n\n\n\nHyperSnips is a snippet engine for vscode heavily inspired by vim's\n[UltiSnips]"
},
{
"path": "language-configuration.json",
"chars": 150,
"preview": "{\n \"comments\": {\n \"lineComment\": \"#\"\n },\n \"folding\": {\n \"markers\": {\n \"start\": \"^snippet\\\\b\",\n \"end\":"
},
{
"path": "package.json",
"chars": 3929,
"preview": "{\n \"name\": \"hsnips\",\n \"displayName\": \"HyperSnips\",\n \"icon\": \"images/hypersnips.png\",\n \"version\": \"0.2.9\",\n "
},
{
"path": "src/completion.ts",
"chars": 5169,
"preview": "import * as vscode from 'vscode';\nimport { lineRange } from './utils';\nimport { HSnippet } from './hsnippet';\n\nexport cl"
},
{
"path": "src/consts.ts",
"chars": 238,
"preview": "export const COMPLETIONS_TRIGGERS = [\n ' ',\n '.',\n '(',\n ')',\n '{',\n '}',\n '[',\n ']',\n ',',\n ':',\n \"'\",\n '\"'"
},
{
"path": "src/dynamicRange.ts",
"chars": 2618,
"preview": "import * as vscode from 'vscode';\n\ntype PositionDelta = { characterDelta: number; lineDelta: number };\n\nexport enum Grow"
},
{
"path": "src/extension.ts",
"chars": 8643,
"preview": "import * as vscode from 'vscode';\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from 'fs';\nimp"
},
{
"path": "src/hsnippet.ts",
"chars": 1675,
"preview": "import { HSnippetUtils } from './hsnippetUtils';\n\nexport type GeneratorResult = [(string | { block: number })[], string["
},
{
"path": "src/hsnippetInstance.ts",
"chars": 8449,
"preview": "import * as vscode from 'vscode';\nimport { DynamicRange, GrowthType, IChangeInfo } from './dynamicRange';\nimport { apply"
},
{
"path": "src/hsnippetUtils.ts",
"chars": 903,
"preview": "function makeId(length: number) {\n let result = '';\n const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs"
},
{
"path": "src/parser.ts",
"chars": 4694,
"preview": "import { HSnippet, IHSnippetHeader, GeneratorFunction, ContextFilter } from './hsnippet';\n\nconst CODE_DELIMITER = '``';\n"
},
{
"path": "src/test/expansions/box.hsnips",
"chars": 116,
"preview": "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",
"chars": 8,
"preview": "boxtest\n"
},
{
"path": "src/test/expansions/box.output.txt",
"chars": 27,
"preview": "┌──────┐\n│ test │\n└──────┘\n"
},
{
"path": "src/test/index.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/utils.ts",
"chars": 5486,
"preview": "import * as vscode from 'vscode';\nimport * as path from 'path';\nimport * as os from 'os';\n\nexport enum SnippetDirType {\n"
},
{
"path": "syntaxes/hsnips.tmLanguage.json",
"chars": 2181,
"preview": "{\n \"scopeName\": \"source.hsnips\",\n \"name\": \"comment\",\n \"patterns\": [\n {\n \"match\": \"^#.*\",\n \"captures\": {\n"
},
{
"path": "syntaxes/hsnips.tmLanguage.yaml",
"chars": 1330,
"preview": "scopeName: 'source.hsnips'\n\nname: comment\npatterns:\n - match: '^#.*'\n captures:\n 0:\n name: comment\n\n - "
},
{
"path": "tsconfig.json",
"chars": 258,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"target\": \"es2019\",\n \"lib\": [\"ES2019\"],\n \"outDir\": \"out\",\n "
},
{
"path": "types/hscopes.d.ts",
"chars": 1021,
"preview": "import * as vscode from 'vscode';\n\n/**\n * A grammar\n */\n\nexport interface IGrammar {\n /**\n * Tokenize `lineText` usin"
},
{
"path": "types/open-file-explorer.d.ts",
"chars": 143,
"preview": "declare module 'open-file-explorer' {\n function openExplorer(path: string, callback?: (err: Error) => void): void;\n ex"
}
]
About this extraction
This page contains the full source code of the draivin/hsnips GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (54.4 KB), approximately 14.1k tokens, and a symbol index with 66 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.