4 from __future__
import absolute_import
, division
, print_function
, unicode_literals
13 from typing
import Iterable
, List
, Mapping
, Tuple
16 from hh_paths
import hh_server
17 from lspcommand
import LspCommandProcessor
, Transcript
18 from lsptestspec
import LspTestSpec
, NoResponse
, line
19 from test_case
import TestCase
20 from utils
import Json
, JsonObject
, interpolate_variables
23 class InsertTextFormat(enum
.Enum
):
28 class LspTestDriver(common_tests
.CommonTestDriver
):
29 def write_load_config(self
, use_saved_state
: bool = False) -> None:
30 # Will use the .hhconfig already in the repo directory
31 # As for hh.conf, we'll write it explicitly each test.
32 # Note that hh.conf uses lower-case...
33 use_saved_state_str
= "true" if use_saved_state
else "false"
34 with
open(os
.path
.join(self
.repo_dir
, "hh.conf"), "w") as f
:
38 watchman_subscribe_v2 = true
39 interrupt_on_watchman = true
40 interrupt_on_client = true
42 load_state_natively_v4 = {use_saved_state}
43 use_mini_state = {use_saved_state}
44 require_mini_state = {use_saved_state}
45 lazy_decl = {use_saved_state}
46 lazy_parse = {use_saved_state}
47 lazy_init2 = {use_saved_state}
48 symbolindex_search_provider = SqliteIndex
50 use_saved_state
=use_saved_state_str
54 def write_naming_table_saved_state(self
) -> str:
55 naming_table_saved_state_path
= os
.path
.join(
56 self
.repo_dir
, "naming_table_saved_state.sqlite"
58 (stdout
, stderr
, retcode
) = self
.proc_call(
64 naming_table_saved_state_path
,
67 assert retcode
== 0, (
68 f
"Failed to save naming table saved state: {retcode}\n"
69 + f
"STDOUT:\n{stdout}\n"
70 + f
"STDERR:\n{stderr}\n"
72 return naming_table_saved_state_path
75 class TestLsp(TestCase
[LspTestDriver
]):
77 def get_test_driver(cls
) -> LspTestDriver
:
78 return LspTestDriver()
81 def get_template_repo(cls
) -> str:
82 return "hphp/hack/test/integration/data/lsp_exchanges/"
84 def repo_file(self
, file: str) -> str:
85 return os
.path
.join(self
.test_driver
.repo_dir
, file)
87 def read_repo_file(self
, file: str) -> str:
88 with
open(self
.repo_file(file), "r") as f
:
91 def repo_file_uri(self
, file: str) -> str:
92 return urllib
.parse
.urljoin("file://", self
.repo_file(file))
94 # pyre-fixme[11]: Annotation `Json` is not defined as a type.
95 def parse_test_data(self
, file: str, variables
: Mapping
[str, str]) -> Json
:
96 text
= self
.read_repo_file(file)
97 data
: Json
= json
.loads(text
)
98 data
= interpolate_variables(data
, variables
)
102 self
, test_name
: str, variables
: Mapping
[str, str]
103 ) -> Tuple
[Json
, Json
]:
104 test
= self
.parse_test_data(test_name
+ ".json", variables
)
105 expected
= self
.parse_test_data(test_name
+ ".expected", variables
)
106 return (test
, expected
)
108 def write_observed(self
, test_name
: str, observed_transcript
: Json
) -> None:
109 file = os
.path
.join(self
.test_driver
.template_repo
, test_name
+ ".observed.log")
111 list(self
.get_important_received_items(observed_transcript
)), indent
=2
113 with
open(file, "w") as f
:
116 # pyre-fixme[11]: Annotation `JsonObject` is not defined as a type.
117 def order_response(self
, response
: JsonObject
) -> str:
119 return str(response
["id"])
121 return json
.dumps(response
, indent
=2)
123 # sorts a list of responses using the 'id' parameter so they can be
124 # compared in sequence even if they came back from the server out of sequence.
125 # this can happen based on how json rpc is specified to work.
126 # if 'id' isn't present the response is a notification. we sort notifications
127 # by their entire text.
128 def sort_responses(self
, responses
: Iterable
[JsonObject
]) -> List
[JsonObject
]:
129 return sorted(responses
, key
=lambda response
: self
.order_response(response
))
131 # removes stack traces from error responses since these can be noisy
132 # as code changes and they contain execution environment specific details
133 # by ignoring these when comparing responses we might miss some minor issues
134 # but will still catch the core error being thrown or not.
135 def sanitize_exceptions(
136 self
, responses
: Iterable
[JsonObject
]
137 ) -> Iterable
[JsonObject
]:
138 sanitized
= copy
.deepcopy(responses
)
139 for response
in sanitized
:
140 if "error" in response
:
141 if "data" in response
["error"]:
142 if "stack" in response
["error"]["data"]:
143 del response
["error"]["data"]["stack"]
144 if "current_stack" in response
["error"]["data"]:
145 del response
["error"]["data"]["current_stack"]
146 if "server_finale_stack" in response
["error"]["data"]:
147 del response
["error"]["data"]["server_finale_stack"]
150 # dumps an LSP response into a standard json format that can be used for
151 # doing precise text comparison in a way that is human readable in the case
152 # of there being an error.
153 def serialize_responses(self
, responses
: Iterable
[Json
]) -> List
[str]:
154 return [json
.dumps(response
, indent
=2) for response
in responses
]
156 # generates received responses from an LSP communication transcript
157 # ignoring the non-deterministic ones "progress" and "actionRequired"
158 def get_important_received_items(self
, transcript
: Transcript
) -> Iterable
[Json
]:
159 for entry
in transcript
.values():
160 received
= entry
.received
or None
163 method
= received
.get("method") or ""
166 "window/actionRequired",
173 # gets a set of loaded responses ready for validation by sorting them
174 # by id and serializing them for precise text comparison
175 def prepare_responses(self
, responses
: Iterable
[JsonObject
]) -> List
[str]:
176 return self
.serialize_responses(
177 self
.sanitize_exceptions(self
.sort_responses(responses
))
185 wait_for_server
: bool,
186 use_serverless_ide
: bool,
189 assert not use_serverless_ide
, (
190 "Warning: both `wait_for_server` and `use_serverless_ide` "
191 + "were set to `True` for testing in "
192 + self
.run_lsp_test
.__name
__
194 + "While this is a possible test case, it hasn't been written yet, "
195 + "so it's more likely that this is a mistake "
196 + "and you're accidentally relying on hh_server to fulfill "
197 + "serverless IDE requests."
198 + "(If you're writing that test, "
199 + "then it's time to remove this assertion.)"
202 # wait until hh_server is ready before starting lsp
203 self
.test_driver
.run_check()
204 elif use_serverless_ide
:
205 self
.test_driver
.stop_hh_server()
207 with LspCommandProcessor
.create(
208 self
.test_driver
.test_env
, use_serverless_ide
=use_serverless_ide
210 observed_transcript
= lsp
.communicate(test
)
212 self
.write_observed(test_name
, observed_transcript
)
214 expected_items
= self
.prepare_responses(expected
)
215 observed_items
= self
.prepare_responses(
216 list(self
.get_important_received_items(observed_transcript
))
219 if not use_serverless_ide
:
220 # If the server's busy, maybe the machine's just under too much
221 # pressure to give results in a timely fashion. Doing a retry would
222 # only defer the question of what to do in that case, so instead
224 self
.throw_on_skip(observed_transcript
)
226 # validation checks that the number of items matches and that
227 # the responses are exactly identical to what we expect
231 "Wrong count. Observed this:\n"
232 + json
.dumps(observed_transcript
, indent
=2, separators
=(",", ": ")),
234 for i
in range(len(expected_items
)):
235 self
.assertEqual(expected_items
[i
], observed_items
[i
])
237 def throw_on_skip(self
, transcript
: Transcript
) -> None:
238 failure_messages
= ["Server busy", "timed out"]
239 for entry
in transcript
.values():
240 received
= entry
.received
243 if received
.get("error"):
244 message
= received
["error"]["message"]
245 for failure_message
in failure_messages
:
246 if failure_message
in message
:
247 raise unittest
.SkipTest(message
)
249 def prepare_server_environment(self
) -> None:
251 self
.test_driver
.write_load_config()
252 self
.test_driver
.start_hh_server()
253 (output
, err
, _
) = self
.test_driver
.run_check()
254 if "Error: Ran out of retries" in err
:
255 raise unittest
.SkipTest("Hack server could not be launched")
256 self
.assertEqual(output
.strip(), "No errors!")
258 def prepare_serverless_ide_environment(self
) -> Mapping
[str, str]:
260 self
.test_driver
.write_load_config(use_saved_state
=False)
261 naming_table_saved_state_path
= (
262 self
.test_driver
.write_naming_table_saved_state()
264 return {"naming_table_saved_state_path": naming_table_saved_state_path
}
269 variables
: Mapping
[str, str],
270 wait_for_server
: bool = True,
271 use_serverless_ide
: bool = False,
273 test
, expected
= self
.load_test_data(test_name
, variables
)
278 wait_for_server
=wait_for_server
,
279 use_serverless_ide
=use_serverless_ide
,
285 variables
: Mapping
[str, str],
286 wait_for_server
: bool,
287 use_serverless_ide
: bool,
290 # wait until hh_server is ready before starting lsp
291 self
.test_driver
.run_check()
292 elif use_serverless_ide
:
293 self
.test_driver
.stop_hh_server()
295 with LspCommandProcessor
.create(
296 self
.test_driver
.test_env
, use_serverless_ide
=use_serverless_ide
297 ) as lsp_command_processor
:
298 (observed_transcript
, error_details
) = spec
.run(
299 lsp_command_processor
=lsp_command_processor
, variables
=variables
301 file = os
.path
.join(self
.test_driver
.template_repo
, spec
.name
+ ".sent.log")
305 for sent
, _received
in observed_transcript
.values()
310 with
open(file, "w") as f
:
313 file = os
.path
.join(self
.test_driver
.template_repo
, spec
.name
+ ".received.log")
317 for _sent
, received
in observed_transcript
.values()
318 if received
is not None
322 with
open(file, "w") as f
:
325 if not use_serverless_ide
:
326 # If the server's busy, maybe the machine's just under too much
327 # pressure to give results in a timely fashion. Doing a retry would
328 # only defer the question of what to do in that case, so instead
330 self
.throw_on_skip(observed_transcript
)
332 if error_details
is not None:
333 raise AssertionError(error_details
)
335 def setup_php_file(self
, test_php
: str) -> Mapping
[str, str]:
336 # We want the path to the builtins directory. This is best we can do.
337 (output
, err
, retcode
) = self
.test_driver
.run_check(
338 options
=["--identify-function", "2:21", "--json"],
339 stdin
="<?hh // partial\nfunction f():void {PHP_EOL;}\n",
343 "Could not discover builtins directory -- "
344 + "got exit code 7 (either Out_of_time or Out_of_retries). "
345 + "The test machine is likely under too much load."
347 self
.assertEqual(retcode
, 0)
348 constants_path
= json
.loads(output
)[0]["definition_pos"]["filename"]
350 "hhi_path": re
.sub("/constants.hhi$", "", constants_path
),
351 "root_path": self
.test_driver
.repo_dir
,
352 "php_file_uri": self
.repo_file_uri(test_php
),
353 "php_file": self
.read_repo_file(test_php
),
356 def test_init_shutdown(self
) -> None:
357 self
.prepare_server_environment()
360 "initialize_shutdown", {"root_path": self
.test_driver
.repo_dir
}
363 def test_serverless_ide_completion(self
) -> None:
364 variables
= dict(self
.prepare_serverless_ide_environment())
365 variables
.update(self
.setup_php_file("completion.php"))
366 self
.test_driver
.stop_hh_server()
368 self
.initialize_spec(LspTestSpec("ide_completion"), use_serverless_ide
=True)
370 method
="textDocument/didOpen",
373 "uri": "${php_file_uri}",
374 "languageId": "hack",
376 "text": "${php_file}",
381 comment
="Add '$x = $point1['' to test autocomplete for shapes",
382 method
="textDocument/didChange",
384 "textDocument": {"uri": "${php_file_uri}"},
388 "start": {"line": 22, "character": 0},
389 "end": {"line": 22, "character": 0},
391 "text": "$x = $point1['",
398 comment
="autocomplete after user types a shape",
399 method
="textDocument/completion",
401 "textDocument": {"uri": "${php_file_uri}"},
402 "position": {"line": 22, "character": 14},
405 "isIncomplete": False,
411 "inlineDetail": "literal",
414 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
417 "filename": "${root_path}/completion.php",
426 "inlineDetail": "literal",
429 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
432 "filename": "${root_path}/completion.php",
439 powered_by
="serverless_ide",
442 comment
="Add automatically closed apostrophes when typing a shape key, the way visual studio code does it",
443 method
="textDocument/didChange",
445 "textDocument": {"uri": "${php_file_uri}"},
449 "start": {"line": 22, "character": 0},
450 "end": {"line": 22, "character": 14},
452 "text": "$x = $point1['']",
459 comment
="autocomplete after a shape, with VS Code automatically closed apostrophes",
460 method
="textDocument/completion",
462 "textDocument": {"uri": "${php_file_uri}"},
463 "position": {"line": 22, "character": 14},
466 "isIncomplete": False,
472 "inlineDetail": "literal",
475 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
478 "filename": "${root_path}/completion.php",
487 "inlineDetail": "literal",
490 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
493 "filename": "${root_path}/completion.php",
500 powered_by
="serverless_ide",
503 comment
="Add '$x = <'",
504 method
="textDocument/didChange",
506 "textDocument": {"uri": "${php_file_uri}"},
510 "start": {"line": 3, "character": 0},
511 "end": {"line": 3, "character": 0},
520 comment
="autocomplete after '$x = <'",
521 method
="textDocument/completion",
523 "textDocument": {"uri": "${php_file_uri}"},
524 "position": {"line": 3, "character": 6},
527 "isIncomplete": False,
530 "label": "ab:cd:alpha",
533 "inlineDetail": "class",
534 "sortText": "ab:cd:alpha",
535 "insertText": "ab:cd:alpha",
536 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
537 "data": {"fullname": ":ab:cd:alpha"},
540 "label": "ab:cd:text",
543 "inlineDetail": "class",
544 "sortText": "ab:cd:text",
545 "insertText": "ab:cd:text",
546 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
547 "data": {"fullname": ":ab:cd:text"},
551 powered_by
="serverless_ide",
554 comment
="Add '$x = <a'",
555 method
="textDocument/didChange",
557 "textDocument": {"uri": "${php_file_uri}"},
561 "start": {"line": 3, "character": 0},
562 "end": {"line": 3, "character": 6},
571 comment
="autocomplete after '$x = <a'",
572 method
="textDocument/completion",
574 "textDocument": {"uri": "${php_file_uri}"},
575 "position": {"line": 3, "character": 7},
578 "isIncomplete": False,
581 "label": "ab:cd:alpha",
584 "inlineDetail": "class",
585 "sortText": "ab:cd:alpha",
586 "insertText": "ab:cd:alpha",
587 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
588 "data": {"fullname": ":ab:cd:alpha"},
591 "label": "ab:cd:text",
594 "inlineDetail": "class",
595 "sortText": "ab:cd:text",
596 "insertText": "ab:cd:text",
597 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
598 "data": {"fullname": ":ab:cd:text"},
602 powered_by
="serverless_ide",
605 comment
="Add '$x = <ab:'",
606 method
="textDocument/didChange",
608 "textDocument": {"uri": "${php_file_uri}"},
612 "start": {"line": 3, "character": 0},
613 "end": {"line": 3, "character": 7},
622 comment
="autocomplete after '$x = <ab:'",
623 method
="textDocument/completion",
625 "textDocument": {"uri": "${php_file_uri}"},
626 "position": {"line": 3, "character": 9},
629 "isIncomplete": False,
632 "label": "ab:cd:alpha",
635 "inlineDetail": "class",
636 "sortText": "ab:cd:alpha",
637 "insertText": "ab:cd:alpha",
638 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
639 "data": {"fullname": ":ab:cd:alpha"},
642 "label": "ab:cd:text",
645 "inlineDetail": "class",
646 "sortText": "ab:cd:text",
647 "insertText": "ab:cd:text",
648 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
649 "data": {"fullname": ":ab:cd:text"},
653 powered_by
="serverless_ide",
656 comment
="Add '$x = <ab:cd:text '",
657 method
="textDocument/didChange",
659 "textDocument": {"uri": "${php_file_uri}"},
663 "start": {"line": 3, "character": 0},
664 "end": {"line": 3, "character": 9},
666 "text": "$x = <ab:cd:text ",
673 comment
="autocomplete after '$x = <ab:cd:text '",
674 method
="textDocument/completion",
676 "textDocument": {"uri": "${php_file_uri}"},
677 "position": {"line": 3, "character": 17},
680 "isIncomplete": False,
686 "inlineDetail": "?int",
688 "insertText": "width",
689 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
691 "fullname": ":width",
692 "filename": "${root_path}/completion_extras.php",
695 "base_class": "\\:ab:cd:text",
702 "inlineDetail": "?string",
704 "insertText": "color",
705 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
707 "fullname": ":color",
708 "filename": "${root_path}/completion_extras.php",
711 "base_class": "\\:ab:cd:text",
716 powered_by
="serverless_ide",
719 comment
="Add '$x = <ab:cd:text w'",
720 method
="textDocument/didChange",
722 "textDocument": {"uri": "${php_file_uri}"},
726 "start": {"line": 3, "character": 0},
727 "end": {"line": 3, "character": 17},
729 "text": "$x = <ab:cd:text w",
736 comment
="autocomplete after '$x = <ab:cd:text w'",
737 method
="textDocument/completion",
739 "textDocument": {"uri": "${php_file_uri}"},
740 "position": {"line": 3, "character": 18},
743 "isIncomplete": False,
749 "inlineDetail": "?int",
751 "insertText": "width",
752 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
754 "fullname": ":width",
755 "filename": "${root_path}/completion_extras.php",
758 "base_class": "\\:ab:cd:text",
765 "inlineDetail": "?string",
767 "insertText": "color",
768 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
770 "fullname": ":color",
771 "filename": "${root_path}/completion_extras.php",
774 "base_class": "\\:ab:cd:text",
779 powered_by
="serverless_ide",
782 comment
="Add '$x = new :'",
783 method
="textDocument/didChange",
785 "textDocument": {"uri": "${php_file_uri}"},
789 "start": {"line": 3, "character": 0},
790 "end": {"line": 3, "character": 18},
792 "text": "$x = new :",
799 comment
="autocomplete after '$x = new :'",
800 method
="textDocument/completion",
802 "textDocument": {"uri": "${php_file_uri}"},
803 "position": {"line": 3, "character": 10},
806 "isIncomplete": False,
809 "label": ":ab:cd:alpha",
812 "inlineDetail": "class",
813 "sortText": ":ab:cd:alpha",
814 "insertText": ":ab:cd:alpha",
815 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
816 "data": {"fullname": ":ab:cd:alpha"},
819 "label": ":ab:cd:text",
822 "inlineDetail": "class",
823 "sortText": ":ab:cd:text",
824 "insertText": ":ab:cd:text",
825 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
826 "data": {"fullname": ":ab:cd:text"},
830 powered_by
="serverless_ide",
833 comment
="Add '$x = new :a'",
834 method
="textDocument/didChange",
836 "textDocument": {"uri": "${php_file_uri}"},
840 "start": {"line": 3, "character": 0},
841 "end": {"line": 3, "character": 10},
843 "text": "$x = new :a",
850 comment
="autocomplete after '$x = new :a'",
851 method
="textDocument/completion",
853 "textDocument": {"uri": "${php_file_uri}"},
854 "position": {"line": 3, "character": 11},
857 "isIncomplete": False,
860 "label": ":ab:cd:alpha",
863 "inlineDetail": "class",
864 "sortText": ":ab:cd:alpha",
865 "insertText": ":ab:cd:alpha",
866 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
867 "data": {"fullname": ":ab:cd:alpha"},
870 "label": ":ab:cd:text",
873 "inlineDetail": "class",
874 "sortText": ":ab:cd:text",
875 "insertText": ":ab:cd:text",
876 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
877 "data": {"fullname": ":ab:cd:text"},
881 powered_by
="serverless_ide",
883 # Note that this request should match the result in the previous example
886 comment
="autocomplete resolving after '$x = new :a'",
887 method
="completionItem/resolve",
889 "label": ":ab:cd:alpha",
892 "inlineDetail": "class",
893 "itemType": ":ab:cd:alpha",
894 "insertText": ":ab:cd:alpha",
895 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
896 "data": {"fullname": ":ab:cd:alpha"},
899 "label": ":ab:cd:alpha",
902 "inlineDetail": "class",
903 "itemType": ":ab:cd:alpha",
906 "value": ":ab:cd:alpha docblock",
908 "insertText": ":ab:cd:alpha",
909 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
910 "data": {"fullname": ":ab:cd:alpha"},
912 powered_by
="serverless_ide",
914 # Try the same thing again, but this time without "new", instead using "<xhp" style
916 comment
="Add '$x = <a'",
917 method
="textDocument/didChange",
919 "textDocument": {"uri": "${php_file_uri}"},
923 "start": {"line": 3, "character": 0},
924 "end": {"line": 3, "character": 11},
933 comment
="autocomplete after '$x = <a'",
934 method
="textDocument/completion",
936 "textDocument": {"uri": "${php_file_uri}"},
937 "position": {"line": 3, "character": 7},
940 "isIncomplete": False,
943 "label": "ab:cd:alpha",
946 "inlineDetail": "class",
947 "sortText": "ab:cd:alpha",
948 "insertText": "ab:cd:alpha",
949 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
950 "data": {"fullname": ":ab:cd:alpha"},
953 "label": "ab:cd:text",
956 "inlineDetail": "class",
957 "sortText": "ab:cd:text",
958 "insertText": "ab:cd:text",
959 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
960 "data": {"fullname": ":ab:cd:text"},
964 powered_by
="serverless_ide",
968 comment
="autocomplete resolving after '$x = <a'",
969 method
="completionItem/resolve",
971 "label": "ab:cd:alpha",
974 "inlineDetail": "class",
975 "insertText": "ab:cd:alpha",
976 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
977 "data": {"fullname": ":ab:cd:alpha"},
980 "label": "ab:cd:alpha",
983 "inlineDetail": "class",
986 "value": ":ab:cd:alpha docblock",
988 "insertText": "ab:cd:alpha",
989 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
990 "data": {"fullname": ":ab:cd:alpha"},
992 powered_by
="serverless_ide",
995 comment
="Add '$x = <ab:cd:text/>; $y = $x->'",
996 method
="textDocument/didChange",
998 "textDocument": {"uri": "${php_file_uri}"},
1002 "start": {"line": 3, "character": 0},
1003 "end": {"line": 3, "character": 7},
1005 "text": "$x = <ab:cd:text/>; $y = $x->",
1012 comment
="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
1013 method
="textDocument/completion",
1015 "textDocument": {"uri": "${php_file_uri}"},
1016 "position": {"line": 3, "character": 29},
1019 "isIncomplete": False,
1025 "inlineDetail": "?int",
1026 "sortText": ":width",
1027 "insertText": ":width",
1028 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1030 "fullname": ":width",
1031 "filename": "${root_path}/completion_extras.php",
1034 "base_class": "\\:ab:cd:text",
1040 "detail": "?string",
1041 "inlineDetail": "?string",
1042 "sortText": ":color",
1043 "insertText": ":color",
1044 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1046 "fullname": ":color",
1047 "filename": "${root_path}/completion_extras.php",
1050 "base_class": "\\:ab:cd:text",
1055 powered_by
="serverless_ide",
1058 comment
="Add '$x = <ab:cd:text/>; $y = $x->:'",
1059 method
="textDocument/didChange",
1061 "textDocument": {"uri": "${php_file_uri}"},
1065 "start": {"line": 3, "character": 0},
1066 "end": {"line": 3, "character": 29},
1068 "text": "$x = <ab:cd:text/>; $y = $x->:",
1075 comment
="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
1076 method
="textDocument/completion",
1078 "textDocument": {"uri": "${php_file_uri}"},
1079 "position": {"line": 3, "character": 30},
1082 "isIncomplete": False,
1088 "inlineDetail": "?int",
1089 "sortText": ":width",
1090 "insertText": ":width",
1091 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1093 "fullname": ":width",
1094 "filename": "${root_path}/completion_extras.php",
1097 "base_class": "\\:ab:cd:text",
1103 "detail": "?string",
1104 "inlineDetail": "?string",
1105 "sortText": ":color",
1106 "insertText": ":color",
1107 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1109 "fullname": ":color",
1110 "filename": "${root_path}/completion_extras.php",
1113 "base_class": "\\:ab:cd:text",
1118 powered_by
="serverless_ide",
1121 comment
="Add 'test_fun'",
1122 method
="textDocument/didChange",
1124 "textDocument": {"uri": "${php_file_uri}"},
1128 "start": {"line": 3, "character": 0},
1129 "end": {"line": 3, "character": 30},
1138 comment
="autocomplete after 'test_fun'",
1139 method
="textDocument/completion",
1141 "textDocument": {"uri": "${php_file_uri}"},
1142 "position": {"line": 3, "character": 8},
1145 "isIncomplete": False,
1148 "label": "test_function",
1150 "detail": "function",
1151 "inlineDetail": "function",
1152 "sortText": "test_function",
1153 "insertText": "test_function",
1154 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1155 "data": {"fullname": "test_function"},
1159 powered_by
="serverless_ide",
1163 comment
="autocomplete resolving after 'test_fun'",
1164 method
="completionItem/resolve",
1166 "label": "test_function",
1168 "detail": "function(): void",
1169 "inlineDetail": "()",
1171 "insertText": "test_function",
1172 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1174 "filename": "${root_path}/completion.php",
1180 "label": "test_function",
1182 "detail": "function(): void",
1183 "inlineDetail": "()",
1187 "value": "test_function docblock.",
1189 "insertText": "test_function",
1190 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1192 "filename": "${root_path}/completion.php",
1197 powered_by
="serverless_ide",
1200 comment
="Add 'switch (Elsa::Alonso) { case Elsa:'",
1201 method
="textDocument/didChange",
1203 "textDocument": {"uri": "${php_file_uri}"},
1207 "start": {"line": 3, "character": 0},
1208 "end": {"line": 3, "character": 8},
1210 "text": "switch (Elsa::Alonso) { case Elsa:",
1217 comment
="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
1218 method
="textDocument/completion",
1220 "textDocument": {"uri": "${php_file_uri}"},
1221 "position": {"line": 3, "character": 34},
1223 result
={"isIncomplete": False, "items": []},
1224 powered_by
="serverless_ide",
1227 comment
="Add 'switch (Elsa::Alonso) { case Elsa::'",
1228 method
="textDocument/didChange",
1230 "textDocument": {"uri": "${php_file_uri}"},
1234 "start": {"line": 3, "character": 0},
1235 "end": {"line": 3, "character": 34},
1237 "text": "switch (Elsa::Alonso) { case Elsa::",
1244 comment
="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
1245 method
="textDocument/completion",
1247 "textDocument": {"uri": "${php_file_uri}"},
1248 "position": {"line": 3, "character": 35},
1251 "isIncomplete": False,
1256 "detail": "classname<this>",
1257 "inlineDetail": "classname<this>",
1258 "sortText": "class",
1259 "insertText": "class",
1260 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1262 "fullname": "class",
1263 "filename": "${root_path}/completion_extras.php",
1266 "base_class": "\\Elsa",
1273 "inlineDetail": "Elsa",
1275 "insertText": "Bard",
1276 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1279 "filename": "${root_path}/completion_extras.php",
1282 "base_class": "\\Elsa",
1289 "inlineDetail": "Elsa",
1290 "sortText": "Alonso",
1291 "insertText": "Alonso",
1292 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1294 "fullname": "Alonso",
1295 "filename": "${root_path}/completion_extras.php",
1298 "base_class": "\\Elsa",
1304 "detail": "function(mixed $value): bool",
1305 "inlineDetail": "(mixed $value)",
1307 "sortText": "isValid",
1308 "insertText": "isValid(${1:\\$value})",
1309 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1311 "fullname": "isValid",
1312 "filename": "${hhi_path}/BuiltinEnum.hhi",
1315 "base_class": "\\Elsa",
1319 "label": "getValues",
1321 "detail": "function(): darray<string, Elsa>",
1322 "inlineDetail": "()",
1323 "itemType": "darray<string, Elsa>",
1324 "sortText": "getValues",
1325 "insertText": "getValues()",
1326 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1328 "fullname": "getValues",
1329 "filename": "${hhi_path}/BuiltinEnum.hhi",
1332 "base_class": "\\Elsa",
1336 "label": "getNames",
1338 "detail": "function(): darray<Elsa, string>",
1339 "inlineDetail": "()",
1340 "itemType": "darray<Elsa, string>",
1341 "sortText": "getNames",
1342 "insertText": "getNames()",
1343 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1345 "fullname": "getNames",
1346 "filename": "${hhi_path}/BuiltinEnum.hhi",
1349 "base_class": "\\Elsa",
1355 "detail": "function(mixed $value): ?Elsa",
1356 "inlineDetail": "(mixed $value)",
1357 "itemType": "?Elsa",
1358 "sortText": "coerce",
1359 "insertText": "coerce(${1:\\$value})",
1360 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1362 "fullname": "coerce",
1363 "filename": "${hhi_path}/BuiltinEnum.hhi",
1366 "base_class": "\\Elsa",
1370 "label": "assertAll",
1372 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
1373 "inlineDetail": "(Traversable<mixed> $values)",
1374 "itemType": "Container<Elsa>",
1375 "sortText": "assertAll",
1376 "insertText": "assertAll(${1:\\$values})",
1377 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1379 "fullname": "assertAll",
1380 "filename": "${hhi_path}/BuiltinEnum.hhi",
1383 "base_class": "\\Elsa",
1389 "detail": "function(mixed $value): Elsa",
1390 "inlineDetail": "(mixed $value)",
1392 "sortText": "assert",
1393 "insertText": "assert(${1:\\$value})",
1394 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1396 "fullname": "assert",
1397 "filename": "${hhi_path}/BuiltinEnum.hhi",
1400 "base_class": "\\Elsa",
1405 powered_by
="serverless_ide",
1408 comment
="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1409 method
="textDocument/didChange",
1411 "textDocument": {"uri": "${php_file_uri}"},
1415 "start": {"line": 3, "character": 0},
1416 "end": {"line": 3, "character": 35},
1418 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
1425 comment
="docblock resolve after 'switch (Elsa::Alonso) { case Elsa::'",
1426 method
="completionItem/resolve",
1430 "detail": "function(mixed $value): bool",
1431 "inlineDetail": "(mixed $value)",
1433 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1436 "start": {"line": 3, "character": 35},
1437 "end": {"line": 3, "character": 35},
1439 "newText": "isValid",
1442 "filename": "${hhi_path}/BuiltinEnum.hhi",
1450 "detail": "function(mixed $value): bool",
1451 "inlineDetail": "(mixed $value)",
1455 "value": "Returns whether or not the value is defined as a constant.",
1457 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1460 "start": {"line": 3, "character": 35},
1461 "end": {"line": 3, "character": 35},
1463 "newText": "isValid",
1466 "filename": "${hhi_path}/BuiltinEnum.hhi",
1471 powered_by
="serverless_ide",
1475 comment
="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1476 method
="textDocument/completion",
1478 "textDocument": {"uri": "${php_file_uri}"},
1479 "position": {"line": 3, "character": 42},
1481 result
={"isIncomplete": False, "items": []},
1482 powered_by
="serverless_ide",
1485 comment
="Add 'TestNS\\'",
1486 method
="textDocument/didChange",
1488 "textDocument": {"uri": "${php_file_uri}"},
1492 "start": {"line": 3, "character": 0},
1493 "end": {"line": 3, "character": 42},
1502 comment
="autocomplete after 'TestNS\\'",
1503 method
="textDocument/completion",
1505 "textDocument": {"uri": "${php_file_uri}"},
1506 "position": {"line": 3, "character": 7},
1509 "isIncomplete": False,
1512 "label": "test_func",
1514 "detail": "function",
1515 "inlineDetail": "function",
1516 "sortText": "test_func",
1517 "insertText": "test_func",
1518 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1519 "data": {"fullname": "TestNS\\test_func"},
1523 powered_by
="serverless_ide",
1526 comment
="Add '$cc = new CompletionClass(); $cc->interfa'",
1527 method
="textDocument/didChange",
1529 "textDocument": {"uri": "${php_file_uri}"},
1533 "start": {"line": 3, "character": 0},
1534 "end": {"line": 3, "character": 7},
1536 "text": "$cc = new CompletionClass(); $cc->interfa",
1543 comment
="autocomplete after '$cc = new CompletionClass(); $cc->interfa'",
1544 method
="textDocument/completion",
1546 "textDocument": {"uri": "${php_file_uri}"},
1547 "position": {"line": 3, "character": 41},
1550 "isIncomplete": False,
1553 "label": "interfaceDocBlockMethod",
1555 "detail": "function(): void",
1556 "inlineDetail": "()",
1558 "sortText": "interfaceDocBlockMethod",
1559 "insertText": "interfaceDocBlockMethod()",
1560 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1562 "fullname": "interfaceDocBlockMethod",
1563 "filename": "${root_path}/completion.php",
1566 "base_class": "\\CompletionClass",
1571 powered_by
="serverless_ide",
1575 comment
="autocomplete resolving after '$cc = new CompletionClass(); $cc->interfa'",
1576 method
="completionItem/resolve",
1578 "label": "interfaceDocBlockMethod",
1580 "detail": "function(): void",
1581 "inlineDetail": "()",
1583 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1586 "start": {"line": 3, "character": 34},
1587 "end": {"line": 3, "character": 41},
1589 "newText": "interfaceDocBlockMethod",
1592 "filename": "${root_path}/completion.php",
1598 "label": "interfaceDocBlockMethod",
1600 "detail": "function(): void",
1601 "inlineDetail": "()",
1603 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1606 "start": {"line": 3, "character": 34},
1607 "end": {"line": 3, "character": 41},
1609 "newText": "interfaceDocBlockMethod",
1612 "filename": "${root_path}/completion.php",
1617 powered_by
="serverless_ide",
1620 comment
="Add 'DeprecatedClass::'",
1621 method
="textDocument/didChange",
1623 "textDocument": {"uri": "${php_file_uri}"},
1627 "start": {"line": 3, "character": 0},
1628 "end": {"line": 3, "character": 41},
1630 "text": "DeprecatedClass::",
1637 comment
="autocomplete after 'DeprecatedClass::'",
1638 method
="textDocument/completion",
1640 "textDocument": {"uri": "${php_file_uri}"},
1641 "position": {"line": 3, "character": 17},
1644 "isIncomplete": False,
1649 "detail": "classname<this>",
1650 "inlineDetail": "classname<this>",
1651 "sortText": "class",
1652 "insertText": "class",
1653 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1655 "fullname": "class",
1656 "filename": "${root_path}/completion_extras.php",
1659 "base_class": "\\DeprecatedClass",
1663 "label": "test_do_not_use",
1665 "detail": "function(): void",
1666 "inlineDetail": "()",
1668 "sortText": "~test_do_not_use",
1669 "insertText": "test_do_not_use()",
1670 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1672 "fullname": "test_do_not_use",
1673 "filename": "${root_path}/completion_extras.php",
1676 "base_class": "\\DeprecatedClass",
1682 "detail": "function(): void",
1683 "inlineDetail": "()",
1685 "sortText": "getName",
1686 "insertText": "getName()",
1687 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1689 "fullname": "getName",
1690 "filename": "${root_path}/completion_extras.php",
1693 "base_class": "\\DeprecatedClass",
1697 "label": "getAttributes_DO_NOT_USE",
1699 "detail": "function(): void",
1700 "inlineDetail": "()",
1702 "sortText": "~getAttributes_DO_NOT_USE",
1703 "insertText": "getAttributes_DO_NOT_USE()",
1704 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1706 "fullname": "getAttributes_DO_NOT_USE",
1707 "filename": "${root_path}/completion_extras.php",
1710 "base_class": "\\DeprecatedClass",
1714 "label": "__getLoader",
1716 "detail": "function(): void",
1717 "inlineDetail": "()",
1719 "sortText": "~__getLoader",
1720 "insertText": "__getLoader()",
1721 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
1723 "fullname": "__getLoader",
1724 "filename": "${root_path}/completion_extras.php",
1727 "base_class": "\\DeprecatedClass",
1732 powered_by
="serverless_ide",
1735 comment
="Add 'call_lambda(3, $m'",
1736 method
="textDocument/didChange",
1738 "textDocument": {"uri": "${php_file_uri}"},
1742 "start": {"line": 30, "character": 0},
1743 "end": {"line": 30, "character": 0},
1745 "text": " call_lambda(3, $m",
1752 comment
="autocomplete results for 'call_lambda(3, $m'",
1753 method
="textDocument/completion",
1755 "textDocument": {"uri": "${php_file_uri}"},
1756 "position": {"line": 30, "character": 19},
1759 "isIncomplete": False,
1762 "label": "$mylambda",
1764 "detail": "local variable",
1765 "inlineDetail": "(num $n)",
1767 "sortText": "$mylambda",
1768 "insertText": "$mylambda",
1769 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1771 "fullname": "$mylambda",
1772 "filename": "${root_path}/completion.php",
1779 powered_by
="serverless_ide",
1783 comment
="resolve autocompletion for $mylambda'",
1784 method
="completionItem/resolve",
1786 "label": "$mylambda",
1788 "detail": "local variable",
1789 "inlineDetail": "(num $n)",
1791 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1794 "start": {"line": 30, "character": 17},
1795 "end": {"line": 30, "character": 19},
1797 "newText": "$mylambda",
1800 "filename": "${root_path}/completion.php",
1806 "label": "$mylambda",
1808 "detail": "local variable",
1809 "inlineDetail": "(num $n)",
1811 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1814 "start": {"line": 30, "character": 17},
1815 "end": {"line": 30, "character": 19},
1817 "newText": "$mylambda",
1820 "filename": "${root_path}/completion.php",
1825 powered_by
="serverless_ide",
1827 .request(line
=line(), method
="shutdown", params
={}, result
=None)
1828 .notification(method
="exit", params
={})
1830 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
1832 def test_serverless_ide_completion_legacy(self
) -> None:
1833 variables
= dict(self
.prepare_serverless_ide_environment())
1834 variables
.update(self
.setup_php_file("completion.php"))
1835 self
.test_driver
.stop_hh_server()
1838 self
.initialize_spec(
1839 LspTestSpec("serverless_ide_completion_legacy"), use_serverless_ide
=True
1842 method
="textDocument/didOpen",
1845 "uri": "${php_file_uri}",
1846 "languageId": "hack",
1848 "text": "${php_file}",
1853 comment
="Add '$x = <'",
1854 method
="textDocument/didChange",
1856 "textDocument": {"uri": "${php_file_uri}"},
1860 "start": {"line": 3, "character": 0},
1861 "end": {"line": 3, "character": 0},
1870 comment
="autocomplete after '$x = <'",
1871 method
="textDocument/completion",
1873 "textDocument": {"uri": "${php_file_uri}"},
1874 "position": {"line": 3, "character": 6},
1877 "isIncomplete": False,
1880 "label": "ab:cd:alpha",
1883 "inlineDetail": "class",
1884 "sortText": "ab:cd:alpha",
1885 "insertText": "ab:cd:alpha",
1886 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1887 "data": {"fullname": ":ab:cd:alpha"},
1890 "label": "ab:cd:text",
1893 "inlineDetail": "class",
1894 "sortText": "ab:cd:text",
1895 "insertText": "ab:cd:text",
1896 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1897 "data": {"fullname": ":ab:cd:text"},
1901 powered_by
="serverless_ide",
1904 comment
="Add '$x = <a'",
1905 method
="textDocument/didChange",
1907 "textDocument": {"uri": "${php_file_uri}"},
1911 "start": {"line": 3, "character": 0},
1912 "end": {"line": 3, "character": 6},
1921 comment
="autocomplete after '$x = <a'",
1922 method
="textDocument/completion",
1924 "textDocument": {"uri": "${php_file_uri}"},
1925 "position": {"line": 3, "character": 7},
1928 "isIncomplete": False,
1931 "label": "ab:cd:alpha",
1934 "inlineDetail": "class",
1935 "sortText": "ab:cd:alpha",
1936 "insertText": "ab:cd:alpha",
1937 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1938 "data": {"fullname": ":ab:cd:alpha"},
1941 "label": "ab:cd:text",
1944 "inlineDetail": "class",
1945 "sortText": "ab:cd:text",
1946 "insertText": "ab:cd:text",
1947 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1948 "data": {"fullname": ":ab:cd:text"},
1952 powered_by
="serverless_ide",
1955 comment
="Add '$x = <ab:'",
1956 method
="textDocument/didChange",
1958 "textDocument": {"uri": "${php_file_uri}"},
1962 "start": {"line": 3, "character": 0},
1963 "end": {"line": 3, "character": 7},
1965 "text": "$x = <ab:",
1972 comment
="autocomplete after '$x = <ab:'.",
1973 method
="textDocument/completion",
1975 "textDocument": {"uri": "${php_file_uri}"},
1976 "position": {"line": 3, "character": 9},
1979 "isIncomplete": False,
1982 "label": "ab:cd:alpha",
1985 "inlineDetail": "class",
1986 "sortText": "ab:cd:alpha",
1987 "insertText": "ab:cd:alpha",
1988 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1989 "data": {"fullname": ":ab:cd:alpha"},
1992 "label": "ab:cd:text",
1995 "inlineDetail": "class",
1996 "sortText": "ab:cd:text",
1997 "insertText": "ab:cd:text",
1998 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
1999 "data": {"fullname": ":ab:cd:text"},
2003 powered_by
="serverless_ide",
2006 comment
="Add '$x = <ab:cd:text '",
2007 method
="textDocument/didChange",
2009 "textDocument": {"uri": "${php_file_uri}"},
2013 "start": {"line": 3, "character": 0},
2014 "end": {"line": 3, "character": 9},
2016 "text": "$x = <ab:cd:text ",
2023 comment
="autocomplete after '$x = <ab:cd:text '",
2024 method
="textDocument/completion",
2026 "textDocument": {"uri": "${php_file_uri}"},
2027 "position": {"line": 3, "character": 17},
2030 "isIncomplete": False,
2036 "inlineDetail": "?int",
2037 "sortText": "width",
2038 "insertText": "width",
2039 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2041 "fullname": ":width",
2042 "filename": "${root_path}/completion_extras.php",
2045 "base_class": "\\:ab:cd:text",
2051 "detail": "?string",
2052 "inlineDetail": "?string",
2053 "sortText": "color",
2054 "insertText": "color",
2055 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2057 "fullname": ":color",
2058 "filename": "${root_path}/completion_extras.php",
2061 "base_class": "\\:ab:cd:text",
2066 powered_by
="serverless_ide",
2069 comment
="Add '$x = <ab:cd:text w'",
2070 method
="textDocument/didChange",
2072 "textDocument": {"uri": "${php_file_uri}"},
2076 "start": {"line": 3, "character": 0},
2077 "end": {"line": 3, "character": 17},
2079 "text": "$x = <ab:cd:text w",
2086 comment
="autocomplete after '$x = <ab:cd:text w'",
2087 method
="textDocument/completion",
2089 "textDocument": {"uri": "${php_file_uri}"},
2090 "position": {"line": 3, "character": 18},
2093 "isIncomplete": False,
2099 "inlineDetail": "?int",
2100 "sortText": "width",
2101 "insertText": "width",
2102 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2104 "fullname": ":width",
2105 "filename": "${root_path}/completion_extras.php",
2108 "base_class": "\\:ab:cd:text",
2114 "detail": "?string",
2115 "inlineDetail": "?string",
2116 "sortText": "color",
2117 "insertText": "color",
2118 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2120 "fullname": ":color",
2121 "filename": "${root_path}/completion_extras.php",
2124 "base_class": "\\:ab:cd:text",
2129 powered_by
="serverless_ide",
2132 comment
="Add '$x = new :''",
2133 method
="textDocument/didChange",
2135 "textDocument": {"uri": "${php_file_uri}"},
2139 "start": {"line": 3, "character": 0},
2140 "end": {"line": 3, "character": 18},
2142 "text": "$x = new :",
2149 comment
="autocomplete after '$x = new :'",
2150 method
="textDocument/completion",
2152 "textDocument": {"uri": "${php_file_uri}"},
2153 "position": {"line": 3, "character": 10},
2156 "isIncomplete": False,
2159 "label": ":ab:cd:alpha",
2162 "inlineDetail": "class",
2163 "sortText": ":ab:cd:alpha",
2164 "insertText": ":ab:cd:alpha",
2165 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2166 "data": {"fullname": ":ab:cd:alpha"},
2169 "label": ":ab:cd:text",
2172 "inlineDetail": "class",
2173 "sortText": ":ab:cd:text",
2174 "insertText": ":ab:cd:text",
2175 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2176 "data": {"fullname": ":ab:cd:text"},
2180 powered_by
="serverless_ide",
2183 comment
="Add '$x = new :a'",
2184 method
="textDocument/didChange",
2186 "textDocument": {"uri": "${php_file_uri}"},
2190 "start": {"line": 3, "character": 0},
2191 "end": {"line": 3, "character": 10},
2193 "text": "$x = new :a",
2200 comment
="autocomplete after '$x = new :a'",
2201 method
="textDocument/completion",
2203 "textDocument": {"uri": "${php_file_uri}"},
2204 "position": {"line": 3, "character": 11},
2207 "isIncomplete": False,
2210 "label": ":ab:cd:alpha",
2213 "inlineDetail": "class",
2214 "sortText": ":ab:cd:alpha",
2215 "insertText": ":ab:cd:alpha",
2216 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2217 "data": {"fullname": ":ab:cd:alpha"},
2220 "label": ":ab:cd:text",
2223 "inlineDetail": "class",
2224 "sortText": ":ab:cd:text",
2225 "insertText": ":ab:cd:text",
2226 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2227 "data": {"fullname": ":ab:cd:text"},
2231 powered_by
="serverless_ide",
2233 # Note that this request sent should match the result given in the previous example
2236 comment
="autocomplete resolving after '$x = new :a'",
2237 method
="completionItem/resolve",
2239 "label": ":ab:cd:alpha",
2242 "inlineDetail": "class",
2243 "itemType": ":ab:cd:alpha",
2244 "insertText": ":ab:cd:alpha",
2245 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2246 "data": {"fullname": ":ab:cd:alpha"},
2249 "label": ":ab:cd:alpha",
2252 "inlineDetail": "class",
2253 "itemType": ":ab:cd:alpha",
2256 "value": ":ab:cd:alpha docblock",
2258 "insertText": ":ab:cd:alpha",
2259 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2260 "data": {"fullname": ":ab:cd:alpha"},
2262 powered_by
="serverless_ide",
2265 comment
="Add '$x = <ab:cd:text/>; $y = $x->'",
2266 method
="textDocument/didChange",
2268 "textDocument": {"uri": "${php_file_uri}"},
2272 "start": {"line": 3, "character": 0},
2273 "end": {"line": 3, "character": 11},
2275 "text": "$x = <ab:cd:text/>; $y = $x->",
2282 comment
="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
2283 method
="textDocument/completion",
2285 "textDocument": {"uri": "${php_file_uri}"},
2286 "position": {"line": 3, "character": 29},
2289 "isIncomplete": False,
2295 "inlineDetail": "?int",
2296 "sortText": ":width",
2297 "insertText": ":width",
2298 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2300 "fullname": ":width",
2301 "filename": "${root_path}/completion_extras.php",
2304 "base_class": "\\:ab:cd:text",
2310 "detail": "?string",
2311 "inlineDetail": "?string",
2312 "sortText": ":color",
2313 "insertText": ":color",
2314 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2316 "fullname": ":color",
2317 "filename": "${root_path}/completion_extras.php",
2320 "base_class": "\\:ab:cd:text",
2325 powered_by
="serverless_ide",
2328 comment
="Add '$x = <ab:cd:text/>; $y = $x->:'",
2329 method
="textDocument/didChange",
2331 "textDocument": {"uri": "${php_file_uri}"},
2335 "start": {"line": 3, "character": 0},
2336 "end": {"line": 3, "character": 29},
2338 "text": "$x = <ab:cd:text/>; $y = $x->:",
2345 comment
="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
2346 method
="textDocument/completion",
2348 "textDocument": {"uri": "${php_file_uri}"},
2349 "position": {"line": 3, "character": 30},
2352 "isIncomplete": False,
2358 "inlineDetail": "?int",
2359 "sortText": ":width",
2360 "insertText": ":width",
2361 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2363 "fullname": ":width",
2364 "filename": "${root_path}/completion_extras.php",
2367 "base_class": "\\:ab:cd:text",
2373 "detail": "?string",
2374 "inlineDetail": "?string",
2375 "sortText": ":color",
2376 "insertText": ":color",
2377 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2379 "fullname": ":color",
2380 "filename": "${root_path}/completion_extras.php",
2383 "base_class": "\\:ab:cd:text",
2388 powered_by
="serverless_ide",
2391 comment
="Add 'test_fun'",
2392 method
="textDocument/didChange",
2394 "textDocument": {"uri": "${php_file_uri}"},
2398 "start": {"line": 3, "character": 0},
2399 "end": {"line": 3, "character": 30},
2408 comment
="autocomplete after 'test_fun'",
2409 method
="textDocument/completion",
2411 "textDocument": {"uri": "${php_file_uri}"},
2412 "position": {"line": 3, "character": 8},
2415 "isIncomplete": False,
2418 "label": "test_function",
2420 "detail": "function",
2421 "inlineDetail": "function",
2422 "sortText": "test_function",
2423 "insertText": "test_function",
2424 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2425 "data": {"fullname": "test_function"},
2429 powered_by
="serverless_ide",
2433 comment
="autocomplete resolving after 'test_fun'",
2434 method
="completionItem/resolve",
2436 "label": "test_function",
2438 "detail": "function(): void",
2439 "inlineDetail": "()",
2441 "insertText": "test_function",
2442 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2444 "filename": "${root_path}/completion.php",
2450 "label": "test_function",
2452 "detail": "function(): void",
2453 "inlineDetail": "()",
2457 "value": "test_function docblock.",
2459 "insertText": "test_function",
2460 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2462 "filename": "${root_path}/completion.php",
2467 powered_by
="serverless_ide",
2470 comment
="Add 'switch (Elsa::Alonso) { case Elsa:'",
2471 method
="textDocument/didChange",
2473 "textDocument": {"uri": "${php_file_uri}"},
2477 "start": {"line": 3, "character": 0},
2478 "end": {"line": 3, "character": 8},
2480 "text": "switch (Elsa::Alonso) { case Elsa:",
2487 comment
="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
2488 method
="textDocument/completion",
2490 "textDocument": {"uri": "${php_file_uri}"},
2491 "position": {"line": 3, "character": 34},
2493 result
={"isIncomplete": False, "items": []},
2494 powered_by
="serverless_ide",
2497 comment
="Add 'switch (Elsa::Alonso) { case Elsa::'",
2498 method
="textDocument/didChange",
2500 "textDocument": {"uri": "${php_file_uri}"},
2504 "start": {"line": 3, "character": 0},
2505 "end": {"line": 3, "character": 34},
2507 "text": "switch (Elsa::Alonso) { case Elsa::",
2514 comment
="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
2515 method
="textDocument/completion",
2517 "textDocument": {"uri": "${php_file_uri}"},
2518 "position": {"line": 3, "character": 35},
2521 "isIncomplete": False,
2526 "detail": "classname<this>",
2527 "inlineDetail": "classname<this>",
2528 "sortText": "class",
2529 "insertText": "class",
2530 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2532 "fullname": "class",
2533 "filename": "${root_path}/completion_extras.php",
2536 "base_class": "\\Elsa",
2543 "inlineDetail": "Elsa",
2545 "insertText": "Bard",
2546 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2549 "filename": "${root_path}/completion_extras.php",
2552 "base_class": "\\Elsa",
2559 "inlineDetail": "Elsa",
2560 "sortText": "Alonso",
2561 "insertText": "Alonso",
2562 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2564 "fullname": "Alonso",
2565 "filename": "${root_path}/completion_extras.php",
2568 "base_class": "\\Elsa",
2574 "detail": "function(mixed $value): bool",
2575 "inlineDetail": "(mixed $value)",
2577 "sortText": "isValid",
2578 "insertText": "isValid(${1:\\$value})",
2579 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2581 "fullname": "isValid",
2582 "filename": "${hhi_path}/BuiltinEnum.hhi",
2585 "base_class": "\\Elsa",
2589 "label": "getValues",
2591 "detail": "function(): darray<string, Elsa>",
2592 "inlineDetail": "()",
2593 "itemType": "darray<string, Elsa>",
2594 "sortText": "getValues",
2595 "insertText": "getValues()",
2596 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2598 "fullname": "getValues",
2599 "filename": "${hhi_path}/BuiltinEnum.hhi",
2602 "base_class": "\\Elsa",
2606 "label": "getNames",
2608 "detail": "function(): darray<Elsa, string>",
2609 "inlineDetail": "()",
2610 "itemType": "darray<Elsa, string>",
2611 "sortText": "getNames",
2612 "insertText": "getNames()",
2613 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2615 "fullname": "getNames",
2616 "filename": "${hhi_path}/BuiltinEnum.hhi",
2619 "base_class": "\\Elsa",
2625 "detail": "function(mixed $value): ?Elsa",
2626 "inlineDetail": "(mixed $value)",
2627 "itemType": "?Elsa",
2628 "sortText": "coerce",
2629 "insertText": "coerce(${1:\\$value})",
2630 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2632 "fullname": "coerce",
2633 "filename": "${hhi_path}/BuiltinEnum.hhi",
2636 "base_class": "\\Elsa",
2640 "label": "assertAll",
2642 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
2643 "inlineDetail": "(Traversable<mixed> $values)",
2644 "itemType": "Container<Elsa>",
2645 "sortText": "assertAll",
2646 "insertText": "assertAll(${1:\\$values})",
2647 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2649 "fullname": "assertAll",
2650 "filename": "${hhi_path}/BuiltinEnum.hhi",
2653 "base_class": "\\Elsa",
2659 "detail": "function(mixed $value): Elsa",
2660 "inlineDetail": "(mixed $value)",
2662 "sortText": "assert",
2663 "insertText": "assert(${1:\\$value})",
2664 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2666 "fullname": "assert",
2667 "filename": "${hhi_path}/BuiltinEnum.hhi",
2670 "base_class": "\\Elsa",
2675 powered_by
="serverless_ide",
2678 comment
="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
2679 method
="textDocument/didChange",
2681 "textDocument": {"uri": "${php_file_uri}"},
2685 "start": {"line": 3, "character": 0},
2686 "end": {"line": 3, "character": 35},
2688 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
2695 comment
="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
2696 method
="textDocument/completion",
2698 "textDocument": {"uri": "${php_file_uri}"},
2699 "position": {"line": 3, "character": 42},
2701 result
={"isIncomplete": False, "items": []},
2702 powered_by
="serverless_ide",
2705 comment
="Add 'DeprecatedClass::'",
2706 method
="textDocument/didChange",
2708 "textDocument": {"uri": "${php_file_uri}"},
2712 "start": {"line": 3, "character": 0},
2713 "end": {"line": 3, "character": 41},
2715 "text": "DeprecatedClass::",
2722 comment
="autocomplete after 'DeprecatedClass::'",
2723 method
="textDocument/completion",
2725 "textDocument": {"uri": "${php_file_uri}"},
2726 "position": {"line": 3, "character": 17},
2729 "isIncomplete": False,
2734 "detail": "classname<this>",
2735 "inlineDetail": "classname<this>",
2736 "sortText": "class",
2737 "insertText": "class",
2738 "insertTextFormat": InsertTextFormat
.PlainText
.value
,
2740 "fullname": "class",
2741 "filename": "${root_path}/completion_extras.php",
2744 "base_class": "\\DeprecatedClass",
2748 "label": "test_do_not_use",
2750 "detail": "function(): void",
2751 "inlineDetail": "()",
2753 "sortText": "~test_do_not_use",
2754 "insertText": "test_do_not_use()",
2755 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2757 "fullname": "test_do_not_use",
2758 "filename": "${root_path}/completion_extras.php",
2761 "base_class": "\\DeprecatedClass",
2767 "detail": "function(): void",
2768 "inlineDetail": "()",
2770 "sortText": "getName",
2771 "insertText": "getName()",
2772 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2774 "fullname": "getName",
2775 "filename": "${root_path}/completion_extras.php",
2778 "base_class": "\\DeprecatedClass",
2782 "label": "getAttributes_DO_NOT_USE",
2784 "detail": "function(): void",
2785 "inlineDetail": "()",
2787 "sortText": "~getAttributes_DO_NOT_USE",
2788 "insertText": "getAttributes_DO_NOT_USE()",
2789 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2791 "fullname": "getAttributes_DO_NOT_USE",
2792 "filename": "${root_path}/completion_extras.php",
2795 "base_class": "\\DeprecatedClass",
2799 "label": "__getLoader",
2801 "detail": "function(): void",
2802 "inlineDetail": "()",
2804 "sortText": "~__getLoader",
2805 "insertText": "__getLoader()",
2806 "insertTextFormat": InsertTextFormat
.Snippet
.value
,
2808 "fullname": "__getLoader",
2809 "filename": "${root_path}/completion_extras.php",
2812 "base_class": "\\DeprecatedClass",
2817 powered_by
="serverless_ide",
2819 .request(line
=line(), method
="shutdown", params
={}, result
=None)
2820 .notification(method
="exit", params
={})
2822 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
2824 def test_serverless_ide_definition(self
) -> None:
2825 variables
= dict(self
.prepare_serverless_ide_environment())
2826 variables
.update(self
.setup_php_file("definition.php"))
2827 self
.test_driver
.stop_hh_server()
2830 self
.initialize_spec(
2831 LspTestSpec("serverless_ide_definition"), use_serverless_ide
=True
2834 method
="textDocument/didOpen",
2837 "uri": "${php_file_uri}",
2838 "languageId": "hack",
2840 "text": "${php_file}",
2846 comment
="call to `b_definition`",
2847 method
="textDocument/definition",
2849 "textDocument": {"uri": "${php_file_uri}"},
2850 "position": {"line": 3, "character": 10},
2854 "uri": "file://${root_path}/definition.php",
2856 "start": {"line": 6, "character": 9},
2857 "end": {"line": 6, "character": 21},
2859 "title": "b_definition",
2862 powered_by
="serverless_ide",
2866 comment
="call to `new BB(1)`",
2867 method
="textDocument/definition",
2869 "textDocument": {"uri": "${php_file_uri}"},
2870 "position": {"line": 29, "character": 13},
2874 "uri": "file://${root_path}/definition.php",
2876 "start": {"line": 11, "character": 18},
2877 "end": {"line": 11, "character": 29},
2879 "title": "BB::__construct",
2882 powered_by
="serverless_ide",
2886 comment
="call to `new CC(1)`",
2887 method
="textDocument/definition",
2889 "textDocument": {"uri": "${php_file_uri}"},
2890 "position": {"line": 30, "character": 13},
2894 "uri": "file://${root_path}/definition.php",
2896 "start": {"line": 14, "character": 6},
2897 "end": {"line": 14, "character": 8},
2902 "uri": "file://${root_path}/definition.php",
2904 "start": {"line": 11, "character": 18},
2905 "end": {"line": 11, "character": 29},
2907 "title": "BB::__construct",
2910 powered_by
="serverless_ide",
2914 comment
="call to `new DD(1)`",
2915 method
="textDocument/definition",
2917 "textDocument": {"uri": "${php_file_uri}"},
2918 "position": {"line": 31, "character": 13},
2922 "uri": "file://${root_path}/definition.php",
2924 "start": {"line": 17, "character": 6},
2925 "end": {"line": 17, "character": 8},
2930 "uri": "file://${root_path}/definition.php",
2932 "start": {"line": 11, "character": 18},
2933 "end": {"line": 11, "character": 29},
2935 "title": "BB::__construct",
2938 powered_by
="serverless_ide",
2942 comment
="call to `new EE(1)`",
2943 method
="textDocument/definition",
2945 "textDocument": {"uri": "${php_file_uri}"},
2946 "position": {"line": 32, "character": 13},
2950 "uri": "file://${root_path}/definition.php",
2952 "start": {"line": 21, "character": 18},
2953 "end": {"line": 21, "character": 29},
2955 "title": "EE::__construct",
2958 powered_by
="serverless_ide",
2962 comment
="call to `new FF(1)`",
2963 method
="textDocument/definition",
2965 "textDocument": {"uri": "${php_file_uri}"},
2966 "position": {"line": 33, "character": 13},
2970 "uri": "file://${root_path}/definition.php",
2972 "start": {"line": 26, "character": 6},
2973 "end": {"line": 26, "character": 8},
2978 powered_by
="serverless_ide",
2982 comment
="call to `new TakesString(HasString::MyString)`",
2983 method
="textDocument/definition",
2985 "textDocument": {"uri": "${php_file_uri}"},
2986 "position": {"line": 45, "character": 23},
2990 "uri": "file://${root_path}/definition.php",
2992 "start": {"line": 40, "character": 6},
2993 "end": {"line": 40, "character": 15},
2995 "title": "HasString",
2998 powered_by
="serverless_ide",
3001 comment
="make local, unsaved change to the file",
3002 method
="textDocument/didChange",
3004 "textDocument": {"uri": "${php_file_uri}", "version": 2},
3009 "start": {"line": 3, "character": 9},
3010 "end": {"line": 3, "character": 21},
3018 comment
="call to `test` instead of `b_definition`",
3019 method
="textDocument/definition",
3021 "textDocument": {"uri": "${php_file_uri}"},
3022 "position": {"line": 3, "character": 10},
3026 "uri": "file://${root_path}/definition.php",
3028 "start": {"line": 28, "character": 9},
3029 "end": {"line": 28, "character": 13},
3034 powered_by
="serverless_ide",
3036 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3037 .notification(method
="exit", params
={})
3039 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3041 def test_serverless_ide_overridden_definition(self
) -> None:
3042 variables
= dict(self
.prepare_serverless_ide_environment())
3043 variables
.update(self
.setup_php_file("override.php"))
3044 self
.test_driver
.stop_hh_server()
3047 self
.initialize_spec(
3048 LspTestSpec("serverless_ide_overridden_definition"),
3049 use_serverless_ide
=True,
3052 method
="textDocument/didOpen",
3055 "uri": "${php_file_uri}",
3056 "languageId": "hack",
3058 "text": "${php_file}",
3064 comment
="find overridden method from trait. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3065 method
="textDocument/definition",
3067 "textDocument": {"uri": "${php_file_uri}"},
3068 "position": {"line": 13, "character": 5},
3072 "uri": "file://${root_path}/override.php",
3074 "start": {"line": 7, "character": 18},
3075 "end": {"line": 7, "character": 21},
3077 "title": "MyTrait::foo",
3080 powered_by
="serverless_ide",
3084 comment
="find overridden static method. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3085 method
="textDocument/definition",
3087 "textDocument": {"uri": "${php_file_uri}"},
3088 "position": {"line": 26, "character": 5},
3092 "uri": "file://${root_path}/override.php",
3094 "start": {"line": 23, "character": 25},
3095 "end": {"line": 23, "character": 28},
3100 powered_by
="serverless_ide",
3104 comment
="find overridden interface method",
3105 method
="textDocument/definition",
3107 "textDocument": {"uri": "${php_file_uri}"},
3108 "position": {"line": 35, "character": 5},
3112 "uri": "file://${root_path}/override.php",
3114 "start": {"line": 32, "character": 18},
3115 "end": {"line": 32, "character": 22},
3117 "title": "I1::quux",
3120 powered_by
="serverless_ide",
3122 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3123 .notification(method
="exit", params
={})
3125 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3127 def test_serverless_ide_document_symbol(self
) -> None:
3128 variables
= dict(self
.prepare_serverless_ide_environment())
3129 variables
.update(self
.setup_php_file("definition.php"))
3130 self
.test_driver
.stop_hh_server()
3133 self
.initialize_spec(
3134 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide
=True
3137 method
="textDocument/didOpen",
3140 "uri": "${php_file_uri}",
3141 "languageId": "hack",
3143 "text": "${php_file}",
3149 comment
="documentSymbol call",
3150 method
="textDocument/documentSymbol",
3151 params
={"textDocument": {"uri": "${php_file_uri}"}},
3154 "name": "testClassMemberInsideConstructorInvocation",
3157 "uri": "file://${root_path}/definition.php",
3159 "start": {"line": 44, "character": 0},
3160 "end": {"line": 46, "character": 1},
3168 "uri": "file://${root_path}/definition.php",
3170 "start": {"line": 41, "character": 8},
3171 "end": {"line": 41, "character": 29},
3174 "containerName": "HasString",
3177 "name": "HasString",
3180 "uri": "file://${root_path}/definition.php",
3182 "start": {"line": 40, "character": 0},
3183 "end": {"line": 42, "character": 1},
3188 "name": "__construct",
3191 "uri": "file://${root_path}/definition.php",
3193 "start": {"line": 37, "character": 2},
3194 "end": {"line": 37, "character": 43},
3197 "containerName": "TakesString",
3200 "name": "TakesString",
3203 "uri": "file://${root_path}/definition.php",
3205 "start": {"line": 36, "character": 0},
3206 "end": {"line": 38, "character": 1},
3214 "uri": "file://${root_path}/definition.php",
3216 "start": {"line": 26, "character": 0},
3217 "end": {"line": 26, "character": 11},
3222 "name": "__construct",
3225 "uri": "file://${root_path}/definition.php",
3227 "start": {"line": 21, "character": 2},
3228 "end": {"line": 23, "character": 3},
3231 "containerName": "EE",
3237 "uri": "file://${root_path}/definition.php",
3239 "start": {"line": 20, "character": 0},
3240 "end": {"line": 24, "character": 1},
3248 "uri": "file://${root_path}/definition.php",
3250 "start": {"line": 14, "character": 0},
3251 "end": {"line": 15, "character": 1},
3256 "name": "__construct",
3259 "uri": "file://${root_path}/definition.php",
3261 "start": {"line": 11, "character": 2},
3262 "end": {"line": 11, "character": 40},
3265 "containerName": "BB",
3271 "uri": "file://${root_path}/definition.php",
3273 "start": {"line": 10, "character": 0},
3274 "end": {"line": 12, "character": 1},
3279 "name": "a_definition",
3282 "uri": "file://${root_path}/definition.php",
3284 "start": {"line": 2, "character": 0},
3285 "end": {"line": 4, "character": 1},
3290 "name": "b_definition",
3293 "uri": "file://${root_path}/definition.php",
3295 "start": {"line": 6, "character": 0},
3296 "end": {"line": 8, "character": 1},
3304 "uri": "file://${root_path}/definition.php",
3306 "start": {"line": 17, "character": 0},
3307 "end": {"line": 18, "character": 1},
3315 "uri": "file://${root_path}/definition.php",
3317 "start": {"line": 28, "character": 0},
3318 "end": {"line": 34, "character": 1},
3323 powered_by
="serverless_ide",
3325 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3326 .notification(method
="exit", params
={})
3328 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3330 def initialize_spec(
3333 use_serverless_ide
: bool,
3334 supports_status
: bool = False, # does the caller wish to see all status messages?
3335 supports_init
: bool = False, # do we wish to interact with init, rather than waiting for init ok?
3337 if use_serverless_ide
:
3338 initialization_options
= {
3339 "namingTableSavedStatePath": "${naming_table_saved_state_path}",
3340 "namingTableSavedStateTestDelay": 0.0,
3343 # A small delay, since otherwise init completes immediately
3344 # This isn't very racy. All we need is a tiny delay so that
3345 # other things which are in the queue get processed, rather
3346 # than continuing synchronously
3347 initialization_options
["namingTableSavedStateTestDelay"] = 0.5
3349 initialization_options
= {}
3351 window_capabilities
= {}
3353 window_capabilities
["status"] = {"dynamicRegistration": False}
3355 spec
= spec
.ignore_notifications(method
="telemetry/event").request(
3357 method
="initialize",
3359 "initializationOptions": initialization_options
,
3361 "rootPath": "${root_path}",
3363 "window": window_capabilities
,
3365 "completion": {"completionItem": {"snippetSupport": True}}
3371 "textDocumentSync": {
3375 "willSaveWaitUntil": False,
3376 "save": {"includeText": False},
3378 "hoverProvider": True,
3379 "completionProvider": {
3380 "resolveProvider": True,
3381 "triggerCharacters": ["$", ">", "\\", ":", "<", "[", "'", '"'],
3383 "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
3384 "definitionProvider": True,
3385 "typeDefinitionProvider": True,
3386 "referencesProvider": True,
3387 "documentHighlightProvider": True,
3388 "documentSymbolProvider": True,
3389 "workspaceSymbolProvider": True,
3390 "codeActionProvider": False,
3391 "documentFormattingProvider": True,
3392 "documentRangeFormattingProvider": True,
3393 "documentOnTypeFormattingProvider": {
3394 "firstTriggerCharacter": ";",
3395 "moreTriggerCharacter": ["}"],
3397 "renameProvider": True,
3398 "implementationProvider": True,
3399 "typeCoverageProvider": True,
3400 "rageProvider": True,
3404 if use_serverless_ide
:
3405 spec
= spec
.wait_for_server_request(
3406 method
="client/registerCapability",
3410 "id": "did-change-watched-files",
3411 "method": "workspace/didChangeWatchedFiles",
3412 "registerOptions": {
3413 "watchers": [{"globPattern": "**", "kind": 7}]
3420 if not supports_status
:
3421 spec
= spec
.ignore_status_diagnostics(True)
3423 if use_serverless_ide
and not supports_init
:
3424 spec
= spec
.wait_for_notification(
3425 comment
="wait for sIDE to finish init",
3426 method
="telemetry/event",
3427 params
={"type": 4, "message": "[client-ide] Finished init: ok"},
3432 def test_serverless_ide_type_definition(self
) -> None:
3433 variables
= dict(self
.prepare_serverless_ide_environment())
3434 variables
.update(self
.setup_php_file("type_definition.php"))
3435 self
.test_driver
.stop_hh_server()
3438 self
.initialize_spec(
3439 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide
=True
3442 method
="textDocument/didOpen",
3445 "uri": "${php_file_uri}",
3446 "languageId": "hack",
3448 "text": "${php_file}",
3454 comment
="Conditional Type Definition of HH or II",
3455 method
="textDocument/typeDefinition",
3457 "textDocument": {"uri": "${php_file_uri}"},
3458 "position": {"line": 32, "character": 2},
3462 "uri": "${php_file_uri}",
3464 "start": {"line": 2, "character": 6},
3465 "end": {"line": 2, "character": 8},
3470 "uri": "${php_file_uri}",
3472 "start": {"line": 12, "character": 6},
3473 "end": {"line": 12, "character": 8},
3478 powered_by
="serverless_ide",
3482 comment
="Standard Class Definition",
3483 method
="textDocument/typeDefinition",
3485 "textDocument": {"uri": "${php_file_uri}"},
3486 "position": {"line": 40, "character": 2},
3490 "uri": "${php_file_uri}",
3492 "start": {"line": 2, "character": 6},
3493 "end": {"line": 2, "character": 8},
3498 powered_by
="serverless_ide",
3502 comment
="Class Type Definition with Casting",
3503 method
="textDocument/typeDefinition",
3505 "textDocument": {"uri": "${php_file_uri}"},
3506 "position": {"line": 41, "character": 2},
3510 "uri": "${php_file_uri}",
3512 "start": {"line": 2, "character": 6},
3513 "end": {"line": 2, "character": 8},
3518 powered_by
="serverless_ide",
3522 comment
="Primitive Type Definition",
3523 method
="textDocument/typeDefinition",
3525 "textDocument": {"uri": "${php_file_uri}"},
3526 "position": {"line": 42, "character": 2},
3529 powered_by
="serverless_ide",
3533 comment
="Function Return Type Definition",
3534 method
="textDocument/typeDefinition",
3536 "textDocument": {"uri": "${php_file_uri}"},
3537 "position": {"line": 43, "character": 2},
3541 "uri": "${php_file_uri}",
3543 "start": {"line": 12, "character": 6},
3544 "end": {"line": 12, "character": 8},
3549 powered_by
="serverless_ide",
3553 comment
="Function definition with primitive return type",
3554 method
="textDocument/typeDefinition",
3556 "textDocument": {"uri": "${php_file_uri}"},
3557 "position": {"line": 44, "character": 2},
3561 "uri": "${php_file_uri}",
3563 "start": {"line": 22, "character": 9},
3564 "end": {"line": 22, "character": 29},
3566 "title": "(function(): int)",
3569 powered_by
="serverless_ide",
3571 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3572 .notification(method
="exit", params
={})
3574 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3576 def test_serverless_ide_hover(self
) -> None:
3577 variables
= dict(self
.prepare_serverless_ide_environment())
3578 variables
.update(self
.setup_php_file("hover.php"))
3579 self
.test_driver
.stop_hh_server()
3582 self
.initialize_spec(
3583 LspTestSpec("serverless_ide_hover"), use_serverless_ide
=True
3586 method
="textDocument/didOpen",
3589 "uri": "${php_file_uri}",
3590 "languageId": "hack",
3592 "text": "${php_file}",
3598 comment
="hover over function invocation",
3599 method
="textDocument/hover",
3601 "textDocument": {"uri": "${php_file_uri}"},
3602 "position": {"line": 3, "character": 16},
3606 {"language": "hack", "value": "int"},
3607 "A comment describing b_hover.",
3610 "start": {"line": 3, "character": 9},
3611 "end": {"line": 3, "character": 16},
3614 powered_by
="serverless_ide",
3618 comment
="hover over string literal outside call",
3619 method
="textDocument/hover",
3621 "textDocument": {"uri": "${php_file_uri}"},
3622 "position": {"line": 25, "character": 12}, # 9 - 16
3624 result
={"contents": [{"language": "hack", "value": "string"}]},
3625 powered_by
="serverless_ide",
3629 comment
="hover over string literal inside call",
3630 method
="textDocument/hover",
3632 "textDocument": {"uri": "${php_file_uri}"},
3633 "position": {"line": 26, "character": 20}, # 16 - 29
3635 result
={"contents": [{"language": "hack", "value": "string"}]},
3636 powered_by
="serverless_ide",
3640 comment
="hover over int literal inside call",
3641 method
="textDocument/hover",
3643 "textDocument": {"uri": "${php_file_uri}"},
3644 "position": {"line": 26, "character": 32}, # 31 - 33
3646 result
={"contents": [{"language": "hack", "value": "int"}]},
3647 powered_by
="serverless_ide",
3651 comment
="hover over constant reference",
3652 method
="textDocument/hover",
3654 "textDocument": {"uri": "${php_file_uri}"},
3655 "position": {"line": 15, "character": 19},
3659 {"language": "hack", "value": "THE_ANSWER"},
3660 "A comment describing THE_ANSWER",
3661 "int THE_ANSWER = 42",
3664 "start": {"line": 15, "character": 9},
3665 "end": {"line": 15, "character": 19},
3668 powered_by
="serverless_ide",
3672 comment
="hover over whitespace",
3673 method
="textDocument/hover",
3675 "textDocument": {"uri": "${php_file_uri}"},
3676 "position": {"line": 3, "character": 1},
3679 powered_by
="serverless_ide",
3683 comment
="hover over a keyword",
3684 method
="textDocument/hover",
3686 "textDocument": {"uri": "${php_file_uri}"},
3687 "position": {"line": 2, "character": 1},
3690 powered_by
="serverless_ide",
3694 comment
="hover over a comment",
3695 method
="textDocument/hover",
3697 "textDocument": {"uri": "${php_file_uri}"},
3698 "position": {"line": 1, "character": 4},
3701 powered_by
="serverless_ide",
3705 comment
="hover past the end of a line",
3706 method
="textDocument/hover",
3708 "textDocument": {"uri": "${php_file_uri}"},
3709 "position": {"line": 3, "character": 100},
3712 powered_by
="serverless_ide",
3716 comment
="hover past the end of a file",
3717 method
="textDocument/hover",
3719 "textDocument": {"uri": "${php_file_uri}"},
3720 "position": {"line": 300, "character": 0},
3723 powered_by
="serverless_ide",
3727 comment
="hover over class with copyright docblock",
3728 method
="textDocument/hover",
3730 "textDocument": {"uri": "${php_file_uri}"},
3731 "position": {"line": 37, "character": 15},
3735 {"language": "hack", "value": "final class CopyrightClass"},
3736 "Testing copyright removal",
3739 "start": {"line": 37, "character": 2},
3740 "end": {"line": 37, "character": 16},
3743 powered_by
="serverless_ide",
3747 comment
="hover over class with generated docblock",
3748 method
="textDocument/hover",
3750 "textDocument": {"uri": "${php_file_uri}"},
3751 "position": {"line": 58, "character": 15},
3755 {"language": "hack", "value": "final class GeneratedClass"},
3756 "Testing generated text removal",
3759 "start": {"line": 58, "character": 2},
3760 "end": {"line": 58, "character": 16},
3763 powered_by
="serverless_ide",
3765 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3766 .notification(method
="exit", params
={})
3768 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3770 def test_serverless_ide_file_touched_on_disk(self
) -> None:
3771 variables
= dict(self
.prepare_serverless_ide_environment())
3772 variables
.update(self
.setup_php_file("hover.php"))
3773 self
.test_driver
.stop_hh_server()
3776 self
.initialize_spec(
3777 LspTestSpec("serverless_ide_file_on_disk_change"),
3778 use_serverless_ide
=True,
3781 method
="textDocument/didOpen",
3784 "uri": "${php_file_uri}",
3785 "languageId": "hack",
3787 "text": "${php_file}",
3792 method
="workspace/didChangeWatchedFiles",
3793 params
={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
3795 .wait_for_notification(
3796 comment
="wait for sIDE to process file change",
3797 method
="telemetry/event",
3800 "message": "[client-ide] Done processing file changes",
3805 method
="textDocument/hover",
3807 "textDocument": {"uri": "${php_file_uri}"},
3808 "position": {"line": 3, "character": 16},
3812 {"language": "hack", "value": "int"},
3813 "A comment describing b_hover.",
3816 "start": {"line": 3, "character": 9},
3817 "end": {"line": 3, "character": 16},
3820 powered_by
="serverless_ide",
3822 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3823 .notification(method
="exit", params
={})
3825 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3827 def test_serverless_ide_file_hover_with_errors(self
) -> None:
3828 variables
= dict(self
.prepare_serverless_ide_environment())
3829 variables
.update(self
.setup_php_file("hover_with_errors.php"))
3830 self
.test_driver
.stop_hh_server()
3833 self
.initialize_spec(
3834 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide
=True
3837 method
="textDocument/didOpen",
3840 "uri": "${php_file_uri}",
3841 "languageId": "hack",
3843 "text": "${php_file}",
3848 method
="workspace/didChangeWatchedFiles",
3849 params
={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
3851 .wait_for_notification(
3852 comment
="wait for sIDE to process file change",
3853 method
="telemetry/event",
3856 "message": "[client-ide] Done processing file changes",
3861 comment
="Totally normal hover",
3862 method
="textDocument/hover",
3864 "textDocument": {"uri": "${php_file_uri}"},
3865 "position": {"line": 14, "character": 37},
3871 "value": "public static function staticMethod(string $z): void",
3873 'During testing, we\'ll remove the "public" tag from this '
3875 "to ensure that we can still get IDE services",
3876 "Return type: `void`",
3877 "Full name: `HoverWithErrorsClass::staticMethod`",
3880 "end": {"character": 39, "line": 14},
3881 "start": {"character": 27, "line": 14},
3884 powered_by
="serverless_ide",
3887 comment
="Remove the 'public' visibility modifier which triggers AST->AAST errors",
3888 method
="textDocument/didChange",
3890 "textDocument": {"uri": "${php_file_uri}"},
3894 "start": {"line": 10, "character": 2},
3895 "end": {"line": 10, "character": 8},
3904 comment
="Hover should still work even if visibility modifier has been removed",
3905 method
="textDocument/hover",
3907 "textDocument": {"uri": "${php_file_uri}"},
3908 "position": {"line": 14, "character": 37},
3914 "value": "public static function staticMethod(string $z): void",
3916 'During testing, we\'ll remove the "public" tag from this '
3918 "to ensure that we can still get IDE services",
3919 "Return type: `void`",
3920 "Full name: `HoverWithErrorsClass::staticMethod`",
3923 "end": {"character": 39, "line": 14},
3924 "start": {"character": 27, "line": 14},
3927 powered_by
="serverless_ide",
3929 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3930 .notification(method
="exit", params
={})
3932 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3934 def test_serverless_ide_formatting(self
) -> None:
3935 # This test will fail if hackfmt can't be found
3936 if not self
.test_driver
.run_hackfmt_check():
3937 raise unittest
.SkipTest("Hackfmt can't be found. Skipping.")
3939 variables
= dict(self
.prepare_serverless_ide_environment())
3940 variables
.update(self
.setup_php_file("messy.php"))
3942 self
.test_driver
.stop_hh_server()
3945 self
.initialize_spec(LspTestSpec("formatting"), use_serverless_ide
=True)
3947 method
="textDocument/didOpen",
3950 "uri": "${php_file_uri}",
3951 "languageId": "hack",
3953 "text": "${php_file}",
3959 method
="textDocument/formatting",
3961 "textDocument": {"uri": "${php_file_uri}"},
3962 "options": {"tabSize": 5, "insertSpaces": True},
3967 "start": {"line": 0, "character": 0},
3968 "end": {"line": 15, "character": 0},
3970 "newText": "<?hh //strict\n\nfunction x(): string {\n"
3971 + " /* @lint-ignore TXT2 3 tabs on purpose */\n"
3972 + ' $a = "this";\n\n'
3973 + " /* @lint-ignore TXT2 2 tabs on purpose */\n"
3975 + " /* lint-ignore TXT2 1 tab on purpose */\n"
3976 + ' $c = "messy"; // 1 tab\n\n'
3977 + ' $d = "."; // 4 spaces\n'
3978 + ' return "$a"."$b"."$c"."d";\n}\n',
3982 .request(line
=line(), method
="shutdown", params
={}, result
=None)
3983 .notification(method
="exit", params
={})
3985 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
3987 def test_serverless_ide_rangeformatting(self
) -> None:
3988 # This test will fail if hackfmt can't be found
3989 if not self
.test_driver
.run_hackfmt_check():
3990 raise unittest
.SkipTest("Hackfmt can't be found. Skipping.")
3992 variables
= dict(self
.prepare_serverless_ide_environment())
3993 variables
.update(self
.setup_php_file("messy.php"))
3995 self
.test_driver
.stop_hh_server()
3998 self
.initialize_spec(
3999 LspTestSpec("range_formatting"), use_serverless_ide
=True
4002 method
="textDocument/didOpen",
4005 "uri": "${php_file_uri}",
4006 "languageId": "hack",
4008 "text": "${php_file}",
4014 method
="textDocument/rangeFormatting",
4016 "textDocument": {"uri": "${php_file_uri}"},
4018 "start": {"line": 4, "character": 0},
4019 "end": {"line": 5, "character": 0},
4021 "options": {"tabSize": 5, "insertSpaces": True},
4026 "start": {"line": 4, "character": 0},
4027 "end": {"line": 5, "character": 0},
4029 "newText": ' $a = "this";\n',
4033 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4034 .notification(method
="exit", params
={})
4036 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
4038 def test_serverless_ide_ontypeformatting(self
) -> None:
4039 # This test will fail if hackfmt can't be found
4040 if not self
.test_driver
.run_hackfmt_check():
4041 raise unittest
.SkipTest("Hackfmt can't be found. Skipping.")
4043 variables
= dict(self
.prepare_serverless_ide_environment())
4044 variables
.update(self
.setup_php_file("ontypeformatting.php"))
4047 self
.initialize_spec(
4048 LspTestSpec("ontypeformatting"), use_serverless_ide
=True
4051 method
="textDocument/didOpen",
4054 "uri": "${php_file_uri}",
4055 "languageId": "hack",
4057 "text": "${php_file}",
4063 method
="textDocument/onTypeFormatting",
4065 "textDocument": {"uri": "${php_file_uri}"},
4066 "position": {"line": 9, "character": 58},
4068 "options": {"tabSize": 2, "insertSpaces": True},
4073 "start": {"line": 5, "character": 17},
4074 "end": {"line": 9, "character": 58},
4076 "newText": "{\n test_otf(\n"
4077 + " '1234567890',\n"
4078 + " '1234567890',\n"
4079 + " '1234567890',\n"
4080 + " '1234567890',\n"
4081 + " '1234567890',\n"
4082 + " '1234567890',\n );",
4088 method
="textDocument/onTypeFormatting",
4090 "textDocument": {"uri": "${php_file_uri}"},
4091 "position": {"line": 13, "character": 1},
4093 "options": {"tabSize": 2, "insertSpaces": True},
4098 "start": {"line": 13, "character": 0},
4099 "end": {"line": 13, "character": 1},
4107 method
="textDocument/onTypeFormatting",
4109 "textDocument": {"uri": "${php_file_uri}"},
4110 "position": {"line": 15, "character": 16},
4112 "options": {"tabSize": 2, "insertSpaces": True},
4117 "start": {"line": 15, "character": 0},
4118 "end": {"line": 15, "character": 16},
4120 "newText": "function otf() {}",
4124 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4125 .notification(method
="exit", params
={})
4128 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
4130 def test_did_change(self
) -> None:
4131 self
.prepare_server_environment()
4132 variables
= self
.setup_php_file("didchange.php")
4134 self
.initialize_spec(LspTestSpec("did_change"), use_serverless_ide
=False)
4135 .wait_for_hh_server_ready()
4137 method
="textDocument/didOpen",
4140 "uri": "${php_file_uri}",
4141 "languageId": "hack",
4143 "text": "${php_file}",
4148 method
="textDocument/didChange",
4150 "textDocument": {"uri": "${php_file_uri}"},
4154 "start": {"line": 7, "character": 11},
4155 "end": {"line": 7, "character": 12},
4162 .wait_for_notification(
4163 method
="textDocument/publishDiagnostics",
4165 "uri": "${php_file_uri}",
4169 "start": {"line": 7, "character": 11},
4170 "end": {"line": 7, "character": 11},
4175 "message": "A semicolon ; is expected here.",
4176 "relatedLocations": [],
4177 "relatedInformation": [],
4182 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4183 .wait_for_notification(
4184 comment
="Hack appears to clear out diagnostics before shutting down",
4185 method
="textDocument/publishDiagnostics",
4186 params
={"uri": "${php_file_uri}", "diagnostics": []},
4188 .notification(method
="exit", params
={})
4190 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
4192 def test_go_to_implementation(self
) -> None:
4193 self
.prepare_server_environment()
4194 variables
= self
.setup_php_file("go_to_implementation.php")
4196 self
.initialize_spec(
4197 LspTestSpec("test_go_to_implementation"), use_serverless_ide
=False
4199 .wait_for_hh_server_ready()
4201 method
="textDocument/didOpen",
4204 "uri": "${php_file_uri}",
4205 "languageId": "hack",
4207 "text": "${php_file}",
4213 comment
="go to implemenetation: abstract class",
4214 method
="textDocument/implementation",
4216 "textDocument": {"uri": "${php_file_uri}"},
4217 "position": {"line": 1, "character": 17},
4221 "uri": "${php_file_uri}",
4223 "start": {"line": 7, "character": 6},
4224 "end": {"line": 7, "character": 9},
4231 comment
="go to implemenetation: interface",
4232 method
="textDocument/implementation",
4234 "textDocument": {"uri": "${php_file_uri}"},
4235 "position": {"line": 13, "character": 13},
4239 "uri": "${php_file_uri}",
4241 "start": {"line": 17, "character": 6},
4242 "end": {"line": 17, "character": 9},
4249 comment
="go to implemenetation: trait",
4250 method
="textDocument/implementation",
4252 "textDocument": {"uri": "${php_file_uri}"},
4253 "position": {"line": 23, "character": 10},
4257 "uri": "${php_file_uri}",
4259 "start": {"line": 30, "character": 6},
4260 "end": {"line": 30, "character": 16},
4267 comment
="go to implemenetation: method",
4268 method
="textDocument/implementation",
4270 "textDocument": {"uri": "${php_file_uri}"},
4271 "position": {"line": 19, "character": 18},
4275 "uri": "${php_file_uri}",
4277 "start": {"line": 8, "character": 18},
4278 "end": {"line": 8, "character": 22},
4283 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4284 .notification(method
="exit", params
={})
4286 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
4288 def test_signature_help(self
) -> None:
4289 self
.prepare_server_environment()
4290 variables
= self
.setup_php_file("signaturehelp.php")
4292 self
.initialize_spec(
4293 LspTestSpec("test_signature_help"), use_serverless_ide
=False
4295 .wait_for_hh_server_ready()
4297 method
="textDocument/didOpen",
4300 "uri": "${php_file_uri}",
4301 "languageId": "hack",
4303 "text": "${php_file}",
4309 comment
="signature help for 0-argument constructor"
4310 " (left of opening paren)",
4311 method
="textDocument/signatureHelp",
4313 "textDocument": {"uri": "${php_file_uri}"},
4314 "position": {"line": 16, "character": 18},
4320 comment
="signature help for 0-argument constructor",
4321 method
="textDocument/signatureHelp",
4323 "textDocument": {"uri": "${php_file_uri}"},
4324 "position": {"line": 16, "character": 19},
4329 "label": "public function __construct(): void",
4330 "documentation": "Constructor with doc block",
4334 "activeSignature": 0,
4335 "activeParameter": 0,
4340 comment
="signature help for 0-argument constructor"
4341 " (right of closing paren)",
4342 method
="textDocument/signatureHelp",
4344 "textDocument": {"uri": "${php_file_uri}"},
4345 "position": {"line": 16, "character": 20},
4351 comment
="signature help for 2-argument instance method"
4352 " (left of opening paren)",
4353 method
="textDocument/signatureHelp",
4355 "textDocument": {"uri": "${php_file_uri}"},
4356 "position": {"line": 17, "character": 20},
4362 comment
="signature help for 2-argument instance method"
4363 " (right of opening paren)",
4364 method
="textDocument/signatureHelp",
4366 "textDocument": {"uri": "${php_file_uri}"},
4367 "position": {"line": 17, "character": 21},
4372 "label": "public function instanceMethod"
4373 "(int $x1, int $x2): void",
4374 "documentation": "Instance method with doc block",
4375 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4378 "activeSignature": 0,
4379 "activeParameter": 0,
4384 comment
="signature help for 2-argument instance method"
4385 " (left of first comma)",
4386 method
="textDocument/signatureHelp",
4388 "textDocument": {"uri": "${php_file_uri}"},
4389 "position": {"line": 17, "character": 22},
4394 "label": "public function instanceMethod"
4395 "(int $x1, int $x2): void",
4396 "documentation": "Instance method with doc block",
4397 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4400 "activeSignature": 0,
4401 "activeParameter": 1,
4406 comment
="signature help for 2-argument instance method"
4407 " (right of first comma)",
4408 method
="textDocument/signatureHelp",
4410 "textDocument": {"uri": "${php_file_uri}"},
4411 "position": {"line": 17, "character": 23},
4416 "label": "public function instanceMethod"
4417 "(int $x1, int $x2): void",
4418 "documentation": "Instance method with doc block",
4419 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4422 "activeSignature": 0,
4423 "activeParameter": 1,
4428 comment
="signature help for 2-argument instance method"
4429 " (left of closing paren)",
4430 method
="textDocument/signatureHelp",
4432 "textDocument": {"uri": "${php_file_uri}"},
4433 "position": {"line": 17, "character": 24},
4438 "label": "public function instanceMethod"
4439 "(int $x1, int $x2): void",
4440 "documentation": "Instance method with doc block",
4441 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4444 "activeSignature": 0,
4445 "activeParameter": 1,
4450 comment
="signature help for 2-argument instance method"
4451 " (right of closing paren)",
4452 method
="textDocument/signatureHelp",
4454 "textDocument": {"uri": "${php_file_uri}"},
4455 "position": {"line": 17, "character": 25},
4461 comment
="signature help for 1-argument static method"
4462 " (left of open paren)",
4463 method
="textDocument/signatureHelp",
4465 "textDocument": {"uri": "${php_file_uri}"},
4466 "position": {"line": 18, "character": 23},
4472 comment
="signature help for 1-argument static method"
4473 " (right of open paren)",
4474 method
="textDocument/signatureHelp",
4476 "textDocument": {"uri": "${php_file_uri}"},
4477 "position": {"line": 18, "character": 24},
4482 "label": "public static function staticMethod"
4483 "(string $z): void",
4484 "documentation": "Static method with doc block",
4485 "parameters": [{"label": "$z"}],
4488 "activeSignature": 0,
4489 "activeParameter": 0,
4494 comment
="signature help for 2-argument global function"
4495 " (left of open paren)",
4496 method
="textDocument/signatureHelp",
4498 "textDocument": {"uri": "${php_file_uri}"},
4499 "position": {"line": 19, "character": 17},
4505 comment
="signature help for 2-argument global function"
4506 " (right of open paren)",
4507 method
="textDocument/signatureHelp",
4509 "textDocument": {"uri": "${php_file_uri}"},
4510 "position": {"line": 19, "character": 18},
4515 "label": "function global_function"
4516 "(string $s, int $x): void",
4517 "documentation": "Global function with doc block",
4518 "parameters": [{"label": "$s"}, {"label": "$x"}],
4521 "activeSignature": 0,
4522 "activeParameter": 0,
4527 comment
="signature help for 1-argument namespace-aliased global"
4528 " function (right of open paren)",
4529 method
="textDocument/signatureHelp",
4531 "textDocument": {"uri": "${php_file_uri}"},
4532 "position": {"line": 20, "character": 26},
4538 comment
="signature help for 1-argument namespace-aliased global"
4539 " function (right of open paren)",
4540 method
="textDocument/signatureHelp",
4542 "textDocument": {"uri": "${php_file_uri}"},
4543 "position": {"line": 20, "character": 26},
4549 comment
="signature help for 1-argument namespace-aliased global"
4550 " function (right of open paren)",
4551 method
="textDocument/signatureHelp",
4553 "textDocument": {"uri": "${php_file_uri}"},
4554 "position": {"line": 20, "character": 27},
4559 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
4560 "documentation": "Namespace-aliased function with doc block",
4561 "parameters": [{"label": "$s"}],
4564 "activeSignature": 0,
4565 "activeParameter": 0,
4570 comment
="signature help for 1-argument namespace-aliased global"
4571 " function (right of open paren)",
4572 method
="textDocument/signatureHelp",
4574 "textDocument": {"uri": "${php_file_uri}"},
4575 "position": {"line": 20, "character": 28},
4580 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
4581 "documentation": "Namespace-aliased function with doc block",
4582 "parameters": [{"label": "$s"}],
4585 "activeSignature": 0,
4586 "activeParameter": 0,
4591 comment
="signature help for 2-argument function with params"
4592 " (right of open paren)",
4593 method
="textDocument/signatureHelp",
4595 "textDocument": {"uri": "${php_file_uri}"},
4596 "position": {"line": 21, "character": 30},
4601 "label": "function test_signature_help_params1("
4602 "\n string $param1,\n string $param2\n): void",
4603 "documentation": "comment describing the method"
4604 "\n@param $param1 info1"
4605 "\n@param param2 info2",
4607 {"label": "$param1", "documentation": "info1"},
4608 {"label": "$param2", "documentation": "info2"},
4612 "activeSignature": 0,
4613 "activeParameter": 0,
4618 comment
="signature help for 2-argument function with params"
4619 " (right of open paren)",
4620 method
="textDocument/signatureHelp",
4622 "textDocument": {"uri": "${php_file_uri}"},
4623 "position": {"line": 22, "character": 30},
4628 "label": "function test_signature_help_params2("
4629 "\n string $param1,\n string $param2\n): void",
4630 "documentation": "comment describing the method"
4631 "\n@param $param1 info1",
4633 {"label": "$param1", "documentation": "info1"},
4634 {"label": "$param2"},
4638 "activeSignature": 0,
4639 "activeParameter": 0,
4644 comment
="signature help for 2-argument function with params"
4645 " (right of open paren)",
4646 method
="textDocument/signatureHelp",
4648 "textDocument": {"uri": "${php_file_uri}"},
4649 "position": {"line": 23, "character": 30},
4654 "label": "function test_signature_help_params3("
4655 "\n string $param1,\n string $param2\n): string",
4656 "documentation": "@param $param1 info1"
4658 "\n@param $param2 info2"
4659 "\n@return the string"
4664 "documentation": "info1 for param1",
4666 {"label": "$param2", "documentation": "info2"},
4670 "activeSignature": 0,
4671 "activeParameter": 0,
4674 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4675 .notification(method
="exit", params
={})
4677 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
4679 def test_signature_help_lambda(self
) -> None:
4680 self
.prepare_server_environment()
4681 variables
= self
.setup_php_file("signaturehelp_lambda.php")
4683 self
.initialize_spec(
4684 LspTestSpec("test_serverless_ide_signature_help_lambda"),
4685 use_serverless_ide
=False,
4687 .wait_for_hh_server_ready()
4689 method
="textDocument/didOpen",
4692 "uri": "${php_file_uri}",
4693 "languageId": "hack",
4695 "text": "${php_file}",
4701 comment
="signature help for a normal function call",
4702 method
="textDocument/signatureHelp",
4704 "textDocument": {"uri": "${php_file_uri}"},
4705 "position": {"line": 8, "character": 29},
4708 "activeParameter": 0,
4709 "activeSignature": 0,
4712 "label": "function test_lambda_sighelp(\n"
4714 " (function(string): int) $f\n"
4716 "parameters": [{"label": "$str"}, {"label": "$f"}],
4723 comment
="signature help for normal function call within a lambda",
4724 method
="textDocument/signatureHelp",
4726 "textDocument": {"uri": "${php_file_uri}"},
4727 "position": {"line": 9, "character": 21},
4730 "activeParameter": 0,
4731 "activeSignature": 0,
4734 "label": "function normal_test_func(string $str): void",
4735 "parameters": [{"label": "$str"}],
4742 comment
="signature help for text within a lambda, left side of an open paren",
4743 method
="textDocument/signatureHelp",
4745 "textDocument": {"uri": "${php_file_uri}"},
4746 "position": {"line": 10, "character": 15},
4752 comment
="signature help for text within a lambda, right side of an open paren",
4753 method
="textDocument/signatureHelp",
4755 "textDocument": {"uri": "${php_file_uri}"},
4756 "position": {"line": 10, "character": 16},
4760 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4761 .notification(method
="exit", params
={})
4763 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
4765 def test_rename(self
) -> None:
4766 self
.prepare_server_environment()
4767 variables
= self
.setup_php_file("rename.php")
4768 self
.load_and_run("rename", variables
)
4770 def test_references(self
) -> None:
4771 self
.prepare_server_environment()
4772 variables
= self
.setup_php_file("references.php")
4773 self
.load_and_run("references", variables
)
4775 def test_non_existing_method(self
) -> None:
4776 self
.prepare_server_environment()
4777 variables
= self
.setup_php_file("nomethod.php")
4778 self
.load_and_run("nomethod", variables
)
4780 def test_bad_call(self
) -> None:
4781 self
.prepare_server_environment()
4782 variables
= self
.setup_php_file("bad_call.php")
4783 self
.load_and_run("bad_call", variables
)
4785 def test_non_blocking(self
) -> None:
4786 self
.prepare_server_environment()
4787 variables
= self
.setup_php_file("non_blocking.php")
4788 self
.test_driver
.start_hh_loop_forever_assert_timeout()
4790 self
.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide
=False)
4791 .wait_for_hh_server_ready()
4794 method
="textDocument/definition",
4796 "textDocument": {"uri": "${php_file_uri}"},
4797 "position": {"line": 7, "character": 11},
4801 "uri": "file://${root_path}/non_blocking.php",
4803 "start": {"line": 2, "character": 9},
4804 "end": {"line": 2, "character": 32},
4806 "title": "non_blocking_definition",
4809 wait_id
="definition request",
4812 comment
="remove hh_loop_forever() invocation to break the infinite loop",
4813 method
="textDocument/didOpen",
4816 "uri": "${root_path}/__hh_loop_forever_foo.php",
4817 "languageId": "hack",
4822 function __hh_loop_forever_foo(): int {
4829 .wait_for_response(wait_id
="definition request")
4830 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4831 .notification(method
="exit", params
={})
4833 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
4835 def test_serverless_ide_hierarchy_file_change_on_disk(self
) -> None:
4836 variables
= dict(self
.prepare_serverless_ide_environment())
4837 variables
.update(self
.setup_php_file("incremental_derived.php"))
4838 changed_php_file_uri
= self
.repo_file("incremental_base.php")
4839 variables
.update({"changed_php_file_uri": changed_php_file_uri
})
4840 self
.test_driver
.stop_hh_server()
4843 self
.initialize_spec(
4844 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
4845 use_serverless_ide
=True,
4848 method
="textDocument/didOpen",
4851 "uri": "${php_file_uri}",
4852 "languageId": "hack",
4854 "text": "${php_file}",
4860 comment
="hover before change to class hierarchy should be `int`",
4861 method
="textDocument/hover",
4863 "textDocument": {"uri": "${php_file_uri}"},
4864 "position": {"line": 7, "character": 14},
4868 {"language": "hack", "value": "public function foo(): int"},
4869 "Return type: `int`",
4870 "Full name: `BaseClassIncremental::foo`",
4873 "start": {"line": 7, "character": 12},
4874 "end": {"line": 7, "character": 15},
4877 powered_by
="serverless_ide",
4880 uri
=changed_php_file_uri
,
4883 class BaseClassIncremental {
4884 public function foo(): string { return ''; }
4891 comment
="hover after change to class hierarchy should be `string`",
4892 method
="textDocument/hover",
4894 "textDocument": {"uri": "${php_file_uri}"},
4895 "position": {"line": 7, "character": 14},
4899 {"language": "hack", "value": "public function foo(): string"},
4900 "Return type: `string`",
4901 "Full name: `BaseClassIncremental::foo`",
4904 "start": {"line": 7, "character": 12},
4905 "end": {"line": 7, "character": 15},
4908 powered_by
="serverless_ide",
4910 .request(line
=line(), method
="shutdown", params
={}, result
=None)
4911 .notification(method
="exit", params
={})
4914 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
4916 def test_serverless_ide_decl_in_unsaved_buffer_changed(self
) -> None:
4917 variables
= dict(self
.prepare_serverless_ide_environment())
4918 variables
.update(self
.setup_php_file("hover.php"))
4919 self
.test_driver
.stop_hh_server()
4922 self
.initialize_spec(
4923 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
4924 use_serverless_ide
=True,
4927 method
="textDocument/didOpen",
4930 "uri": "${php_file_uri}",
4931 "languageId": "hack",
4933 "text": "${php_file}",
4939 comment
="hover over function invocation",
4940 method
="textDocument/hover",
4942 "textDocument": {"uri": "${php_file_uri}"},
4943 "position": {"line": 3, "character": 16},
4947 {"language": "hack", "value": "int"},
4948 "A comment describing b_hover.",
4951 "start": {"line": 3, "character": 9},
4952 "end": {"line": 3, "character": 16},
4955 powered_by
="serverless_ide",
4958 comment
="make local, unsaved change to the file",
4959 method
="textDocument/didChange",
4961 "textDocument": {"uri": "${php_file_uri}", "version": 2},
4967 function a_hover(): int {
4970 # A comment describing b_hover differently.
4971 function b_hover(): string {
4981 comment
="another hover over function invocation, should be string now",
4982 method
="textDocument/hover",
4984 "textDocument": {"uri": "${php_file_uri}"},
4985 "position": {"line": 3, "character": 16},
4989 {"language": "hack", "value": "string"},
4990 "A comment describing b_hover differently.",
4993 "start": {"line": 3, "character": 9},
4994 "end": {"line": 3, "character": 16},
4997 powered_by
="serverless_ide",
4999 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5000 .notification(method
="exit", params
={})
5003 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5005 def test_serverless_ide_decl_two_unsaved_buffers(self
) -> None:
5006 variables
= dict(self
.prepare_serverless_ide_environment())
5007 variables
.update(self
.setup_php_file("unsaved1.php"))
5008 variables
.update({"unsaved2_file_uri": self
.repo_file_uri("unsaved2.php")})
5009 self
.test_driver
.stop_hh_server()
5012 self
.initialize_spec(
5013 LspTestSpec("test_serverless_ide_decl_two_unsaved_buffers"),
5014 use_serverless_ide
=True,
5017 comment
="open 'unsaved1.php', since we'll be hovering in it",
5018 method
="textDocument/didOpen",
5021 "uri": "${php_file_uri}",
5022 "languageId": "hack",
5024 "text": "${php_file}",
5029 comment
="open 'unsaved2.php' with a bool-returning signature, different from disk",
5030 method
="textDocument/didOpen",
5033 "uri": "${unsaved2_file_uri}",
5034 "languageId": "hack",
5038 function unsaved_bar(): bool { return true; }
5045 comment
="hover 'unsaved1.php' is with respect to disk contents of 'unsaved2.php'",
5046 method
="textDocument/hover",
5048 "textDocument": {"uri": "${php_file_uri}"},
5049 "position": {"line": 1, "character": 39},
5053 {"language": "hack", "value": "function unsaved_bar(): int"},
5054 "Return type: `int`",
5057 "start": {"line": 1, "character": 34},
5058 "end": {"line": 1, "character": 45},
5061 powered_by
="serverless_ide",
5064 comment
="change signature in 'unsaved2.php' to return string",
5065 method
="textDocument/didChange",
5067 "textDocument": {"uri": "${unsaved2_file_uri}", "version": 2},
5072 function unsaved_bar(): string { return "hello"; }
5080 comment
="this is a dummy hover in 'unsaved2.php' just to ensure its decl is cached",
5081 method
="textDocument/hover",
5083 "textDocument": {"uri": "${unsaved2_file_uri}"},
5084 "position": {"line": 0, "character": 0},
5087 powered_by
="serverless_ide",
5091 comment
="hover 'unsaved1.php' is still with respect to disk contents of 'unsaved2.php'",
5092 method
="textDocument/hover",
5094 "textDocument": {"uri": "${php_file_uri}"},
5095 "position": {"line": 1, "character": 39},
5099 {"language": "hack", "value": "function unsaved_bar(): int"},
5100 "Return type: `int`",
5103 "start": {"line": 1, "character": 34},
5104 "end": {"line": 1, "character": 45},
5107 powered_by
="serverless_ide",
5110 comment
="save signature in 'unsaved2' to return string",
5111 uri
=variables
["unsaved2_file_uri"],
5114 function unsaved_bar(): string { return "hello"; }
5120 comment
="hover 'unsaved1.php' gets new disk contents of 'unsaved2.php'",
5121 method
="textDocument/hover",
5123 "textDocument": {"uri": "${php_file_uri}"},
5124 "position": {"line": 1, "character": 39},
5128 {"language": "hack", "value": "function unsaved_bar(): string"},
5129 "Return type: `string`",
5132 "start": {"line": 1, "character": 34},
5133 "end": {"line": 1, "character": 45},
5136 powered_by
="serverless_ide",
5138 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5139 .notification(method
="exit", params
={})
5142 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5144 def test_hover_without_file_open(self
) -> None:
5145 variables
= dict(self
.prepare_serverless_ide_environment())
5146 variables
.update(self
.setup_php_file("hover.php"))
5147 self
.test_driver
.stop_hh_server()
5150 self
.initialize_spec(
5151 LspTestSpec("test_hover_without_file_open"),
5152 use_serverless_ide
=True,
5153 supports_status
=True,
5155 .ignore_notifications(method
="textDocument/publishDiagnostics")
5157 comment
="Ignore 'initializing...' messages since they're racy",
5158 method
="window/showStatus",
5161 "actions": [{"title": "Restart hh_server"}],
5162 "message": "Hack IDE: initializing.\nhh_server: stopped.",
5163 "shortMessage": "Hack: initializing",
5167 comment
="another racy initializing, before hh_server has even responded",
5168 method
="window/showStatus",
5172 "message": "Hack IDE: initializing.",
5173 "shortMessage": "Hack: initializing",
5177 comment
="another racy initialization to ignore, again before hh_server",
5178 method
="window/showStatus",
5182 "message": "Hack IDE: ready.",
5183 "shortMessage": "Hack: ready",
5186 .wait_for_server_request(
5187 method
="window/showStatus",
5189 "actions": [{"title": "Restart hh_server"}],
5190 "message": "Hack IDE: ready.\nhh_server: stopped.",
5191 "shortMessage": "Hack: ready",
5194 result
=NoResponse(),
5198 comment
="hover before file_open will fail",
5199 method
="textDocument/hover",
5201 "textDocument": {"uri": "${php_file_uri}"},
5202 "position": {"line": 26, "character": 20},
5207 method
="textDocument/didOpen",
5210 "uri": "${php_file_uri}",
5211 "languageId": "hack",
5213 "text": "${php_file}",
5219 comment
="hover after file_open will succeed",
5220 method
="textDocument/hover",
5222 "textDocument": {"uri": "${php_file_uri}"},
5223 "position": {"line": 26, "character": 20},
5225 result
={"contents": [{"language": "hack", "value": "string"}]},
5226 powered_by
="serverless_ide",
5230 method
="$test/shutdownServerlessIde",
5233 powered_by
="serverless_ide",
5235 .wait_for_server_request(
5236 method
="window/showStatus",
5239 {"title": "Restart Hack IDE"},
5240 {"title": "Restart hh_server"},
5242 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: stopped.",
5243 "shortMessage": "Hack: failed",
5246 result
={"title": "Restart Hack IDE"},
5248 .wait_for_server_request(
5249 method
="window/showStatus",
5251 "actions": [{"title": "Restart hh_server"}],
5252 "message": "Hack IDE: ready.\nhh_server: stopped.",
5253 "shortMessage": "Hack: ready",
5256 result
=NoResponse(),
5260 comment
="hover after restart will succeed",
5261 method
="textDocument/hover",
5263 "textDocument": {"uri": "${php_file_uri}"},
5264 "position": {"line": 26, "character": 20},
5266 result
={"contents": [{"language": "hack", "value": "string"}]},
5267 powered_by
="serverless_ide",
5270 method
="textDocument/didClose",
5271 params
={"textDocument": {"uri": "${php_file_uri}"}},
5275 comment
="hover after file_close will fail",
5276 method
="textDocument/hover",
5278 "textDocument": {"uri": "${php_file_uri}"},
5279 "position": {"line": 26, "character": 20},
5283 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5284 .notification(method
="exit", params
={})
5287 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5289 def test_hh_server_status_diagnostic(self
) -> None:
5290 variables
= dict(self
.prepare_serverless_ide_environment())
5291 variables
.update(self
.setup_php_file("unsaved1.php"))
5294 "unsaved2_file_uri": self
.repo_file_uri("unsaved2.php"),
5295 "unsaved2_file": self
.read_repo_file("unsaved2.php"),
5298 self
.test_driver
.stop_hh_server()
5301 self
.initialize_spec(
5302 LspTestSpec("test_hh_server_status_diagnostic"), use_serverless_ide
=True
5304 .ignore_status_diagnostics(False)
5306 method
="textDocument/didOpen",
5309 "uri": "${php_file_uri}",
5310 "languageId": "hack",
5312 "text": "${php_file}",
5316 .wait_for_notification(
5317 comment
="After didOpen(file1), the hh_server_status diagnostic should appear in file1",
5318 method
="textDocument/publishDiagnostics",
5320 "uri": "${php_file_uri}",
5324 "start": {"line": 0, "character": 0},
5325 "end": {"line": 0, "character": 1},
5328 "source": "hh_server",
5329 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5330 "relatedInformation": [],
5331 "relatedLocations": [],
5338 method
="textDocument/didOpen",
5341 "uri": "${unsaved2_file_uri}",
5342 "languageId": "hack",
5344 "text": "${unsaved2_file}",
5348 .wait_for_notification(
5349 comment
="After didOpen(file2), the hh_server_status diagnostic should disappear from file1",
5350 method
="textDocument/publishDiagnostics",
5352 "uri": "${php_file_uri}",
5357 .wait_for_notification(
5358 comment
="After didOpen(file2), the hh_server_status diagnostic should reappear in file2",
5359 method
="textDocument/publishDiagnostics",
5361 "uri": "${unsaved2_file_uri}",
5365 "start": {"line": 0, "character": 0},
5366 "end": {"line": 0, "character": 1},
5369 "source": "hh_server",
5370 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5371 "relatedInformation": [],
5372 "relatedLocations": [],
5379 method
="textDocument/didClose",
5380 params
={"textDocument": {"uri": "${unsaved2_file_uri}"}},
5382 .wait_for_notification(
5383 comment
="After didClose(file2), the hh_server_status diagnostic should disappear from file2",
5384 method
="textDocument/publishDiagnostics",
5386 "uri": "${unsaved2_file_uri}",
5391 .wait_for_notification(
5392 comment
="After didClose(file2), the hh_server_status diagnostic should reappear in file1",
5393 method
="textDocument/publishDiagnostics",
5395 "uri": "${php_file_uri}",
5399 "start": {"line": 0, "character": 0},
5400 "end": {"line": 0, "character": 1},
5403 "source": "hh_server",
5404 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5405 "relatedInformation": [],
5406 "relatedLocations": [],
5413 method
="textDocument/didClose",
5414 params
={"textDocument": {"uri": "${php_file_uri}"}},
5416 .wait_for_notification(
5417 comment
="After didClose(file1), the hh_server_status diagnostic should disappear from file1",
5418 method
="textDocument/publishDiagnostics",
5420 "uri": "${php_file_uri}",
5425 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5426 .notification(method
="exit", params
={})
5429 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5431 def _sanitize_gutter_line_numbers(self
, s
: str) -> str:
5432 gutter_line_number_re
= re
.compile(r
"^[ ]*[0-9]+ \|", re
.MULTILINE
)
5433 return re
.sub(gutter_line_number_re
, " XXXX |", s
)
5435 def test_lsptestspec_incorrect_request_result(self
) -> None:
5436 variables
= dict(self
.prepare_serverless_ide_environment())
5437 variables
.update(self
.setup_php_file("hover.php"))
5438 self
.test_driver
.stop_hh_server()
5441 self
.initialize_spec(
5442 LspTestSpec("test_lsptestspec_incorrect_request_result"),
5443 use_serverless_ide
=True,
5446 method
="textDocument/didOpen",
5449 "uri": "${php_file_uri}",
5450 "languageId": "hack",
5452 "text": "${php_file}",
5458 comment
="hover over function invocation",
5459 method
="textDocument/hover",
5461 "textDocument": {"uri": "${php_file_uri}"},
5462 "position": {"line": 3, "character": 16},
5466 {"language": "hack", "value": "int"},
5467 "INCORRECT COMMENT HERE",
5470 "start": {"line": 3, "character": 9},
5471 "end": {"line": 3, "character": 16},
5474 powered_by
="serverless_ide",
5476 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5477 .notification(method
="exit", params
={})
5482 variables
=variables
,
5483 wait_for_server
=False,
5484 use_serverless_ide
=True,
5486 raise AssertionError("Expected an error here")
5487 except AssertionError as e
:
5489 self
._sanitize
_gutter
_line
_numbers
(str(e
)),
5491 Test case test_lsptestspec_incorrect_request_result failed with 1 errors:
5494 Description: Request with ID 5 (comment: 'hover over function invocation') \
5495 got an incorrect result:
5497 (+ is expected lines, - is actual lines)
5498 - {'contents': [{'language': 'hack', 'value': 'int'},
5499 + {'contents': [{'language': 'hack', 'value': 'int'}, 'INCORRECT COMMENT HERE'],
5500 ? +++++++++++++++++++++++++++
5502 - 'A comment describing b_hover.'],
5503 'range': {'end': {'character': 16, 'line': 3},
5504 'start': {'character': 9, 'line': 3}}}
5507 This was the associated request:
5509 hphp/hack/test/integration/test_lsp.py
5512 XXXX | comment="hover over function invocation",
5513 XXXX | method="textDocument/hover",
5515 XXXX | "textDocument": {"uri": "${php_file_uri}"},
5516 XXXX | "position": {"line": 3, "character": 16},
5519 XXXX | "contents": [
5520 XXXX | {"language": "hack", "value": "int"},
5521 XXXX | "INCORRECT COMMENT HERE",
5524 XXXX | "start": {"line": 3, "character": 9},
5525 XXXX | "end": {"line": 3, "character": 16},
5528 XXXX | powered_by="serverless_ide",
5532 1) If this was unexpected, then the language server is buggy and should be
5535 2) If this was expected, you can update your request with the following code to
5540 comment='hover over function invocation',
5541 method='textDocument/hover',
5542 params={'textDocument': {'uri': '${php_file_uri}'}, \
5543 'position': {'line': 3, 'character': 16}},
5544 result={'contents': [{'language': 'hack', 'value': 'int'}, \
5545 'A comment describing b_hover.'], \
5546 'range': {'start': {'line': 3, 'character': 9}, \
5547 'end': {'line': 3, 'character': 16}}},
5548 powered_by='serverless_ide',
5551 If you want to examine the raw LSP logs, you can check the `.sent.log` and
5552 `.received.log` files that were generated in the template repo for this test.\
5556 def test_lsptestspec_unexpected_notification(self
) -> None:
5557 self
.prepare_server_environment()
5558 variables
= self
.setup_php_file("didchange.php")
5560 self
.initialize_spec(LspTestSpec("did_change"), use_serverless_ide
=False)
5561 .wait_for_hh_server_ready()
5563 method
="textDocument/didOpen",
5566 "uri": "${php_file_uri}",
5567 "languageId": "hack",
5569 "text": "${php_file}",
5574 method
="textDocument/didChange",
5576 "textDocument": {"uri": "${php_file_uri}"},
5580 "start": {"line": 7, "character": 11},
5581 "end": {"line": 7, "character": 12},
5588 .wait_for_notification(
5589 method
="textDocument/publishDiagnostics",
5591 "uri": "${php_file_uri}",
5595 "start": {"line": 7, "character": 11},
5596 "end": {"line": 7, "character": 11},
5601 "message": "A semicolon ; is expected here.",
5602 "relatedLocations": [],
5603 "relatedInformation": [],
5608 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5609 .notification(method
="exit", params
={})
5613 spec
, variables
, wait_for_server
=True, use_serverless_ide
=False
5615 raise AssertionError("Expected an error here")
5616 except AssertionError as e
:
5618 self
._sanitize
_gutter
_line
_numbers
(str(e
)),
5620 Test case did_change failed with 1 errors:
5623 Description: An unexpected notification of type \
5624 'textDocument/publishDiagnostics' was sent by the language server.
5625 Here is the notification payload:
5628 'method': 'textDocument/publishDiagnostics',
5629 'params': {'diagnostics': [],
5630 'uri': '__PHP_FILE_URI__'}}
5633 This was the most recent request issued from the language client before it
5634 received the notification:
5636 hphp/hack/test/integration/test_lsp.py
5637 XXXX | .request(line=line(), method="shutdown", params={}, result=None)
5640 1) If this was unexpected, then the language server is buggy and should be
5643 2) If all notifications of type 'textDocument/publishDiagnostics' should be \
5644 ignored, add this directive
5645 anywhere in your test:
5647 .ignore_notifications(method='textDocument/publishDiagnostics')
5649 3) If this single instance of the notification was expected, add this directive
5650 to your test to wait for it before proceeding:
5652 .wait_for_notification(
5653 method='textDocument/publishDiagnostics',
5654 params={'uri': '${php_file_uri}', 'diagnostics': []},
5657 If you want to examine the raw LSP logs, you can check the `.sent.log` and
5658 `.received.log` files that were generated in the template repo for this test.\
5660 # There's an instance of a literal `${php_file_uri}` in there
5661 # which we don't want to change, so use a different name than
5663 .replace("__PHP_FILE_URI__", variables
["php_file_uri"]),
5666 def test_serverless_ide_highlight(self
) -> None:
5667 variables
= dict(self
.prepare_serverless_ide_environment())
5668 variables
.update(self
.setup_php_file("highlight.php"))
5669 self
.test_driver
.stop_hh_server()
5672 self
.initialize_spec(
5673 LspTestSpec("serverless_ide_highlight"), use_serverless_ide
=True
5676 method
="textDocument/didOpen",
5679 "uri": "${php_file_uri}",
5680 "languageId": "hack",
5682 "text": "${php_file}",
5688 comment
="document highlight, id 2",
5689 method
="textDocument/documentHighlight",
5691 "textDocument": {"uri": "${php_file_uri}"},
5692 "position": {"line": 3, "character": 10},
5697 "start": {"line": 3, "character": 9},
5698 "end": {"line": 3, "character": 20},
5702 powered_by
="serverless_ide",
5706 comment
="shutdown, id 3",
5712 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5714 def test_serverless_ide_coverage(self
) -> None:
5715 variables
= dict(self
.prepare_serverless_ide_environment())
5716 variables
.update(self
.setup_php_file("coverage.php"))
5717 self
.test_driver
.stop_hh_server()
5720 self
.initialize_spec(
5721 LspTestSpec("serverless_ide_coverage"), use_serverless_ide
=True
5724 method
="textDocument/didOpen",
5727 "uri": "${php_file_uri}",
5728 "languageId": "hack",
5730 "text": "${php_file}",
5736 comment
="Check type coverage",
5737 method
="textDocument/typeCoverage",
5738 params
={"textDocument": {"uri": "${php_file_uri}"}},
5740 "coveredPercent": 0,
5741 "uncoveredRanges": [
5744 "start": {"line": 3, "character": 12},
5745 "end": {"line": 3, "character": 17},
5750 "start": {"line": 3, "character": 8},
5751 "end": {"line": 3, "character": 10},
5756 "start": {"line": 3, "character": 2},
5757 "end": {"line": 3, "character": 5},
5761 "defaultMessage": "Un-type checked code. Consider adding type annotations.",
5763 powered_by
="serverless_ide",
5773 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5775 def test_status_stopped(self
) -> None:
5776 variables
= dict(self
.prepare_serverless_ide_environment())
5777 variables
.update(self
.setup_php_file("hover.php"))
5778 self
.test_driver
.stop_hh_server()
5781 self
.initialize_spec(
5782 LspTestSpec("status_stopped"),
5783 use_serverless_ide
=False,
5784 supports_status
=True,
5786 .wait_for_server_request(
5787 method
="window/showStatus",
5789 "shortMessage": "Hack: stopped",
5790 "message": "hh_server: stopped.",
5791 "actions": [{"title": "Restart hh_server"}],
5794 result
=NoResponse(),
5796 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5797 .notification(method
="exit", params
={})
5799 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=False)
5801 def test_status_running(self
) -> None:
5802 variables
= dict(self
.prepare_serverless_ide_environment())
5803 variables
.update(self
.setup_php_file("hover.php"))
5806 self
.initialize_spec(
5807 LspTestSpec("status_running"),
5808 use_serverless_ide
=False,
5809 supports_status
=True,
5812 comment
="Ignore initializing... requests since they're racy",
5813 method
="window/showStatus",
5816 "shortMessage": "Hack: initializing",
5817 "message": "hh_server initializing: processing [<test> seconds]",
5821 .wait_for_server_request(
5822 method
="window/showStatus",
5823 params
={"actions": [], "message": "hh_server: ready.", "type": 3},
5824 result
=NoResponse(),
5826 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5827 .notification(method
="exit", params
={})
5829 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
5831 def test_serverless_ide_status_stopped(self
) -> None:
5832 variables
= dict(self
.prepare_serverless_ide_environment())
5833 variables
.update(self
.setup_php_file("hover.php"))
5834 self
.test_driver
.stop_hh_server()
5837 self
.initialize_spec(
5838 LspTestSpec("serverless_ide_status_stopped"),
5839 use_serverless_ide
=True,
5840 supports_status
=True,
5843 comment
="ignore initializing... messages since they're kind of racy",
5844 method
="window/showStatus",
5847 "actions": [{"title": "Restart hh_server"}],
5848 "message": "Hack IDE: initializing.\nhh_server: stopped.",
5849 "shortMessage": "Hack: initializing",
5853 comment
="another racy initialization to ignore, before hh_server has even reported its status",
5854 method
="window/showStatus",
5858 "message": "Hack IDE: initializing.",
5859 "shortMessage": "Hack: initializing",
5863 comment
="another racy initialization to ignore, again before hh_server",
5864 method
="window/showStatus",
5868 "message": "Hack IDE: ready.",
5869 "shortMessage": "Hack: ready",
5872 .wait_for_server_request(
5873 method
="window/showStatus",
5875 "message": "Hack IDE: ready.\nhh_server: stopped.",
5876 "shortMessage": "Hack: ready",
5877 "actions": [{"title": "Restart hh_server"}],
5880 result
=NoResponse(),
5882 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5883 .notification(method
="exit", params
={})
5885 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
5887 def test_serverless_ide_status_restart(self
) -> None:
5888 variables
= dict(self
.prepare_serverless_ide_environment())
5889 variables
.update(self
.setup_php_file("hover.php"))
5892 self
.initialize_spec(
5893 LspTestSpec("serverless_ide_status_restart"),
5894 use_serverless_ide
=True,
5895 supports_status
=True,
5898 comment
="Ignore initializing messages since they're racy",
5899 method
="window/showStatus",
5903 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
5904 "shortMessage": "Hack: initializing",
5908 comment
="Another form of initializing to ignore",
5909 method
="window/showStatus",
5913 "message": "Hack IDE: initializing.\nhh_server: ready.",
5914 "shortMessage": "Hack: initializing",
5918 comment
="Another form of initializing to ignore before we've even heard the first peep from hh_server",
5919 method
="window/showStatus",
5923 "message": "Hack IDE: initializing.",
5924 "shortMessage": "Hack: initializing",
5928 comment
="another racy initialization to ignore, again before hh_server",
5929 method
="window/showStatus",
5933 "message": "Hack IDE: ready.",
5934 "shortMessage": "Hack: ready",
5937 .wait_for_server_request(
5938 method
="window/showStatus",
5941 "message": "Hack IDE: ready.\nhh_server: ready.",
5942 "shortMessage": "Hack: ready",
5945 result
=NoResponse(),
5949 method
="$test/shutdownServerlessIde",
5952 powered_by
="serverless_ide",
5954 .wait_for_server_request(
5955 method
="window/showStatus",
5957 "actions": [{"title": "Restart Hack IDE"}],
5958 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
5959 "shortMessage": "Hack: failed",
5962 result
={"title": "Restart Hack IDE"},
5964 .wait_for_server_request(
5965 method
="window/showStatus",
5968 "message": "Hack IDE: ready.\nhh_server: ready.",
5969 "shortMessage": "Hack: ready",
5972 result
=NoResponse(),
5974 .request(line
=line(), method
="shutdown", params
={}, result
=None)
5975 .notification(method
="exit", params
={})
5977 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=True)
5979 def test_serverless_ide_failed_to_load_saved_state(self
) -> None:
5980 variables
= dict(self
.prepare_serverless_ide_environment())
5981 variables
.update(self
.setup_php_file("hover.php"))
5982 assert "naming_table_saved_state_path" in variables
5983 variables
["naming_table_saved_state_path"] = "/tmp/nonexistent"
5986 self
.initialize_spec(
5987 LspTestSpec("serverless_ide_status_failed_to_load_saved_state"),
5988 use_serverless_ide
=True,
5989 supports_status
=True,
5993 comment
="Ignore initializing since they're kind of racy",
5994 method
="window/showStatus",
5998 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
5999 "shortMessage": "Hack: initializing",
6003 comment
="Ignore another form of initializing",
6004 method
="window/showStatus",
6008 "message": "Hack IDE: initializing.\nhh_server: ready.",
6009 "shortMessage": "Hack: initializing",
6013 comment
="Ignore another form of initializing, from before we've even heard the first peep out of hh_server",
6014 method
="window/showStatus",
6018 "message": "Hack IDE: initializing.",
6019 "shortMessage": "Hack: initializing",
6023 comment
="Ignore another form of initializing, again before hh_server",
6024 method
="window/showStatus",
6027 "actions": [{"title": "Restart Hack IDE"}],
6028 "message": "Hack IDE has failed. See Output›Hack for details.",
6029 "shortMessage": "Hack: failed",
6032 .wait_for_notification(
6033 method
="window/logMessage",
6036 "message": "Hack IDE has failed.\nThis is unexpected.\nPlease file a bug within your IDE.\nMore details: http://dummy/HH_TEST_MODE",
6039 .wait_for_server_request(
6040 method
="window/showStatus",
6042 "actions": [{"title": "Restart Hack IDE"}],
6043 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6044 "shortMessage": "Hack: failed",
6047 result
=NoResponse(),
6049 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6050 .notification(method
="exit", params
={})
6052 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=True)
6054 def test_workspace_symbol(self
) -> None:
6055 self
.prepare_server_environment()
6056 variables
= self
.setup_php_file("didchange.php")
6058 self
.initialize_spec(
6059 LspTestSpec("test_workspace_symbol"), use_serverless_ide
=False
6061 .wait_for_hh_server_ready()
6064 comment
="Look up symbols",
6065 method
="workspace/symbol",
6066 params
={"query": "TestNS\\test"},
6069 "name": "TestNS\\test_func",
6072 "uri": "file://${root_path}/completion_extras_namespace.php",
6074 "start": {"line": 4, "character": 9},
6075 "end": {"line": 4, "character": 25},
6083 comment
="Look up symbols starting with 'test_f' within multiple namespaces",
6084 method
="workspace/symbol",
6085 params
={"query": "test_f"},
6088 "name": "test_function",
6091 "uri": "file://${root_path}/completion.php",
6093 "start": {"line": 7, "character": 9},
6094 "end": {"line": 7, "character": 22},
6099 "name": "TestNS\\test_func",
6102 "uri": "file://${root_path}/completion_extras_namespace.php",
6104 "start": {"line": 4, "character": 9},
6105 "end": {"line": 4, "character": 25},
6111 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6112 .notification(method
="exit", params
={})
6114 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
6116 def test_serverless_ide_during_hh_server_restart(self
) -> None:
6117 variables
= dict(self
.prepare_serverless_ide_environment())
6118 variables
.update(self
.setup_php_file("didchange.php"))
6120 self
.initialize_spec(
6121 LspTestSpec("test_serverless_ide_during_hh_server_restart"),
6122 use_serverless_ide
=True,
6125 method
="textDocument/didOpen",
6128 "uri": "${php_file_uri}",
6129 "languageId": "hack",
6131 "text": "${php_file}",
6136 comment
="Send a 'didChange' notification before HH Server is functional.",
6137 method
="textDocument/didChange",
6139 "textDocument": {"uri": "${php_file_uri}"},
6143 "start": {"line": 7, "character": 9},
6144 "end": {"line": 7, "character": 11},
6151 .start_hh_server("Start HH Server; should detect the bad edit")
6152 .wait_for_notification(
6153 method
="textDocument/publishDiagnostics",
6155 "uri": "${php_file_uri}",
6159 "message": "Invalid return type",
6161 "end": {"character": 14, "line": 7},
6162 "start": {"character": 9, "line": 7},
6164 "relatedInformation": [
6168 "end": {"character": 27, "line": 6},
6169 "start": {"character": 24, "line": 6},
6171 "uri": "${php_file_uri}",
6173 "message": "Expected int",
6178 "end": {"character": 14, "line": 7},
6179 "start": {"character": 9, "line": 7},
6181 "uri": "${php_file_uri}",
6183 "message": "But got string",
6186 "relatedLocations": [
6190 "end": {"character": 27, "line": 6},
6191 "start": {"character": 24, "line": 6},
6193 "uri": "${php_file_uri}",
6195 "message": "Expected int",
6200 "end": {"character": 14, "line": 7},
6201 "start": {"character": 9, "line": 7},
6203 "uri": "${php_file_uri}",
6205 "message": "But got string",
6214 .stop_hh_server("Shutdown HH Server")
6215 .start_hh_server("Restart HH Server")
6216 .wait_for_notification(
6217 comment
="On startup it thinks everything is okay ...",
6218 method
="textDocument/publishDiagnostics",
6219 params
={"uri": "${php_file_uri}", "diagnostics": []},
6221 .wait_for_notification(
6222 comment
="But then hh_server sends a hello message and it gets the edited files, which leads it to see the problem.",
6223 method
="textDocument/publishDiagnostics",
6225 "uri": "${php_file_uri}",
6229 "message": "Invalid return type",
6231 "end": {"character": 14, "line": 7},
6232 "start": {"character": 9, "line": 7},
6234 "relatedInformation": [
6238 "end": {"character": 27, "line": 6},
6239 "start": {"character": 24, "line": 6},
6241 "uri": "${php_file_uri}",
6243 "message": "Expected int",
6248 "end": {"character": 14, "line": 7},
6249 "start": {"character": 9, "line": 7},
6251 "uri": "${php_file_uri}",
6253 "message": "But got string",
6256 "relatedLocations": [
6260 "end": {"character": 27, "line": 6},
6261 "start": {"character": 24, "line": 6},
6263 "uri": "${php_file_uri}",
6265 "message": "Expected int",
6270 "end": {"character": 14, "line": 7},
6271 "start": {"character": 9, "line": 7},
6273 "uri": "${php_file_uri}",
6275 "message": "But got string",
6284 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6285 .notification(method
="exit", params
={})
6287 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=True)
6289 def test_serverless_ide_naming_error1(self
) -> None:
6290 variables
= dict(self
.prepare_serverless_ide_environment())
6291 variables
.update(self
.setup_php_file("didchange.php"))
6294 "main_file": self
.repo_file("main.php"),
6295 "main_file_contents": """\
6297 function main(): int {
6301 "file_a": self
.repo_file("a.php"),
6302 "file_b": self
.repo_file("b.php"),
6306 self
.initialize_spec(
6307 LspTestSpec("serverless_ide_naming_error1"), use_serverless_ide
=True
6310 uri
="${main_file}", contents
="${main_file_contents}", notify
=True
6313 method
="textDocument/didOpen",
6316 "uri": "${main_file}",
6317 "languageId": "hack",
6319 "text": "${main_file_contents}",
6325 comment
="Ensure that hover over `aaa` works even when the name is not yet defined",
6326 method
="textDocument/hover",
6328 "textDocument": {"uri": "${main_file}"},
6329 "position": {"line": 2, "character": 13},
6332 "contents": [{"language": "hack", "value": "_"}],
6334 "start": {"line": 2, "character": 11},
6335 "end": {"line": 2, "character": 14},
6338 powered_by
="serverless_ide",
6341 comment
="create file A",
6345 function aaa(): int {
6353 comment
="Ensure that hover over `aaa` works when there are no naming errors",
6354 method
="textDocument/hover",
6356 "textDocument": {"uri": "${main_file}"},
6357 "position": {"line": 2, "character": 13},
6361 {"language": "hack", "value": "function aaa(): int"},
6362 "Return type: `int`",
6365 "start": {"line": 2, "character": 11},
6366 "end": {"line": 2, "character": 14},
6369 powered_by
="serverless_ide",
6372 comment
="create file B",
6376 function aaa(): string {
6384 comment
="Ensure that hover over `aaa` works even when there is a duplicate name",
6385 method
="textDocument/hover",
6387 "textDocument": {"uri": "${main_file}"},
6388 "position": {"line": 2, "character": 13},
6392 {"language": "hack", "value": "function aaa(): int"},
6393 "Return type: `int`",
6396 "start": {"line": 2, "character": 11},
6397 "end": {"line": 2, "character": 14},
6400 powered_by
="serverless_ide",
6403 comment
="delete file A", uri
="${file_a}", contents
=None, notify
=True
6407 comment
="Now that we've fixed the error, hover should work.",
6408 method
="textDocument/hover",
6410 "textDocument": {"uri": "${main_file}"},
6411 "position": {"line": 2, "character": 13},
6415 {"language": "hack", "value": "function aaa(): string"},
6416 "Return type: `string`",
6419 "start": {"line": 2, "character": 11},
6420 "end": {"line": 2, "character": 14},
6423 powered_by
="serverless_ide",
6425 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6426 .notification(method
="exit", params
={})
6428 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
6430 def test_serverless_ide_naming_error2(self
) -> None:
6431 variables
= dict(self
.prepare_serverless_ide_environment())
6432 self
.test_driver
.stop_hh_server()
6433 variables
.update(self
.setup_php_file("naming_error_caller.php"))
6436 "contents": self
.read_repo_file("naming_error_declaration.php"),
6437 "original": self
.repo_file("naming_error_declaration.php"),
6438 "copy": self
.repo_file("naming_error_copy.php"),
6442 self
.initialize_spec(
6443 LspTestSpec("serverless_ide_naming_error2"), use_serverless_ide
=True
6446 method
="textDocument/didOpen",
6449 "uri": "${php_file_uri}",
6450 "languageId": "hack",
6452 "text": "${php_file}",
6457 comment
="create copy",
6459 contents
="${contents}",
6463 comment
="delete copy", uri
="${copy}", contents
=None, notify
=True
6467 comment
="hover should work fine after making copy then deleting copy.",
6468 method
="textDocument/hover",
6470 "textDocument": {"uri": "${php_file_uri}"},
6471 "position": {"line": 3, "character": 15},
6477 "value": "function naming_error_declaration(): void",
6479 "Return type: `void`",
6482 "start": {"line": 3, "character": 2},
6483 "end": {"line": 3, "character": 26},
6486 powered_by
="serverless_ide",
6488 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6489 .notification(method
="exit", params
={})
6491 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
6493 def test_serverless_ide_naming_error3(self
) -> None:
6494 variables
= dict(self
.prepare_serverless_ide_environment())
6495 self
.test_driver
.stop_hh_server()
6496 variables
.update(self
.setup_php_file("naming_error_caller.php"))
6499 "contents": self
.read_repo_file("naming_error_declaration.php"),
6500 "original": self
.repo_file("naming_error_declaration.php"),
6501 "copy": self
.repo_file("naming_error_copy.php"),
6505 self
.initialize_spec(
6506 LspTestSpec("serverless_ide_naming_error3"), use_serverless_ide
=True
6509 method
="textDocument/didOpen",
6512 "uri": "${php_file_uri}",
6513 "languageId": "hack",
6515 "text": "${php_file}",
6520 comment
="create copy",
6522 contents
="${contents}",
6526 comment
="delete original", uri
="${original}", contents
=None, notify
=True
6530 comment
="hover should work fine after making copy then deleting original.",
6531 method
="textDocument/hover",
6533 "textDocument": {"uri": "${php_file_uri}"},
6534 "position": {"line": 3, "character": 15},
6540 "value": "function naming_error_declaration(): void",
6542 "Return type: `void`",
6545 "start": {"line": 3, "character": 2},
6546 "end": {"line": 3, "character": 26},
6549 powered_by
="serverless_ide",
6551 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6552 .notification(method
="exit", params
={})
6554 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
6556 def test_serverless_ide_requests_before_init(self
) -> None:
6557 variables
= dict(self
.prepare_serverless_ide_environment())
6558 variables
["root_path"] = self
.test_driver
.repo_dir
6559 self
.test_driver
.stop_hh_server()
6562 self
.initialize_spec(
6563 LspTestSpec("test_serverless_ide_requests_before_init"),
6564 use_serverless_ide
=True,
6565 supports_status
=True,
6568 .ignore_notifications(method
="textDocument/publishDiagnostics")
6570 comment
="Ignore 'initializing...' messages since they're racy",
6571 method
="window/showStatus",
6574 "actions": [{"title": "Restart hh_server"}],
6575 "message": "Hack IDE: initializing.\nhh_server: stopped.",
6576 "shortMessage": "Hack: initializing",
6580 comment
="another racy initialization, before we've yet heard from hh_server",
6581 method
="window/showStatus",
6585 "message": "Hack IDE: initializing.",
6586 "shortMessage": "Hack: initializing",
6590 comment
="another racy initialization, if HackIDE is done before hh_server has yet sent status",
6591 method
="window/showStatus",
6595 "message": "Hack IDE: ready.",
6596 "shortMessage": "Hack: ready",
6602 uri
="file://${root_path}/beforeInit1.php",
6603 contents
="<?hh // strict\nfunction beforeInit1(): int {\n return 42;\n}\n",
6606 comment
="open a file before init has finished",
6607 method
="textDocument/didOpen",
6610 "uri": "file://${root_path}/beforeInit2.php",
6611 "languageId": "hack",
6613 "text": "<?hh // strict\nfunction beforeInit2(): void {\n $foo = beforeInit1();\n}\n",
6619 comment
="hover before init will fail",
6620 method
="textDocument/hover",
6622 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
6623 "position": {"line": 2, "character": 4},
6629 comment
="documentSymbol before init will succeed",
6630 method
="textDocument/documentSymbol",
6631 params
={"textDocument": {"uri": "file://${root_path}/beforeInit2.php"}},
6634 "name": "beforeInit2",
6637 "uri": "file://${root_path}/beforeInit2.php",
6639 "start": {"line": 1, "character": 0},
6640 "end": {"line": 3, "character": 1},
6645 powered_by
="serverless_ide",
6647 .wait_for_notification(
6648 comment
="wait for sIDE to init",
6649 method
="telemetry/event",
6650 params
={"type": 4, "message": "[client-ide] Finished init: ok"},
6652 .wait_for_server_request(
6653 method
="window/showStatus",
6655 "actions": [{"title": "Restart hh_server"}],
6656 "message": "Hack IDE: ready.\nhh_server: stopped.",
6657 "shortMessage": "Hack: ready",
6660 result
=NoResponse(),
6664 comment
="hover after init will succeed",
6665 method
="textDocument/hover",
6667 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
6668 "position": {"line": 2, "character": 4},
6671 "contents": [{"language": "hack", "value": "int"}],
6673 "start": {"line": 2, "character": 2},
6674 "end": {"line": 2, "character": 6},
6677 powered_by
="serverless_ide",
6679 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6680 .notification(method
="exit", params
={})
6683 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
6685 def test_serverless_ide_workspace_symbol(self
) -> None:
6686 variables
= dict(self
.prepare_serverless_ide_environment())
6687 variables
["root_path"] = self
.test_driver
.repo_dir
6688 self
.test_driver
.stop_hh_server()
6691 self
.initialize_spec(
6692 LspTestSpec("serverless_ide_workspace_symbol"), use_serverless_ide
=True
6696 comment
="workspace symbol call, global, powered by sqlite (generated during serverless-ide-init)",
6697 method
="workspace/symbol",
6698 params
={"query": "TakesString"},
6701 "name": "TakesString",
6704 "uri": "file://${root_path}/definition.php",
6706 "start": {"line": 36, "character": 6},
6707 "end": {"line": 36, "character": 17},
6712 powered_by
="serverless_ide",
6716 comment
="workspace symbol call, member (derived from naming-table)",
6717 method
="workspace/symbol",
6718 params
={"query": "TakesString::"},
6721 "name": "__construct",
6724 "uri": "file://${root_path}/definition.php",
6726 "start": {"line": 37, "character": 18},
6727 "end": {"line": 37, "character": 29},
6732 powered_by
="serverless_ide",
6734 .request(line
=line(), method
="shutdown", params
={}, result
=None)
6735 .notification(method
="exit", params
={})
6737 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)