Customising Extend Selection

I'd like to customise the way that Select/UnselectWordAtCaret work. In Clojure, Extend Selection is really great but it would be very useful to be able to extend the selection laterally to sibling forms too. The Clojure environment for Eclipse, CounterClockwise, supports this and it's really useful. The way this works there is that Alt-Up extends the selection as in IntelliJ, Alt-Left/Right extend it laterally, and Alt-Down undoes the last selection expansion. I'd like to mimic this behaviour for Clojure.

My plan is to add EditorActionHandlers for Select/UnselectWordAtCaret with priority first. These will delegate as normal when not editing Clojure. I'll also add new actions for ExtendSelectionLeft/Right. After these actions (extend left/right will use custom code, SelectWordAtCaret will delegate to the existing action) I'll push the new selections on a stack and store it somewhere. My UnselectWordAtCaret will simply pop the stack and return the selections to a previous state. I'll also add a SelectionListener, and invalidate the cache if the selection has been removed via editing, cutting or whatever.

  1. Does this seem like a valid way to achieve this?
  2. If so, where should I store the stack - on the editor? I'm guessing that the editor makes more sense than doing it on the PsiFile.
  3. In the listener, should I invalidate the stack when the number of new selections is zero, or just when it's less than the number of old selections?
  4. Are there any edge cases around multiple cursors I should be aware of here?
  5. My stack will have a collection of selection TextRanges, one per caret - should I use the Caret instance to associate the selections with a particular Caret, and check if it's valid before restoring the selection? They don't seem to have any sort of ID.


Thanks for any advice!

10 comments
Comment actions Permalink

Oh, one more question - should I develop the new actions as TextComponentEditorActions with EditorActionHandlers? What is the advantage to doing this over a normal action?

0
Comment actions Permalink

I tried implementing this to see how it works. However I ran into an issue - to implement this functionality as described, I need to be able to execute some code after the whole action is run, not the per-caret part. I cannot see a way to do this, since EditorActionHandler#execute(Editor, Caret, DataContext) is final. Is there any way to do this apart from totally rewriting the actions?

0
Comment actions Permalink

1. Sounds fine to me.
2. Associating stack data with editor makes total sense.
3. I'd also consider an option of invalidating the cache on any selection change not originating from your handlers.
4. Just be aware that some carets can be merged after selection manipulations, as different carets are not allowed to have overlapping selections.
5. Caret instances, as returned by CaretModel.getAllCarets(), are always sorted in visual order. CaretModel.runForEachCaret() also processes carets in that order. You can also associate any information with caret instances, as Caret extends UserDataHolderEx.

0
Comment actions Permalink

TextComponentEditorAction instances, as opposed to EditorAction instances, will also work for JTextComponent fields, adapting them to Editor interface, using TextComponentEditor class.

0
Comment actions Permalink

Just don't make your EditorActionHandler a per-caret handler, i.e. don't pass runForEachCaret=true to EditorActionHander constructor. Your handler will only be invoked once then, and you can customize its logic as you wish, including delegation to the rest of handler chain. You'll just need to invoke CaretModel.runForEachCaret() or implement iteration over carets returned by CaretModel.getAllCarets() yourselves in your hander.

0
Comment actions Permalink

Fantastic Dmitry, thanks for the great info!

For #3, do you have a suggestion for how to detect changes which don't originate from my handlers? I can't see anything in SelectionEvent that would help me detect that, except for perhaps comparing the new selection to the top of the stack.

0
Comment actions Permalink

As handlers operate synchronously (always in EDT), you can just set some flag on handler start, clear it on finish, and check for flag value in your selection listener.

0
Comment actions Permalink

Oh nice, so the selection listener is called synchronously inside the handler? That should make things much easier, thanks.

The caret merging thing is tricky. I'm wondering if I should just store and restore the entire caret and selection state using CaretModel/get/setCaretsAndSelections? The potential problem there is if the user has managed to move carets while maintaining the selection - is this possible? If this is possible, I'll have to store the selections and associate them with the Caret instance, and just not restore any selections for carets which no longer exist or are no longer valid. It seems like I'll have to use the Caret instance as a map key in this instance, or store a key in the Caret's user data and use that.

0
Comment actions Permalink
... so the selection listener is called synchronously inside the handler? ...

Yes.

.. The potential problem there is if the user has managed to move carets while maintaining the selection - is this possible? ...

Theoretically, yes (even though it's a very rare case). But you can listen to caret movements just like you listen to selection changes, and invalidate cache in that case.

0
Comment actions Permalink

Ok, if it's a very rare case I don't think I'll worry about it - the user will just get a caret movement they wouldn't expect in the edge case where they move the carets and then undo the selection action.

0

Please sign in to leave a comment.