Add position metadata to desugaring
[hiphop-php.git] / hphp / hack / test / integration / test_lsp.py
blobd9f2c4713f58011ac46063726e895ed2aedbda0d
1 # pyre-strict
2 # flake8: noqa: B950
4 from __future__ import absolute_import, division, print_function, unicode_literals
6 import copy
7 import enum
8 import json
9 import os
10 import re
11 import unittest
12 import urllib.parse
13 from typing import Iterable, List, Mapping, Tuple
15 import common_tests
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):
24 PlainText = 1
25 Snippet = 2
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:
35 f.write(
36 """
37 use_watchman = true
38 watchman_subscribe_v2 = true
39 interrupt_on_watchman = true
40 interrupt_on_client = true
41 max_workers = 2
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
49 """.format(
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(
60 hh_server,
61 "--check",
62 self.repo_dir,
63 "--save-naming",
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]):
76 @classmethod
77 def get_test_driver(cls) -> LspTestDriver:
78 return LspTestDriver()
80 @classmethod
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:
89 return f.read()
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)
99 return data
101 def load_test_data(
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")
110 text = json.dumps(
111 list(self.get_important_received_items(observed_transcript)), indent=2
113 with open(file, "w") as f:
114 f.write(text)
116 # pyre-fixme[11]: Annotation `JsonObject` is not defined as a type.
117 def order_response(self, response: JsonObject) -> str:
118 if "id" in response:
119 return str(response["id"])
120 else:
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"]
148 return sanitized
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
161 if received is None:
162 continue
163 method = received.get("method") or ""
164 if method in [
165 "window/progress",
166 "window/actionRequired",
167 "window/showStatus",
168 "telemetry/event",
170 continue
171 yield received
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))
180 def run_lsp_test(
181 self,
182 test_name: str,
183 test: Json,
184 expected: Json,
185 wait_for_server: bool,
186 use_serverless_ide: bool,
187 ) -> None:
188 if wait_for_server:
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__
193 + ". "
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
209 ) as lsp:
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
223 # we'll just skip.
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
228 self.assertEqual(
229 len(expected_items),
230 len(observed_items),
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
241 if received is None:
242 continue
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:
250 self.maxDiff = 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]:
259 self.maxDiff = None
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}
266 def load_and_run(
267 self,
268 test_name: str,
269 variables: Mapping[str, str],
270 wait_for_server: bool = True,
271 use_serverless_ide: bool = False,
272 ) -> None:
273 test, expected = self.load_test_data(test_name, variables)
274 self.run_lsp_test(
275 test_name=test_name,
276 test=test,
277 expected=expected,
278 wait_for_server=wait_for_server,
279 use_serverless_ide=use_serverless_ide,
282 def run_spec(
283 self,
284 spec: LspTestSpec,
285 variables: Mapping[str, str],
286 wait_for_server: bool,
287 use_serverless_ide: bool,
288 ) -> None:
289 if wait_for_server:
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")
302 text = json.dumps(
304 sent
305 for sent, _received in observed_transcript.values()
306 if sent is not None
308 indent=2,
310 with open(file, "w") as f:
311 f.write(text)
313 file = os.path.join(self.test_driver.template_repo, spec.name + ".received.log")
314 text = json.dumps(
316 received
317 for _sent, received in observed_transcript.values()
318 if received is not None
320 indent=2,
322 with open(file, "w") as f:
323 f.write(text)
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
329 # we'll just skip.
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",
341 if retcode == 7:
342 self.skipTest(
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"]
349 return {
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()
359 self.load_and_run(
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()
367 spec = (
368 self.initialize_spec(LspTestSpec("ide_completion"), use_serverless_ide=True)
369 .notification(
370 method="textDocument/didOpen",
371 params={
372 "textDocument": {
373 "uri": "${php_file_uri}",
374 "languageId": "hack",
375 "version": 1,
376 "text": "${php_file}",
380 .notification(
381 comment="Add '$x = $point1['' to test autocomplete for shapes",
382 method="textDocument/didChange",
383 params={
384 "textDocument": {"uri": "${php_file_uri}"},
385 "contentChanges": [
387 "range": {
388 "start": {"line": 22, "character": 0},
389 "end": {"line": 22, "character": 0},
391 "text": "$x = $point1['",
396 .request(
397 line=line(),
398 comment="autocomplete after user types a shape",
399 method="textDocument/completion",
400 params={
401 "textDocument": {"uri": "${php_file_uri}"},
402 "position": {"line": 22, "character": 14},
404 result={
405 "isIncomplete": False,
406 "items": [
408 "label": "'x'",
409 "kind": 12,
410 "detail": "literal",
411 "inlineDetail": "literal",
412 "sortText": "'x'",
413 "insertText": "'x'",
414 "insertTextFormat": InsertTextFormat.PlainText.value,
415 "data": {
416 "fullname": "'x'",
417 "filename": "${root_path}/completion.php",
418 "line": 22,
419 "char": 19,
423 "label": "'y'",
424 "kind": 12,
425 "detail": "literal",
426 "inlineDetail": "literal",
427 "sortText": "'y'",
428 "insertText": "'y'",
429 "insertTextFormat": InsertTextFormat.PlainText.value,
430 "data": {
431 "fullname": "'y'",
432 "filename": "${root_path}/completion.php",
433 "line": 22,
434 "char": 30,
439 powered_by="serverless_ide",
441 .notification(
442 comment="Add automatically closed apostrophes when typing a shape key, the way visual studio code does it",
443 method="textDocument/didChange",
444 params={
445 "textDocument": {"uri": "${php_file_uri}"},
446 "contentChanges": [
448 "range": {
449 "start": {"line": 22, "character": 0},
450 "end": {"line": 22, "character": 14},
452 "text": "$x = $point1['']",
457 .request(
458 line=line(),
459 comment="autocomplete after a shape, with VS Code automatically closed apostrophes",
460 method="textDocument/completion",
461 params={
462 "textDocument": {"uri": "${php_file_uri}"},
463 "position": {"line": 22, "character": 14},
465 result={
466 "isIncomplete": False,
467 "items": [
469 "label": "'x",
470 "kind": 12,
471 "detail": "literal",
472 "inlineDetail": "literal",
473 "sortText": "'x",
474 "insertText": "'x",
475 "insertTextFormat": InsertTextFormat.PlainText.value,
476 "data": {
477 "fullname": "'x'",
478 "filename": "${root_path}/completion.php",
479 "line": 22,
480 "char": 19,
484 "label": "'y",
485 "kind": 12,
486 "detail": "literal",
487 "inlineDetail": "literal",
488 "sortText": "'y",
489 "insertText": "'y",
490 "insertTextFormat": InsertTextFormat.PlainText.value,
491 "data": {
492 "fullname": "'y'",
493 "filename": "${root_path}/completion.php",
494 "line": 22,
495 "char": 30,
500 powered_by="serverless_ide",
502 .notification(
503 comment="Add '$x = <'",
504 method="textDocument/didChange",
505 params={
506 "textDocument": {"uri": "${php_file_uri}"},
507 "contentChanges": [
509 "range": {
510 "start": {"line": 3, "character": 0},
511 "end": {"line": 3, "character": 0},
513 "text": "$x = <",
518 .request(
519 line=line(),
520 comment="autocomplete after '$x = <'",
521 method="textDocument/completion",
522 params={
523 "textDocument": {"uri": "${php_file_uri}"},
524 "position": {"line": 3, "character": 6},
526 result={
527 "isIncomplete": False,
528 "items": [
530 "label": "ab:cd:alpha",
531 "kind": 7,
532 "detail": "class",
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",
541 "kind": 7,
542 "detail": "class",
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",
553 .notification(
554 comment="Add '$x = <a'",
555 method="textDocument/didChange",
556 params={
557 "textDocument": {"uri": "${php_file_uri}"},
558 "contentChanges": [
560 "range": {
561 "start": {"line": 3, "character": 0},
562 "end": {"line": 3, "character": 6},
564 "text": "$x = <a",
569 .request(
570 line=line(),
571 comment="autocomplete after '$x = <a'",
572 method="textDocument/completion",
573 params={
574 "textDocument": {"uri": "${php_file_uri}"},
575 "position": {"line": 3, "character": 7},
577 result={
578 "isIncomplete": False,
579 "items": [
581 "label": "ab:cd:alpha",
582 "kind": 7,
583 "detail": "class",
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",
592 "kind": 7,
593 "detail": "class",
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",
604 .notification(
605 comment="Add '$x = <ab:'",
606 method="textDocument/didChange",
607 params={
608 "textDocument": {"uri": "${php_file_uri}"},
609 "contentChanges": [
611 "range": {
612 "start": {"line": 3, "character": 0},
613 "end": {"line": 3, "character": 7},
615 "text": "$x = <ab:",
620 .request(
621 line=line(),
622 comment="autocomplete after '$x = <ab:'",
623 method="textDocument/completion",
624 params={
625 "textDocument": {"uri": "${php_file_uri}"},
626 "position": {"line": 3, "character": 9},
628 result={
629 "isIncomplete": False,
630 "items": [
632 "label": "ab:cd:alpha",
633 "kind": 7,
634 "detail": "class",
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",
643 "kind": 7,
644 "detail": "class",
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",
655 .notification(
656 comment="Add '$x = <ab:cd:text '",
657 method="textDocument/didChange",
658 params={
659 "textDocument": {"uri": "${php_file_uri}"},
660 "contentChanges": [
662 "range": {
663 "start": {"line": 3, "character": 0},
664 "end": {"line": 3, "character": 9},
666 "text": "$x = <ab:cd:text ",
671 .request(
672 line=line(),
673 comment="autocomplete after '$x = <ab:cd:text '",
674 method="textDocument/completion",
675 params={
676 "textDocument": {"uri": "${php_file_uri}"},
677 "position": {"line": 3, "character": 17},
679 result={
680 "isIncomplete": False,
681 "items": [
683 "label": "width",
684 "kind": 10,
685 "detail": "?int",
686 "inlineDetail": "?int",
687 "sortText": "width",
688 "insertText": "width",
689 "insertTextFormat": InsertTextFormat.PlainText.value,
690 "data": {
691 "fullname": ":width",
692 "filename": "${root_path}/completion_extras.php",
693 "line": 5,
694 "char": 27,
695 "base_class": "\\:ab:cd:text",
699 "label": "color",
700 "kind": 10,
701 "detail": "?string",
702 "inlineDetail": "?string",
703 "sortText": "color",
704 "insertText": "color",
705 "insertTextFormat": InsertTextFormat.PlainText.value,
706 "data": {
707 "fullname": ":color",
708 "filename": "${root_path}/completion_extras.php",
709 "line": 5,
710 "char": 13,
711 "base_class": "\\:ab:cd:text",
716 powered_by="serverless_ide",
718 .notification(
719 comment="Add '$x = <ab:cd:text w'",
720 method="textDocument/didChange",
721 params={
722 "textDocument": {"uri": "${php_file_uri}"},
723 "contentChanges": [
725 "range": {
726 "start": {"line": 3, "character": 0},
727 "end": {"line": 3, "character": 17},
729 "text": "$x = <ab:cd:text w",
734 .request(
735 line=line(),
736 comment="autocomplete after '$x = <ab:cd:text w'",
737 method="textDocument/completion",
738 params={
739 "textDocument": {"uri": "${php_file_uri}"},
740 "position": {"line": 3, "character": 18},
742 result={
743 "isIncomplete": False,
744 "items": [
746 "label": "width",
747 "kind": 10,
748 "detail": "?int",
749 "inlineDetail": "?int",
750 "sortText": "width",
751 "insertText": "width",
752 "insertTextFormat": InsertTextFormat.PlainText.value,
753 "data": {
754 "fullname": ":width",
755 "filename": "${root_path}/completion_extras.php",
756 "line": 5,
757 "char": 27,
758 "base_class": "\\:ab:cd:text",
762 "label": "color",
763 "kind": 10,
764 "detail": "?string",
765 "inlineDetail": "?string",
766 "sortText": "color",
767 "insertText": "color",
768 "insertTextFormat": InsertTextFormat.PlainText.value,
769 "data": {
770 "fullname": ":color",
771 "filename": "${root_path}/completion_extras.php",
772 "line": 5,
773 "char": 13,
774 "base_class": "\\:ab:cd:text",
779 powered_by="serverless_ide",
781 .notification(
782 comment="Add '$x = new :'",
783 method="textDocument/didChange",
784 params={
785 "textDocument": {"uri": "${php_file_uri}"},
786 "contentChanges": [
788 "range": {
789 "start": {"line": 3, "character": 0},
790 "end": {"line": 3, "character": 18},
792 "text": "$x = new :",
797 .request(
798 line=line(),
799 comment="autocomplete after '$x = new :'",
800 method="textDocument/completion",
801 params={
802 "textDocument": {"uri": "${php_file_uri}"},
803 "position": {"line": 3, "character": 10},
805 result={
806 "isIncomplete": False,
807 "items": [
809 "label": ":ab:cd:alpha",
810 "kind": 7,
811 "detail": "class",
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",
820 "kind": 7,
821 "detail": "class",
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",
832 .notification(
833 comment="Add '$x = new :a'",
834 method="textDocument/didChange",
835 params={
836 "textDocument": {"uri": "${php_file_uri}"},
837 "contentChanges": [
839 "range": {
840 "start": {"line": 3, "character": 0},
841 "end": {"line": 3, "character": 10},
843 "text": "$x = new :a",
848 .request(
849 line=line(),
850 comment="autocomplete after '$x = new :a'",
851 method="textDocument/completion",
852 params={
853 "textDocument": {"uri": "${php_file_uri}"},
854 "position": {"line": 3, "character": 11},
856 result={
857 "isIncomplete": False,
858 "items": [
860 "label": ":ab:cd:alpha",
861 "kind": 7,
862 "detail": "class",
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",
871 "kind": 7,
872 "detail": "class",
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
884 .request(
885 line=line(),
886 comment="autocomplete resolving after '$x = new :a'",
887 method="completionItem/resolve",
888 params={
889 "label": ":ab:cd:alpha",
890 "kind": 7,
891 "detail": "class",
892 "inlineDetail": "class",
893 "itemType": ":ab:cd:alpha",
894 "insertText": ":ab:cd:alpha",
895 "insertTextFormat": InsertTextFormat.PlainText.value,
896 "data": {"fullname": ":ab:cd:alpha"},
898 result={
899 "label": ":ab:cd:alpha",
900 "kind": 7,
901 "detail": "class",
902 "inlineDetail": "class",
903 "itemType": ":ab:cd:alpha",
904 "documentation": {
905 "kind": "markdown",
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
915 .notification(
916 comment="Add '$x = <a'",
917 method="textDocument/didChange",
918 params={
919 "textDocument": {"uri": "${php_file_uri}"},
920 "contentChanges": [
922 "range": {
923 "start": {"line": 3, "character": 0},
924 "end": {"line": 3, "character": 11},
926 "text": "$x = <a",
931 .request(
932 line=line(),
933 comment="autocomplete after '$x = <a'",
934 method="textDocument/completion",
935 params={
936 "textDocument": {"uri": "${php_file_uri}"},
937 "position": {"line": 3, "character": 7},
939 result={
940 "isIncomplete": False,
941 "items": [
943 "label": "ab:cd:alpha",
944 "kind": 7,
945 "detail": "class",
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",
954 "kind": 7,
955 "detail": "class",
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",
966 .request(
967 line=line(),
968 comment="autocomplete resolving after '$x = <a'",
969 method="completionItem/resolve",
970 params={
971 "label": "ab:cd:alpha",
972 "kind": 7,
973 "detail": "class",
974 "inlineDetail": "class",
975 "insertText": "ab:cd:alpha",
976 "insertTextFormat": InsertTextFormat.PlainText.value,
977 "data": {"fullname": ":ab:cd:alpha"},
979 result={
980 "label": "ab:cd:alpha",
981 "kind": 7,
982 "detail": "class",
983 "inlineDetail": "class",
984 "documentation": {
985 "kind": "markdown",
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",
994 .notification(
995 comment="Add '$x = <ab:cd:text/>; $y = $x->'",
996 method="textDocument/didChange",
997 params={
998 "textDocument": {"uri": "${php_file_uri}"},
999 "contentChanges": [
1001 "range": {
1002 "start": {"line": 3, "character": 0},
1003 "end": {"line": 3, "character": 7},
1005 "text": "$x = <ab:cd:text/>; $y = $x->",
1010 .request(
1011 line=line(),
1012 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
1013 method="textDocument/completion",
1014 params={
1015 "textDocument": {"uri": "${php_file_uri}"},
1016 "position": {"line": 3, "character": 29},
1018 result={
1019 "isIncomplete": False,
1020 "items": [
1022 "label": ":width",
1023 "kind": 10,
1024 "detail": "?int",
1025 "inlineDetail": "?int",
1026 "sortText": ":width",
1027 "insertText": ":width",
1028 "insertTextFormat": InsertTextFormat.PlainText.value,
1029 "data": {
1030 "fullname": ":width",
1031 "filename": "${root_path}/completion_extras.php",
1032 "line": 5,
1033 "char": 27,
1034 "base_class": "\\:ab:cd:text",
1038 "label": ":color",
1039 "kind": 10,
1040 "detail": "?string",
1041 "inlineDetail": "?string",
1042 "sortText": ":color",
1043 "insertText": ":color",
1044 "insertTextFormat": InsertTextFormat.PlainText.value,
1045 "data": {
1046 "fullname": ":color",
1047 "filename": "${root_path}/completion_extras.php",
1048 "line": 5,
1049 "char": 13,
1050 "base_class": "\\:ab:cd:text",
1055 powered_by="serverless_ide",
1057 .notification(
1058 comment="Add '$x = <ab:cd:text/>; $y = $x->:'",
1059 method="textDocument/didChange",
1060 params={
1061 "textDocument": {"uri": "${php_file_uri}"},
1062 "contentChanges": [
1064 "range": {
1065 "start": {"line": 3, "character": 0},
1066 "end": {"line": 3, "character": 29},
1068 "text": "$x = <ab:cd:text/>; $y = $x->:",
1073 .request(
1074 line=line(),
1075 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
1076 method="textDocument/completion",
1077 params={
1078 "textDocument": {"uri": "${php_file_uri}"},
1079 "position": {"line": 3, "character": 30},
1081 result={
1082 "isIncomplete": False,
1083 "items": [
1085 "label": ":width",
1086 "kind": 10,
1087 "detail": "?int",
1088 "inlineDetail": "?int",
1089 "sortText": ":width",
1090 "insertText": ":width",
1091 "insertTextFormat": InsertTextFormat.PlainText.value,
1092 "data": {
1093 "fullname": ":width",
1094 "filename": "${root_path}/completion_extras.php",
1095 "line": 5,
1096 "char": 27,
1097 "base_class": "\\:ab:cd:text",
1101 "label": ":color",
1102 "kind": 10,
1103 "detail": "?string",
1104 "inlineDetail": "?string",
1105 "sortText": ":color",
1106 "insertText": ":color",
1107 "insertTextFormat": InsertTextFormat.PlainText.value,
1108 "data": {
1109 "fullname": ":color",
1110 "filename": "${root_path}/completion_extras.php",
1111 "line": 5,
1112 "char": 13,
1113 "base_class": "\\:ab:cd:text",
1118 powered_by="serverless_ide",
1120 .notification(
1121 comment="Add 'test_fun'",
1122 method="textDocument/didChange",
1123 params={
1124 "textDocument": {"uri": "${php_file_uri}"},
1125 "contentChanges": [
1127 "range": {
1128 "start": {"line": 3, "character": 0},
1129 "end": {"line": 3, "character": 30},
1131 "text": "test_fun",
1136 .request(
1137 line=line(),
1138 comment="autocomplete after 'test_fun'",
1139 method="textDocument/completion",
1140 params={
1141 "textDocument": {"uri": "${php_file_uri}"},
1142 "position": {"line": 3, "character": 8},
1144 result={
1145 "isIncomplete": False,
1146 "items": [
1148 "label": "test_function",
1149 "kind": 3,
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",
1161 .request(
1162 line=line(),
1163 comment="autocomplete resolving after 'test_fun'",
1164 method="completionItem/resolve",
1165 params={
1166 "label": "test_function",
1167 "kind": 3,
1168 "detail": "function(): void",
1169 "inlineDetail": "()",
1170 "itemType": "void",
1171 "insertText": "test_function",
1172 "insertTextFormat": InsertTextFormat.PlainText.value,
1173 "data": {
1174 "filename": "${root_path}/completion.php",
1175 "line": 8,
1176 "char": 10,
1179 result={
1180 "label": "test_function",
1181 "kind": 3,
1182 "detail": "function(): void",
1183 "inlineDetail": "()",
1184 "itemType": "void",
1185 "documentation": {
1186 "kind": "markdown",
1187 "value": "test_function docblock.",
1189 "insertText": "test_function",
1190 "insertTextFormat": InsertTextFormat.PlainText.value,
1191 "data": {
1192 "filename": "${root_path}/completion.php",
1193 "line": 8,
1194 "char": 10,
1197 powered_by="serverless_ide",
1199 .notification(
1200 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
1201 method="textDocument/didChange",
1202 params={
1203 "textDocument": {"uri": "${php_file_uri}"},
1204 "contentChanges": [
1206 "range": {
1207 "start": {"line": 3, "character": 0},
1208 "end": {"line": 3, "character": 8},
1210 "text": "switch (Elsa::Alonso) { case Elsa:",
1215 .request(
1216 line=line(),
1217 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
1218 method="textDocument/completion",
1219 params={
1220 "textDocument": {"uri": "${php_file_uri}"},
1221 "position": {"line": 3, "character": 34},
1223 result={"isIncomplete": False, "items": []},
1224 powered_by="serverless_ide",
1226 .notification(
1227 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
1228 method="textDocument/didChange",
1229 params={
1230 "textDocument": {"uri": "${php_file_uri}"},
1231 "contentChanges": [
1233 "range": {
1234 "start": {"line": 3, "character": 0},
1235 "end": {"line": 3, "character": 34},
1237 "text": "switch (Elsa::Alonso) { case Elsa::",
1242 .request(
1243 line=line(),
1244 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
1245 method="textDocument/completion",
1246 params={
1247 "textDocument": {"uri": "${php_file_uri}"},
1248 "position": {"line": 3, "character": 35},
1250 result={
1251 "isIncomplete": False,
1252 "items": [
1254 "label": "class",
1255 "kind": 21,
1256 "detail": "classname<this>",
1257 "inlineDetail": "classname<this>",
1258 "sortText": "class",
1259 "insertText": "class",
1260 "insertTextFormat": InsertTextFormat.PlainText.value,
1261 "data": {
1262 "fullname": "class",
1263 "filename": "${root_path}/completion_extras.php",
1264 "line": 13,
1265 "char": 6,
1266 "base_class": "\\Elsa",
1270 "label": "Bard",
1271 "kind": 21,
1272 "detail": "Elsa",
1273 "inlineDetail": "Elsa",
1274 "sortText": "Bard",
1275 "insertText": "Bard",
1276 "insertTextFormat": InsertTextFormat.PlainText.value,
1277 "data": {
1278 "fullname": "Bard",
1279 "filename": "${root_path}/completion_extras.php",
1280 "line": 13,
1281 "char": 12,
1282 "base_class": "\\Elsa",
1286 "label": "Alonso",
1287 "kind": 21,
1288 "detail": "Elsa",
1289 "inlineDetail": "Elsa",
1290 "sortText": "Alonso",
1291 "insertText": "Alonso",
1292 "insertTextFormat": InsertTextFormat.PlainText.value,
1293 "data": {
1294 "fullname": "Alonso",
1295 "filename": "${root_path}/completion_extras.php",
1296 "line": 13,
1297 "char": 12,
1298 "base_class": "\\Elsa",
1302 "label": "isValid",
1303 "kind": 2,
1304 "detail": "function(mixed $value): bool",
1305 "inlineDetail": "(mixed $value)",
1306 "itemType": "bool",
1307 "sortText": "isValid",
1308 "insertText": "isValid(${1:\\$value})",
1309 "insertTextFormat": InsertTextFormat.Snippet.value,
1310 "data": {
1311 "fullname": "isValid",
1312 "filename": "${hhi_path}/BuiltinEnum.hhi",
1313 "line": 49,
1314 "char": 32,
1315 "base_class": "\\Elsa",
1319 "label": "getValues",
1320 "kind": 2,
1321 "detail": "function(): darray<string, Elsa>",
1322 "inlineDetail": "()",
1323 "itemType": "darray<string, Elsa>",
1324 "sortText": "getValues",
1325 "insertText": "getValues()",
1326 "insertTextFormat": InsertTextFormat.Snippet.value,
1327 "data": {
1328 "fullname": "getValues",
1329 "filename": "${hhi_path}/BuiltinEnum.hhi",
1330 "line": 34,
1331 "char": 32,
1332 "base_class": "\\Elsa",
1336 "label": "getNames",
1337 "kind": 2,
1338 "detail": "function(): darray<Elsa, string>",
1339 "inlineDetail": "()",
1340 "itemType": "darray<Elsa, string>",
1341 "sortText": "getNames",
1342 "insertText": "getNames()",
1343 "insertTextFormat": InsertTextFormat.Snippet.value,
1344 "data": {
1345 "fullname": "getNames",
1346 "filename": "${hhi_path}/BuiltinEnum.hhi",
1347 "line": 43,
1348 "char": 32,
1349 "base_class": "\\Elsa",
1353 "label": "coerce",
1354 "kind": 2,
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,
1361 "data": {
1362 "fullname": "coerce",
1363 "filename": "${hhi_path}/BuiltinEnum.hhi",
1364 "line": 56,
1365 "char": 32,
1366 "base_class": "\\Elsa",
1370 "label": "assertAll",
1371 "kind": 2,
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,
1378 "data": {
1379 "fullname": "assertAll",
1380 "filename": "${hhi_path}/BuiltinEnum.hhi",
1381 "line": 70,
1382 "char": 32,
1383 "base_class": "\\Elsa",
1387 "label": "assert",
1388 "kind": 2,
1389 "detail": "function(mixed $value): Elsa",
1390 "inlineDetail": "(mixed $value)",
1391 "itemType": "Elsa",
1392 "sortText": "assert",
1393 "insertText": "assert(${1:\\$value})",
1394 "insertTextFormat": InsertTextFormat.Snippet.value,
1395 "data": {
1396 "fullname": "assert",
1397 "filename": "${hhi_path}/BuiltinEnum.hhi",
1398 "line": 63,
1399 "char": 32,
1400 "base_class": "\\Elsa",
1405 powered_by="serverless_ide",
1407 .notification(
1408 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1409 method="textDocument/didChange",
1410 params={
1411 "textDocument": {"uri": "${php_file_uri}"},
1412 "contentChanges": [
1414 "range": {
1415 "start": {"line": 3, "character": 0},
1416 "end": {"line": 3, "character": 35},
1418 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
1423 .request(
1424 line=line(),
1425 comment="docblock resolve after 'switch (Elsa::Alonso) { case Elsa::'",
1426 method="completionItem/resolve",
1427 params={
1428 "label": "isValid",
1429 "kind": 2,
1430 "detail": "function(mixed $value): bool",
1431 "inlineDetail": "(mixed $value)",
1432 "itemType": "bool",
1433 "insertTextFormat": InsertTextFormat.PlainText.value,
1434 "textEdit": {
1435 "range": {
1436 "start": {"line": 3, "character": 35},
1437 "end": {"line": 3, "character": 35},
1439 "newText": "isValid",
1441 "data": {
1442 "filename": "${hhi_path}/BuiltinEnum.hhi",
1443 "line": 49,
1444 "char": 32,
1447 result={
1448 "label": "isValid",
1449 "kind": 2,
1450 "detail": "function(mixed $value): bool",
1451 "inlineDetail": "(mixed $value)",
1452 "itemType": "bool",
1453 "documentation": {
1454 "kind": "markdown",
1455 "value": "Returns whether or not the value is defined as a constant.",
1457 "insertTextFormat": InsertTextFormat.PlainText.value,
1458 "textEdit": {
1459 "range": {
1460 "start": {"line": 3, "character": 35},
1461 "end": {"line": 3, "character": 35},
1463 "newText": "isValid",
1465 "data": {
1466 "filename": "${hhi_path}/BuiltinEnum.hhi",
1467 "line": 49,
1468 "char": 32,
1471 powered_by="serverless_ide",
1473 .request(
1474 line=line(),
1475 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1476 method="textDocument/completion",
1477 params={
1478 "textDocument": {"uri": "${php_file_uri}"},
1479 "position": {"line": 3, "character": 42},
1481 result={"isIncomplete": False, "items": []},
1482 powered_by="serverless_ide",
1484 .notification(
1485 comment="Add 'TestNS\\'",
1486 method="textDocument/didChange",
1487 params={
1488 "textDocument": {"uri": "${php_file_uri}"},
1489 "contentChanges": [
1491 "range": {
1492 "start": {"line": 3, "character": 0},
1493 "end": {"line": 3, "character": 42},
1495 "text": "TestNS\\",
1500 .request(
1501 line=line(),
1502 comment="autocomplete after 'TestNS\\'",
1503 method="textDocument/completion",
1504 params={
1505 "textDocument": {"uri": "${php_file_uri}"},
1506 "position": {"line": 3, "character": 7},
1508 result={
1509 "isIncomplete": False,
1510 "items": [
1512 "label": "test_func",
1513 "kind": 3,
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",
1525 .notification(
1526 comment="Add '$cc = new CompletionClass(); $cc->interfa'",
1527 method="textDocument/didChange",
1528 params={
1529 "textDocument": {"uri": "${php_file_uri}"},
1530 "contentChanges": [
1532 "range": {
1533 "start": {"line": 3, "character": 0},
1534 "end": {"line": 3, "character": 7},
1536 "text": "$cc = new CompletionClass(); $cc->interfa",
1541 .request(
1542 line=line(),
1543 comment="autocomplete after '$cc = new CompletionClass(); $cc->interfa'",
1544 method="textDocument/completion",
1545 params={
1546 "textDocument": {"uri": "${php_file_uri}"},
1547 "position": {"line": 3, "character": 41},
1549 result={
1550 "isIncomplete": False,
1551 "items": [
1553 "label": "interfaceDocBlockMethod",
1554 "kind": 2,
1555 "detail": "function(): void",
1556 "inlineDetail": "()",
1557 "itemType": "void",
1558 "sortText": "interfaceDocBlockMethod",
1559 "insertText": "interfaceDocBlockMethod()",
1560 "insertTextFormat": InsertTextFormat.Snippet.value,
1561 "data": {
1562 "fullname": "interfaceDocBlockMethod",
1563 "filename": "${root_path}/completion.php",
1564 "line": 18,
1565 "char": 19,
1566 "base_class": "\\CompletionClass",
1571 powered_by="serverless_ide",
1573 .request(
1574 line=line(),
1575 comment="autocomplete resolving after '$cc = new CompletionClass(); $cc->interfa'",
1576 method="completionItem/resolve",
1577 params={
1578 "label": "interfaceDocBlockMethod",
1579 "kind": 2,
1580 "detail": "function(): void",
1581 "inlineDetail": "()",
1582 "itemType": "void",
1583 "insertTextFormat": InsertTextFormat.PlainText.value,
1584 "textEdit": {
1585 "range": {
1586 "start": {"line": 3, "character": 34},
1587 "end": {"line": 3, "character": 41},
1589 "newText": "interfaceDocBlockMethod",
1591 "data": {
1592 "filename": "${root_path}/completion.php",
1593 "line": 18,
1594 "char": 19,
1597 result={
1598 "label": "interfaceDocBlockMethod",
1599 "kind": 2,
1600 "detail": "function(): void",
1601 "inlineDetail": "()",
1602 "itemType": "void",
1603 "insertTextFormat": InsertTextFormat.PlainText.value,
1604 "textEdit": {
1605 "range": {
1606 "start": {"line": 3, "character": 34},
1607 "end": {"line": 3, "character": 41},
1609 "newText": "interfaceDocBlockMethod",
1611 "data": {
1612 "filename": "${root_path}/completion.php",
1613 "line": 18,
1614 "char": 19,
1617 powered_by="serverless_ide",
1619 .notification(
1620 comment="Add 'DeprecatedClass::'",
1621 method="textDocument/didChange",
1622 params={
1623 "textDocument": {"uri": "${php_file_uri}"},
1624 "contentChanges": [
1626 "range": {
1627 "start": {"line": 3, "character": 0},
1628 "end": {"line": 3, "character": 41},
1630 "text": "DeprecatedClass::",
1635 .request(
1636 line=line(),
1637 comment="autocomplete after 'DeprecatedClass::'",
1638 method="textDocument/completion",
1639 params={
1640 "textDocument": {"uri": "${php_file_uri}"},
1641 "position": {"line": 3, "character": 17},
1643 result={
1644 "isIncomplete": False,
1645 "items": [
1647 "label": "class",
1648 "kind": 21,
1649 "detail": "classname<this>",
1650 "inlineDetail": "classname<this>",
1651 "sortText": "class",
1652 "insertText": "class",
1653 "insertTextFormat": InsertTextFormat.PlainText.value,
1654 "data": {
1655 "fullname": "class",
1656 "filename": "${root_path}/completion_extras.php",
1657 "line": 18,
1658 "char": 13,
1659 "base_class": "\\DeprecatedClass",
1663 "label": "test_do_not_use",
1664 "kind": 2,
1665 "detail": "function(): void",
1666 "inlineDetail": "()",
1667 "itemType": "void",
1668 "sortText": "~test_do_not_use",
1669 "insertText": "test_do_not_use()",
1670 "insertTextFormat": InsertTextFormat.Snippet.value,
1671 "data": {
1672 "fullname": "test_do_not_use",
1673 "filename": "${root_path}/completion_extras.php",
1674 "line": 22,
1675 "char": 26,
1676 "base_class": "\\DeprecatedClass",
1680 "label": "getName",
1681 "kind": 2,
1682 "detail": "function(): void",
1683 "inlineDetail": "()",
1684 "itemType": "void",
1685 "sortText": "getName",
1686 "insertText": "getName()",
1687 "insertTextFormat": InsertTextFormat.Snippet.value,
1688 "data": {
1689 "fullname": "getName",
1690 "filename": "${root_path}/completion_extras.php",
1691 "line": 19,
1692 "char": 26,
1693 "base_class": "\\DeprecatedClass",
1697 "label": "getAttributes_DO_NOT_USE",
1698 "kind": 2,
1699 "detail": "function(): void",
1700 "inlineDetail": "()",
1701 "itemType": "void",
1702 "sortText": "~getAttributes_DO_NOT_USE",
1703 "insertText": "getAttributes_DO_NOT_USE()",
1704 "insertTextFormat": InsertTextFormat.Snippet.value,
1705 "data": {
1706 "fullname": "getAttributes_DO_NOT_USE",
1707 "filename": "${root_path}/completion_extras.php",
1708 "line": 21,
1709 "char": 26,
1710 "base_class": "\\DeprecatedClass",
1714 "label": "__getLoader",
1715 "kind": 2,
1716 "detail": "function(): void",
1717 "inlineDetail": "()",
1718 "itemType": "void",
1719 "sortText": "~__getLoader",
1720 "insertText": "__getLoader()",
1721 "insertTextFormat": InsertTextFormat.Snippet.value,
1722 "data": {
1723 "fullname": "__getLoader",
1724 "filename": "${root_path}/completion_extras.php",
1725 "line": 20,
1726 "char": 26,
1727 "base_class": "\\DeprecatedClass",
1732 powered_by="serverless_ide",
1734 .notification(
1735 comment="Add 'call_lambda(3, $m'",
1736 method="textDocument/didChange",
1737 params={
1738 "textDocument": {"uri": "${php_file_uri}"},
1739 "contentChanges": [
1741 "range": {
1742 "start": {"line": 30, "character": 0},
1743 "end": {"line": 30, "character": 0},
1745 "text": " call_lambda(3, $m",
1750 .request(
1751 line=line(),
1752 comment="autocomplete results for 'call_lambda(3, $m'",
1753 method="textDocument/completion",
1754 params={
1755 "textDocument": {"uri": "${php_file_uri}"},
1756 "position": {"line": 30, "character": 19},
1758 result={
1759 "isIncomplete": False,
1760 "items": [
1762 "label": "$mylambda",
1763 "kind": 6,
1764 "detail": "local variable",
1765 "inlineDetail": "(num $n)",
1766 "itemType": "int",
1767 "sortText": "$mylambda",
1768 "insertText": "$mylambda",
1769 "insertTextFormat": InsertTextFormat.PlainText.value,
1770 "data": {
1771 "fullname": "$mylambda",
1772 "filename": "${root_path}/completion.php",
1773 "line": 30,
1774 "char": 15,
1779 powered_by="serverless_ide",
1781 .request(
1782 line=line(),
1783 comment="resolve autocompletion for $mylambda'",
1784 method="completionItem/resolve",
1785 params={
1786 "label": "$mylambda",
1787 "kind": 6,
1788 "detail": "local variable",
1789 "inlineDetail": "(num $n)",
1790 "itemType": "int",
1791 "insertTextFormat": InsertTextFormat.PlainText.value,
1792 "textEdit": {
1793 "range": {
1794 "start": {"line": 30, "character": 17},
1795 "end": {"line": 30, "character": 19},
1797 "newText": "$mylambda",
1799 "data": {
1800 "filename": "${root_path}/completion.php",
1801 "line": 30,
1802 "char": 15,
1805 result={
1806 "label": "$mylambda",
1807 "kind": 6,
1808 "detail": "local variable",
1809 "inlineDetail": "(num $n)",
1810 "itemType": "int",
1811 "insertTextFormat": InsertTextFormat.PlainText.value,
1812 "textEdit": {
1813 "range": {
1814 "start": {"line": 30, "character": 17},
1815 "end": {"line": 30, "character": 19},
1817 "newText": "$mylambda",
1819 "data": {
1820 "filename": "${root_path}/completion.php",
1821 "line": 30,
1822 "char": 15,
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()
1837 spec = (
1838 self.initialize_spec(
1839 LspTestSpec("serverless_ide_completion_legacy"), use_serverless_ide=True
1841 .notification(
1842 method="textDocument/didOpen",
1843 params={
1844 "textDocument": {
1845 "uri": "${php_file_uri}",
1846 "languageId": "hack",
1847 "version": 1,
1848 "text": "${php_file}",
1852 .notification(
1853 comment="Add '$x = <'",
1854 method="textDocument/didChange",
1855 params={
1856 "textDocument": {"uri": "${php_file_uri}"},
1857 "contentChanges": [
1859 "range": {
1860 "start": {"line": 3, "character": 0},
1861 "end": {"line": 3, "character": 0},
1863 "text": "$x = <",
1868 .request(
1869 line=line(),
1870 comment="autocomplete after '$x = <'",
1871 method="textDocument/completion",
1872 params={
1873 "textDocument": {"uri": "${php_file_uri}"},
1874 "position": {"line": 3, "character": 6},
1876 result={
1877 "isIncomplete": False,
1878 "items": [
1880 "label": "ab:cd:alpha",
1881 "kind": 7,
1882 "detail": "class",
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",
1891 "kind": 7,
1892 "detail": "class",
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",
1903 .notification(
1904 comment="Add '$x = <a'",
1905 method="textDocument/didChange",
1906 params={
1907 "textDocument": {"uri": "${php_file_uri}"},
1908 "contentChanges": [
1910 "range": {
1911 "start": {"line": 3, "character": 0},
1912 "end": {"line": 3, "character": 6},
1914 "text": "$x = <a",
1919 .request(
1920 line=line(),
1921 comment="autocomplete after '$x = <a'",
1922 method="textDocument/completion",
1923 params={
1924 "textDocument": {"uri": "${php_file_uri}"},
1925 "position": {"line": 3, "character": 7},
1927 result={
1928 "isIncomplete": False,
1929 "items": [
1931 "label": "ab:cd:alpha",
1932 "kind": 7,
1933 "detail": "class",
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",
1942 "kind": 7,
1943 "detail": "class",
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",
1954 .notification(
1955 comment="Add '$x = <ab:'",
1956 method="textDocument/didChange",
1957 params={
1958 "textDocument": {"uri": "${php_file_uri}"},
1959 "contentChanges": [
1961 "range": {
1962 "start": {"line": 3, "character": 0},
1963 "end": {"line": 3, "character": 7},
1965 "text": "$x = <ab:",
1970 .request(
1971 line=line(),
1972 comment="autocomplete after '$x = <ab:'.",
1973 method="textDocument/completion",
1974 params={
1975 "textDocument": {"uri": "${php_file_uri}"},
1976 "position": {"line": 3, "character": 9},
1978 result={
1979 "isIncomplete": False,
1980 "items": [
1982 "label": "ab:cd:alpha",
1983 "kind": 7,
1984 "detail": "class",
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",
1993 "kind": 7,
1994 "detail": "class",
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",
2005 .notification(
2006 comment="Add '$x = <ab:cd:text '",
2007 method="textDocument/didChange",
2008 params={
2009 "textDocument": {"uri": "${php_file_uri}"},
2010 "contentChanges": [
2012 "range": {
2013 "start": {"line": 3, "character": 0},
2014 "end": {"line": 3, "character": 9},
2016 "text": "$x = <ab:cd:text ",
2021 .request(
2022 line=line(),
2023 comment="autocomplete after '$x = <ab:cd:text '",
2024 method="textDocument/completion",
2025 params={
2026 "textDocument": {"uri": "${php_file_uri}"},
2027 "position": {"line": 3, "character": 17},
2029 result={
2030 "isIncomplete": False,
2031 "items": [
2033 "label": "width",
2034 "kind": 10,
2035 "detail": "?int",
2036 "inlineDetail": "?int",
2037 "sortText": "width",
2038 "insertText": "width",
2039 "insertTextFormat": InsertTextFormat.PlainText.value,
2040 "data": {
2041 "fullname": ":width",
2042 "filename": "${root_path}/completion_extras.php",
2043 "line": 5,
2044 "char": 27,
2045 "base_class": "\\:ab:cd:text",
2049 "label": "color",
2050 "kind": 10,
2051 "detail": "?string",
2052 "inlineDetail": "?string",
2053 "sortText": "color",
2054 "insertText": "color",
2055 "insertTextFormat": InsertTextFormat.PlainText.value,
2056 "data": {
2057 "fullname": ":color",
2058 "filename": "${root_path}/completion_extras.php",
2059 "line": 5,
2060 "char": 13,
2061 "base_class": "\\:ab:cd:text",
2066 powered_by="serverless_ide",
2068 .notification(
2069 comment="Add '$x = <ab:cd:text w'",
2070 method="textDocument/didChange",
2071 params={
2072 "textDocument": {"uri": "${php_file_uri}"},
2073 "contentChanges": [
2075 "range": {
2076 "start": {"line": 3, "character": 0},
2077 "end": {"line": 3, "character": 17},
2079 "text": "$x = <ab:cd:text w",
2084 .request(
2085 line=line(),
2086 comment="autocomplete after '$x = <ab:cd:text w'",
2087 method="textDocument/completion",
2088 params={
2089 "textDocument": {"uri": "${php_file_uri}"},
2090 "position": {"line": 3, "character": 18},
2092 result={
2093 "isIncomplete": False,
2094 "items": [
2096 "label": "width",
2097 "kind": 10,
2098 "detail": "?int",
2099 "inlineDetail": "?int",
2100 "sortText": "width",
2101 "insertText": "width",
2102 "insertTextFormat": InsertTextFormat.PlainText.value,
2103 "data": {
2104 "fullname": ":width",
2105 "filename": "${root_path}/completion_extras.php",
2106 "line": 5,
2107 "char": 27,
2108 "base_class": "\\:ab:cd:text",
2112 "label": "color",
2113 "kind": 10,
2114 "detail": "?string",
2115 "inlineDetail": "?string",
2116 "sortText": "color",
2117 "insertText": "color",
2118 "insertTextFormat": InsertTextFormat.PlainText.value,
2119 "data": {
2120 "fullname": ":color",
2121 "filename": "${root_path}/completion_extras.php",
2122 "line": 5,
2123 "char": 13,
2124 "base_class": "\\:ab:cd:text",
2129 powered_by="serverless_ide",
2131 .notification(
2132 comment="Add '$x = new :''",
2133 method="textDocument/didChange",
2134 params={
2135 "textDocument": {"uri": "${php_file_uri}"},
2136 "contentChanges": [
2138 "range": {
2139 "start": {"line": 3, "character": 0},
2140 "end": {"line": 3, "character": 18},
2142 "text": "$x = new :",
2147 .request(
2148 line=line(),
2149 comment="autocomplete after '$x = new :'",
2150 method="textDocument/completion",
2151 params={
2152 "textDocument": {"uri": "${php_file_uri}"},
2153 "position": {"line": 3, "character": 10},
2155 result={
2156 "isIncomplete": False,
2157 "items": [
2159 "label": ":ab:cd:alpha",
2160 "kind": 7,
2161 "detail": "class",
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",
2170 "kind": 7,
2171 "detail": "class",
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",
2182 .notification(
2183 comment="Add '$x = new :a'",
2184 method="textDocument/didChange",
2185 params={
2186 "textDocument": {"uri": "${php_file_uri}"},
2187 "contentChanges": [
2189 "range": {
2190 "start": {"line": 3, "character": 0},
2191 "end": {"line": 3, "character": 10},
2193 "text": "$x = new :a",
2198 .request(
2199 line=line(),
2200 comment="autocomplete after '$x = new :a'",
2201 method="textDocument/completion",
2202 params={
2203 "textDocument": {"uri": "${php_file_uri}"},
2204 "position": {"line": 3, "character": 11},
2206 result={
2207 "isIncomplete": False,
2208 "items": [
2210 "label": ":ab:cd:alpha",
2211 "kind": 7,
2212 "detail": "class",
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",
2221 "kind": 7,
2222 "detail": "class",
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
2234 .request(
2235 line=line(),
2236 comment="autocomplete resolving after '$x = new :a'",
2237 method="completionItem/resolve",
2238 params={
2239 "label": ":ab:cd:alpha",
2240 "kind": 7,
2241 "detail": "class",
2242 "inlineDetail": "class",
2243 "itemType": ":ab:cd:alpha",
2244 "insertText": ":ab:cd:alpha",
2245 "insertTextFormat": InsertTextFormat.PlainText.value,
2246 "data": {"fullname": ":ab:cd:alpha"},
2248 result={
2249 "label": ":ab:cd:alpha",
2250 "kind": 7,
2251 "detail": "class",
2252 "inlineDetail": "class",
2253 "itemType": ":ab:cd:alpha",
2254 "documentation": {
2255 "kind": "markdown",
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",
2264 .notification(
2265 comment="Add '$x = <ab:cd:text/>; $y = $x->'",
2266 method="textDocument/didChange",
2267 params={
2268 "textDocument": {"uri": "${php_file_uri}"},
2269 "contentChanges": [
2271 "range": {
2272 "start": {"line": 3, "character": 0},
2273 "end": {"line": 3, "character": 11},
2275 "text": "$x = <ab:cd:text/>; $y = $x->",
2280 .request(
2281 line=line(),
2282 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
2283 method="textDocument/completion",
2284 params={
2285 "textDocument": {"uri": "${php_file_uri}"},
2286 "position": {"line": 3, "character": 29},
2288 result={
2289 "isIncomplete": False,
2290 "items": [
2292 "label": ":width",
2293 "kind": 10,
2294 "detail": "?int",
2295 "inlineDetail": "?int",
2296 "sortText": ":width",
2297 "insertText": ":width",
2298 "insertTextFormat": InsertTextFormat.PlainText.value,
2299 "data": {
2300 "fullname": ":width",
2301 "filename": "${root_path}/completion_extras.php",
2302 "line": 5,
2303 "char": 27,
2304 "base_class": "\\:ab:cd:text",
2308 "label": ":color",
2309 "kind": 10,
2310 "detail": "?string",
2311 "inlineDetail": "?string",
2312 "sortText": ":color",
2313 "insertText": ":color",
2314 "insertTextFormat": InsertTextFormat.PlainText.value,
2315 "data": {
2316 "fullname": ":color",
2317 "filename": "${root_path}/completion_extras.php",
2318 "line": 5,
2319 "char": 13,
2320 "base_class": "\\:ab:cd:text",
2325 powered_by="serverless_ide",
2327 .notification(
2328 comment="Add '$x = <ab:cd:text/>; $y = $x->:'",
2329 method="textDocument/didChange",
2330 params={
2331 "textDocument": {"uri": "${php_file_uri}"},
2332 "contentChanges": [
2334 "range": {
2335 "start": {"line": 3, "character": 0},
2336 "end": {"line": 3, "character": 29},
2338 "text": "$x = <ab:cd:text/>; $y = $x->:",
2343 .request(
2344 line=line(),
2345 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
2346 method="textDocument/completion",
2347 params={
2348 "textDocument": {"uri": "${php_file_uri}"},
2349 "position": {"line": 3, "character": 30},
2351 result={
2352 "isIncomplete": False,
2353 "items": [
2355 "label": ":width",
2356 "kind": 10,
2357 "detail": "?int",
2358 "inlineDetail": "?int",
2359 "sortText": ":width",
2360 "insertText": ":width",
2361 "insertTextFormat": InsertTextFormat.PlainText.value,
2362 "data": {
2363 "fullname": ":width",
2364 "filename": "${root_path}/completion_extras.php",
2365 "line": 5,
2366 "char": 27,
2367 "base_class": "\\:ab:cd:text",
2371 "label": ":color",
2372 "kind": 10,
2373 "detail": "?string",
2374 "inlineDetail": "?string",
2375 "sortText": ":color",
2376 "insertText": ":color",
2377 "insertTextFormat": InsertTextFormat.PlainText.value,
2378 "data": {
2379 "fullname": ":color",
2380 "filename": "${root_path}/completion_extras.php",
2381 "line": 5,
2382 "char": 13,
2383 "base_class": "\\:ab:cd:text",
2388 powered_by="serverless_ide",
2390 .notification(
2391 comment="Add 'test_fun'",
2392 method="textDocument/didChange",
2393 params={
2394 "textDocument": {"uri": "${php_file_uri}"},
2395 "contentChanges": [
2397 "range": {
2398 "start": {"line": 3, "character": 0},
2399 "end": {"line": 3, "character": 30},
2401 "text": "test_fun",
2406 .request(
2407 line=line(),
2408 comment="autocomplete after 'test_fun'",
2409 method="textDocument/completion",
2410 params={
2411 "textDocument": {"uri": "${php_file_uri}"},
2412 "position": {"line": 3, "character": 8},
2414 result={
2415 "isIncomplete": False,
2416 "items": [
2418 "label": "test_function",
2419 "kind": 3,
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",
2431 .request(
2432 line=line(),
2433 comment="autocomplete resolving after 'test_fun'",
2434 method="completionItem/resolve",
2435 params={
2436 "label": "test_function",
2437 "kind": 3,
2438 "detail": "function(): void",
2439 "inlineDetail": "()",
2440 "itemType": "void",
2441 "insertText": "test_function",
2442 "insertTextFormat": InsertTextFormat.PlainText.value,
2443 "data": {
2444 "filename": "${root_path}/completion.php",
2445 "line": 8,
2446 "char": 10,
2449 result={
2450 "label": "test_function",
2451 "kind": 3,
2452 "detail": "function(): void",
2453 "inlineDetail": "()",
2454 "itemType": "void",
2455 "documentation": {
2456 "kind": "markdown",
2457 "value": "test_function docblock.",
2459 "insertText": "test_function",
2460 "insertTextFormat": InsertTextFormat.PlainText.value,
2461 "data": {
2462 "filename": "${root_path}/completion.php",
2463 "line": 8,
2464 "char": 10,
2467 powered_by="serverless_ide",
2469 .notification(
2470 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
2471 method="textDocument/didChange",
2472 params={
2473 "textDocument": {"uri": "${php_file_uri}"},
2474 "contentChanges": [
2476 "range": {
2477 "start": {"line": 3, "character": 0},
2478 "end": {"line": 3, "character": 8},
2480 "text": "switch (Elsa::Alonso) { case Elsa:",
2485 .request(
2486 line=line(),
2487 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
2488 method="textDocument/completion",
2489 params={
2490 "textDocument": {"uri": "${php_file_uri}"},
2491 "position": {"line": 3, "character": 34},
2493 result={"isIncomplete": False, "items": []},
2494 powered_by="serverless_ide",
2496 .notification(
2497 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
2498 method="textDocument/didChange",
2499 params={
2500 "textDocument": {"uri": "${php_file_uri}"},
2501 "contentChanges": [
2503 "range": {
2504 "start": {"line": 3, "character": 0},
2505 "end": {"line": 3, "character": 34},
2507 "text": "switch (Elsa::Alonso) { case Elsa::",
2512 .request(
2513 line=line(),
2514 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
2515 method="textDocument/completion",
2516 params={
2517 "textDocument": {"uri": "${php_file_uri}"},
2518 "position": {"line": 3, "character": 35},
2520 result={
2521 "isIncomplete": False,
2522 "items": [
2524 "label": "class",
2525 "kind": 21,
2526 "detail": "classname<this>",
2527 "inlineDetail": "classname<this>",
2528 "sortText": "class",
2529 "insertText": "class",
2530 "insertTextFormat": InsertTextFormat.PlainText.value,
2531 "data": {
2532 "fullname": "class",
2533 "filename": "${root_path}/completion_extras.php",
2534 "line": 13,
2535 "char": 6,
2536 "base_class": "\\Elsa",
2540 "label": "Bard",
2541 "kind": 21,
2542 "detail": "Elsa",
2543 "inlineDetail": "Elsa",
2544 "sortText": "Bard",
2545 "insertText": "Bard",
2546 "insertTextFormat": InsertTextFormat.PlainText.value,
2547 "data": {
2548 "fullname": "Bard",
2549 "filename": "${root_path}/completion_extras.php",
2550 "line": 13,
2551 "char": 12,
2552 "base_class": "\\Elsa",
2556 "label": "Alonso",
2557 "kind": 21,
2558 "detail": "Elsa",
2559 "inlineDetail": "Elsa",
2560 "sortText": "Alonso",
2561 "insertText": "Alonso",
2562 "insertTextFormat": InsertTextFormat.PlainText.value,
2563 "data": {
2564 "fullname": "Alonso",
2565 "filename": "${root_path}/completion_extras.php",
2566 "line": 13,
2567 "char": 12,
2568 "base_class": "\\Elsa",
2572 "label": "isValid",
2573 "kind": 2,
2574 "detail": "function(mixed $value): bool",
2575 "inlineDetail": "(mixed $value)",
2576 "itemType": "bool",
2577 "sortText": "isValid",
2578 "insertText": "isValid(${1:\\$value})",
2579 "insertTextFormat": InsertTextFormat.Snippet.value,
2580 "data": {
2581 "fullname": "isValid",
2582 "filename": "${hhi_path}/BuiltinEnum.hhi",
2583 "line": 49,
2584 "char": 32,
2585 "base_class": "\\Elsa",
2589 "label": "getValues",
2590 "kind": 2,
2591 "detail": "function(): darray<string, Elsa>",
2592 "inlineDetail": "()",
2593 "itemType": "darray<string, Elsa>",
2594 "sortText": "getValues",
2595 "insertText": "getValues()",
2596 "insertTextFormat": InsertTextFormat.Snippet.value,
2597 "data": {
2598 "fullname": "getValues",
2599 "filename": "${hhi_path}/BuiltinEnum.hhi",
2600 "line": 34,
2601 "char": 32,
2602 "base_class": "\\Elsa",
2606 "label": "getNames",
2607 "kind": 2,
2608 "detail": "function(): darray<Elsa, string>",
2609 "inlineDetail": "()",
2610 "itemType": "darray<Elsa, string>",
2611 "sortText": "getNames",
2612 "insertText": "getNames()",
2613 "insertTextFormat": InsertTextFormat.Snippet.value,
2614 "data": {
2615 "fullname": "getNames",
2616 "filename": "${hhi_path}/BuiltinEnum.hhi",
2617 "line": 43,
2618 "char": 32,
2619 "base_class": "\\Elsa",
2623 "label": "coerce",
2624 "kind": 2,
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,
2631 "data": {
2632 "fullname": "coerce",
2633 "filename": "${hhi_path}/BuiltinEnum.hhi",
2634 "line": 56,
2635 "char": 32,
2636 "base_class": "\\Elsa",
2640 "label": "assertAll",
2641 "kind": 2,
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,
2648 "data": {
2649 "fullname": "assertAll",
2650 "filename": "${hhi_path}/BuiltinEnum.hhi",
2651 "line": 70,
2652 "char": 32,
2653 "base_class": "\\Elsa",
2657 "label": "assert",
2658 "kind": 2,
2659 "detail": "function(mixed $value): Elsa",
2660 "inlineDetail": "(mixed $value)",
2661 "itemType": "Elsa",
2662 "sortText": "assert",
2663 "insertText": "assert(${1:\\$value})",
2664 "insertTextFormat": InsertTextFormat.Snippet.value,
2665 "data": {
2666 "fullname": "assert",
2667 "filename": "${hhi_path}/BuiltinEnum.hhi",
2668 "line": 63,
2669 "char": 32,
2670 "base_class": "\\Elsa",
2675 powered_by="serverless_ide",
2677 .notification(
2678 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
2679 method="textDocument/didChange",
2680 params={
2681 "textDocument": {"uri": "${php_file_uri}"},
2682 "contentChanges": [
2684 "range": {
2685 "start": {"line": 3, "character": 0},
2686 "end": {"line": 3, "character": 35},
2688 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
2693 .request(
2694 line=line(),
2695 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
2696 method="textDocument/completion",
2697 params={
2698 "textDocument": {"uri": "${php_file_uri}"},
2699 "position": {"line": 3, "character": 42},
2701 result={"isIncomplete": False, "items": []},
2702 powered_by="serverless_ide",
2704 .notification(
2705 comment="Add 'DeprecatedClass::'",
2706 method="textDocument/didChange",
2707 params={
2708 "textDocument": {"uri": "${php_file_uri}"},
2709 "contentChanges": [
2711 "range": {
2712 "start": {"line": 3, "character": 0},
2713 "end": {"line": 3, "character": 41},
2715 "text": "DeprecatedClass::",
2720 .request(
2721 line=line(),
2722 comment="autocomplete after 'DeprecatedClass::'",
2723 method="textDocument/completion",
2724 params={
2725 "textDocument": {"uri": "${php_file_uri}"},
2726 "position": {"line": 3, "character": 17},
2728 result={
2729 "isIncomplete": False,
2730 "items": [
2732 "label": "class",
2733 "kind": 21,
2734 "detail": "classname<this>",
2735 "inlineDetail": "classname<this>",
2736 "sortText": "class",
2737 "insertText": "class",
2738 "insertTextFormat": InsertTextFormat.PlainText.value,
2739 "data": {
2740 "fullname": "class",
2741 "filename": "${root_path}/completion_extras.php",
2742 "line": 18,
2743 "char": 13,
2744 "base_class": "\\DeprecatedClass",
2748 "label": "test_do_not_use",
2749 "kind": 2,
2750 "detail": "function(): void",
2751 "inlineDetail": "()",
2752 "itemType": "void",
2753 "sortText": "~test_do_not_use",
2754 "insertText": "test_do_not_use()",
2755 "insertTextFormat": InsertTextFormat.Snippet.value,
2756 "data": {
2757 "fullname": "test_do_not_use",
2758 "filename": "${root_path}/completion_extras.php",
2759 "line": 22,
2760 "char": 26,
2761 "base_class": "\\DeprecatedClass",
2765 "label": "getName",
2766 "kind": 2,
2767 "detail": "function(): void",
2768 "inlineDetail": "()",
2769 "itemType": "void",
2770 "sortText": "getName",
2771 "insertText": "getName()",
2772 "insertTextFormat": InsertTextFormat.Snippet.value,
2773 "data": {
2774 "fullname": "getName",
2775 "filename": "${root_path}/completion_extras.php",
2776 "line": 19,
2777 "char": 26,
2778 "base_class": "\\DeprecatedClass",
2782 "label": "getAttributes_DO_NOT_USE",
2783 "kind": 2,
2784 "detail": "function(): void",
2785 "inlineDetail": "()",
2786 "itemType": "void",
2787 "sortText": "~getAttributes_DO_NOT_USE",
2788 "insertText": "getAttributes_DO_NOT_USE()",
2789 "insertTextFormat": InsertTextFormat.Snippet.value,
2790 "data": {
2791 "fullname": "getAttributes_DO_NOT_USE",
2792 "filename": "${root_path}/completion_extras.php",
2793 "line": 21,
2794 "char": 26,
2795 "base_class": "\\DeprecatedClass",
2799 "label": "__getLoader",
2800 "kind": 2,
2801 "detail": "function(): void",
2802 "inlineDetail": "()",
2803 "itemType": "void",
2804 "sortText": "~__getLoader",
2805 "insertText": "__getLoader()",
2806 "insertTextFormat": InsertTextFormat.Snippet.value,
2807 "data": {
2808 "fullname": "__getLoader",
2809 "filename": "${root_path}/completion_extras.php",
2810 "line": 20,
2811 "char": 26,
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()
2829 spec = (
2830 self.initialize_spec(
2831 LspTestSpec("serverless_ide_definition"), use_serverless_ide=True
2833 .notification(
2834 method="textDocument/didOpen",
2835 params={
2836 "textDocument": {
2837 "uri": "${php_file_uri}",
2838 "languageId": "hack",
2839 "version": 1,
2840 "text": "${php_file}",
2844 .request(
2845 line=line(),
2846 comment="call to `b_definition`",
2847 method="textDocument/definition",
2848 params={
2849 "textDocument": {"uri": "${php_file_uri}"},
2850 "position": {"line": 3, "character": 10},
2852 result=[
2854 "uri": "file://${root_path}/definition.php",
2855 "range": {
2856 "start": {"line": 6, "character": 9},
2857 "end": {"line": 6, "character": 21},
2859 "title": "b_definition",
2862 powered_by="serverless_ide",
2864 .request(
2865 line=line(),
2866 comment="call to `new BB(1)`",
2867 method="textDocument/definition",
2868 params={
2869 "textDocument": {"uri": "${php_file_uri}"},
2870 "position": {"line": 29, "character": 13},
2872 result=[
2874 "uri": "file://${root_path}/definition.php",
2875 "range": {
2876 "start": {"line": 11, "character": 18},
2877 "end": {"line": 11, "character": 29},
2879 "title": "BB::__construct",
2882 powered_by="serverless_ide",
2884 .request(
2885 line=line(),
2886 comment="call to `new CC(1)`",
2887 method="textDocument/definition",
2888 params={
2889 "textDocument": {"uri": "${php_file_uri}"},
2890 "position": {"line": 30, "character": 13},
2892 result=[
2894 "uri": "file://${root_path}/definition.php",
2895 "range": {
2896 "start": {"line": 14, "character": 6},
2897 "end": {"line": 14, "character": 8},
2899 "title": "CC",
2902 "uri": "file://${root_path}/definition.php",
2903 "range": {
2904 "start": {"line": 11, "character": 18},
2905 "end": {"line": 11, "character": 29},
2907 "title": "BB::__construct",
2910 powered_by="serverless_ide",
2912 .request(
2913 line=line(),
2914 comment="call to `new DD(1)`",
2915 method="textDocument/definition",
2916 params={
2917 "textDocument": {"uri": "${php_file_uri}"},
2918 "position": {"line": 31, "character": 13},
2920 result=[
2922 "uri": "file://${root_path}/definition.php",
2923 "range": {
2924 "start": {"line": 17, "character": 6},
2925 "end": {"line": 17, "character": 8},
2927 "title": "DD",
2930 "uri": "file://${root_path}/definition.php",
2931 "range": {
2932 "start": {"line": 11, "character": 18},
2933 "end": {"line": 11, "character": 29},
2935 "title": "BB::__construct",
2938 powered_by="serverless_ide",
2940 .request(
2941 line=line(),
2942 comment="call to `new EE(1)`",
2943 method="textDocument/definition",
2944 params={
2945 "textDocument": {"uri": "${php_file_uri}"},
2946 "position": {"line": 32, "character": 13},
2948 result=[
2950 "uri": "file://${root_path}/definition.php",
2951 "range": {
2952 "start": {"line": 21, "character": 18},
2953 "end": {"line": 21, "character": 29},
2955 "title": "EE::__construct",
2958 powered_by="serverless_ide",
2960 .request(
2961 line=line(),
2962 comment="call to `new FF(1)`",
2963 method="textDocument/definition",
2964 params={
2965 "textDocument": {"uri": "${php_file_uri}"},
2966 "position": {"line": 33, "character": 13},
2968 result=[
2970 "uri": "file://${root_path}/definition.php",
2971 "range": {
2972 "start": {"line": 26, "character": 6},
2973 "end": {"line": 26, "character": 8},
2975 "title": "FF",
2978 powered_by="serverless_ide",
2980 .request(
2981 line=line(),
2982 comment="call to `new TakesString(HasString::MyString)`",
2983 method="textDocument/definition",
2984 params={
2985 "textDocument": {"uri": "${php_file_uri}"},
2986 "position": {"line": 45, "character": 23},
2988 result=[
2990 "uri": "file://${root_path}/definition.php",
2991 "range": {
2992 "start": {"line": 40, "character": 6},
2993 "end": {"line": 40, "character": 15},
2995 "title": "HasString",
2998 powered_by="serverless_ide",
3000 .notification(
3001 comment="make local, unsaved change to the file",
3002 method="textDocument/didChange",
3003 params={
3004 "textDocument": {"uri": "${php_file_uri}", "version": 2},
3005 "contentChanges": [
3007 "text": "test",
3008 "range": {
3009 "start": {"line": 3, "character": 9},
3010 "end": {"line": 3, "character": 21},
3016 .request(
3017 line=line(),
3018 comment="call to `test` instead of `b_definition`",
3019 method="textDocument/definition",
3020 params={
3021 "textDocument": {"uri": "${php_file_uri}"},
3022 "position": {"line": 3, "character": 10},
3024 result=[
3026 "uri": "file://${root_path}/definition.php",
3027 "range": {
3028 "start": {"line": 28, "character": 9},
3029 "end": {"line": 28, "character": 13},
3031 "title": "test",
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()
3046 spec = (
3047 self.initialize_spec(
3048 LspTestSpec("serverless_ide_overridden_definition"),
3049 use_serverless_ide=True,
3051 .notification(
3052 method="textDocument/didOpen",
3053 params={
3054 "textDocument": {
3055 "uri": "${php_file_uri}",
3056 "languageId": "hack",
3057 "version": 1,
3058 "text": "${php_file}",
3062 .request(
3063 line=line(),
3064 comment="find overridden method from trait. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3065 method="textDocument/definition",
3066 params={
3067 "textDocument": {"uri": "${php_file_uri}"},
3068 "position": {"line": 13, "character": 5},
3070 result=[
3072 "uri": "file://${root_path}/override.php",
3073 "range": {
3074 "start": {"line": 7, "character": 18},
3075 "end": {"line": 7, "character": 21},
3077 "title": "MyTrait::foo",
3080 powered_by="serverless_ide",
3082 .request(
3083 line=line(),
3084 comment="find overridden static method. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3085 method="textDocument/definition",
3086 params={
3087 "textDocument": {"uri": "${php_file_uri}"},
3088 "position": {"line": 26, "character": 5},
3090 result=[
3092 "uri": "file://${root_path}/override.php",
3093 "range": {
3094 "start": {"line": 23, "character": 25},
3095 "end": {"line": 23, "character": 28},
3097 "title": "C2::bar",
3100 powered_by="serverless_ide",
3102 .request(
3103 line=line(),
3104 comment="find overridden interface method",
3105 method="textDocument/definition",
3106 params={
3107 "textDocument": {"uri": "${php_file_uri}"},
3108 "position": {"line": 35, "character": 5},
3110 result=[
3112 "uri": "file://${root_path}/override.php",
3113 "range": {
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()
3132 spec = (
3133 self.initialize_spec(
3134 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide=True
3136 .notification(
3137 method="textDocument/didOpen",
3138 params={
3139 "textDocument": {
3140 "uri": "${php_file_uri}",
3141 "languageId": "hack",
3142 "version": 1,
3143 "text": "${php_file}",
3147 .request(
3148 line=line(),
3149 comment="documentSymbol call",
3150 method="textDocument/documentSymbol",
3151 params={"textDocument": {"uri": "${php_file_uri}"}},
3152 result=[
3154 "name": "testClassMemberInsideConstructorInvocation",
3155 "kind": 12,
3156 "location": {
3157 "uri": "file://${root_path}/definition.php",
3158 "range": {
3159 "start": {"line": 44, "character": 0},
3160 "end": {"line": 46, "character": 1},
3165 "name": "MyString",
3166 "kind": 14,
3167 "location": {
3168 "uri": "file://${root_path}/definition.php",
3169 "range": {
3170 "start": {"line": 41, "character": 8},
3171 "end": {"line": 41, "character": 29},
3174 "containerName": "HasString",
3177 "name": "HasString",
3178 "kind": 5,
3179 "location": {
3180 "uri": "file://${root_path}/definition.php",
3181 "range": {
3182 "start": {"line": 40, "character": 0},
3183 "end": {"line": 42, "character": 1},
3188 "name": "__construct",
3189 "kind": 6,
3190 "location": {
3191 "uri": "file://${root_path}/definition.php",
3192 "range": {
3193 "start": {"line": 37, "character": 2},
3194 "end": {"line": 37, "character": 43},
3197 "containerName": "TakesString",
3200 "name": "TakesString",
3201 "kind": 5,
3202 "location": {
3203 "uri": "file://${root_path}/definition.php",
3204 "range": {
3205 "start": {"line": 36, "character": 0},
3206 "end": {"line": 38, "character": 1},
3211 "name": "FF",
3212 "kind": 5,
3213 "location": {
3214 "uri": "file://${root_path}/definition.php",
3215 "range": {
3216 "start": {"line": 26, "character": 0},
3217 "end": {"line": 26, "character": 11},
3222 "name": "__construct",
3223 "kind": 6,
3224 "location": {
3225 "uri": "file://${root_path}/definition.php",
3226 "range": {
3227 "start": {"line": 21, "character": 2},
3228 "end": {"line": 23, "character": 3},
3231 "containerName": "EE",
3234 "name": "EE",
3235 "kind": 5,
3236 "location": {
3237 "uri": "file://${root_path}/definition.php",
3238 "range": {
3239 "start": {"line": 20, "character": 0},
3240 "end": {"line": 24, "character": 1},
3245 "name": "CC",
3246 "kind": 5,
3247 "location": {
3248 "uri": "file://${root_path}/definition.php",
3249 "range": {
3250 "start": {"line": 14, "character": 0},
3251 "end": {"line": 15, "character": 1},
3256 "name": "__construct",
3257 "kind": 6,
3258 "location": {
3259 "uri": "file://${root_path}/definition.php",
3260 "range": {
3261 "start": {"line": 11, "character": 2},
3262 "end": {"line": 11, "character": 40},
3265 "containerName": "BB",
3268 "name": "BB",
3269 "kind": 5,
3270 "location": {
3271 "uri": "file://${root_path}/definition.php",
3272 "range": {
3273 "start": {"line": 10, "character": 0},
3274 "end": {"line": 12, "character": 1},
3279 "name": "a_definition",
3280 "kind": 12,
3281 "location": {
3282 "uri": "file://${root_path}/definition.php",
3283 "range": {
3284 "start": {"line": 2, "character": 0},
3285 "end": {"line": 4, "character": 1},
3290 "name": "b_definition",
3291 "kind": 12,
3292 "location": {
3293 "uri": "file://${root_path}/definition.php",
3294 "range": {
3295 "start": {"line": 6, "character": 0},
3296 "end": {"line": 8, "character": 1},
3301 "name": "DD",
3302 "kind": 5,
3303 "location": {
3304 "uri": "file://${root_path}/definition.php",
3305 "range": {
3306 "start": {"line": 17, "character": 0},
3307 "end": {"line": 18, "character": 1},
3312 "name": "test",
3313 "kind": 12,
3314 "location": {
3315 "uri": "file://${root_path}/definition.php",
3316 "range": {
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(
3331 self,
3332 spec: LspTestSpec,
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?
3336 ) -> LspTestSpec:
3337 if use_serverless_ide:
3338 initialization_options = {
3339 "namingTableSavedStatePath": "${naming_table_saved_state_path}",
3340 "namingTableSavedStateTestDelay": 0.0,
3342 if supports_init:
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
3348 else:
3349 initialization_options = {}
3351 window_capabilities = {}
3352 if supports_status:
3353 window_capabilities["status"] = {"dynamicRegistration": False}
3355 spec = spec.ignore_notifications(method="telemetry/event").request(
3356 line=line(),
3357 method="initialize",
3358 params={
3359 "initializationOptions": initialization_options,
3360 "processId": None,
3361 "rootPath": "${root_path}",
3362 "capabilities": {
3363 "window": window_capabilities,
3364 "textDocument": {
3365 "completion": {"completionItem": {"snippetSupport": True}}
3369 result={
3370 "capabilities": {
3371 "textDocumentSync": {
3372 "openClose": True,
3373 "change": 2,
3374 "willSave": False,
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",
3407 params={
3408 "registrations": [
3410 "id": "did-change-watched-files",
3411 "method": "workspace/didChangeWatchedFiles",
3412 "registerOptions": {
3413 "watchers": [{"globPattern": "**", "kind": 7}]
3418 result=None,
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"},
3430 return spec
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()
3437 spec = (
3438 self.initialize_spec(
3439 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide=True
3441 .notification(
3442 method="textDocument/didOpen",
3443 params={
3444 "textDocument": {
3445 "uri": "${php_file_uri}",
3446 "languageId": "hack",
3447 "version": 1,
3448 "text": "${php_file}",
3452 .request(
3453 line=line(),
3454 comment="Conditional Type Definition of HH or II",
3455 method="textDocument/typeDefinition",
3456 params={
3457 "textDocument": {"uri": "${php_file_uri}"},
3458 "position": {"line": 32, "character": 2},
3460 result=[
3462 "uri": "${php_file_uri}",
3463 "range": {
3464 "start": {"line": 2, "character": 6},
3465 "end": {"line": 2, "character": 8},
3467 "title": "\\HH",
3470 "uri": "${php_file_uri}",
3471 "range": {
3472 "start": {"line": 12, "character": 6},
3473 "end": {"line": 12, "character": 8},
3475 "title": "\\LL",
3478 powered_by="serverless_ide",
3480 .request(
3481 line=line(),
3482 comment="Standard Class Definition",
3483 method="textDocument/typeDefinition",
3484 params={
3485 "textDocument": {"uri": "${php_file_uri}"},
3486 "position": {"line": 40, "character": 2},
3488 result=[
3490 "uri": "${php_file_uri}",
3491 "range": {
3492 "start": {"line": 2, "character": 6},
3493 "end": {"line": 2, "character": 8},
3495 "title": "\\HH",
3498 powered_by="serverless_ide",
3500 .request(
3501 line=line(),
3502 comment="Class Type Definition with Casting",
3503 method="textDocument/typeDefinition",
3504 params={
3505 "textDocument": {"uri": "${php_file_uri}"},
3506 "position": {"line": 41, "character": 2},
3508 result=[
3510 "uri": "${php_file_uri}",
3511 "range": {
3512 "start": {"line": 2, "character": 6},
3513 "end": {"line": 2, "character": 8},
3515 "title": "\\HH",
3518 powered_by="serverless_ide",
3520 .request(
3521 line=line(),
3522 comment="Primitive Type Definition",
3523 method="textDocument/typeDefinition",
3524 params={
3525 "textDocument": {"uri": "${php_file_uri}"},
3526 "position": {"line": 42, "character": 2},
3528 result=[],
3529 powered_by="serverless_ide",
3531 .request(
3532 line=line(),
3533 comment="Function Return Type Definition",
3534 method="textDocument/typeDefinition",
3535 params={
3536 "textDocument": {"uri": "${php_file_uri}"},
3537 "position": {"line": 43, "character": 2},
3539 result=[
3541 "uri": "${php_file_uri}",
3542 "range": {
3543 "start": {"line": 12, "character": 6},
3544 "end": {"line": 12, "character": 8},
3546 "title": "\\LL",
3549 powered_by="serverless_ide",
3551 .request(
3552 line=line(),
3553 comment="Function definition with primitive return type",
3554 method="textDocument/typeDefinition",
3555 params={
3556 "textDocument": {"uri": "${php_file_uri}"},
3557 "position": {"line": 44, "character": 2},
3559 result=[
3561 "uri": "${php_file_uri}",
3562 "range": {
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()
3581 spec = (
3582 self.initialize_spec(
3583 LspTestSpec("serverless_ide_hover"), use_serverless_ide=True
3585 .notification(
3586 method="textDocument/didOpen",
3587 params={
3588 "textDocument": {
3589 "uri": "${php_file_uri}",
3590 "languageId": "hack",
3591 "version": 1,
3592 "text": "${php_file}",
3596 .request(
3597 line=line(),
3598 comment="hover over function invocation",
3599 method="textDocument/hover",
3600 params={
3601 "textDocument": {"uri": "${php_file_uri}"},
3602 "position": {"line": 3, "character": 16},
3604 result={
3605 "contents": [
3606 {"language": "hack", "value": "int"},
3607 "A comment describing b_hover.",
3609 "range": {
3610 "start": {"line": 3, "character": 9},
3611 "end": {"line": 3, "character": 16},
3614 powered_by="serverless_ide",
3616 .request(
3617 line=line(),
3618 comment="hover over string literal outside call",
3619 method="textDocument/hover",
3620 params={
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",
3627 .request(
3628 line=line(),
3629 comment="hover over string literal inside call",
3630 method="textDocument/hover",
3631 params={
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",
3638 .request(
3639 line=line(),
3640 comment="hover over int literal inside call",
3641 method="textDocument/hover",
3642 params={
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",
3649 .request(
3650 line=line(),
3651 comment="hover over constant reference",
3652 method="textDocument/hover",
3653 params={
3654 "textDocument": {"uri": "${php_file_uri}"},
3655 "position": {"line": 15, "character": 19},
3657 result={
3658 "contents": [
3659 {"language": "hack", "value": "THE_ANSWER"},
3660 "A comment describing THE_ANSWER",
3661 "int THE_ANSWER = 42",
3663 "range": {
3664 "start": {"line": 15, "character": 9},
3665 "end": {"line": 15, "character": 19},
3668 powered_by="serverless_ide",
3670 .request(
3671 line=line(),
3672 comment="hover over whitespace",
3673 method="textDocument/hover",
3674 params={
3675 "textDocument": {"uri": "${php_file_uri}"},
3676 "position": {"line": 3, "character": 1},
3678 result=None,
3679 powered_by="serverless_ide",
3681 .request(
3682 line=line(),
3683 comment="hover over a keyword",
3684 method="textDocument/hover",
3685 params={
3686 "textDocument": {"uri": "${php_file_uri}"},
3687 "position": {"line": 2, "character": 1},
3689 result=None,
3690 powered_by="serverless_ide",
3692 .request(
3693 line=line(),
3694 comment="hover over a comment",
3695 method="textDocument/hover",
3696 params={
3697 "textDocument": {"uri": "${php_file_uri}"},
3698 "position": {"line": 1, "character": 4},
3700 result=None,
3701 powered_by="serverless_ide",
3703 .request(
3704 line=line(),
3705 comment="hover past the end of a line",
3706 method="textDocument/hover",
3707 params={
3708 "textDocument": {"uri": "${php_file_uri}"},
3709 "position": {"line": 3, "character": 100},
3711 result=None,
3712 powered_by="serverless_ide",
3714 .request(
3715 line=line(),
3716 comment="hover past the end of a file",
3717 method="textDocument/hover",
3718 params={
3719 "textDocument": {"uri": "${php_file_uri}"},
3720 "position": {"line": 300, "character": 0},
3722 result=None,
3723 powered_by="serverless_ide",
3725 .request(
3726 line=line(),
3727 comment="hover over class with copyright docblock",
3728 method="textDocument/hover",
3729 params={
3730 "textDocument": {"uri": "${php_file_uri}"},
3731 "position": {"line": 37, "character": 15},
3733 result={
3734 "contents": [
3735 {"language": "hack", "value": "final class CopyrightClass"},
3736 "Testing copyright removal",
3738 "range": {
3739 "start": {"line": 37, "character": 2},
3740 "end": {"line": 37, "character": 16},
3743 powered_by="serverless_ide",
3745 .request(
3746 line=line(),
3747 comment="hover over class with generated docblock",
3748 method="textDocument/hover",
3749 params={
3750 "textDocument": {"uri": "${php_file_uri}"},
3751 "position": {"line": 58, "character": 15},
3753 result={
3754 "contents": [
3755 {"language": "hack", "value": "final class GeneratedClass"},
3756 "Testing generated text removal",
3758 "range": {
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()
3775 spec = (
3776 self.initialize_spec(
3777 LspTestSpec("serverless_ide_file_on_disk_change"),
3778 use_serverless_ide=True,
3780 .notification(
3781 method="textDocument/didOpen",
3782 params={
3783 "textDocument": {
3784 "uri": "${php_file_uri}",
3785 "languageId": "hack",
3786 "version": 1,
3787 "text": "${php_file}",
3791 .notification(
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",
3798 params={
3799 "type": 4,
3800 "message": "[client-ide] Done processing file changes",
3803 .request(
3804 line=line(),
3805 method="textDocument/hover",
3806 params={
3807 "textDocument": {"uri": "${php_file_uri}"},
3808 "position": {"line": 3, "character": 16},
3810 result={
3811 "contents": [
3812 {"language": "hack", "value": "int"},
3813 "A comment describing b_hover.",
3815 "range": {
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()
3832 spec = (
3833 self.initialize_spec(
3834 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide=True
3836 .notification(
3837 method="textDocument/didOpen",
3838 params={
3839 "textDocument": {
3840 "uri": "${php_file_uri}",
3841 "languageId": "hack",
3842 "version": 1,
3843 "text": "${php_file}",
3847 .notification(
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",
3854 params={
3855 "type": 4,
3856 "message": "[client-ide] Done processing file changes",
3859 .request(
3860 line=line(),
3861 comment="Totally normal hover",
3862 method="textDocument/hover",
3863 params={
3864 "textDocument": {"uri": "${php_file_uri}"},
3865 "position": {"line": 14, "character": 37},
3867 result={
3868 "contents": [
3870 "language": "hack",
3871 "value": "public static function staticMethod(string $z): void",
3873 'During testing, we\'ll remove the "public" tag from this '
3874 "method\n"
3875 "to ensure that we can still get IDE services",
3876 "Return type: `void`",
3877 "Full name: `HoverWithErrorsClass::staticMethod`",
3879 "range": {
3880 "end": {"character": 39, "line": 14},
3881 "start": {"character": 27, "line": 14},
3884 powered_by="serverless_ide",
3886 .notification(
3887 comment="Remove the 'public' visibility modifier which triggers AST->AAST errors",
3888 method="textDocument/didChange",
3889 params={
3890 "textDocument": {"uri": "${php_file_uri}"},
3891 "contentChanges": [
3893 "range": {
3894 "start": {"line": 10, "character": 2},
3895 "end": {"line": 10, "character": 8},
3897 "text": "",
3902 .request(
3903 line=line(),
3904 comment="Hover should still work even if visibility modifier has been removed",
3905 method="textDocument/hover",
3906 params={
3907 "textDocument": {"uri": "${php_file_uri}"},
3908 "position": {"line": 14, "character": 37},
3910 result={
3911 "contents": [
3913 "language": "hack",
3914 "value": "public static function staticMethod(string $z): void",
3916 'During testing, we\'ll remove the "public" tag from this '
3917 "method\n"
3918 "to ensure that we can still get IDE services",
3919 "Return type: `void`",
3920 "Full name: `HoverWithErrorsClass::staticMethod`",
3922 "range": {
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()
3944 spec = (
3945 self.initialize_spec(LspTestSpec("formatting"), use_serverless_ide=True)
3946 .notification(
3947 method="textDocument/didOpen",
3948 params={
3949 "textDocument": {
3950 "uri": "${php_file_uri}",
3951 "languageId": "hack",
3952 "version": 1,
3953 "text": "${php_file}",
3957 .request(
3958 line=line(),
3959 method="textDocument/formatting",
3960 params={
3961 "textDocument": {"uri": "${php_file_uri}"},
3962 "options": {"tabSize": 5, "insertSpaces": True},
3964 result=[
3966 "range": {
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"
3974 + ' $b = "is";\n\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()
3997 spec = (
3998 self.initialize_spec(
3999 LspTestSpec("range_formatting"), use_serverless_ide=True
4001 .notification(
4002 method="textDocument/didOpen",
4003 params={
4004 "textDocument": {
4005 "uri": "${php_file_uri}",
4006 "languageId": "hack",
4007 "version": 1,
4008 "text": "${php_file}",
4012 .request(
4013 line=line(),
4014 method="textDocument/rangeFormatting",
4015 params={
4016 "textDocument": {"uri": "${php_file_uri}"},
4017 "range": {
4018 "start": {"line": 4, "character": 0},
4019 "end": {"line": 5, "character": 0},
4021 "options": {"tabSize": 5, "insertSpaces": True},
4023 result=[
4025 "range": {
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"))
4046 spec = (
4047 self.initialize_spec(
4048 LspTestSpec("ontypeformatting"), use_serverless_ide=True
4050 .notification(
4051 method="textDocument/didOpen",
4052 params={
4053 "textDocument": {
4054 "uri": "${php_file_uri}",
4055 "languageId": "hack",
4056 "version": 1,
4057 "text": "${php_file}",
4061 .request(
4062 line=line(),
4063 method="textDocument/onTypeFormatting",
4064 params={
4065 "textDocument": {"uri": "${php_file_uri}"},
4066 "position": {"line": 9, "character": 58},
4067 "ch": ";",
4068 "options": {"tabSize": 2, "insertSpaces": True},
4070 result=[
4072 "range": {
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 );",
4086 .request(
4087 line=line(),
4088 method="textDocument/onTypeFormatting",
4089 params={
4090 "textDocument": {"uri": "${php_file_uri}"},
4091 "position": {"line": 13, "character": 1},
4092 "ch": "}",
4093 "options": {"tabSize": 2, "insertSpaces": True},
4095 result=[
4097 "range": {
4098 "start": {"line": 13, "character": 0},
4099 "end": {"line": 13, "character": 1},
4101 "newText": "{",
4105 .request(
4106 line=line(),
4107 method="textDocument/onTypeFormatting",
4108 params={
4109 "textDocument": {"uri": "${php_file_uri}"},
4110 "position": {"line": 15, "character": 16},
4111 "ch": "}",
4112 "options": {"tabSize": 2, "insertSpaces": True},
4114 result=[
4116 "range": {
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")
4133 spec = (
4134 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
4135 .wait_for_hh_server_ready()
4136 .notification(
4137 method="textDocument/didOpen",
4138 params={
4139 "textDocument": {
4140 "uri": "${php_file_uri}",
4141 "languageId": "hack",
4142 "version": 1,
4143 "text": "${php_file}",
4147 .notification(
4148 method="textDocument/didChange",
4149 params={
4150 "textDocument": {"uri": "${php_file_uri}"},
4151 "contentChanges": [
4153 "range": {
4154 "start": {"line": 7, "character": 11},
4155 "end": {"line": 7, "character": 12},
4157 "text": "a",
4162 .wait_for_notification(
4163 method="textDocument/publishDiagnostics",
4164 params={
4165 "uri": "${php_file_uri}",
4166 "diagnostics": [
4168 "range": {
4169 "start": {"line": 7, "character": 11},
4170 "end": {"line": 7, "character": 11},
4172 "severity": 1,
4173 "code": 1002,
4174 "source": "Hack",
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")
4195 spec = (
4196 self.initialize_spec(
4197 LspTestSpec("test_go_to_implementation"), use_serverless_ide=False
4199 .wait_for_hh_server_ready()
4200 .notification(
4201 method="textDocument/didOpen",
4202 params={
4203 "textDocument": {
4204 "uri": "${php_file_uri}",
4205 "languageId": "hack",
4206 "version": 1,
4207 "text": "${php_file}",
4211 .request(
4212 line=line(),
4213 comment="go to implemenetation: abstract class",
4214 method="textDocument/implementation",
4215 params={
4216 "textDocument": {"uri": "${php_file_uri}"},
4217 "position": {"line": 1, "character": 17},
4219 result=[
4221 "uri": "${php_file_uri}",
4222 "range": {
4223 "start": {"line": 7, "character": 6},
4224 "end": {"line": 7, "character": 9},
4229 .request(
4230 line=line(),
4231 comment="go to implemenetation: interface",
4232 method="textDocument/implementation",
4233 params={
4234 "textDocument": {"uri": "${php_file_uri}"},
4235 "position": {"line": 13, "character": 13},
4237 result=[
4239 "uri": "${php_file_uri}",
4240 "range": {
4241 "start": {"line": 17, "character": 6},
4242 "end": {"line": 17, "character": 9},
4247 .request(
4248 line=line(),
4249 comment="go to implemenetation: trait",
4250 method="textDocument/implementation",
4251 params={
4252 "textDocument": {"uri": "${php_file_uri}"},
4253 "position": {"line": 23, "character": 10},
4255 result=[
4257 "uri": "${php_file_uri}",
4258 "range": {
4259 "start": {"line": 30, "character": 6},
4260 "end": {"line": 30, "character": 16},
4265 .request(
4266 line=line(),
4267 comment="go to implemenetation: method",
4268 method="textDocument/implementation",
4269 params={
4270 "textDocument": {"uri": "${php_file_uri}"},
4271 "position": {"line": 19, "character": 18},
4273 result=[
4275 "uri": "${php_file_uri}",
4276 "range": {
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")
4291 spec = (
4292 self.initialize_spec(
4293 LspTestSpec("test_signature_help"), use_serverless_ide=False
4295 .wait_for_hh_server_ready()
4296 .notification(
4297 method="textDocument/didOpen",
4298 params={
4299 "textDocument": {
4300 "uri": "${php_file_uri}",
4301 "languageId": "hack",
4302 "version": 1,
4303 "text": "${php_file}",
4307 .request(
4308 line=line(),
4309 comment="signature help for 0-argument constructor"
4310 " (left of opening paren)",
4311 method="textDocument/signatureHelp",
4312 params={
4313 "textDocument": {"uri": "${php_file_uri}"},
4314 "position": {"line": 16, "character": 18},
4316 result=None,
4318 .request(
4319 line=line(),
4320 comment="signature help for 0-argument constructor",
4321 method="textDocument/signatureHelp",
4322 params={
4323 "textDocument": {"uri": "${php_file_uri}"},
4324 "position": {"line": 16, "character": 19},
4326 result={
4327 "signatures": [
4329 "label": "public function __construct(): void",
4330 "documentation": "Constructor with doc block",
4331 "parameters": [],
4334 "activeSignature": 0,
4335 "activeParameter": 0,
4338 .request(
4339 line=line(),
4340 comment="signature help for 0-argument constructor"
4341 " (right of closing paren)",
4342 method="textDocument/signatureHelp",
4343 params={
4344 "textDocument": {"uri": "${php_file_uri}"},
4345 "position": {"line": 16, "character": 20},
4347 result=None,
4349 .request(
4350 line=line(),
4351 comment="signature help for 2-argument instance method"
4352 " (left of opening paren)",
4353 method="textDocument/signatureHelp",
4354 params={
4355 "textDocument": {"uri": "${php_file_uri}"},
4356 "position": {"line": 17, "character": 20},
4358 result=None,
4360 .request(
4361 line=line(),
4362 comment="signature help for 2-argument instance method"
4363 " (right of opening paren)",
4364 method="textDocument/signatureHelp",
4365 params={
4366 "textDocument": {"uri": "${php_file_uri}"},
4367 "position": {"line": 17, "character": 21},
4369 result={
4370 "signatures": [
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,
4382 .request(
4383 line=line(),
4384 comment="signature help for 2-argument instance method"
4385 " (left of first comma)",
4386 method="textDocument/signatureHelp",
4387 params={
4388 "textDocument": {"uri": "${php_file_uri}"},
4389 "position": {"line": 17, "character": 22},
4391 result={
4392 "signatures": [
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,
4404 .request(
4405 line=line(),
4406 comment="signature help for 2-argument instance method"
4407 " (right of first comma)",
4408 method="textDocument/signatureHelp",
4409 params={
4410 "textDocument": {"uri": "${php_file_uri}"},
4411 "position": {"line": 17, "character": 23},
4413 result={
4414 "signatures": [
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,
4426 .request(
4427 line=line(),
4428 comment="signature help for 2-argument instance method"
4429 " (left of closing paren)",
4430 method="textDocument/signatureHelp",
4431 params={
4432 "textDocument": {"uri": "${php_file_uri}"},
4433 "position": {"line": 17, "character": 24},
4435 result={
4436 "signatures": [
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,
4448 .request(
4449 line=line(),
4450 comment="signature help for 2-argument instance method"
4451 " (right of closing paren)",
4452 method="textDocument/signatureHelp",
4453 params={
4454 "textDocument": {"uri": "${php_file_uri}"},
4455 "position": {"line": 17, "character": 25},
4457 result=None,
4459 .request(
4460 line=line(),
4461 comment="signature help for 1-argument static method"
4462 " (left of open paren)",
4463 method="textDocument/signatureHelp",
4464 params={
4465 "textDocument": {"uri": "${php_file_uri}"},
4466 "position": {"line": 18, "character": 23},
4468 result=None,
4470 .request(
4471 line=line(),
4472 comment="signature help for 1-argument static method"
4473 " (right of open paren)",
4474 method="textDocument/signatureHelp",
4475 params={
4476 "textDocument": {"uri": "${php_file_uri}"},
4477 "position": {"line": 18, "character": 24},
4479 result={
4480 "signatures": [
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,
4492 .request(
4493 line=line(),
4494 comment="signature help for 2-argument global function"
4495 " (left of open paren)",
4496 method="textDocument/signatureHelp",
4497 params={
4498 "textDocument": {"uri": "${php_file_uri}"},
4499 "position": {"line": 19, "character": 17},
4501 result=None,
4503 .request(
4504 line=line(),
4505 comment="signature help for 2-argument global function"
4506 " (right of open paren)",
4507 method="textDocument/signatureHelp",
4508 params={
4509 "textDocument": {"uri": "${php_file_uri}"},
4510 "position": {"line": 19, "character": 18},
4512 result={
4513 "signatures": [
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,
4525 .request(
4526 line=line(),
4527 comment="signature help for 1-argument namespace-aliased global"
4528 " function (right of open paren)",
4529 method="textDocument/signatureHelp",
4530 params={
4531 "textDocument": {"uri": "${php_file_uri}"},
4532 "position": {"line": 20, "character": 26},
4534 result=None,
4536 .request(
4537 line=line(),
4538 comment="signature help for 1-argument namespace-aliased global"
4539 " function (right of open paren)",
4540 method="textDocument/signatureHelp",
4541 params={
4542 "textDocument": {"uri": "${php_file_uri}"},
4543 "position": {"line": 20, "character": 26},
4545 result=None,
4547 .request(
4548 line=line(),
4549 comment="signature help for 1-argument namespace-aliased global"
4550 " function (right of open paren)",
4551 method="textDocument/signatureHelp",
4552 params={
4553 "textDocument": {"uri": "${php_file_uri}"},
4554 "position": {"line": 20, "character": 27},
4556 result={
4557 "signatures": [
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,
4568 .request(
4569 line=line(),
4570 comment="signature help for 1-argument namespace-aliased global"
4571 " function (right of open paren)",
4572 method="textDocument/signatureHelp",
4573 params={
4574 "textDocument": {"uri": "${php_file_uri}"},
4575 "position": {"line": 20, "character": 28},
4577 result={
4578 "signatures": [
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,
4589 .request(
4590 line=line(),
4591 comment="signature help for 2-argument function with params"
4592 " (right of open paren)",
4593 method="textDocument/signatureHelp",
4594 params={
4595 "textDocument": {"uri": "${php_file_uri}"},
4596 "position": {"line": 21, "character": 30},
4598 result={
4599 "signatures": [
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",
4606 "parameters": [
4607 {"label": "$param1", "documentation": "info1"},
4608 {"label": "$param2", "documentation": "info2"},
4612 "activeSignature": 0,
4613 "activeParameter": 0,
4616 .request(
4617 line=line(),
4618 comment="signature help for 2-argument function with params"
4619 " (right of open paren)",
4620 method="textDocument/signatureHelp",
4621 params={
4622 "textDocument": {"uri": "${php_file_uri}"},
4623 "position": {"line": 22, "character": 30},
4625 result={
4626 "signatures": [
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",
4632 "parameters": [
4633 {"label": "$param1", "documentation": "info1"},
4634 {"label": "$param2"},
4638 "activeSignature": 0,
4639 "activeParameter": 0,
4642 .request(
4643 line=line(),
4644 comment="signature help for 2-argument function with params"
4645 " (right of open paren)",
4646 method="textDocument/signatureHelp",
4647 params={
4648 "textDocument": {"uri": "${php_file_uri}"},
4649 "position": {"line": 23, "character": 30},
4651 result={
4652 "signatures": [
4654 "label": "function test_signature_help_params3("
4655 "\n string $param1,\n string $param2\n): string",
4656 "documentation": "@param $param1 info1"
4657 "\n for param1"
4658 "\n@param $param2 info2"
4659 "\n@return the string"
4660 "\n 'hack'",
4661 "parameters": [
4663 "label": "$param1",
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")
4682 spec = (
4683 self.initialize_spec(
4684 LspTestSpec("test_serverless_ide_signature_help_lambda"),
4685 use_serverless_ide=False,
4687 .wait_for_hh_server_ready()
4688 .notification(
4689 method="textDocument/didOpen",
4690 params={
4691 "textDocument": {
4692 "uri": "${php_file_uri}",
4693 "languageId": "hack",
4694 "version": 1,
4695 "text": "${php_file}",
4699 .request(
4700 line=line(),
4701 comment="signature help for a normal function call",
4702 method="textDocument/signatureHelp",
4703 params={
4704 "textDocument": {"uri": "${php_file_uri}"},
4705 "position": {"line": 8, "character": 29},
4707 result={
4708 "activeParameter": 0,
4709 "activeSignature": 0,
4710 "signatures": [
4712 "label": "function test_lambda_sighelp(\n"
4713 " string $str,\n"
4714 " (function(string): int) $f\n"
4715 "): int",
4716 "parameters": [{"label": "$str"}, {"label": "$f"}],
4721 .request(
4722 line=line(),
4723 comment="signature help for normal function call within a lambda",
4724 method="textDocument/signatureHelp",
4725 params={
4726 "textDocument": {"uri": "${php_file_uri}"},
4727 "position": {"line": 9, "character": 21},
4729 result={
4730 "activeParameter": 0,
4731 "activeSignature": 0,
4732 "signatures": [
4734 "label": "function normal_test_func(string $str): void",
4735 "parameters": [{"label": "$str"}],
4740 .request(
4741 line=line(),
4742 comment="signature help for text within a lambda, left side of an open paren",
4743 method="textDocument/signatureHelp",
4744 params={
4745 "textDocument": {"uri": "${php_file_uri}"},
4746 "position": {"line": 10, "character": 15},
4748 result=None,
4750 .request(
4751 line=line(),
4752 comment="signature help for text within a lambda, right side of an open paren",
4753 method="textDocument/signatureHelp",
4754 params={
4755 "textDocument": {"uri": "${php_file_uri}"},
4756 "position": {"line": 10, "character": 16},
4758 result=None,
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()
4789 spec = (
4790 self.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide=False)
4791 .wait_for_hh_server_ready()
4792 .request(
4793 line=line(),
4794 method="textDocument/definition",
4795 params={
4796 "textDocument": {"uri": "${php_file_uri}"},
4797 "position": {"line": 7, "character": 11},
4799 result=[
4801 "uri": "file://${root_path}/non_blocking.php",
4802 "range": {
4803 "start": {"line": 2, "character": 9},
4804 "end": {"line": 2, "character": 32},
4806 "title": "non_blocking_definition",
4809 wait_id="definition request",
4811 .notification(
4812 comment="remove hh_loop_forever() invocation to break the infinite loop",
4813 method="textDocument/didOpen",
4814 params={
4815 "textDocument": {
4816 "uri": "${root_path}/__hh_loop_forever_foo.php",
4817 "languageId": "hack",
4818 "version": 1,
4819 "text": """\
4820 <?hh // strict
4822 function __hh_loop_forever_foo(): int {
4823 return 4;
4825 """,
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()
4842 spec = (
4843 self.initialize_spec(
4844 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
4845 use_serverless_ide=True,
4847 .notification(
4848 method="textDocument/didOpen",
4849 params={
4850 "textDocument": {
4851 "uri": "${php_file_uri}",
4852 "languageId": "hack",
4853 "version": 1,
4854 "text": "${php_file}",
4858 .request(
4859 line=line(),
4860 comment="hover before change to class hierarchy should be `int`",
4861 method="textDocument/hover",
4862 params={
4863 "textDocument": {"uri": "${php_file_uri}"},
4864 "position": {"line": 7, "character": 14},
4866 result={
4867 "contents": [
4868 {"language": "hack", "value": "public function foo(): int"},
4869 "Return type: `int`",
4870 "Full name: `BaseClassIncremental::foo`",
4872 "range": {
4873 "start": {"line": 7, "character": 12},
4874 "end": {"line": 7, "character": 15},
4877 powered_by="serverless_ide",
4879 .write_to_disk(
4880 uri=changed_php_file_uri,
4881 contents="""\
4882 <?hh // strict
4883 class BaseClassIncremental {
4884 public function foo(): string { return ''; }
4886 """,
4887 notify=True,
4889 .request(
4890 line=line(),
4891 comment="hover after change to class hierarchy should be `string`",
4892 method="textDocument/hover",
4893 params={
4894 "textDocument": {"uri": "${php_file_uri}"},
4895 "position": {"line": 7, "character": 14},
4897 result={
4898 "contents": [
4899 {"language": "hack", "value": "public function foo(): string"},
4900 "Return type: `string`",
4901 "Full name: `BaseClassIncremental::foo`",
4903 "range": {
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()
4921 spec = (
4922 self.initialize_spec(
4923 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
4924 use_serverless_ide=True,
4926 .notification(
4927 method="textDocument/didOpen",
4928 params={
4929 "textDocument": {
4930 "uri": "${php_file_uri}",
4931 "languageId": "hack",
4932 "version": 1,
4933 "text": "${php_file}",
4937 .request(
4938 line=line(),
4939 comment="hover over function invocation",
4940 method="textDocument/hover",
4941 params={
4942 "textDocument": {"uri": "${php_file_uri}"},
4943 "position": {"line": 3, "character": 16},
4945 result={
4946 "contents": [
4947 {"language": "hack", "value": "int"},
4948 "A comment describing b_hover.",
4950 "range": {
4951 "start": {"line": 3, "character": 9},
4952 "end": {"line": 3, "character": 16},
4955 powered_by="serverless_ide",
4957 .notification(
4958 comment="make local, unsaved change to the file",
4959 method="textDocument/didChange",
4960 params={
4961 "textDocument": {"uri": "${php_file_uri}", "version": 2},
4962 "contentChanges": [
4964 "text": """\
4965 <?hh // strict
4966 // comment
4967 function a_hover(): int {
4968 return b_hover();
4970 # A comment describing b_hover differently.
4971 function b_hover(): string {
4972 return 42;
4979 .request(
4980 line=line(),
4981 comment="another hover over function invocation, should be string now",
4982 method="textDocument/hover",
4983 params={
4984 "textDocument": {"uri": "${php_file_uri}"},
4985 "position": {"line": 3, "character": 16},
4987 result={
4988 "contents": [
4989 {"language": "hack", "value": "string"},
4990 "A comment describing b_hover differently.",
4992 "range": {
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()
5011 spec = (
5012 self.initialize_spec(
5013 LspTestSpec("test_serverless_ide_decl_two_unsaved_buffers"),
5014 use_serverless_ide=True,
5016 .notification(
5017 comment="open 'unsaved1.php', since we'll be hovering in it",
5018 method="textDocument/didOpen",
5019 params={
5020 "textDocument": {
5021 "uri": "${php_file_uri}",
5022 "languageId": "hack",
5023 "version": 1,
5024 "text": "${php_file}",
5028 .notification(
5029 comment="open 'unsaved2.php' with a bool-returning signature, different from disk",
5030 method="textDocument/didOpen",
5031 params={
5032 "textDocument": {
5033 "uri": "${unsaved2_file_uri}",
5034 "languageId": "hack",
5035 "version": 1,
5036 "text": """\
5037 <?hh //strict
5038 function unsaved_bar(): bool { return true; }
5039 """,
5043 .request(
5044 line=line(),
5045 comment="hover 'unsaved1.php' is with respect to disk contents of 'unsaved2.php'",
5046 method="textDocument/hover",
5047 params={
5048 "textDocument": {"uri": "${php_file_uri}"},
5049 "position": {"line": 1, "character": 39},
5051 result={
5052 "contents": [
5053 {"language": "hack", "value": "function unsaved_bar(): int"},
5054 "Return type: `int`",
5056 "range": {
5057 "start": {"line": 1, "character": 34},
5058 "end": {"line": 1, "character": 45},
5061 powered_by="serverless_ide",
5063 .notification(
5064 comment="change signature in 'unsaved2.php' to return string",
5065 method="textDocument/didChange",
5066 params={
5067 "textDocument": {"uri": "${unsaved2_file_uri}", "version": 2},
5068 "contentChanges": [
5070 "text": """\
5071 <?hh //strict
5072 function unsaved_bar(): string { return "hello"; }
5078 .request(
5079 line=line(),
5080 comment="this is a dummy hover in 'unsaved2.php' just to ensure its decl is cached",
5081 method="textDocument/hover",
5082 params={
5083 "textDocument": {"uri": "${unsaved2_file_uri}"},
5084 "position": {"line": 0, "character": 0},
5086 result=None,
5087 powered_by="serverless_ide",
5089 .request(
5090 line=line(),
5091 comment="hover 'unsaved1.php' is still with respect to disk contents of 'unsaved2.php'",
5092 method="textDocument/hover",
5093 params={
5094 "textDocument": {"uri": "${php_file_uri}"},
5095 "position": {"line": 1, "character": 39},
5097 result={
5098 "contents": [
5099 {"language": "hack", "value": "function unsaved_bar(): int"},
5100 "Return type: `int`",
5102 "range": {
5103 "start": {"line": 1, "character": 34},
5104 "end": {"line": 1, "character": 45},
5107 powered_by="serverless_ide",
5109 .write_to_disk(
5110 comment="save signature in 'unsaved2' to return string",
5111 uri=variables["unsaved2_file_uri"],
5112 contents="""\
5113 <?hh // strict
5114 function unsaved_bar(): string { return "hello"; }
5115 """,
5116 notify=True,
5118 .request(
5119 line=line(),
5120 comment="hover 'unsaved1.php' gets new disk contents of 'unsaved2.php'",
5121 method="textDocument/hover",
5122 params={
5123 "textDocument": {"uri": "${php_file_uri}"},
5124 "position": {"line": 1, "character": 39},
5126 result={
5127 "contents": [
5128 {"language": "hack", "value": "function unsaved_bar(): string"},
5129 "Return type: `string`",
5131 "range": {
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()
5149 spec = (
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")
5156 .ignore_requests(
5157 comment="Ignore 'initializing...' messages since they're racy",
5158 method="window/showStatus",
5159 params={
5160 "type": 2,
5161 "actions": [{"title": "Restart hh_server"}],
5162 "message": "Hack IDE: initializing.\nhh_server: stopped.",
5163 "shortMessage": "Hack: initializing",
5166 .ignore_requests(
5167 comment="another racy initializing, before hh_server has even responded",
5168 method="window/showStatus",
5169 params={
5170 "type": 2,
5171 "actions": [],
5172 "message": "Hack IDE: initializing.",
5173 "shortMessage": "Hack: initializing",
5176 .ignore_requests(
5177 comment="another racy initialization to ignore, again before hh_server",
5178 method="window/showStatus",
5179 params={
5180 "type": 3,
5181 "actions": [],
5182 "message": "Hack IDE: ready.",
5183 "shortMessage": "Hack: ready",
5186 .wait_for_server_request(
5187 method="window/showStatus",
5188 params={
5189 "actions": [{"title": "Restart hh_server"}],
5190 "message": "Hack IDE: ready.\nhh_server: stopped.",
5191 "shortMessage": "Hack: ready",
5192 "type": 3,
5194 result=NoResponse(),
5196 .request(
5197 line=line(),
5198 comment="hover before file_open will fail",
5199 method="textDocument/hover",
5200 params={
5201 "textDocument": {"uri": "${php_file_uri}"},
5202 "position": {"line": 26, "character": 20},
5204 result=None,
5206 .notification(
5207 method="textDocument/didOpen",
5208 params={
5209 "textDocument": {
5210 "uri": "${php_file_uri}",
5211 "languageId": "hack",
5212 "version": 1,
5213 "text": "${php_file}",
5217 .request(
5218 line=line(),
5219 comment="hover after file_open will succeed",
5220 method="textDocument/hover",
5221 params={
5222 "textDocument": {"uri": "${php_file_uri}"},
5223 "position": {"line": 26, "character": 20},
5225 result={"contents": [{"language": "hack", "value": "string"}]},
5226 powered_by="serverless_ide",
5228 .request(
5229 line=line(),
5230 method="$test/shutdownServerlessIde",
5231 params={},
5232 result=None,
5233 powered_by="serverless_ide",
5235 .wait_for_server_request(
5236 method="window/showStatus",
5237 params={
5238 "actions": [
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",
5244 "type": 1,
5246 result={"title": "Restart Hack IDE"},
5248 .wait_for_server_request(
5249 method="window/showStatus",
5250 params={
5251 "actions": [{"title": "Restart hh_server"}],
5252 "message": "Hack IDE: ready.\nhh_server: stopped.",
5253 "shortMessage": "Hack: ready",
5254 "type": 3,
5256 result=NoResponse(),
5258 .request(
5259 line=line(),
5260 comment="hover after restart will succeed",
5261 method="textDocument/hover",
5262 params={
5263 "textDocument": {"uri": "${php_file_uri}"},
5264 "position": {"line": 26, "character": 20},
5266 result={"contents": [{"language": "hack", "value": "string"}]},
5267 powered_by="serverless_ide",
5269 .notification(
5270 method="textDocument/didClose",
5271 params={"textDocument": {"uri": "${php_file_uri}"}},
5273 .request(
5274 line=line(),
5275 comment="hover after file_close will fail",
5276 method="textDocument/hover",
5277 params={
5278 "textDocument": {"uri": "${php_file_uri}"},
5279 "position": {"line": 26, "character": 20},
5281 result=None,
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"))
5292 variables.update(
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()
5300 spec = (
5301 self.initialize_spec(
5302 LspTestSpec("test_hh_server_status_diagnostic"), use_serverless_ide=True
5304 .ignore_status_diagnostics(False)
5305 .notification(
5306 method="textDocument/didOpen",
5307 params={
5308 "textDocument": {
5309 "uri": "${php_file_uri}",
5310 "languageId": "hack",
5311 "version": 1,
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",
5319 params={
5320 "uri": "${php_file_uri}",
5321 "diagnostics": [
5323 "range": {
5324 "start": {"line": 0, "character": 0},
5325 "end": {"line": 0, "character": 1},
5327 "severity": 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": [],
5334 "isStatusFB": True,
5337 .notification(
5338 method="textDocument/didOpen",
5339 params={
5340 "textDocument": {
5341 "uri": "${unsaved2_file_uri}",
5342 "languageId": "hack",
5343 "version": 1,
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",
5351 params={
5352 "uri": "${php_file_uri}",
5353 "diagnostics": [],
5354 "isStatusFB": True,
5357 .wait_for_notification(
5358 comment="After didOpen(file2), the hh_server_status diagnostic should reappear in file2",
5359 method="textDocument/publishDiagnostics",
5360 params={
5361 "uri": "${unsaved2_file_uri}",
5362 "diagnostics": [
5364 "range": {
5365 "start": {"line": 0, "character": 0},
5366 "end": {"line": 0, "character": 1},
5368 "severity": 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": [],
5375 "isStatusFB": True,
5378 .notification(
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",
5385 params={
5386 "uri": "${unsaved2_file_uri}",
5387 "diagnostics": [],
5388 "isStatusFB": True,
5391 .wait_for_notification(
5392 comment="After didClose(file2), the hh_server_status diagnostic should reappear in file1",
5393 method="textDocument/publishDiagnostics",
5394 params={
5395 "uri": "${php_file_uri}",
5396 "diagnostics": [
5398 "range": {
5399 "start": {"line": 0, "character": 0},
5400 "end": {"line": 0, "character": 1},
5402 "severity": 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": [],
5409 "isStatusFB": True,
5412 .notification(
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",
5419 params={
5420 "uri": "${php_file_uri}",
5421 "diagnostics": [],
5422 "isStatusFB": True,
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()
5440 spec = (
5441 self.initialize_spec(
5442 LspTestSpec("test_lsptestspec_incorrect_request_result"),
5443 use_serverless_ide=True,
5445 .notification(
5446 method="textDocument/didOpen",
5447 params={
5448 "textDocument": {
5449 "uri": "${php_file_uri}",
5450 "languageId": "hack",
5451 "version": 1,
5452 "text": "${php_file}",
5456 .request(
5457 line=line(),
5458 comment="hover over function invocation",
5459 method="textDocument/hover",
5460 params={
5461 "textDocument": {"uri": "${php_file_uri}"},
5462 "position": {"line": 3, "character": 16},
5464 result={
5465 "contents": [
5466 {"language": "hack", "value": "int"},
5467 "INCORRECT COMMENT HERE",
5469 "range": {
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={})
5479 try:
5480 self.run_spec(
5481 spec,
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:
5488 self.assertEqual(
5489 self._sanitize_gutter_line_numbers(str(e)),
5490 """\
5491 Test case test_lsptestspec_incorrect_request_result failed with 1 errors:
5493 Error 1/1:
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}}}
5506 Context:
5507 This was the associated request:
5509 hphp/hack/test/integration/test_lsp.py
5510 XXXX | .request(
5511 XXXX | line=line(),
5512 XXXX | comment="hover over function invocation",
5513 XXXX | method="textDocument/hover",
5514 XXXX | params={
5515 XXXX | "textDocument": {"uri": "${php_file_uri}"},
5516 XXXX | "position": {"line": 3, "character": 16},
5517 XXXX | },
5518 XXXX | result={
5519 XXXX | "contents": [
5520 XXXX | {"language": "hack", "value": "int"},
5521 XXXX | "INCORRECT COMMENT HERE",
5522 XXXX | ],
5523 XXXX | "range": {
5524 XXXX | "start": {"line": 3, "character": 9},
5525 XXXX | "end": {"line": 3, "character": 16},
5526 XXXX | },
5527 XXXX | },
5528 XXXX | powered_by="serverless_ide",
5529 XXXX | )
5531 Remediation:
5532 1) If this was unexpected, then the language server is buggy and should be
5533 fixed.
5535 2) If this was expected, you can update your request with the following code to
5536 make it match:
5538 .request(
5539 line=line(),
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.\
5553 """,
5556 def test_lsptestspec_unexpected_notification(self) -> None:
5557 self.prepare_server_environment()
5558 variables = self.setup_php_file("didchange.php")
5559 spec = (
5560 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
5561 .wait_for_hh_server_ready()
5562 .notification(
5563 method="textDocument/didOpen",
5564 params={
5565 "textDocument": {
5566 "uri": "${php_file_uri}",
5567 "languageId": "hack",
5568 "version": 1,
5569 "text": "${php_file}",
5573 .notification(
5574 method="textDocument/didChange",
5575 params={
5576 "textDocument": {"uri": "${php_file_uri}"},
5577 "contentChanges": [
5579 "range": {
5580 "start": {"line": 7, "character": 11},
5581 "end": {"line": 7, "character": 12},
5583 "text": "a",
5588 .wait_for_notification(
5589 method="textDocument/publishDiagnostics",
5590 params={
5591 "uri": "${php_file_uri}",
5592 "diagnostics": [
5594 "range": {
5595 "start": {"line": 7, "character": 11},
5596 "end": {"line": 7, "character": 11},
5598 "severity": 1,
5599 "code": 1002,
5600 "source": "Hack",
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={})
5611 try:
5612 self.run_spec(
5613 spec, variables, wait_for_server=True, use_serverless_ide=False
5615 raise AssertionError("Expected an error here")
5616 except AssertionError as e:
5617 self.assertEqual(
5618 self._sanitize_gutter_line_numbers(str(e)),
5619 """\
5620 Test case did_change failed with 1 errors:
5622 Error 1/1:
5623 Description: An unexpected notification of type \
5624 'textDocument/publishDiagnostics' was sent by the language server.
5625 Here is the notification payload:
5627 {'jsonrpc': '2.0',
5628 'method': 'textDocument/publishDiagnostics',
5629 'params': {'diagnostics': [],
5630 'uri': '__PHP_FILE_URI__'}}
5632 Context:
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)
5639 Remediation:
5640 1) If this was unexpected, then the language server is buggy and should be
5641 fixed.
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
5662 # that one.
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()
5671 spec = (
5672 self.initialize_spec(
5673 LspTestSpec("serverless_ide_highlight"), use_serverless_ide=True
5675 .notification(
5676 method="textDocument/didOpen",
5677 params={
5678 "textDocument": {
5679 "uri": "${php_file_uri}",
5680 "languageId": "hack",
5681 "version": 1,
5682 "text": "${php_file}",
5686 .request(
5687 line=line(),
5688 comment="document highlight, id 2",
5689 method="textDocument/documentHighlight",
5690 params={
5691 "textDocument": {"uri": "${php_file_uri}"},
5692 "position": {"line": 3, "character": 10},
5694 result=[
5696 "range": {
5697 "start": {"line": 3, "character": 9},
5698 "end": {"line": 3, "character": 20},
5702 powered_by="serverless_ide",
5704 .request(
5705 line=line(),
5706 comment="shutdown, id 3",
5707 method="shutdown",
5708 params={},
5709 result=None,
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()
5719 spec = (
5720 self.initialize_spec(
5721 LspTestSpec("serverless_ide_coverage"), use_serverless_ide=True
5723 .notification(
5724 method="textDocument/didOpen",
5725 params={
5726 "textDocument": {
5727 "uri": "${php_file_uri}",
5728 "languageId": "hack",
5729 "version": 1,
5730 "text": "${php_file}",
5734 .request(
5735 line=line(),
5736 comment="Check type coverage",
5737 method="textDocument/typeCoverage",
5738 params={"textDocument": {"uri": "${php_file_uri}"}},
5739 result={
5740 "coveredPercent": 0,
5741 "uncoveredRanges": [
5743 "range": {
5744 "start": {"line": 3, "character": 12},
5745 "end": {"line": 3, "character": 17},
5749 "range": {
5750 "start": {"line": 3, "character": 8},
5751 "end": {"line": 3, "character": 10},
5755 "range": {
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",
5765 .request(
5766 line=line(),
5767 comment="Shutdown",
5768 method="shutdown",
5769 params={},
5770 result=None,
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()
5780 spec = (
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",
5788 params={
5789 "shortMessage": "Hack: stopped",
5790 "message": "hh_server: stopped.",
5791 "actions": [{"title": "Restart hh_server"}],
5792 "type": 1,
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"))
5805 spec = (
5806 self.initialize_spec(
5807 LspTestSpec("status_running"),
5808 use_serverless_ide=False,
5809 supports_status=True,
5811 .ignore_requests(
5812 comment="Ignore initializing... requests since they're racy",
5813 method="window/showStatus",
5814 params={
5815 "type": 2,
5816 "shortMessage": "Hack: initializing",
5817 "message": "hh_server initializing: processing [<test> seconds]",
5818 "actions": [],
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()
5836 spec = (
5837 self.initialize_spec(
5838 LspTestSpec("serverless_ide_status_stopped"),
5839 use_serverless_ide=True,
5840 supports_status=True,
5842 .ignore_requests(
5843 comment="ignore initializing... messages since they're kind of racy",
5844 method="window/showStatus",
5845 params={
5846 "type": 2,
5847 "actions": [{"title": "Restart hh_server"}],
5848 "message": "Hack IDE: initializing.\nhh_server: stopped.",
5849 "shortMessage": "Hack: initializing",
5852 .ignore_requests(
5853 comment="another racy initialization to ignore, before hh_server has even reported its status",
5854 method="window/showStatus",
5855 params={
5856 "type": 2,
5857 "actions": [],
5858 "message": "Hack IDE: initializing.",
5859 "shortMessage": "Hack: initializing",
5862 .ignore_requests(
5863 comment="another racy initialization to ignore, again before hh_server",
5864 method="window/showStatus",
5865 params={
5866 "type": 3,
5867 "actions": [],
5868 "message": "Hack IDE: ready.",
5869 "shortMessage": "Hack: ready",
5872 .wait_for_server_request(
5873 method="window/showStatus",
5874 params={
5875 "message": "Hack IDE: ready.\nhh_server: stopped.",
5876 "shortMessage": "Hack: ready",
5877 "actions": [{"title": "Restart hh_server"}],
5878 "type": 3,
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"))
5891 spec = (
5892 self.initialize_spec(
5893 LspTestSpec("serverless_ide_status_restart"),
5894 use_serverless_ide=True,
5895 supports_status=True,
5897 .ignore_requests(
5898 comment="Ignore initializing messages since they're racy",
5899 method="window/showStatus",
5900 params={
5901 "type": 2,
5902 "actions": [],
5903 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
5904 "shortMessage": "Hack: initializing",
5907 .ignore_requests(
5908 comment="Another form of initializing to ignore",
5909 method="window/showStatus",
5910 params={
5911 "type": 2,
5912 "actions": [],
5913 "message": "Hack IDE: initializing.\nhh_server: ready.",
5914 "shortMessage": "Hack: initializing",
5917 .ignore_requests(
5918 comment="Another form of initializing to ignore before we've even heard the first peep from hh_server",
5919 method="window/showStatus",
5920 params={
5921 "type": 2,
5922 "actions": [],
5923 "message": "Hack IDE: initializing.",
5924 "shortMessage": "Hack: initializing",
5927 .ignore_requests(
5928 comment="another racy initialization to ignore, again before hh_server",
5929 method="window/showStatus",
5930 params={
5931 "type": 3,
5932 "actions": [],
5933 "message": "Hack IDE: ready.",
5934 "shortMessage": "Hack: ready",
5937 .wait_for_server_request(
5938 method="window/showStatus",
5939 params={
5940 "actions": [],
5941 "message": "Hack IDE: ready.\nhh_server: ready.",
5942 "shortMessage": "Hack: ready",
5943 "type": 3,
5945 result=NoResponse(),
5947 .request(
5948 line=line(),
5949 method="$test/shutdownServerlessIde",
5950 params={},
5951 result=None,
5952 powered_by="serverless_ide",
5954 .wait_for_server_request(
5955 method="window/showStatus",
5956 params={
5957 "actions": [{"title": "Restart Hack IDE"}],
5958 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
5959 "shortMessage": "Hack: failed",
5960 "type": 1,
5962 result={"title": "Restart Hack IDE"},
5964 .wait_for_server_request(
5965 method="window/showStatus",
5966 params={
5967 "actions": [],
5968 "message": "Hack IDE: ready.\nhh_server: ready.",
5969 "shortMessage": "Hack: ready",
5970 "type": 3,
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"
5985 spec = (
5986 self.initialize_spec(
5987 LspTestSpec("serverless_ide_status_failed_to_load_saved_state"),
5988 use_serverless_ide=True,
5989 supports_status=True,
5990 supports_init=True,
5992 .ignore_requests(
5993 comment="Ignore initializing since they're kind of racy",
5994 method="window/showStatus",
5995 params={
5996 "type": 2,
5997 "actions": [],
5998 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
5999 "shortMessage": "Hack: initializing",
6002 .ignore_requests(
6003 comment="Ignore another form of initializing",
6004 method="window/showStatus",
6005 params={
6006 "type": 2,
6007 "actions": [],
6008 "message": "Hack IDE: initializing.\nhh_server: ready.",
6009 "shortMessage": "Hack: initializing",
6012 .ignore_requests(
6013 comment="Ignore another form of initializing, from before we've even heard the first peep out of hh_server",
6014 method="window/showStatus",
6015 params={
6016 "type": 2,
6017 "actions": [],
6018 "message": "Hack IDE: initializing.",
6019 "shortMessage": "Hack: initializing",
6022 .ignore_requests(
6023 comment="Ignore another form of initializing, again before hh_server",
6024 method="window/showStatus",
6025 params={
6026 "type": 1,
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",
6034 params={
6035 "type": 1,
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",
6041 params={
6042 "actions": [{"title": "Restart Hack IDE"}],
6043 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6044 "shortMessage": "Hack: failed",
6045 "type": 1,
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")
6057 spec = (
6058 self.initialize_spec(
6059 LspTestSpec("test_workspace_symbol"), use_serverless_ide=False
6061 .wait_for_hh_server_ready()
6062 .request(
6063 line=line(),
6064 comment="Look up symbols",
6065 method="workspace/symbol",
6066 params={"query": "TestNS\\test"},
6067 result=[
6069 "name": "TestNS\\test_func",
6070 "kind": 12,
6071 "location": {
6072 "uri": "file://${root_path}/completion_extras_namespace.php",
6073 "range": {
6074 "start": {"line": 4, "character": 9},
6075 "end": {"line": 4, "character": 25},
6081 .request(
6082 line=line(),
6083 comment="Look up symbols starting with 'test_f' within multiple namespaces",
6084 method="workspace/symbol",
6085 params={"query": "test_f"},
6086 result=[
6088 "name": "test_function",
6089 "kind": 12,
6090 "location": {
6091 "uri": "file://${root_path}/completion.php",
6092 "range": {
6093 "start": {"line": 7, "character": 9},
6094 "end": {"line": 7, "character": 22},
6099 "name": "TestNS\\test_func",
6100 "kind": 12,
6101 "location": {
6102 "uri": "file://${root_path}/completion_extras_namespace.php",
6103 "range": {
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"))
6119 spec = (
6120 self.initialize_spec(
6121 LspTestSpec("test_serverless_ide_during_hh_server_restart"),
6122 use_serverless_ide=True,
6124 .notification(
6125 method="textDocument/didOpen",
6126 params={
6127 "textDocument": {
6128 "uri": "${php_file_uri}",
6129 "languageId": "hack",
6130 "version": 1,
6131 "text": "${php_file}",
6135 .notification(
6136 comment="Send a 'didChange' notification before HH Server is functional.",
6137 method="textDocument/didChange",
6138 params={
6139 "textDocument": {"uri": "${php_file_uri}"},
6140 "contentChanges": [
6142 "range": {
6143 "start": {"line": 7, "character": 9},
6144 "end": {"line": 7, "character": 11},
6146 "text": "'foo'",
6151 .start_hh_server("Start HH Server; should detect the bad edit")
6152 .wait_for_notification(
6153 method="textDocument/publishDiagnostics",
6154 params={
6155 "uri": "${php_file_uri}",
6156 "diagnostics": [
6158 "code": 4110,
6159 "message": "Invalid return type",
6160 "range": {
6161 "end": {"character": 14, "line": 7},
6162 "start": {"character": 9, "line": 7},
6164 "relatedInformation": [
6166 "location": {
6167 "range": {
6168 "end": {"character": 27, "line": 6},
6169 "start": {"character": 24, "line": 6},
6171 "uri": "${php_file_uri}",
6173 "message": "Expected int",
6176 "location": {
6177 "range": {
6178 "end": {"character": 14, "line": 7},
6179 "start": {"character": 9, "line": 7},
6181 "uri": "${php_file_uri}",
6183 "message": "But got string",
6186 "relatedLocations": [
6188 "location": {
6189 "range": {
6190 "end": {"character": 27, "line": 6},
6191 "start": {"character": 24, "line": 6},
6193 "uri": "${php_file_uri}",
6195 "message": "Expected int",
6198 "location": {
6199 "range": {
6200 "end": {"character": 14, "line": 7},
6201 "start": {"character": 9, "line": 7},
6203 "uri": "${php_file_uri}",
6205 "message": "But got string",
6208 "severity": 1,
6209 "source": "Hack",
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",
6224 params={
6225 "uri": "${php_file_uri}",
6226 "diagnostics": [
6228 "code": 4110,
6229 "message": "Invalid return type",
6230 "range": {
6231 "end": {"character": 14, "line": 7},
6232 "start": {"character": 9, "line": 7},
6234 "relatedInformation": [
6236 "location": {
6237 "range": {
6238 "end": {"character": 27, "line": 6},
6239 "start": {"character": 24, "line": 6},
6241 "uri": "${php_file_uri}",
6243 "message": "Expected int",
6246 "location": {
6247 "range": {
6248 "end": {"character": 14, "line": 7},
6249 "start": {"character": 9, "line": 7},
6251 "uri": "${php_file_uri}",
6253 "message": "But got string",
6256 "relatedLocations": [
6258 "location": {
6259 "range": {
6260 "end": {"character": 27, "line": 6},
6261 "start": {"character": 24, "line": 6},
6263 "uri": "${php_file_uri}",
6265 "message": "Expected int",
6268 "location": {
6269 "range": {
6270 "end": {"character": 14, "line": 7},
6271 "start": {"character": 9, "line": 7},
6273 "uri": "${php_file_uri}",
6275 "message": "But got string",
6278 "severity": 1,
6279 "source": "Hack",
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"))
6292 variables.update(
6294 "main_file": self.repo_file("main.php"),
6295 "main_file_contents": """\
6296 <?hh
6297 function main(): int {
6298 return aaa();
6300 """,
6301 "file_a": self.repo_file("a.php"),
6302 "file_b": self.repo_file("b.php"),
6305 spec = (
6306 self.initialize_spec(
6307 LspTestSpec("serverless_ide_naming_error1"), use_serverless_ide=True
6309 .write_to_disk(
6310 uri="${main_file}", contents="${main_file_contents}", notify=True
6312 .notification(
6313 method="textDocument/didOpen",
6314 params={
6315 "textDocument": {
6316 "uri": "${main_file}",
6317 "languageId": "hack",
6318 "version": 1,
6319 "text": "${main_file_contents}",
6323 .request(
6324 line=line(),
6325 comment="Ensure that hover over `aaa` works even when the name is not yet defined",
6326 method="textDocument/hover",
6327 params={
6328 "textDocument": {"uri": "${main_file}"},
6329 "position": {"line": 2, "character": 13},
6331 result={
6332 "contents": [{"language": "hack", "value": "_"}],
6333 "range": {
6334 "start": {"line": 2, "character": 11},
6335 "end": {"line": 2, "character": 14},
6338 powered_by="serverless_ide",
6340 .write_to_disk(
6341 comment="create file A",
6342 uri="${file_a}",
6343 contents="""\
6344 <?hh
6345 function aaa(): int {
6346 return 1;
6348 """,
6349 notify=True,
6351 .request(
6352 line=line(),
6353 comment="Ensure that hover over `aaa` works when there are no naming errors",
6354 method="textDocument/hover",
6355 params={
6356 "textDocument": {"uri": "${main_file}"},
6357 "position": {"line": 2, "character": 13},
6359 result={
6360 "contents": [
6361 {"language": "hack", "value": "function aaa(): int"},
6362 "Return type: `int`",
6364 "range": {
6365 "start": {"line": 2, "character": 11},
6366 "end": {"line": 2, "character": 14},
6369 powered_by="serverless_ide",
6371 .write_to_disk(
6372 comment="create file B",
6373 uri="${file_b}",
6374 contents="""\
6375 <?hh
6376 function aaa(): string {
6377 return "foo";
6379 """,
6380 notify=True,
6382 .request(
6383 line=line(),
6384 comment="Ensure that hover over `aaa` works even when there is a duplicate name",
6385 method="textDocument/hover",
6386 params={
6387 "textDocument": {"uri": "${main_file}"},
6388 "position": {"line": 2, "character": 13},
6390 result={
6391 "contents": [
6392 {"language": "hack", "value": "function aaa(): int"},
6393 "Return type: `int`",
6395 "range": {
6396 "start": {"line": 2, "character": 11},
6397 "end": {"line": 2, "character": 14},
6400 powered_by="serverless_ide",
6402 .write_to_disk(
6403 comment="delete file A", uri="${file_a}", contents=None, notify=True
6405 .request(
6406 line=line(),
6407 comment="Now that we've fixed the error, hover should work.",
6408 method="textDocument/hover",
6409 params={
6410 "textDocument": {"uri": "${main_file}"},
6411 "position": {"line": 2, "character": 13},
6413 result={
6414 "contents": [
6415 {"language": "hack", "value": "function aaa(): string"},
6416 "Return type: `string`",
6418 "range": {
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"))
6434 variables.update(
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"),
6441 spec = (
6442 self.initialize_spec(
6443 LspTestSpec("serverless_ide_naming_error2"), use_serverless_ide=True
6445 .notification(
6446 method="textDocument/didOpen",
6447 params={
6448 "textDocument": {
6449 "uri": "${php_file_uri}",
6450 "languageId": "hack",
6451 "version": 1,
6452 "text": "${php_file}",
6456 .write_to_disk(
6457 comment="create copy",
6458 uri="${copy}",
6459 contents="${contents}",
6460 notify=True,
6462 .write_to_disk(
6463 comment="delete copy", uri="${copy}", contents=None, notify=True
6465 .request(
6466 line=line(),
6467 comment="hover should work fine after making copy then deleting copy.",
6468 method="textDocument/hover",
6469 params={
6470 "textDocument": {"uri": "${php_file_uri}"},
6471 "position": {"line": 3, "character": 15},
6473 result={
6474 "contents": [
6476 "language": "hack",
6477 "value": "function naming_error_declaration(): void",
6479 "Return type: `void`",
6481 "range": {
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"))
6497 variables.update(
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"),
6504 spec = (
6505 self.initialize_spec(
6506 LspTestSpec("serverless_ide_naming_error3"), use_serverless_ide=True
6508 .notification(
6509 method="textDocument/didOpen",
6510 params={
6511 "textDocument": {
6512 "uri": "${php_file_uri}",
6513 "languageId": "hack",
6514 "version": 1,
6515 "text": "${php_file}",
6519 .write_to_disk(
6520 comment="create copy",
6521 uri="${copy}",
6522 contents="${contents}",
6523 notify=True,
6525 .write_to_disk(
6526 comment="delete original", uri="${original}", contents=None, notify=True
6528 .request(
6529 line=line(),
6530 comment="hover should work fine after making copy then deleting original.",
6531 method="textDocument/hover",
6532 params={
6533 "textDocument": {"uri": "${php_file_uri}"},
6534 "position": {"line": 3, "character": 15},
6536 result={
6537 "contents": [
6539 "language": "hack",
6540 "value": "function naming_error_declaration(): void",
6542 "Return type: `void`",
6544 "range": {
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()
6561 spec = (
6562 self.initialize_spec(
6563 LspTestSpec("test_serverless_ide_requests_before_init"),
6564 use_serverless_ide=True,
6565 supports_status=True,
6566 supports_init=True,
6568 .ignore_notifications(method="textDocument/publishDiagnostics")
6569 .ignore_requests(
6570 comment="Ignore 'initializing...' messages since they're racy",
6571 method="window/showStatus",
6572 params={
6573 "type": 2,
6574 "actions": [{"title": "Restart hh_server"}],
6575 "message": "Hack IDE: initializing.\nhh_server: stopped.",
6576 "shortMessage": "Hack: initializing",
6579 .ignore_requests(
6580 comment="another racy initialization, before we've yet heard from hh_server",
6581 method="window/showStatus",
6582 params={
6583 "type": 2,
6584 "actions": [],
6585 "message": "Hack IDE: initializing.",
6586 "shortMessage": "Hack: initializing",
6589 .ignore_requests(
6590 comment="another racy initialization, if HackIDE is done before hh_server has yet sent status",
6591 method="window/showStatus",
6592 params={
6593 "type": 3,
6594 "actions": [],
6595 "message": "Hack IDE: ready.",
6596 "shortMessage": "Hack: ready",
6599 .write_to_disk(
6600 notify=True,
6601 wait=False,
6602 uri="file://${root_path}/beforeInit1.php",
6603 contents="<?hh // strict\nfunction beforeInit1(): int {\n return 42;\n}\n",
6605 .notification(
6606 comment="open a file before init has finished",
6607 method="textDocument/didOpen",
6608 params={
6609 "textDocument": {
6610 "uri": "file://${root_path}/beforeInit2.php",
6611 "languageId": "hack",
6612 "version": 1,
6613 "text": "<?hh // strict\nfunction beforeInit2(): void {\n $foo = beforeInit1();\n}\n",
6617 .request(
6618 line=line(),
6619 comment="hover before init will fail",
6620 method="textDocument/hover",
6621 params={
6622 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
6623 "position": {"line": 2, "character": 4},
6625 result=None,
6627 .request(
6628 line=line(),
6629 comment="documentSymbol before init will succeed",
6630 method="textDocument/documentSymbol",
6631 params={"textDocument": {"uri": "file://${root_path}/beforeInit2.php"}},
6632 result=[
6634 "name": "beforeInit2",
6635 "kind": 12,
6636 "location": {
6637 "uri": "file://${root_path}/beforeInit2.php",
6638 "range": {
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",
6654 params={
6655 "actions": [{"title": "Restart hh_server"}],
6656 "message": "Hack IDE: ready.\nhh_server: stopped.",
6657 "shortMessage": "Hack: ready",
6658 "type": 3,
6660 result=NoResponse(),
6662 .request(
6663 line=line(),
6664 comment="hover after init will succeed",
6665 method="textDocument/hover",
6666 params={
6667 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
6668 "position": {"line": 2, "character": 4},
6670 result={
6671 "contents": [{"language": "hack", "value": "int"}],
6672 "range": {
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()
6690 spec = (
6691 self.initialize_spec(
6692 LspTestSpec("serverless_ide_workspace_symbol"), use_serverless_ide=True
6694 .request(
6695 line=line(),
6696 comment="workspace symbol call, global, powered by sqlite (generated during serverless-ide-init)",
6697 method="workspace/symbol",
6698 params={"query": "TakesString"},
6699 result=[
6701 "name": "TakesString",
6702 "kind": 5,
6703 "location": {
6704 "uri": "file://${root_path}/definition.php",
6705 "range": {
6706 "start": {"line": 36, "character": 6},
6707 "end": {"line": 36, "character": 17},
6712 powered_by="serverless_ide",
6714 .request(
6715 line=line(),
6716 comment="workspace symbol call, member (derived from naming-table)",
6717 method="workspace/symbol",
6718 params={"query": "TakesString::"},
6719 result=[
6721 "name": "__construct",
6722 "kind": 6,
6723 "location": {
6724 "uri": "file://${root_path}/definition.php",
6725 "range": {
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)