Skip to content

Commit 934ff4e

Browse files
authored
New framework for testing language server handlers (#1992)
Or the Rego-based ones anyway.. but there's really nothing to stop us from reusing this elsewhere later. With Regal's language server moving away from a Go orchestration layer and towards passing through requests and responses straight to and from OPA wherever possible, our mostly Go-based tests of routing/handlers increasingly felt like a bad fit. But while most of our tests should be in Rego, we still need a solid suite of tests to target the server one layer above in the stack. Starting out with the Rego handlers, but again, the format should be easily reusable should we want that. Since we need to move above Rego here, and "pure" Go isn't particularly nice for writing or reading tests dealing with `[]byte` inputs and outputs without marshalling into native Go types, this framework has test expressed as **markdown** documents. The rationale for this is mainly that I want all test data compiled in a single document, not a directory of policy + input + data + more. This makes it very easy to quickly see that given policy X and input Y, the response will be Z. I think it works very well here, but would be happy to hear thoughts from others. Also: - MUCH improved completion provider now able to optimize for clients (such as VS Code) capable of handling a default range for **all** items sent back in the response instead of repeating the same rather verbose range over and over. This effectively cuts the payload sent to the client in half, and results in much snappier completions. - With that — finally seem to be able to set `isIncomplete` to `false` for at least the above mentioned clients, meaning that completion suggestions now normally only needs to be fetched once rather than at every key press. Another huge win! - A few other improvements to slim down the size of completion items in order to avoid allocations - Fix issue in hover provider that would provider hover tooltip for package keyword despite there being no example docs on that - Various stability issues fixed as I encountered them while working on this. Signed-off-by: Anders Eknert <anders.eknert@apple.com>
1 parent 2158863 commit 934ff4e

60 files changed

Lines changed: 2727 additions & 1160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bundle/regal/lsp/codeaction/codeaction.rego

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import data.regal.lsp.client
1212

1313
# METADATA
1414
# entrypoint: true
15-
result["response"] := actions
15+
default result["response"] := null
16+
17+
result["response"] := actions if actions != set()
1618

1719
# METADATA
1820
# description: A set of all code actions applicable in the current document

bundle/regal/lsp/codelens/codelens.rego

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import data.regal.lsp.util.range
1515

1616
# METADATA
1717
# entrypoint: true
18-
default result["response"] := []
18+
default result["response"] := null
1919

2020
result["response"] := lenses if input.regal.file.parse_errors == []
2121

bundle/regal/lsp/codelens/codelens_test.rego

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ test_code_lenses_for_module if {
1616

1717
lenses := codelens.lenses
1818
with input.params.textDocument.uri as "file://policy.rego"
19-
with input.regal.file.name as "policy.rego"
2019
with input.regal.file.lines as split(policy, "\n")
2120
with data.server.feature_flags.debug_provider as true
2221
with data.client.init_options.enableDebugCodelens as true

bundle/regal/lsp/completion/main.rego

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,45 @@
1111
# scope: subpackages
1212
package regal.lsp.completion
1313

14+
import data.regal.lsp.client
15+
import data.regal.lsp.location
16+
1417
import data.regal.util
1518

1619
# METADATA
1720
# entrypoint: true
21+
default result["response"] := null
22+
1823
result["response"] := {
1924
"items": items,
2025
"isIncomplete": true,
2126
} if {
2227
input.method == "textDocument/completion"
28+
not _default_edit_range_supported
29+
}
30+
31+
# Client supports setting a default edit range for completion items.
32+
result["response"] := {
33+
"items": items,
34+
"isIncomplete": false,
35+
"itemDefaults": {"editRange": range},
36+
} if {
37+
input.method == "textDocument/completion"
38+
_default_edit_range_supported
39+
40+
line := input.regal.file.lines[input.params.position.line]
41+
line != ""
42+
43+
location.in_rule_body(line)
44+
45+
ref := location.ref_at(line, input.params.position.character + 1)
46+
range := location.word_range(ref, input.params.position)
2347
}
2448

2549
# METADATA
2650
# schemas:
2751
# - input: {}
28-
result["response"] := data.regal.lsp.completion.resolvers[input.params.data.resolver].resolve if {
52+
result["response"] := data.regal.lsp.completion.resolvers[input.params.data].resolve if {
2953
input.method == "completionItem/resolve"
3054
} else := input.params if {
3155
# if there was nothing to resolve, return the input as-is
@@ -35,7 +59,7 @@ result["response"] := data.regal.lsp.completion.resolvers[input.params.data.reso
3559
# METADATA
3660
# description: main entry point for completion suggestions
3761
# entrypoint: true
38-
items contains object.union(completion, {"_regal": {"provider": provider}}) if {
62+
items contains completion if {
3963
# exit early if caret position is inside a comment. We currently don't have any provider
4064
# where doing completions inside of a comment makes sense. Behavior is also editor-specific:
4165
# - Zed: always on, with no way to disable
@@ -59,3 +83,7 @@ inside_comment if {
5983
startswith(comment.location, line)
6084
util.to_location_no_text(comment.location).col <= input.params.position.character + 1
6185
}
86+
87+
_default_edit_range_supported if {
88+
"editRange" in client.capabilities.textDocument.completion.completionList.itemDefaults
89+
}

bundle/regal/lsp/completion/main_test.rego

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import data.regal.lsp.completion
55
test_completion_entrypoint if {
66
items := completion.items with completion.providers as {"test": {"items": {{"foo": "bar"}}}}
77

8-
items == {{"_regal": {"provider": "test"}, "foo": "bar"}}
8+
items == {{"foo": "bar"}}
99
}
1010

1111
test_inside_comment if completion.inside_comment

bundle/regal/lsp/completion/providers/builtins/builtins.rego

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
# description: Completion suggestions for built-in functions
33
package regal.lsp.completion.providers.builtins
44

5+
import data.regal.lsp.client
56
import data.regal.lsp.completion.kind
67
import data.regal.lsp.location
78

89
# METADATA
910
# description: suggest built-in functions matching typed ref
11+
# scope: document
12+
13+
# METADATA
14+
# description: without editRange item defaults (large amount of duplicated data)
1015
items contains item if {
16+
not _default_edit_range_supported
17+
1118
line := input.regal.file.lines[input.params.position.line]
1219

1320
line != ""
@@ -20,6 +27,7 @@ items contains item if {
2027

2128
not builtin.infix # avoid suggesting 'eq', 'plus', etc
2229
not builtin.deprecated
30+
not startswith(builtin.name, "internal.")
2331

2432
startswith(builtin.name, ref.text)
2533

@@ -28,6 +36,37 @@ items contains item if {
2836
"kind": kind.function,
2937
"detail": "built-in function",
3038
"textEdit": {"range": location.word_range(ref, input.params.position), "newText": builtin.name},
31-
"data": {"resolver": "builtins"},
39+
"data": "builtins",
40+
}
41+
}
42+
43+
# METADATA
44+
# description: with editRange item defaults (range defined in main, label used as textEdit.newText))
45+
items contains item if {
46+
_default_edit_range_supported
47+
48+
line := input.regal.file.lines[input.params.position.line]
49+
50+
not startswith(line, "default ")
51+
52+
ref := location.ref_at(line, input.params.position.character + 1)
53+
54+
some builtin in data.workspace.builtins
55+
56+
not builtin.infix # avoid suggesting 'eq', 'plus', etc
57+
not builtin.deprecated
58+
not startswith(builtin.name, "internal.")
59+
60+
startswith(builtin.name, ref.text)
61+
62+
item := {
63+
"label": builtin.name,
64+
"kind": kind.function,
65+
"detail": "built-in function",
66+
"data": "builtins",
3267
}
3368
}
69+
70+
_default_edit_range_supported if {
71+
"editRange" in client.capabilities.textDocument.completion.completionList.itemDefaults
72+
}

bundle/regal/lsp/completion/providers/builtins/builtins_test.rego

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ test_simple_builtin_completion if {
3131
"start": {"character": 9, "line": 3},
3232
},
3333
},
34-
"data": {"resolver": "builtins"},
34+
"data": "builtins",
3535
},
3636
{
3737
"detail": "built-in function",
@@ -44,7 +44,7 @@ test_simple_builtin_completion if {
4444
"start": {"character": 9, "line": 3},
4545
},
4646
},
47-
"data": {"resolver": "builtins"},
47+
"data": "builtins",
4848
},
4949
}
5050
}
@@ -77,7 +77,7 @@ test_simple_builtin_completion_single_match if {
7777
"start": {"character": 9, "line": 3},
7878
},
7979
},
80-
"data": {"resolver": "builtins"},
80+
"data": "builtins",
8181
}}
8282
}
8383

@@ -109,7 +109,7 @@ test_simple_builtin_completion_single_match_longer_ref if {
109109
"start": {"character": 9, "line": 3},
110110
},
111111
},
112-
"data": {"resolver": "builtins"},
112+
"data": "builtins",
113113
}}
114114
}
115115

bundle/regal/lsp/completion/providers/input/input.rego

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ items contains item if {
2929
"range": location.word_range(word, input.params.position),
3030
"newText": "input",
3131
},
32-
"data": {"resolver": "input"},
32+
"data": "input",
3333
}
3434
}

bundle/regal/lsp/completion/providers/input/input_test.rego

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ allow if {
1313

1414
items == {{
1515
"detail": "input document",
16-
"data": {"resolver": "input"},
16+
"data": "input",
1717
"kind": 14,
1818
"label": "input",
1919
"textEdit": {

bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ package regal.lsp.completion.providers.rulerefs
44

55
import data.regal.ast
66

7+
import data.regal.lsp.client
78
import data.regal.lsp.completion.kind
89
import data.regal.lsp.location
910

10-
_ref_is_internal(ref) if contains(ref, "._")
11-
1211
_line := input.regal.file.lines[input.params.position.line]
1312

1413
_word := location.ref_at(_line, input.params.position.character + 1)
@@ -37,7 +36,7 @@ _current_package_refs contains ref if {
3736
_imported_package_refs contains ref if {
3837
some ref in _workspace_rule_refs
3938

40-
not _ref_is_internal(ref)
39+
not contains(ref, "._")
4140

4241
strings.any_prefix_match(ref, _current_file_imports)
4342
}
@@ -48,7 +47,7 @@ _other_package_refs contains ref if {
4847
not ref in _imported_package_refs
4948
not ref in _current_package_refs
5049

51-
not _ref_is_internal(ref)
50+
not contains(ref, "._")
5251
}
5352

5453
# from the current package
@@ -92,17 +91,39 @@ _matching_rule_ref_suggestions contains ref if {
9291
}
9392

9493
# METADATA
95-
# description: set of completion suggestions for references to rules
94+
# description: set of completion suggestions for references to rules (no edit range defaults support)
9695
items contains item if {
96+
not _default_edit_range_supported
97+
98+
range := location.word_range(_word, input.params.position)
99+
97100
some ref in _matching_rule_ref_suggestions
98101

99102
item := {
100103
"label": ref,
101104
"kind": kind.variable,
102105
"detail": "reference",
103106
"textEdit": {
104-
"range": location.word_range(_word, input.params.position),
107+
"range": range,
105108
"newText": ref,
106109
},
107110
}
108111
}
112+
113+
# METADATA
114+
# description: with editRange item defaults (range defined in main, label used as textEdit.newText))
115+
items contains item if {
116+
_default_edit_range_supported
117+
118+
some ref in _matching_rule_ref_suggestions
119+
120+
item := {
121+
"label": ref,
122+
"kind": kind.variable,
123+
"detail": "reference",
124+
}
125+
}
126+
127+
_default_edit_range_supported if {
128+
"editRange" in client.capabilities.textDocument.completion.completionList.itemDefaults
129+
}

0 commit comments

Comments
 (0)