Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for variables LINE_COMMENT, BLOCK_COMMENT_START #21

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ The following features from VSCode snippets are not yet supported:

Pulsar snippets support all of the variables mentioned in the [LSP specification][lsp], plus many of the variables [supported by VSCode][vscode].

Variables can be referenced with `$`, either without braces (`$CLIPBOARD`) or with braces (`${CLIPBOARD}`). Variables can also have fallback values (`${CLIPBOARD:http://example.com}`), simple flag-based transformations (`${CLIPBOARD:/upcase}`), or `sed`-style transformations `${CLIPBOARD/ /_/g}`.
Variables can be referenced with `$`, either without braces (`$CLIPBOARD`) or with braces (`${CLIPBOARD}`). Variables can also have fallback values (`${CLIPBOARD:http://example.com}`), simple flag-based transformations (`${CLIPBOARD:/upcase}`), or `sed`-style transformations (`${CLIPBOARD/ /_/g}`).

One of the most useful is `TM_SELECTED_TEXT`, which represents whatever text was selected when the snippet was invoked. (Naturally, this can only happen when a snippet is invoked via command or key shortcut, rather than by typing in a <kbd>Tab</kbd> trigger.)

Expand All @@ -118,8 +118,9 @@ Others that can be useful:
* `TM_CURRENT_WORD`: The entire word that the cursor is within or adjacent to, as interpreted by `cursor.getCurrentWordBufferRange`.
* `CLIPBOARD`: The current contents of the clipboard.
* `CURRENT_YEAR`, `CURRENT_MONTH`, et cetera: referneces to the current date and time in various formats.
* `LINE_COMMENT`, `BLOCK_COMMENT_START`, `BLOCK_COMMENT_END`: uses the correct comment delimiters for whatever language you’re in.

Any variable that has no value — for instance, `TM_FILENAME` on an untitled document — will resolve to an empty string.
Any variable that has no value — for instance, `TM_FILENAME` on an untitled document, or `LINE_COMMENT` in a CSS file — will resolve to an empty string.

#### Variable transformation flags

Expand All @@ -140,10 +141,7 @@ Pulsar supports the three flags defined in the [LSP snippets specification][lsp]

Of the variables supported by VSCode, Pulsar does not yet support:

* `UUID`
* `BLOCK_COMMENT_START`
* `BLOCK_COMMENT_END`
* `LINE_COMMENT`
* `UUID` (Will automatically be supported when Pulsar uses a version of Electron that has native `crypto.randomUUID`.)

## Multi-line Snippet Body

Expand Down
29 changes: 21 additions & 8 deletions lib/variable.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,31 @@ const RESOLVERS = {

'RANDOM_HEX' () {
return Math.random().toString(16).slice(-6)
},

'BLOCK_COMMENT_START' ({editor, cursor}) {
let delimiters = editor.getCommentDelimitersForBufferPosition(
cursor.getBufferPosition()
)
return (delimiters?.block?.[0] ?? '').trim()
},

'BLOCK_COMMENT_END' ({editor, cursor}) {
let delimiters = editor.getCommentDelimitersForBufferPosition(
cursor.getBufferPosition()
)
return (delimiters?.block?.[1] ?? '').trim()
},

'LINE_COMMENT' ({editor, cursor}) {
let delimiters = editor.getCommentDelimitersForBufferPosition(
cursor.getBufferPosition()
)
return (delimiters?.line ?? '').trim()
}

// TODO: VSCode also supports:
//
// BLOCK_COMMENT_START
// BLOCK_COMMENT_END
// LINE_COMMENT
//
// (grammars don't provide this information right now; see
// https://github.com/atom/atom/pull/18816)
//
// UUID
//
// (can be done without dependencies once we use Node >= 14.17.0 or >=
Expand Down Expand Up @@ -243,7 +257,6 @@ class Variable {
// This is the more complex sed-style substitution.
let {find, replace} = this.substitution
this.replacer ??= new Replacer(replace)
let matches = base.match(find)
return base.replace(find, (...args) => {
return this.replacer.replace(...args)
})
Expand Down
126 changes: 109 additions & 17 deletions spec/snippets-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ const path = require('path');
const temp = require('temp').track();
const Snippets = require('../lib/snippets');
const {TextEditor} = require('atom');
const crypto = require('crypto');

const SUPPORTS_UUID = ('randomUUID' in crypto) && (typeof crypto.randomUUID === 'function');

describe("Snippets extension", () => {
let editorElement, editor, languageMode;
Expand Down Expand Up @@ -32,6 +35,7 @@ describe("Snippets extension", () => {

await atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.js'));
await atom.packages.activatePackage('language-javascript');
await atom.packages.activatePackage('language-python');
await atom.packages.activatePackage('language-html');
await atom.packages.activatePackage('snippets');

Expand Down Expand Up @@ -351,6 +355,15 @@ third tabstop $3\
}
}
});

Snippets.add(__filename, {
".source, .text": {
"banner with generic comment delimiters": {
prefix: "bannerGeneric",
body: "$LINE_COMMENT $1\n$LINE_COMMENT ${1/./=/g}"
}
}
});
});

it("parses snippets once, reusing cached ones on subsequent queries", () => {
Expand Down Expand Up @@ -992,6 +1005,50 @@ foo\
});
});

describe("when the snippet contains generic line comment delimiter variables", () => {
describe("and the document is JavaScript", () => {
it("uses the right delimiters", () => {
editor.setText('bannerGeneric');
editor.setCursorScreenPosition([0, 13]);
simulateTabKeyEvent();
expect(editor.getText()).toBe("// \n// ");
editor.insertText('TEST');
expect(editor.getText()).toBe("// TEST\n// ====");
});
});

describe("and the document is HTML", () => {
beforeEach(() => {
atom.grammars.assignLanguageMode(editor, 'text.html.basic');
editor.setText('');
});

it("falls back to an empty string, for HTML has no line comment", () => {
editor.setText('bannerGeneric');
editor.setCursorScreenPosition([0, 13]);
simulateTabKeyEvent();
expect(editor.getText()).toBe(" \n ");
editor.insertText('TEST');
expect(editor.getText()).toBe(" TEST\n ====");
});
});

describe("and the document is Python", () => {
beforeEach(() => {
atom.grammars.assignLanguageMode(editor, 'source.python');
editor.setText('');
});
it("uses the right delimiters", () => {
editor.setText('bannerGeneric');
editor.setCursorScreenPosition([0, 13]);
simulateTabKeyEvent();
expect(editor.getText()).toBe("# \n# ");
editor.insertText('TEST');
expect(editor.getText()).toBe("# TEST\n# ====");
});
});
});

describe("when the snippet contains multiple tab stops, some with transformations and some without", () => {
it("does not get confused", () => {
editor.setText('t14');
Expand Down Expand Up @@ -1468,6 +1525,12 @@ foo\
command: "some-python-command-snippet"
}
},
".source, .text": {
"wrap in block comment": {
body: "$BLOCK_COMMENT_START $TM_SELECTED_TEXT ${BLOCK_COMMENT_END}${0}",
command: 'wrap-in-block-comment'
}
},
".text.html": {
"wrap in tag": {
"command": "wrap-in-html-tag",
Expand Down Expand Up @@ -1532,6 +1595,13 @@ foo\
expect(editor.getText()).toBe("");
});

it("uses language-specific comment delimiters", () => {
editor.setText("something");
editor.selectAll();
atom.commands.dispatch(editor.element, 'snippets:wrap-in-block-comment');
expect(editor.getText()).toBe("/* something */");
});

});

describe("and the command is invoked in an HTML document", () => {
Expand All @@ -1553,6 +1623,29 @@ foo\
simulateTabKeyEvent();
expect(cursor.getBufferPosition()).toEqual([0, 19]);
});

it("uses language-specific comment delimiters", () => {
editor.setText("something");
editor.selectAll();
atom.commands.dispatch(editor.element, 'snippets:wrap-in-block-comment');
expect(editor.getText()).toBe("<!-- something -->");
});

});

describe("and the command is invoked in a Python document", () => {
beforeEach(() => {
atom.grammars.assignLanguageMode(editor, 'source.python');
editor.setText('');
});

it("uses language-specific comment delimiters, or empty strings if those delimiters don't exist in Python", () => {
editor.setText("something");
editor.selectAll();
atom.commands.dispatch(editor.element, 'snippets:wrap-in-block-comment');
expect(editor.getText()).toBe(" something ");
});

});
});

Expand Down Expand Up @@ -1739,23 +1832,22 @@ foo\
expect(reRandomHex.test(randomHex2)).toBe(true);
expect(randomHex2).not.toEqual(randomHex1);

// TODO: These tests are commented out because we won't support UUID
// until we use a version of Node that implements `crypto.randomUUID`.
// It's not crucial enough to require a new external dependency in the
// meantime.

// editor.setText('');
// editor.insertText('rndmuuid');
// simulateTabKeyEvent();
// let randomUUID1 = editor.lineTextForBufferRow(1);
// expect(reUUID.test(randomUUID1)).toBe(true);
//
// editor.setText('');
// editor.insertText('rndmuuid');
// simulateTabKeyEvent();
// let randomUUID2 = editor.lineTextForBufferRow(1);
// expect(reUUID.test(randomUUID2)).toBe(true);
// expect(randomUUID2).not.toEqual(randomUUID1);
// TODO: These tests will start running when we use a version of Electron
// that supports `crypto.randomUUID`.
if (SUPPORTS_UUID) {
editor.setText('');
editor.insertText('rndmuuid');
simulateTabKeyEvent();
let randomUUID1 = editor.lineTextForBufferRow(1);
expect(reUUID.test(randomUUID1)).toBe(true);

editor.setText('');
editor.insertText('rndmuuid');
simulateTabKeyEvent();
let randomUUID2 = editor.lineTextForBufferRow(1);
expect(reUUID.test(randomUUID2)).toBe(true);
expect(randomUUID2).not.toEqual(randomUUID1);
}
});

describe("and the command is invoked in an HTML document", () => {
Expand Down
Loading