Skip to content

Commit 056032b

Browse files
authored
Add code action combine cases (#1514)
* Add `combine_cases` code Action * Add tests for `combine-cases` * Add CHANGE entry
1 parent d17c679 commit 056032b

File tree

5 files changed

+175
-0
lines changed

5 files changed

+175
-0
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Make `inlay-hint` for function parameters configurable (#1515)
66
- Add custom `ocamllsp/jumpToTypedHole` to navigate through typed holes (#1516)
7+
- Add a code-action for combining pattern cases (just relaying on regex) (#1514)
78

89
## Fixes
910

ocaml-lsp-server/src/code_actions.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ let compute_ocaml_code_actions (params : CodeActionParams.t) state doc =
3838
[ Action_destruct_line.t state
3939
; Action_destruct.t state
4040
; Action_update_signature.t state
41+
; Action_combine_cases.t
4142
; Action_inferred_intf.t state
4243
; Action_type_annotate.t
4344
; Action_remove_type_annotation.t
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
open Import
2+
3+
let action_kind = "combine-cases"
4+
let kind = CodeActionKind.Other action_kind
5+
6+
let select_complete_lines (range : Range.t) =
7+
if range.start.line = range.end_.line
8+
then None
9+
else (
10+
let start = { range.start with character = 0 } in
11+
match range.end_.character with
12+
| 0 -> Some { range with start }
13+
| _ ->
14+
let end_ = Position.{ line = range.end_.line + 1; character = 0 } in
15+
Some (Range.create ~start ~end_))
16+
;;
17+
18+
let split_cases code =
19+
let lines = String.split code ~on:'\n' in
20+
let case_regex = Re.Perl.re {|^\s*\|.*->|} |> Re.compile in
21+
let drop_from_lines lines regex =
22+
List.map lines ~f:(Re.replace_string (Re.compile regex) ~by:"")
23+
in
24+
match List.for_all ~f:(Re.execp case_regex) lines with
25+
| false -> None
26+
| true ->
27+
let without_pipes = drop_from_lines lines (Re.Perl.re {|\s*\|\s*|}) in
28+
let lhs_patterns = drop_from_lines without_pipes (Re.Perl.re {|\s*->.*$|}) in
29+
let rhs_expressions = drop_from_lines without_pipes (Re.Perl.re {|^.*->\s*|}) in
30+
Some (lhs_patterns, rhs_expressions)
31+
;;
32+
33+
let pick_rhs rhs_expressions =
34+
let distinct_nonempty =
35+
List.map rhs_expressions ~f:String.strip
36+
|> List.filter ~f:(fun s -> (not (String.is_empty s)) && not (String.equal s "_"))
37+
|> Base.List.dedup_and_sort ~compare:Base.String.compare
38+
in
39+
match distinct_nonempty with
40+
| [ expr ] -> expr
41+
| _ -> "_"
42+
;;
43+
44+
let make_text_edit ~range ~newText ~doc ~uri =
45+
let text_edit = TextEdit.create ~range ~newText in
46+
let version = Document.version doc in
47+
let textDocument = OptionalVersionedTextDocumentIdentifier.create ~uri ~version () in
48+
let edit = TextDocumentEdit.create ~textDocument ~edits:[ `TextEdit text_edit ] in
49+
WorkspaceEdit.create ~documentChanges:[ `TextDocumentEdit edit ] ()
50+
;;
51+
52+
let code_action doc params =
53+
match Document.kind doc with
54+
| `Other -> Fiber.return None
55+
| `Merlin merlin ->
56+
(match Document.Merlin.kind merlin with
57+
| Intf -> Fiber.return None
58+
| Impl ->
59+
let result =
60+
let open Option.O in
61+
let* range = select_complete_lines params.CodeActionParams.range in
62+
let* code = Document.substring doc range in
63+
let code = String.strip ~drop:(fun c -> Char.equal c '\n') code in
64+
let* lhs_patterns, rhs_expressions = split_cases code in
65+
let+ i = Base.String.index code '|' in
66+
let indent = String.sub code ~pos:0 ~len:i in
67+
let lhs = String.concat ~sep:" | " lhs_patterns in
68+
let rhs = pick_rhs rhs_expressions in
69+
let newText = indent ^ "| " ^ lhs ^ " -> " ^ rhs ^ "\n" in
70+
let edit = make_text_edit ~range ~newText ~doc ~uri:params.textDocument.uri in
71+
CodeAction.create
72+
~title:(String.capitalize action_kind)
73+
~kind
74+
~edit
75+
~isPreferred:false
76+
()
77+
in
78+
Fiber.return result)
79+
;;
80+
81+
let t = { Code_action.kind; run = `Non_batchable code_action }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
open Import
2+
3+
val kind : CodeActionKind.t
4+
val t : Code_action.t

ocaml-lsp-server/test/e2e-new/code_actions.ml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,94 @@ let%expect_test "shouldn't find the jump target on the same line" =
17291729
No code actions |}]
17301730
;;
17311731
1732+
let%expect_test "can combine cases with multiple RHSes" =
1733+
let source =
1734+
{ocaml|
1735+
match card with
1736+
| Ace -> _
1737+
| King -> _
1738+
| Queen -> "Face card!"
1739+
| Jack -> "Face card?"
1740+
| Number _ -> _
1741+
|ocaml}
1742+
in
1743+
let range =
1744+
let start = Position.create ~line:3 ~character:3 in
1745+
let end_ = Position.create ~line:6 ~character:6 in
1746+
Range.create ~start ~end_
1747+
in
1748+
print_code_actions source range ~filter:(find_action "combine-cases");
1749+
[%expect
1750+
{|
1751+
Code actions:
1752+
{
1753+
"edit": {
1754+
"documentChanges": [
1755+
{
1756+
"edits": [
1757+
{
1758+
"newText": " | King | Queen | Jack | Number _ -> _\n",
1759+
"range": {
1760+
"end": { "character": 0, "line": 7 },
1761+
"start": { "character": 0, "line": 3 }
1762+
}
1763+
}
1764+
],
1765+
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
1766+
}
1767+
]
1768+
},
1769+
"isPreferred": false,
1770+
"kind": "combine-cases",
1771+
"title": "Combine-cases"
1772+
}
1773+
|}]
1774+
;;
1775+
1776+
let%expect_test "can combine cases with one unique RHS" =
1777+
let source =
1778+
{ocaml|
1779+
match card with
1780+
| Ace -> _
1781+
| King -> _
1782+
| Queen -> "Face card!"
1783+
| Jack -> "Face card?"
1784+
| Number _ -> _
1785+
|ocaml}
1786+
in
1787+
let range =
1788+
let start = Position.create ~line:3 ~character:3 in
1789+
let end_ = Position.create ~line:4 ~character:4 in
1790+
Range.create ~start ~end_
1791+
in
1792+
print_code_actions source range ~filter:(find_action "combine-cases");
1793+
[%expect
1794+
{|
1795+
Code actions:
1796+
{
1797+
"edit": {
1798+
"documentChanges": [
1799+
{
1800+
"edits": [
1801+
{
1802+
"newText": " | King | Queen -> \"Face card!\"\n",
1803+
"range": {
1804+
"end": { "character": 0, "line": 5 },
1805+
"start": { "character": 0, "line": 3 }
1806+
}
1807+
}
1808+
],
1809+
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
1810+
}
1811+
]
1812+
},
1813+
"isPreferred": false,
1814+
"kind": "combine-cases",
1815+
"title": "Combine-cases"
1816+
}
1817+
|}]
1818+
;;
1819+
17321820
let position_of_offset src x =
17331821
assert (0 <= x && x < String.length src);
17341822
let cnum = ref 0

0 commit comments

Comments
 (0)