parinfer / parinfer.js

Let's simplify the way we write Lisp
https://shaunlebron.github.io/parinfer
MIT License
1.76k stars 40 forks source link

Oakinfer - infer parens behind unmatched (sometimes) #131

Closed shaunlebron closed 7 years ago

shaunlebron commented 7 years ago

Due to the negative reaction to Parinfer v2 allowing unbalanced code as a means to prevent the breaking behavior in Parinfer v1, an alternative to both is being explored under the name Oakinfer. Original discussion: https://github.com/oakmac/atom-parinfer/issues/76

Expected Cases

The challenge is to find rules to implement the conflicting expected cases:

(foo [bar |] baz ... )
        ; ^ insert a "(" here

;; expected
(foo [bar (|)] baz ... )
(foo [bar (| baz ... )])
         ; ^ insert a "]" here

;; expected
(foo [bar (| baz ... )])

Rule Proposals

One way to do this is to use the cursor position to distinguish the two cases:

(foo [bar (]| baz ...
         ; ^ rule 1: unmatched "]" detected LEFT of cursor  => remove

;; expected output
(foo [bar (| baz ...
(foo [bar (|] baz ...
          ; ^ rule 2: unmatched "]" detected RIGHT of cursor  => infer parens behind it

;; expected output
(foo [bar (|)] baz ...

Constraining for Sanity

Only infer a paren to the right of the cursor if the most recent paren is an open-paren. This will still allow the following "barf" case:

[... (foo [bar | baz ]  ... )]
             ; ^ insert "]"

;; expected:
[... (foo [bar ]| baz ... )]
                   ; ^ the "]" was removed here despite being RIGHT of cursor

Will explore other cases that will require further constraints.

shaunlebron commented 7 years ago

some progress on test cases. not liking the implementation so far (still broken): https://github.com/shaunlebron/parinfer/commit/52e5d5840b83bcc2908adcd0a9dd558e463d58c7

shaunlebron commented 7 years ago

@oakmac which option do you expect below?

(foo (bar |) baz ... )
        ; ^ insert a "(" here

;; option A
(foo (bar (|) baz ... ))

;; option B
(foo (bar (|)) baz ... )
shaunlebron commented 7 years ago

Some unpredictability emerging. Even with our conservative constraints, this craziness happens:

(let [{:keys [|foo bar]} my-map])
;             ^ backspace

(let [{:keys |foo bar}] my-map)
;                    ^^ swapped

step by step:

(let [{:keys [|foo bar]} my-map])
;             ^ backspace

(let [{:keys |foo bar]} my-map])
;                    ^ unbalanced on right side of cursor

(let [{:keys |foo bar}]} my-map])
;                    ^ infer closing curly

(let [{:keys |foo bar}]} my-map])
;                      ^       ^ remove unmatched

(let [{:keys |foo bar}] my-map)
shaunlebron commented 7 years ago

context from v2 decision is returning to me, so now that i've double-checked this rabbit hole:

What makes Inference natural?

  1. It is natural to infer close-parens based on indentation:
(a
 (b
  (c
   d)
  e)^
 f)^
  ^
  1. But we cannot infer the inner close-parens when indentation is elided:
(a (b (c d) e) f)
          ^  ^ 
  1. Thus, when one inevitably becomes unmatched, we must either erase it (v1) or suspend until balanced (v2).

Erase (v1)

unmatched paren is removed, with the following results:

Suspend (v2)

unmatched paren is highlighted RED, with the following results:

v3 will provide option, defaulting to Suspend

v3 will have an option onBadInnerParen: 'erase' | 'suspend'. Default is suspend.

shaunlebron commented 7 years ago

published 2.1.0 without the option, and with a few other fixes.

@oakmac if you or anyone wants to revisit Oakinfer, I outlined all the test cases that will have to be accounted for here: https://github.com/shaunlebron/parinfer/blob/master/lib/test/cases/indent-mode.md#unmatched-close-parens