From 11eb3cf1288c00d500107541c0e66fb3ace3766d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 9 Apr 2024 20:51:45 -0700 Subject: [PATCH] =?UTF-8?q?Add=20support=20for=20variables=20`LINE=5FCOMME?= =?UTF-8?q?NT`,=20`BLOCK=5FCOMMENT=5FSTART`=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and `BLOCK_COMMENT_END`. --- README.md | 10 ++-- lib/variable.js | 29 +++++++--- spec/snippets-spec.js | 126 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 134 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7d80e87..4693abc 100644 --- a/README.md +++ b/README.md @@ -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 Tab trigger.) @@ -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 @@ -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 diff --git a/lib/variable.js b/lib/variable.js index 9d43a38..a68731a 100644 --- a/lib/variable.js +++ b/lib/variable.js @@ -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 >= @@ -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) }) diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 6d09ab4..5adc137 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -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; @@ -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'); @@ -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", () => { @@ -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'); @@ -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", @@ -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", () => { @@ -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(""); + }); + + }); + + 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 "); + }); + }); }); @@ -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", () => {