Prefer Field for class properties in LSP
[hiphop-php.git] / hphp / hack / test / integration / test_lsp.py
bloba377ce2a7fd293762ae98de5f623a2a087b7e0df
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 line, LspTestSpec, NoResponse
19 from test_case import TestCase
20 from utils import interpolate_variables, Json, JsonObject
23 class LspTestDriver(common_tests.CommonTestDriver):
24 def write_load_config(
25 self, use_serverless_ide: bool = False, use_saved_state: bool = False
26 ) -> None:
27 # Will use the .hhconfig already in the repo directory
28 # As for hh.conf, we'll write it explicitly each test.
29 with open(os.path.join(self.repo_dir, "hh.conf"), "w") as f:
30 f.write(
31 """
32 use_watchman = true
33 watchman_subscribe_v2 = true
34 interrupt_on_watchman = true
35 interrupt_on_client = true
36 max_workers = 2
37 load_state_natively_v4 = {use_saved_state}
38 use_mini_state = {use_saved_state}
39 require_mini_state = {use_saved_state}
40 lazy_decl = {use_saved_state}
41 lazy_parse = {use_saved_state}
42 lazy_init2 = {use_saved_state}
43 symbolindex_search_provider = SqliteIndex
44 allow_unstable_features = true
45 ide_serverless = {use_serverless_ide}
46 """.format(
47 use_saved_state=str(use_saved_state).lower(),
48 use_serverless_ide=str(use_serverless_ide).lower(),
52 def write_naming_table_saved_state(self) -> str:
53 naming_table_saved_state_path = os.path.join(
54 self.repo_dir, "naming_table_saved_state.sqlite"
56 (stdout, stderr, retcode) = self.proc_call(
58 hh_server,
59 "--check",
60 self.repo_dir,
61 "--save-naming",
62 naming_table_saved_state_path,
65 assert retcode == 0, (
66 f"Failed to save naming table saved state: {retcode}\n"
67 + f"STDOUT:\n{stdout}\n"
68 + f"STDERR:\n{stderr}\n"
70 return naming_table_saved_state_path
73 class TestLsp(TestCase[LspTestDriver]):
74 @classmethod
75 def get_test_driver(cls) -> LspTestDriver:
76 return LspTestDriver()
78 @classmethod
79 def get_template_repo(cls) -> str:
80 return "hphp/hack/test/integration/data/lsp_exchanges/"
82 def repo_file(self, file: str) -> str:
83 return os.path.join(self.test_driver.repo_dir, file)
85 def read_repo_file(self, file: str) -> str:
86 with open(self.repo_file(file), "r") as f:
87 return f.read()
89 def repo_file_uri(self, file: str) -> str:
90 return urllib.parse.urljoin("file://", self.repo_file(file))
92 # pyre-fixme[11]: Annotation `Json` is not defined as a type.
93 def parse_test_data(self, file: str, variables: Mapping[str, str]) -> Json:
94 text = self.read_repo_file(file)
95 data: Json = json.loads(text)
96 data = interpolate_variables(data, variables)
97 return data
99 def load_test_data(
100 self, test_name: str, variables: Mapping[str, str]
101 ) -> Tuple[Json, Json]:
102 test = self.parse_test_data(test_name + ".json", variables)
103 expected = self.parse_test_data(test_name + ".expected", variables)
104 return (test, expected)
106 def write_observed(self, test_name: str, observed_transcript: Json) -> None:
107 file = os.path.join(self.test_driver.template_repo, test_name + ".observed.log")
108 text = json.dumps(
109 list(self.get_important_received_items(observed_transcript)), indent=2
111 with open(file, "w") as f:
112 f.write(text)
114 # pyre-fixme[11]: Annotation `JsonObject` is not defined as a type.
115 def order_response(self, response: JsonObject) -> str:
116 if "id" in response:
117 return str(response["id"])
118 else:
119 return json.dumps(response, indent=2)
121 # sorts a list of responses using the 'id' parameter so they can be
122 # compared in sequence even if they came back from the server out of sequence.
123 # this can happen based on how json rpc is specified to work.
124 # if 'id' isn't present the response is a notification. we sort notifications
125 # by their entire text.
126 def sort_responses(self, responses: Iterable[JsonObject]) -> List[JsonObject]:
127 return sorted(responses, key=lambda response: self.order_response(response))
129 # removes stack traces from error responses since these can be noisy
130 # as code changes and they contain execution environment specific details
131 # by ignoring these when comparing responses we might miss some minor issues
132 # but will still catch the core error being thrown or not.
133 def sanitize_exceptions(
134 self, responses: Iterable[JsonObject]
135 ) -> Iterable[JsonObject]:
136 sanitized = copy.deepcopy(responses)
137 for response in sanitized:
138 if "error" in response:
139 if "data" in response["error"]:
140 if "stack" in response["error"]["data"]:
141 del response["error"]["data"]["stack"]
142 if "current_stack" in response["error"]["data"]:
143 del response["error"]["data"]["current_stack"]
144 if "server_finale_stack" in response["error"]["data"]:
145 del response["error"]["data"]["server_finale_stack"]
146 return sanitized
148 # dumps an LSP response into a standard json format that can be used for
149 # doing precise text comparison in a way that is human readable in the case
150 # of there being an error.
151 def serialize_responses(self, responses: Iterable[Json]) -> List[str]:
152 return [json.dumps(response, indent=2) for response in responses]
154 # generates received responses from an LSP communication transcript
155 # ignoring the non-deterministic ones "progress" and "actionRequired"
156 def get_important_received_items(self, transcript: Transcript) -> Iterable[Json]:
157 for entry in transcript.values():
158 received = entry.received or None
159 if received is None:
160 continue
161 method = received.get("method") or ""
162 if method in [
163 "window/progress",
164 "window/actionRequired",
165 "window/showStatus",
166 "telemetry/event",
168 continue
169 yield received
171 # gets a set of loaded responses ready for validation by sorting them
172 # by id and serializing them for precise text comparison
173 def prepare_responses(self, responses: Iterable[JsonObject]) -> List[str]:
174 return self.serialize_responses(
175 self.sanitize_exceptions(self.sort_responses(responses))
178 def run_lsp_test(
179 self,
180 test_name: str,
181 test: Json,
182 expected: Json,
183 wait_for_server: bool,
184 use_serverless_ide: bool,
185 ) -> None:
186 if wait_for_server:
187 assert not use_serverless_ide, (
188 "Warning: both `wait_for_server` and `use_serverless_ide` "
189 + "were set to `True` for testing in "
190 + self.run_lsp_test.__name__
191 + ". "
192 + "While this is a possible test case, it hasn't been written yet, "
193 + "so it's more likely that this is a mistake "
194 + "and you're accidentally relying on hh_server to fulfill "
195 + "serverless IDE requests."
196 + "(If you're writing that test, "
197 + "then it's time to remove this assertion.)"
200 # wait until hh_server is ready before starting lsp
201 self.test_driver.run_check()
202 elif use_serverless_ide:
203 self.test_driver.stop_hh_server()
205 with LspCommandProcessor.create(self.test_driver.test_env) as lsp:
206 observed_transcript = lsp.communicate(test)
208 self.write_observed(test_name, observed_transcript)
210 expected_items = self.prepare_responses(expected)
211 observed_items = self.prepare_responses(
212 list(self.get_important_received_items(observed_transcript))
215 if not use_serverless_ide:
216 # If the server's busy, maybe the machine's just under too much
217 # pressure to give results in a timely fashion. Doing a retry would
218 # only defer the question of what to do in that case, so instead
219 # we'll just skip.
220 self.throw_on_skip(observed_transcript)
222 # validation checks that the number of items matches and that
223 # the responses are exactly identical to what we expect
224 self.assertEqual(
225 len(expected_items),
226 len(observed_items),
227 "Wrong count. Observed this:\n"
228 + json.dumps(observed_transcript, indent=2, separators=(",", ": ")),
230 for i in range(len(expected_items)):
231 self.assertEqual(expected_items[i], observed_items[i])
233 def throw_on_skip(self, transcript: Transcript) -> None:
234 failure_messages = ["Server busy", "timed out"]
235 for entry in transcript.values():
236 received = entry.received
237 if received is None:
238 continue
239 if received.get("error"):
240 message = received["error"]["message"]
241 for failure_message in failure_messages:
242 if failure_message in message:
243 raise unittest.SkipTest(message)
245 def prepare_server_environment(self) -> None:
246 self.maxDiff = None
247 self.test_driver.write_load_config(use_serverless_ide=False)
248 self.test_driver.start_hh_server()
249 (output, err, _) = self.test_driver.run_check()
250 if "Error: Ran out of retries" in err:
251 raise unittest.SkipTest("Hack server could not be launched")
252 self.assertEqual(output.strip(), "No errors!")
254 def prepare_serverless_ide_environment(self) -> Mapping[str, str]:
255 self.maxDiff = None
256 self.test_driver.write_load_config(
257 use_serverless_ide=True, use_saved_state=False
259 naming_table_saved_state_path = (
260 self.test_driver.write_naming_table_saved_state()
262 return {"naming_table_saved_state_path": naming_table_saved_state_path}
264 def load_and_run(
265 self,
266 test_name: str,
267 variables: Mapping[str, str],
268 wait_for_server: bool = True,
269 use_serverless_ide: bool = False,
270 ) -> None:
271 test, expected = self.load_test_data(test_name, variables)
272 self.run_lsp_test(
273 test_name=test_name,
274 test=test,
275 expected=expected,
276 wait_for_server=wait_for_server,
277 use_serverless_ide=use_serverless_ide,
280 def run_spec(
281 self,
282 spec: LspTestSpec,
283 variables: Mapping[str, str],
284 wait_for_server: bool,
285 use_serverless_ide: bool,
286 ) -> None:
287 if wait_for_server:
288 # wait until hh_server is ready before starting lsp
289 self.test_driver.run_check()
290 elif use_serverless_ide:
291 self.test_driver.stop_hh_server()
293 with LspCommandProcessor.create(
294 self.test_driver.test_env
295 ) as lsp_command_processor:
296 (observed_transcript, error_details) = spec.run(
297 lsp_command_processor=lsp_command_processor, variables=variables
299 file = os.path.join(self.test_driver.template_repo, spec.name + ".sent.log")
300 text = json.dumps(
302 sent
303 for sent, _received in observed_transcript.values()
304 if sent is not None
306 indent=2,
308 with open(file, "w") as f:
309 f.write(text)
311 file = os.path.join(self.test_driver.template_repo, spec.name + ".received.log")
312 text = json.dumps(
314 received
315 for _sent, received in observed_transcript.values()
316 if received is not None
318 indent=2,
320 with open(file, "w") as f:
321 f.write(text)
323 if not use_serverless_ide:
324 # If the server's busy, maybe the machine's just under too much
325 # pressure to give results in a timely fashion. Doing a retry would
326 # only defer the question of what to do in that case, so instead
327 # we'll just skip.
328 self.throw_on_skip(observed_transcript)
330 if error_details is not None:
331 raise AssertionError(error_details)
333 def setup_php_file(self, test_php: str) -> Mapping[str, str]:
334 # We want the path to the builtins directory. This is best we can do.
335 (output, err, retcode) = self.test_driver.run_check(
336 options=["--identify-function", "2:21", "--json"],
337 stdin="<?hh\nfunction f():void {PHP_EOL;}\n",
339 if retcode == 7:
340 self.skipTest(
341 "Could not discover builtins directory -- "
342 + "got exit code 7 (either Out_of_time or Out_of_retries). "
343 + "The test machine is likely under too much load."
345 self.assertEqual(retcode, 0)
346 constants_path = json.loads(output)[0]["definition_pos"]["filename"]
347 return {
348 "hhi_path": re.sub("/constants.hhi$", "", constants_path),
349 "root_path": self.test_driver.repo_dir,
350 "php_file_uri": self.repo_file_uri(test_php),
351 "php_file": self.read_repo_file(test_php),
354 def test_init_shutdown(self) -> None:
355 self.prepare_server_environment()
357 self.load_and_run(
358 "initialize_shutdown", {"root_path": self.test_driver.repo_dir}
361 def test_serverless_ide_completion(self) -> None:
362 variables = dict(self.prepare_serverless_ide_environment())
363 variables.update(self.setup_php_file("completion.php"))
364 self.test_driver.stop_hh_server()
365 spec = (
366 self.initialize_spec(LspTestSpec("ide_completion"), use_serverless_ide=True)
367 .notification(
368 method="textDocument/didOpen",
369 params={
370 "textDocument": {
371 "uri": "${php_file_uri}",
372 "languageId": "hack",
373 "version": 1,
374 "text": "${php_file}",
378 .notification(
379 comment="Add '$x = $point1['' to test autocomplete for shapes",
380 method="textDocument/didChange",
381 params={
382 "textDocument": {"uri": "${php_file_uri}"},
383 "contentChanges": [
385 "range": {
386 "start": {"line": 22, "character": 0},
387 "end": {"line": 22, "character": 0},
389 "text": "$x = $point1['",
394 .request(
395 line=line(),
396 comment="autocomplete after user types a shape",
397 method="textDocument/completion",
398 params={
399 "textDocument": {"uri": "${php_file_uri}"},
400 "position": {"line": 22, "character": 14},
402 result={
403 "isIncomplete": False,
404 "items": [
406 "label": "'x'",
407 "kind": 12,
408 "detail": "literal",
409 "inlineDetail": "literal",
410 "sortText": "'x'",
411 "insertTextFormat": 1,
412 "textEdit": {
413 "range": {
414 "start": {"line": 22, "character": 13},
415 "end": {"line": 22, "character": 14},
417 "newText": "'x'",
419 "data": {
420 "fullname": "'x'",
421 "filename": "${root_path}/completion.php",
422 "line": 22,
423 "char": 19,
427 "label": "'y'",
428 "kind": 12,
429 "detail": "literal",
430 "inlineDetail": "literal",
431 "sortText": "'y'",
432 "insertTextFormat": 1,
433 "textEdit": {
434 "range": {
435 "start": {"line": 22, "character": 13},
436 "end": {"line": 22, "character": 14},
438 "newText": "'y'",
440 "data": {
441 "fullname": "'y'",
442 "filename": "${root_path}/completion.php",
443 "line": 22,
444 "char": 30,
449 powered_by="serverless_ide",
451 .notification(
452 comment="Add automatically closed apostrophes when typing a shape key, the way visual studio code does it",
453 method="textDocument/didChange",
454 params={
455 "textDocument": {"uri": "${php_file_uri}"},
456 "contentChanges": [
458 "range": {
459 "start": {"line": 22, "character": 0},
460 "end": {"line": 22, "character": 14},
462 "text": "$x = $point1['']",
467 .request(
468 line=line(),
469 comment="autocomplete after a shape, with VS Code automatically closed apostrophes",
470 method="textDocument/completion",
471 params={
472 "textDocument": {"uri": "${php_file_uri}"},
473 "position": {"line": 22, "character": 14},
475 result={
476 "isIncomplete": False,
477 "items": [
479 "label": "'x",
480 "kind": 12,
481 "detail": "literal",
482 "inlineDetail": "literal",
483 "sortText": "'x",
484 "insertTextFormat": 1,
485 "textEdit": {
486 "range": {
487 "start": {"line": 22, "character": 13},
488 "end": {"line": 22, "character": 13},
490 "newText": "'x",
492 "data": {
493 "fullname": "'x'",
494 "filename": "${root_path}/completion.php",
495 "line": 22,
496 "char": 19,
500 "label": "'y",
501 "kind": 12,
502 "detail": "literal",
503 "inlineDetail": "literal",
504 "sortText": "'y",
505 "insertTextFormat": 1,
506 "textEdit": {
507 "range": {
508 "start": {"line": 22, "character": 13},
509 "end": {"line": 22, "character": 13},
511 "newText": "'y",
513 "data": {
514 "fullname": "'y'",
515 "filename": "${root_path}/completion.php",
516 "line": 22,
517 "char": 30,
522 powered_by="serverless_ide",
524 .notification(
525 comment="Add '$x = <'",
526 method="textDocument/didChange",
527 params={
528 "textDocument": {"uri": "${php_file_uri}"},
529 "contentChanges": [
531 "range": {
532 "start": {"line": 3, "character": 0},
533 "end": {"line": 3, "character": 0},
535 "text": "$x = <",
540 .request(
541 line=line(),
542 comment="autocomplete after '$x = <'",
543 method="textDocument/completion",
544 params={
545 "textDocument": {"uri": "${php_file_uri}"},
546 "position": {"line": 3, "character": 6},
548 result={
549 "isIncomplete": False,
550 "items": [
552 "label": "ab:cd:alpha",
553 "kind": 7,
554 "detail": "class",
555 "inlineDetail": "class",
556 "sortText": "ab:cd:alpha",
557 "insertTextFormat": 1,
558 "textEdit": {
559 "range": {
560 "start": {"line": 3, "character": 6},
561 "end": {"line": 3, "character": 6},
563 "newText": "ab:cd:alpha",
565 "data": {"fullname": ":ab:cd:alpha"},
568 "label": "ab:cd:text",
569 "kind": 7,
570 "detail": "class",
571 "inlineDetail": "class",
572 "sortText": "ab:cd:text",
573 "insertTextFormat": 1,
574 "textEdit": {
575 "range": {
576 "start": {"line": 3, "character": 6},
577 "end": {"line": 3, "character": 6},
579 "newText": "ab:cd:text",
581 "data": {"fullname": ":ab:cd:text"},
584 "label": "xhp:enum-attribute",
585 "kind": 7,
586 "detail": "class",
587 "inlineDetail": "class",
588 "sortText": "xhp:enum-attribute",
589 "insertTextFormat": 1,
590 "textEdit": {
591 "range": {
592 "start": {"line": 3, "character": 6},
593 "end": {"line": 3, "character": 6},
595 "newText": "xhp:enum-attribute",
597 "data": {"fullname": ":xhp:enum-attribute"},
600 "label": "xhp:generic",
601 "kind": 7,
602 "detail": "class",
603 "inlineDetail": "class",
604 "sortText": "xhp:generic",
605 "insertTextFormat": 1,
606 "textEdit": {
607 "range": {
608 "start": {"line": 3, "character": 6},
609 "end": {"line": 3, "character": 6},
611 "newText": "xhp:generic",
613 "data": {"fullname": ":xhp:generic"},
617 powered_by="serverless_ide",
619 .notification(
620 comment="Add '$x = <a'",
621 method="textDocument/didChange",
622 params={
623 "textDocument": {"uri": "${php_file_uri}"},
624 "contentChanges": [
626 "range": {
627 "start": {"line": 3, "character": 0},
628 "end": {"line": 3, "character": 6},
630 "text": "$x = <a",
635 .request(
636 line=line(),
637 comment="autocomplete after '$x = <a'",
638 method="textDocument/completion",
639 params={
640 "textDocument": {"uri": "${php_file_uri}"},
641 "position": {"line": 3, "character": 7},
643 result={
644 "isIncomplete": False,
645 "items": [
647 "label": "ab:cd:alpha",
648 "kind": 7,
649 "detail": "class",
650 "inlineDetail": "class",
651 "sortText": "ab:cd:alpha",
652 "insertTextFormat": 1,
653 "textEdit": {
654 "range": {
655 "start": {"line": 3, "character": 6},
656 "end": {"line": 3, "character": 7},
658 "newText": "ab:cd:alpha",
660 "data": {"fullname": ":ab:cd:alpha"},
663 "label": "ab:cd:text",
664 "kind": 7,
665 "detail": "class",
666 "inlineDetail": "class",
667 "sortText": "ab:cd:text",
668 "insertTextFormat": 1,
669 "textEdit": {
670 "range": {
671 "start": {"line": 3, "character": 6},
672 "end": {"line": 3, "character": 7},
674 "newText": "ab:cd:text",
676 "data": {"fullname": ":ab:cd:text"},
680 powered_by="serverless_ide",
682 .notification(
683 comment="Add '$x = <ab:'",
684 method="textDocument/didChange",
685 params={
686 "textDocument": {"uri": "${php_file_uri}"},
687 "contentChanges": [
689 "range": {
690 "start": {"line": 3, "character": 0},
691 "end": {"line": 3, "character": 7},
693 "text": "$x = <ab:",
698 .request(
699 line=line(),
700 comment="autocomplete after '$x = <ab:'",
701 method="textDocument/completion",
702 params={
703 "textDocument": {"uri": "${php_file_uri}"},
704 "position": {"line": 3, "character": 9},
706 result={
707 "isIncomplete": False,
708 "items": [
710 "label": "ab:cd:alpha",
711 "kind": 7,
712 "detail": "class",
713 "inlineDetail": "class",
714 "sortText": "ab:cd:alpha",
715 "insertTextFormat": 1,
716 "textEdit": {
717 "range": {
718 "start": {"line": 3, "character": 6},
719 "end": {"line": 3, "character": 9},
721 "newText": "ab:cd:alpha",
723 "data": {"fullname": ":ab:cd:alpha"},
726 "label": "ab:cd:text",
727 "kind": 7,
728 "detail": "class",
729 "inlineDetail": "class",
730 "sortText": "ab:cd:text",
731 "insertTextFormat": 1,
732 "textEdit": {
733 "range": {
734 "start": {"line": 3, "character": 6},
735 "end": {"line": 3, "character": 9},
737 "newText": "ab:cd:text",
739 "data": {"fullname": ":ab:cd:text"},
743 powered_by="serverless_ide",
745 .notification(
746 comment="Add '$x = <ab:cd:text '",
747 method="textDocument/didChange",
748 params={
749 "textDocument": {"uri": "${php_file_uri}"},
750 "contentChanges": [
752 "range": {
753 "start": {"line": 3, "character": 0},
754 "end": {"line": 3, "character": 9},
756 "text": "$x = <ab:cd:text ",
761 .request(
762 line=line(),
763 comment="autocomplete after '$x = <ab:cd:text '",
764 method="textDocument/completion",
765 params={
766 "textDocument": {"uri": "${php_file_uri}"},
767 "position": {"line": 3, "character": 17},
769 result={
770 "isIncomplete": False,
771 "items": [
773 "label": "width",
774 "kind": 5,
775 "detail": "?int",
776 "inlineDetail": "?int",
777 "sortText": "width",
778 "insertTextFormat": 1,
779 "textEdit": {
780 "range": {
781 "start": {"line": 3, "character": 17},
782 "end": {"line": 3, "character": 17},
784 "newText": "width",
786 "data": {
787 "fullname": ":width",
788 "filename": "${root_path}/xhp_class_definitions.php",
789 "line": 5,
790 "char": 27,
791 "base_class": "\\:ab:cd:text",
795 "label": "color",
796 "kind": 5,
797 "detail": "?string",
798 "inlineDetail": "?string",
799 "sortText": "color",
800 "insertTextFormat": 1,
801 "textEdit": {
802 "range": {
803 "start": {"line": 3, "character": 17},
804 "end": {"line": 3, "character": 17},
806 "newText": "color",
808 "data": {
809 "fullname": ":color",
810 "filename": "${root_path}/xhp_class_definitions.php",
811 "line": 5,
812 "char": 13,
813 "base_class": "\\:ab:cd:text",
818 powered_by="serverless_ide",
820 .notification(
821 comment="Add '$x = <ab:cd:text w'",
822 method="textDocument/didChange",
823 params={
824 "textDocument": {"uri": "${php_file_uri}"},
825 "contentChanges": [
827 "range": {
828 "start": {"line": 3, "character": 0},
829 "end": {"line": 3, "character": 17},
831 "text": "$x = <ab:cd:text w",
836 .request(
837 line=line(),
838 comment="autocomplete after '$x = <ab:cd:text w'",
839 method="textDocument/completion",
840 params={
841 "textDocument": {"uri": "${php_file_uri}"},
842 "position": {"line": 3, "character": 18},
844 result={
845 "isIncomplete": False,
846 "items": [
848 "label": "width",
849 "kind": 5,
850 "detail": "?int",
851 "inlineDetail": "?int",
852 "sortText": "width",
853 "insertTextFormat": 1,
854 "textEdit": {
855 "range": {
856 "start": {"line": 3, "character": 17},
857 "end": {"line": 3, "character": 18},
859 "newText": "width",
861 "data": {
862 "fullname": ":width",
863 "filename": "${root_path}/xhp_class_definitions.php",
864 "line": 5,
865 "char": 27,
866 "base_class": "\\:ab:cd:text",
870 "label": "color",
871 "kind": 5,
872 "detail": "?string",
873 "inlineDetail": "?string",
874 "sortText": "color",
875 "insertTextFormat": 1,
876 "textEdit": {
877 "range": {
878 "start": {"line": 3, "character": 17},
879 "end": {"line": 3, "character": 18},
881 "newText": "color",
883 "data": {
884 "fullname": ":color",
885 "filename": "${root_path}/xhp_class_definitions.php",
886 "line": 5,
887 "char": 13,
888 "base_class": "\\:ab:cd:text",
893 powered_by="serverless_ide",
895 .notification(
896 comment="Add '$x = new :'",
897 method="textDocument/didChange",
898 params={
899 "textDocument": {"uri": "${php_file_uri}"},
900 "contentChanges": [
902 "range": {
903 "start": {"line": 3, "character": 0},
904 "end": {"line": 3, "character": 18},
906 "text": "$x = new :",
911 .request(
912 line=line(),
913 comment="autocomplete after '$x = new :'",
914 method="textDocument/completion",
915 params={
916 "textDocument": {"uri": "${php_file_uri}"},
917 "position": {"line": 3, "character": 10},
919 result={
920 "isIncomplete": False,
921 "items": [
923 "label": ":ab:cd:alpha",
924 "kind": 7,
925 "detail": "class",
926 "inlineDetail": "class",
927 "sortText": ":ab:cd:alpha",
928 "insertTextFormat": 1,
929 "textEdit": {
930 "range": {
931 "start": {"line": 3, "character": 9},
932 "end": {"line": 3, "character": 10},
934 "newText": ":ab:cd:alpha",
936 "data": {"fullname": ":ab:cd:alpha"},
939 "label": ":ab:cd:text",
940 "kind": 7,
941 "detail": "class",
942 "inlineDetail": "class",
943 "sortText": ":ab:cd:text",
944 "insertTextFormat": 1,
945 "textEdit": {
946 "range": {
947 "start": {"line": 3, "character": 9},
948 "end": {"line": 3, "character": 10},
950 "newText": ":ab:cd:text",
952 "data": {"fullname": ":ab:cd:text"},
955 "label": ":xhp:enum-attribute",
956 "kind": 7,
957 "detail": "class",
958 "inlineDetail": "class",
959 "sortText": ":xhp:enum-attribute",
960 "insertTextFormat": 1,
961 "textEdit": {
962 "range": {
963 "start": {"line": 3, "character": 9},
964 "end": {"line": 3, "character": 10},
966 "newText": ":xhp:enum-attribute",
968 "data": {"fullname": ":xhp:enum-attribute"},
971 "label": ":xhp:generic",
972 "kind": 7,
973 "detail": "class",
974 "inlineDetail": "class",
975 "sortText": ":xhp:generic",
976 "insertTextFormat": 1,
977 "textEdit": {
978 "range": {
979 "start": {"line": 3, "character": 9},
980 "end": {"line": 3, "character": 10},
982 "newText": ":xhp:generic",
984 "data": {"fullname": ":xhp:generic"},
988 powered_by="serverless_ide",
990 .notification(
991 comment="Add '$x = new :a'",
992 method="textDocument/didChange",
993 params={
994 "textDocument": {"uri": "${php_file_uri}"},
995 "contentChanges": [
997 "range": {
998 "start": {"line": 3, "character": 0},
999 "end": {"line": 3, "character": 10},
1001 "text": "$x = new :a",
1006 .request(
1007 line=line(),
1008 comment="autocomplete after '$x = new :a'",
1009 method="textDocument/completion",
1010 params={
1011 "textDocument": {"uri": "${php_file_uri}"},
1012 "position": {"line": 3, "character": 11},
1014 result={
1015 "isIncomplete": False,
1016 "items": [
1018 "label": ":ab:cd:alpha",
1019 "kind": 7,
1020 "detail": "class",
1021 "inlineDetail": "class",
1022 "sortText": ":ab:cd:alpha",
1023 "insertTextFormat": 1,
1024 "textEdit": {
1025 "range": {
1026 "start": {"line": 3, "character": 9},
1027 "end": {"line": 3, "character": 11},
1029 "newText": ":ab:cd:alpha",
1031 "data": {"fullname": ":ab:cd:alpha"},
1034 "label": ":ab:cd:text",
1035 "kind": 7,
1036 "detail": "class",
1037 "inlineDetail": "class",
1038 "sortText": ":ab:cd:text",
1039 "insertTextFormat": 1,
1040 "textEdit": {
1041 "range": {
1042 "start": {"line": 3, "character": 9},
1043 "end": {"line": 3, "character": 11},
1045 "newText": ":ab:cd:text",
1047 "data": {"fullname": ":ab:cd:text"},
1051 powered_by="serverless_ide",
1053 # Note that this request should match the result in the previous example
1054 .request(
1055 line=line(),
1056 comment="autocomplete resolving after '$x = new :a'",
1057 method="completionItem/resolve",
1058 params={
1059 "label": ":ab:cd:alpha",
1060 "kind": 7,
1061 "detail": "class",
1062 "inlineDetail": "class",
1063 "itemType": ":ab:cd:alpha",
1064 "insertText": ":ab:cd:alpha",
1065 "insertTextFormat": 1,
1066 "data": {"fullname": ":ab:cd:alpha"},
1068 result={
1069 "label": ":ab:cd:alpha",
1070 "kind": 7,
1071 "detail": "class",
1072 "inlineDetail": "class",
1073 "itemType": ":ab:cd:alpha",
1074 "documentation": {
1075 "kind": "markdown",
1076 "value": ":ab:cd:alpha docblock",
1078 "insertText": ":ab:cd:alpha",
1079 "insertTextFormat": 1,
1080 "data": {"fullname": ":ab:cd:alpha"},
1082 powered_by="serverless_ide",
1084 # Try the same thing again, but this time without "new", instead using "<xhp" style
1085 .notification(
1086 comment="Add '$x = <a'",
1087 method="textDocument/didChange",
1088 params={
1089 "textDocument": {"uri": "${php_file_uri}"},
1090 "contentChanges": [
1092 "range": {
1093 "start": {"line": 3, "character": 0},
1094 "end": {"line": 3, "character": 11},
1096 "text": "$x = <a",
1101 .request(
1102 line=line(),
1103 comment="autocomplete after '$x = <a'",
1104 method="textDocument/completion",
1105 params={
1106 "textDocument": {"uri": "${php_file_uri}"},
1107 "position": {"line": 3, "character": 7},
1109 result={
1110 "isIncomplete": False,
1111 "items": [
1113 "label": "ab:cd:alpha",
1114 "kind": 7,
1115 "detail": "class",
1116 "inlineDetail": "class",
1117 "sortText": "ab:cd:alpha",
1118 "insertTextFormat": 1,
1119 "textEdit": {
1120 "range": {
1121 "start": {"line": 3, "character": 6},
1122 "end": {"line": 3, "character": 7},
1124 "newText": "ab:cd:alpha",
1126 "data": {"fullname": ":ab:cd:alpha"},
1129 "label": "ab:cd:text",
1130 "kind": 7,
1131 "detail": "class",
1132 "inlineDetail": "class",
1133 "sortText": "ab:cd:text",
1134 "insertTextFormat": 1,
1135 "textEdit": {
1136 "range": {
1137 "start": {"line": 3, "character": 6},
1138 "end": {"line": 3, "character": 7},
1140 "newText": "ab:cd:text",
1142 "data": {"fullname": ":ab:cd:text"},
1146 powered_by="serverless_ide",
1148 .request(
1149 line=line(),
1150 comment="autocomplete resolving after '$x = <a'",
1151 method="completionItem/resolve",
1152 params={
1153 "label": "ab:cd:alpha",
1154 "kind": 7,
1155 "detail": "class",
1156 "inlineDetail": "class",
1157 "insertText": "ab:cd:alpha",
1158 "insertTextFormat": 1,
1159 "data": {"fullname": ":ab:cd:alpha"},
1161 result={
1162 "label": "ab:cd:alpha",
1163 "kind": 7,
1164 "detail": "class",
1165 "inlineDetail": "class",
1166 "documentation": {
1167 "kind": "markdown",
1168 "value": ":ab:cd:alpha docblock",
1170 "insertText": "ab:cd:alpha",
1171 "insertTextFormat": 1,
1172 "data": {"fullname": ":ab:cd:alpha"},
1174 powered_by="serverless_ide",
1176 .notification(
1177 comment="Add '$x = <ab:cd:text/>; $y = $x->'",
1178 method="textDocument/didChange",
1179 params={
1180 "textDocument": {"uri": "${php_file_uri}"},
1181 "contentChanges": [
1183 "range": {
1184 "start": {"line": 3, "character": 0},
1185 "end": {"line": 3, "character": 7},
1187 "text": "$x = <ab:cd:text/>; $y = $x->",
1192 .request(
1193 line=line(),
1194 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
1195 method="textDocument/completion",
1196 params={
1197 "textDocument": {"uri": "${php_file_uri}"},
1198 "position": {"line": 3, "character": 29},
1200 result={
1201 "isIncomplete": False,
1202 "items": [
1204 "label": ":width",
1205 "kind": 5,
1206 "detail": "?int",
1207 "inlineDetail": "?int",
1208 "sortText": ":width",
1209 "insertTextFormat": 1,
1210 "textEdit": {
1211 "range": {
1212 "start": {"line": 3, "character": 29},
1213 "end": {"line": 3, "character": 29},
1215 "newText": ":width",
1217 "data": {
1218 "fullname": ":width",
1219 "filename": "${root_path}/xhp_class_definitions.php",
1220 "line": 5,
1221 "char": 27,
1222 "base_class": "\\:ab:cd:text",
1226 "label": ":color",
1227 "kind": 5,
1228 "detail": "?string",
1229 "inlineDetail": "?string",
1230 "sortText": ":color",
1231 "insertTextFormat": 1,
1232 "textEdit": {
1233 "range": {
1234 "start": {"line": 3, "character": 29},
1235 "end": {"line": 3, "character": 29},
1237 "newText": ":color",
1239 "data": {
1240 "fullname": ":color",
1241 "filename": "${root_path}/xhp_class_definitions.php",
1242 "line": 5,
1243 "char": 13,
1244 "base_class": "\\:ab:cd:text",
1249 powered_by="serverless_ide",
1251 .notification(
1252 comment="Add '$x = <ab:cd:text/>; $y = $x->:'",
1253 method="textDocument/didChange",
1254 params={
1255 "textDocument": {"uri": "${php_file_uri}"},
1256 "contentChanges": [
1258 "range": {
1259 "start": {"line": 3, "character": 0},
1260 "end": {"line": 3, "character": 29},
1262 "text": "$x = <ab:cd:text/>; $y = $x->:",
1267 .request(
1268 line=line(),
1269 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
1270 method="textDocument/completion",
1271 params={
1272 "textDocument": {"uri": "${php_file_uri}"},
1273 "position": {"line": 3, "character": 30},
1275 result={
1276 "isIncomplete": False,
1277 "items": [
1279 "label": ":width",
1280 "kind": 5,
1281 "detail": "?int",
1282 "inlineDetail": "?int",
1283 "sortText": ":width",
1284 "insertTextFormat": 1,
1285 "textEdit": {
1286 "range": {
1287 "start": {"line": 3, "character": 29},
1288 "end": {"line": 3, "character": 30},
1290 "newText": ":width",
1292 "data": {
1293 "fullname": ":width",
1294 "filename": "${root_path}/xhp_class_definitions.php",
1295 "line": 5,
1296 "char": 27,
1297 "base_class": "\\:ab:cd:text",
1301 "label": ":color",
1302 "kind": 5,
1303 "detail": "?string",
1304 "inlineDetail": "?string",
1305 "sortText": ":color",
1306 "insertTextFormat": 1,
1307 "textEdit": {
1308 "range": {
1309 "start": {"line": 3, "character": 29},
1310 "end": {"line": 3, "character": 30},
1312 "newText": ":color",
1314 "data": {
1315 "fullname": ":color",
1316 "filename": "${root_path}/xhp_class_definitions.php",
1317 "line": 5,
1318 "char": 13,
1319 "base_class": "\\:ab:cd:text",
1324 powered_by="serverless_ide",
1326 .notification(
1327 comment="Add 'test_fun'",
1328 method="textDocument/didChange",
1329 params={
1330 "textDocument": {"uri": "${php_file_uri}"},
1331 "contentChanges": [
1333 "range": {
1334 "start": {"line": 3, "character": 0},
1335 "end": {"line": 3, "character": 30},
1337 "text": "test_fun",
1342 .request(
1343 line=line(),
1344 comment="autocomplete after 'test_fun'",
1345 method="textDocument/completion",
1346 params={
1347 "textDocument": {"uri": "${php_file_uri}"},
1348 "position": {"line": 3, "character": 8},
1350 result={
1351 "isIncomplete": False,
1352 "items": [
1354 "label": "test_function",
1355 "kind": 3,
1356 "detail": "function",
1357 "inlineDetail": "function",
1358 "sortText": "test_function",
1359 "insertTextFormat": 1,
1360 "textEdit": {
1361 "range": {
1362 "start": {"line": 3, "character": 0},
1363 "end": {"line": 3, "character": 8},
1365 "newText": "test_function",
1367 "data": {"fullname": "test_function"},
1371 powered_by="serverless_ide",
1373 .request(
1374 line=line(),
1375 comment="autocomplete resolving after 'test_fun'",
1376 method="completionItem/resolve",
1377 params={
1378 "label": "test_function",
1379 "kind": 3,
1380 "detail": "function(): void",
1381 "inlineDetail": "()",
1382 "itemType": "void",
1383 "insertText": "test_function",
1384 "insertTextFormat": 1,
1385 "data": {
1386 "filename": "${root_path}/completion.php",
1387 "line": 8,
1388 "char": 10,
1391 result={
1392 "label": "test_function",
1393 "kind": 3,
1394 "detail": "function(): void",
1395 "inlineDetail": "()",
1396 "itemType": "void",
1397 "documentation": {
1398 "kind": "markdown",
1399 "value": "test_function docblock.",
1401 "insertText": "test_function",
1402 "insertTextFormat": 1,
1403 "data": {
1404 "filename": "${root_path}/completion.php",
1405 "line": 8,
1406 "char": 10,
1409 powered_by="serverless_ide",
1411 .notification(
1412 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
1413 method="textDocument/didChange",
1414 params={
1415 "textDocument": {"uri": "${php_file_uri}"},
1416 "contentChanges": [
1418 "range": {
1419 "start": {"line": 3, "character": 0},
1420 "end": {"line": 3, "character": 8},
1422 "text": "switch (Elsa::Alonso) { case Elsa:",
1427 .request(
1428 line=line(),
1429 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
1430 method="textDocument/completion",
1431 params={
1432 "textDocument": {"uri": "${php_file_uri}"},
1433 "position": {"line": 3, "character": 34},
1435 result={"isIncomplete": False, "items": []},
1436 powered_by="serverless_ide",
1438 .notification(
1439 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
1440 method="textDocument/didChange",
1441 params={
1442 "textDocument": {"uri": "${php_file_uri}"},
1443 "contentChanges": [
1445 "range": {
1446 "start": {"line": 3, "character": 0},
1447 "end": {"line": 3, "character": 34},
1449 "text": "switch (Elsa::Alonso) { case Elsa::",
1454 .request(
1455 line=line(),
1456 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
1457 method="textDocument/completion",
1458 params={
1459 "textDocument": {"uri": "${php_file_uri}"},
1460 "position": {"line": 3, "character": 35},
1462 result={
1463 "isIncomplete": False,
1464 "items": [
1466 "label": "class",
1467 "kind": 21,
1468 "detail": "classname<this>",
1469 "inlineDetail": "classname<this>",
1470 "sortText": "class",
1471 "insertTextFormat": 1,
1472 "textEdit": {
1473 "range": {
1474 "start": {"line": 3, "character": 35},
1475 "end": {"line": 3, "character": 35},
1477 "newText": "class",
1479 "data": {
1480 "fullname": "class",
1481 "filename": "${root_path}/completion_extras.php",
1482 "line": 3,
1483 "char": 6,
1484 "base_class": "\\Elsa",
1488 "label": "Bard",
1489 "kind": 21,
1490 "detail": "Elsa",
1491 "inlineDetail": "Elsa",
1492 "sortText": "Bard",
1493 "insertTextFormat": 1,
1494 "textEdit": {
1495 "range": {
1496 "start": {"line": 3, "character": 35},
1497 "end": {"line": 3, "character": 35},
1499 "newText": "Bard",
1501 "data": {
1502 "fullname": "Bard",
1503 "filename": "${root_path}/completion_extras.php",
1504 "line": 3,
1505 "char": 12,
1506 "base_class": "\\Elsa",
1510 "label": "Alonso",
1511 "kind": 21,
1512 "detail": "Elsa",
1513 "inlineDetail": "Elsa",
1514 "sortText": "Alonso",
1515 "insertTextFormat": 1,
1516 "textEdit": {
1517 "range": {
1518 "start": {"line": 3, "character": 35},
1519 "end": {"line": 3, "character": 35},
1521 "newText": "Alonso",
1523 "data": {
1524 "fullname": "Alonso",
1525 "filename": "${root_path}/completion_extras.php",
1526 "line": 3,
1527 "char": 12,
1528 "base_class": "\\Elsa",
1532 "label": "isValid",
1533 "kind": 2,
1534 "detail": "function(mixed $value): bool",
1535 "inlineDetail": "(mixed $value)",
1536 "itemType": "bool",
1537 "sortText": "isValid",
1538 "insertText": "isValid(${1:\\$value})",
1539 "insertTextFormat": 2,
1540 "data": {
1541 "fullname": "isValid",
1542 "filename": "${hhi_path}/BuiltinEnum.hhi",
1543 "line": 46,
1544 "char": 32,
1545 "base_class": "\\Elsa",
1549 "label": "getValues",
1550 "kind": 2,
1551 "detail": "function(): dict<string, Elsa>",
1552 "inlineDetail": "()",
1553 "itemType": "dict<string, Elsa>",
1554 "sortText": "getValues",
1555 "insertText": "getValues()",
1556 "insertTextFormat": 2,
1557 "data": {
1558 "fullname": "getValues",
1559 "filename": "${hhi_path}/BuiltinEnum.hhi",
1560 "line": 33,
1561 "char": 32,
1562 "base_class": "\\Elsa",
1566 "label": "getNames",
1567 "kind": 2,
1568 "detail": "function(): dict<Elsa, string>",
1569 "inlineDetail": "()",
1570 "itemType": "dict<Elsa, string>",
1571 "sortText": "getNames",
1572 "insertText": "getNames()",
1573 "insertTextFormat": 2,
1574 "data": {
1575 "fullname": "getNames",
1576 "filename": "${hhi_path}/BuiltinEnum.hhi",
1577 "line": 41,
1578 "char": 32,
1579 "base_class": "\\Elsa",
1583 "label": "coerce",
1584 "kind": 2,
1585 "detail": "function(mixed $value): ?Elsa",
1586 "inlineDetail": "(mixed $value)",
1587 "itemType": "?Elsa",
1588 "sortText": "coerce",
1589 "insertText": "coerce(${1:\\$value})",
1590 "insertTextFormat": 2,
1591 "data": {
1592 "fullname": "coerce",
1593 "filename": "${hhi_path}/BuiltinEnum.hhi",
1594 "line": 52,
1595 "char": 32,
1596 "base_class": "\\Elsa",
1600 "label": "assertAll",
1601 "kind": 2,
1602 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
1603 "inlineDetail": "(Traversable<mixed> $values)",
1604 "itemType": "Container<Elsa>",
1605 "sortText": "assertAll",
1606 "insertText": "assertAll(${1:\\$values})",
1607 "insertTextFormat": 2,
1608 "data": {
1609 "fullname": "assertAll",
1610 "filename": "${hhi_path}/BuiltinEnum.hhi",
1611 "line": 64,
1612 "char": 32,
1613 "base_class": "\\Elsa",
1617 "label": "assert",
1618 "kind": 2,
1619 "detail": "function(mixed $value): Elsa",
1620 "inlineDetail": "(mixed $value)",
1621 "itemType": "Elsa",
1622 "sortText": "assert",
1623 "insertText": "assert(${1:\\$value})",
1624 "insertTextFormat": 2,
1625 "data": {
1626 "fullname": "assert",
1627 "filename": "${hhi_path}/BuiltinEnum.hhi",
1628 "line": 58,
1629 "char": 32,
1630 "base_class": "\\Elsa",
1635 powered_by="serverless_ide",
1637 .notification(
1638 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1639 method="textDocument/didChange",
1640 params={
1641 "textDocument": {"uri": "${php_file_uri}"},
1642 "contentChanges": [
1644 "range": {
1645 "start": {"line": 3, "character": 0},
1646 "end": {"line": 3, "character": 35},
1648 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
1653 .request(
1654 line=line(),
1655 comment="docblock resolve after 'switch (Elsa::Alonso) { case Elsa::'",
1656 method="completionItem/resolve",
1657 params={
1658 "label": "isValid",
1659 "kind": 2,
1660 "detail": "function(mixed $value): bool",
1661 "inlineDetail": "(mixed $value)",
1662 "itemType": "bool",
1663 "insertTextFormat": 1,
1664 "textEdit": {
1665 "range": {
1666 "start": {"line": 3, "character": 35},
1667 "end": {"line": 3, "character": 35},
1669 "newText": "isValid",
1671 "data": {
1672 "filename": "${hhi_path}/BuiltinEnum.hhi",
1673 "line": 46,
1674 "char": 32,
1677 result={
1678 "label": "isValid",
1679 "kind": 2,
1680 "detail": "function(mixed $value): bool",
1681 "inlineDetail": "(mixed $value)",
1682 "itemType": "bool",
1683 "documentation": {
1684 "kind": "markdown",
1685 "value": "Returns whether or not the value is defined as a constant.",
1687 "insertTextFormat": 1,
1688 "textEdit": {
1689 "range": {
1690 "start": {"line": 3, "character": 35},
1691 "end": {"line": 3, "character": 35},
1693 "newText": "isValid",
1695 "data": {
1696 "filename": "${hhi_path}/BuiltinEnum.hhi",
1697 "line": 46,
1698 "char": 32,
1701 powered_by="serverless_ide",
1703 .request(
1704 line=line(),
1705 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1706 method="textDocument/completion",
1707 params={
1708 "textDocument": {"uri": "${php_file_uri}"},
1709 "position": {"line": 3, "character": 42},
1711 result={"isIncomplete": False, "items": []},
1712 powered_by="serverless_ide",
1714 .notification(
1715 comment="Add 'TestNS\\'",
1716 method="textDocument/didChange",
1717 params={
1718 "textDocument": {"uri": "${php_file_uri}"},
1719 "contentChanges": [
1721 "range": {
1722 "start": {"line": 3, "character": 0},
1723 "end": {"line": 3, "character": 42},
1725 "text": "TestNS\\",
1730 .request(
1731 line=line(),
1732 comment="autocomplete after 'TestNS\\'",
1733 method="textDocument/completion",
1734 params={
1735 "textDocument": {"uri": "${php_file_uri}"},
1736 "position": {"line": 3, "character": 7},
1738 result={
1739 "isIncomplete": False,
1740 "items": [
1742 "label": "test_func",
1743 "kind": 3,
1744 "detail": "function",
1745 "inlineDetail": "function",
1746 "sortText": "test_func",
1747 "insertTextFormat": 1,
1748 "textEdit": {
1749 "range": {
1750 "start": {"line": 3, "character": 7},
1751 "end": {"line": 3, "character": 7},
1753 "newText": "test_func",
1755 "data": {"fullname": "TestNS\\test_func"},
1759 powered_by="serverless_ide",
1761 .notification(
1762 comment="Add '$cc = new CompletionClass(); $cc->interfa'",
1763 method="textDocument/didChange",
1764 params={
1765 "textDocument": {"uri": "${php_file_uri}"},
1766 "contentChanges": [
1768 "range": {
1769 "start": {"line": 3, "character": 0},
1770 "end": {"line": 3, "character": 7},
1772 "text": "$cc = new CompletionClass(); $cc->interfa",
1777 .request(
1778 line=line(),
1779 comment="autocomplete after '$cc = new CompletionClass(); $cc->interfa'",
1780 method="textDocument/completion",
1781 params={
1782 "textDocument": {"uri": "${php_file_uri}"},
1783 "position": {"line": 3, "character": 41},
1785 result={
1786 "isIncomplete": False,
1787 "items": [
1789 "label": "interfaceDocBlockMethod",
1790 "kind": 2,
1791 "detail": "function(): void",
1792 "inlineDetail": "()",
1793 "itemType": "void",
1794 "sortText": "interfaceDocBlockMethod",
1795 "insertText": "interfaceDocBlockMethod()",
1796 "insertTextFormat": 2,
1797 "data": {
1798 "fullname": "interfaceDocBlockMethod",
1799 "filename": "${root_path}/completion.php",
1800 "line": 18,
1801 "char": 19,
1802 "base_class": "\\CompletionClass",
1807 powered_by="serverless_ide",
1809 .request(
1810 line=line(),
1811 comment="autocomplete resolving after '$cc = new CompletionClass(); $cc->interfa'",
1812 method="completionItem/resolve",
1813 params={
1814 "label": "interfaceDocBlockMethod",
1815 "kind": 2,
1816 "detail": "function(): void",
1817 "inlineDetail": "()",
1818 "itemType": "void",
1819 "insertTextFormat": 1,
1820 "textEdit": {
1821 "range": {
1822 "start": {"line": 3, "character": 34},
1823 "end": {"line": 3, "character": 41},
1825 "newText": "interfaceDocBlockMethod",
1827 "data": {
1828 "filename": "${root_path}/completion.php",
1829 "line": 18,
1830 "char": 19,
1833 result={
1834 "label": "interfaceDocBlockMethod",
1835 "kind": 2,
1836 "detail": "function(): void",
1837 "inlineDetail": "()",
1838 "itemType": "void",
1839 "insertTextFormat": 1,
1840 "textEdit": {
1841 "range": {
1842 "start": {"line": 3, "character": 34},
1843 "end": {"line": 3, "character": 41},
1845 "newText": "interfaceDocBlockMethod",
1847 "data": {
1848 "filename": "${root_path}/completion.php",
1849 "line": 18,
1850 "char": 19,
1853 powered_by="serverless_ide",
1855 .notification(
1856 comment="Add 'DeprecatedClass::'",
1857 method="textDocument/didChange",
1858 params={
1859 "textDocument": {"uri": "${php_file_uri}"},
1860 "contentChanges": [
1862 "range": {
1863 "start": {"line": 3, "character": 0},
1864 "end": {"line": 3, "character": 41},
1866 "text": "DeprecatedClass::",
1871 .request(
1872 line=line(),
1873 comment="autocomplete after 'DeprecatedClass::'",
1874 method="textDocument/completion",
1875 params={
1876 "textDocument": {"uri": "${php_file_uri}"},
1877 "position": {"line": 3, "character": 17},
1879 result={
1880 "isIncomplete": False,
1881 "items": [
1883 "label": "class",
1884 "kind": 21,
1885 "detail": "classname<this>",
1886 "inlineDetail": "classname<this>",
1887 "sortText": "class",
1888 "insertTextFormat": 1,
1889 "textEdit": {
1890 "range": {
1891 "start": {"line": 3, "character": 17},
1892 "end": {"line": 3, "character": 17},
1894 "newText": "class",
1896 "data": {
1897 "fullname": "class",
1898 "filename": "${root_path}/completion_extras.php",
1899 "line": 8,
1900 "char": 13,
1901 "base_class": "\\DeprecatedClass",
1905 "label": "test_do_not_use",
1906 "kind": 2,
1907 "detail": "function(): void",
1908 "inlineDetail": "()",
1909 "itemType": "void",
1910 "sortText": "~test_do_not_use",
1911 "insertText": "test_do_not_use()",
1912 "insertTextFormat": 2,
1913 "data": {
1914 "fullname": "test_do_not_use",
1915 "filename": "${root_path}/completion_extras.php",
1916 "line": 12,
1917 "char": 26,
1918 "base_class": "\\DeprecatedClass",
1922 "label": "getName",
1923 "kind": 2,
1924 "detail": "function(): void",
1925 "inlineDetail": "()",
1926 "itemType": "void",
1927 "sortText": "getName",
1928 "insertText": "getName()",
1929 "insertTextFormat": 2,
1930 "data": {
1931 "fullname": "getName",
1932 "filename": "${root_path}/completion_extras.php",
1933 "line": 9,
1934 "char": 26,
1935 "base_class": "\\DeprecatedClass",
1939 "label": "getAttributes_DO_NOT_USE",
1940 "kind": 2,
1941 "detail": "function(): void",
1942 "inlineDetail": "()",
1943 "itemType": "void",
1944 "sortText": "~getAttributes_DO_NOT_USE",
1945 "insertText": "getAttributes_DO_NOT_USE()",
1946 "insertTextFormat": 2,
1947 "data": {
1948 "fullname": "getAttributes_DO_NOT_USE",
1949 "filename": "${root_path}/completion_extras.php",
1950 "line": 11,
1951 "char": 26,
1952 "base_class": "\\DeprecatedClass",
1956 "label": "__getLoader",
1957 "kind": 2,
1958 "detail": "function(): void",
1959 "inlineDetail": "()",
1960 "itemType": "void",
1961 "sortText": "~__getLoader",
1962 "insertText": "__getLoader()",
1963 "insertTextFormat": 2,
1964 "data": {
1965 "fullname": "__getLoader",
1966 "filename": "${root_path}/completion_extras.php",
1967 "line": 10,
1968 "char": 26,
1969 "base_class": "\\DeprecatedClass",
1974 powered_by="serverless_ide",
1976 .notification(
1977 comment="Add 'call_lambda(3, $m'",
1978 method="textDocument/didChange",
1979 params={
1980 "textDocument": {"uri": "${php_file_uri}"},
1981 "contentChanges": [
1983 "range": {
1984 "start": {"line": 30, "character": 0},
1985 "end": {"line": 30, "character": 0},
1987 "text": " call_lambda(3, $m",
1992 .request(
1993 line=line(),
1994 comment="autocomplete results for 'call_lambda(3, $m'",
1995 method="textDocument/completion",
1996 params={
1997 "textDocument": {"uri": "${php_file_uri}"},
1998 "position": {"line": 30, "character": 19},
2000 result={
2001 "isIncomplete": False,
2002 "items": [
2004 "label": "$mylambda",
2005 "kind": 6,
2006 "detail": "local variable",
2007 "inlineDetail": "(int $n)",
2008 "itemType": "int",
2009 "sortText": "$mylambda",
2010 "insertTextFormat": 1,
2011 "textEdit": {
2012 "range": {
2013 "start": {"line": 30, "character": 17},
2014 "end": {"line": 30, "character": 19},
2016 "newText": "$mylambda",
2018 "data": {
2019 "fullname": "$mylambda",
2020 "filename": "${root_path}/completion.php",
2021 "line": 30,
2022 "char": 15,
2027 powered_by="serverless_ide",
2029 .request(
2030 line=line(),
2031 comment="resolve autocompletion for $mylambda'",
2032 method="completionItem/resolve",
2033 params={
2034 "label": "$mylambda",
2035 "kind": 6,
2036 "detail": "local variable",
2037 "inlineDetail": "(num $n)",
2038 "itemType": "int",
2039 "insertTextFormat": 1,
2040 "textEdit": {
2041 "range": {
2042 "start": {"line": 30, "character": 17},
2043 "end": {"line": 30, "character": 19},
2045 "newText": "$mylambda",
2047 "data": {
2048 "filename": "${root_path}/completion.php",
2049 "line": 30,
2050 "char": 15,
2053 result={
2054 "label": "$mylambda",
2055 "kind": 6,
2056 "detail": "local variable",
2057 "inlineDetail": "(num $n)",
2058 "itemType": "int",
2059 "insertTextFormat": 1,
2060 "textEdit": {
2061 "range": {
2062 "start": {"line": 30, "character": 17},
2063 "end": {"line": 30, "character": 19},
2065 "newText": "$mylambda",
2067 "data": {
2068 "filename": "${root_path}/completion.php",
2069 "line": 30,
2070 "char": 15,
2073 powered_by="serverless_ide",
2075 .notification(
2076 comment="Add '<xhp:enum-attribute enum-attribute={}'",
2077 method="textDocument/didChange",
2078 params={
2079 "textDocument": {"uri": "${php_file_uri}"},
2080 "contentChanges": [
2082 "range": {
2083 "start": {"line": 3, "character": 0},
2084 "end": {"line": 3, "character": 17},
2086 "text": "<xhp:enum-attribute enum-attribute={}",
2091 .request(
2092 line=line(),
2093 comment="autocomplete after '<xhp:enum-attribute enum-attribute={'",
2094 method="textDocument/completion",
2095 params={
2096 "textDocument": {"uri": "${php_file_uri}"},
2097 "position": {"line": 3, "character": 36},
2098 "context": {"triggerKind": 2, "triggerCharacter": "{"},
2100 result={
2101 "isIncomplete": False,
2102 "items": [
2104 "label": "MyEnum::TYPE_C",
2105 "kind": 13,
2106 "detail": "enum",
2107 "inlineDetail": "enum",
2108 "sortText": "MyEnum::TYPE_C",
2109 "insertTextFormat": 1,
2110 "textEdit": {
2111 "range": {
2112 "start": {"line": 3, "character": 36},
2113 "end": {"line": 3, "character": 36},
2115 "newText": "MyEnum::TYPE_C",
2117 "data": {
2118 "fullname": "MyEnum::TYPE_C",
2119 "filename": "${root_path}/xhp_class_definitions.php",
2120 "line": 13,
2121 "char": 14,
2122 "base_class": "\\MyEnum",
2126 "label": "MyEnum::TYPE_A",
2127 "kind": 13,
2128 "detail": "enum",
2129 "inlineDetail": "enum",
2130 "sortText": "MyEnum::TYPE_A",
2131 "insertTextFormat": 1,
2132 "textEdit": {
2133 "range": {
2134 "start": {"line": 3, "character": 36},
2135 "end": {"line": 3, "character": 36},
2137 "newText": "MyEnum::TYPE_A",
2139 "data": {
2140 "fullname": "MyEnum::TYPE_A",
2141 "filename": "${root_path}/xhp_class_definitions.php",
2142 "line": 13,
2143 "char": 14,
2144 "base_class": "\\MyEnum",
2148 "label": "MyEnum::TYPE_B",
2149 "kind": 13,
2150 "detail": "enum",
2151 "inlineDetail": "enum",
2152 "sortText": "MyEnum::TYPE_B",
2153 "insertTextFormat": 1,
2154 "textEdit": {
2155 "range": {
2156 "start": {"line": 3, "character": 36},
2157 "end": {"line": 3, "character": 36},
2159 "newText": "MyEnum::TYPE_B",
2161 "data": {
2162 "fullname": "MyEnum::TYPE_B",
2163 "filename": "${root_path}/xhp_class_definitions.php",
2164 "line": 13,
2165 "char": 14,
2166 "base_class": "\\MyEnum",
2171 powered_by="serverless_ide",
2173 .notification(
2174 comment="Add '1 is strin'",
2175 method="textDocument/didChange",
2176 params={
2177 "textDocument": {"uri": "${php_file_uri}"},
2178 "contentChanges": [
2180 "range": {
2181 "start": {"line": 3, "character": 0},
2182 "end": {"line": 3, "character": 37},
2184 "text": "1 is strin",
2189 .request(
2190 line=line(),
2191 comment="autocomplete after '1 is strin'",
2192 method="textDocument/completion",
2193 params={
2194 "textDocument": {"uri": "${php_file_uri}"},
2195 "position": {"line": 3, "character": 10},
2197 result={
2198 "isIncomplete": False,
2199 "items": [
2201 "label": "string",
2202 "kind": 25,
2203 "detail": "builtin",
2204 "inlineDetail": "builtin",
2205 "documentation": {
2206 "kind": "markdown",
2207 "value": "A sequence of characters.",
2209 "sortText": "string",
2210 "insertTextFormat": 1,
2211 "textEdit": {
2212 "range": {
2213 "start": {"line": 3, "character": 5},
2214 "end": {"line": 3, "character": 10},
2216 "newText": "string",
2218 "data": {"fullname": "string"},
2221 "label": "StringBuffer",
2222 "kind": 7,
2223 "detail": "class",
2224 "inlineDetail": "class",
2225 "sortText": "StringBuffer",
2226 "insertTextFormat": 1,
2227 "textEdit": {
2228 "range": {
2229 "start": {"line": 3, "character": 5},
2230 "end": {"line": 3, "character": 10},
2232 "newText": "StringBuffer",
2234 "data": {"fullname": "StringBuffer"},
2237 "label": "Stringish",
2238 "kind": 8,
2239 "detail": "interface",
2240 "inlineDetail": "interface",
2241 "sortText": "Stringish",
2242 "insertTextFormat": 1,
2243 "textEdit": {
2244 "range": {
2245 "start": {"line": 3, "character": 5},
2246 "end": {"line": 3, "character": 10},
2248 "newText": "Stringish",
2250 "data": {"fullname": "Stringish"},
2253 "label": "StringishObject",
2254 "kind": 8,
2255 "detail": "interface",
2256 "inlineDetail": "interface",
2257 "sortText": "StringishObject",
2258 "insertTextFormat": 1,
2259 "textEdit": {
2260 "range": {
2261 "start": {"line": 3, "character": 5},
2262 "end": {"line": 3, "character": 10},
2264 "newText": "StringishObject",
2266 "data": {"fullname": "StringishObject"},
2270 powered_by="serverless_ide",
2272 .request(
2273 line=line(),
2274 comment="autocomplete resolving after '1 is strin'",
2275 method="completionItem/resolve",
2276 params={
2277 "data": {"fullname": "string"},
2278 "detail": "builtin",
2279 "documentation": {
2280 "kind": "markdown",
2281 "value": "A sequence of characters.",
2283 "inlineDetail": "builtin",
2284 "insertText": "string",
2285 "insertTextFormat": 1,
2286 "kind": 25,
2287 "label": "string",
2288 "sortText": "string",
2290 result={
2291 "data": {"fullname": "string"},
2292 "detail": "builtin",
2293 "documentation": {
2294 "kind": "markdown",
2295 "value": "A sequence of characters.",
2297 "inlineDetail": "builtin",
2298 "insertText": "string",
2299 "insertTextFormat": 1,
2300 "kind": 25,
2301 "label": "string",
2302 "sortText": "string",
2304 powered_by="serverless_ide",
2306 .request(line=line(), method="shutdown", params={}, result=None)
2307 .notification(method="exit", params={})
2309 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
2311 def test_serverless_ide_completion_legacy(self) -> None:
2312 variables = dict(self.prepare_serverless_ide_environment())
2313 variables.update(self.setup_php_file("completion.php"))
2314 self.test_driver.stop_hh_server()
2316 spec = (
2317 self.initialize_spec(
2318 LspTestSpec("serverless_ide_completion_legacy"), use_serverless_ide=True
2320 .notification(
2321 method="textDocument/didOpen",
2322 params={
2323 "textDocument": {
2324 "uri": "${php_file_uri}",
2325 "languageId": "hack",
2326 "version": 1,
2327 "text": "${php_file}",
2331 .notification(
2332 comment="Add '$x = <'",
2333 method="textDocument/didChange",
2334 params={
2335 "textDocument": {"uri": "${php_file_uri}"},
2336 "contentChanges": [
2338 "range": {
2339 "start": {"line": 3, "character": 0},
2340 "end": {"line": 3, "character": 0},
2342 "text": "$x = <",
2347 .request(
2348 line=line(),
2349 comment="autocomplete after '$x = <' legacy",
2350 method="textDocument/completion",
2351 params={
2352 "textDocument": {"uri": "${php_file_uri}"},
2353 "position": {"line": 3, "character": 6},
2355 result={
2356 "isIncomplete": False,
2357 "items": [
2359 "label": "ab:cd:alpha",
2360 "kind": 7,
2361 "detail": "class",
2362 "inlineDetail": "class",
2363 "sortText": "ab:cd:alpha",
2364 "insertTextFormat": 1,
2365 "textEdit": {
2366 "range": {
2367 "start": {"line": 3, "character": 6},
2368 "end": {"line": 3, "character": 6},
2370 "newText": "ab:cd:alpha",
2372 "data": {"fullname": ":ab:cd:alpha"},
2375 "label": "ab:cd:text",
2376 "kind": 7,
2377 "detail": "class",
2378 "inlineDetail": "class",
2379 "sortText": "ab:cd:text",
2380 "insertTextFormat": 1,
2381 "textEdit": {
2382 "range": {
2383 "start": {"line": 3, "character": 6},
2384 "end": {"line": 3, "character": 6},
2386 "newText": "ab:cd:text",
2388 "data": {"fullname": ":ab:cd:text"},
2391 "label": "xhp:enum-attribute",
2392 "kind": 7,
2393 "detail": "class",
2394 "inlineDetail": "class",
2395 "sortText": "xhp:enum-attribute",
2396 "insertTextFormat": 1,
2397 "textEdit": {
2398 "range": {
2399 "start": {"line": 3, "character": 6},
2400 "end": {"line": 3, "character": 6},
2402 "newText": "xhp:enum-attribute",
2404 "data": {"fullname": ":xhp:enum-attribute"},
2407 "label": "xhp:generic",
2408 "kind": 7,
2409 "detail": "class",
2410 "inlineDetail": "class",
2411 "sortText": "xhp:generic",
2412 "insertTextFormat": 1,
2413 "textEdit": {
2414 "range": {
2415 "start": {"line": 3, "character": 6},
2416 "end": {"line": 3, "character": 6},
2418 "newText": "xhp:generic",
2420 "data": {"fullname": ":xhp:generic"},
2424 powered_by="serverless_ide",
2426 .notification(
2427 comment="Add '$x = <a'",
2428 method="textDocument/didChange",
2429 params={
2430 "textDocument": {"uri": "${php_file_uri}"},
2431 "contentChanges": [
2433 "range": {
2434 "start": {"line": 3, "character": 0},
2435 "end": {"line": 3, "character": 6},
2437 "text": "$x = <a",
2442 .request(
2443 line=line(),
2444 comment="autocomplete after '$x = <a'",
2445 method="textDocument/completion",
2446 params={
2447 "textDocument": {"uri": "${php_file_uri}"},
2448 "position": {"line": 3, "character": 7},
2450 result={
2451 "isIncomplete": False,
2452 "items": [
2454 "label": "ab:cd:alpha",
2455 "kind": 7,
2456 "detail": "class",
2457 "inlineDetail": "class",
2458 "sortText": "ab:cd:alpha",
2459 "insertTextFormat": 1,
2460 "textEdit": {
2461 "range": {
2462 "start": {"line": 3, "character": 6},
2463 "end": {"line": 3, "character": 7},
2465 "newText": "ab:cd:alpha",
2467 "data": {"fullname": ":ab:cd:alpha"},
2470 "label": "ab:cd:text",
2471 "kind": 7,
2472 "detail": "class",
2473 "inlineDetail": "class",
2474 "sortText": "ab:cd:text",
2475 "insertTextFormat": 1,
2476 "textEdit": {
2477 "range": {
2478 "start": {"line": 3, "character": 6},
2479 "end": {"line": 3, "character": 7},
2481 "newText": "ab:cd:text",
2483 "data": {"fullname": ":ab:cd:text"},
2487 powered_by="serverless_ide",
2489 .notification(
2490 comment="Add '$x = <ab:'",
2491 method="textDocument/didChange",
2492 params={
2493 "textDocument": {"uri": "${php_file_uri}"},
2494 "contentChanges": [
2496 "range": {
2497 "start": {"line": 3, "character": 0},
2498 "end": {"line": 3, "character": 7},
2500 "text": "$x = <ab:",
2505 .request(
2506 line=line(),
2507 comment="autocomplete after '$x = <ab:'.",
2508 method="textDocument/completion",
2509 params={
2510 "textDocument": {"uri": "${php_file_uri}"},
2511 "position": {"line": 3, "character": 9},
2513 result={
2514 "isIncomplete": False,
2515 "items": [
2517 "label": "ab:cd:alpha",
2518 "kind": 7,
2519 "detail": "class",
2520 "inlineDetail": "class",
2521 "sortText": "ab:cd:alpha",
2522 "insertTextFormat": 1,
2523 "textEdit": {
2524 "range": {
2525 "start": {"line": 3, "character": 6},
2526 "end": {"line": 3, "character": 9},
2528 "newText": "ab:cd:alpha",
2530 "data": {"fullname": ":ab:cd:alpha"},
2533 "label": "ab:cd:text",
2534 "kind": 7,
2535 "detail": "class",
2536 "inlineDetail": "class",
2537 "sortText": "ab:cd:text",
2538 "insertTextFormat": 1,
2539 "textEdit": {
2540 "range": {
2541 "start": {"line": 3, "character": 6},
2542 "end": {"line": 3, "character": 9},
2544 "newText": "ab:cd:text",
2546 "data": {"fullname": ":ab:cd:text"},
2550 powered_by="serverless_ide",
2552 .notification(
2553 comment="Add '$x = <ab:cd:text '",
2554 method="textDocument/didChange",
2555 params={
2556 "textDocument": {"uri": "${php_file_uri}"},
2557 "contentChanges": [
2559 "range": {
2560 "start": {"line": 3, "character": 0},
2561 "end": {"line": 3, "character": 9},
2563 "text": "$x = <ab:cd:text ",
2568 .request(
2569 line=line(),
2570 comment="autocomplete after '$x = <ab:cd:text '",
2571 method="textDocument/completion",
2572 params={
2573 "textDocument": {"uri": "${php_file_uri}"},
2574 "position": {"line": 3, "character": 17},
2576 result={
2577 "isIncomplete": False,
2578 "items": [
2580 "label": "width",
2581 "kind": 5,
2582 "detail": "?int",
2583 "inlineDetail": "?int",
2584 "sortText": "width",
2585 "insertTextFormat": 1,
2586 "textEdit": {
2587 "range": {
2588 "start": {"line": 3, "character": 17},
2589 "end": {"line": 3, "character": 17},
2591 "newText": "width",
2593 "data": {
2594 "fullname": ":width",
2595 "filename": "${root_path}/xhp_class_definitions.php",
2596 "line": 5,
2597 "char": 27,
2598 "base_class": "\\:ab:cd:text",
2602 "label": "color",
2603 "kind": 5,
2604 "detail": "?string",
2605 "inlineDetail": "?string",
2606 "sortText": "color",
2607 "insertTextFormat": 1,
2608 "textEdit": {
2609 "range": {
2610 "start": {"line": 3, "character": 17},
2611 "end": {"line": 3, "character": 17},
2613 "newText": "color",
2615 "data": {
2616 "fullname": ":color",
2617 "filename": "${root_path}/xhp_class_definitions.php",
2618 "line": 5,
2619 "char": 13,
2620 "base_class": "\\:ab:cd:text",
2625 powered_by="serverless_ide",
2627 .notification(
2628 comment="Add '$x = <ab:cd:text w'",
2629 method="textDocument/didChange",
2630 params={
2631 "textDocument": {"uri": "${php_file_uri}"},
2632 "contentChanges": [
2634 "range": {
2635 "start": {"line": 3, "character": 0},
2636 "end": {"line": 3, "character": 17},
2638 "text": "$x = <ab:cd:text w",
2643 .request(
2644 line=line(),
2645 comment="autocomplete after '$x = <ab:cd:text w'",
2646 method="textDocument/completion",
2647 params={
2648 "textDocument": {"uri": "${php_file_uri}"},
2649 "position": {"line": 3, "character": 18},
2651 result={
2652 "isIncomplete": False,
2653 "items": [
2655 "label": "width",
2656 "kind": 5,
2657 "detail": "?int",
2658 "inlineDetail": "?int",
2659 "sortText": "width",
2660 "insertTextFormat": 1,
2661 "textEdit": {
2662 "range": {
2663 "start": {"line": 3, "character": 17},
2664 "end": {"line": 3, "character": 18},
2666 "newText": "width",
2668 "data": {
2669 "fullname": ":width",
2670 "filename": "${root_path}/xhp_class_definitions.php",
2671 "line": 5,
2672 "char": 27,
2673 "base_class": "\\:ab:cd:text",
2677 "label": "color",
2678 "kind": 5,
2679 "detail": "?string",
2680 "inlineDetail": "?string",
2681 "sortText": "color",
2682 "insertTextFormat": 1,
2683 "textEdit": {
2684 "range": {
2685 "start": {"line": 3, "character": 17},
2686 "end": {"line": 3, "character": 18},
2688 "newText": "color",
2690 "data": {
2691 "fullname": ":color",
2692 "filename": "${root_path}/xhp_class_definitions.php",
2693 "line": 5,
2694 "char": 13,
2695 "base_class": "\\:ab:cd:text",
2700 powered_by="serverless_ide",
2702 .notification(
2703 comment="Add '$x = new :''",
2704 method="textDocument/didChange",
2705 params={
2706 "textDocument": {"uri": "${php_file_uri}"},
2707 "contentChanges": [
2709 "range": {
2710 "start": {"line": 3, "character": 0},
2711 "end": {"line": 3, "character": 18},
2713 "text": "$x = new :",
2718 .request(
2719 line=line(),
2720 comment="autocomplete after '$x = new :'",
2721 method="textDocument/completion",
2722 params={
2723 "textDocument": {"uri": "${php_file_uri}"},
2724 "position": {"line": 3, "character": 10},
2726 result={
2727 "isIncomplete": False,
2728 "items": [
2730 "label": ":ab:cd:alpha",
2731 "kind": 7,
2732 "detail": "class",
2733 "inlineDetail": "class",
2734 "sortText": ":ab:cd:alpha",
2735 "insertTextFormat": 1,
2736 "textEdit": {
2737 "range": {
2738 "start": {"line": 3, "character": 9},
2739 "end": {"line": 3, "character": 10},
2741 "newText": ":ab:cd:alpha",
2743 "data": {"fullname": ":ab:cd:alpha"},
2746 "label": ":ab:cd:text",
2747 "kind": 7,
2748 "detail": "class",
2749 "inlineDetail": "class",
2750 "sortText": ":ab:cd:text",
2751 "insertTextFormat": 1,
2752 "textEdit": {
2753 "range": {
2754 "start": {"line": 3, "character": 9},
2755 "end": {"line": 3, "character": 10},
2757 "newText": ":ab:cd:text",
2759 "data": {"fullname": ":ab:cd:text"},
2762 "label": ":xhp:enum-attribute",
2763 "kind": 7,
2764 "detail": "class",
2765 "inlineDetail": "class",
2766 "sortText": ":xhp:enum-attribute",
2767 "insertTextFormat": 1,
2768 "textEdit": {
2769 "range": {
2770 "start": {"line": 3, "character": 9},
2771 "end": {"line": 3, "character": 10},
2773 "newText": ":xhp:enum-attribute",
2775 "data": {"fullname": ":xhp:enum-attribute"},
2778 "label": ":xhp:generic",
2779 "kind": 7,
2780 "detail": "class",
2781 "inlineDetail": "class",
2782 "sortText": ":xhp:generic",
2783 "insertTextFormat": 1,
2784 "textEdit": {
2785 "range": {
2786 "start": {"line": 3, "character": 9},
2787 "end": {"line": 3, "character": 10},
2789 "newText": ":xhp:generic",
2791 "data": {"fullname": ":xhp:generic"},
2795 powered_by="serverless_ide",
2797 .notification(
2798 comment="Add '$x = new :a'",
2799 method="textDocument/didChange",
2800 params={
2801 "textDocument": {"uri": "${php_file_uri}"},
2802 "contentChanges": [
2804 "range": {
2805 "start": {"line": 3, "character": 0},
2806 "end": {"line": 3, "character": 10},
2808 "text": "$x = new :a",
2813 .request(
2814 line=line(),
2815 comment="autocomplete after '$x = new :a'",
2816 method="textDocument/completion",
2817 params={
2818 "textDocument": {"uri": "${php_file_uri}"},
2819 "position": {"line": 3, "character": 11},
2821 result={
2822 "isIncomplete": False,
2823 "items": [
2825 "label": ":ab:cd:alpha",
2826 "kind": 7,
2827 "detail": "class",
2828 "inlineDetail": "class",
2829 "sortText": ":ab:cd:alpha",
2830 "insertTextFormat": 1,
2831 "textEdit": {
2832 "range": {
2833 "start": {"line": 3, "character": 9},
2834 "end": {"line": 3, "character": 11},
2836 "newText": ":ab:cd:alpha",
2838 "data": {"fullname": ":ab:cd:alpha"},
2841 "label": ":ab:cd:text",
2842 "kind": 7,
2843 "detail": "class",
2844 "inlineDetail": "class",
2845 "sortText": ":ab:cd:text",
2846 "insertTextFormat": 1,
2847 "textEdit": {
2848 "range": {
2849 "start": {"line": 3, "character": 9},
2850 "end": {"line": 3, "character": 11},
2852 "newText": ":ab:cd:text",
2854 "data": {"fullname": ":ab:cd:text"},
2858 powered_by="serverless_ide",
2860 # Note that this request sent should match the result given in the previous example
2861 .request(
2862 line=line(),
2863 comment="autocomplete resolving after '$x = new :a'",
2864 method="completionItem/resolve",
2865 params={
2866 "label": ":ab:cd:alpha",
2867 "kind": 7,
2868 "detail": "class",
2869 "inlineDetail": "class",
2870 "itemType": ":ab:cd:alpha",
2871 "insertText": ":ab:cd:alpha",
2872 "insertTextFormat": 1,
2873 "data": {"fullname": ":ab:cd:alpha"},
2875 result={
2876 "label": ":ab:cd:alpha",
2877 "kind": 7,
2878 "detail": "class",
2879 "inlineDetail": "class",
2880 "itemType": ":ab:cd:alpha",
2881 "documentation": {
2882 "kind": "markdown",
2883 "value": ":ab:cd:alpha docblock",
2885 "insertText": ":ab:cd:alpha",
2886 "insertTextFormat": 1,
2887 "data": {"fullname": ":ab:cd:alpha"},
2889 powered_by="serverless_ide",
2891 .notification(
2892 comment="Add '$x = <ab:cd:text/>; $y = $x->'",
2893 method="textDocument/didChange",
2894 params={
2895 "textDocument": {"uri": "${php_file_uri}"},
2896 "contentChanges": [
2898 "range": {
2899 "start": {"line": 3, "character": 0},
2900 "end": {"line": 3, "character": 11},
2902 "text": "$x = <ab:cd:text/>; $y = $x->",
2907 .request(
2908 line=line(),
2909 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
2910 method="textDocument/completion",
2911 params={
2912 "textDocument": {"uri": "${php_file_uri}"},
2913 "position": {"line": 3, "character": 29},
2915 result={
2916 "isIncomplete": False,
2917 "items": [
2919 "label": ":width",
2920 "kind": 5,
2921 "detail": "?int",
2922 "inlineDetail": "?int",
2923 "sortText": ":width",
2924 "insertTextFormat": 1,
2925 "textEdit": {
2926 "range": {
2927 "start": {"line": 3, "character": 29},
2928 "end": {"line": 3, "character": 29},
2930 "newText": ":width",
2932 "data": {
2933 "fullname": ":width",
2934 "filename": "${root_path}/xhp_class_definitions.php",
2935 "line": 5,
2936 "char": 27,
2937 "base_class": "\\:ab:cd:text",
2941 "label": ":color",
2942 "kind": 5,
2943 "detail": "?string",
2944 "inlineDetail": "?string",
2945 "sortText": ":color",
2946 "insertTextFormat": 1,
2947 "textEdit": {
2948 "range": {
2949 "start": {"line": 3, "character": 29},
2950 "end": {"line": 3, "character": 29},
2952 "newText": ":color",
2954 "data": {
2955 "fullname": ":color",
2956 "filename": "${root_path}/xhp_class_definitions.php",
2957 "line": 5,
2958 "char": 13,
2959 "base_class": "\\:ab:cd:text",
2964 powered_by="serverless_ide",
2966 .notification(
2967 comment="Add '$x = <ab:cd:text/>; $y = $x->:'",
2968 method="textDocument/didChange",
2969 params={
2970 "textDocument": {"uri": "${php_file_uri}"},
2971 "contentChanges": [
2973 "range": {
2974 "start": {"line": 3, "character": 0},
2975 "end": {"line": 3, "character": 29},
2977 "text": "$x = <ab:cd:text/>; $y = $x->:",
2982 .request(
2983 line=line(),
2984 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
2985 method="textDocument/completion",
2986 params={
2987 "textDocument": {"uri": "${php_file_uri}"},
2988 "position": {"line": 3, "character": 30},
2990 result={
2991 "isIncomplete": False,
2992 "items": [
2994 "label": ":width",
2995 "kind": 5,
2996 "detail": "?int",
2997 "inlineDetail": "?int",
2998 "sortText": ":width",
2999 "insertTextFormat": 1,
3000 "textEdit": {
3001 "range": {
3002 "start": {"line": 3, "character": 29},
3003 "end": {"line": 3, "character": 30},
3005 "newText": ":width",
3007 "data": {
3008 "fullname": ":width",
3009 "filename": "${root_path}/xhp_class_definitions.php",
3010 "line": 5,
3011 "char": 27,
3012 "base_class": "\\:ab:cd:text",
3016 "label": ":color",
3017 "kind": 5,
3018 "detail": "?string",
3019 "inlineDetail": "?string",
3020 "sortText": ":color",
3021 "insertTextFormat": 1,
3022 "textEdit": {
3023 "range": {
3024 "start": {"line": 3, "character": 29},
3025 "end": {"line": 3, "character": 30},
3027 "newText": ":color",
3029 "data": {
3030 "fullname": ":color",
3031 "filename": "${root_path}/xhp_class_definitions.php",
3032 "line": 5,
3033 "char": 13,
3034 "base_class": "\\:ab:cd:text",
3039 powered_by="serverless_ide",
3041 .notification(
3042 comment="Add 'test_fun'",
3043 method="textDocument/didChange",
3044 params={
3045 "textDocument": {"uri": "${php_file_uri}"},
3046 "contentChanges": [
3048 "range": {
3049 "start": {"line": 3, "character": 0},
3050 "end": {"line": 3, "character": 30},
3052 "text": "test_fun",
3057 .request(
3058 line=line(),
3059 comment="autocomplete after 'test_fun'",
3060 method="textDocument/completion",
3061 params={
3062 "textDocument": {"uri": "${php_file_uri}"},
3063 "position": {"line": 3, "character": 8},
3065 result={
3066 "isIncomplete": False,
3067 "items": [
3069 "label": "test_function",
3070 "kind": 3,
3071 "detail": "function",
3072 "inlineDetail": "function",
3073 "sortText": "test_function",
3074 "insertTextFormat": 1,
3075 "textEdit": {
3076 "range": {
3077 "start": {"line": 3, "character": 0},
3078 "end": {"line": 3, "character": 8},
3080 "newText": "test_function",
3082 "data": {"fullname": "test_function"},
3086 powered_by="serverless_ide",
3088 .request(
3089 line=line(),
3090 comment="autocomplete resolving after 'test_fun'",
3091 method="completionItem/resolve",
3092 params={
3093 "label": "test_function",
3094 "kind": 3,
3095 "detail": "function(): void",
3096 "inlineDetail": "()",
3097 "itemType": "void",
3098 "insertText": "test_function",
3099 "insertTextFormat": 1,
3100 "data": {
3101 "filename": "${root_path}/completion.php",
3102 "line": 8,
3103 "char": 10,
3106 result={
3107 "label": "test_function",
3108 "kind": 3,
3109 "detail": "function(): void",
3110 "inlineDetail": "()",
3111 "itemType": "void",
3112 "documentation": {
3113 "kind": "markdown",
3114 "value": "test_function docblock.",
3116 "insertText": "test_function",
3117 "insertTextFormat": 1,
3118 "data": {
3119 "filename": "${root_path}/completion.php",
3120 "line": 8,
3121 "char": 10,
3124 powered_by="serverless_ide",
3126 .notification(
3127 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
3128 method="textDocument/didChange",
3129 params={
3130 "textDocument": {"uri": "${php_file_uri}"},
3131 "contentChanges": [
3133 "range": {
3134 "start": {"line": 3, "character": 0},
3135 "end": {"line": 3, "character": 8},
3137 "text": "switch (Elsa::Alonso) { case Elsa:",
3142 .request(
3143 line=line(),
3144 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
3145 method="textDocument/completion",
3146 params={
3147 "textDocument": {"uri": "${php_file_uri}"},
3148 "position": {"line": 3, "character": 34},
3150 result={"isIncomplete": False, "items": []},
3151 powered_by="serverless_ide",
3153 .notification(
3154 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
3155 method="textDocument/didChange",
3156 params={
3157 "textDocument": {"uri": "${php_file_uri}"},
3158 "contentChanges": [
3160 "range": {
3161 "start": {"line": 3, "character": 0},
3162 "end": {"line": 3, "character": 34},
3164 "text": "switch (Elsa::Alonso) { case Elsa::",
3169 .request(
3170 line=line(),
3171 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
3172 method="textDocument/completion",
3173 params={
3174 "textDocument": {"uri": "${php_file_uri}"},
3175 "position": {"line": 3, "character": 35},
3177 result={
3178 "isIncomplete": False,
3179 "items": [
3181 "label": "class",
3182 "kind": 21,
3183 "detail": "classname<this>",
3184 "inlineDetail": "classname<this>",
3185 "sortText": "class",
3186 "insertTextFormat": 1,
3187 "textEdit": {
3188 "range": {
3189 "start": {"line": 3, "character": 35},
3190 "end": {"line": 3, "character": 35},
3192 "newText": "class",
3194 "data": {
3195 "fullname": "class",
3196 "filename": "${root_path}/completion_extras.php",
3197 "line": 3,
3198 "char": 6,
3199 "base_class": "\\Elsa",
3203 "label": "Bard",
3204 "kind": 21,
3205 "detail": "Elsa",
3206 "inlineDetail": "Elsa",
3207 "sortText": "Bard",
3208 "insertTextFormat": 1,
3209 "textEdit": {
3210 "range": {
3211 "start": {"line": 3, "character": 35},
3212 "end": {"line": 3, "character": 35},
3214 "newText": "Bard",
3216 "data": {
3217 "fullname": "Bard",
3218 "filename": "${root_path}/completion_extras.php",
3219 "line": 3,
3220 "char": 12,
3221 "base_class": "\\Elsa",
3225 "label": "Alonso",
3226 "kind": 21,
3227 "detail": "Elsa",
3228 "inlineDetail": "Elsa",
3229 "sortText": "Alonso",
3230 "insertTextFormat": 1,
3231 "textEdit": {
3232 "range": {
3233 "start": {"line": 3, "character": 35},
3234 "end": {"line": 3, "character": 35},
3236 "newText": "Alonso",
3238 "data": {
3239 "fullname": "Alonso",
3240 "filename": "${root_path}/completion_extras.php",
3241 "line": 3,
3242 "char": 12,
3243 "base_class": "\\Elsa",
3247 "label": "isValid",
3248 "kind": 2,
3249 "detail": "function(mixed $value): bool",
3250 "inlineDetail": "(mixed $value)",
3251 "itemType": "bool",
3252 "sortText": "isValid",
3253 "insertText": "isValid(${1:\\$value})",
3254 "insertTextFormat": 2,
3255 "data": {
3256 "fullname": "isValid",
3257 "filename": "${hhi_path}/BuiltinEnum.hhi",
3258 "line": 46,
3259 "char": 32,
3260 "base_class": "\\Elsa",
3264 "label": "getValues",
3265 "kind": 2,
3266 "detail": "function(): dict<string, Elsa>",
3267 "inlineDetail": "()",
3268 "itemType": "dict<string, Elsa>",
3269 "sortText": "getValues",
3270 "insertText": "getValues()",
3271 "insertTextFormat": 2,
3272 "data": {
3273 "fullname": "getValues",
3274 "filename": "${hhi_path}/BuiltinEnum.hhi",
3275 "line": 33,
3276 "char": 32,
3277 "base_class": "\\Elsa",
3281 "label": "getNames",
3282 "kind": 2,
3283 "detail": "function(): dict<Elsa, string>",
3284 "inlineDetail": "()",
3285 "itemType": "dict<Elsa, string>",
3286 "sortText": "getNames",
3287 "insertText": "getNames()",
3288 "insertTextFormat": 2,
3289 "data": {
3290 "fullname": "getNames",
3291 "filename": "${hhi_path}/BuiltinEnum.hhi",
3292 "line": 41,
3293 "char": 32,
3294 "base_class": "\\Elsa",
3298 "label": "coerce",
3299 "kind": 2,
3300 "detail": "function(mixed $value): ?Elsa",
3301 "inlineDetail": "(mixed $value)",
3302 "itemType": "?Elsa",
3303 "sortText": "coerce",
3304 "insertText": "coerce(${1:\\$value})",
3305 "insertTextFormat": 2,
3306 "data": {
3307 "fullname": "coerce",
3308 "filename": "${hhi_path}/BuiltinEnum.hhi",
3309 "line": 52,
3310 "char": 32,
3311 "base_class": "\\Elsa",
3315 "label": "assertAll",
3316 "kind": 2,
3317 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
3318 "inlineDetail": "(Traversable<mixed> $values)",
3319 "itemType": "Container<Elsa>",
3320 "sortText": "assertAll",
3321 "insertText": "assertAll(${1:\\$values})",
3322 "insertTextFormat": 2,
3323 "data": {
3324 "fullname": "assertAll",
3325 "filename": "${hhi_path}/BuiltinEnum.hhi",
3326 "line": 64,
3327 "char": 32,
3328 "base_class": "\\Elsa",
3332 "label": "assert",
3333 "kind": 2,
3334 "detail": "function(mixed $value): Elsa",
3335 "inlineDetail": "(mixed $value)",
3336 "itemType": "Elsa",
3337 "sortText": "assert",
3338 "insertText": "assert(${1:\\$value})",
3339 "insertTextFormat": 2,
3340 "data": {
3341 "fullname": "assert",
3342 "filename": "${hhi_path}/BuiltinEnum.hhi",
3343 "line": 58,
3344 "char": 32,
3345 "base_class": "\\Elsa",
3350 powered_by="serverless_ide",
3352 .notification(
3353 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
3354 method="textDocument/didChange",
3355 params={
3356 "textDocument": {"uri": "${php_file_uri}"},
3357 "contentChanges": [
3359 "range": {
3360 "start": {"line": 3, "character": 0},
3361 "end": {"line": 3, "character": 35},
3363 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
3368 .request(
3369 line=line(),
3370 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
3371 method="textDocument/completion",
3372 params={
3373 "textDocument": {"uri": "${php_file_uri}"},
3374 "position": {"line": 3, "character": 42},
3376 result={"isIncomplete": False, "items": []},
3377 powered_by="serverless_ide",
3379 .notification(
3380 comment="Add 'DeprecatedClass::'",
3381 method="textDocument/didChange",
3382 params={
3383 "textDocument": {"uri": "${php_file_uri}"},
3384 "contentChanges": [
3386 "range": {
3387 "start": {"line": 3, "character": 0},
3388 "end": {"line": 3, "character": 41},
3390 "text": "DeprecatedClass::",
3395 .request(
3396 line=line(),
3397 comment="autocomplete after 'DeprecatedClass::'",
3398 method="textDocument/completion",
3399 params={
3400 "textDocument": {"uri": "${php_file_uri}"},
3401 "position": {"line": 3, "character": 17},
3403 result={
3404 "isIncomplete": False,
3405 "items": [
3407 "label": "class",
3408 "kind": 21,
3409 "detail": "classname<this>",
3410 "inlineDetail": "classname<this>",
3411 "sortText": "class",
3412 "insertTextFormat": 1,
3413 "textEdit": {
3414 "range": {
3415 "start": {"line": 3, "character": 17},
3416 "end": {"line": 3, "character": 17},
3418 "newText": "class",
3420 "data": {
3421 "fullname": "class",
3422 "filename": "${root_path}/completion_extras.php",
3423 "line": 8,
3424 "char": 13,
3425 "base_class": "\\DeprecatedClass",
3429 "label": "test_do_not_use",
3430 "kind": 2,
3431 "detail": "function(): void",
3432 "inlineDetail": "()",
3433 "itemType": "void",
3434 "sortText": "~test_do_not_use",
3435 "insertText": "test_do_not_use()",
3436 "insertTextFormat": 2,
3437 "data": {
3438 "fullname": "test_do_not_use",
3439 "filename": "${root_path}/completion_extras.php",
3440 "line": 12,
3441 "char": 26,
3442 "base_class": "\\DeprecatedClass",
3446 "label": "getName",
3447 "kind": 2,
3448 "detail": "function(): void",
3449 "inlineDetail": "()",
3450 "itemType": "void",
3451 "sortText": "getName",
3452 "insertText": "getName()",
3453 "insertTextFormat": 2,
3454 "data": {
3455 "fullname": "getName",
3456 "filename": "${root_path}/completion_extras.php",
3457 "line": 9,
3458 "char": 26,
3459 "base_class": "\\DeprecatedClass",
3463 "label": "getAttributes_DO_NOT_USE",
3464 "kind": 2,
3465 "detail": "function(): void",
3466 "inlineDetail": "()",
3467 "itemType": "void",
3468 "sortText": "~getAttributes_DO_NOT_USE",
3469 "insertText": "getAttributes_DO_NOT_USE()",
3470 "insertTextFormat": 2,
3471 "data": {
3472 "fullname": "getAttributes_DO_NOT_USE",
3473 "filename": "${root_path}/completion_extras.php",
3474 "line": 11,
3475 "char": 26,
3476 "base_class": "\\DeprecatedClass",
3480 "label": "__getLoader",
3481 "kind": 2,
3482 "detail": "function(): void",
3483 "inlineDetail": "()",
3484 "itemType": "void",
3485 "sortText": "~__getLoader",
3486 "insertText": "__getLoader()",
3487 "insertTextFormat": 2,
3488 "data": {
3489 "fullname": "__getLoader",
3490 "filename": "${root_path}/completion_extras.php",
3491 "line": 10,
3492 "char": 26,
3493 "base_class": "\\DeprecatedClass",
3498 powered_by="serverless_ide",
3500 .request(line=line(), method="shutdown", params={}, result=None)
3501 .notification(method="exit", params={})
3503 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3505 def test_serverless_ide_definition(self) -> None:
3506 variables = dict(self.prepare_serverless_ide_environment())
3507 variables.update(self.setup_php_file("definition.php"))
3508 self.test_driver.stop_hh_server()
3510 spec = (
3511 self.initialize_spec(
3512 LspTestSpec("serverless_ide_definition"), use_serverless_ide=True
3514 .notification(
3515 method="textDocument/didOpen",
3516 params={
3517 "textDocument": {
3518 "uri": "${php_file_uri}",
3519 "languageId": "hack",
3520 "version": 1,
3521 "text": "${php_file}",
3525 .request(
3526 line=line(),
3527 comment="call to `b_definition`",
3528 method="textDocument/definition",
3529 params={
3530 "textDocument": {"uri": "${php_file_uri}"},
3531 "position": {"line": 3, "character": 10},
3533 result=[
3535 "uri": "file://${root_path}/definition.php",
3536 "range": {
3537 "start": {"line": 6, "character": 9},
3538 "end": {"line": 6, "character": 21},
3540 "title": "b_definition",
3543 powered_by="serverless_ide",
3545 .request(
3546 line=line(),
3547 comment="call to `new BB(1)`",
3548 method="textDocument/definition",
3549 params={
3550 "textDocument": {"uri": "${php_file_uri}"},
3551 "position": {"line": 29, "character": 13},
3553 result=[
3555 "uri": "file://${root_path}/definition.php",
3556 "range": {
3557 "start": {"line": 11, "character": 18},
3558 "end": {"line": 11, "character": 29},
3560 "title": "BB::__construct",
3563 powered_by="serverless_ide",
3565 .request(
3566 line=line(),
3567 comment="call to `new CC(1)`",
3568 method="textDocument/definition",
3569 params={
3570 "textDocument": {"uri": "${php_file_uri}"},
3571 "position": {"line": 30, "character": 13},
3573 result=[
3575 "uri": "file://${root_path}/definition.php",
3576 "range": {
3577 "start": {"line": 14, "character": 6},
3578 "end": {"line": 14, "character": 8},
3580 "title": "CC",
3583 "uri": "file://${root_path}/definition.php",
3584 "range": {
3585 "start": {"line": 11, "character": 18},
3586 "end": {"line": 11, "character": 29},
3588 "title": "BB::__construct",
3591 powered_by="serverless_ide",
3593 .request(
3594 line=line(),
3595 comment="call to `new DD(1)`",
3596 method="textDocument/definition",
3597 params={
3598 "textDocument": {"uri": "${php_file_uri}"},
3599 "position": {"line": 31, "character": 13},
3601 result=[
3603 "uri": "file://${root_path}/definition.php",
3604 "range": {
3605 "start": {"line": 17, "character": 6},
3606 "end": {"line": 17, "character": 8},
3608 "title": "DD",
3611 "uri": "file://${root_path}/definition.php",
3612 "range": {
3613 "start": {"line": 11, "character": 18},
3614 "end": {"line": 11, "character": 29},
3616 "title": "BB::__construct",
3619 powered_by="serverless_ide",
3621 .request(
3622 line=line(),
3623 comment="call to `new EE(1)`",
3624 method="textDocument/definition",
3625 params={
3626 "textDocument": {"uri": "${php_file_uri}"},
3627 "position": {"line": 32, "character": 13},
3629 result=[
3631 "uri": "file://${root_path}/definition.php",
3632 "range": {
3633 "start": {"line": 21, "character": 18},
3634 "end": {"line": 21, "character": 29},
3636 "title": "EE::__construct",
3639 powered_by="serverless_ide",
3641 .request(
3642 line=line(),
3643 comment="call to `new FF(1)`",
3644 method="textDocument/definition",
3645 params={
3646 "textDocument": {"uri": "${php_file_uri}"},
3647 "position": {"line": 33, "character": 13},
3649 result=[
3651 "uri": "file://${root_path}/definition.php",
3652 "range": {
3653 "start": {"line": 26, "character": 6},
3654 "end": {"line": 26, "character": 8},
3656 "title": "FF",
3659 powered_by="serverless_ide",
3661 .request(
3662 line=line(),
3663 comment="call to `new TakesString(HasString::MyString)`",
3664 method="textDocument/definition",
3665 params={
3666 "textDocument": {"uri": "${php_file_uri}"},
3667 "position": {"line": 45, "character": 23},
3669 result=[
3671 "uri": "file://${root_path}/definition.php",
3672 "range": {
3673 "start": {"line": 40, "character": 6},
3674 "end": {"line": 40, "character": 15},
3676 "title": "HasString",
3679 powered_by="serverless_ide",
3681 .notification(
3682 comment="make local, unsaved change to the file",
3683 method="textDocument/didChange",
3684 params={
3685 "textDocument": {"uri": "${php_file_uri}", "version": 2},
3686 "contentChanges": [
3688 "text": "test",
3689 "range": {
3690 "start": {"line": 3, "character": 9},
3691 "end": {"line": 3, "character": 21},
3697 .request(
3698 line=line(),
3699 comment="call to `test` instead of `b_definition`",
3700 method="textDocument/definition",
3701 params={
3702 "textDocument": {"uri": "${php_file_uri}"},
3703 "position": {"line": 3, "character": 10},
3705 result=[
3707 "uri": "file://${root_path}/definition.php",
3708 "range": {
3709 "start": {"line": 28, "character": 9},
3710 "end": {"line": 28, "character": 13},
3712 "title": "test",
3715 powered_by="serverless_ide",
3717 .request(line=line(), method="shutdown", params={}, result=None)
3718 .notification(method="exit", params={})
3720 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3722 def test_serverless_ide_overridden_definition(self) -> None:
3723 variables = dict(self.prepare_serverless_ide_environment())
3724 variables.update(self.setup_php_file("override.php"))
3725 self.test_driver.stop_hh_server()
3727 spec = (
3728 self.initialize_spec(
3729 LspTestSpec("serverless_ide_overridden_definition"),
3730 use_serverless_ide=True,
3732 .notification(
3733 method="textDocument/didOpen",
3734 params={
3735 "textDocument": {
3736 "uri": "${php_file_uri}",
3737 "languageId": "hack",
3738 "version": 1,
3739 "text": "${php_file}",
3743 .request(
3744 line=line(),
3745 comment="find overridden method from trait. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3746 method="textDocument/definition",
3747 params={
3748 "textDocument": {"uri": "${php_file_uri}"},
3749 "position": {"line": 13, "character": 5},
3751 result=[
3753 "uri": "file://${root_path}/override.php",
3754 "range": {
3755 "start": {"line": 7, "character": 18},
3756 "end": {"line": 7, "character": 21},
3758 "title": "MyTrait::foo",
3761 powered_by="serverless_ide",
3763 .request(
3764 line=line(),
3765 comment="find overridden static method. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3766 method="textDocument/definition",
3767 params={
3768 "textDocument": {"uri": "${php_file_uri}"},
3769 "position": {"line": 26, "character": 5},
3771 result=[
3773 "uri": "file://${root_path}/override.php",
3774 "range": {
3775 "start": {"line": 23, "character": 25},
3776 "end": {"line": 23, "character": 28},
3778 "title": "C2::bar",
3781 powered_by="serverless_ide",
3783 .request(
3784 line=line(),
3785 comment="find overridden interface method",
3786 method="textDocument/definition",
3787 params={
3788 "textDocument": {"uri": "${php_file_uri}"},
3789 "position": {"line": 35, "character": 5},
3791 result=[
3793 "uri": "file://${root_path}/override.php",
3794 "range": {
3795 "start": {"line": 32, "character": 18},
3796 "end": {"line": 32, "character": 22},
3798 "title": "I1::quux",
3801 powered_by="serverless_ide",
3803 .request(line=line(), method="shutdown", params={}, result=None)
3804 .notification(method="exit", params={})
3806 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3808 def test_serverless_ide_document_symbol(self) -> None:
3809 variables = dict(self.prepare_serverless_ide_environment())
3810 variables.update(self.setup_php_file("definition.php"))
3811 self.test_driver.stop_hh_server()
3813 spec = (
3814 self.initialize_spec(
3815 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide=True
3817 .notification(
3818 method="textDocument/didOpen",
3819 params={
3820 "textDocument": {
3821 "uri": "${php_file_uri}",
3822 "languageId": "hack",
3823 "version": 1,
3824 "text": "${php_file}",
3828 .request(
3829 line=line(),
3830 comment="documentSymbol call",
3831 method="textDocument/documentSymbol",
3832 params={"textDocument": {"uri": "${php_file_uri}"}},
3833 result=[
3835 "name": "First",
3836 "kind": 14,
3837 "location": {
3838 "uri": "file://${root_path}/definition.php",
3839 "range": {
3840 "start": {"line": 50, "character": 18},
3841 "end": {"line": 50, "character": 47},
3844 "containerName": "MyEnumClass",
3847 "name": "MyEnumClass",
3848 "kind": 10,
3849 "location": {
3850 "uri": "file://${root_path}/definition.php",
3851 "range": {
3852 "start": {"line": 49, "character": 0},
3853 "end": {"line": 52, "character": 1},
3858 "name": "testClassMemberInsideConstructorInvocation",
3859 "kind": 12,
3860 "location": {
3861 "uri": "file://${root_path}/definition.php",
3862 "range": {
3863 "start": {"line": 44, "character": 0},
3864 "end": {"line": 46, "character": 1},
3869 "name": "MyString",
3870 "kind": 14,
3871 "location": {
3872 "uri": "file://${root_path}/definition.php",
3873 "range": {
3874 "start": {"line": 41, "character": 8},
3875 "end": {"line": 41, "character": 29},
3878 "containerName": "HasString",
3881 "name": "HasString",
3882 "kind": 5,
3883 "location": {
3884 "uri": "file://${root_path}/definition.php",
3885 "range": {
3886 "start": {"line": 40, "character": 0},
3887 "end": {"line": 42, "character": 1},
3892 "name": "__construct",
3893 "kind": 6,
3894 "location": {
3895 "uri": "file://${root_path}/definition.php",
3896 "range": {
3897 "start": {"line": 37, "character": 2},
3898 "end": {"line": 37, "character": 43},
3901 "containerName": "TakesString",
3904 "name": "TakesString",
3905 "kind": 5,
3906 "location": {
3907 "uri": "file://${root_path}/definition.php",
3908 "range": {
3909 "start": {"line": 36, "character": 0},
3910 "end": {"line": 38, "character": 1},
3915 "name": "FF",
3916 "kind": 5,
3917 "location": {
3918 "uri": "file://${root_path}/definition.php",
3919 "range": {
3920 "start": {"line": 26, "character": 0},
3921 "end": {"line": 26, "character": 11},
3926 "name": "__construct",
3927 "kind": 6,
3928 "location": {
3929 "uri": "file://${root_path}/definition.php",
3930 "range": {
3931 "start": {"line": 21, "character": 2},
3932 "end": {"line": 23, "character": 3},
3935 "containerName": "EE",
3938 "name": "EE",
3939 "kind": 5,
3940 "location": {
3941 "uri": "file://${root_path}/definition.php",
3942 "range": {
3943 "start": {"line": 20, "character": 0},
3944 "end": {"line": 24, "character": 1},
3949 "name": "CC",
3950 "kind": 5,
3951 "location": {
3952 "uri": "file://${root_path}/definition.php",
3953 "range": {
3954 "start": {"line": 14, "character": 0},
3955 "end": {"line": 15, "character": 1},
3960 "name": "__construct",
3961 "kind": 6,
3962 "location": {
3963 "uri": "file://${root_path}/definition.php",
3964 "range": {
3965 "start": {"line": 11, "character": 2},
3966 "end": {"line": 11, "character": 40},
3969 "containerName": "BB",
3972 "name": "BB",
3973 "kind": 5,
3974 "location": {
3975 "uri": "file://${root_path}/definition.php",
3976 "range": {
3977 "start": {"line": 10, "character": 0},
3978 "end": {"line": 12, "character": 1},
3983 "name": "a_definition",
3984 "kind": 12,
3985 "location": {
3986 "uri": "file://${root_path}/definition.php",
3987 "range": {
3988 "start": {"line": 2, "character": 0},
3989 "end": {"line": 4, "character": 1},
3994 "name": "b_definition",
3995 "kind": 12,
3996 "location": {
3997 "uri": "file://${root_path}/definition.php",
3998 "range": {
3999 "start": {"line": 6, "character": 0},
4000 "end": {"line": 8, "character": 1},
4005 "name": "DD",
4006 "kind": 5,
4007 "location": {
4008 "uri": "file://${root_path}/definition.php",
4009 "range": {
4010 "start": {"line": 17, "character": 0},
4011 "end": {"line": 18, "character": 1},
4016 "name": "test",
4017 "kind": 12,
4018 "location": {
4019 "uri": "file://${root_path}/definition.php",
4020 "range": {
4021 "start": {"line": 28, "character": 0},
4022 "end": {"line": 34, "character": 1},
4027 "name": "MyEnumClassKind",
4028 "kind": 5,
4029 "location": {
4030 "uri": "file://${root_path}/definition.php",
4031 "range": {
4032 "start": {"line": 48, "character": 0},
4033 "end": {"line": 48, "character": 24},
4038 "name": "Second",
4039 "kind": 14,
4040 "location": {
4041 "uri": "${php_file_uri}",
4042 "range": {
4043 "start": {"line": 51, "character": 18},
4044 "end": {"line": 51, "character": 48},
4047 "containerName": "MyEnumClass",
4050 powered_by="serverless_ide",
4052 .request(line=line(), method="shutdown", params={}, result=None)
4053 .notification(method="exit", params={})
4055 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4057 def initialize_spec(
4058 self,
4059 spec: LspTestSpec,
4060 use_serverless_ide: bool,
4061 supports_status: bool = False, # does the caller wish to see all status messages?
4062 supports_init: bool = False, # do we wish to interact with init, rather than waiting for init ok?
4063 ) -> LspTestSpec:
4064 if use_serverless_ide:
4065 initialization_options = {
4066 "namingTableSavedStatePath": "${naming_table_saved_state_path}",
4067 "namingTableSavedStateTestDelay": 0.0,
4069 if supports_init:
4070 # A small delay, since otherwise init completes immediately
4071 # This isn't very racy. All we need is a tiny delay so that
4072 # other things which are in the queue get processed, rather
4073 # than continuing synchronously
4074 initialization_options["namingTableSavedStateTestDelay"] = 0.5
4075 else:
4076 initialization_options = {}
4078 window_capabilities = {}
4079 if supports_status:
4080 window_capabilities["status"] = {"dynamicRegistration": False}
4082 spec = spec.ignore_notifications(method="telemetry/event").request(
4083 line=line(),
4084 method="initialize",
4085 params={
4086 "initializationOptions": initialization_options,
4087 "processId": None,
4088 "rootPath": "${root_path}",
4089 "capabilities": {
4090 "window": window_capabilities,
4091 "textDocument": {
4092 "completion": {"completionItem": {"snippetSupport": True}}
4096 result={
4097 "capabilities": {
4098 "textDocumentSync": {
4099 "openClose": True,
4100 "change": 2,
4101 "willSave": False,
4102 "willSaveWaitUntil": True,
4103 "save": {"includeText": False},
4105 "hoverProvider": True,
4106 "completionProvider": {
4107 "resolveProvider": True,
4108 "triggerCharacters": [
4109 "$",
4110 ">",
4111 "\\",
4112 ":",
4113 "<",
4114 "[",
4115 "'",
4116 '"',
4117 "{",
4118 "#",
4121 "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
4122 "definitionProvider": True,
4123 "typeDefinitionProvider": True,
4124 "referencesProvider": True,
4125 "documentHighlightProvider": True,
4126 "documentSymbolProvider": True,
4127 "workspaceSymbolProvider": True,
4128 "codeActionProvider": True,
4129 "documentFormattingProvider": True,
4130 "documentRangeFormattingProvider": True,
4131 "documentOnTypeFormattingProvider": {
4132 "firstTriggerCharacter": ";",
4133 "moreTriggerCharacter": ["}"],
4135 "renameProvider": True,
4136 "implementationProvider": True,
4137 "typeCoverageProvider": True,
4138 "rageProvider": True,
4142 if use_serverless_ide:
4143 spec = spec.wait_for_server_request(
4144 method="client/registerCapability",
4145 params={
4146 "registrations": [
4148 "id": "did-change-watched-files",
4149 "method": "workspace/didChangeWatchedFiles",
4150 "registerOptions": {
4151 "watchers": [
4153 "globPattern": "**/*.{php,phpt,hack,hackpartial,hck,hh,hhi,xhp}",
4154 "kind": 7,
4161 result=None,
4163 if not supports_status:
4164 spec = spec.ignore_status_diagnostics(True)
4166 if use_serverless_ide and not supports_init:
4167 spec = spec.wait_for_notification(
4168 comment="wait for sIDE to finish init",
4169 method="telemetry/event",
4170 params={"type": 4, "message": "[client-ide] Finished init: ok"},
4173 return spec
4175 def test_serverless_ide_type_definition(self) -> None:
4176 variables = dict(self.prepare_serverless_ide_environment())
4177 variables.update(self.setup_php_file("type_definition.php"))
4178 self.test_driver.stop_hh_server()
4180 spec = (
4181 self.initialize_spec(
4182 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide=True
4184 .notification(
4185 method="textDocument/didOpen",
4186 params={
4187 "textDocument": {
4188 "uri": "${php_file_uri}",
4189 "languageId": "hack",
4190 "version": 1,
4191 "text": "${php_file}",
4195 .request(
4196 line=line(),
4197 comment="Conditional Type Definition of HH or II",
4198 method="textDocument/typeDefinition",
4199 params={
4200 "textDocument": {"uri": "${php_file_uri}"},
4201 "position": {"line": 32, "character": 2},
4203 result=[
4205 "uri": "${php_file_uri}",
4206 "range": {
4207 "start": {"line": 2, "character": 6},
4208 "end": {"line": 2, "character": 8},
4210 "title": "\\HH",
4213 "uri": "${php_file_uri}",
4214 "range": {
4215 "start": {"line": 12, "character": 6},
4216 "end": {"line": 12, "character": 8},
4218 "title": "\\LL",
4221 powered_by="serverless_ide",
4223 .request(
4224 line=line(),
4225 comment="Standard Class Definition",
4226 method="textDocument/typeDefinition",
4227 params={
4228 "textDocument": {"uri": "${php_file_uri}"},
4229 "position": {"line": 40, "character": 2},
4231 result=[
4233 "uri": "${php_file_uri}",
4234 "range": {
4235 "start": {"line": 2, "character": 6},
4236 "end": {"line": 2, "character": 8},
4238 "title": "\\HH",
4241 powered_by="serverless_ide",
4243 .request(
4244 line=line(),
4245 comment="Class Type Definition with Casting",
4246 method="textDocument/typeDefinition",
4247 params={
4248 "textDocument": {"uri": "${php_file_uri}"},
4249 "position": {"line": 41, "character": 2},
4251 result=[
4253 "uri": "${php_file_uri}",
4254 "range": {
4255 "start": {"line": 2, "character": 6},
4256 "end": {"line": 2, "character": 8},
4258 "title": "\\HH",
4261 powered_by="serverless_ide",
4263 .request(
4264 line=line(),
4265 comment="Primitive Type Definition",
4266 method="textDocument/typeDefinition",
4267 params={
4268 "textDocument": {"uri": "${php_file_uri}"},
4269 "position": {"line": 42, "character": 2},
4271 result=[],
4272 powered_by="serverless_ide",
4274 .request(
4275 line=line(),
4276 comment="Function Return Type Definition",
4277 method="textDocument/typeDefinition",
4278 params={
4279 "textDocument": {"uri": "${php_file_uri}"},
4280 "position": {"line": 43, "character": 2},
4282 result=[
4284 "uri": "${php_file_uri}",
4285 "range": {
4286 "start": {"line": 12, "character": 6},
4287 "end": {"line": 12, "character": 8},
4289 "title": "\\LL",
4292 powered_by="serverless_ide",
4294 .request(
4295 line=line(),
4296 comment="Function definition with primitive return type",
4297 method="textDocument/typeDefinition",
4298 params={
4299 "textDocument": {"uri": "${php_file_uri}"},
4300 "position": {"line": 44, "character": 2},
4302 result=[
4304 "uri": "${php_file_uri}",
4305 "range": {
4306 "start": {"line": 22, "character": 9},
4307 "end": {"line": 22, "character": 29},
4309 "title": "(function(): int)",
4312 powered_by="serverless_ide",
4314 .request(line=line(), method="shutdown", params={}, result=None)
4315 .notification(method="exit", params={})
4317 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4319 def test_serverless_ide_hover(self) -> None:
4320 variables = dict(self.prepare_serverless_ide_environment())
4321 variables.update(self.setup_php_file("hover.php"))
4322 self.test_driver.stop_hh_server()
4324 spec = (
4325 self.initialize_spec(
4326 LspTestSpec("serverless_ide_hover"), use_serverless_ide=True
4328 .notification(
4329 method="textDocument/didOpen",
4330 params={
4331 "textDocument": {
4332 "uri": "${php_file_uri}",
4333 "languageId": "hack",
4334 "version": 1,
4335 "text": "${php_file}",
4339 .request(
4340 line=line(),
4341 comment="hover over function invocation",
4342 method="textDocument/hover",
4343 params={
4344 "textDocument": {"uri": "${php_file_uri}"},
4345 "position": {"line": 3, "character": 16},
4347 result={
4348 "contents": [
4349 {"language": "hack", "value": "int"},
4350 "A comment describing b_hover.",
4352 "range": {
4353 "start": {"line": 3, "character": 9},
4354 "end": {"line": 3, "character": 16},
4357 powered_by="serverless_ide",
4359 .request(
4360 line=line(),
4361 comment="hover over string literal outside call",
4362 method="textDocument/hover",
4363 params={
4364 "textDocument": {"uri": "${php_file_uri}"},
4365 "position": {"line": 25, "character": 12}, # 9 - 16
4367 result={"contents": [{"language": "hack", "value": "string"}]},
4368 powered_by="serverless_ide",
4370 .request(
4371 line=line(),
4372 comment="hover over string literal inside call",
4373 method="textDocument/hover",
4374 params={
4375 "textDocument": {"uri": "${php_file_uri}"},
4376 "position": {"line": 26, "character": 20}, # 16 - 29
4378 result={
4379 "contents": [
4380 {"language": "hack", "value": "string"},
4381 {"language": "hack", "value": "Parameter: $s"},
4384 powered_by="serverless_ide",
4386 .request(
4387 line=line(),
4388 comment="hover over int literal inside call",
4389 method="textDocument/hover",
4390 params={
4391 "textDocument": {"uri": "${php_file_uri}"},
4392 "position": {"line": 26, "character": 32}, # 31 - 33
4394 result={
4395 "contents": [
4396 {"language": "hack", "value": "int"},
4397 {"language": "hack", "value": "Parameter: $i"},
4400 powered_by="serverless_ide",
4402 .request(
4403 line=line(),
4404 comment="hover over constant reference",
4405 method="textDocument/hover",
4406 params={
4407 "textDocument": {"uri": "${php_file_uri}"},
4408 "position": {"line": 15, "character": 19},
4410 result={
4411 "contents": [
4412 {"language": "hack", "value": "THE_ANSWER"},
4413 "A comment describing THE_ANSWER",
4414 "int THE_ANSWER = 42",
4416 "range": {
4417 "start": {"line": 15, "character": 9},
4418 "end": {"line": 15, "character": 19},
4421 powered_by="serverless_ide",
4423 .request(
4424 line=line(),
4425 comment="hover over whitespace",
4426 method="textDocument/hover",
4427 params={
4428 "textDocument": {"uri": "${php_file_uri}"},
4429 "position": {"line": 3, "character": 1},
4431 result=None,
4432 powered_by="serverless_ide",
4434 .request(
4435 line=line(),
4436 comment="hover over a keyword",
4437 method="textDocument/hover",
4438 params={
4439 "textDocument": {"uri": "${php_file_uri}"},
4440 "position": {"line": 2, "character": 1},
4442 result=None,
4443 powered_by="serverless_ide",
4445 .request(
4446 line=line(),
4447 comment="hover over a comment",
4448 method="textDocument/hover",
4449 params={
4450 "textDocument": {"uri": "${php_file_uri}"},
4451 "position": {"line": 1, "character": 4},
4453 result=None,
4454 powered_by="serverless_ide",
4456 .request(
4457 line=line(),
4458 comment="hover past the end of a line",
4459 method="textDocument/hover",
4460 params={
4461 "textDocument": {"uri": "${php_file_uri}"},
4462 "position": {"line": 3, "character": 100},
4464 result=None,
4465 powered_by="serverless_ide",
4467 .request(
4468 line=line(),
4469 comment="hover past the end of a file",
4470 method="textDocument/hover",
4471 params={
4472 "textDocument": {"uri": "${php_file_uri}"},
4473 "position": {"line": 300, "character": 0},
4475 result=None,
4476 powered_by="serverless_ide",
4478 .request(
4479 line=line(),
4480 comment="hover over class with copyright docblock",
4481 method="textDocument/hover",
4482 params={
4483 "textDocument": {"uri": "${php_file_uri}"},
4484 "position": {"line": 37, "character": 15},
4486 result={
4487 "contents": [
4488 {"language": "hack", "value": "final class CopyrightClass"},
4489 "Testing copyright removal",
4491 "range": {
4492 "start": {"line": 37, "character": 2},
4493 "end": {"line": 37, "character": 16},
4496 powered_by="serverless_ide",
4498 .request(
4499 line=line(),
4500 comment="hover over class with generated docblock",
4501 method="textDocument/hover",
4502 params={
4503 "textDocument": {"uri": "${php_file_uri}"},
4504 "position": {"line": 58, "character": 15},
4506 result={
4507 "contents": [
4508 {"language": "hack", "value": "final class GeneratedClass"},
4509 "Testing generated text removal",
4511 "range": {
4512 "start": {"line": 58, "character": 2},
4513 "end": {"line": 58, "character": 16},
4516 powered_by="serverless_ide",
4518 .request(
4519 line=line(),
4520 comment="hover over an primitive attribute in an xhp literal",
4521 method="textDocument/hover",
4522 params={
4523 "textDocument": {"uri": "${php_file_uri}"},
4524 "position": {"line": 62, "character": 25},
4526 result={
4527 "contents": [
4528 {"language": "hack", "value": "public ?string name"},
4529 ":xhp:enum-attribute::name docblock",
4531 "range": {
4532 "start": {"line": 62, "character": 22},
4533 "end": {"line": 62, "character": 26},
4536 powered_by="serverless_ide",
4538 .request(
4539 line=line(),
4540 comment="hover over a nonprimitive attribute in an xhp literal",
4541 method="textDocument/hover",
4542 params={
4543 "textDocument": {"uri": "${php_file_uri}"},
4544 "position": {"line": 62, "character": 36},
4546 result={
4547 "contents": [
4548 {"language": "hack", "value": "public ?MyEnum enum-attribute"}
4550 "range": {
4551 "start": {"line": 62, "character": 33},
4552 "end": {"line": 62, "character": 47},
4555 powered_by="serverless_ide",
4557 .request(
4558 line=line(),
4559 comment="hover over a generic attribute in an xhp literal",
4560 method="textDocument/hover",
4561 params={
4562 "textDocument": {"uri": "${php_file_uri}"},
4563 "position": {"line": 63, "character": 16},
4565 result={
4566 "contents": [
4567 {"language": "hack", "value": "public ?ID<EntSomething> id"}
4569 "range": {
4570 "start": {"line": 63, "character": 15},
4571 "end": {"line": 63, "character": 17},
4574 powered_by="serverless_ide",
4576 .notification(
4577 comment="Add '<xhp:enum-attribute name' to test hover for incomplete xhp attribute",
4578 method="textDocument/didChange",
4579 params={
4580 "textDocument": {"uri": "${php_file_uri}"},
4581 "contentChanges": [
4583 "range": {
4584 "start": {"line": 69, "character": 0},
4585 "end": {"line": 69, "character": 0},
4587 "text": "<xhp:enum-attribute name",
4592 .request(
4593 line=line(),
4594 comment="hover over an attribute in an xhp literal without a value",
4595 method="textDocument/hover",
4596 params={
4597 "textDocument": {"uri": "${php_file_uri}"},
4598 "position": {"line": 69, "character": 22},
4600 result={
4601 "contents": [
4602 {"language": "hack", "value": "public ?string name"},
4603 ":xhp:enum-attribute::name docblock",
4605 "range": {
4606 "start": {"line": 69, "character": 20},
4607 "end": {"line": 69, "character": 24},
4610 powered_by="serverless_ide",
4612 .request(line=line(), method="shutdown", params={}, result=None)
4613 .notification(method="exit", params={})
4615 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4617 def test_serverless_ide_file_touched_on_disk(self) -> None:
4618 variables = dict(self.prepare_serverless_ide_environment())
4619 variables.update(self.setup_php_file("hover.php"))
4620 self.test_driver.stop_hh_server()
4622 spec = (
4623 self.initialize_spec(
4624 LspTestSpec("serverless_ide_file_on_disk_change"),
4625 use_serverless_ide=True,
4627 .notification(
4628 method="textDocument/didOpen",
4629 params={
4630 "textDocument": {
4631 "uri": "${php_file_uri}",
4632 "languageId": "hack",
4633 "version": 1,
4634 "text": "${php_file}",
4638 .notification(
4639 method="workspace/didChangeWatchedFiles",
4640 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
4642 .wait_for_notification(
4643 comment="wait for sIDE to process file change",
4644 method="telemetry/event",
4645 params={
4646 "type": 4,
4647 "message": "[client-ide] Done processing file changes",
4650 .request(
4651 line=line(),
4652 method="textDocument/hover",
4653 params={
4654 "textDocument": {"uri": "${php_file_uri}"},
4655 "position": {"line": 3, "character": 16},
4657 result={
4658 "contents": [
4659 {"language": "hack", "value": "int"},
4660 "A comment describing b_hover.",
4662 "range": {
4663 "start": {"line": 3, "character": 9},
4664 "end": {"line": 3, "character": 16},
4667 powered_by="serverless_ide",
4669 .request(line=line(), method="shutdown", params={}, result=None)
4670 .notification(method="exit", params={})
4672 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4674 def test_serverless_ide_file_hover_with_errors(self) -> None:
4675 variables = dict(self.prepare_serverless_ide_environment())
4676 variables.update(self.setup_php_file("hover_with_errors.php"))
4677 self.test_driver.stop_hh_server()
4679 spec = (
4680 self.initialize_spec(
4681 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide=True
4683 .notification(
4684 method="textDocument/didOpen",
4685 params={
4686 "textDocument": {
4687 "uri": "${php_file_uri}",
4688 "languageId": "hack",
4689 "version": 1,
4690 "text": "${php_file}",
4694 .notification(
4695 method="workspace/didChangeWatchedFiles",
4696 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
4698 .wait_for_notification(
4699 comment="wait for sIDE to process file change",
4700 method="telemetry/event",
4701 params={
4702 "type": 4,
4703 "message": "[client-ide] Done processing file changes",
4706 .request(
4707 line=line(),
4708 comment="Totally normal hover",
4709 method="textDocument/hover",
4710 params={
4711 "textDocument": {"uri": "${php_file_uri}"},
4712 "position": {"line": 14, "character": 37},
4714 result={
4715 "contents": [
4717 "language": "hack",
4718 "value": "public static function staticMethod(string $z): void",
4720 'During testing, we\'ll remove the "public" tag from this '
4721 "method\n"
4722 "to ensure that we can still get IDE services",
4723 "Full name: `HoverWithErrorsClass::staticMethod`",
4725 "range": {
4726 "end": {"character": 39, "line": 14},
4727 "start": {"character": 27, "line": 14},
4730 powered_by="serverless_ide",
4732 .notification(
4733 comment="Remove the 'public' visibility modifier which triggers AST->AAST errors",
4734 method="textDocument/didChange",
4735 params={
4736 "textDocument": {"uri": "${php_file_uri}"},
4737 "contentChanges": [
4739 "range": {
4740 "start": {"line": 10, "character": 2},
4741 "end": {"line": 10, "character": 8},
4743 "text": "",
4748 .request(
4749 line=line(),
4750 comment="Hover should still work even if visibility modifier has been removed",
4751 method="textDocument/hover",
4752 params={
4753 "textDocument": {"uri": "${php_file_uri}"},
4754 "position": {"line": 14, "character": 37},
4756 result={
4757 "contents": [
4759 "language": "hack",
4760 "value": "public static function staticMethod(string $z): void",
4762 'During testing, we\'ll remove the "public" tag from this '
4763 "method\n"
4764 "to ensure that we can still get IDE services",
4765 "Full name: `HoverWithErrorsClass::staticMethod`",
4767 "range": {
4768 "end": {"character": 39, "line": 14},
4769 "start": {"character": 27, "line": 14},
4772 powered_by="serverless_ide",
4774 .request(line=line(), method="shutdown", params={}, result=None)
4775 .notification(method="exit", params={})
4777 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4779 def test_serverless_ide_formatting(self) -> None:
4780 # This test will fail if hackfmt can't be found
4781 if not self.test_driver.run_hackfmt_check():
4782 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4784 variables = dict(self.prepare_serverless_ide_environment())
4785 variables.update(self.setup_php_file("messy.php"))
4787 self.test_driver.stop_hh_server()
4789 spec = (
4790 self.initialize_spec(LspTestSpec("formatting"), use_serverless_ide=True)
4791 .notification(
4792 method="textDocument/didOpen",
4793 params={
4794 "textDocument": {
4795 "uri": "${php_file_uri}",
4796 "languageId": "hack",
4797 "version": 1,
4798 "text": "${php_file}",
4802 .request(
4803 line=line(),
4804 method="textDocument/formatting",
4805 params={
4806 "textDocument": {"uri": "${php_file_uri}"},
4807 "options": {"tabSize": 5, "insertSpaces": True},
4809 result=[
4811 "range": {
4812 "start": {"line": 0, "character": 0},
4813 "end": {"line": 11, "character": 0},
4815 "newText": "<?hh //strict\n\nfunction x(): string {\n"
4816 + ' $a = "this";\n\n'
4817 + ' $b = "is";\n\n'
4818 + ' $c = "messy";\n\n'
4819 + ' $d = ".";\n'
4820 + ' return "$a"."$b"."$c"."d";\n',
4824 .request(line=line(), method="shutdown", params={}, result=None)
4825 .notification(method="exit", params={})
4827 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4829 def test_serverless_ide_rangeformatting(self) -> None:
4830 # This test will fail if hackfmt can't be found
4831 if not self.test_driver.run_hackfmt_check():
4832 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4834 variables = dict(self.prepare_serverless_ide_environment())
4835 variables.update(self.setup_php_file("messy.php"))
4837 self.test_driver.stop_hh_server()
4839 spec = (
4840 self.initialize_spec(
4841 LspTestSpec("range_formatting"), use_serverless_ide=True
4843 .notification(
4844 method="textDocument/didOpen",
4845 params={
4846 "textDocument": {
4847 "uri": "${php_file_uri}",
4848 "languageId": "hack",
4849 "version": 1,
4850 "text": "${php_file}",
4854 .request(
4855 line=line(),
4856 method="textDocument/rangeFormatting",
4857 params={
4858 "textDocument": {"uri": "${php_file_uri}"},
4859 "range": {
4860 "start": {"line": 3, "character": 0},
4861 "end": {"line": 4, "character": 0},
4863 "options": {"tabSize": 5, "insertSpaces": True},
4865 result=[
4867 "range": {
4868 "start": {"line": 3, "character": 0},
4869 "end": {"line": 4, "character": 0},
4871 "newText": ' $a = "this";\n',
4875 .request(line=line(), method="shutdown", params={}, result=None)
4876 .notification(method="exit", params={})
4878 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4880 def test_serverless_ide_ontypeformatting(self) -> None:
4881 # This test will fail if hackfmt can't be found
4882 if not self.test_driver.run_hackfmt_check():
4883 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4885 variables = dict(self.prepare_serverless_ide_environment())
4886 variables.update(self.setup_php_file("ontypeformatting.php"))
4888 spec = (
4889 self.initialize_spec(
4890 LspTestSpec("ontypeformatting"), use_serverless_ide=True
4892 .notification(
4893 method="textDocument/didOpen",
4894 params={
4895 "textDocument": {
4896 "uri": "${php_file_uri}",
4897 "languageId": "hack",
4898 "version": 1,
4899 "text": "${php_file}",
4903 .request(
4904 line=line(),
4905 method="textDocument/onTypeFormatting",
4906 params={
4907 "textDocument": {"uri": "${php_file_uri}"},
4908 "position": {"line": 9, "character": 58},
4909 "ch": ";",
4910 "options": {"tabSize": 2, "insertSpaces": True},
4912 result=[
4914 "range": {
4915 "start": {"line": 5, "character": 23},
4916 "end": {"line": 9, "character": 58},
4918 "newText": "{\n test_otf(\n"
4919 + " '1234567890',\n"
4920 + " '1234567890',\n"
4921 + " '1234567890',\n"
4922 + " '1234567890',\n"
4923 + " '1234567890',\n"
4924 + " '1234567890',\n );",
4928 .request(
4929 line=line(),
4930 method="textDocument/onTypeFormatting",
4931 params={
4932 "textDocument": {"uri": "${php_file_uri}"},
4933 "position": {"line": 15, "character": 23},
4934 "ch": "}",
4935 "options": {"tabSize": 2, "insertSpaces": True},
4937 result=[
4939 "range": {
4940 "start": {"line": 15, "character": 0},
4941 "end": {"line": 15, "character": 23},
4943 "newText": "function otf(): void {}",
4947 .request(line=line(), method="shutdown", params={}, result=None)
4948 .notification(method="exit", params={})
4951 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4953 def test_did_change(self) -> None:
4954 self.prepare_server_environment()
4955 variables = self.setup_php_file("didchange.php")
4956 spec = (
4957 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
4958 .wait_for_hh_server_ready()
4959 .notification(
4960 method="textDocument/didOpen",
4961 params={
4962 "textDocument": {
4963 "uri": "${php_file_uri}",
4964 "languageId": "hack",
4965 "version": 1,
4966 "text": "${php_file}",
4970 .notification(
4971 method="textDocument/didChange",
4972 params={
4973 "textDocument": {"uri": "${php_file_uri}"},
4974 "contentChanges": [
4976 "range": {
4977 "start": {"line": 7, "character": 11},
4978 "end": {"line": 7, "character": 12},
4980 "text": "a",
4985 .wait_for_notification(
4986 method="textDocument/publishDiagnostics",
4987 params={
4988 "uri": "${php_file_uri}",
4989 "diagnostics": [
4991 "range": {
4992 "start": {"line": 7, "character": 11},
4993 "end": {"line": 7, "character": 11},
4995 "severity": 1,
4996 "code": 1002,
4997 "source": "Hack",
4998 "message": "A semicolon ; is expected here.",
4999 "relatedLocations": [],
5000 "relatedInformation": [],
5005 .request(line=line(), method="shutdown", params={}, result=None)
5006 .wait_for_notification(
5007 comment="Hack appears to clear out diagnostics before shutting down",
5008 method="textDocument/publishDiagnostics",
5009 params={"uri": "${php_file_uri}", "diagnostics": []},
5011 .notification(method="exit", params={})
5013 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5015 def test_go_to_implementation(self) -> None:
5016 self.prepare_server_environment()
5017 variables = self.setup_php_file("go_to_implementation.php")
5018 spec = (
5019 self.initialize_spec(
5020 LspTestSpec("test_go_to_implementation"), use_serverless_ide=False
5022 .wait_for_hh_server_ready()
5023 .notification(
5024 method="textDocument/didOpen",
5025 params={
5026 "textDocument": {
5027 "uri": "${php_file_uri}",
5028 "languageId": "hack",
5029 "version": 1,
5030 "text": "${php_file}",
5034 .request(
5035 line=line(),
5036 comment="go to implemenetation: abstract class",
5037 method="textDocument/implementation",
5038 params={
5039 "textDocument": {"uri": "${php_file_uri}"},
5040 "position": {"line": 1, "character": 17},
5042 result=[
5044 "uri": "${php_file_uri}",
5045 "range": {
5046 "start": {"line": 7, "character": 6},
5047 "end": {"line": 7, "character": 9},
5052 .request(
5053 line=line(),
5054 comment="go to implemenetation: interface",
5055 method="textDocument/implementation",
5056 params={
5057 "textDocument": {"uri": "${php_file_uri}"},
5058 "position": {"line": 13, "character": 13},
5060 result=[
5062 "uri": "${php_file_uri}",
5063 "range": {
5064 "start": {"line": 17, "character": 6},
5065 "end": {"line": 17, "character": 9},
5070 .request(
5071 line=line(),
5072 comment="go to implemenetation: trait",
5073 method="textDocument/implementation",
5074 params={
5075 "textDocument": {"uri": "${php_file_uri}"},
5076 "position": {"line": 23, "character": 10},
5078 result=[
5080 "uri": "${php_file_uri}",
5081 "range": {
5082 "start": {"line": 30, "character": 6},
5083 "end": {"line": 30, "character": 16},
5088 .request(
5089 line=line(),
5090 comment="go to implemenetation: method",
5091 method="textDocument/implementation",
5092 params={
5093 "textDocument": {"uri": "${php_file_uri}"},
5094 "position": {"line": 19, "character": 18},
5096 result=[
5098 "uri": "${php_file_uri}",
5099 "range": {
5100 "start": {"line": 8, "character": 18},
5101 "end": {"line": 8, "character": 22},
5106 .request(line=line(), method="shutdown", params={}, result=None)
5107 .notification(method="exit", params={})
5109 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5111 def test_signature_help(self) -> None:
5112 self.prepare_server_environment()
5113 variables = self.setup_php_file("signaturehelp.php")
5114 spec = (
5115 self.initialize_spec(
5116 LspTestSpec("test_signature_help"), use_serverless_ide=False
5118 .wait_for_hh_server_ready()
5119 .notification(
5120 method="textDocument/didOpen",
5121 params={
5122 "textDocument": {
5123 "uri": "${php_file_uri}",
5124 "languageId": "hack",
5125 "version": 1,
5126 "text": "${php_file}",
5130 .request(
5131 line=line(),
5132 comment="signature help for 0-argument constructor"
5133 " (left of opening paren)",
5134 method="textDocument/signatureHelp",
5135 params={
5136 "textDocument": {"uri": "${php_file_uri}"},
5137 "position": {"line": 16, "character": 18},
5139 result=None,
5141 .request(
5142 line=line(),
5143 comment="signature help for 0-argument constructor",
5144 method="textDocument/signatureHelp",
5145 params={
5146 "textDocument": {"uri": "${php_file_uri}"},
5147 "position": {"line": 16, "character": 19},
5149 result={
5150 "signatures": [
5152 "label": "public function __construct(): void",
5153 "documentation": "Constructor with doc block",
5154 "parameters": [],
5157 "activeSignature": 0,
5158 "activeParameter": 0,
5161 .request(
5162 line=line(),
5163 comment="signature help for 0-argument constructor"
5164 " (right of closing paren)",
5165 method="textDocument/signatureHelp",
5166 params={
5167 "textDocument": {"uri": "${php_file_uri}"},
5168 "position": {"line": 16, "character": 20},
5170 result=None,
5172 .request(
5173 line=line(),
5174 comment="signature help for 2-argument instance method"
5175 " (left of opening paren)",
5176 method="textDocument/signatureHelp",
5177 params={
5178 "textDocument": {"uri": "${php_file_uri}"},
5179 "position": {"line": 17, "character": 20},
5181 result=None,
5183 .request(
5184 line=line(),
5185 comment="signature help for 2-argument instance method"
5186 " (right of opening paren)",
5187 method="textDocument/signatureHelp",
5188 params={
5189 "textDocument": {"uri": "${php_file_uri}"},
5190 "position": {"line": 17, "character": 21},
5192 result={
5193 "signatures": [
5195 "label": "public function instanceMethod"
5196 "(int $x1, int $x2): void",
5197 "documentation": "Instance method with doc block",
5198 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
5201 "activeSignature": 0,
5202 "activeParameter": 0,
5205 .request(
5206 line=line(),
5207 comment="signature help for 2-argument instance method"
5208 " (left of first comma)",
5209 method="textDocument/signatureHelp",
5210 params={
5211 "textDocument": {"uri": "${php_file_uri}"},
5212 "position": {"line": 17, "character": 22},
5214 result={
5215 "signatures": [
5217 "label": "public function instanceMethod"
5218 "(int $x1, int $x2): void",
5219 "documentation": "Instance method with doc block",
5220 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
5223 "activeSignature": 0,
5224 "activeParameter": 1,
5227 .request(
5228 line=line(),
5229 comment="signature help for 2-argument instance method"
5230 " (right of first comma)",
5231 method="textDocument/signatureHelp",
5232 params={
5233 "textDocument": {"uri": "${php_file_uri}"},
5234 "position": {"line": 17, "character": 23},
5236 result={
5237 "signatures": [
5239 "label": "public function instanceMethod"
5240 "(int $x1, int $x2): void",
5241 "documentation": "Instance method with doc block",
5242 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
5245 "activeSignature": 0,
5246 "activeParameter": 1,
5249 .request(
5250 line=line(),
5251 comment="signature help for 2-argument instance method"
5252 " (left of closing paren)",
5253 method="textDocument/signatureHelp",
5254 params={
5255 "textDocument": {"uri": "${php_file_uri}"},
5256 "position": {"line": 17, "character": 24},
5258 result={
5259 "signatures": [
5261 "label": "public function instanceMethod"
5262 "(int $x1, int $x2): void",
5263 "documentation": "Instance method with doc block",
5264 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
5267 "activeSignature": 0,
5268 "activeParameter": 1,
5271 .request(
5272 line=line(),
5273 comment="signature help for 2-argument instance method"
5274 " (right of closing paren)",
5275 method="textDocument/signatureHelp",
5276 params={
5277 "textDocument": {"uri": "${php_file_uri}"},
5278 "position": {"line": 17, "character": 25},
5280 result=None,
5282 .request(
5283 line=line(),
5284 comment="signature help for 1-argument static method"
5285 " (left of open paren)",
5286 method="textDocument/signatureHelp",
5287 params={
5288 "textDocument": {"uri": "${php_file_uri}"},
5289 "position": {"line": 18, "character": 23},
5291 result=None,
5293 .request(
5294 line=line(),
5295 comment="signature help for 1-argument static method"
5296 " (right of open paren)",
5297 method="textDocument/signatureHelp",
5298 params={
5299 "textDocument": {"uri": "${php_file_uri}"},
5300 "position": {"line": 18, "character": 24},
5302 result={
5303 "signatures": [
5305 "label": "public static function staticMethod"
5306 "(string $z): void",
5307 "documentation": "Static method with doc block",
5308 "parameters": [{"label": "$z"}],
5311 "activeSignature": 0,
5312 "activeParameter": 0,
5315 .request(
5316 line=line(),
5317 comment="signature help for 2-argument global function"
5318 " (left of open paren)",
5319 method="textDocument/signatureHelp",
5320 params={
5321 "textDocument": {"uri": "${php_file_uri}"},
5322 "position": {"line": 19, "character": 17},
5324 result=None,
5326 .request(
5327 line=line(),
5328 comment="signature help for 2-argument global function"
5329 " (right of open paren)",
5330 method="textDocument/signatureHelp",
5331 params={
5332 "textDocument": {"uri": "${php_file_uri}"},
5333 "position": {"line": 19, "character": 18},
5335 result={
5336 "signatures": [
5338 "label": "function global_function"
5339 "(string $s, int $x): void",
5340 "documentation": "Global function with doc block",
5341 "parameters": [{"label": "$s"}, {"label": "$x"}],
5344 "activeSignature": 0,
5345 "activeParameter": 0,
5348 .request(
5349 line=line(),
5350 comment="signature help for 1-argument namespace-aliased global"
5351 " function (right of open paren)",
5352 method="textDocument/signatureHelp",
5353 params={
5354 "textDocument": {"uri": "${php_file_uri}"},
5355 "position": {"line": 20, "character": 26},
5357 result=None,
5359 .request(
5360 line=line(),
5361 comment="signature help for 1-argument namespace-aliased global"
5362 " function (right of open paren)",
5363 method="textDocument/signatureHelp",
5364 params={
5365 "textDocument": {"uri": "${php_file_uri}"},
5366 "position": {"line": 20, "character": 26},
5368 result=None,
5370 .request(
5371 line=line(),
5372 comment="signature help for 1-argument namespace-aliased global"
5373 " function (right of open paren)",
5374 method="textDocument/signatureHelp",
5375 params={
5376 "textDocument": {"uri": "${php_file_uri}"},
5377 "position": {"line": 20, "character": 27},
5379 result={
5380 "signatures": [
5382 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
5383 "documentation": "Namespace-aliased function with doc block",
5384 "parameters": [{"label": "$s"}],
5387 "activeSignature": 0,
5388 "activeParameter": 0,
5391 .request(
5392 line=line(),
5393 comment="signature help for 1-argument namespace-aliased global"
5394 " function (right of open paren)",
5395 method="textDocument/signatureHelp",
5396 params={
5397 "textDocument": {"uri": "${php_file_uri}"},
5398 "position": {"line": 20, "character": 28},
5400 result={
5401 "signatures": [
5403 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
5404 "documentation": "Namespace-aliased function with doc block",
5405 "parameters": [{"label": "$s"}],
5408 "activeSignature": 0,
5409 "activeParameter": 0,
5412 .request(
5413 line=line(),
5414 comment="signature help for 2-argument function with params"
5415 " (right of open paren)",
5416 method="textDocument/signatureHelp",
5417 params={
5418 "textDocument": {"uri": "${php_file_uri}"},
5419 "position": {"line": 21, "character": 30},
5421 result={
5422 "signatures": [
5424 "label": "function test_signature_help_params1("
5425 "\n string $param1,\n string $param2\n): void",
5426 "documentation": "comment describing the method"
5427 "\n@param $param1 info1"
5428 "\n@param param2 info2",
5429 "parameters": [
5430 {"label": "$param1", "documentation": "info1"},
5431 {"label": "$param2", "documentation": "info2"},
5435 "activeSignature": 0,
5436 "activeParameter": 0,
5439 .request(
5440 line=line(),
5441 comment="signature help for 2-argument function with params"
5442 " (right of open paren)",
5443 method="textDocument/signatureHelp",
5444 params={
5445 "textDocument": {"uri": "${php_file_uri}"},
5446 "position": {"line": 22, "character": 30},
5448 result={
5449 "signatures": [
5451 "label": "function test_signature_help_params2("
5452 "\n string $param1,\n string $param2\n): void",
5453 "documentation": "comment describing the method"
5454 "\n@param $param1 info1",
5455 "parameters": [
5456 {"label": "$param1", "documentation": "info1"},
5457 {"label": "$param2"},
5461 "activeSignature": 0,
5462 "activeParameter": 0,
5465 .request(
5466 line=line(),
5467 comment="signature help for 2-argument function with params"
5468 " (right of open paren)",
5469 method="textDocument/signatureHelp",
5470 params={
5471 "textDocument": {"uri": "${php_file_uri}"},
5472 "position": {"line": 23, "character": 30},
5474 result={
5475 "signatures": [
5477 "label": "function test_signature_help_params3("
5478 "\n string $param1,\n string $param2\n): string",
5479 "documentation": "@param $param1 info1"
5480 "\n for param1"
5481 "\n@param $param2 info2"
5482 "\n@return the string"
5483 "\n 'hack'",
5484 "parameters": [
5486 "label": "$param1",
5487 "documentation": "info1 for param1",
5489 {"label": "$param2", "documentation": "info2"},
5493 "activeSignature": 0,
5494 "activeParameter": 0,
5497 .request(line=line(), method="shutdown", params={}, result=None)
5498 .notification(method="exit", params={})
5500 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5502 def test_signature_help_lambda(self) -> None:
5503 self.prepare_server_environment()
5504 variables = self.setup_php_file("signaturehelp_lambda.php")
5505 spec = (
5506 self.initialize_spec(
5507 LspTestSpec("test_serverless_ide_signature_help_lambda"),
5508 use_serverless_ide=False,
5510 .wait_for_hh_server_ready()
5511 .notification(
5512 method="textDocument/didOpen",
5513 params={
5514 "textDocument": {
5515 "uri": "${php_file_uri}",
5516 "languageId": "hack",
5517 "version": 1,
5518 "text": "${php_file}",
5522 .request(
5523 line=line(),
5524 comment="signature help for a normal function call",
5525 method="textDocument/signatureHelp",
5526 params={
5527 "textDocument": {"uri": "${php_file_uri}"},
5528 "position": {"line": 8, "character": 29},
5530 result={
5531 "activeParameter": 0,
5532 "activeSignature": 0,
5533 "signatures": [
5535 "label": "function test_lambda_sighelp(\n"
5536 " string $str,\n"
5537 " (function(string): int) $f\n"
5538 "): int",
5539 "parameters": [{"label": "$str"}, {"label": "$f"}],
5544 .request(
5545 line=line(),
5546 comment="signature help for normal function call within a lambda",
5547 method="textDocument/signatureHelp",
5548 params={
5549 "textDocument": {"uri": "${php_file_uri}"},
5550 "position": {"line": 9, "character": 21},
5552 result={
5553 "activeParameter": 0,
5554 "activeSignature": 0,
5555 "signatures": [
5557 "label": "function normal_test_func(string $str): void",
5558 "parameters": [{"label": "$str"}],
5563 .request(
5564 line=line(),
5565 comment="signature help for text within a lambda, left side of an open paren",
5566 method="textDocument/signatureHelp",
5567 params={
5568 "textDocument": {"uri": "${php_file_uri}"},
5569 "position": {"line": 10, "character": 15},
5571 result=None,
5573 .request(
5574 line=line(),
5575 comment="signature help for text within a lambda, right side of an open paren",
5576 method="textDocument/signatureHelp",
5577 params={
5578 "textDocument": {"uri": "${php_file_uri}"},
5579 "position": {"line": 10, "character": 16},
5581 result=None,
5583 .request(line=line(), method="shutdown", params={}, result=None)
5584 .notification(method="exit", params={})
5586 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5588 def test_rename(self) -> None:
5589 self.prepare_server_environment()
5590 variables = self.setup_php_file("rename.php")
5591 self.load_and_run("rename", variables)
5593 def test_references(self) -> None:
5594 self.prepare_server_environment()
5595 variables = self.setup_php_file("references.php")
5596 self.load_and_run("references", variables)
5598 def test_non_existing_method(self) -> None:
5599 self.prepare_server_environment()
5600 variables = self.setup_php_file("nomethod.php")
5601 self.load_and_run("nomethod", variables)
5603 def test_bad_call(self) -> None:
5604 self.prepare_server_environment()
5605 variables = self.setup_php_file("bad_call.php")
5606 self.load_and_run("bad_call", variables)
5608 def test_code_action_missing_method(self) -> None:
5609 variables = dict(self.prepare_serverless_ide_environment())
5610 variables.update(self.setup_php_file("code_action_missing_method.php"))
5611 self.test_driver.stop_hh_server()
5613 spec = (
5614 self.initialize_spec(
5615 LspTestSpec("code_action_missing_method"), use_serverless_ide=True
5617 .notification(
5618 method="textDocument/didOpen",
5619 params={
5620 "textDocument": {
5621 "uri": "${php_file_uri}",
5622 "languageId": "hack",
5623 "version": 1,
5624 "text": "${php_file}",
5628 .notification(
5629 comment="make local, unsaved change to the file",
5630 method="textDocument/didChange",
5631 params={
5632 "textDocument": {"uri": "${php_file_uri}", "version": 2},
5633 "contentChanges": [
5635 "text": """\
5636 <?hh
5638 class ClassWithFooBar {
5639 public function foobar(): void {}
5642 function call_method(ClassWithFooBar $mc): void {
5643 $mc->foobaz();
5650 .request(
5651 line=line(),
5652 comment="get actions",
5653 method="textDocument/codeAction",
5654 params={
5655 "textDocument": {"uri": "${php_file_uri}"},
5656 "range": {
5657 "start": {"line": 7, "character": 7},
5658 "end": {"line": 7, "character": 13},
5660 "context": {
5661 "diagnostics": [
5663 "range": {
5664 "start": {"line": 7, "character": 7},
5665 "end": {"line": 7, "character": 13},
5667 "severity": 1,
5668 "code": 4053,
5669 "source": "Hack",
5670 "message": "No instance method foobaz in ClassWithFooBar",
5671 "relatedInformation": [
5673 "location": {
5674 "uri": "${php_file_uri}",
5675 "range": {
5676 "start": {"line": 3, "character": 18},
5677 "end": {"line": 3, "character": 24},
5680 "message": "Did you mean foobar instead?",
5683 "location": {
5684 "uri": "${php_file_uri}",
5685 "range": {
5686 "start": {"line": 6, "character": 21},
5687 "end": {"line": 6, "character": 36},
5690 "message": "This is why I think it is an object of type ClassWithFooBar",
5693 "location": {
5694 "uri": "${php_file_uri}",
5695 "range": {
5696 "start": {"line": 2, "character": 6},
5697 "end": {"line": 2, "character": 21},
5700 "message": "Declaration of ClassWithFooBar is here",
5703 "relatedLocations": [
5705 "location": {
5706 "uri": "${php_file_uri}",
5707 "range": {
5708 "start": {"line": 3, "character": 18},
5709 "end": {"line": 3, "character": 24},
5712 "message": "Did you mean foobar instead?",
5715 "location": {
5716 "uri": "${php_file_uri}",
5717 "range": {
5718 "start": {"line": 6, "character": 21},
5719 "end": {"line": 6, "character": 36},
5722 "message": "This is why I think it is an object of type ClassWithFooBar",
5725 "location": {
5726 "uri": "${php_file_uri}",
5727 "range": {
5728 "start": {"line": 2, "character": 6},
5729 "end": {"line": 2, "character": 21},
5732 "message": "Declaration of ClassWithFooBar is here",
5739 result=[
5741 "title": "Change to ->foobar",
5742 "kind": "quickfix",
5743 "diagnostics": [],
5744 "edit": {
5745 "changes": {
5746 "${root_path}/code_action_missing_method.php": [
5748 "range": {
5749 "start": {"line": 7, "character": 7},
5750 "end": {"line": 7, "character": 13},
5752 "newText": "foobar",
5759 powered_by="serverless_ide",
5761 .request(line=line(), method="shutdown", params={}, result=None)
5762 .notification(method="exit", params={})
5764 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5766 def test_non_blocking(self) -> None:
5767 self.prepare_server_environment()
5768 variables = self.setup_php_file("non_blocking.php")
5769 self.test_driver.start_hh_loop_forever_assert_timeout()
5770 spec = (
5771 self.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide=False)
5772 .ignore_notifications(method="textDocument/publishDiagnostics")
5773 .wait_for_hh_server_ready()
5774 .request(
5775 line=line(),
5776 method="textDocument/definition",
5777 params={
5778 "textDocument": {"uri": "${php_file_uri}"},
5779 "position": {"line": 7, "character": 11},
5781 result=[
5783 "uri": "file://${root_path}/non_blocking.php",
5784 "range": {
5785 "start": {"line": 2, "character": 9},
5786 "end": {"line": 2, "character": 32},
5788 "title": "non_blocking_definition",
5791 wait_id="definition request",
5793 .notification(
5794 comment="remove hh_loop_forever() invocation to break the infinite loop",
5795 method="textDocument/didOpen",
5796 params={
5797 "textDocument": {
5798 "uri": "${root_path}/__hh_loop_forever_foo.php",
5799 "languageId": "hack",
5800 "version": 1,
5801 "text": """\
5802 <?hh // strict
5804 function __hh_loop_forever_foo(): int {
5805 return 4;
5807 """,
5811 .wait_for_response(wait_id="definition request")
5812 .request(line=line(), method="shutdown", params={}, result=None)
5813 .notification(method="exit", params={})
5815 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5817 def test_serverless_ide_hierarchy_file_change_on_disk(self) -> None:
5818 variables = dict(self.prepare_serverless_ide_environment())
5819 variables.update(self.setup_php_file("incremental_derived.php"))
5820 changed_php_file_uri = self.repo_file("incremental_base.php")
5821 variables.update({"changed_php_file_uri": changed_php_file_uri})
5822 self.test_driver.stop_hh_server()
5824 spec = (
5825 self.initialize_spec(
5826 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
5827 use_serverless_ide=True,
5829 .notification(
5830 method="textDocument/didOpen",
5831 params={
5832 "textDocument": {
5833 "uri": "${php_file_uri}",
5834 "languageId": "hack",
5835 "version": 1,
5836 "text": "${php_file}",
5840 .request(
5841 line=line(),
5842 comment="hover before change to class hierarchy should be `int`",
5843 method="textDocument/hover",
5844 params={
5845 "textDocument": {"uri": "${php_file_uri}"},
5846 "position": {"line": 7, "character": 14},
5848 result={
5849 "contents": [
5850 {"language": "hack", "value": "public function foo(): int"},
5851 "Full name: `BaseClassIncremental::foo`",
5853 "range": {
5854 "start": {"line": 7, "character": 12},
5855 "end": {"line": 7, "character": 15},
5858 powered_by="serverless_ide",
5860 .write_to_disk(
5861 uri=changed_php_file_uri,
5862 contents="""\
5863 <?hh // strict
5864 class BaseClassIncremental {
5865 public function foo(): string { return ''; }
5867 """,
5868 notify=True,
5870 .request(
5871 line=line(),
5872 comment="hover after change to class hierarchy should be `string`",
5873 method="textDocument/hover",
5874 params={
5875 "textDocument": {"uri": "${php_file_uri}"},
5876 "position": {"line": 7, "character": 14},
5878 result={
5879 "contents": [
5880 {"language": "hack", "value": "public function foo(): string"},
5881 "Full name: `BaseClassIncremental::foo`",
5883 "range": {
5884 "start": {"line": 7, "character": 12},
5885 "end": {"line": 7, "character": 15},
5888 powered_by="serverless_ide",
5890 .request(line=line(), method="shutdown", params={}, result=None)
5891 .notification(method="exit", params={})
5894 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5896 def test_serverless_ide_decl_in_unsaved_buffer_changed(self) -> None:
5897 variables = dict(self.prepare_serverless_ide_environment())
5898 variables.update(self.setup_php_file("hover.php"))
5899 self.test_driver.stop_hh_server()
5901 spec = (
5902 self.initialize_spec(
5903 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
5904 use_serverless_ide=True,
5906 .notification(
5907 method="textDocument/didOpen",
5908 params={
5909 "textDocument": {
5910 "uri": "${php_file_uri}",
5911 "languageId": "hack",
5912 "version": 1,
5913 "text": "${php_file}",
5917 .request(
5918 line=line(),
5919 comment="hover over function invocation",
5920 method="textDocument/hover",
5921 params={
5922 "textDocument": {"uri": "${php_file_uri}"},
5923 "position": {"line": 3, "character": 16},
5925 result={
5926 "contents": [
5927 {"language": "hack", "value": "int"},
5928 "A comment describing b_hover.",
5930 "range": {
5931 "start": {"line": 3, "character": 9},
5932 "end": {"line": 3, "character": 16},
5935 powered_by="serverless_ide",
5937 .notification(
5938 comment="make local, unsaved change to the file",
5939 method="textDocument/didChange",
5940 params={
5941 "textDocument": {"uri": "${php_file_uri}", "version": 2},
5942 "contentChanges": [
5944 "text": """\
5945 <?hh // strict
5946 // comment
5947 function a_hover(): int {
5948 return b_hover();
5950 // A comment describing b_hover differently.
5951 function b_hover(): string {
5952 return 42;
5959 .request(
5960 line=line(),
5961 comment="another hover over function invocation, should be string now",
5962 method="textDocument/hover",
5963 params={
5964 "textDocument": {"uri": "${php_file_uri}"},
5965 "position": {"line": 3, "character": 16},
5967 result={
5968 "contents": [
5969 {"language": "hack", "value": "string"},
5970 "A comment describing b_hover differently.",
5972 "range": {
5973 "start": {"line": 3, "character": 9},
5974 "end": {"line": 3, "character": 16},
5977 powered_by="serverless_ide",
5979 .request(line=line(), method="shutdown", params={}, result=None)
5980 .notification(method="exit", params={})
5983 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5985 def test_serverless_ide_decl_two_unsaved_buffers(self) -> None:
5986 variables = dict(self.prepare_serverless_ide_environment())
5987 variables.update(self.setup_php_file("unsaved1.php"))
5988 variables.update({"unsaved2_file_uri": self.repo_file_uri("unsaved2.php")})
5989 self.test_driver.stop_hh_server()
5991 spec = (
5992 self.initialize_spec(
5993 LspTestSpec("test_serverless_ide_decl_two_unsaved_buffers"),
5994 use_serverless_ide=True,
5996 .notification(
5997 comment="open 'unsaved1.php', since we'll be hovering in it",
5998 method="textDocument/didOpen",
5999 params={
6000 "textDocument": {
6001 "uri": "${php_file_uri}",
6002 "languageId": "hack",
6003 "version": 1,
6004 "text": "${php_file}",
6008 .notification(
6009 comment="open 'unsaved2.php' with a bool-returning signature, different from disk",
6010 method="textDocument/didOpen",
6011 params={
6012 "textDocument": {
6013 "uri": "${unsaved2_file_uri}",
6014 "languageId": "hack",
6015 "version": 1,
6016 "text": """\
6017 <?hh //strict
6018 function unsaved_bar(): bool { return true; }
6019 """,
6023 .request(
6024 line=line(),
6025 comment="hover 'unsaved1.php' is with respect to disk contents of 'unsaved2.php'",
6026 method="textDocument/hover",
6027 params={
6028 "textDocument": {"uri": "${php_file_uri}"},
6029 "position": {"line": 1, "character": 39},
6031 result={
6032 "contents": [
6033 {"language": "hack", "value": "function unsaved_bar(): int"},
6035 "range": {
6036 "start": {"line": 1, "character": 34},
6037 "end": {"line": 1, "character": 45},
6040 powered_by="serverless_ide",
6042 .notification(
6043 comment="change signature in 'unsaved2.php' to return string",
6044 method="textDocument/didChange",
6045 params={
6046 "textDocument": {"uri": "${unsaved2_file_uri}", "version": 2},
6047 "contentChanges": [
6049 "text": """\
6050 <?hh //strict
6051 function unsaved_bar(): string { return "hello"; }
6057 .request(
6058 line=line(),
6059 comment="this is a dummy hover in 'unsaved2.php' just to ensure its decl is cached",
6060 method="textDocument/hover",
6061 params={
6062 "textDocument": {"uri": "${unsaved2_file_uri}"},
6063 "position": {"line": 0, "character": 0},
6065 result=None,
6066 powered_by="serverless_ide",
6068 .request(
6069 line=line(),
6070 comment="hover 'unsaved1.php' is still with respect to disk contents of 'unsaved2.php'",
6071 method="textDocument/hover",
6072 params={
6073 "textDocument": {"uri": "${php_file_uri}"},
6074 "position": {"line": 1, "character": 39},
6076 result={
6077 "contents": [
6078 {"language": "hack", "value": "function unsaved_bar(): int"},
6080 "range": {
6081 "start": {"line": 1, "character": 34},
6082 "end": {"line": 1, "character": 45},
6085 powered_by="serverless_ide",
6087 .write_to_disk(
6088 comment="save signature in 'unsaved2' to return string",
6089 uri=variables["unsaved2_file_uri"],
6090 contents="""\
6091 <?hh // strict
6092 function unsaved_bar(): string { return "hello"; }
6093 """,
6094 notify=True,
6096 .request(
6097 line=line(),
6098 comment="hover 'unsaved1.php' gets new disk contents of 'unsaved2.php'",
6099 method="textDocument/hover",
6100 params={
6101 "textDocument": {"uri": "${php_file_uri}"},
6102 "position": {"line": 1, "character": 39},
6104 result={
6105 "contents": [
6106 {"language": "hack", "value": "function unsaved_bar(): string"},
6108 "range": {
6109 "start": {"line": 1, "character": 34},
6110 "end": {"line": 1, "character": 45},
6113 powered_by="serverless_ide",
6115 .request(line=line(), method="shutdown", params={}, result=None)
6116 .notification(method="exit", params={})
6119 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6121 def test_hover_without_file_open(self) -> None:
6122 variables = dict(self.prepare_serverless_ide_environment())
6123 variables.update(self.setup_php_file("hover.php"))
6124 self.test_driver.stop_hh_server()
6126 spec = (
6127 self.initialize_spec(
6128 LspTestSpec("test_hover_without_file_open"),
6129 use_serverless_ide=True,
6130 supports_status=True,
6132 .ignore_notifications(method="textDocument/publishDiagnostics")
6133 .ignore_requests(
6134 comment="Ignore 'initializing...' messages since they're racy",
6135 method="window/showStatus",
6136 params={
6137 "type": 2,
6138 "actions": [{"title": "Restart hh_server"}],
6139 "message": "Hack IDE: initializing.\nhh_server: stopped.",
6140 "shortMessage": "Hack: initializing",
6143 .ignore_requests(
6144 comment="another racy initializing, before hh_server has even responded",
6145 method="window/showStatus",
6146 params={
6147 "type": 2,
6148 "actions": [],
6149 "message": "Hack IDE: initializing.",
6150 "shortMessage": "Hack: initializing",
6153 .ignore_requests(
6154 comment="another racy initialization to ignore, again before hh_server",
6155 method="window/showStatus",
6156 params={
6157 "type": 3,
6158 "actions": [],
6159 "message": "Hack IDE: ready.",
6160 "shortMessage": "Hack: ready",
6163 .wait_for_server_request(
6164 method="window/showStatus",
6165 params={
6166 "actions": [{"title": "Restart hh_server"}],
6167 "message": "Hack IDE: ready.\nhh_server: stopped.",
6168 "shortMessage": "Hack: ready",
6169 "type": 3,
6171 result=NoResponse(),
6173 .request(
6174 line=line(),
6175 comment="hover before file_open will fail",
6176 method="textDocument/hover",
6177 params={
6178 "textDocument": {"uri": "${php_file_uri}"},
6179 "position": {"line": 26, "character": 20},
6181 result=None,
6183 .notification(
6184 method="textDocument/didOpen",
6185 params={
6186 "textDocument": {
6187 "uri": "${php_file_uri}",
6188 "languageId": "hack",
6189 "version": 1,
6190 "text": "${php_file}",
6194 .request(
6195 line=line(),
6196 comment="hover after file_open will succeed",
6197 method="textDocument/hover",
6198 params={
6199 "textDocument": {"uri": "${php_file_uri}"},
6200 "position": {"line": 26, "character": 20},
6202 result={
6203 "contents": [
6204 {"language": "hack", "value": "string"},
6205 {"language": "hack", "value": "Parameter: $s"},
6208 powered_by="serverless_ide",
6210 .request(
6211 line=line(),
6212 method="$test/shutdownServerlessIde",
6213 params={},
6214 result=None,
6215 powered_by="serverless_ide",
6217 .wait_for_server_request(
6218 method="window/showStatus",
6219 params={
6220 "actions": [
6221 {"title": "Restart Hack IDE"},
6222 {"title": "Restart hh_server"},
6224 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: stopped.",
6225 "shortMessage": "Hack: failed",
6226 "type": 1,
6228 result={"title": "Restart Hack IDE"},
6230 .wait_for_server_request(
6231 method="window/showStatus",
6232 params={
6233 "actions": [{"title": "Restart hh_server"}],
6234 "message": "Hack IDE: ready.\nhh_server: stopped.",
6235 "shortMessage": "Hack: ready",
6236 "type": 3,
6238 result=NoResponse(),
6240 .request(
6241 line=line(),
6242 comment="hover after restart will succeed",
6243 method="textDocument/hover",
6244 params={
6245 "textDocument": {"uri": "${php_file_uri}"},
6246 "position": {"line": 26, "character": 20},
6248 result={
6249 "contents": [
6250 {"language": "hack", "value": "string"},
6251 {"language": "hack", "value": "Parameter: $s"},
6254 powered_by="serverless_ide",
6256 .notification(
6257 method="textDocument/didClose",
6258 params={"textDocument": {"uri": "${php_file_uri}"}},
6260 .request(
6261 line=line(),
6262 comment="hover after file_close will fail",
6263 method="textDocument/hover",
6264 params={
6265 "textDocument": {"uri": "${php_file_uri}"},
6266 "position": {"line": 26, "character": 20},
6268 result=None,
6270 .request(line=line(), method="shutdown", params={}, result=None)
6271 .notification(method="exit", params={})
6274 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6276 def test_hh_server_status_diagnostic(self) -> None:
6277 variables = dict(self.prepare_serverless_ide_environment())
6278 variables.update(self.setup_php_file("unsaved1.php"))
6279 variables.update(
6281 "unsaved2_file_uri": self.repo_file_uri("unsaved2.php"),
6282 "unsaved2_file": self.read_repo_file("unsaved2.php"),
6285 self.test_driver.stop_hh_server()
6287 spec = (
6288 self.initialize_spec(
6289 LspTestSpec("test_hh_server_status_diagnostic"), use_serverless_ide=True
6291 .ignore_status_diagnostics(False)
6292 .notification(
6293 method="textDocument/didOpen",
6294 params={
6295 "textDocument": {
6296 "uri": "${php_file_uri}",
6297 "languageId": "hack",
6298 "version": 1,
6299 "text": "${php_file}",
6303 .wait_for_notification(
6304 comment="After didOpen(file1), the hh_server_status diagnostic should appear in file1",
6305 method="textDocument/publishDiagnostics",
6306 params={
6307 "uri": "${php_file_uri}",
6308 "diagnostics": [
6310 "range": {
6311 "start": {"line": 0, "character": 0},
6312 "end": {"line": 0, "character": 1},
6314 "severity": 1,
6315 "source": "hh_server",
6316 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
6317 "relatedInformation": [],
6318 "relatedLocations": [],
6321 "isStatusFB": True,
6324 .notification(
6325 method="textDocument/didOpen",
6326 params={
6327 "textDocument": {
6328 "uri": "${unsaved2_file_uri}",
6329 "languageId": "hack",
6330 "version": 1,
6331 "text": "${unsaved2_file}",
6335 .wait_for_notification(
6336 comment="After didOpen(file2), the hh_server_status diagnostic should disappear from file1",
6337 method="textDocument/publishDiagnostics",
6338 params={
6339 "uri": "${php_file_uri}",
6340 "diagnostics": [],
6341 "isStatusFB": True,
6344 .wait_for_notification(
6345 comment="After didOpen(file2), the hh_server_status diagnostic should reappear in file2",
6346 method="textDocument/publishDiagnostics",
6347 params={
6348 "uri": "${unsaved2_file_uri}",
6349 "diagnostics": [
6351 "range": {
6352 "start": {"line": 0, "character": 0},
6353 "end": {"line": 0, "character": 1},
6355 "severity": 1,
6356 "source": "hh_server",
6357 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
6358 "relatedInformation": [],
6359 "relatedLocations": [],
6362 "isStatusFB": True,
6365 .notification(
6366 method="textDocument/didClose",
6367 params={"textDocument": {"uri": "${unsaved2_file_uri}"}},
6369 .wait_for_notification(
6370 comment="After didClose(file2), the hh_server_status diagnostic should disappear from file2",
6371 method="textDocument/publishDiagnostics",
6372 params={
6373 "uri": "${unsaved2_file_uri}",
6374 "diagnostics": [],
6375 "isStatusFB": True,
6378 .wait_for_notification(
6379 comment="After didClose(file2), the hh_server_status diagnostic should reappear in file1",
6380 method="textDocument/publishDiagnostics",
6381 params={
6382 "uri": "${php_file_uri}",
6383 "diagnostics": [
6385 "range": {
6386 "start": {"line": 0, "character": 0},
6387 "end": {"line": 0, "character": 1},
6389 "severity": 1,
6390 "source": "hh_server",
6391 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
6392 "relatedInformation": [],
6393 "relatedLocations": [],
6396 "isStatusFB": True,
6399 .notification(
6400 method="textDocument/didClose",
6401 params={"textDocument": {"uri": "${php_file_uri}"}},
6403 .wait_for_notification(
6404 comment="After didClose(file1), the hh_server_status diagnostic should disappear from file1",
6405 method="textDocument/publishDiagnostics",
6406 params={
6407 "uri": "${php_file_uri}",
6408 "diagnostics": [],
6409 "isStatusFB": True,
6412 .request(line=line(), method="shutdown", params={}, result=None)
6413 .notification(method="exit", params={})
6416 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6418 def _sanitize_gutter_line_numbers(self, s: str) -> str:
6419 gutter_line_number_re = re.compile(r"^[ ]*[0-9]+ \|", re.MULTILINE)
6420 return re.sub(gutter_line_number_re, " XXXX |", s)
6422 def test_lsptestspec_incorrect_request_result(self) -> None:
6423 variables = dict(self.prepare_serverless_ide_environment())
6424 variables.update(self.setup_php_file("hover.php"))
6425 self.test_driver.stop_hh_server()
6427 spec = (
6428 self.initialize_spec(
6429 LspTestSpec("test_lsptestspec_incorrect_request_result"),
6430 use_serverless_ide=True,
6432 .notification(
6433 method="textDocument/didOpen",
6434 params={
6435 "textDocument": {
6436 "uri": "${php_file_uri}",
6437 "languageId": "hack",
6438 "version": 1,
6439 "text": "${php_file}",
6443 .request(
6444 line=line(),
6445 comment="hover over function invocation",
6446 method="textDocument/hover",
6447 params={
6448 "textDocument": {"uri": "${php_file_uri}"},
6449 "position": {"line": 3, "character": 16},
6451 result={
6452 "contents": [
6453 {"language": "hack", "value": "int"},
6454 "INCORRECT COMMENT HERE",
6456 "range": {
6457 "start": {"line": 3, "character": 9},
6458 "end": {"line": 3, "character": 16},
6461 powered_by="serverless_ide",
6463 .request(line=line(), method="shutdown", params={}, result=None)
6464 .notification(method="exit", params={})
6466 try:
6467 self.run_spec(
6468 spec,
6469 variables=variables,
6470 wait_for_server=False,
6471 use_serverless_ide=True,
6473 raise AssertionError("Expected an error here")
6474 except AssertionError as e:
6475 self.assertEqual(
6476 self._sanitize_gutter_line_numbers(str(e)),
6477 """\
6478 Test case test_lsptestspec_incorrect_request_result failed with 1 errors:
6480 Error 1/1:
6481 Description: Request with ID 5 (comment: 'hover over function invocation') \
6482 got an incorrect result:
6484 (- is expected lines, + is actual lines)
6485 - {'contents': [{'language': 'hack', 'value': 'int'}, 'INCORRECT COMMENT HERE'],
6486 ? ---------------------------
6488 + {'contents': [{'language': 'hack', 'value': 'int'},
6489 + 'A comment describing b_hover.'],
6490 'range': {'end': {'character': 16, 'line': 3},
6491 'start': {'character': 9, 'line': 3}}}
6493 Context:
6494 This was the associated request:
6496 hphp/hack/test/integration/test_lsp.py
6497 XXXX | .request(
6498 XXXX | line=line(),
6499 XXXX | comment="hover over function invocation",
6500 XXXX | method="textDocument/hover",
6501 XXXX | params={
6502 XXXX | "textDocument": {"uri": "${php_file_uri}"},
6503 XXXX | "position": {"line": 3, "character": 16},
6504 XXXX | },
6505 XXXX | result={
6506 XXXX | "contents": [
6507 XXXX | {"language": "hack", "value": "int"},
6508 XXXX | "INCORRECT COMMENT HERE",
6509 XXXX | ],
6510 XXXX | "range": {
6511 XXXX | "start": {"line": 3, "character": 9},
6512 XXXX | "end": {"line": 3, "character": 16},
6513 XXXX | },
6514 XXXX | },
6515 XXXX | powered_by="serverless_ide",
6516 XXXX | )
6518 Remediation:
6519 1) If this was unexpected, then the language server is buggy and should be
6520 fixed.
6522 2) If this was expected, you can update your request with the following code to
6523 make it match:
6525 .request(
6526 line=line(),
6527 comment='hover over function invocation',
6528 method='textDocument/hover',
6529 params={'textDocument': {'uri': '${php_file_uri}'}, \
6530 'position': {'line': 3, 'character': 16}},
6531 result={'contents': [{'language': 'hack', 'value': 'int'}, \
6532 'A comment describing b_hover.'], \
6533 'range': {'start': {'line': 3, 'character': 9}, \
6534 'end': {'line': 3, 'character': 16}}},
6535 powered_by='serverless_ide',
6538 If you want to examine the raw LSP logs, you can check the `.sent.log` and
6539 `.received.log` files that were generated in the template repo for this test.\
6540 """,
6543 def test_lsptestspec_unexpected_notification(self) -> None:
6544 self.prepare_server_environment()
6545 variables = self.setup_php_file("didchange.php")
6546 spec = (
6547 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
6548 .wait_for_hh_server_ready()
6549 .notification(
6550 method="textDocument/didOpen",
6551 params={
6552 "textDocument": {
6553 "uri": "${php_file_uri}",
6554 "languageId": "hack",
6555 "version": 1,
6556 "text": "${php_file}",
6560 .notification(
6561 method="textDocument/didChange",
6562 params={
6563 "textDocument": {"uri": "${php_file_uri}"},
6564 "contentChanges": [
6566 "range": {
6567 "start": {"line": 7, "character": 11},
6568 "end": {"line": 7, "character": 12},
6570 "text": "a",
6575 .wait_for_notification(
6576 method="textDocument/publishDiagnostics",
6577 params={
6578 "uri": "${php_file_uri}",
6579 "diagnostics": [
6581 "range": {
6582 "start": {"line": 7, "character": 11},
6583 "end": {"line": 7, "character": 11},
6585 "severity": 1,
6586 "code": 1002,
6587 "source": "Hack",
6588 "message": "A semicolon ; is expected here.",
6589 "relatedLocations": [],
6590 "relatedInformation": [],
6595 .request(line=line(), method="shutdown", params={}, result=None)
6596 .notification(method="exit", params={})
6598 try:
6599 self.run_spec(
6600 spec, variables, wait_for_server=True, use_serverless_ide=False
6602 raise AssertionError("Expected an error here")
6603 except AssertionError as e:
6604 self.assertEqual(
6605 self._sanitize_gutter_line_numbers(str(e)),
6606 """\
6607 Test case did_change failed with 1 errors:
6609 Error 1/1:
6610 Description: An unexpected notification of type \
6611 'textDocument/publishDiagnostics' was sent by the language server.
6612 Here is the notification payload:
6614 {'jsonrpc': '2.0',
6615 'method': 'textDocument/publishDiagnostics',
6616 'params': {'diagnostics': [],
6617 'uri': '__PHP_FILE_URI__'}}
6619 Context:
6620 This was the most recent request issued from the language client before it
6621 received the notification:
6623 hphp/hack/test/integration/test_lsp.py
6624 XXXX | .request(line=line(), method="shutdown", params={}, result=None)
6626 Remediation:
6627 1) If this was unexpected, then the language server is buggy and should be
6628 fixed.
6630 2) If all notifications of type 'textDocument/publishDiagnostics' should be \
6631 ignored, add this directive
6632 anywhere in your test:
6634 .ignore_notifications(method='textDocument/publishDiagnostics')
6636 3) If this single instance of the notification was expected, add this directive
6637 to your test to wait for it before proceeding:
6639 .wait_for_notification(
6640 method='textDocument/publishDiagnostics',
6641 params={'uri': '${php_file_uri}', 'diagnostics': []},
6644 If you want to examine the raw LSP logs, you can check the `.sent.log` and
6645 `.received.log` files that were generated in the template repo for this test.\
6647 # There's an instance of a literal `${php_file_uri}` in there
6648 # which we don't want to change, so use a different name than
6649 # that one.
6650 .replace("__PHP_FILE_URI__", variables["php_file_uri"]),
6653 def test_serverless_ide_highlight(self) -> None:
6654 variables = dict(self.prepare_serverless_ide_environment())
6655 variables.update(self.setup_php_file("highlight.php"))
6656 self.test_driver.stop_hh_server()
6658 spec = (
6659 self.initialize_spec(
6660 LspTestSpec("serverless_ide_highlight"), use_serverless_ide=True
6662 .notification(
6663 method="textDocument/didOpen",
6664 params={
6665 "textDocument": {
6666 "uri": "${php_file_uri}",
6667 "languageId": "hack",
6668 "version": 1,
6669 "text": "${php_file}",
6673 .request(
6674 line=line(),
6675 comment="document highlight, id 2",
6676 method="textDocument/documentHighlight",
6677 params={
6678 "textDocument": {"uri": "${php_file_uri}"},
6679 "position": {"line": 3, "character": 10},
6681 result=[
6683 "range": {
6684 "start": {"line": 3, "character": 9},
6685 "end": {"line": 3, "character": 20},
6689 powered_by="serverless_ide",
6691 .request(
6692 line=line(),
6693 comment="shutdown, id 3",
6694 method="shutdown",
6695 params={},
6696 result=None,
6699 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6701 def test_serverless_ide_coverage(self) -> None:
6702 variables = dict(self.prepare_serverless_ide_environment())
6703 variables.update(self.setup_php_file("coverage.php"))
6704 self.test_driver.stop_hh_server()
6706 spec = (
6707 self.initialize_spec(
6708 LspTestSpec("serverless_ide_coverage"), use_serverless_ide=True
6710 .notification(
6711 method="textDocument/didOpen",
6712 params={
6713 "textDocument": {
6714 "uri": "${php_file_uri}",
6715 "languageId": "hack",
6716 "version": 1,
6717 "text": "${php_file}",
6721 .request(
6722 line=line(),
6723 comment="Check type coverage",
6724 method="textDocument/typeCoverage",
6725 params={"textDocument": {"uri": "${php_file_uri}"}},
6726 result={
6727 "coveredPercent": 100,
6728 "uncoveredRanges": [],
6729 "defaultMessage": "Un-type checked code. Consider adding type annotations.",
6731 powered_by="serverless_ide",
6733 .request(
6734 line=line(),
6735 comment="Shutdown",
6736 method="shutdown",
6737 params={},
6738 result=None,
6741 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6743 def test_status_stopped(self) -> None:
6744 self.prepare_server_environment()
6745 variables = self.setup_php_file("hover.php")
6746 self.test_driver.stop_hh_server()
6748 spec = (
6749 self.initialize_spec(
6750 LspTestSpec("status_stopped"),
6751 use_serverless_ide=False,
6752 supports_status=True,
6754 .wait_for_server_request(
6755 method="window/showStatus",
6756 params={
6757 "shortMessage": "Hack: stopped",
6758 "message": "hh_server: stopped.",
6759 "actions": [{"title": "Restart hh_server"}],
6760 "type": 1,
6762 result=NoResponse(),
6764 .request(line=line(), method="shutdown", params={}, result=None)
6765 .notification(method="exit", params={})
6767 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=False)
6769 def test_status_running(self) -> None:
6770 self.prepare_server_environment()
6771 variables = self.setup_php_file("hover.php")
6773 spec = (
6774 self.initialize_spec(
6775 LspTestSpec("status_running"),
6776 use_serverless_ide=False,
6777 supports_status=True,
6779 .ignore_requests(
6780 comment="Ignore initializing... requests since they're racy",
6781 method="window/showStatus",
6782 params={
6783 "type": 2,
6784 "shortMessage": "Hack: initializing",
6785 "message": "hh_server initializing: processing [<test> seconds]",
6786 "actions": [],
6789 .wait_for_server_request(
6790 method="window/showStatus",
6791 params={"actions": [], "message": "hh_server: ready.", "type": 3},
6792 result=NoResponse(),
6794 .request(line=line(), method="shutdown", params={}, result=None)
6795 .notification(method="exit", params={})
6797 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
6799 def test_serverless_ide_status_stopped(self) -> None:
6800 variables = dict(self.prepare_serverless_ide_environment())
6801 variables.update(self.setup_php_file("hover.php"))
6802 self.test_driver.stop_hh_server()
6804 spec = (
6805 self.initialize_spec(
6806 LspTestSpec("serverless_ide_status_stopped"),
6807 use_serverless_ide=True,
6808 supports_status=True,
6810 .ignore_requests(
6811 comment="ignore initializing... messages since they're kind of racy",
6812 method="window/showStatus",
6813 params={
6814 "type": 2,
6815 "actions": [{"title": "Restart hh_server"}],
6816 "message": "Hack IDE: initializing.\nhh_server: stopped.",
6817 "shortMessage": "Hack: initializing",
6820 .ignore_requests(
6821 comment="another racy initialization to ignore, before hh_server has even reported its status",
6822 method="window/showStatus",
6823 params={
6824 "type": 2,
6825 "actions": [],
6826 "message": "Hack IDE: initializing.",
6827 "shortMessage": "Hack: initializing",
6830 .ignore_requests(
6831 comment="another racy initialization to ignore, again before hh_server",
6832 method="window/showStatus",
6833 params={
6834 "type": 3,
6835 "actions": [],
6836 "message": "Hack IDE: ready.",
6837 "shortMessage": "Hack: ready",
6840 .wait_for_server_request(
6841 method="window/showStatus",
6842 params={
6843 "message": "Hack IDE: ready.\nhh_server: stopped.",
6844 "shortMessage": "Hack: ready",
6845 "actions": [{"title": "Restart hh_server"}],
6846 "type": 3,
6848 result=NoResponse(),
6850 .request(line=line(), method="shutdown", params={}, result=None)
6851 .notification(method="exit", params={})
6853 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6855 def test_serverless_ide_status_restart(self) -> None:
6856 variables = dict(self.prepare_serverless_ide_environment())
6857 variables.update(self.setup_php_file("hover.php"))
6859 spec = (
6860 self.initialize_spec(
6861 LspTestSpec("serverless_ide_status_restart"),
6862 use_serverless_ide=True,
6863 supports_status=True,
6865 .ignore_requests(
6866 comment="Ignore initializing messages since they're racy",
6867 method="window/showStatus",
6868 params={
6869 "type": 2,
6870 "actions": [],
6871 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
6872 "shortMessage": "Hack: initializing",
6875 .ignore_requests(
6876 comment="Another form of initializing to ignore",
6877 method="window/showStatus",
6878 params={
6879 "type": 2,
6880 "actions": [],
6881 "message": "Hack IDE: initializing.\nhh_server: ready.",
6882 "shortMessage": "Hack: initializing",
6885 .ignore_requests(
6886 comment="Another form of initializing to ignore before we've even heard the first peep from hh_server",
6887 method="window/showStatus",
6888 params={
6889 "type": 2,
6890 "actions": [],
6891 "message": "Hack IDE: initializing.",
6892 "shortMessage": "Hack: initializing",
6895 .ignore_requests(
6896 comment="another racy initialization to ignore, again before hh_server",
6897 method="window/showStatus",
6898 params={
6899 "type": 3,
6900 "actions": [],
6901 "message": "Hack IDE: ready.",
6902 "shortMessage": "Hack: ready",
6905 .wait_for_server_request(
6906 method="window/showStatus",
6907 params={
6908 "actions": [],
6909 "message": "Hack IDE: ready.\nhh_server: ready.",
6910 "shortMessage": "Hack: ready",
6911 "type": 3,
6913 result=NoResponse(),
6915 .request(
6916 line=line(),
6917 method="$test/shutdownServerlessIde",
6918 params={},
6919 result=None,
6920 powered_by="serverless_ide",
6922 .wait_for_server_request(
6923 method="window/showStatus",
6924 params={
6925 "actions": [{"title": "Restart Hack IDE"}],
6926 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6927 "shortMessage": "Hack: failed",
6928 "type": 1,
6930 result={"title": "Restart Hack IDE"},
6932 .wait_for_server_request(
6933 method="window/showStatus",
6934 params={
6935 "actions": [],
6936 "message": "Hack IDE: ready.\nhh_server: ready.",
6937 "shortMessage": "Hack: ready",
6938 "type": 3,
6940 result=NoResponse(),
6942 .request(line=line(), method="shutdown", params={}, result=None)
6943 .notification(method="exit", params={})
6945 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6947 def test_serverless_ide_failed_to_load_saved_state(self) -> None:
6948 variables = dict(self.prepare_serverless_ide_environment())
6949 variables.update(self.setup_php_file("hover.php"))
6950 assert "naming_table_saved_state_path" in variables
6951 variables["naming_table_saved_state_path"] = "/tmp/nonexistent"
6953 spec = (
6954 self.initialize_spec(
6955 LspTestSpec("serverless_ide_status_failed_to_load_saved_state"),
6956 use_serverless_ide=True,
6957 supports_status=True,
6958 supports_init=True,
6960 .ignore_requests(
6961 comment="Ignore initializing since they're kind of racy",
6962 method="window/showStatus",
6963 params={
6964 "type": 2,
6965 "actions": [],
6966 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
6967 "shortMessage": "Hack: initializing",
6970 .ignore_requests(
6971 comment="Ignore another form of initializing",
6972 method="window/showStatus",
6973 params={
6974 "type": 2,
6975 "actions": [],
6976 "message": "Hack IDE: initializing.\nhh_server: ready.",
6977 "shortMessage": "Hack: initializing",
6980 .ignore_requests(
6981 comment="Ignore another form of initializing, from before we've even heard the first peep out of hh_server",
6982 method="window/showStatus",
6983 params={
6984 "type": 2,
6985 "actions": [],
6986 "message": "Hack IDE: initializing.",
6987 "shortMessage": "Hack: initializing",
6990 .ignore_requests(
6991 comment="Ignore another form of initializing, again before hh_server",
6992 method="window/showStatus",
6993 params={
6994 "type": 1,
6995 "actions": [{"title": "Restart Hack IDE"}],
6996 "message": "Hack IDE has failed. See Output›Hack for details.",
6997 "shortMessage": "Hack: failed",
7000 .wait_for_notification(
7001 method="window/logMessage",
7002 params={
7003 "type": 1,
7004 "message": "Hack IDE has failed.\nThis is unexpected.\nPlease file a bug within your IDE.\nMore details: http://dummy/HH_TEST_MODE",
7007 .wait_for_server_request(
7008 method="window/showStatus",
7009 params={
7010 "actions": [{"title": "Restart Hack IDE"}],
7011 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
7012 "shortMessage": "Hack: failed",
7013 "type": 1,
7015 result=NoResponse(),
7017 .request(line=line(), method="shutdown", params={}, result=None)
7018 .notification(method="exit", params={})
7020 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
7022 def test_workspace_symbol(self) -> None:
7023 self.prepare_server_environment()
7024 variables = self.setup_php_file("didchange.php")
7025 spec = (
7026 self.initialize_spec(
7027 LspTestSpec("test_workspace_symbol"), use_serverless_ide=False
7029 .wait_for_hh_server_ready()
7030 .request(
7031 line=line(),
7032 comment="Look up symbols",
7033 method="workspace/symbol",
7034 params={"query": "TestNS\\test"},
7035 result=[
7037 "name": "TestNS\\test_func",
7038 "kind": 12,
7039 "location": {
7040 "uri": "file://${root_path}/completion_extras_namespace.php",
7041 "range": {
7042 "start": {"line": 4, "character": 9},
7043 "end": {"line": 4, "character": 25},
7049 .request(
7050 line=line(),
7051 comment="Look up symbols starting with 'test_f' within multiple namespaces",
7052 method="workspace/symbol",
7053 params={"query": "test_f"},
7054 result=[
7056 "name": "test_function",
7057 "kind": 12,
7058 "location": {
7059 "uri": "file://${root_path}/completion.php",
7060 "range": {
7061 "start": {"line": 7, "character": 9},
7062 "end": {"line": 7, "character": 22},
7067 "name": "TestNS\\test_func",
7068 "kind": 12,
7069 "location": {
7070 "uri": "file://${root_path}/completion_extras_namespace.php",
7071 "range": {
7072 "start": {"line": 4, "character": 9},
7073 "end": {"line": 4, "character": 25},
7079 .request(line=line(), method="shutdown", params={}, result=None)
7080 .notification(method="exit", params={})
7082 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
7084 def test_serverless_ide_during_hh_server_restart(self) -> None:
7085 variables = dict(self.prepare_serverless_ide_environment())
7086 variables.update(self.setup_php_file("didchange.php"))
7087 spec = (
7088 self.initialize_spec(
7089 LspTestSpec("test_serverless_ide_during_hh_server_restart"),
7090 use_serverless_ide=True,
7092 .notification(
7093 method="textDocument/didOpen",
7094 params={
7095 "textDocument": {
7096 "uri": "${php_file_uri}",
7097 "languageId": "hack",
7098 "version": 1,
7099 "text": "${php_file}",
7103 .notification(
7104 comment="Send a 'didChange' notification before HH Server is functional.",
7105 method="textDocument/didChange",
7106 params={
7107 "textDocument": {"uri": "${php_file_uri}"},
7108 "contentChanges": [
7110 "range": {
7111 "start": {"line": 7, "character": 9},
7112 "end": {"line": 7, "character": 11},
7114 "text": "'foo'",
7119 .start_hh_server("Start HH Server; should detect the bad edit")
7120 .wait_for_notification(
7121 method="textDocument/publishDiagnostics",
7122 params={
7123 "uri": "${php_file_uri}",
7124 "diagnostics": [
7126 "code": 4110,
7127 "message": "Invalid return type",
7128 "range": {
7129 "end": {"character": 14, "line": 7},
7130 "start": {"character": 9, "line": 7},
7132 "relatedInformation": [
7134 "location": {
7135 "range": {
7136 "end": {"character": 27, "line": 6},
7137 "start": {"character": 24, "line": 6},
7139 "uri": "${php_file_uri}",
7141 "message": "Expected int",
7144 "location": {
7145 "range": {
7146 "end": {"character": 14, "line": 7},
7147 "start": {"character": 9, "line": 7},
7149 "uri": "${php_file_uri}",
7151 "message": "But got string",
7154 "relatedLocations": [
7156 "location": {
7157 "range": {
7158 "end": {"character": 27, "line": 6},
7159 "start": {"character": 24, "line": 6},
7161 "uri": "${php_file_uri}",
7163 "message": "Expected int",
7166 "location": {
7167 "range": {
7168 "end": {"character": 14, "line": 7},
7169 "start": {"character": 9, "line": 7},
7171 "uri": "${php_file_uri}",
7173 "message": "But got string",
7176 "severity": 1,
7177 "source": "Hack",
7182 .stop_hh_server("Shutdown HH Server")
7183 .start_hh_server("Restart HH Server")
7184 .wait_for_notification(
7185 comment="On startup it thinks everything is okay ...",
7186 method="textDocument/publishDiagnostics",
7187 params={"uri": "${php_file_uri}", "diagnostics": []},
7189 .wait_for_notification(
7190 comment="But then hh_server sends a hello message and it gets the edited files, which leads it to see the problem.",
7191 method="textDocument/publishDiagnostics",
7192 params={
7193 "uri": "${php_file_uri}",
7194 "diagnostics": [
7196 "code": 4110,
7197 "message": "Invalid return type",
7198 "range": {
7199 "end": {"character": 14, "line": 7},
7200 "start": {"character": 9, "line": 7},
7202 "relatedInformation": [
7204 "location": {
7205 "range": {
7206 "end": {"character": 27, "line": 6},
7207 "start": {"character": 24, "line": 6},
7209 "uri": "${php_file_uri}",
7211 "message": "Expected int",
7214 "location": {
7215 "range": {
7216 "end": {"character": 14, "line": 7},
7217 "start": {"character": 9, "line": 7},
7219 "uri": "${php_file_uri}",
7221 "message": "But got string",
7224 "relatedLocations": [
7226 "location": {
7227 "range": {
7228 "end": {"character": 27, "line": 6},
7229 "start": {"character": 24, "line": 6},
7231 "uri": "${php_file_uri}",
7233 "message": "Expected int",
7236 "location": {
7237 "range": {
7238 "end": {"character": 14, "line": 7},
7239 "start": {"character": 9, "line": 7},
7241 "uri": "${php_file_uri}",
7243 "message": "But got string",
7246 "severity": 1,
7247 "source": "Hack",
7252 .request(line=line(), method="shutdown", params={}, result=None)
7253 .notification(method="exit", params={})
7255 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
7257 def test_serverless_ide_naming_error1(self) -> None:
7258 variables = dict(self.prepare_serverless_ide_environment())
7259 variables.update(self.setup_php_file("didchange.php"))
7260 variables.update(
7262 "main_file": self.repo_file("main.php"),
7263 "main_file_contents": """\
7264 <?hh
7265 function main(): int {
7266 return aaa();
7268 """,
7269 "file_a": self.repo_file("a.php"),
7270 "file_b": self.repo_file("b.php"),
7273 spec = (
7274 self.initialize_spec(
7275 LspTestSpec("serverless_ide_naming_error1"), use_serverless_ide=True
7277 .write_to_disk(
7278 uri="${main_file}", contents="${main_file_contents}", notify=True
7280 .notification(
7281 method="textDocument/didOpen",
7282 params={
7283 "textDocument": {
7284 "uri": "${main_file}",
7285 "languageId": "hack",
7286 "version": 1,
7287 "text": "${main_file_contents}",
7291 .request(
7292 line=line(),
7293 comment="Ensure that hover over `aaa` works even when the name is not yet defined",
7294 method="textDocument/hover",
7295 params={
7296 "textDocument": {"uri": "${main_file}"},
7297 "position": {"line": 2, "character": 13},
7299 result={
7300 "contents": [{"language": "hack", "value": "_"}],
7301 "range": {
7302 "start": {"line": 2, "character": 11},
7303 "end": {"line": 2, "character": 14},
7306 powered_by="serverless_ide",
7308 .write_to_disk(
7309 comment="create file A",
7310 uri="${file_a}",
7311 contents="""\
7312 <?hh
7313 function aaa(): int {
7314 return 1;
7316 """,
7317 notify=True,
7319 .request(
7320 line=line(),
7321 comment="Ensure that hover over `aaa` works when there are no naming errors",
7322 method="textDocument/hover",
7323 params={
7324 "textDocument": {"uri": "${main_file}"},
7325 "position": {"line": 2, "character": 13},
7327 result={
7328 "contents": [
7329 {"language": "hack", "value": "function aaa(): int"},
7331 "range": {
7332 "start": {"line": 2, "character": 11},
7333 "end": {"line": 2, "character": 14},
7336 powered_by="serverless_ide",
7338 .write_to_disk(
7339 comment="create file B",
7340 uri="${file_b}",
7341 contents="""\
7342 <?hh
7343 function aaa(): string {
7344 return "foo";
7346 """,
7347 notify=True,
7349 .request(
7350 line=line(),
7351 comment="Ensure that hover over `aaa` works even when there is a duplicate name",
7352 method="textDocument/hover",
7353 params={
7354 "textDocument": {"uri": "${main_file}"},
7355 "position": {"line": 2, "character": 13},
7357 result={
7358 "contents": [
7359 {"language": "hack", "value": "function aaa(): int"},
7361 "range": {
7362 "start": {"line": 2, "character": 11},
7363 "end": {"line": 2, "character": 14},
7366 powered_by="serverless_ide",
7368 .write_to_disk(
7369 comment="delete file A", uri="${file_a}", contents=None, notify=True
7371 .request(
7372 line=line(),
7373 comment="Now that we've fixed the error, hover should work.",
7374 method="textDocument/hover",
7375 params={
7376 "textDocument": {"uri": "${main_file}"},
7377 "position": {"line": 2, "character": 13},
7379 result={
7380 "contents": [
7381 {"language": "hack", "value": "function aaa(): string"},
7383 "range": {
7384 "start": {"line": 2, "character": 11},
7385 "end": {"line": 2, "character": 14},
7388 powered_by="serverless_ide",
7390 .request(line=line(), method="shutdown", params={}, result=None)
7391 .notification(method="exit", params={})
7393 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7395 def test_serverless_ide_naming_error2(self) -> None:
7396 variables = dict(self.prepare_serverless_ide_environment())
7397 self.test_driver.stop_hh_server()
7398 variables.update(self.setup_php_file("naming_error_caller.php"))
7399 variables.update(
7401 "contents": self.read_repo_file("naming_error_declaration.php"),
7402 "original": self.repo_file("naming_error_declaration.php"),
7403 "copy": self.repo_file("naming_error_copy.php"),
7406 spec = (
7407 self.initialize_spec(
7408 LspTestSpec("serverless_ide_naming_error2"), use_serverless_ide=True
7410 .notification(
7411 method="textDocument/didOpen",
7412 params={
7413 "textDocument": {
7414 "uri": "${php_file_uri}",
7415 "languageId": "hack",
7416 "version": 1,
7417 "text": "${php_file}",
7421 .write_to_disk(
7422 comment="create copy",
7423 uri="${copy}",
7424 contents="${contents}",
7425 notify=True,
7427 .write_to_disk(
7428 comment="delete copy", uri="${copy}", contents=None, notify=True
7430 .request(
7431 line=line(),
7432 comment="hover should work fine after making copy then deleting copy.",
7433 method="textDocument/hover",
7434 params={
7435 "textDocument": {"uri": "${php_file_uri}"},
7436 "position": {"line": 3, "character": 15},
7438 result={
7439 "contents": [
7441 "language": "hack",
7442 "value": "function naming_error_declaration(): void",
7445 "range": {
7446 "start": {"line": 3, "character": 2},
7447 "end": {"line": 3, "character": 26},
7450 powered_by="serverless_ide",
7452 .request(line=line(), method="shutdown", params={}, result=None)
7453 .notification(method="exit", params={})
7455 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7457 def test_serverless_ide_naming_error3(self) -> None:
7458 variables = dict(self.prepare_serverless_ide_environment())
7459 self.test_driver.stop_hh_server()
7460 variables.update(self.setup_php_file("naming_error_caller.php"))
7461 variables.update(
7463 "contents": self.read_repo_file("naming_error_declaration.php"),
7464 "original": self.repo_file("naming_error_declaration.php"),
7465 "copy": self.repo_file("naming_error_copy.php"),
7468 spec = (
7469 self.initialize_spec(
7470 LspTestSpec("serverless_ide_naming_error3"), use_serverless_ide=True
7472 .notification(
7473 method="textDocument/didOpen",
7474 params={
7475 "textDocument": {
7476 "uri": "${php_file_uri}",
7477 "languageId": "hack",
7478 "version": 1,
7479 "text": "${php_file}",
7483 .write_to_disk(
7484 comment="create copy",
7485 uri="${copy}",
7486 contents="${contents}",
7487 notify=True,
7489 .write_to_disk(
7490 comment="delete original", uri="${original}", contents=None, notify=True
7492 .request(
7493 line=line(),
7494 comment="hover should work fine after making copy then deleting original.",
7495 method="textDocument/hover",
7496 params={
7497 "textDocument": {"uri": "${php_file_uri}"},
7498 "position": {"line": 3, "character": 15},
7500 result={
7501 "contents": [
7503 "language": "hack",
7504 "value": "function naming_error_declaration(): void",
7507 "range": {
7508 "start": {"line": 3, "character": 2},
7509 "end": {"line": 3, "character": 26},
7512 powered_by="serverless_ide",
7514 .request(line=line(), method="shutdown", params={}, result=None)
7515 .notification(method="exit", params={})
7517 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7519 def test_serverless_ide_requests_before_init(self) -> None:
7520 variables = dict(self.prepare_serverless_ide_environment())
7521 variables["root_path"] = self.test_driver.repo_dir
7522 self.test_driver.stop_hh_server()
7524 spec = (
7525 self.initialize_spec(
7526 LspTestSpec("test_serverless_ide_requests_before_init"),
7527 use_serverless_ide=True,
7528 supports_status=True,
7529 supports_init=True,
7531 .ignore_notifications(method="textDocument/publishDiagnostics")
7532 .ignore_requests(
7533 comment="Ignore 'initializing...' messages since they're racy",
7534 method="window/showStatus",
7535 params={
7536 "type": 2,
7537 "actions": [{"title": "Restart hh_server"}],
7538 "message": "Hack IDE: initializing.\nhh_server: stopped.",
7539 "shortMessage": "Hack: initializing",
7542 .ignore_requests(
7543 comment="another racy initialization, before we've yet heard from hh_server",
7544 method="window/showStatus",
7545 params={
7546 "type": 2,
7547 "actions": [],
7548 "message": "Hack IDE: initializing.",
7549 "shortMessage": "Hack: initializing",
7552 .ignore_requests(
7553 comment="another racy initialization, if HackIDE is done before hh_server has yet sent status",
7554 method="window/showStatus",
7555 params={
7556 "type": 3,
7557 "actions": [],
7558 "message": "Hack IDE: ready.",
7559 "shortMessage": "Hack: ready",
7562 .write_to_disk(
7563 notify=True,
7564 wait=False,
7565 uri="file://${root_path}/beforeInit1.php",
7566 contents="<?hh // strict\nfunction beforeInit1(): int {\n return 42;\n}\n",
7568 .notification(
7569 comment="open a file before init has finished",
7570 method="textDocument/didOpen",
7571 params={
7572 "textDocument": {
7573 "uri": "file://${root_path}/beforeInit2.php",
7574 "languageId": "hack",
7575 "version": 1,
7576 "text": "<?hh // strict\nfunction beforeInit2(): void {\n $foo = beforeInit1();\n}\n",
7580 .request(
7581 line=line(),
7582 comment="hover before init will fail",
7583 method="textDocument/hover",
7584 params={
7585 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
7586 "position": {"line": 2, "character": 4},
7588 result=None,
7590 .request(
7591 line=line(),
7592 comment="documentSymbol before init will succeed",
7593 method="textDocument/documentSymbol",
7594 params={"textDocument": {"uri": "file://${root_path}/beforeInit2.php"}},
7595 result=[
7597 "name": "beforeInit2",
7598 "kind": 12,
7599 "location": {
7600 "uri": "file://${root_path}/beforeInit2.php",
7601 "range": {
7602 "start": {"line": 1, "character": 0},
7603 "end": {"line": 3, "character": 1},
7608 powered_by="serverless_ide",
7610 .wait_for_notification(
7611 comment="wait for sIDE to init",
7612 method="telemetry/event",
7613 params={"type": 4, "message": "[client-ide] Finished init: ok"},
7615 .wait_for_server_request(
7616 method="window/showStatus",
7617 params={
7618 "actions": [{"title": "Restart hh_server"}],
7619 "message": "Hack IDE: ready.\nhh_server: stopped.",
7620 "shortMessage": "Hack: ready",
7621 "type": 3,
7623 result=NoResponse(),
7625 .request(
7626 line=line(),
7627 comment="hover after init will succeed",
7628 method="textDocument/hover",
7629 params={
7630 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
7631 "position": {"line": 2, "character": 4},
7633 result={
7634 "contents": [{"language": "hack", "value": "int"}],
7635 "range": {
7636 "start": {"line": 2, "character": 2},
7637 "end": {"line": 2, "character": 6},
7640 powered_by="serverless_ide",
7642 .request(line=line(), method="shutdown", params={}, result=None)
7643 .notification(method="exit", params={})
7646 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7648 def test_serverless_ide_workspace_symbol(self) -> None:
7649 variables = dict(self.prepare_serverless_ide_environment())
7650 variables["root_path"] = self.test_driver.repo_dir
7651 self.test_driver.stop_hh_server()
7653 spec = (
7654 self.initialize_spec(
7655 LspTestSpec("serverless_ide_workspace_symbol"), use_serverless_ide=True
7657 .request(
7658 line=line(),
7659 comment="workspace symbol call, global, powered by sqlite (generated during serverless-ide-init)",
7660 method="workspace/symbol",
7661 params={"query": "TakesString"},
7662 result=[
7664 "name": "TakesString",
7665 "kind": 5,
7666 "location": {
7667 "uri": "file://${root_path}/definition.php",
7668 "range": {
7669 "start": {"line": 36, "character": 6},
7670 "end": {"line": 36, "character": 17},
7675 powered_by="serverless_ide",
7677 .request(
7678 line=line(),
7679 comment="workspace symbol call, member (derived from naming-table)",
7680 method="workspace/symbol",
7681 params={"query": "TakesString::"},
7682 result=[
7684 "name": "__construct",
7685 "kind": 6,
7686 "location": {
7687 "uri": "file://${root_path}/definition.php",
7688 "range": {
7689 "start": {"line": 37, "character": 18},
7690 "end": {"line": 37, "character": 29},
7695 powered_by="serverless_ide",
7697 .request(line=line(), method="shutdown", params={}, result=None)
7698 .notification(method="exit", params={})
7700 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)