Skip to content

Commit

Permalink
Merge pull request #21 from savetheclocktower/add-comment-delimiter-v…
Browse files Browse the repository at this point in the history
…ariables

Add support for variables `LINE_COMMENT`, `BLOCK_COMMENT_START`…
  • Loading branch information
savetheclocktower committed Apr 14, 2024
2 parents 6b91634 + 11eb3cf commit 6aa1607
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 31 deletions.
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

0 comments on commit 6aa1607

Please sign in to comment.