-
Notifications
You must be signed in to change notification settings - Fork 107
Add support for simple regex transformations in tab stops. #260
Conversation
…so we can consolidate history entries across multiple expansions. Add tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aside from some minor changes, this is looking really excellent. Solid test coverage, well thought out design, a great PR description, and a super substantial contribution. This PR was a pleasure to review.
I plan on testing this out tomorrow morning. @ungb or @Ben3eeE, either of your help breaking this would be appreciated.
spec/snippets-spec.coffee
Outdated
@@ -608,6 +634,7 @@ describe "Snippets extension", -> | |||
editor.insertText('t9b') | |||
simulateTabKeyEvent() | |||
editor.getCursors()[0].destroy() | |||
buf = editor.getCursorBufferPosition() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed?
lib/tab-stop-list.js
Outdated
@@ -0,0 +1,46 @@ | |||
/** @babel */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to avoid using Babel on new files. Everything except the fancy import
syntax works fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, was unclear on what the best practice was for new files.
|
||
# Create a checkpoint here to consolidate all the changes we just made into | ||
# the transaction that prompted them. | ||
@makeCheckpoint(editor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading this right, you're doing this to group any transformations with a previous transaction because the transaction is already closed by the time you receive the event. But I'm confused as to how the initial checkpoint preceding any changes gets populated.
Incidentally, in 1.25 we're adding a groupLastChanges
method to the buffer that lets you combine the last two transactions on the undo stack, which might make this easier. But you can't really use it until Atom 1.25 hits stable without breaking this package's tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Working off of recollection here, but I think this is the deal:
The initial snippet expansion is easy because it's invoking a command. Everything that we do in response to that command is automatically atomic because of the TextEditor#transact
call.
The checkpoint logic is needed only for typing that happens while a snippet is active but not in response to a command invocation. Consider: If I have a snippet like $1\n${1/./\\u$1}
(i.e., transforming what I type to all-caps on the following line):
- I type its abbreviation and expand the snippet.
- I type
a
. - Atom inserts
A
on the following line.
We're observing text editor changes, so we act in between steps 2 and 3, but we have to bundle the changes of steps 2 and 3 into one atom — i.e., we have to create a transaction that includes something that already happened. The way I arrived at to do this was to make the checkpoint after the initial snippet expansion and then make a new one each time we did step 3. And right before I make a new checkpoint I bundle everything together that happened since the last checkpoint.
This took a lot of time to reason through. I wrote a lot of tests around this precisely because I suspected I didn't understand it correctly.
It does sound like groupLastChanges
would make this easier — it would theoretically allow me to toss out checkpoints altogether and duct-tape the changes i'm making with the user action that prompted them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After re-reading your question, I'm pretty sure all I did was restate the part that you already understood. The shorter answer to your question is “I don't know.” I’ll investigate when I get a chance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way I arrived at to do this was to make the checkpoint after the initial snippet expansion and then make a new one each time we did step 3
Actually I think this answered my question perfectly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, after my long comment I skimmed the source again and can't 100% guarantee that the code does what I claimed it did in the part you quoted. 🤷♂️ I can't find the part where I make the initial checkpoint; the makeCheckpoint
method seems like it only gets called after we set up the listener, not on initial snippet expansion.
@@ -298,6 +299,13 @@ module.exports = | |||
expandSnippetsUnderCursors: (editor) -> | |||
return false unless snippet = @snippetToExpandUnderCursor(editor) | |||
|
|||
@getStore(editor).observeHistory({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should really supply a flag in the change event indicating when the change is coming from an undo
or redo
. If you'd have any interest in adding that, I'd be happy to shepherd it. Then we could simplify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that’s certainly a better idea. I’ll investigate.
spec/insertion-spec.js
Outdated
const range = new Range(0, 0); | ||
|
||
describe('Insertion', () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super minor, but noticing some extra newlines in the file such as this one.
lib/editor-store.coffee
Outdated
@@ -0,0 +1,63 @@ | |||
SnippetHistoryProvider = require './snippet-history-provider' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be cool doing this file in ordinary JS?
I went ahead and made the changes myself to save you the trouble since it's been a while since you opened this.
Published as snippets@1.2.0. Thanks for the great work adding this. You combined a bunch of APIs in a pretty complex piece of code to add a really cool feature. I've gone ahead and given you commit bit on this repository ⚡️. |
Thanks! I'll revisit this when 1.25 is out, and the undo/redo thing will be on my radar as well. |
Forgive my misunderstanding but, is this actually available in non-beta releases? I see it's published in snippets@1.2.0 (current is 1.3.3). |
Yeah, it went out with Atom 1.25.0. |
That's great news! Looks fantastic. I'll dig around in the source a bit to see how to use it. Thanks! |
Note: this is my first deep-dive into adding an Atom feature and I’m happy to get pushback on code formatting, architectural choices, and the like.
Description of the Change
This implements a slimmed-down version of snippet transformations. TextMate’s version of this feature is documented here.
For example, this snippet:
can be used like this:
Design
I went through a bunch of possible ways to design this. Here’s what I landed on:
pegjs
file so that we can parse the new syntax.Insertion
, which belongs to a tab stop and can either mirror or transform its input.Worth noting
Mirrored tab stops have always been treated like multiple selections…
…and that doesn’t change. But transformed tab stops are not treated like selections, because I think a user wouldn’t reasonably expect the same input to produce different output when applied to multiple selections.
Transformed tab stops cannot have placeholders; the first transformation gets applied as soon as the snippet is expanded, so any placeholder value would likely be lost immediately.
What is and isn't included
As explained in this comment, I wanted to go minimal here. The find-and-replace syntax roughly corresponds to the two arguments you'd pass to
String#replace
(the regex is automatically given the global flag). There's one extra feature that I brought over from TextMate format strings: the ability to control case through the\u
,\l
,\U
,\L
, and\E
flags:\u
and\l
will force the next character to uppercase or lowercase, respectively.\U
and\L
will force the rest of the string to uppercase or lowercase, respectively (or until we encounter a different case directive or\E
).\E
stops the effects of\U
and\L
.There are lots of other features of TextMate format strings, but a lot of them are obscure. I went with this small extension because it's quite useful and is also supported by Sublime Text's snippet transformation syntax. Other features, like conditional insertions, could be added rather easily in the future just by updating the parser and the
Insertion
class.Alternate Designs
I don’t like how a snippet expansion needs to listen to
TextBuffer
changes in order to do transformations, but I didn’t see another way. An asynchronous listener doesn’t act quickly enough. But we do listen at the transaction level (TextBuffer#onDidChangeText
) rather than the keystroke level (TextEditor#onDidChange
).If there is a simpler way to do this, I’m all for it.
Benefits
Makes snippets far more useful. There are a couple more examples in the (duplicate) ticket that I opened.
Possible Drawbacks
Not sure. Increased code complexity, perhaps?
This PR doesn't change any existing behavior — barely any of the existing tests even needed to be touched — and the theoretical performance penalty of observing
TextBuffer
transactions should only be encountered when using a snippet with a transformation; for existing use cases there should be no performance regression.Applicable Issues
Ticket #40 contains the original request.