Skip to content
cgrand edited this page Oct 8, 2014 · 33 revisions

This document is a proposal for a more strict structural editor. The term structedit is used instead of paredit because it does not try to emulate paredit.

In examples, vertical bars | are used to delimit selection, one of these vertical bars may be replaced by a broken bar ¦ to indicate that the cursor is at this position. (When only vertical bars are used the position of the cursor is left unspecified.)

An editor with structedit is modal: it's either in normal mode or in struct mode.

The transition from normal mode to struct mode happens when a command establishes a structural selection. For example, just typing an opening parentheses won't trigger struct mode but typing an open parenthesis to wrap the current selection will switch to struct mode (chances are that in that you are already in struct mode unless the selection has been done with the pointer or character by character).

The transition from struct mode to normal mode happens when the user enters text or performs a text-altering non-struct command (eg paste).

The biggest difference between normal mode and struct mode is the behaviour of the arrow keys.

Moving the focus

Focus is an extension of the cursor notion found traditional in text editors. The focus can be on an insertion point (blinking cursor at a sensible position to type text) or a text selection (remembering if the text was selected from the left or from the right).

These commands could be bound to arrow keys without modifiers.

Left/right: traversing leaves and insertion points

(a |b¦ c) -(left)> (a ¦b| c) -(left)> (a ¦ b c) -(left)> (¦a| b c)
-(left)> (¦ a b c) -(left)> ¦ (a b c)

This sequence of lefts demonstrates several behaviors:

  • (a |b¦ c) -(left)> (a ¦b| c) when the cursor is at the right end, it moves to the left end and the selection remains otherwise untouched,
  • (a ¦b| c) -(left)> (a ¦ b c) the selection is collapsed and the cursor set at an insertion point between the two siblings
  • (a ¦ b c) -(left)> (¦a| b c) the extra space previously inserted is removed and the next sibling is selected with the cursor on the left end

right works in a symmetric manner.

Up: move cursor to the parent inner and outer boundaries

(a b (d ¦e|) f) -(up)> (a b (¦ d e) f) -(up)> (a b ¦ (d e) f) -(up)> (¦ a b (d e) f) -(up)> ¦ (a b (d e) f)
(a b (d |e¦) f) -(up)> (a b (d e ¦) f) -(up)> (a b (d e) ¦ f) -(up)> (a b (d e) f ¦) -(up)> (a b (d e) f) ¦ 

Those examples demonstrate that the bias must be stored and can't be always reconstructed from the selection (when it's collapsed right after a move left/right for example).

TBD: down

down performs ??? (rewind selection history?)

Expand/shrink selection

These commands could be bound to shift + arrow keys.

Expand/shrink selection: es-left/es-right

(a ¦b| c) -(es-left)> (¦a b| c) -(right)> (|a b¦ c) -(es-right)> (|a b c¦) -(es-left)> (|a b¦ c) 

Expand: es-up

(a b¦ c) -(es-up)> (a |b¦ c) -(es-up)> (|a b c¦) -(es-up)> |(a b c)¦

Select children and narrow: es-down

¦(a b c)| -(es-down)> (¦a b c|) -(es-down)> (¦a| b c) -(es-down)> (¦ a b c)
|(a b c)¦ -(es-down)> (|a b c¦) -(es-down)> (a b |c¦) -(es-down)> (a b c ¦)
¦((a b) c)| -(es-down)> (¦(a b) c|) -(es-down)> -(es-down)> (¦(a b)| c) -(es-down)> ((¦a b|) c)
 -(es-down)> ((¦a| b) c) -(es-down)> ((¦ a b) c)

Moving the selection content

These commands could be bound to cmd/ctrl + arrow keys.

Move selected nodes: mv-left/mv-right

(a b ¦c|) -(mv-left)> (a ¦c| b) -(mv-left)> (¦c| a b) -(mv-left)> ¦c| (a b)
(a ¦b c|) -(mv-left)> (¦b c| a) -(mv-left)> ¦b c| (a)
(a b) ¦c| -(mv-left)> (a b ¦c|) -(mv-left)> (a ¦c| b) -(mv-left)> (¦c| a b) -(mv-left)> ¦c| (a b)

Move to parent inner/outer boundaries: mv-up

(a b) |c| -(mv-left)> (a b ¦c|) -(mv-up)> (¦c| a b) -(mv-up)> ¦c| (a b)
(f (a b)) |c| -(mv-left)> (f (a b) ¦c|) -(mv-left)> (f (a b ¦c|)) -(mv-up)> (f (¦c| a b)) -(mv-up)> (f ¦c| (a b)) -(mv-up)> (¦c| f (a b)) -(mv-up)> ¦c| (f (a b)) ; the second mv-left is useless, just to illustrate that there's more than one way to do it
(f (a b)) |c| -(mv-left)> (f (a b) ¦c|) -(mv-up)> (¦c| f (a b)) -(mv-up)> ¦c| (f (a b))

Killing siblings

Backspace/Delete: delete-left/delete-right

(a b |c|) -(delete-left)> (a ¦c|) -(delete-left)> (¦c|) -(delete-left)> (¦c|)
(a b ¦ c) -(delete-left)> (a ¦ c) -(delete-left)> (¦ c) -(delete-left)> (¦ c)
(|a| b c) -(delete-right)> (¦a| c) -(delete-right)> (¦a|) -(delete-right)> (¦a|)
(¦ a b c) -(delete-right)> (¦ b c) -(delete-right)> (¦ c) -(delete-right)> (¦) -(delete-right)> (¦)

They don't change the cursor end.

Shift + Backspace/Delete: delete-lefts/delete-rights

(a b |c|) -(delete-lefts)> (¦c|) -(delete-lefts)> (¦c|)
(a b ¦ c) -(delete-lefts)> (¦ c) -(delete-lefts)> (¦ c)
(|a| b c) -(delete-rights)> (¦a|) -(delete-rights)> (¦a|)
(¦ a b c) -(delete-rights)> (¦) -(delete-rights)> (¦)

They don't change the cursor end.

Leafing through the structure

One controversial behavior of this proposal is that when the selection moves or the selected nodes are displaced it's done step by step because a traversal of leaves and insertion points is performed instead of trying to spare some moves by skipping whole branches at a time.

This is by design because figuring out the hierarchical relation (what is the closest common ancestor between where the cursor is and where I want it to be) is hard – even with rainbow parenthesis.

This approach leverages the fact that keypress events repeat when the keys are kept down: you just keep left (or cmd+left) pressed until you arrives at the target position. Contrast that to aiming the common ancestor then issuing several commands to ascend the hierarchy, some to skip unrelated branches and then some last ones to descend and skip towards the target position.

Back to normal mode

Esc: esc

Back to normal mode, selection unchanged.

(a ¦b| c) -(esc)> (a ¦b| c)

Space: space

Space deletes the selection and stays in struct mode. If the selection was empty go back to normal mode.

(a ¦b| c) -(space)> (a ¦ c) ; still in struct mode
(a ¦ b c) -(space)> (a ¦ b c) ; in normal mode

Typing characters not bound to struct commands

When typing characters not bound to struct commands (not "(" for example) the cursor would move to the closest insertion point and the selection would collapse. It means that unlike a regular text editor it would not delete the selection content.

(a ¦b| c) -(text-mode)> (a ¦ b c)
(a |b¦ c) -(text-mode)> (a b ¦ c)

Other struct commands

Raise

Split

Split is also bound to all closing symbols ) ] and }.

(a ¦ b) -(split)> (a) ¦ (b)
(a ¦b| c) -(split)> (a) ¦b| (c)
(a ¦b c|) -(split)> (a) ¦b c|
(a b ¦) -(split)> (a b) ¦

Bubble: bubble/bobble

Also known as convolute but I think bubble is a better name.

(let [] (if test |then| else)) -(bubble)> (if test |(let [] then)| else) 
(if test (let [] |then|) else) -(bubble)> (let [] |(if test then else)|)

The broadening of the selection is to allow repeated bubbling.

Another way to implement that would require a bubble mode with some visualization, denoted here by angle brackets.

(let [] (if test |then| else))
 -(bubble)>
(let [] <(if test |then| else)>) 
 -(expand up)> 
<(if test |(let [] then)| else)>
 -(bobble)> ;-) or bubble or esc
(if test |(let [] then)| else)

The interesting feature of the bubble mode would be to allow easy bubbling down (eg narrowing a let to a specific nested branch). All of this while piggy backing the regular moves/selections.

(let [] ¦(if a (if b c d) e)|)
-(bubble)>
<(let [] ¦(if a (if b c d) e)|)>
-(es-down)>
(<(let [] ¦if a (if b c d) e|)>)
-(es-down)>
(<(let [] ¦if|)> a (if b c d) e)
-(right)>
(if <(let [] ¦ )> a (if b c d) e)
-(right (Nx))>
(if a (if b <(let [] |c¦)> d) e)
-(bobble)>
(if a (if b (let [] |c¦) d) e)

So, basically the bubble would surround the selection until you pop it.

Bubbling also offers limited help when threading a form:

(-> (assoc |m¦ :k 42))
-(bubble)>
(-> <(assoc |m¦ :k 42)>)
-(right)>
(-> m <(assoc ¦ :k 42)>)
-(bobble)>
(-> m (assoc ¦ :k 42))

...

(|let [foo bar]¦ (if a b c))
move-right (4x)
((if a b |let [foo bar]¦ c))
expand-right
((if a b |let [foo bar] c¦))
wrap-list
((if a b (|let [foo bar] c¦)))
up (3x)
(|(if a b (let [foo bar] c))¦)
raise
|(if a b (let [foo bar] c))¦

...

(a |b|| c)
type "x"
(a x| ¦b¦ c)
type "y"
(a xy| ¦b¦ c)
type space or right
(a xy ||b| c)