Remove repeated return type in hover
[hiphop-php.git] / hphp / hack / test / integration / test_lsp.py
blob1bc7fbc0bcb779bfc41aa5928f08becb7093ed56
1 # pyre-strict
2 # flake8: noqa: B950
4 from __future__ import absolute_import, division, print_function, unicode_literals
6 import copy
7 import enum
8 import json
9 import os
10 import re
11 import unittest
12 import urllib.parse
13 from typing import Iterable, List, Mapping, Tuple
15 import common_tests
16 from hh_paths import hh_server
17 from lspcommand import LspCommandProcessor, Transcript
18 from lsptestspec import LspTestSpec, NoResponse, line
19 from test_case import TestCase
20 from utils import Json, JsonObject, interpolate_variables
23 class InsertTextFormat(enum.Enum):
24 PlainText = 1
25 Snippet = 2
28 class LspTestDriver(common_tests.CommonTestDriver):
29 def write_load_config(
30 self, use_serverless_ide: bool = False, use_saved_state: bool = False
31 ) -> None:
32 # Will use the .hhconfig already in the repo directory
33 # As for hh.conf, we'll write it explicitly each test.
34 with open(os.path.join(self.repo_dir, "hh.conf"), "w") as f:
35 f.write(
36 """
37 use_watchman = true
38 watchman_subscribe_v2 = true
39 interrupt_on_watchman = true
40 interrupt_on_client = true
41 max_workers = 2
42 load_state_natively_v4 = {use_saved_state}
43 use_mini_state = {use_saved_state}
44 require_mini_state = {use_saved_state}
45 lazy_decl = {use_saved_state}
46 lazy_parse = {use_saved_state}
47 lazy_init2 = {use_saved_state}
48 symbolindex_search_provider = SqliteIndex
49 allow_unstable_features = true
50 ide_serverless = {use_serverless_ide}
51 """.format(
52 use_saved_state=str(use_saved_state).lower(),
53 use_serverless_ide=str(use_serverless_ide).lower(),
57 def write_naming_table_saved_state(self) -> str:
58 naming_table_saved_state_path = os.path.join(
59 self.repo_dir, "naming_table_saved_state.sqlite"
61 (stdout, stderr, retcode) = self.proc_call(
63 hh_server,
64 "--check",
65 self.repo_dir,
66 "--save-naming",
67 naming_table_saved_state_path,
70 assert retcode == 0, (
71 f"Failed to save naming table saved state: {retcode}\n"
72 + f"STDOUT:\n{stdout}\n"
73 + f"STDERR:\n{stderr}\n"
75 return naming_table_saved_state_path
78 class TestLsp(TestCase[LspTestDriver]):
79 @classmethod
80 def get_test_driver(cls) -> LspTestDriver:
81 return LspTestDriver()
83 @classmethod
84 def get_template_repo(cls) -> str:
85 return "hphp/hack/test/integration/data/lsp_exchanges/"
87 def repo_file(self, file: str) -> str:
88 return os.path.join(self.test_driver.repo_dir, file)
90 def read_repo_file(self, file: str) -> str:
91 with open(self.repo_file(file), "r") as f:
92 return f.read()
94 def repo_file_uri(self, file: str) -> str:
95 return urllib.parse.urljoin("file://", self.repo_file(file))
97 # pyre-fixme[11]: Annotation `Json` is not defined as a type.
98 def parse_test_data(self, file: str, variables: Mapping[str, str]) -> Json:
99 text = self.read_repo_file(file)
100 data: Json = json.loads(text)
101 data = interpolate_variables(data, variables)
102 return data
104 def load_test_data(
105 self, test_name: str, variables: Mapping[str, str]
106 ) -> Tuple[Json, Json]:
107 test = self.parse_test_data(test_name + ".json", variables)
108 expected = self.parse_test_data(test_name + ".expected", variables)
109 return (test, expected)
111 def write_observed(self, test_name: str, observed_transcript: Json) -> None:
112 file = os.path.join(self.test_driver.template_repo, test_name + ".observed.log")
113 text = json.dumps(
114 list(self.get_important_received_items(observed_transcript)), indent=2
116 with open(file, "w") as f:
117 f.write(text)
119 # pyre-fixme[11]: Annotation `JsonObject` is not defined as a type.
120 def order_response(self, response: JsonObject) -> str:
121 if "id" in response:
122 return str(response["id"])
123 else:
124 return json.dumps(response, indent=2)
126 # sorts a list of responses using the 'id' parameter so they can be
127 # compared in sequence even if they came back from the server out of sequence.
128 # this can happen based on how json rpc is specified to work.
129 # if 'id' isn't present the response is a notification. we sort notifications
130 # by their entire text.
131 def sort_responses(self, responses: Iterable[JsonObject]) -> List[JsonObject]:
132 return sorted(responses, key=lambda response: self.order_response(response))
134 # removes stack traces from error responses since these can be noisy
135 # as code changes and they contain execution environment specific details
136 # by ignoring these when comparing responses we might miss some minor issues
137 # but will still catch the core error being thrown or not.
138 def sanitize_exceptions(
139 self, responses: Iterable[JsonObject]
140 ) -> Iterable[JsonObject]:
141 sanitized = copy.deepcopy(responses)
142 for response in sanitized:
143 if "error" in response:
144 if "data" in response["error"]:
145 if "stack" in response["error"]["data"]:
146 del response["error"]["data"]["stack"]
147 if "current_stack" in response["error"]["data"]:
148 del response["error"]["data"]["current_stack"]
149 if "server_finale_stack" in response["error"]["data"]:
150 del response["error"]["data"]["server_finale_stack"]
151 return sanitized
153 # dumps an LSP response into a standard json format that can be used for
154 # doing precise text comparison in a way that is human readable in the case
155 # of there being an error.
156 def serialize_responses(self, responses: Iterable[Json]) -> List[str]:
157 return [json.dumps(response, indent=2) for response in responses]
159 # generates received responses from an LSP communication transcript
160 # ignoring the non-deterministic ones "progress" and "actionRequired"
161 def get_important_received_items(self, transcript: Transcript) -> Iterable[Json]:
162 for entry in transcript.values():
163 received = entry.received or None
164 if received is None:
165 continue
166 method = received.get("method") or ""
167 if method in [
168 "window/progress",
169 "window/actionRequired",
170 "window/showStatus",
171 "telemetry/event",
173 continue
174 yield received
176 # gets a set of loaded responses ready for validation by sorting them
177 # by id and serializing them for precise text comparison
178 def prepare_responses(self, responses: Iterable[JsonObject]) -> List[str]:
179 return self.serialize_responses(
180 self.sanitize_exceptions(self.sort_responses(responses))
183 def run_lsp_test(
184 self,
185 test_name: str,
186 test: Json,
187 expected: Json,
188 wait_for_server: bool,
189 use_serverless_ide: bool,
190 ) -> None:
191 if wait_for_server:
192 assert not use_serverless_ide, (
193 "Warning: both `wait_for_server` and `use_serverless_ide` "
194 + "were set to `True` for testing in "
195 + self.run_lsp_test.__name__
196 + ". "
197 + "While this is a possible test case, it hasn't been written yet, "
198 + "so it's more likely that this is a mistake "
199 + "and you're accidentally relying on hh_server to fulfill "
200 + "serverless IDE requests."
201 + "(If you're writing that test, "
202 + "then it's time to remove this assertion.)"
205 # wait until hh_server is ready before starting lsp
206 self.test_driver.run_check()
207 elif use_serverless_ide:
208 self.test_driver.stop_hh_server()
210 with LspCommandProcessor.create(self.test_driver.test_env) as lsp:
211 observed_transcript = lsp.communicate(test)
213 self.write_observed(test_name, observed_transcript)
215 expected_items = self.prepare_responses(expected)
216 observed_items = self.prepare_responses(
217 list(self.get_important_received_items(observed_transcript))
220 if not use_serverless_ide:
221 # If the server's busy, maybe the machine's just under too much
222 # pressure to give results in a timely fashion. Doing a retry would
223 # only defer the question of what to do in that case, so instead
224 # we'll just skip.
225 self.throw_on_skip(observed_transcript)
227 # validation checks that the number of items matches and that
228 # the responses are exactly identical to what we expect
229 self.assertEqual(
230 len(expected_items),
231 len(observed_items),
232 "Wrong count. Observed this:\n"
233 + json.dumps(observed_transcript, indent=2, separators=(",", ": ")),
235 for i in range(len(expected_items)):
236 self.assertEqual(expected_items[i], observed_items[i])
238 def throw_on_skip(self, transcript: Transcript) -> None:
239 failure_messages = ["Server busy", "timed out"]
240 for entry in transcript.values():
241 received = entry.received
242 if received is None:
243 continue
244 if received.get("error"):
245 message = received["error"]["message"]
246 for failure_message in failure_messages:
247 if failure_message in message:
248 raise unittest.SkipTest(message)
250 def prepare_server_environment(self) -> None:
251 self.maxDiff = None
252 self.test_driver.write_load_config(use_serverless_ide=False)
253 self.test_driver.start_hh_server()
254 (output, err, _) = self.test_driver.run_check()
255 if "Error: Ran out of retries" in err:
256 raise unittest.SkipTest("Hack server could not be launched")
257 self.assertEqual(output.strip(), "No errors!")
259 def prepare_serverless_ide_environment(self) -> Mapping[str, str]:
260 self.maxDiff = None
261 self.test_driver.write_load_config(
262 use_serverless_ide=True, use_saved_state=False
264 naming_table_saved_state_path = (
265 self.test_driver.write_naming_table_saved_state()
267 return {"naming_table_saved_state_path": naming_table_saved_state_path}
269 def load_and_run(
270 self,
271 test_name: str,
272 variables: Mapping[str, str],
273 wait_for_server: bool = True,
274 use_serverless_ide: bool = False,
275 ) -> None:
276 test, expected = self.load_test_data(test_name, variables)
277 self.run_lsp_test(
278 test_name=test_name,
279 test=test,
280 expected=expected,
281 wait_for_server=wait_for_server,
282 use_serverless_ide=use_serverless_ide,
285 def run_spec(
286 self,
287 spec: LspTestSpec,
288 variables: Mapping[str, str],
289 wait_for_server: bool,
290 use_serverless_ide: bool,
291 ) -> None:
292 if wait_for_server:
293 # wait until hh_server is ready before starting lsp
294 self.test_driver.run_check()
295 elif use_serverless_ide:
296 self.test_driver.stop_hh_server()
298 with LspCommandProcessor.create(
299 self.test_driver.test_env
300 ) as lsp_command_processor:
301 (observed_transcript, error_details) = spec.run(
302 lsp_command_processor=lsp_command_processor, variables=variables
304 file = os.path.join(self.test_driver.template_repo, spec.name + ".sent.log")
305 text = json.dumps(
307 sent
308 for sent, _received in observed_transcript.values()
309 if sent is not None
311 indent=2,
313 with open(file, "w") as f:
314 f.write(text)
316 file = os.path.join(self.test_driver.template_repo, spec.name + ".received.log")
317 text = json.dumps(
319 received
320 for _sent, received in observed_transcript.values()
321 if received is not None
323 indent=2,
325 with open(file, "w") as f:
326 f.write(text)
328 if not use_serverless_ide:
329 # If the server's busy, maybe the machine's just under too much
330 # pressure to give results in a timely fashion. Doing a retry would
331 # only defer the question of what to do in that case, so instead
332 # we'll just skip.
333 self.throw_on_skip(observed_transcript)
335 if error_details is not None:
336 raise AssertionError(error_details)
338 def setup_php_file(self, test_php: str) -> Mapping[str, str]:
339 # We want the path to the builtins directory. This is best we can do.
340 (output, err, retcode) = self.test_driver.run_check(
341 options=["--identify-function", "2:21", "--json"],
342 stdin="<?hh\nfunction f():void {PHP_EOL;}\n",
344 if retcode == 7:
345 self.skipTest(
346 "Could not discover builtins directory -- "
347 + "got exit code 7 (either Out_of_time or Out_of_retries). "
348 + "The test machine is likely under too much load."
350 self.assertEqual(retcode, 0)
351 constants_path = json.loads(output)[0]["definition_pos"]["filename"]
352 return {
353 "hhi_path": re.sub("/constants.hhi$", "", constants_path),
354 "root_path": self.test_driver.repo_dir,
355 "php_file_uri": self.repo_file_uri(test_php),
356 "php_file": self.read_repo_file(test_php),
359 def test_init_shutdown(self) -> None:
360 self.prepare_server_environment()
362 self.load_and_run(
363 "initialize_shutdown", {"root_path": self.test_driver.repo_dir}
366 def test_serverless_ide_completion(self) -> None:
367 variables = dict(self.prepare_serverless_ide_environment())
368 variables.update(self.setup_php_file("completion.php"))
369 self.test_driver.stop_hh_server()
370 spec = (
371 self.initialize_spec(LspTestSpec("ide_completion"), use_serverless_ide=True)
372 .notification(
373 method="textDocument/didOpen",
374 params={
375 "textDocument": {
376 "uri": "${php_file_uri}",
377 "languageId": "hack",
378 "version": 1,
379 "text": "${php_file}",
383 .notification(
384 comment="Add '$x = $point1['' to test autocomplete for shapes",
385 method="textDocument/didChange",
386 params={
387 "textDocument": {"uri": "${php_file_uri}"},
388 "contentChanges": [
390 "range": {
391 "start": {"line": 22, "character": 0},
392 "end": {"line": 22, "character": 0},
394 "text": "$x = $point1['",
399 .request(
400 line=line(),
401 comment="autocomplete after user types a shape",
402 method="textDocument/completion",
403 params={
404 "textDocument": {"uri": "${php_file_uri}"},
405 "position": {"line": 22, "character": 14},
407 result={
408 "isIncomplete": False,
409 "items": [
411 "label": "'x'",
412 "kind": 12,
413 "detail": "literal",
414 "inlineDetail": "literal",
415 "sortText": "'x'",
416 "insertText": "'x'",
417 "insertTextFormat": InsertTextFormat.PlainText.value,
418 "data": {
419 "fullname": "'x'",
420 "filename": "${root_path}/completion.php",
421 "line": 22,
422 "char": 19,
426 "label": "'y'",
427 "kind": 12,
428 "detail": "literal",
429 "inlineDetail": "literal",
430 "sortText": "'y'",
431 "insertText": "'y'",
432 "insertTextFormat": InsertTextFormat.PlainText.value,
433 "data": {
434 "fullname": "'y'",
435 "filename": "${root_path}/completion.php",
436 "line": 22,
437 "char": 30,
442 powered_by="serverless_ide",
444 .notification(
445 comment="Add automatically closed apostrophes when typing a shape key, the way visual studio code does it",
446 method="textDocument/didChange",
447 params={
448 "textDocument": {"uri": "${php_file_uri}"},
449 "contentChanges": [
451 "range": {
452 "start": {"line": 22, "character": 0},
453 "end": {"line": 22, "character": 14},
455 "text": "$x = $point1['']",
460 .request(
461 line=line(),
462 comment="autocomplete after a shape, with VS Code automatically closed apostrophes",
463 method="textDocument/completion",
464 params={
465 "textDocument": {"uri": "${php_file_uri}"},
466 "position": {"line": 22, "character": 14},
468 result={
469 "isIncomplete": False,
470 "items": [
472 "label": "'x",
473 "kind": 12,
474 "detail": "literal",
475 "inlineDetail": "literal",
476 "sortText": "'x",
477 "insertText": "'x",
478 "insertTextFormat": InsertTextFormat.PlainText.value,
479 "data": {
480 "fullname": "'x'",
481 "filename": "${root_path}/completion.php",
482 "line": 22,
483 "char": 19,
487 "label": "'y",
488 "kind": 12,
489 "detail": "literal",
490 "inlineDetail": "literal",
491 "sortText": "'y",
492 "insertText": "'y",
493 "insertTextFormat": InsertTextFormat.PlainText.value,
494 "data": {
495 "fullname": "'y'",
496 "filename": "${root_path}/completion.php",
497 "line": 22,
498 "char": 30,
503 powered_by="serverless_ide",
505 .notification(
506 comment="Add '$x = <'",
507 method="textDocument/didChange",
508 params={
509 "textDocument": {"uri": "${php_file_uri}"},
510 "contentChanges": [
512 "range": {
513 "start": {"line": 3, "character": 0},
514 "end": {"line": 3, "character": 0},
516 "text": "$x = <",
521 .request(
522 line=line(),
523 comment="autocomplete after '$x = <'",
524 method="textDocument/completion",
525 params={
526 "textDocument": {"uri": "${php_file_uri}"},
527 "position": {"line": 3, "character": 6},
529 result={
530 "isIncomplete": False,
531 "items": [
533 "label": "ab:cd:alpha",
534 "kind": 7,
535 "detail": "class",
536 "inlineDetail": "class",
537 "sortText": "ab:cd:alpha",
538 "insertText": "ab:cd:alpha",
539 "insertTextFormat": InsertTextFormat.PlainText.value,
540 "data": {"fullname": ":ab:cd:alpha"},
543 "label": "ab:cd:text",
544 "kind": 7,
545 "detail": "class",
546 "inlineDetail": "class",
547 "sortText": "ab:cd:text",
548 "insertText": "ab:cd:text",
549 "insertTextFormat": InsertTextFormat.PlainText.value,
550 "data": {"fullname": ":ab:cd:text"},
553 "label": "xhp:enum-attribute",
554 "kind": 7,
555 "detail": "class",
556 "inlineDetail": "class",
557 "sortText": "xhp:enum-attribute",
558 "insertText": "xhp:enum-attribute",
559 "insertTextFormat": InsertTextFormat.PlainText.value,
560 "data": {"fullname": ":xhp:enum-attribute"},
563 "label": "xhp:generic",
564 "kind": 7,
565 "detail": "class",
566 "inlineDetail": "class",
567 "sortText": "xhp:generic",
568 "insertText": "xhp:generic",
569 "insertTextFormat": InsertTextFormat.PlainText.value,
570 "data": {"fullname": ":xhp:generic"},
574 powered_by="serverless_ide",
576 .notification(
577 comment="Add '$x = <a'",
578 method="textDocument/didChange",
579 params={
580 "textDocument": {"uri": "${php_file_uri}"},
581 "contentChanges": [
583 "range": {
584 "start": {"line": 3, "character": 0},
585 "end": {"line": 3, "character": 6},
587 "text": "$x = <a",
592 .request(
593 line=line(),
594 comment="autocomplete after '$x = <a'",
595 method="textDocument/completion",
596 params={
597 "textDocument": {"uri": "${php_file_uri}"},
598 "position": {"line": 3, "character": 7},
600 result={
601 "isIncomplete": False,
602 "items": [
604 "label": "ab:cd:alpha",
605 "kind": 7,
606 "detail": "class",
607 "inlineDetail": "class",
608 "sortText": "ab:cd:alpha",
609 "insertText": "ab:cd:alpha",
610 "insertTextFormat": InsertTextFormat.PlainText.value,
611 "data": {"fullname": ":ab:cd:alpha"},
614 "label": "ab:cd:text",
615 "kind": 7,
616 "detail": "class",
617 "inlineDetail": "class",
618 "sortText": "ab:cd:text",
619 "insertText": "ab:cd:text",
620 "insertTextFormat": InsertTextFormat.PlainText.value,
621 "data": {"fullname": ":ab:cd:text"},
625 powered_by="serverless_ide",
627 .notification(
628 comment="Add '$x = <ab:'",
629 method="textDocument/didChange",
630 params={
631 "textDocument": {"uri": "${php_file_uri}"},
632 "contentChanges": [
634 "range": {
635 "start": {"line": 3, "character": 0},
636 "end": {"line": 3, "character": 7},
638 "text": "$x = <ab:",
643 .request(
644 line=line(),
645 comment="autocomplete after '$x = <ab:'",
646 method="textDocument/completion",
647 params={
648 "textDocument": {"uri": "${php_file_uri}"},
649 "position": {"line": 3, "character": 9},
651 result={
652 "isIncomplete": False,
653 "items": [
655 "label": "ab:cd:alpha",
656 "kind": 7,
657 "detail": "class",
658 "inlineDetail": "class",
659 "sortText": "ab:cd:alpha",
660 "insertText": "ab:cd:alpha",
661 "insertTextFormat": InsertTextFormat.PlainText.value,
662 "data": {"fullname": ":ab:cd:alpha"},
665 "label": "ab:cd:text",
666 "kind": 7,
667 "detail": "class",
668 "inlineDetail": "class",
669 "sortText": "ab:cd:text",
670 "insertText": "ab:cd:text",
671 "insertTextFormat": InsertTextFormat.PlainText.value,
672 "data": {"fullname": ":ab:cd:text"},
676 powered_by="serverless_ide",
678 .notification(
679 comment="Add '$x = <ab:cd:text '",
680 method="textDocument/didChange",
681 params={
682 "textDocument": {"uri": "${php_file_uri}"},
683 "contentChanges": [
685 "range": {
686 "start": {"line": 3, "character": 0},
687 "end": {"line": 3, "character": 9},
689 "text": "$x = <ab:cd:text ",
694 .request(
695 line=line(),
696 comment="autocomplete after '$x = <ab:cd:text '",
697 method="textDocument/completion",
698 params={
699 "textDocument": {"uri": "${php_file_uri}"},
700 "position": {"line": 3, "character": 17},
702 result={
703 "isIncomplete": False,
704 "items": [
706 "label": "width",
707 "kind": 10,
708 "detail": "?int",
709 "inlineDetail": "?int",
710 "sortText": "width",
711 "insertText": "width",
712 "insertTextFormat": InsertTextFormat.PlainText.value,
713 "data": {
714 "fullname": ":width",
715 "filename": "${root_path}/xhp_class_definitions.php",
716 "line": 5,
717 "char": 27,
718 "base_class": "\\:ab:cd:text",
722 "label": "color",
723 "kind": 10,
724 "detail": "?string",
725 "inlineDetail": "?string",
726 "sortText": "color",
727 "insertText": "color",
728 "insertTextFormat": InsertTextFormat.PlainText.value,
729 "data": {
730 "fullname": ":color",
731 "filename": "${root_path}/xhp_class_definitions.php",
732 "line": 5,
733 "char": 13,
734 "base_class": "\\:ab:cd:text",
739 powered_by="serverless_ide",
741 .notification(
742 comment="Add '$x = <ab:cd:text w'",
743 method="textDocument/didChange",
744 params={
745 "textDocument": {"uri": "${php_file_uri}"},
746 "contentChanges": [
748 "range": {
749 "start": {"line": 3, "character": 0},
750 "end": {"line": 3, "character": 17},
752 "text": "$x = <ab:cd:text w",
757 .request(
758 line=line(),
759 comment="autocomplete after '$x = <ab:cd:text w'",
760 method="textDocument/completion",
761 params={
762 "textDocument": {"uri": "${php_file_uri}"},
763 "position": {"line": 3, "character": 18},
765 result={
766 "isIncomplete": False,
767 "items": [
769 "label": "width",
770 "kind": 10,
771 "detail": "?int",
772 "inlineDetail": "?int",
773 "sortText": "width",
774 "insertText": "width",
775 "insertTextFormat": InsertTextFormat.PlainText.value,
776 "data": {
777 "fullname": ":width",
778 "filename": "${root_path}/xhp_class_definitions.php",
779 "line": 5,
780 "char": 27,
781 "base_class": "\\:ab:cd:text",
785 "label": "color",
786 "kind": 10,
787 "detail": "?string",
788 "inlineDetail": "?string",
789 "sortText": "color",
790 "insertText": "color",
791 "insertTextFormat": InsertTextFormat.PlainText.value,
792 "data": {
793 "fullname": ":color",
794 "filename": "${root_path}/xhp_class_definitions.php",
795 "line": 5,
796 "char": 13,
797 "base_class": "\\:ab:cd:text",
802 powered_by="serverless_ide",
804 .notification(
805 comment="Add '$x = new :'",
806 method="textDocument/didChange",
807 params={
808 "textDocument": {"uri": "${php_file_uri}"},
809 "contentChanges": [
811 "range": {
812 "start": {"line": 3, "character": 0},
813 "end": {"line": 3, "character": 18},
815 "text": "$x = new :",
820 .request(
821 line=line(),
822 comment="autocomplete after '$x = new :'",
823 method="textDocument/completion",
824 params={
825 "textDocument": {"uri": "${php_file_uri}"},
826 "position": {"line": 3, "character": 10},
828 result={
829 "isIncomplete": False,
830 "items": [
832 "label": ":ab:cd:alpha",
833 "kind": 7,
834 "detail": "class",
835 "inlineDetail": "class",
836 "sortText": ":ab:cd:alpha",
837 "insertText": ":ab:cd:alpha",
838 "insertTextFormat": InsertTextFormat.PlainText.value,
839 "data": {"fullname": ":ab:cd:alpha"},
842 "label": ":ab:cd:text",
843 "kind": 7,
844 "detail": "class",
845 "inlineDetail": "class",
846 "sortText": ":ab:cd:text",
847 "insertText": ":ab:cd:text",
848 "insertTextFormat": InsertTextFormat.PlainText.value,
849 "data": {"fullname": ":ab:cd:text"},
852 "label": ":xhp:enum-attribute",
853 "kind": 7,
854 "detail": "class",
855 "inlineDetail": "class",
856 "sortText": ":xhp:enum-attribute",
857 "insertText": ":xhp:enum-attribute",
858 "insertTextFormat": InsertTextFormat.PlainText.value,
859 "data": {"fullname": ":xhp:enum-attribute"},
862 "label": ":xhp:generic",
863 "kind": 7,
864 "detail": "class",
865 "inlineDetail": "class",
866 "sortText": ":xhp:generic",
867 "insertText": ":xhp:generic",
868 "insertTextFormat": InsertTextFormat.PlainText.value,
869 "data": {"fullname": ":xhp:generic"},
873 powered_by="serverless_ide",
875 .notification(
876 comment="Add '$x = new :a'",
877 method="textDocument/didChange",
878 params={
879 "textDocument": {"uri": "${php_file_uri}"},
880 "contentChanges": [
882 "range": {
883 "start": {"line": 3, "character": 0},
884 "end": {"line": 3, "character": 10},
886 "text": "$x = new :a",
891 .request(
892 line=line(),
893 comment="autocomplete after '$x = new :a'",
894 method="textDocument/completion",
895 params={
896 "textDocument": {"uri": "${php_file_uri}"},
897 "position": {"line": 3, "character": 11},
899 result={
900 "isIncomplete": False,
901 "items": [
903 "label": ":ab:cd:alpha",
904 "kind": 7,
905 "detail": "class",
906 "inlineDetail": "class",
907 "sortText": ":ab:cd:alpha",
908 "insertText": ":ab:cd:alpha",
909 "insertTextFormat": InsertTextFormat.PlainText.value,
910 "data": {"fullname": ":ab:cd:alpha"},
913 "label": ":ab:cd:text",
914 "kind": 7,
915 "detail": "class",
916 "inlineDetail": "class",
917 "sortText": ":ab:cd:text",
918 "insertText": ":ab:cd:text",
919 "insertTextFormat": InsertTextFormat.PlainText.value,
920 "data": {"fullname": ":ab:cd:text"},
924 powered_by="serverless_ide",
926 # Note that this request should match the result in the previous example
927 .request(
928 line=line(),
929 comment="autocomplete resolving after '$x = new :a'",
930 method="completionItem/resolve",
931 params={
932 "label": ":ab:cd:alpha",
933 "kind": 7,
934 "detail": "class",
935 "inlineDetail": "class",
936 "itemType": ":ab:cd:alpha",
937 "insertText": ":ab:cd:alpha",
938 "insertTextFormat": InsertTextFormat.PlainText.value,
939 "data": {"fullname": ":ab:cd:alpha"},
941 result={
942 "label": ":ab:cd:alpha",
943 "kind": 7,
944 "detail": "class",
945 "inlineDetail": "class",
946 "itemType": ":ab:cd:alpha",
947 "documentation": {
948 "kind": "markdown",
949 "value": ":ab:cd:alpha docblock",
951 "insertText": ":ab:cd:alpha",
952 "insertTextFormat": InsertTextFormat.PlainText.value,
953 "data": {"fullname": ":ab:cd:alpha"},
955 powered_by="serverless_ide",
957 # Try the same thing again, but this time without "new", instead using "<xhp" style
958 .notification(
959 comment="Add '$x = <a'",
960 method="textDocument/didChange",
961 params={
962 "textDocument": {"uri": "${php_file_uri}"},
963 "contentChanges": [
965 "range": {
966 "start": {"line": 3, "character": 0},
967 "end": {"line": 3, "character": 11},
969 "text": "$x = <a",
974 .request(
975 line=line(),
976 comment="autocomplete after '$x = <a'",
977 method="textDocument/completion",
978 params={
979 "textDocument": {"uri": "${php_file_uri}"},
980 "position": {"line": 3, "character": 7},
982 result={
983 "isIncomplete": False,
984 "items": [
986 "label": "ab:cd:alpha",
987 "kind": 7,
988 "detail": "class",
989 "inlineDetail": "class",
990 "sortText": "ab:cd:alpha",
991 "insertText": "ab:cd:alpha",
992 "insertTextFormat": InsertTextFormat.PlainText.value,
993 "data": {"fullname": ":ab:cd:alpha"},
996 "label": "ab:cd:text",
997 "kind": 7,
998 "detail": "class",
999 "inlineDetail": "class",
1000 "sortText": "ab:cd:text",
1001 "insertText": "ab:cd:text",
1002 "insertTextFormat": InsertTextFormat.PlainText.value,
1003 "data": {"fullname": ":ab:cd:text"},
1007 powered_by="serverless_ide",
1009 .request(
1010 line=line(),
1011 comment="autocomplete resolving after '$x = <a'",
1012 method="completionItem/resolve",
1013 params={
1014 "label": "ab:cd:alpha",
1015 "kind": 7,
1016 "detail": "class",
1017 "inlineDetail": "class",
1018 "insertText": "ab:cd:alpha",
1019 "insertTextFormat": InsertTextFormat.PlainText.value,
1020 "data": {"fullname": ":ab:cd:alpha"},
1022 result={
1023 "label": "ab:cd:alpha",
1024 "kind": 7,
1025 "detail": "class",
1026 "inlineDetail": "class",
1027 "documentation": {
1028 "kind": "markdown",
1029 "value": ":ab:cd:alpha docblock",
1031 "insertText": "ab:cd:alpha",
1032 "insertTextFormat": InsertTextFormat.PlainText.value,
1033 "data": {"fullname": ":ab:cd:alpha"},
1035 powered_by="serverless_ide",
1037 .notification(
1038 comment="Add '$x = <ab:cd:text/>; $y = $x->'",
1039 method="textDocument/didChange",
1040 params={
1041 "textDocument": {"uri": "${php_file_uri}"},
1042 "contentChanges": [
1044 "range": {
1045 "start": {"line": 3, "character": 0},
1046 "end": {"line": 3, "character": 7},
1048 "text": "$x = <ab:cd:text/>; $y = $x->",
1053 .request(
1054 line=line(),
1055 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
1056 method="textDocument/completion",
1057 params={
1058 "textDocument": {"uri": "${php_file_uri}"},
1059 "position": {"line": 3, "character": 29},
1061 result={
1062 "isIncomplete": False,
1063 "items": [
1065 "label": ":width",
1066 "kind": 10,
1067 "detail": "?int",
1068 "inlineDetail": "?int",
1069 "sortText": ":width",
1070 "insertText": ":width",
1071 "insertTextFormat": InsertTextFormat.PlainText.value,
1072 "data": {
1073 "fullname": ":width",
1074 "filename": "${root_path}/xhp_class_definitions.php",
1075 "line": 5,
1076 "char": 27,
1077 "base_class": "\\:ab:cd:text",
1081 "label": ":color",
1082 "kind": 10,
1083 "detail": "?string",
1084 "inlineDetail": "?string",
1085 "sortText": ":color",
1086 "insertText": ":color",
1087 "insertTextFormat": InsertTextFormat.PlainText.value,
1088 "data": {
1089 "fullname": ":color",
1090 "filename": "${root_path}/xhp_class_definitions.php",
1091 "line": 5,
1092 "char": 13,
1093 "base_class": "\\:ab:cd:text",
1098 powered_by="serverless_ide",
1100 .notification(
1101 comment="Add '$x = <ab:cd:text/>; $y = $x->:'",
1102 method="textDocument/didChange",
1103 params={
1104 "textDocument": {"uri": "${php_file_uri}"},
1105 "contentChanges": [
1107 "range": {
1108 "start": {"line": 3, "character": 0},
1109 "end": {"line": 3, "character": 29},
1111 "text": "$x = <ab:cd:text/>; $y = $x->:",
1116 .request(
1117 line=line(),
1118 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
1119 method="textDocument/completion",
1120 params={
1121 "textDocument": {"uri": "${php_file_uri}"},
1122 "position": {"line": 3, "character": 30},
1124 result={
1125 "isIncomplete": False,
1126 "items": [
1128 "label": ":width",
1129 "kind": 10,
1130 "detail": "?int",
1131 "inlineDetail": "?int",
1132 "sortText": ":width",
1133 "insertText": ":width",
1134 "insertTextFormat": InsertTextFormat.PlainText.value,
1135 "data": {
1136 "fullname": ":width",
1137 "filename": "${root_path}/xhp_class_definitions.php",
1138 "line": 5,
1139 "char": 27,
1140 "base_class": "\\:ab:cd:text",
1144 "label": ":color",
1145 "kind": 10,
1146 "detail": "?string",
1147 "inlineDetail": "?string",
1148 "sortText": ":color",
1149 "insertText": ":color",
1150 "insertTextFormat": InsertTextFormat.PlainText.value,
1151 "data": {
1152 "fullname": ":color",
1153 "filename": "${root_path}/xhp_class_definitions.php",
1154 "line": 5,
1155 "char": 13,
1156 "base_class": "\\:ab:cd:text",
1161 powered_by="serverless_ide",
1163 .notification(
1164 comment="Add 'test_fun'",
1165 method="textDocument/didChange",
1166 params={
1167 "textDocument": {"uri": "${php_file_uri}"},
1168 "contentChanges": [
1170 "range": {
1171 "start": {"line": 3, "character": 0},
1172 "end": {"line": 3, "character": 30},
1174 "text": "test_fun",
1179 .request(
1180 line=line(),
1181 comment="autocomplete after 'test_fun'",
1182 method="textDocument/completion",
1183 params={
1184 "textDocument": {"uri": "${php_file_uri}"},
1185 "position": {"line": 3, "character": 8},
1187 result={
1188 "isIncomplete": False,
1189 "items": [
1191 "label": "test_function",
1192 "kind": 3,
1193 "detail": "function",
1194 "inlineDetail": "function",
1195 "sortText": "test_function",
1196 "insertText": "test_function",
1197 "insertTextFormat": InsertTextFormat.PlainText.value,
1198 "data": {"fullname": "test_function"},
1202 powered_by="serverless_ide",
1204 .request(
1205 line=line(),
1206 comment="autocomplete resolving after 'test_fun'",
1207 method="completionItem/resolve",
1208 params={
1209 "label": "test_function",
1210 "kind": 3,
1211 "detail": "function(): void",
1212 "inlineDetail": "()",
1213 "itemType": "void",
1214 "insertText": "test_function",
1215 "insertTextFormat": InsertTextFormat.PlainText.value,
1216 "data": {
1217 "filename": "${root_path}/completion.php",
1218 "line": 8,
1219 "char": 10,
1222 result={
1223 "label": "test_function",
1224 "kind": 3,
1225 "detail": "function(): void",
1226 "inlineDetail": "()",
1227 "itemType": "void",
1228 "documentation": {
1229 "kind": "markdown",
1230 "value": "test_function docblock.",
1232 "insertText": "test_function",
1233 "insertTextFormat": InsertTextFormat.PlainText.value,
1234 "data": {
1235 "filename": "${root_path}/completion.php",
1236 "line": 8,
1237 "char": 10,
1240 powered_by="serverless_ide",
1242 .notification(
1243 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
1244 method="textDocument/didChange",
1245 params={
1246 "textDocument": {"uri": "${php_file_uri}"},
1247 "contentChanges": [
1249 "range": {
1250 "start": {"line": 3, "character": 0},
1251 "end": {"line": 3, "character": 8},
1253 "text": "switch (Elsa::Alonso) { case Elsa:",
1258 .request(
1259 line=line(),
1260 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
1261 method="textDocument/completion",
1262 params={
1263 "textDocument": {"uri": "${php_file_uri}"},
1264 "position": {"line": 3, "character": 34},
1266 result={"isIncomplete": False, "items": []},
1267 powered_by="serverless_ide",
1269 .notification(
1270 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
1271 method="textDocument/didChange",
1272 params={
1273 "textDocument": {"uri": "${php_file_uri}"},
1274 "contentChanges": [
1276 "range": {
1277 "start": {"line": 3, "character": 0},
1278 "end": {"line": 3, "character": 34},
1280 "text": "switch (Elsa::Alonso) { case Elsa::",
1285 .request(
1286 line=line(),
1287 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
1288 method="textDocument/completion",
1289 params={
1290 "textDocument": {"uri": "${php_file_uri}"},
1291 "position": {"line": 3, "character": 35},
1293 result={
1294 "isIncomplete": False,
1295 "items": [
1297 "label": "class",
1298 "kind": 21,
1299 "detail": "classname<this>",
1300 "inlineDetail": "classname<this>",
1301 "sortText": "class",
1302 "insertText": "class",
1303 "insertTextFormat": InsertTextFormat.PlainText.value,
1304 "data": {
1305 "fullname": "class",
1306 "filename": "${root_path}/completion_extras.php",
1307 "line": 3,
1308 "char": 6,
1309 "base_class": "\\Elsa",
1313 "label": "Bard",
1314 "kind": 21,
1315 "detail": "Elsa",
1316 "inlineDetail": "Elsa",
1317 "sortText": "Bard",
1318 "insertText": "Bard",
1319 "insertTextFormat": InsertTextFormat.PlainText.value,
1320 "data": {
1321 "fullname": "Bard",
1322 "filename": "${root_path}/completion_extras.php",
1323 "line": 3,
1324 "char": 12,
1325 "base_class": "\\Elsa",
1329 "label": "Alonso",
1330 "kind": 21,
1331 "detail": "Elsa",
1332 "inlineDetail": "Elsa",
1333 "sortText": "Alonso",
1334 "insertText": "Alonso",
1335 "insertTextFormat": InsertTextFormat.PlainText.value,
1336 "data": {
1337 "fullname": "Alonso",
1338 "filename": "${root_path}/completion_extras.php",
1339 "line": 3,
1340 "char": 12,
1341 "base_class": "\\Elsa",
1345 "label": "isValid",
1346 "kind": 2,
1347 "detail": "function(mixed $value): bool",
1348 "inlineDetail": "(mixed $value)",
1349 "itemType": "bool",
1350 "sortText": "isValid",
1351 "insertText": "isValid(${1:\\$value})",
1352 "insertTextFormat": InsertTextFormat.Snippet.value,
1353 "data": {
1354 "fullname": "isValid",
1355 "filename": "${hhi_path}/BuiltinEnum.hhi",
1356 "line": 46,
1357 "char": 32,
1358 "base_class": "\\Elsa",
1362 "label": "getValues",
1363 "kind": 2,
1364 "detail": "function(): dict<string, Elsa>",
1365 "inlineDetail": "()",
1366 "itemType": "dict<string, Elsa>",
1367 "sortText": "getValues",
1368 "insertText": "getValues()",
1369 "insertTextFormat": InsertTextFormat.Snippet.value,
1370 "data": {
1371 "fullname": "getValues",
1372 "filename": "${hhi_path}/BuiltinEnum.hhi",
1373 "line": 33,
1374 "char": 32,
1375 "base_class": "\\Elsa",
1379 "label": "getNames",
1380 "kind": 2,
1381 "detail": "function(): dict<Elsa, string>",
1382 "inlineDetail": "()",
1383 "itemType": "dict<Elsa, string>",
1384 "sortText": "getNames",
1385 "insertText": "getNames()",
1386 "insertTextFormat": InsertTextFormat.Snippet.value,
1387 "data": {
1388 "fullname": "getNames",
1389 "filename": "${hhi_path}/BuiltinEnum.hhi",
1390 "line": 41,
1391 "char": 32,
1392 "base_class": "\\Elsa",
1396 "label": "coerce",
1397 "kind": 2,
1398 "detail": "function(mixed $value): ?Elsa",
1399 "inlineDetail": "(mixed $value)",
1400 "itemType": "?Elsa",
1401 "sortText": "coerce",
1402 "insertText": "coerce(${1:\\$value})",
1403 "insertTextFormat": InsertTextFormat.Snippet.value,
1404 "data": {
1405 "fullname": "coerce",
1406 "filename": "${hhi_path}/BuiltinEnum.hhi",
1407 "line": 52,
1408 "char": 32,
1409 "base_class": "\\Elsa",
1413 "label": "assertAll",
1414 "kind": 2,
1415 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
1416 "inlineDetail": "(Traversable<mixed> $values)",
1417 "itemType": "Container<Elsa>",
1418 "sortText": "assertAll",
1419 "insertText": "assertAll(${1:\\$values})",
1420 "insertTextFormat": InsertTextFormat.Snippet.value,
1421 "data": {
1422 "fullname": "assertAll",
1423 "filename": "${hhi_path}/BuiltinEnum.hhi",
1424 "line": 64,
1425 "char": 32,
1426 "base_class": "\\Elsa",
1430 "label": "assert",
1431 "kind": 2,
1432 "detail": "function(mixed $value): Elsa",
1433 "inlineDetail": "(mixed $value)",
1434 "itemType": "Elsa",
1435 "sortText": "assert",
1436 "insertText": "assert(${1:\\$value})",
1437 "insertTextFormat": InsertTextFormat.Snippet.value,
1438 "data": {
1439 "fullname": "assert",
1440 "filename": "${hhi_path}/BuiltinEnum.hhi",
1441 "line": 58,
1442 "char": 32,
1443 "base_class": "\\Elsa",
1448 powered_by="serverless_ide",
1450 .notification(
1451 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1452 method="textDocument/didChange",
1453 params={
1454 "textDocument": {"uri": "${php_file_uri}"},
1455 "contentChanges": [
1457 "range": {
1458 "start": {"line": 3, "character": 0},
1459 "end": {"line": 3, "character": 35},
1461 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
1466 .request(
1467 line=line(),
1468 comment="docblock resolve after 'switch (Elsa::Alonso) { case Elsa::'",
1469 method="completionItem/resolve",
1470 params={
1471 "label": "isValid",
1472 "kind": 2,
1473 "detail": "function(mixed $value): bool",
1474 "inlineDetail": "(mixed $value)",
1475 "itemType": "bool",
1476 "insertTextFormat": InsertTextFormat.PlainText.value,
1477 "textEdit": {
1478 "range": {
1479 "start": {"line": 3, "character": 35},
1480 "end": {"line": 3, "character": 35},
1482 "newText": "isValid",
1484 "data": {
1485 "filename": "${hhi_path}/BuiltinEnum.hhi",
1486 "line": 46,
1487 "char": 32,
1490 result={
1491 "label": "isValid",
1492 "kind": 2,
1493 "detail": "function(mixed $value): bool",
1494 "inlineDetail": "(mixed $value)",
1495 "itemType": "bool",
1496 "documentation": {
1497 "kind": "markdown",
1498 "value": "Returns whether or not the value is defined as a constant.",
1500 "insertTextFormat": InsertTextFormat.PlainText.value,
1501 "textEdit": {
1502 "range": {
1503 "start": {"line": 3, "character": 35},
1504 "end": {"line": 3, "character": 35},
1506 "newText": "isValid",
1508 "data": {
1509 "filename": "${hhi_path}/BuiltinEnum.hhi",
1510 "line": 46,
1511 "char": 32,
1514 powered_by="serverless_ide",
1516 .request(
1517 line=line(),
1518 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
1519 method="textDocument/completion",
1520 params={
1521 "textDocument": {"uri": "${php_file_uri}"},
1522 "position": {"line": 3, "character": 42},
1524 result={"isIncomplete": False, "items": []},
1525 powered_by="serverless_ide",
1527 .notification(
1528 comment="Add 'TestNS\\'",
1529 method="textDocument/didChange",
1530 params={
1531 "textDocument": {"uri": "${php_file_uri}"},
1532 "contentChanges": [
1534 "range": {
1535 "start": {"line": 3, "character": 0},
1536 "end": {"line": 3, "character": 42},
1538 "text": "TestNS\\",
1543 .request(
1544 line=line(),
1545 comment="autocomplete after 'TestNS\\'",
1546 method="textDocument/completion",
1547 params={
1548 "textDocument": {"uri": "${php_file_uri}"},
1549 "position": {"line": 3, "character": 7},
1551 result={
1552 "isIncomplete": False,
1553 "items": [
1555 "label": "test_func",
1556 "kind": 3,
1557 "detail": "function",
1558 "inlineDetail": "function",
1559 "sortText": "test_func",
1560 "insertText": "test_func",
1561 "insertTextFormat": InsertTextFormat.PlainText.value,
1562 "data": {"fullname": "TestNS\\test_func"},
1566 powered_by="serverless_ide",
1568 .notification(
1569 comment="Add '$cc = new CompletionClass(); $cc->interfa'",
1570 method="textDocument/didChange",
1571 params={
1572 "textDocument": {"uri": "${php_file_uri}"},
1573 "contentChanges": [
1575 "range": {
1576 "start": {"line": 3, "character": 0},
1577 "end": {"line": 3, "character": 7},
1579 "text": "$cc = new CompletionClass(); $cc->interfa",
1584 .request(
1585 line=line(),
1586 comment="autocomplete after '$cc = new CompletionClass(); $cc->interfa'",
1587 method="textDocument/completion",
1588 params={
1589 "textDocument": {"uri": "${php_file_uri}"},
1590 "position": {"line": 3, "character": 41},
1592 result={
1593 "isIncomplete": False,
1594 "items": [
1596 "label": "interfaceDocBlockMethod",
1597 "kind": 2,
1598 "detail": "function(): void",
1599 "inlineDetail": "()",
1600 "itemType": "void",
1601 "sortText": "interfaceDocBlockMethod",
1602 "insertText": "interfaceDocBlockMethod()",
1603 "insertTextFormat": InsertTextFormat.Snippet.value,
1604 "data": {
1605 "fullname": "interfaceDocBlockMethod",
1606 "filename": "${root_path}/completion.php",
1607 "line": 18,
1608 "char": 19,
1609 "base_class": "\\CompletionClass",
1614 powered_by="serverless_ide",
1616 .request(
1617 line=line(),
1618 comment="autocomplete resolving after '$cc = new CompletionClass(); $cc->interfa'",
1619 method="completionItem/resolve",
1620 params={
1621 "label": "interfaceDocBlockMethod",
1622 "kind": 2,
1623 "detail": "function(): void",
1624 "inlineDetail": "()",
1625 "itemType": "void",
1626 "insertTextFormat": InsertTextFormat.PlainText.value,
1627 "textEdit": {
1628 "range": {
1629 "start": {"line": 3, "character": 34},
1630 "end": {"line": 3, "character": 41},
1632 "newText": "interfaceDocBlockMethod",
1634 "data": {
1635 "filename": "${root_path}/completion.php",
1636 "line": 18,
1637 "char": 19,
1640 result={
1641 "label": "interfaceDocBlockMethod",
1642 "kind": 2,
1643 "detail": "function(): void",
1644 "inlineDetail": "()",
1645 "itemType": "void",
1646 "insertTextFormat": InsertTextFormat.PlainText.value,
1647 "textEdit": {
1648 "range": {
1649 "start": {"line": 3, "character": 34},
1650 "end": {"line": 3, "character": 41},
1652 "newText": "interfaceDocBlockMethod",
1654 "data": {
1655 "filename": "${root_path}/completion.php",
1656 "line": 18,
1657 "char": 19,
1660 powered_by="serverless_ide",
1662 .notification(
1663 comment="Add 'DeprecatedClass::'",
1664 method="textDocument/didChange",
1665 params={
1666 "textDocument": {"uri": "${php_file_uri}"},
1667 "contentChanges": [
1669 "range": {
1670 "start": {"line": 3, "character": 0},
1671 "end": {"line": 3, "character": 41},
1673 "text": "DeprecatedClass::",
1678 .request(
1679 line=line(),
1680 comment="autocomplete after 'DeprecatedClass::'",
1681 method="textDocument/completion",
1682 params={
1683 "textDocument": {"uri": "${php_file_uri}"},
1684 "position": {"line": 3, "character": 17},
1686 result={
1687 "isIncomplete": False,
1688 "items": [
1690 "label": "class",
1691 "kind": 21,
1692 "detail": "classname<this>",
1693 "inlineDetail": "classname<this>",
1694 "sortText": "class",
1695 "insertText": "class",
1696 "insertTextFormat": InsertTextFormat.PlainText.value,
1697 "data": {
1698 "fullname": "class",
1699 "filename": "${root_path}/completion_extras.php",
1700 "line": 8,
1701 "char": 13,
1702 "base_class": "\\DeprecatedClass",
1706 "label": "test_do_not_use",
1707 "kind": 2,
1708 "detail": "function(): void",
1709 "inlineDetail": "()",
1710 "itemType": "void",
1711 "sortText": "~test_do_not_use",
1712 "insertText": "test_do_not_use()",
1713 "insertTextFormat": InsertTextFormat.Snippet.value,
1714 "data": {
1715 "fullname": "test_do_not_use",
1716 "filename": "${root_path}/completion_extras.php",
1717 "line": 12,
1718 "char": 26,
1719 "base_class": "\\DeprecatedClass",
1723 "label": "getName",
1724 "kind": 2,
1725 "detail": "function(): void",
1726 "inlineDetail": "()",
1727 "itemType": "void",
1728 "sortText": "getName",
1729 "insertText": "getName()",
1730 "insertTextFormat": InsertTextFormat.Snippet.value,
1731 "data": {
1732 "fullname": "getName",
1733 "filename": "${root_path}/completion_extras.php",
1734 "line": 9,
1735 "char": 26,
1736 "base_class": "\\DeprecatedClass",
1740 "label": "getAttributes_DO_NOT_USE",
1741 "kind": 2,
1742 "detail": "function(): void",
1743 "inlineDetail": "()",
1744 "itemType": "void",
1745 "sortText": "~getAttributes_DO_NOT_USE",
1746 "insertText": "getAttributes_DO_NOT_USE()",
1747 "insertTextFormat": InsertTextFormat.Snippet.value,
1748 "data": {
1749 "fullname": "getAttributes_DO_NOT_USE",
1750 "filename": "${root_path}/completion_extras.php",
1751 "line": 11,
1752 "char": 26,
1753 "base_class": "\\DeprecatedClass",
1757 "label": "__getLoader",
1758 "kind": 2,
1759 "detail": "function(): void",
1760 "inlineDetail": "()",
1761 "itemType": "void",
1762 "sortText": "~__getLoader",
1763 "insertText": "__getLoader()",
1764 "insertTextFormat": InsertTextFormat.Snippet.value,
1765 "data": {
1766 "fullname": "__getLoader",
1767 "filename": "${root_path}/completion_extras.php",
1768 "line": 10,
1769 "char": 26,
1770 "base_class": "\\DeprecatedClass",
1775 powered_by="serverless_ide",
1777 .notification(
1778 comment="Add 'call_lambda(3, $m'",
1779 method="textDocument/didChange",
1780 params={
1781 "textDocument": {"uri": "${php_file_uri}"},
1782 "contentChanges": [
1784 "range": {
1785 "start": {"line": 30, "character": 0},
1786 "end": {"line": 30, "character": 0},
1788 "text": " call_lambda(3, $m",
1793 .request(
1794 line=line(),
1795 comment="autocomplete results for 'call_lambda(3, $m'",
1796 method="textDocument/completion",
1797 params={
1798 "textDocument": {"uri": "${php_file_uri}"},
1799 "position": {"line": 30, "character": 19},
1801 result={
1802 "isIncomplete": False,
1803 "items": [
1805 "label": "$mylambda",
1806 "kind": 6,
1807 "detail": "local variable",
1808 "inlineDetail": "(num $n)",
1809 "itemType": "int",
1810 "sortText": "$mylambda",
1811 "insertText": "$mylambda",
1812 "insertTextFormat": InsertTextFormat.PlainText.value,
1813 "data": {
1814 "fullname": "$mylambda",
1815 "filename": "${root_path}/completion.php",
1816 "line": 30,
1817 "char": 15,
1822 powered_by="serverless_ide",
1824 .request(
1825 line=line(),
1826 comment="resolve autocompletion for $mylambda'",
1827 method="completionItem/resolve",
1828 params={
1829 "label": "$mylambda",
1830 "kind": 6,
1831 "detail": "local variable",
1832 "inlineDetail": "(num $n)",
1833 "itemType": "int",
1834 "insertTextFormat": InsertTextFormat.PlainText.value,
1835 "textEdit": {
1836 "range": {
1837 "start": {"line": 30, "character": 17},
1838 "end": {"line": 30, "character": 19},
1840 "newText": "$mylambda",
1842 "data": {
1843 "filename": "${root_path}/completion.php",
1844 "line": 30,
1845 "char": 15,
1848 result={
1849 "label": "$mylambda",
1850 "kind": 6,
1851 "detail": "local variable",
1852 "inlineDetail": "(num $n)",
1853 "itemType": "int",
1854 "insertTextFormat": InsertTextFormat.PlainText.value,
1855 "textEdit": {
1856 "range": {
1857 "start": {"line": 30, "character": 17},
1858 "end": {"line": 30, "character": 19},
1860 "newText": "$mylambda",
1862 "data": {
1863 "filename": "${root_path}/completion.php",
1864 "line": 30,
1865 "char": 15,
1868 powered_by="serverless_ide",
1870 .notification(
1871 comment="Add '<xhp:enum-attribute enum-attribute={}'",
1872 method="textDocument/didChange",
1873 params={
1874 "textDocument": {"uri": "${php_file_uri}"},
1875 "contentChanges": [
1877 "range": {
1878 "start": {"line": 3, "character": 0},
1879 "end": {"line": 3, "character": 17},
1881 "text": "<xhp:enum-attribute enum-attribute={}",
1886 .request(
1887 line=line(),
1888 comment="autocomplete after '<xhp:enum-attribute enum-attribute={'",
1889 method="textDocument/completion",
1890 params={
1891 "textDocument": {"uri": "${php_file_uri}"},
1892 "position": {"line": 3, "character": 36},
1893 "context": {"triggerKind": 2, "triggerCharacter": "{"},
1895 result={
1896 "isIncomplete": False,
1897 "items": [
1899 "label": "MyEnum::TYPE_C",
1900 "kind": 13,
1901 "detail": "enum",
1902 "inlineDetail": "enum",
1903 "sortText": "MyEnum::TYPE_C",
1904 "insertText": "MyEnum::TYPE_C",
1905 "insertTextFormat": 1,
1906 "data": {
1907 "fullname": "MyEnum::TYPE_C",
1908 "filename": "${root_path}/xhp_class_definitions.php",
1909 "line": 13,
1910 "char": 14,
1911 "base_class": "\\MyEnum",
1915 "label": "MyEnum::TYPE_A",
1916 "kind": 13,
1917 "detail": "enum",
1918 "inlineDetail": "enum",
1919 "sortText": "MyEnum::TYPE_A",
1920 "insertTextFormat": 1,
1921 "insertText": "MyEnum::TYPE_A",
1922 "data": {
1923 "fullname": "MyEnum::TYPE_A",
1924 "filename": "${root_path}/xhp_class_definitions.php",
1925 "line": 13,
1926 "char": 14,
1927 "base_class": "\\MyEnum",
1931 "label": "MyEnum::TYPE_B",
1932 "kind": 13,
1933 "detail": "enum",
1934 "inlineDetail": "enum",
1935 "sortText": "MyEnum::TYPE_B",
1936 "insertTextFormat": 1,
1937 "insertText": "MyEnum::TYPE_B",
1938 "data": {
1939 "fullname": "MyEnum::TYPE_B",
1940 "filename": "${root_path}/xhp_class_definitions.php",
1941 "line": 13,
1942 "char": 14,
1943 "base_class": "\\MyEnum",
1948 powered_by="serverless_ide",
1950 .notification(
1951 comment="Add '1 is strin'",
1952 method="textDocument/didChange",
1953 params={
1954 "textDocument": {"uri": "${php_file_uri}"},
1955 "contentChanges": [
1957 "range": {
1958 "start": {"line": 3, "character": 0},
1959 "end": {"line": 3, "character": 37},
1961 "text": "1 is strin",
1966 .request(
1967 line=line(),
1968 comment="autocomplete after '1 is strin'",
1969 method="textDocument/completion",
1970 params={
1971 "textDocument": {"uri": "${php_file_uri}"},
1972 "position": {"line": 3, "character": 10},
1974 result={
1975 "isIncomplete": False,
1976 "items": [
1978 "data": {"fullname": "string"},
1979 "detail": "builtin",
1980 "documentation": {
1981 "kind": "markdown",
1982 "value": "A sequence of zero or more characters. Strings are usually manipulated with functions from the `Str\\` namespace",
1984 "inlineDetail": "builtin",
1985 "insertText": "string",
1986 "insertTextFormat": 1,
1987 "kind": 25,
1988 "label": "string",
1989 "sortText": "string",
1992 "data": {"fullname": "StringBuffer"},
1993 "detail": "class",
1994 "inlineDetail": "class",
1995 "insertText": "StringBuffer",
1996 "insertTextFormat": 1,
1997 "kind": 7,
1998 "label": "StringBuffer",
1999 "sortText": "StringBuffer",
2002 "data": {"fullname": "Stringish"},
2003 "detail": "interface",
2004 "inlineDetail": "interface",
2005 "insertText": "Stringish",
2006 "insertTextFormat": 1,
2007 "kind": 8,
2008 "label": "Stringish",
2009 "sortText": "Stringish",
2012 "data": {"fullname": "StringishObject"},
2013 "detail": "interface",
2014 "inlineDetail": "interface",
2015 "insertText": "StringishObject",
2016 "insertTextFormat": 1,
2017 "kind": 8,
2018 "label": "StringishObject",
2019 "sortText": "StringishObject",
2023 powered_by="serverless_ide",
2025 .request(
2026 line=line(),
2027 comment="autocomplete resolving after '1 is strin'",
2028 method="completionItem/resolve",
2029 params={
2030 "data": {"fullname": "string"},
2031 "detail": "builtin",
2032 "documentation": {
2033 "kind": "markdown",
2034 "value": "A sequence of zero or more characters. Strings are usually manipulated with functions from the `Str\\` namespace",
2036 "inlineDetail": "builtin",
2037 "insertText": "string",
2038 "insertTextFormat": 1,
2039 "kind": 25,
2040 "label": "string",
2041 "sortText": "string",
2043 result={
2044 "data": {"fullname": "string"},
2045 "detail": "builtin",
2046 "documentation": {
2047 "kind": "markdown",
2048 "value": "A sequence of zero or more characters. Strings are usually manipulated with functions from the `Str\\` namespace",
2050 "inlineDetail": "builtin",
2051 "insertText": "string",
2052 "insertTextFormat": 1,
2053 "kind": 25,
2054 "label": "string",
2055 "sortText": "string",
2057 powered_by="serverless_ide",
2059 .request(line=line(), method="shutdown", params={}, result=None)
2060 .notification(method="exit", params={})
2062 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
2064 def test_serverless_ide_completion_legacy(self) -> None:
2065 variables = dict(self.prepare_serverless_ide_environment())
2066 variables.update(self.setup_php_file("completion.php"))
2067 self.test_driver.stop_hh_server()
2069 spec = (
2070 self.initialize_spec(
2071 LspTestSpec("serverless_ide_completion_legacy"), use_serverless_ide=True
2073 .notification(
2074 method="textDocument/didOpen",
2075 params={
2076 "textDocument": {
2077 "uri": "${php_file_uri}",
2078 "languageId": "hack",
2079 "version": 1,
2080 "text": "${php_file}",
2084 .notification(
2085 comment="Add '$x = <'",
2086 method="textDocument/didChange",
2087 params={
2088 "textDocument": {"uri": "${php_file_uri}"},
2089 "contentChanges": [
2091 "range": {
2092 "start": {"line": 3, "character": 0},
2093 "end": {"line": 3, "character": 0},
2095 "text": "$x = <",
2100 .request(
2101 line=line(),
2102 comment="autocomplete after '$x = <'",
2103 method="textDocument/completion",
2104 params={
2105 "textDocument": {"uri": "${php_file_uri}"},
2106 "position": {"line": 3, "character": 6},
2108 result={
2109 "isIncomplete": False,
2110 "items": [
2112 "label": "ab:cd:alpha",
2113 "kind": 7,
2114 "detail": "class",
2115 "inlineDetail": "class",
2116 "sortText": "ab:cd:alpha",
2117 "insertText": "ab:cd:alpha",
2118 "insertTextFormat": InsertTextFormat.PlainText.value,
2119 "data": {"fullname": ":ab:cd:alpha"},
2122 "label": "ab:cd:text",
2123 "kind": 7,
2124 "detail": "class",
2125 "inlineDetail": "class",
2126 "sortText": "ab:cd:text",
2127 "insertText": "ab:cd:text",
2128 "insertTextFormat": InsertTextFormat.PlainText.value,
2129 "data": {"fullname": ":ab:cd:text"},
2132 "label": "xhp:enum-attribute",
2133 "kind": 7,
2134 "detail": "class",
2135 "inlineDetail": "class",
2136 "sortText": "xhp:enum-attribute",
2137 "insertText": "xhp:enum-attribute",
2138 "insertTextFormat": InsertTextFormat.PlainText.value,
2139 "data": {"fullname": ":xhp:enum-attribute"},
2142 "label": "xhp:generic",
2143 "kind": 7,
2144 "detail": "class",
2145 "inlineDetail": "class",
2146 "sortText": "xhp:generic",
2147 "insertText": "xhp:generic",
2148 "insertTextFormat": InsertTextFormat.PlainText.value,
2149 "data": {"fullname": ":xhp:generic"},
2153 powered_by="serverless_ide",
2155 .notification(
2156 comment="Add '$x = <a'",
2157 method="textDocument/didChange",
2158 params={
2159 "textDocument": {"uri": "${php_file_uri}"},
2160 "contentChanges": [
2162 "range": {
2163 "start": {"line": 3, "character": 0},
2164 "end": {"line": 3, "character": 6},
2166 "text": "$x = <a",
2171 .request(
2172 line=line(),
2173 comment="autocomplete after '$x = <a'",
2174 method="textDocument/completion",
2175 params={
2176 "textDocument": {"uri": "${php_file_uri}"},
2177 "position": {"line": 3, "character": 7},
2179 result={
2180 "isIncomplete": False,
2181 "items": [
2183 "label": "ab:cd:alpha",
2184 "kind": 7,
2185 "detail": "class",
2186 "inlineDetail": "class",
2187 "sortText": "ab:cd:alpha",
2188 "insertText": "ab:cd:alpha",
2189 "insertTextFormat": InsertTextFormat.PlainText.value,
2190 "data": {"fullname": ":ab:cd:alpha"},
2193 "label": "ab:cd:text",
2194 "kind": 7,
2195 "detail": "class",
2196 "inlineDetail": "class",
2197 "sortText": "ab:cd:text",
2198 "insertText": "ab:cd:text",
2199 "insertTextFormat": InsertTextFormat.PlainText.value,
2200 "data": {"fullname": ":ab:cd:text"},
2204 powered_by="serverless_ide",
2206 .notification(
2207 comment="Add '$x = <ab:'",
2208 method="textDocument/didChange",
2209 params={
2210 "textDocument": {"uri": "${php_file_uri}"},
2211 "contentChanges": [
2213 "range": {
2214 "start": {"line": 3, "character": 0},
2215 "end": {"line": 3, "character": 7},
2217 "text": "$x = <ab:",
2222 .request(
2223 line=line(),
2224 comment="autocomplete after '$x = <ab:'.",
2225 method="textDocument/completion",
2226 params={
2227 "textDocument": {"uri": "${php_file_uri}"},
2228 "position": {"line": 3, "character": 9},
2230 result={
2231 "isIncomplete": False,
2232 "items": [
2234 "label": "ab:cd:alpha",
2235 "kind": 7,
2236 "detail": "class",
2237 "inlineDetail": "class",
2238 "sortText": "ab:cd:alpha",
2239 "insertText": "ab:cd:alpha",
2240 "insertTextFormat": InsertTextFormat.PlainText.value,
2241 "data": {"fullname": ":ab:cd:alpha"},
2244 "label": "ab:cd:text",
2245 "kind": 7,
2246 "detail": "class",
2247 "inlineDetail": "class",
2248 "sortText": "ab:cd:text",
2249 "insertText": "ab:cd:text",
2250 "insertTextFormat": InsertTextFormat.PlainText.value,
2251 "data": {"fullname": ":ab:cd:text"},
2255 powered_by="serverless_ide",
2257 .notification(
2258 comment="Add '$x = <ab:cd:text '",
2259 method="textDocument/didChange",
2260 params={
2261 "textDocument": {"uri": "${php_file_uri}"},
2262 "contentChanges": [
2264 "range": {
2265 "start": {"line": 3, "character": 0},
2266 "end": {"line": 3, "character": 9},
2268 "text": "$x = <ab:cd:text ",
2273 .request(
2274 line=line(),
2275 comment="autocomplete after '$x = <ab:cd:text '",
2276 method="textDocument/completion",
2277 params={
2278 "textDocument": {"uri": "${php_file_uri}"},
2279 "position": {"line": 3, "character": 17},
2281 result={
2282 "isIncomplete": False,
2283 "items": [
2285 "label": "width",
2286 "kind": 10,
2287 "detail": "?int",
2288 "inlineDetail": "?int",
2289 "sortText": "width",
2290 "insertText": "width",
2291 "insertTextFormat": InsertTextFormat.PlainText.value,
2292 "data": {
2293 "fullname": ":width",
2294 "filename": "${root_path}/xhp_class_definitions.php",
2295 "line": 5,
2296 "char": 27,
2297 "base_class": "\\:ab:cd:text",
2301 "label": "color",
2302 "kind": 10,
2303 "detail": "?string",
2304 "inlineDetail": "?string",
2305 "sortText": "color",
2306 "insertText": "color",
2307 "insertTextFormat": InsertTextFormat.PlainText.value,
2308 "data": {
2309 "fullname": ":color",
2310 "filename": "${root_path}/xhp_class_definitions.php",
2311 "line": 5,
2312 "char": 13,
2313 "base_class": "\\:ab:cd:text",
2318 powered_by="serverless_ide",
2320 .notification(
2321 comment="Add '$x = <ab:cd:text w'",
2322 method="textDocument/didChange",
2323 params={
2324 "textDocument": {"uri": "${php_file_uri}"},
2325 "contentChanges": [
2327 "range": {
2328 "start": {"line": 3, "character": 0},
2329 "end": {"line": 3, "character": 17},
2331 "text": "$x = <ab:cd:text w",
2336 .request(
2337 line=line(),
2338 comment="autocomplete after '$x = <ab:cd:text w'",
2339 method="textDocument/completion",
2340 params={
2341 "textDocument": {"uri": "${php_file_uri}"},
2342 "position": {"line": 3, "character": 18},
2344 result={
2345 "isIncomplete": False,
2346 "items": [
2348 "label": "width",
2349 "kind": 10,
2350 "detail": "?int",
2351 "inlineDetail": "?int",
2352 "sortText": "width",
2353 "insertText": "width",
2354 "insertTextFormat": InsertTextFormat.PlainText.value,
2355 "data": {
2356 "fullname": ":width",
2357 "filename": "${root_path}/xhp_class_definitions.php",
2358 "line": 5,
2359 "char": 27,
2360 "base_class": "\\:ab:cd:text",
2364 "label": "color",
2365 "kind": 10,
2366 "detail": "?string",
2367 "inlineDetail": "?string",
2368 "sortText": "color",
2369 "insertText": "color",
2370 "insertTextFormat": InsertTextFormat.PlainText.value,
2371 "data": {
2372 "fullname": ":color",
2373 "filename": "${root_path}/xhp_class_definitions.php",
2374 "line": 5,
2375 "char": 13,
2376 "base_class": "\\:ab:cd:text",
2381 powered_by="serverless_ide",
2383 .notification(
2384 comment="Add '$x = new :''",
2385 method="textDocument/didChange",
2386 params={
2387 "textDocument": {"uri": "${php_file_uri}"},
2388 "contentChanges": [
2390 "range": {
2391 "start": {"line": 3, "character": 0},
2392 "end": {"line": 3, "character": 18},
2394 "text": "$x = new :",
2399 .request(
2400 line=line(),
2401 comment="autocomplete after '$x = new :'",
2402 method="textDocument/completion",
2403 params={
2404 "textDocument": {"uri": "${php_file_uri}"},
2405 "position": {"line": 3, "character": 10},
2407 result={
2408 "isIncomplete": False,
2409 "items": [
2411 "label": ":ab:cd:alpha",
2412 "kind": 7,
2413 "detail": "class",
2414 "inlineDetail": "class",
2415 "sortText": ":ab:cd:alpha",
2416 "insertText": ":ab:cd:alpha",
2417 "insertTextFormat": InsertTextFormat.PlainText.value,
2418 "data": {"fullname": ":ab:cd:alpha"},
2421 "label": ":ab:cd:text",
2422 "kind": 7,
2423 "detail": "class",
2424 "inlineDetail": "class",
2425 "sortText": ":ab:cd:text",
2426 "insertText": ":ab:cd:text",
2427 "insertTextFormat": InsertTextFormat.PlainText.value,
2428 "data": {"fullname": ":ab:cd:text"},
2431 "label": ":xhp:enum-attribute",
2432 "kind": 7,
2433 "detail": "class",
2434 "inlineDetail": "class",
2435 "sortText": ":xhp:enum-attribute",
2436 "insertText": ":xhp:enum-attribute",
2437 "insertTextFormat": InsertTextFormat.PlainText.value,
2438 "data": {"fullname": ":xhp:enum-attribute"},
2441 "label": ":xhp:generic",
2442 "kind": 7,
2443 "detail": "class",
2444 "inlineDetail": "class",
2445 "sortText": ":xhp:generic",
2446 "insertText": ":xhp:generic",
2447 "insertTextFormat": InsertTextFormat.PlainText.value,
2448 "data": {"fullname": ":xhp:generic"},
2452 powered_by="serverless_ide",
2454 .notification(
2455 comment="Add '$x = new :a'",
2456 method="textDocument/didChange",
2457 params={
2458 "textDocument": {"uri": "${php_file_uri}"},
2459 "contentChanges": [
2461 "range": {
2462 "start": {"line": 3, "character": 0},
2463 "end": {"line": 3, "character": 10},
2465 "text": "$x = new :a",
2470 .request(
2471 line=line(),
2472 comment="autocomplete after '$x = new :a'",
2473 method="textDocument/completion",
2474 params={
2475 "textDocument": {"uri": "${php_file_uri}"},
2476 "position": {"line": 3, "character": 11},
2478 result={
2479 "isIncomplete": False,
2480 "items": [
2482 "label": ":ab:cd:alpha",
2483 "kind": 7,
2484 "detail": "class",
2485 "inlineDetail": "class",
2486 "sortText": ":ab:cd:alpha",
2487 "insertText": ":ab:cd:alpha",
2488 "insertTextFormat": InsertTextFormat.PlainText.value,
2489 "data": {"fullname": ":ab:cd:alpha"},
2492 "label": ":ab:cd:text",
2493 "kind": 7,
2494 "detail": "class",
2495 "inlineDetail": "class",
2496 "sortText": ":ab:cd:text",
2497 "insertText": ":ab:cd:text",
2498 "insertTextFormat": InsertTextFormat.PlainText.value,
2499 "data": {"fullname": ":ab:cd:text"},
2503 powered_by="serverless_ide",
2505 # Note that this request sent should match the result given in the previous example
2506 .request(
2507 line=line(),
2508 comment="autocomplete resolving after '$x = new :a'",
2509 method="completionItem/resolve",
2510 params={
2511 "label": ":ab:cd:alpha",
2512 "kind": 7,
2513 "detail": "class",
2514 "inlineDetail": "class",
2515 "itemType": ":ab:cd:alpha",
2516 "insertText": ":ab:cd:alpha",
2517 "insertTextFormat": InsertTextFormat.PlainText.value,
2518 "data": {"fullname": ":ab:cd:alpha"},
2520 result={
2521 "label": ":ab:cd:alpha",
2522 "kind": 7,
2523 "detail": "class",
2524 "inlineDetail": "class",
2525 "itemType": ":ab:cd:alpha",
2526 "documentation": {
2527 "kind": "markdown",
2528 "value": ":ab:cd:alpha docblock",
2530 "insertText": ":ab:cd:alpha",
2531 "insertTextFormat": InsertTextFormat.PlainText.value,
2532 "data": {"fullname": ":ab:cd:alpha"},
2534 powered_by="serverless_ide",
2536 .notification(
2537 comment="Add '$x = <ab:cd:text/>; $y = $x->'",
2538 method="textDocument/didChange",
2539 params={
2540 "textDocument": {"uri": "${php_file_uri}"},
2541 "contentChanges": [
2543 "range": {
2544 "start": {"line": 3, "character": 0},
2545 "end": {"line": 3, "character": 11},
2547 "text": "$x = <ab:cd:text/>; $y = $x->",
2552 .request(
2553 line=line(),
2554 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->'",
2555 method="textDocument/completion",
2556 params={
2557 "textDocument": {"uri": "${php_file_uri}"},
2558 "position": {"line": 3, "character": 29},
2560 result={
2561 "isIncomplete": False,
2562 "items": [
2564 "label": ":width",
2565 "kind": 10,
2566 "detail": "?int",
2567 "inlineDetail": "?int",
2568 "sortText": ":width",
2569 "insertText": ":width",
2570 "insertTextFormat": InsertTextFormat.PlainText.value,
2571 "data": {
2572 "fullname": ":width",
2573 "filename": "${root_path}/xhp_class_definitions.php",
2574 "line": 5,
2575 "char": 27,
2576 "base_class": "\\:ab:cd:text",
2580 "label": ":color",
2581 "kind": 10,
2582 "detail": "?string",
2583 "inlineDetail": "?string",
2584 "sortText": ":color",
2585 "insertText": ":color",
2586 "insertTextFormat": InsertTextFormat.PlainText.value,
2587 "data": {
2588 "fullname": ":color",
2589 "filename": "${root_path}/xhp_class_definitions.php",
2590 "line": 5,
2591 "char": 13,
2592 "base_class": "\\:ab:cd:text",
2597 powered_by="serverless_ide",
2599 .notification(
2600 comment="Add '$x = <ab:cd:text/>; $y = $x->:'",
2601 method="textDocument/didChange",
2602 params={
2603 "textDocument": {"uri": "${php_file_uri}"},
2604 "contentChanges": [
2606 "range": {
2607 "start": {"line": 3, "character": 0},
2608 "end": {"line": 3, "character": 29},
2610 "text": "$x = <ab:cd:text/>; $y = $x->:",
2615 .request(
2616 line=line(),
2617 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
2618 method="textDocument/completion",
2619 params={
2620 "textDocument": {"uri": "${php_file_uri}"},
2621 "position": {"line": 3, "character": 30},
2623 result={
2624 "isIncomplete": False,
2625 "items": [
2627 "label": ":width",
2628 "kind": 10,
2629 "detail": "?int",
2630 "inlineDetail": "?int",
2631 "sortText": ":width",
2632 "insertText": ":width",
2633 "insertTextFormat": InsertTextFormat.PlainText.value,
2634 "data": {
2635 "fullname": ":width",
2636 "filename": "${root_path}/xhp_class_definitions.php",
2637 "line": 5,
2638 "char": 27,
2639 "base_class": "\\:ab:cd:text",
2643 "label": ":color",
2644 "kind": 10,
2645 "detail": "?string",
2646 "inlineDetail": "?string",
2647 "sortText": ":color",
2648 "insertText": ":color",
2649 "insertTextFormat": InsertTextFormat.PlainText.value,
2650 "data": {
2651 "fullname": ":color",
2652 "filename": "${root_path}/xhp_class_definitions.php",
2653 "line": 5,
2654 "char": 13,
2655 "base_class": "\\:ab:cd:text",
2660 powered_by="serverless_ide",
2662 .notification(
2663 comment="Add 'test_fun'",
2664 method="textDocument/didChange",
2665 params={
2666 "textDocument": {"uri": "${php_file_uri}"},
2667 "contentChanges": [
2669 "range": {
2670 "start": {"line": 3, "character": 0},
2671 "end": {"line": 3, "character": 30},
2673 "text": "test_fun",
2678 .request(
2679 line=line(),
2680 comment="autocomplete after 'test_fun'",
2681 method="textDocument/completion",
2682 params={
2683 "textDocument": {"uri": "${php_file_uri}"},
2684 "position": {"line": 3, "character": 8},
2686 result={
2687 "isIncomplete": False,
2688 "items": [
2690 "label": "test_function",
2691 "kind": 3,
2692 "detail": "function",
2693 "inlineDetail": "function",
2694 "sortText": "test_function",
2695 "insertText": "test_function",
2696 "insertTextFormat": InsertTextFormat.PlainText.value,
2697 "data": {"fullname": "test_function"},
2701 powered_by="serverless_ide",
2703 .request(
2704 line=line(),
2705 comment="autocomplete resolving after 'test_fun'",
2706 method="completionItem/resolve",
2707 params={
2708 "label": "test_function",
2709 "kind": 3,
2710 "detail": "function(): void",
2711 "inlineDetail": "()",
2712 "itemType": "void",
2713 "insertText": "test_function",
2714 "insertTextFormat": InsertTextFormat.PlainText.value,
2715 "data": {
2716 "filename": "${root_path}/completion.php",
2717 "line": 8,
2718 "char": 10,
2721 result={
2722 "label": "test_function",
2723 "kind": 3,
2724 "detail": "function(): void",
2725 "inlineDetail": "()",
2726 "itemType": "void",
2727 "documentation": {
2728 "kind": "markdown",
2729 "value": "test_function docblock.",
2731 "insertText": "test_function",
2732 "insertTextFormat": InsertTextFormat.PlainText.value,
2733 "data": {
2734 "filename": "${root_path}/completion.php",
2735 "line": 8,
2736 "char": 10,
2739 powered_by="serverless_ide",
2741 .notification(
2742 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
2743 method="textDocument/didChange",
2744 params={
2745 "textDocument": {"uri": "${php_file_uri}"},
2746 "contentChanges": [
2748 "range": {
2749 "start": {"line": 3, "character": 0},
2750 "end": {"line": 3, "character": 8},
2752 "text": "switch (Elsa::Alonso) { case Elsa:",
2757 .request(
2758 line=line(),
2759 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
2760 method="textDocument/completion",
2761 params={
2762 "textDocument": {"uri": "${php_file_uri}"},
2763 "position": {"line": 3, "character": 34},
2765 result={"isIncomplete": False, "items": []},
2766 powered_by="serverless_ide",
2768 .notification(
2769 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
2770 method="textDocument/didChange",
2771 params={
2772 "textDocument": {"uri": "${php_file_uri}"},
2773 "contentChanges": [
2775 "range": {
2776 "start": {"line": 3, "character": 0},
2777 "end": {"line": 3, "character": 34},
2779 "text": "switch (Elsa::Alonso) { case Elsa::",
2784 .request(
2785 line=line(),
2786 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
2787 method="textDocument/completion",
2788 params={
2789 "textDocument": {"uri": "${php_file_uri}"},
2790 "position": {"line": 3, "character": 35},
2792 result={
2793 "isIncomplete": False,
2794 "items": [
2796 "label": "class",
2797 "kind": 21,
2798 "detail": "classname<this>",
2799 "inlineDetail": "classname<this>",
2800 "sortText": "class",
2801 "insertText": "class",
2802 "insertTextFormat": InsertTextFormat.PlainText.value,
2803 "data": {
2804 "fullname": "class",
2805 "filename": "${root_path}/completion_extras.php",
2806 "line": 3,
2807 "char": 6,
2808 "base_class": "\\Elsa",
2812 "label": "Bard",
2813 "kind": 21,
2814 "detail": "Elsa",
2815 "inlineDetail": "Elsa",
2816 "sortText": "Bard",
2817 "insertText": "Bard",
2818 "insertTextFormat": InsertTextFormat.PlainText.value,
2819 "data": {
2820 "fullname": "Bard",
2821 "filename": "${root_path}/completion_extras.php",
2822 "line": 3,
2823 "char": 12,
2824 "base_class": "\\Elsa",
2828 "label": "Alonso",
2829 "kind": 21,
2830 "detail": "Elsa",
2831 "inlineDetail": "Elsa",
2832 "sortText": "Alonso",
2833 "insertText": "Alonso",
2834 "insertTextFormat": InsertTextFormat.PlainText.value,
2835 "data": {
2836 "fullname": "Alonso",
2837 "filename": "${root_path}/completion_extras.php",
2838 "line": 3,
2839 "char": 12,
2840 "base_class": "\\Elsa",
2844 "label": "isValid",
2845 "kind": 2,
2846 "detail": "function(mixed $value): bool",
2847 "inlineDetail": "(mixed $value)",
2848 "itemType": "bool",
2849 "sortText": "isValid",
2850 "insertText": "isValid(${1:\\$value})",
2851 "insertTextFormat": InsertTextFormat.Snippet.value,
2852 "data": {
2853 "fullname": "isValid",
2854 "filename": "${hhi_path}/BuiltinEnum.hhi",
2855 "line": 46,
2856 "char": 32,
2857 "base_class": "\\Elsa",
2861 "label": "getValues",
2862 "kind": 2,
2863 "detail": "function(): dict<string, Elsa>",
2864 "inlineDetail": "()",
2865 "itemType": "dict<string, Elsa>",
2866 "sortText": "getValues",
2867 "insertText": "getValues()",
2868 "insertTextFormat": InsertTextFormat.Snippet.value,
2869 "data": {
2870 "fullname": "getValues",
2871 "filename": "${hhi_path}/BuiltinEnum.hhi",
2872 "line": 33,
2873 "char": 32,
2874 "base_class": "\\Elsa",
2878 "label": "getNames",
2879 "kind": 2,
2880 "detail": "function(): dict<Elsa, string>",
2881 "inlineDetail": "()",
2882 "itemType": "dict<Elsa, string>",
2883 "sortText": "getNames",
2884 "insertText": "getNames()",
2885 "insertTextFormat": InsertTextFormat.Snippet.value,
2886 "data": {
2887 "fullname": "getNames",
2888 "filename": "${hhi_path}/BuiltinEnum.hhi",
2889 "line": 41,
2890 "char": 32,
2891 "base_class": "\\Elsa",
2895 "label": "coerce",
2896 "kind": 2,
2897 "detail": "function(mixed $value): ?Elsa",
2898 "inlineDetail": "(mixed $value)",
2899 "itemType": "?Elsa",
2900 "sortText": "coerce",
2901 "insertText": "coerce(${1:\\$value})",
2902 "insertTextFormat": InsertTextFormat.Snippet.value,
2903 "data": {
2904 "fullname": "coerce",
2905 "filename": "${hhi_path}/BuiltinEnum.hhi",
2906 "line": 52,
2907 "char": 32,
2908 "base_class": "\\Elsa",
2912 "label": "assertAll",
2913 "kind": 2,
2914 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
2915 "inlineDetail": "(Traversable<mixed> $values)",
2916 "itemType": "Container<Elsa>",
2917 "sortText": "assertAll",
2918 "insertText": "assertAll(${1:\\$values})",
2919 "insertTextFormat": InsertTextFormat.Snippet.value,
2920 "data": {
2921 "fullname": "assertAll",
2922 "filename": "${hhi_path}/BuiltinEnum.hhi",
2923 "line": 64,
2924 "char": 32,
2925 "base_class": "\\Elsa",
2929 "label": "assert",
2930 "kind": 2,
2931 "detail": "function(mixed $value): Elsa",
2932 "inlineDetail": "(mixed $value)",
2933 "itemType": "Elsa",
2934 "sortText": "assert",
2935 "insertText": "assert(${1:\\$value})",
2936 "insertTextFormat": InsertTextFormat.Snippet.value,
2937 "data": {
2938 "fullname": "assert",
2939 "filename": "${hhi_path}/BuiltinEnum.hhi",
2940 "line": 58,
2941 "char": 32,
2942 "base_class": "\\Elsa",
2947 powered_by="serverless_ide",
2949 .notification(
2950 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
2951 method="textDocument/didChange",
2952 params={
2953 "textDocument": {"uri": "${php_file_uri}"},
2954 "contentChanges": [
2956 "range": {
2957 "start": {"line": 3, "character": 0},
2958 "end": {"line": 3, "character": 35},
2960 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
2965 .request(
2966 line=line(),
2967 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
2968 method="textDocument/completion",
2969 params={
2970 "textDocument": {"uri": "${php_file_uri}"},
2971 "position": {"line": 3, "character": 42},
2973 result={"isIncomplete": False, "items": []},
2974 powered_by="serverless_ide",
2976 .notification(
2977 comment="Add 'DeprecatedClass::'",
2978 method="textDocument/didChange",
2979 params={
2980 "textDocument": {"uri": "${php_file_uri}"},
2981 "contentChanges": [
2983 "range": {
2984 "start": {"line": 3, "character": 0},
2985 "end": {"line": 3, "character": 41},
2987 "text": "DeprecatedClass::",
2992 .request(
2993 line=line(),
2994 comment="autocomplete after 'DeprecatedClass::'",
2995 method="textDocument/completion",
2996 params={
2997 "textDocument": {"uri": "${php_file_uri}"},
2998 "position": {"line": 3, "character": 17},
3000 result={
3001 "isIncomplete": False,
3002 "items": [
3004 "label": "class",
3005 "kind": 21,
3006 "detail": "classname<this>",
3007 "inlineDetail": "classname<this>",
3008 "sortText": "class",
3009 "insertText": "class",
3010 "insertTextFormat": InsertTextFormat.PlainText.value,
3011 "data": {
3012 "fullname": "class",
3013 "filename": "${root_path}/completion_extras.php",
3014 "line": 8,
3015 "char": 13,
3016 "base_class": "\\DeprecatedClass",
3020 "label": "test_do_not_use",
3021 "kind": 2,
3022 "detail": "function(): void",
3023 "inlineDetail": "()",
3024 "itemType": "void",
3025 "sortText": "~test_do_not_use",
3026 "insertText": "test_do_not_use()",
3027 "insertTextFormat": InsertTextFormat.Snippet.value,
3028 "data": {
3029 "fullname": "test_do_not_use",
3030 "filename": "${root_path}/completion_extras.php",
3031 "line": 12,
3032 "char": 26,
3033 "base_class": "\\DeprecatedClass",
3037 "label": "getName",
3038 "kind": 2,
3039 "detail": "function(): void",
3040 "inlineDetail": "()",
3041 "itemType": "void",
3042 "sortText": "getName",
3043 "insertText": "getName()",
3044 "insertTextFormat": InsertTextFormat.Snippet.value,
3045 "data": {
3046 "fullname": "getName",
3047 "filename": "${root_path}/completion_extras.php",
3048 "line": 9,
3049 "char": 26,
3050 "base_class": "\\DeprecatedClass",
3054 "label": "getAttributes_DO_NOT_USE",
3055 "kind": 2,
3056 "detail": "function(): void",
3057 "inlineDetail": "()",
3058 "itemType": "void",
3059 "sortText": "~getAttributes_DO_NOT_USE",
3060 "insertText": "getAttributes_DO_NOT_USE()",
3061 "insertTextFormat": InsertTextFormat.Snippet.value,
3062 "data": {
3063 "fullname": "getAttributes_DO_NOT_USE",
3064 "filename": "${root_path}/completion_extras.php",
3065 "line": 11,
3066 "char": 26,
3067 "base_class": "\\DeprecatedClass",
3071 "label": "__getLoader",
3072 "kind": 2,
3073 "detail": "function(): void",
3074 "inlineDetail": "()",
3075 "itemType": "void",
3076 "sortText": "~__getLoader",
3077 "insertText": "__getLoader()",
3078 "insertTextFormat": InsertTextFormat.Snippet.value,
3079 "data": {
3080 "fullname": "__getLoader",
3081 "filename": "${root_path}/completion_extras.php",
3082 "line": 10,
3083 "char": 26,
3084 "base_class": "\\DeprecatedClass",
3089 powered_by="serverless_ide",
3091 .request(line=line(), method="shutdown", params={}, result=None)
3092 .notification(method="exit", params={})
3094 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3096 def test_serverless_ide_definition(self) -> None:
3097 variables = dict(self.prepare_serverless_ide_environment())
3098 variables.update(self.setup_php_file("definition.php"))
3099 self.test_driver.stop_hh_server()
3101 spec = (
3102 self.initialize_spec(
3103 LspTestSpec("serverless_ide_definition"), use_serverless_ide=True
3105 .notification(
3106 method="textDocument/didOpen",
3107 params={
3108 "textDocument": {
3109 "uri": "${php_file_uri}",
3110 "languageId": "hack",
3111 "version": 1,
3112 "text": "${php_file}",
3116 .request(
3117 line=line(),
3118 comment="call to `b_definition`",
3119 method="textDocument/definition",
3120 params={
3121 "textDocument": {"uri": "${php_file_uri}"},
3122 "position": {"line": 3, "character": 10},
3124 result=[
3126 "uri": "file://${root_path}/definition.php",
3127 "range": {
3128 "start": {"line": 6, "character": 9},
3129 "end": {"line": 6, "character": 21},
3131 "title": "b_definition",
3134 powered_by="serverless_ide",
3136 .request(
3137 line=line(),
3138 comment="call to `new BB(1)`",
3139 method="textDocument/definition",
3140 params={
3141 "textDocument": {"uri": "${php_file_uri}"},
3142 "position": {"line": 29, "character": 13},
3144 result=[
3146 "uri": "file://${root_path}/definition.php",
3147 "range": {
3148 "start": {"line": 11, "character": 18},
3149 "end": {"line": 11, "character": 29},
3151 "title": "BB::__construct",
3154 powered_by="serverless_ide",
3156 .request(
3157 line=line(),
3158 comment="call to `new CC(1)`",
3159 method="textDocument/definition",
3160 params={
3161 "textDocument": {"uri": "${php_file_uri}"},
3162 "position": {"line": 30, "character": 13},
3164 result=[
3166 "uri": "file://${root_path}/definition.php",
3167 "range": {
3168 "start": {"line": 14, "character": 6},
3169 "end": {"line": 14, "character": 8},
3171 "title": "CC",
3174 "uri": "file://${root_path}/definition.php",
3175 "range": {
3176 "start": {"line": 11, "character": 18},
3177 "end": {"line": 11, "character": 29},
3179 "title": "BB::__construct",
3182 powered_by="serverless_ide",
3184 .request(
3185 line=line(),
3186 comment="call to `new DD(1)`",
3187 method="textDocument/definition",
3188 params={
3189 "textDocument": {"uri": "${php_file_uri}"},
3190 "position": {"line": 31, "character": 13},
3192 result=[
3194 "uri": "file://${root_path}/definition.php",
3195 "range": {
3196 "start": {"line": 17, "character": 6},
3197 "end": {"line": 17, "character": 8},
3199 "title": "DD",
3202 "uri": "file://${root_path}/definition.php",
3203 "range": {
3204 "start": {"line": 11, "character": 18},
3205 "end": {"line": 11, "character": 29},
3207 "title": "BB::__construct",
3210 powered_by="serverless_ide",
3212 .request(
3213 line=line(),
3214 comment="call to `new EE(1)`",
3215 method="textDocument/definition",
3216 params={
3217 "textDocument": {"uri": "${php_file_uri}"},
3218 "position": {"line": 32, "character": 13},
3220 result=[
3222 "uri": "file://${root_path}/definition.php",
3223 "range": {
3224 "start": {"line": 21, "character": 18},
3225 "end": {"line": 21, "character": 29},
3227 "title": "EE::__construct",
3230 powered_by="serverless_ide",
3232 .request(
3233 line=line(),
3234 comment="call to `new FF(1)`",
3235 method="textDocument/definition",
3236 params={
3237 "textDocument": {"uri": "${php_file_uri}"},
3238 "position": {"line": 33, "character": 13},
3240 result=[
3242 "uri": "file://${root_path}/definition.php",
3243 "range": {
3244 "start": {"line": 26, "character": 6},
3245 "end": {"line": 26, "character": 8},
3247 "title": "FF",
3250 powered_by="serverless_ide",
3252 .request(
3253 line=line(),
3254 comment="call to `new TakesString(HasString::MyString)`",
3255 method="textDocument/definition",
3256 params={
3257 "textDocument": {"uri": "${php_file_uri}"},
3258 "position": {"line": 45, "character": 23},
3260 result=[
3262 "uri": "file://${root_path}/definition.php",
3263 "range": {
3264 "start": {"line": 40, "character": 6},
3265 "end": {"line": 40, "character": 15},
3267 "title": "HasString",
3270 powered_by="serverless_ide",
3272 .notification(
3273 comment="make local, unsaved change to the file",
3274 method="textDocument/didChange",
3275 params={
3276 "textDocument": {"uri": "${php_file_uri}", "version": 2},
3277 "contentChanges": [
3279 "text": "test",
3280 "range": {
3281 "start": {"line": 3, "character": 9},
3282 "end": {"line": 3, "character": 21},
3288 .request(
3289 line=line(),
3290 comment="call to `test` instead of `b_definition`",
3291 method="textDocument/definition",
3292 params={
3293 "textDocument": {"uri": "${php_file_uri}"},
3294 "position": {"line": 3, "character": 10},
3296 result=[
3298 "uri": "file://${root_path}/definition.php",
3299 "range": {
3300 "start": {"line": 28, "character": 9},
3301 "end": {"line": 28, "character": 13},
3303 "title": "test",
3306 powered_by="serverless_ide",
3308 .request(line=line(), method="shutdown", params={}, result=None)
3309 .notification(method="exit", params={})
3311 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3313 def test_serverless_ide_overridden_definition(self) -> None:
3314 variables = dict(self.prepare_serverless_ide_environment())
3315 variables.update(self.setup_php_file("override.php"))
3316 self.test_driver.stop_hh_server()
3318 spec = (
3319 self.initialize_spec(
3320 LspTestSpec("serverless_ide_overridden_definition"),
3321 use_serverless_ide=True,
3323 .notification(
3324 method="textDocument/didOpen",
3325 params={
3326 "textDocument": {
3327 "uri": "${php_file_uri}",
3328 "languageId": "hack",
3329 "version": 1,
3330 "text": "${php_file}",
3334 .request(
3335 line=line(),
3336 comment="find overridden method from trait. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3337 method="textDocument/definition",
3338 params={
3339 "textDocument": {"uri": "${php_file_uri}"},
3340 "position": {"line": 13, "character": 5},
3342 result=[
3344 "uri": "file://${root_path}/override.php",
3345 "range": {
3346 "start": {"line": 7, "character": 18},
3347 "end": {"line": 7, "character": 21},
3349 "title": "MyTrait::foo",
3352 powered_by="serverless_ide",
3354 .request(
3355 line=line(),
3356 comment="find overridden static method. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3357 method="textDocument/definition",
3358 params={
3359 "textDocument": {"uri": "${php_file_uri}"},
3360 "position": {"line": 26, "character": 5},
3362 result=[
3364 "uri": "file://${root_path}/override.php",
3365 "range": {
3366 "start": {"line": 23, "character": 25},
3367 "end": {"line": 23, "character": 28},
3369 "title": "C2::bar",
3372 powered_by="serverless_ide",
3374 .request(
3375 line=line(),
3376 comment="find overridden interface method",
3377 method="textDocument/definition",
3378 params={
3379 "textDocument": {"uri": "${php_file_uri}"},
3380 "position": {"line": 35, "character": 5},
3382 result=[
3384 "uri": "file://${root_path}/override.php",
3385 "range": {
3386 "start": {"line": 32, "character": 18},
3387 "end": {"line": 32, "character": 22},
3389 "title": "I1::quux",
3392 powered_by="serverless_ide",
3394 .request(line=line(), method="shutdown", params={}, result=None)
3395 .notification(method="exit", params={})
3397 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3399 def test_serverless_ide_document_symbol(self) -> None:
3400 variables = dict(self.prepare_serverless_ide_environment())
3401 variables.update(self.setup_php_file("definition.php"))
3402 self.test_driver.stop_hh_server()
3404 spec = (
3405 self.initialize_spec(
3406 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide=True
3408 .notification(
3409 method="textDocument/didOpen",
3410 params={
3411 "textDocument": {
3412 "uri": "${php_file_uri}",
3413 "languageId": "hack",
3414 "version": 1,
3415 "text": "${php_file}",
3419 .request(
3420 line=line(),
3421 comment="documentSymbol call",
3422 method="textDocument/documentSymbol",
3423 params={"textDocument": {"uri": "${php_file_uri}"}},
3424 result=[
3426 "name": "First",
3427 "kind": 14,
3428 "location": {
3429 "uri": "file://${root_path}/definition.php",
3430 "range": {
3431 "start": {"line": 50, "character": 18},
3432 "end": {"line": 50, "character": 47},
3435 "containerName": "MyEnumClass",
3438 "name": "MyEnumClass",
3439 "kind": 10,
3440 "location": {
3441 "uri": "file://${root_path}/definition.php",
3442 "range": {
3443 "start": {"line": 49, "character": 0},
3444 "end": {"line": 52, "character": 1},
3449 "name": "testClassMemberInsideConstructorInvocation",
3450 "kind": 12,
3451 "location": {
3452 "uri": "file://${root_path}/definition.php",
3453 "range": {
3454 "start": {"line": 44, "character": 0},
3455 "end": {"line": 46, "character": 1},
3460 "name": "MyString",
3461 "kind": 14,
3462 "location": {
3463 "uri": "file://${root_path}/definition.php",
3464 "range": {
3465 "start": {"line": 41, "character": 8},
3466 "end": {"line": 41, "character": 29},
3469 "containerName": "HasString",
3472 "name": "HasString",
3473 "kind": 5,
3474 "location": {
3475 "uri": "file://${root_path}/definition.php",
3476 "range": {
3477 "start": {"line": 40, "character": 0},
3478 "end": {"line": 42, "character": 1},
3483 "name": "__construct",
3484 "kind": 6,
3485 "location": {
3486 "uri": "file://${root_path}/definition.php",
3487 "range": {
3488 "start": {"line": 37, "character": 2},
3489 "end": {"line": 37, "character": 43},
3492 "containerName": "TakesString",
3495 "name": "TakesString",
3496 "kind": 5,
3497 "location": {
3498 "uri": "file://${root_path}/definition.php",
3499 "range": {
3500 "start": {"line": 36, "character": 0},
3501 "end": {"line": 38, "character": 1},
3506 "name": "FF",
3507 "kind": 5,
3508 "location": {
3509 "uri": "file://${root_path}/definition.php",
3510 "range": {
3511 "start": {"line": 26, "character": 0},
3512 "end": {"line": 26, "character": 11},
3517 "name": "__construct",
3518 "kind": 6,
3519 "location": {
3520 "uri": "file://${root_path}/definition.php",
3521 "range": {
3522 "start": {"line": 21, "character": 2},
3523 "end": {"line": 23, "character": 3},
3526 "containerName": "EE",
3529 "name": "EE",
3530 "kind": 5,
3531 "location": {
3532 "uri": "file://${root_path}/definition.php",
3533 "range": {
3534 "start": {"line": 20, "character": 0},
3535 "end": {"line": 24, "character": 1},
3540 "name": "CC",
3541 "kind": 5,
3542 "location": {
3543 "uri": "file://${root_path}/definition.php",
3544 "range": {
3545 "start": {"line": 14, "character": 0},
3546 "end": {"line": 15, "character": 1},
3551 "name": "__construct",
3552 "kind": 6,
3553 "location": {
3554 "uri": "file://${root_path}/definition.php",
3555 "range": {
3556 "start": {"line": 11, "character": 2},
3557 "end": {"line": 11, "character": 40},
3560 "containerName": "BB",
3563 "name": "BB",
3564 "kind": 5,
3565 "location": {
3566 "uri": "file://${root_path}/definition.php",
3567 "range": {
3568 "start": {"line": 10, "character": 0},
3569 "end": {"line": 12, "character": 1},
3574 "name": "a_definition",
3575 "kind": 12,
3576 "location": {
3577 "uri": "file://${root_path}/definition.php",
3578 "range": {
3579 "start": {"line": 2, "character": 0},
3580 "end": {"line": 4, "character": 1},
3585 "name": "b_definition",
3586 "kind": 12,
3587 "location": {
3588 "uri": "file://${root_path}/definition.php",
3589 "range": {
3590 "start": {"line": 6, "character": 0},
3591 "end": {"line": 8, "character": 1},
3596 "name": "DD",
3597 "kind": 5,
3598 "location": {
3599 "uri": "file://${root_path}/definition.php",
3600 "range": {
3601 "start": {"line": 17, "character": 0},
3602 "end": {"line": 18, "character": 1},
3607 "name": "test",
3608 "kind": 12,
3609 "location": {
3610 "uri": "file://${root_path}/definition.php",
3611 "range": {
3612 "start": {"line": 28, "character": 0},
3613 "end": {"line": 34, "character": 1},
3618 "name": "MyEnumClassKind",
3619 "kind": 5,
3620 "location": {
3621 "uri": "file://${root_path}/definition.php",
3622 "range": {
3623 "start": {"line": 48, "character": 0},
3624 "end": {"line": 48, "character": 24},
3629 "name": "Second",
3630 "kind": 14,
3631 "location": {
3632 "uri": "${php_file_uri}",
3633 "range": {
3634 "start": {"line": 51, "character": 18},
3635 "end": {"line": 51, "character": 48},
3638 "containerName": "MyEnumClass",
3641 powered_by="serverless_ide",
3643 .request(line=line(), method="shutdown", params={}, result=None)
3644 .notification(method="exit", params={})
3646 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3648 def initialize_spec(
3649 self,
3650 spec: LspTestSpec,
3651 use_serverless_ide: bool,
3652 supports_status: bool = False, # does the caller wish to see all status messages?
3653 supports_init: bool = False, # do we wish to interact with init, rather than waiting for init ok?
3654 ) -> LspTestSpec:
3655 if use_serverless_ide:
3656 initialization_options = {
3657 "namingTableSavedStatePath": "${naming_table_saved_state_path}",
3658 "namingTableSavedStateTestDelay": 0.0,
3660 if supports_init:
3661 # A small delay, since otherwise init completes immediately
3662 # This isn't very racy. All we need is a tiny delay so that
3663 # other things which are in the queue get processed, rather
3664 # than continuing synchronously
3665 initialization_options["namingTableSavedStateTestDelay"] = 0.5
3666 else:
3667 initialization_options = {}
3669 window_capabilities = {}
3670 if supports_status:
3671 window_capabilities["status"] = {"dynamicRegistration": False}
3673 spec = spec.ignore_notifications(method="telemetry/event").request(
3674 line=line(),
3675 method="initialize",
3676 params={
3677 "initializationOptions": initialization_options,
3678 "processId": None,
3679 "rootPath": "${root_path}",
3680 "capabilities": {
3681 "window": window_capabilities,
3682 "textDocument": {
3683 "completion": {"completionItem": {"snippetSupport": True}}
3687 result={
3688 "capabilities": {
3689 "textDocumentSync": {
3690 "openClose": True,
3691 "change": 2,
3692 "willSave": False,
3693 "willSaveWaitUntil": True,
3694 "save": {"includeText": False},
3696 "hoverProvider": True,
3697 "completionProvider": {
3698 "resolveProvider": True,
3699 "triggerCharacters": [
3700 "$",
3701 ">",
3702 "\\",
3703 ":",
3704 "<",
3705 "[",
3706 "'",
3707 '"',
3708 "{",
3709 "#",
3712 "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
3713 "definitionProvider": True,
3714 "typeDefinitionProvider": True,
3715 "referencesProvider": True,
3716 "documentHighlightProvider": True,
3717 "documentSymbolProvider": True,
3718 "workspaceSymbolProvider": True,
3719 "codeActionProvider": True,
3720 "documentFormattingProvider": True,
3721 "documentRangeFormattingProvider": True,
3722 "documentOnTypeFormattingProvider": {
3723 "firstTriggerCharacter": ";",
3724 "moreTriggerCharacter": ["}"],
3726 "renameProvider": True,
3727 "implementationProvider": True,
3728 "typeCoverageProvider": True,
3729 "rageProvider": True,
3733 if use_serverless_ide:
3734 spec = spec.wait_for_server_request(
3735 method="client/registerCapability",
3736 params={
3737 "registrations": [
3739 "id": "did-change-watched-files",
3740 "method": "workspace/didChangeWatchedFiles",
3741 "registerOptions": {
3742 "watchers": [
3744 "globPattern": "**/*.{php,phpt,hack,hackpartial,hck,hh,hhi,xhp}",
3745 "kind": 7,
3752 result=None,
3754 if not supports_status:
3755 spec = spec.ignore_status_diagnostics(True)
3757 if use_serverless_ide and not supports_init:
3758 spec = spec.wait_for_notification(
3759 comment="wait for sIDE to finish init",
3760 method="telemetry/event",
3761 params={"type": 4, "message": "[client-ide] Finished init: ok"},
3764 return spec
3766 def test_serverless_ide_type_definition(self) -> None:
3767 variables = dict(self.prepare_serverless_ide_environment())
3768 variables.update(self.setup_php_file("type_definition.php"))
3769 self.test_driver.stop_hh_server()
3771 spec = (
3772 self.initialize_spec(
3773 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide=True
3775 .notification(
3776 method="textDocument/didOpen",
3777 params={
3778 "textDocument": {
3779 "uri": "${php_file_uri}",
3780 "languageId": "hack",
3781 "version": 1,
3782 "text": "${php_file}",
3786 .request(
3787 line=line(),
3788 comment="Conditional Type Definition of HH or II",
3789 method="textDocument/typeDefinition",
3790 params={
3791 "textDocument": {"uri": "${php_file_uri}"},
3792 "position": {"line": 32, "character": 2},
3794 result=[
3796 "uri": "${php_file_uri}",
3797 "range": {
3798 "start": {"line": 2, "character": 6},
3799 "end": {"line": 2, "character": 8},
3801 "title": "\\HH",
3804 "uri": "${php_file_uri}",
3805 "range": {
3806 "start": {"line": 12, "character": 6},
3807 "end": {"line": 12, "character": 8},
3809 "title": "\\LL",
3812 powered_by="serverless_ide",
3814 .request(
3815 line=line(),
3816 comment="Standard Class Definition",
3817 method="textDocument/typeDefinition",
3818 params={
3819 "textDocument": {"uri": "${php_file_uri}"},
3820 "position": {"line": 40, "character": 2},
3822 result=[
3824 "uri": "${php_file_uri}",
3825 "range": {
3826 "start": {"line": 2, "character": 6},
3827 "end": {"line": 2, "character": 8},
3829 "title": "\\HH",
3832 powered_by="serverless_ide",
3834 .request(
3835 line=line(),
3836 comment="Class Type Definition with Casting",
3837 method="textDocument/typeDefinition",
3838 params={
3839 "textDocument": {"uri": "${php_file_uri}"},
3840 "position": {"line": 41, "character": 2},
3842 result=[
3844 "uri": "${php_file_uri}",
3845 "range": {
3846 "start": {"line": 2, "character": 6},
3847 "end": {"line": 2, "character": 8},
3849 "title": "\\HH",
3852 powered_by="serverless_ide",
3854 .request(
3855 line=line(),
3856 comment="Primitive Type Definition",
3857 method="textDocument/typeDefinition",
3858 params={
3859 "textDocument": {"uri": "${php_file_uri}"},
3860 "position": {"line": 42, "character": 2},
3862 result=[],
3863 powered_by="serverless_ide",
3865 .request(
3866 line=line(),
3867 comment="Function Return Type Definition",
3868 method="textDocument/typeDefinition",
3869 params={
3870 "textDocument": {"uri": "${php_file_uri}"},
3871 "position": {"line": 43, "character": 2},
3873 result=[
3875 "uri": "${php_file_uri}",
3876 "range": {
3877 "start": {"line": 12, "character": 6},
3878 "end": {"line": 12, "character": 8},
3880 "title": "\\LL",
3883 powered_by="serverless_ide",
3885 .request(
3886 line=line(),
3887 comment="Function definition with primitive return type",
3888 method="textDocument/typeDefinition",
3889 params={
3890 "textDocument": {"uri": "${php_file_uri}"},
3891 "position": {"line": 44, "character": 2},
3893 result=[
3895 "uri": "${php_file_uri}",
3896 "range": {
3897 "start": {"line": 22, "character": 9},
3898 "end": {"line": 22, "character": 29},
3900 "title": "(function(): int)",
3903 powered_by="serverless_ide",
3905 .request(line=line(), method="shutdown", params={}, result=None)
3906 .notification(method="exit", params={})
3908 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3910 def test_serverless_ide_hover(self) -> None:
3911 variables = dict(self.prepare_serverless_ide_environment())
3912 variables.update(self.setup_php_file("hover.php"))
3913 self.test_driver.stop_hh_server()
3915 spec = (
3916 self.initialize_spec(
3917 LspTestSpec("serverless_ide_hover"), use_serverless_ide=True
3919 .notification(
3920 method="textDocument/didOpen",
3921 params={
3922 "textDocument": {
3923 "uri": "${php_file_uri}",
3924 "languageId": "hack",
3925 "version": 1,
3926 "text": "${php_file}",
3930 .request(
3931 line=line(),
3932 comment="hover over function invocation",
3933 method="textDocument/hover",
3934 params={
3935 "textDocument": {"uri": "${php_file_uri}"},
3936 "position": {"line": 3, "character": 16},
3938 result={
3939 "contents": [
3940 {"language": "hack", "value": "int"},
3941 "A comment describing b_hover.",
3943 "range": {
3944 "start": {"line": 3, "character": 9},
3945 "end": {"line": 3, "character": 16},
3948 powered_by="serverless_ide",
3950 .request(
3951 line=line(),
3952 comment="hover over string literal outside call",
3953 method="textDocument/hover",
3954 params={
3955 "textDocument": {"uri": "${php_file_uri}"},
3956 "position": {"line": 25, "character": 12}, # 9 - 16
3958 result={"contents": [{"language": "hack", "value": "string"}]},
3959 powered_by="serverless_ide",
3961 .request(
3962 line=line(),
3963 comment="hover over string literal inside call",
3964 method="textDocument/hover",
3965 params={
3966 "textDocument": {"uri": "${php_file_uri}"},
3967 "position": {"line": 26, "character": 20}, # 16 - 29
3969 result={"contents": [{"language": "hack", "value": "string"}]},
3970 powered_by="serverless_ide",
3972 .request(
3973 line=line(),
3974 comment="hover over int literal inside call",
3975 method="textDocument/hover",
3976 params={
3977 "textDocument": {"uri": "${php_file_uri}"},
3978 "position": {"line": 26, "character": 32}, # 31 - 33
3980 result={"contents": [{"language": "hack", "value": "int"}]},
3981 powered_by="serverless_ide",
3983 .request(
3984 line=line(),
3985 comment="hover over constant reference",
3986 method="textDocument/hover",
3987 params={
3988 "textDocument": {"uri": "${php_file_uri}"},
3989 "position": {"line": 15, "character": 19},
3991 result={
3992 "contents": [
3993 {"language": "hack", "value": "THE_ANSWER"},
3994 "A comment describing THE_ANSWER",
3995 "int THE_ANSWER = 42",
3997 "range": {
3998 "start": {"line": 15, "character": 9},
3999 "end": {"line": 15, "character": 19},
4002 powered_by="serverless_ide",
4004 .request(
4005 line=line(),
4006 comment="hover over whitespace",
4007 method="textDocument/hover",
4008 params={
4009 "textDocument": {"uri": "${php_file_uri}"},
4010 "position": {"line": 3, "character": 1},
4012 result=None,
4013 powered_by="serverless_ide",
4015 .request(
4016 line=line(),
4017 comment="hover over a keyword",
4018 method="textDocument/hover",
4019 params={
4020 "textDocument": {"uri": "${php_file_uri}"},
4021 "position": {"line": 2, "character": 1},
4023 result=None,
4024 powered_by="serverless_ide",
4026 .request(
4027 line=line(),
4028 comment="hover over a comment",
4029 method="textDocument/hover",
4030 params={
4031 "textDocument": {"uri": "${php_file_uri}"},
4032 "position": {"line": 1, "character": 4},
4034 result=None,
4035 powered_by="serverless_ide",
4037 .request(
4038 line=line(),
4039 comment="hover past the end of a line",
4040 method="textDocument/hover",
4041 params={
4042 "textDocument": {"uri": "${php_file_uri}"},
4043 "position": {"line": 3, "character": 100},
4045 result=None,
4046 powered_by="serverless_ide",
4048 .request(
4049 line=line(),
4050 comment="hover past the end of a file",
4051 method="textDocument/hover",
4052 params={
4053 "textDocument": {"uri": "${php_file_uri}"},
4054 "position": {"line": 300, "character": 0},
4056 result=None,
4057 powered_by="serverless_ide",
4059 .request(
4060 line=line(),
4061 comment="hover over class with copyright docblock",
4062 method="textDocument/hover",
4063 params={
4064 "textDocument": {"uri": "${php_file_uri}"},
4065 "position": {"line": 37, "character": 15},
4067 result={
4068 "contents": [
4069 {"language": "hack", "value": "final class CopyrightClass"},
4070 "Testing copyright removal",
4072 "range": {
4073 "start": {"line": 37, "character": 2},
4074 "end": {"line": 37, "character": 16},
4077 powered_by="serverless_ide",
4079 .request(
4080 line=line(),
4081 comment="hover over class with generated docblock",
4082 method="textDocument/hover",
4083 params={
4084 "textDocument": {"uri": "${php_file_uri}"},
4085 "position": {"line": 58, "character": 15},
4087 result={
4088 "contents": [
4089 {"language": "hack", "value": "final class GeneratedClass"},
4090 "Testing generated text removal",
4092 "range": {
4093 "start": {"line": 58, "character": 2},
4094 "end": {"line": 58, "character": 16},
4097 powered_by="serverless_ide",
4099 .request(
4100 line=line(),
4101 comment="hover over an primitive attribute in an xhp literal",
4102 method="textDocument/hover",
4103 params={
4104 "textDocument": {"uri": "${php_file_uri}"},
4105 "position": {"line": 62, "character": 25},
4107 result={
4108 "contents": [
4109 {"language": "hack", "value": "public ?string name"},
4110 ":xhp:enum-attribute::name docblock",
4112 "range": {
4113 "start": {"line": 62, "character": 22},
4114 "end": {"line": 62, "character": 26},
4117 powered_by="serverless_ide",
4119 .request(
4120 line=line(),
4121 comment="hover over a nonprimitive attribute in an xhp literal",
4122 method="textDocument/hover",
4123 params={
4124 "textDocument": {"uri": "${php_file_uri}"},
4125 "position": {"line": 62, "character": 36},
4127 result={
4128 "contents": [
4129 {"language": "hack", "value": "public ?MyEnum enum-attribute"}
4131 "range": {
4132 "start": {"line": 62, "character": 33},
4133 "end": {"line": 62, "character": 47},
4136 powered_by="serverless_ide",
4138 .request(
4139 line=line(),
4140 comment="hover over a generic attribute in an xhp literal",
4141 method="textDocument/hover",
4142 params={
4143 "textDocument": {"uri": "${php_file_uri}"},
4144 "position": {"line": 63, "character": 16},
4146 result={
4147 "contents": [
4148 {"language": "hack", "value": "public ?ID<EntSomething> id"}
4150 "range": {
4151 "start": {"line": 63, "character": 15},
4152 "end": {"line": 63, "character": 17},
4155 powered_by="serverless_ide",
4157 .notification(
4158 comment="Add '<xhp:enum-attribute name' to test hover for incomplete xhp attribute",
4159 method="textDocument/didChange",
4160 params={
4161 "textDocument": {"uri": "${php_file_uri}"},
4162 "contentChanges": [
4164 "range": {
4165 "start": {"line": 69, "character": 0},
4166 "end": {"line": 69, "character": 0},
4168 "text": "<xhp:enum-attribute name",
4173 .request(
4174 line=line(),
4175 comment="hover over an attribute in an xhp literal without a value",
4176 method="textDocument/hover",
4177 params={
4178 "textDocument": {"uri": "${php_file_uri}"},
4179 "position": {"line": 69, "character": 22},
4181 result={
4182 "contents": [
4183 {"language": "hack", "value": "public ?string name"},
4184 ":xhp:enum-attribute::name docblock",
4186 "range": {
4187 "start": {"line": 69, "character": 20},
4188 "end": {"line": 69, "character": 24},
4191 powered_by="serverless_ide",
4193 .request(line=line(), method="shutdown", params={}, result=None)
4194 .notification(method="exit", params={})
4196 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4198 def test_serverless_ide_file_touched_on_disk(self) -> None:
4199 variables = dict(self.prepare_serverless_ide_environment())
4200 variables.update(self.setup_php_file("hover.php"))
4201 self.test_driver.stop_hh_server()
4203 spec = (
4204 self.initialize_spec(
4205 LspTestSpec("serverless_ide_file_on_disk_change"),
4206 use_serverless_ide=True,
4208 .notification(
4209 method="textDocument/didOpen",
4210 params={
4211 "textDocument": {
4212 "uri": "${php_file_uri}",
4213 "languageId": "hack",
4214 "version": 1,
4215 "text": "${php_file}",
4219 .notification(
4220 method="workspace/didChangeWatchedFiles",
4221 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
4223 .wait_for_notification(
4224 comment="wait for sIDE to process file change",
4225 method="telemetry/event",
4226 params={
4227 "type": 4,
4228 "message": "[client-ide] Done processing file changes",
4231 .request(
4232 line=line(),
4233 method="textDocument/hover",
4234 params={
4235 "textDocument": {"uri": "${php_file_uri}"},
4236 "position": {"line": 3, "character": 16},
4238 result={
4239 "contents": [
4240 {"language": "hack", "value": "int"},
4241 "A comment describing b_hover.",
4243 "range": {
4244 "start": {"line": 3, "character": 9},
4245 "end": {"line": 3, "character": 16},
4248 powered_by="serverless_ide",
4250 .request(line=line(), method="shutdown", params={}, result=None)
4251 .notification(method="exit", params={})
4253 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4255 def test_serverless_ide_file_hover_with_errors(self) -> None:
4256 variables = dict(self.prepare_serverless_ide_environment())
4257 variables.update(self.setup_php_file("hover_with_errors.php"))
4258 self.test_driver.stop_hh_server()
4260 spec = (
4261 self.initialize_spec(
4262 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide=True
4264 .notification(
4265 method="textDocument/didOpen",
4266 params={
4267 "textDocument": {
4268 "uri": "${php_file_uri}",
4269 "languageId": "hack",
4270 "version": 1,
4271 "text": "${php_file}",
4275 .notification(
4276 method="workspace/didChangeWatchedFiles",
4277 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
4279 .wait_for_notification(
4280 comment="wait for sIDE to process file change",
4281 method="telemetry/event",
4282 params={
4283 "type": 4,
4284 "message": "[client-ide] Done processing file changes",
4287 .request(
4288 line=line(),
4289 comment="Totally normal hover",
4290 method="textDocument/hover",
4291 params={
4292 "textDocument": {"uri": "${php_file_uri}"},
4293 "position": {"line": 14, "character": 37},
4295 result={
4296 "contents": [
4298 "language": "hack",
4299 "value": "public static function staticMethod(string $z): void",
4301 'During testing, we\'ll remove the "public" tag from this '
4302 "method\n"
4303 "to ensure that we can still get IDE services",
4304 "Full name: `HoverWithErrorsClass::staticMethod`",
4306 "range": {
4307 "end": {"character": 39, "line": 14},
4308 "start": {"character": 27, "line": 14},
4311 powered_by="serverless_ide",
4313 .notification(
4314 comment="Remove the 'public' visibility modifier which triggers AST->AAST errors",
4315 method="textDocument/didChange",
4316 params={
4317 "textDocument": {"uri": "${php_file_uri}"},
4318 "contentChanges": [
4320 "range": {
4321 "start": {"line": 10, "character": 2},
4322 "end": {"line": 10, "character": 8},
4324 "text": "",
4329 .request(
4330 line=line(),
4331 comment="Hover should still work even if visibility modifier has been removed",
4332 method="textDocument/hover",
4333 params={
4334 "textDocument": {"uri": "${php_file_uri}"},
4335 "position": {"line": 14, "character": 37},
4337 result={
4338 "contents": [
4340 "language": "hack",
4341 "value": "public static function staticMethod(string $z): void",
4343 'During testing, we\'ll remove the "public" tag from this '
4344 "method\n"
4345 "to ensure that we can still get IDE services",
4346 "Full name: `HoverWithErrorsClass::staticMethod`",
4348 "range": {
4349 "end": {"character": 39, "line": 14},
4350 "start": {"character": 27, "line": 14},
4353 powered_by="serverless_ide",
4355 .request(line=line(), method="shutdown", params={}, result=None)
4356 .notification(method="exit", params={})
4358 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4360 def test_serverless_ide_formatting(self) -> None:
4361 # This test will fail if hackfmt can't be found
4362 if not self.test_driver.run_hackfmt_check():
4363 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4365 variables = dict(self.prepare_serverless_ide_environment())
4366 variables.update(self.setup_php_file("messy.php"))
4368 self.test_driver.stop_hh_server()
4370 spec = (
4371 self.initialize_spec(LspTestSpec("formatting"), use_serverless_ide=True)
4372 .notification(
4373 method="textDocument/didOpen",
4374 params={
4375 "textDocument": {
4376 "uri": "${php_file_uri}",
4377 "languageId": "hack",
4378 "version": 1,
4379 "text": "${php_file}",
4383 .request(
4384 line=line(),
4385 method="textDocument/formatting",
4386 params={
4387 "textDocument": {"uri": "${php_file_uri}"},
4388 "options": {"tabSize": 5, "insertSpaces": True},
4390 result=[
4392 "range": {
4393 "start": {"line": 0, "character": 0},
4394 "end": {"line": 12, "character": 0},
4396 "newText": "<?hh //strict\n\nfunction x(): string {\n"
4397 + ' $a = "this";\n\n'
4398 + ' $b = "is";\n\n'
4399 + ' $c = "messy";\n\n'
4400 + ' $d = ".";\n'
4401 + ' return "$a"."$b"."$c"."d";\n}\n',
4405 .request(line=line(), method="shutdown", params={}, result=None)
4406 .notification(method="exit", params={})
4408 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4410 def test_serverless_ide_rangeformatting(self) -> None:
4411 # This test will fail if hackfmt can't be found
4412 if not self.test_driver.run_hackfmt_check():
4413 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4415 variables = dict(self.prepare_serverless_ide_environment())
4416 variables.update(self.setup_php_file("messy.php"))
4418 self.test_driver.stop_hh_server()
4420 spec = (
4421 self.initialize_spec(
4422 LspTestSpec("range_formatting"), use_serverless_ide=True
4424 .notification(
4425 method="textDocument/didOpen",
4426 params={
4427 "textDocument": {
4428 "uri": "${php_file_uri}",
4429 "languageId": "hack",
4430 "version": 1,
4431 "text": "${php_file}",
4435 .request(
4436 line=line(),
4437 method="textDocument/rangeFormatting",
4438 params={
4439 "textDocument": {"uri": "${php_file_uri}"},
4440 "range": {
4441 "start": {"line": 3, "character": 0},
4442 "end": {"line": 4, "character": 0},
4444 "options": {"tabSize": 5, "insertSpaces": True},
4446 result=[
4448 "range": {
4449 "start": {"line": 3, "character": 0},
4450 "end": {"line": 4, "character": 0},
4452 "newText": ' $a = "this";\n',
4456 .request(line=line(), method="shutdown", params={}, result=None)
4457 .notification(method="exit", params={})
4459 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4461 def test_serverless_ide_ontypeformatting(self) -> None:
4462 # This test will fail if hackfmt can't be found
4463 if not self.test_driver.run_hackfmt_check():
4464 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4466 variables = dict(self.prepare_serverless_ide_environment())
4467 variables.update(self.setup_php_file("ontypeformatting.php"))
4469 spec = (
4470 self.initialize_spec(
4471 LspTestSpec("ontypeformatting"), use_serverless_ide=True
4473 .notification(
4474 method="textDocument/didOpen",
4475 params={
4476 "textDocument": {
4477 "uri": "${php_file_uri}",
4478 "languageId": "hack",
4479 "version": 1,
4480 "text": "${php_file}",
4484 .request(
4485 line=line(),
4486 method="textDocument/onTypeFormatting",
4487 params={
4488 "textDocument": {"uri": "${php_file_uri}"},
4489 "position": {"line": 9, "character": 58},
4490 "ch": ";",
4491 "options": {"tabSize": 2, "insertSpaces": True},
4493 result=[
4495 "range": {
4496 "start": {"line": 5, "character": 23},
4497 "end": {"line": 9, "character": 58},
4499 "newText": "{\n test_otf(\n"
4500 + " '1234567890',\n"
4501 + " '1234567890',\n"
4502 + " '1234567890',\n"
4503 + " '1234567890',\n"
4504 + " '1234567890',\n"
4505 + " '1234567890',\n );",
4509 .request(
4510 line=line(),
4511 method="textDocument/onTypeFormatting",
4512 params={
4513 "textDocument": {"uri": "${php_file_uri}"},
4514 "position": {"line": 15, "character": 23},
4515 "ch": "}",
4516 "options": {"tabSize": 2, "insertSpaces": True},
4518 result=[
4520 "range": {
4521 "start": {"line": 15, "character": 0},
4522 "end": {"line": 15, "character": 23},
4524 "newText": "function otf(): void {}",
4528 .request(line=line(), method="shutdown", params={}, result=None)
4529 .notification(method="exit", params={})
4532 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4534 def test_did_change(self) -> None:
4535 self.prepare_server_environment()
4536 variables = self.setup_php_file("didchange.php")
4537 spec = (
4538 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
4539 .wait_for_hh_server_ready()
4540 .notification(
4541 method="textDocument/didOpen",
4542 params={
4543 "textDocument": {
4544 "uri": "${php_file_uri}",
4545 "languageId": "hack",
4546 "version": 1,
4547 "text": "${php_file}",
4551 .notification(
4552 method="textDocument/didChange",
4553 params={
4554 "textDocument": {"uri": "${php_file_uri}"},
4555 "contentChanges": [
4557 "range": {
4558 "start": {"line": 7, "character": 11},
4559 "end": {"line": 7, "character": 12},
4561 "text": "a",
4566 .wait_for_notification(
4567 method="textDocument/publishDiagnostics",
4568 params={
4569 "uri": "${php_file_uri}",
4570 "diagnostics": [
4572 "range": {
4573 "start": {"line": 7, "character": 11},
4574 "end": {"line": 7, "character": 11},
4576 "severity": 1,
4577 "code": 1002,
4578 "source": "Hack",
4579 "message": "A semicolon ; is expected here.",
4580 "relatedLocations": [],
4581 "relatedInformation": [],
4586 .request(line=line(), method="shutdown", params={}, result=None)
4587 .wait_for_notification(
4588 comment="Hack appears to clear out diagnostics before shutting down",
4589 method="textDocument/publishDiagnostics",
4590 params={"uri": "${php_file_uri}", "diagnostics": []},
4592 .notification(method="exit", params={})
4594 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
4596 def test_go_to_implementation(self) -> None:
4597 self.prepare_server_environment()
4598 variables = self.setup_php_file("go_to_implementation.php")
4599 spec = (
4600 self.initialize_spec(
4601 LspTestSpec("test_go_to_implementation"), use_serverless_ide=False
4603 .wait_for_hh_server_ready()
4604 .notification(
4605 method="textDocument/didOpen",
4606 params={
4607 "textDocument": {
4608 "uri": "${php_file_uri}",
4609 "languageId": "hack",
4610 "version": 1,
4611 "text": "${php_file}",
4615 .request(
4616 line=line(),
4617 comment="go to implemenetation: abstract class",
4618 method="textDocument/implementation",
4619 params={
4620 "textDocument": {"uri": "${php_file_uri}"},
4621 "position": {"line": 1, "character": 17},
4623 result=[
4625 "uri": "${php_file_uri}",
4626 "range": {
4627 "start": {"line": 7, "character": 6},
4628 "end": {"line": 7, "character": 9},
4633 .request(
4634 line=line(),
4635 comment="go to implemenetation: interface",
4636 method="textDocument/implementation",
4637 params={
4638 "textDocument": {"uri": "${php_file_uri}"},
4639 "position": {"line": 13, "character": 13},
4641 result=[
4643 "uri": "${php_file_uri}",
4644 "range": {
4645 "start": {"line": 17, "character": 6},
4646 "end": {"line": 17, "character": 9},
4651 .request(
4652 line=line(),
4653 comment="go to implemenetation: trait",
4654 method="textDocument/implementation",
4655 params={
4656 "textDocument": {"uri": "${php_file_uri}"},
4657 "position": {"line": 23, "character": 10},
4659 result=[
4661 "uri": "${php_file_uri}",
4662 "range": {
4663 "start": {"line": 30, "character": 6},
4664 "end": {"line": 30, "character": 16},
4669 .request(
4670 line=line(),
4671 comment="go to implemenetation: method",
4672 method="textDocument/implementation",
4673 params={
4674 "textDocument": {"uri": "${php_file_uri}"},
4675 "position": {"line": 19, "character": 18},
4677 result=[
4679 "uri": "${php_file_uri}",
4680 "range": {
4681 "start": {"line": 8, "character": 18},
4682 "end": {"line": 8, "character": 22},
4687 .request(line=line(), method="shutdown", params={}, result=None)
4688 .notification(method="exit", params={})
4690 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
4692 def test_signature_help(self) -> None:
4693 self.prepare_server_environment()
4694 variables = self.setup_php_file("signaturehelp.php")
4695 spec = (
4696 self.initialize_spec(
4697 LspTestSpec("test_signature_help"), use_serverless_ide=False
4699 .wait_for_hh_server_ready()
4700 .notification(
4701 method="textDocument/didOpen",
4702 params={
4703 "textDocument": {
4704 "uri": "${php_file_uri}",
4705 "languageId": "hack",
4706 "version": 1,
4707 "text": "${php_file}",
4711 .request(
4712 line=line(),
4713 comment="signature help for 0-argument constructor"
4714 " (left of opening paren)",
4715 method="textDocument/signatureHelp",
4716 params={
4717 "textDocument": {"uri": "${php_file_uri}"},
4718 "position": {"line": 16, "character": 18},
4720 result=None,
4722 .request(
4723 line=line(),
4724 comment="signature help for 0-argument constructor",
4725 method="textDocument/signatureHelp",
4726 params={
4727 "textDocument": {"uri": "${php_file_uri}"},
4728 "position": {"line": 16, "character": 19},
4730 result={
4731 "signatures": [
4733 "label": "public function __construct(): void",
4734 "documentation": "Constructor with doc block",
4735 "parameters": [],
4738 "activeSignature": 0,
4739 "activeParameter": 0,
4742 .request(
4743 line=line(),
4744 comment="signature help for 0-argument constructor"
4745 " (right of closing paren)",
4746 method="textDocument/signatureHelp",
4747 params={
4748 "textDocument": {"uri": "${php_file_uri}"},
4749 "position": {"line": 16, "character": 20},
4751 result=None,
4753 .request(
4754 line=line(),
4755 comment="signature help for 2-argument instance method"
4756 " (left of opening paren)",
4757 method="textDocument/signatureHelp",
4758 params={
4759 "textDocument": {"uri": "${php_file_uri}"},
4760 "position": {"line": 17, "character": 20},
4762 result=None,
4764 .request(
4765 line=line(),
4766 comment="signature help for 2-argument instance method"
4767 " (right of opening paren)",
4768 method="textDocument/signatureHelp",
4769 params={
4770 "textDocument": {"uri": "${php_file_uri}"},
4771 "position": {"line": 17, "character": 21},
4773 result={
4774 "signatures": [
4776 "label": "public function instanceMethod"
4777 "(int $x1, int $x2): void",
4778 "documentation": "Instance method with doc block",
4779 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4782 "activeSignature": 0,
4783 "activeParameter": 0,
4786 .request(
4787 line=line(),
4788 comment="signature help for 2-argument instance method"
4789 " (left of first comma)",
4790 method="textDocument/signatureHelp",
4791 params={
4792 "textDocument": {"uri": "${php_file_uri}"},
4793 "position": {"line": 17, "character": 22},
4795 result={
4796 "signatures": [
4798 "label": "public function instanceMethod"
4799 "(int $x1, int $x2): void",
4800 "documentation": "Instance method with doc block",
4801 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4804 "activeSignature": 0,
4805 "activeParameter": 1,
4808 .request(
4809 line=line(),
4810 comment="signature help for 2-argument instance method"
4811 " (right of first comma)",
4812 method="textDocument/signatureHelp",
4813 params={
4814 "textDocument": {"uri": "${php_file_uri}"},
4815 "position": {"line": 17, "character": 23},
4817 result={
4818 "signatures": [
4820 "label": "public function instanceMethod"
4821 "(int $x1, int $x2): void",
4822 "documentation": "Instance method with doc block",
4823 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4826 "activeSignature": 0,
4827 "activeParameter": 1,
4830 .request(
4831 line=line(),
4832 comment="signature help for 2-argument instance method"
4833 " (left of closing paren)",
4834 method="textDocument/signatureHelp",
4835 params={
4836 "textDocument": {"uri": "${php_file_uri}"},
4837 "position": {"line": 17, "character": 24},
4839 result={
4840 "signatures": [
4842 "label": "public function instanceMethod"
4843 "(int $x1, int $x2): void",
4844 "documentation": "Instance method with doc block",
4845 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4848 "activeSignature": 0,
4849 "activeParameter": 1,
4852 .request(
4853 line=line(),
4854 comment="signature help for 2-argument instance method"
4855 " (right of closing paren)",
4856 method="textDocument/signatureHelp",
4857 params={
4858 "textDocument": {"uri": "${php_file_uri}"},
4859 "position": {"line": 17, "character": 25},
4861 result=None,
4863 .request(
4864 line=line(),
4865 comment="signature help for 1-argument static method"
4866 " (left of open paren)",
4867 method="textDocument/signatureHelp",
4868 params={
4869 "textDocument": {"uri": "${php_file_uri}"},
4870 "position": {"line": 18, "character": 23},
4872 result=None,
4874 .request(
4875 line=line(),
4876 comment="signature help for 1-argument static method"
4877 " (right of open paren)",
4878 method="textDocument/signatureHelp",
4879 params={
4880 "textDocument": {"uri": "${php_file_uri}"},
4881 "position": {"line": 18, "character": 24},
4883 result={
4884 "signatures": [
4886 "label": "public static function staticMethod"
4887 "(string $z): void",
4888 "documentation": "Static method with doc block",
4889 "parameters": [{"label": "$z"}],
4892 "activeSignature": 0,
4893 "activeParameter": 0,
4896 .request(
4897 line=line(),
4898 comment="signature help for 2-argument global function"
4899 " (left of open paren)",
4900 method="textDocument/signatureHelp",
4901 params={
4902 "textDocument": {"uri": "${php_file_uri}"},
4903 "position": {"line": 19, "character": 17},
4905 result=None,
4907 .request(
4908 line=line(),
4909 comment="signature help for 2-argument global function"
4910 " (right of open paren)",
4911 method="textDocument/signatureHelp",
4912 params={
4913 "textDocument": {"uri": "${php_file_uri}"},
4914 "position": {"line": 19, "character": 18},
4916 result={
4917 "signatures": [
4919 "label": "function global_function"
4920 "(string $s, int $x): void",
4921 "documentation": "Global function with doc block",
4922 "parameters": [{"label": "$s"}, {"label": "$x"}],
4925 "activeSignature": 0,
4926 "activeParameter": 0,
4929 .request(
4930 line=line(),
4931 comment="signature help for 1-argument namespace-aliased global"
4932 " function (right of open paren)",
4933 method="textDocument/signatureHelp",
4934 params={
4935 "textDocument": {"uri": "${php_file_uri}"},
4936 "position": {"line": 20, "character": 26},
4938 result=None,
4940 .request(
4941 line=line(),
4942 comment="signature help for 1-argument namespace-aliased global"
4943 " function (right of open paren)",
4944 method="textDocument/signatureHelp",
4945 params={
4946 "textDocument": {"uri": "${php_file_uri}"},
4947 "position": {"line": 20, "character": 26},
4949 result=None,
4951 .request(
4952 line=line(),
4953 comment="signature help for 1-argument namespace-aliased global"
4954 " function (right of open paren)",
4955 method="textDocument/signatureHelp",
4956 params={
4957 "textDocument": {"uri": "${php_file_uri}"},
4958 "position": {"line": 20, "character": 27},
4960 result={
4961 "signatures": [
4963 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
4964 "documentation": "Namespace-aliased function with doc block",
4965 "parameters": [{"label": "$s"}],
4968 "activeSignature": 0,
4969 "activeParameter": 0,
4972 .request(
4973 line=line(),
4974 comment="signature help for 1-argument namespace-aliased global"
4975 " function (right of open paren)",
4976 method="textDocument/signatureHelp",
4977 params={
4978 "textDocument": {"uri": "${php_file_uri}"},
4979 "position": {"line": 20, "character": 28},
4981 result={
4982 "signatures": [
4984 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
4985 "documentation": "Namespace-aliased function with doc block",
4986 "parameters": [{"label": "$s"}],
4989 "activeSignature": 0,
4990 "activeParameter": 0,
4993 .request(
4994 line=line(),
4995 comment="signature help for 2-argument function with params"
4996 " (right of open paren)",
4997 method="textDocument/signatureHelp",
4998 params={
4999 "textDocument": {"uri": "${php_file_uri}"},
5000 "position": {"line": 21, "character": 30},
5002 result={
5003 "signatures": [
5005 "label": "function test_signature_help_params1("
5006 "\n string $param1,\n string $param2\n): void",
5007 "documentation": "comment describing the method"
5008 "\n@param $param1 info1"
5009 "\n@param param2 info2",
5010 "parameters": [
5011 {"label": "$param1", "documentation": "info1"},
5012 {"label": "$param2", "documentation": "info2"},
5016 "activeSignature": 0,
5017 "activeParameter": 0,
5020 .request(
5021 line=line(),
5022 comment="signature help for 2-argument function with params"
5023 " (right of open paren)",
5024 method="textDocument/signatureHelp",
5025 params={
5026 "textDocument": {"uri": "${php_file_uri}"},
5027 "position": {"line": 22, "character": 30},
5029 result={
5030 "signatures": [
5032 "label": "function test_signature_help_params2("
5033 "\n string $param1,\n string $param2\n): void",
5034 "documentation": "comment describing the method"
5035 "\n@param $param1 info1",
5036 "parameters": [
5037 {"label": "$param1", "documentation": "info1"},
5038 {"label": "$param2"},
5042 "activeSignature": 0,
5043 "activeParameter": 0,
5046 .request(
5047 line=line(),
5048 comment="signature help for 2-argument function with params"
5049 " (right of open paren)",
5050 method="textDocument/signatureHelp",
5051 params={
5052 "textDocument": {"uri": "${php_file_uri}"},
5053 "position": {"line": 23, "character": 30},
5055 result={
5056 "signatures": [
5058 "label": "function test_signature_help_params3("
5059 "\n string $param1,\n string $param2\n): string",
5060 "documentation": "@param $param1 info1"
5061 "\n for param1"
5062 "\n@param $param2 info2"
5063 "\n@return the string"
5064 "\n 'hack'",
5065 "parameters": [
5067 "label": "$param1",
5068 "documentation": "info1 for param1",
5070 {"label": "$param2", "documentation": "info2"},
5074 "activeSignature": 0,
5075 "activeParameter": 0,
5078 .request(line=line(), method="shutdown", params={}, result=None)
5079 .notification(method="exit", params={})
5081 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5083 def test_signature_help_lambda(self) -> None:
5084 self.prepare_server_environment()
5085 variables = self.setup_php_file("signaturehelp_lambda.php")
5086 spec = (
5087 self.initialize_spec(
5088 LspTestSpec("test_serverless_ide_signature_help_lambda"),
5089 use_serverless_ide=False,
5091 .wait_for_hh_server_ready()
5092 .notification(
5093 method="textDocument/didOpen",
5094 params={
5095 "textDocument": {
5096 "uri": "${php_file_uri}",
5097 "languageId": "hack",
5098 "version": 1,
5099 "text": "${php_file}",
5103 .request(
5104 line=line(),
5105 comment="signature help for a normal function call",
5106 method="textDocument/signatureHelp",
5107 params={
5108 "textDocument": {"uri": "${php_file_uri}"},
5109 "position": {"line": 8, "character": 29},
5111 result={
5112 "activeParameter": 0,
5113 "activeSignature": 0,
5114 "signatures": [
5116 "label": "function test_lambda_sighelp(\n"
5117 " string $str,\n"
5118 " (function(string): int) $f\n"
5119 "): int",
5120 "parameters": [{"label": "$str"}, {"label": "$f"}],
5125 .request(
5126 line=line(),
5127 comment="signature help for normal function call within a lambda",
5128 method="textDocument/signatureHelp",
5129 params={
5130 "textDocument": {"uri": "${php_file_uri}"},
5131 "position": {"line": 9, "character": 21},
5133 result={
5134 "activeParameter": 0,
5135 "activeSignature": 0,
5136 "signatures": [
5138 "label": "function normal_test_func(string $str): void",
5139 "parameters": [{"label": "$str"}],
5144 .request(
5145 line=line(),
5146 comment="signature help for text within a lambda, left side of an open paren",
5147 method="textDocument/signatureHelp",
5148 params={
5149 "textDocument": {"uri": "${php_file_uri}"},
5150 "position": {"line": 10, "character": 15},
5152 result=None,
5154 .request(
5155 line=line(),
5156 comment="signature help for text within a lambda, right side of an open paren",
5157 method="textDocument/signatureHelp",
5158 params={
5159 "textDocument": {"uri": "${php_file_uri}"},
5160 "position": {"line": 10, "character": 16},
5162 result=None,
5164 .request(line=line(), method="shutdown", params={}, result=None)
5165 .notification(method="exit", params={})
5167 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5169 def test_rename(self) -> None:
5170 self.prepare_server_environment()
5171 variables = self.setup_php_file("rename.php")
5172 self.load_and_run("rename", variables)
5174 def test_references(self) -> None:
5175 self.prepare_server_environment()
5176 variables = self.setup_php_file("references.php")
5177 self.load_and_run("references", variables)
5179 def test_non_existing_method(self) -> None:
5180 self.prepare_server_environment()
5181 variables = self.setup_php_file("nomethod.php")
5182 self.load_and_run("nomethod", variables)
5184 def test_bad_call(self) -> None:
5185 self.prepare_server_environment()
5186 variables = self.setup_php_file("bad_call.php")
5187 self.load_and_run("bad_call", variables)
5189 def test_code_action_missing_method(self) -> None:
5190 variables = dict(self.prepare_serverless_ide_environment())
5191 variables.update(self.setup_php_file("code_action_missing_method.php"))
5192 self.test_driver.stop_hh_server()
5194 spec = (
5195 self.initialize_spec(
5196 LspTestSpec("code_action_missing_method"), use_serverless_ide=True
5198 .notification(
5199 method="textDocument/didOpen",
5200 params={
5201 "textDocument": {
5202 "uri": "${php_file_uri}",
5203 "languageId": "hack",
5204 "version": 1,
5205 "text": "${php_file}",
5209 .notification(
5210 comment="make local, unsaved change to the file",
5211 method="textDocument/didChange",
5212 params={
5213 "textDocument": {"uri": "${php_file_uri}", "version": 2},
5214 "contentChanges": [
5216 "text": """\
5217 <?hh
5219 class ClassWithFooBar {
5220 public function foobar(): void {}
5223 function call_method(ClassWithFooBar $mc): void {
5224 $mc->foobaz();
5231 .request(
5232 line=line(),
5233 comment="get actions",
5234 method="textDocument/codeAction",
5235 params={
5236 "textDocument": {"uri": "${php_file_uri}"},
5237 "range": {
5238 "start": {"line": 7, "character": 7},
5239 "end": {"line": 7, "character": 13},
5241 "context": {
5242 "diagnostics": [
5244 "range": {
5245 "start": {"line": 7, "character": 7},
5246 "end": {"line": 7, "character": 13},
5248 "severity": 1,
5249 "code": 4053,
5250 "source": "Hack",
5251 "message": "No instance method foobaz in ClassWithFooBar",
5252 "relatedInformation": [
5254 "location": {
5255 "uri": "${php_file_uri}",
5256 "range": {
5257 "start": {"line": 3, "character": 18},
5258 "end": {"line": 3, "character": 24},
5261 "message": "Did you mean foobar instead?",
5264 "location": {
5265 "uri": "${php_file_uri}",
5266 "range": {
5267 "start": {"line": 6, "character": 21},
5268 "end": {"line": 6, "character": 36},
5271 "message": "This is why I think it is an object of type ClassWithFooBar",
5274 "location": {
5275 "uri": "${php_file_uri}",
5276 "range": {
5277 "start": {"line": 2, "character": 6},
5278 "end": {"line": 2, "character": 21},
5281 "message": "Declaration of ClassWithFooBar is here",
5284 "relatedLocations": [
5286 "location": {
5287 "uri": "${php_file_uri}",
5288 "range": {
5289 "start": {"line": 3, "character": 18},
5290 "end": {"line": 3, "character": 24},
5293 "message": "Did you mean foobar instead?",
5296 "location": {
5297 "uri": "${php_file_uri}",
5298 "range": {
5299 "start": {"line": 6, "character": 21},
5300 "end": {"line": 6, "character": 36},
5303 "message": "This is why I think it is an object of type ClassWithFooBar",
5306 "location": {
5307 "uri": "${php_file_uri}",
5308 "range": {
5309 "start": {"line": 2, "character": 6},
5310 "end": {"line": 2, "character": 21},
5313 "message": "Declaration of ClassWithFooBar is here",
5320 result=[
5322 "title": "Change to ->foobar",
5323 "kind": "quickfix",
5324 "diagnostics": [],
5325 "edit": {
5326 "changes": {
5327 "${root_path}/code_action_missing_method.php": [
5329 "range": {
5330 "start": {"line": 7, "character": 7},
5331 "end": {"line": 7, "character": 13},
5333 "newText": "foobar",
5340 powered_by="serverless_ide",
5342 .request(line=line(), method="shutdown", params={}, result=None)
5343 .notification(method="exit", params={})
5345 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5347 def test_non_blocking(self) -> None:
5348 self.prepare_server_environment()
5349 variables = self.setup_php_file("non_blocking.php")
5350 self.test_driver.start_hh_loop_forever_assert_timeout()
5351 spec = (
5352 self.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide=False)
5353 .ignore_notifications(method="textDocument/publishDiagnostics")
5354 .wait_for_hh_server_ready()
5355 .request(
5356 line=line(),
5357 method="textDocument/definition",
5358 params={
5359 "textDocument": {"uri": "${php_file_uri}"},
5360 "position": {"line": 7, "character": 11},
5362 result=[
5364 "uri": "file://${root_path}/non_blocking.php",
5365 "range": {
5366 "start": {"line": 2, "character": 9},
5367 "end": {"line": 2, "character": 32},
5369 "title": "non_blocking_definition",
5372 wait_id="definition request",
5374 .notification(
5375 comment="remove hh_loop_forever() invocation to break the infinite loop",
5376 method="textDocument/didOpen",
5377 params={
5378 "textDocument": {
5379 "uri": "${root_path}/__hh_loop_forever_foo.php",
5380 "languageId": "hack",
5381 "version": 1,
5382 "text": """\
5383 <?hh // strict
5385 function __hh_loop_forever_foo(): int {
5386 return 4;
5388 """,
5392 .wait_for_response(wait_id="definition request")
5393 .request(line=line(), method="shutdown", params={}, result=None)
5394 .notification(method="exit", params={})
5396 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5398 def test_serverless_ide_hierarchy_file_change_on_disk(self) -> None:
5399 variables = dict(self.prepare_serverless_ide_environment())
5400 variables.update(self.setup_php_file("incremental_derived.php"))
5401 changed_php_file_uri = self.repo_file("incremental_base.php")
5402 variables.update({"changed_php_file_uri": changed_php_file_uri})
5403 self.test_driver.stop_hh_server()
5405 spec = (
5406 self.initialize_spec(
5407 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
5408 use_serverless_ide=True,
5410 .notification(
5411 method="textDocument/didOpen",
5412 params={
5413 "textDocument": {
5414 "uri": "${php_file_uri}",
5415 "languageId": "hack",
5416 "version": 1,
5417 "text": "${php_file}",
5421 .request(
5422 line=line(),
5423 comment="hover before change to class hierarchy should be `int`",
5424 method="textDocument/hover",
5425 params={
5426 "textDocument": {"uri": "${php_file_uri}"},
5427 "position": {"line": 7, "character": 14},
5429 result={
5430 "contents": [
5431 {"language": "hack", "value": "public function foo(): int"},
5432 "Full name: `BaseClassIncremental::foo`",
5434 "range": {
5435 "start": {"line": 7, "character": 12},
5436 "end": {"line": 7, "character": 15},
5439 powered_by="serverless_ide",
5441 .write_to_disk(
5442 uri=changed_php_file_uri,
5443 contents="""\
5444 <?hh // strict
5445 class BaseClassIncremental {
5446 public function foo(): string { return ''; }
5448 """,
5449 notify=True,
5451 .request(
5452 line=line(),
5453 comment="hover after change to class hierarchy should be `string`",
5454 method="textDocument/hover",
5455 params={
5456 "textDocument": {"uri": "${php_file_uri}"},
5457 "position": {"line": 7, "character": 14},
5459 result={
5460 "contents": [
5461 {"language": "hack", "value": "public function foo(): string"},
5462 "Full name: `BaseClassIncremental::foo`",
5464 "range": {
5465 "start": {"line": 7, "character": 12},
5466 "end": {"line": 7, "character": 15},
5469 powered_by="serverless_ide",
5471 .request(line=line(), method="shutdown", params={}, result=None)
5472 .notification(method="exit", params={})
5475 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5477 def test_serverless_ide_decl_in_unsaved_buffer_changed(self) -> None:
5478 variables = dict(self.prepare_serverless_ide_environment())
5479 variables.update(self.setup_php_file("hover.php"))
5480 self.test_driver.stop_hh_server()
5482 spec = (
5483 self.initialize_spec(
5484 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
5485 use_serverless_ide=True,
5487 .notification(
5488 method="textDocument/didOpen",
5489 params={
5490 "textDocument": {
5491 "uri": "${php_file_uri}",
5492 "languageId": "hack",
5493 "version": 1,
5494 "text": "${php_file}",
5498 .request(
5499 line=line(),
5500 comment="hover over function invocation",
5501 method="textDocument/hover",
5502 params={
5503 "textDocument": {"uri": "${php_file_uri}"},
5504 "position": {"line": 3, "character": 16},
5506 result={
5507 "contents": [
5508 {"language": "hack", "value": "int"},
5509 "A comment describing b_hover.",
5511 "range": {
5512 "start": {"line": 3, "character": 9},
5513 "end": {"line": 3, "character": 16},
5516 powered_by="serverless_ide",
5518 .notification(
5519 comment="make local, unsaved change to the file",
5520 method="textDocument/didChange",
5521 params={
5522 "textDocument": {"uri": "${php_file_uri}", "version": 2},
5523 "contentChanges": [
5525 "text": """\
5526 <?hh // strict
5527 // comment
5528 function a_hover(): int {
5529 return b_hover();
5531 // A comment describing b_hover differently.
5532 function b_hover(): string {
5533 return 42;
5540 .request(
5541 line=line(),
5542 comment="another hover over function invocation, should be string now",
5543 method="textDocument/hover",
5544 params={
5545 "textDocument": {"uri": "${php_file_uri}"},
5546 "position": {"line": 3, "character": 16},
5548 result={
5549 "contents": [
5550 {"language": "hack", "value": "string"},
5551 "A comment describing b_hover differently.",
5553 "range": {
5554 "start": {"line": 3, "character": 9},
5555 "end": {"line": 3, "character": 16},
5558 powered_by="serverless_ide",
5560 .request(line=line(), method="shutdown", params={}, result=None)
5561 .notification(method="exit", params={})
5564 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5566 def test_serverless_ide_decl_two_unsaved_buffers(self) -> None:
5567 variables = dict(self.prepare_serverless_ide_environment())
5568 variables.update(self.setup_php_file("unsaved1.php"))
5569 variables.update({"unsaved2_file_uri": self.repo_file_uri("unsaved2.php")})
5570 self.test_driver.stop_hh_server()
5572 spec = (
5573 self.initialize_spec(
5574 LspTestSpec("test_serverless_ide_decl_two_unsaved_buffers"),
5575 use_serverless_ide=True,
5577 .notification(
5578 comment="open 'unsaved1.php', since we'll be hovering in it",
5579 method="textDocument/didOpen",
5580 params={
5581 "textDocument": {
5582 "uri": "${php_file_uri}",
5583 "languageId": "hack",
5584 "version": 1,
5585 "text": "${php_file}",
5589 .notification(
5590 comment="open 'unsaved2.php' with a bool-returning signature, different from disk",
5591 method="textDocument/didOpen",
5592 params={
5593 "textDocument": {
5594 "uri": "${unsaved2_file_uri}",
5595 "languageId": "hack",
5596 "version": 1,
5597 "text": """\
5598 <?hh //strict
5599 function unsaved_bar(): bool { return true; }
5600 """,
5604 .request(
5605 line=line(),
5606 comment="hover 'unsaved1.php' is with respect to disk contents of 'unsaved2.php'",
5607 method="textDocument/hover",
5608 params={
5609 "textDocument": {"uri": "${php_file_uri}"},
5610 "position": {"line": 1, "character": 39},
5612 result={
5613 "contents": [
5614 {"language": "hack", "value": "function unsaved_bar(): int"},
5616 "range": {
5617 "start": {"line": 1, "character": 34},
5618 "end": {"line": 1, "character": 45},
5621 powered_by="serverless_ide",
5623 .notification(
5624 comment="change signature in 'unsaved2.php' to return string",
5625 method="textDocument/didChange",
5626 params={
5627 "textDocument": {"uri": "${unsaved2_file_uri}", "version": 2},
5628 "contentChanges": [
5630 "text": """\
5631 <?hh //strict
5632 function unsaved_bar(): string { return "hello"; }
5638 .request(
5639 line=line(),
5640 comment="this is a dummy hover in 'unsaved2.php' just to ensure its decl is cached",
5641 method="textDocument/hover",
5642 params={
5643 "textDocument": {"uri": "${unsaved2_file_uri}"},
5644 "position": {"line": 0, "character": 0},
5646 result=None,
5647 powered_by="serverless_ide",
5649 .request(
5650 line=line(),
5651 comment="hover 'unsaved1.php' is still with respect to disk contents of 'unsaved2.php'",
5652 method="textDocument/hover",
5653 params={
5654 "textDocument": {"uri": "${php_file_uri}"},
5655 "position": {"line": 1, "character": 39},
5657 result={
5658 "contents": [
5659 {"language": "hack", "value": "function unsaved_bar(): int"},
5661 "range": {
5662 "start": {"line": 1, "character": 34},
5663 "end": {"line": 1, "character": 45},
5666 powered_by="serverless_ide",
5668 .write_to_disk(
5669 comment="save signature in 'unsaved2' to return string",
5670 uri=variables["unsaved2_file_uri"],
5671 contents="""\
5672 <?hh // strict
5673 function unsaved_bar(): string { return "hello"; }
5674 """,
5675 notify=True,
5677 .request(
5678 line=line(),
5679 comment="hover 'unsaved1.php' gets new disk contents of 'unsaved2.php'",
5680 method="textDocument/hover",
5681 params={
5682 "textDocument": {"uri": "${php_file_uri}"},
5683 "position": {"line": 1, "character": 39},
5685 result={
5686 "contents": [
5687 {"language": "hack", "value": "function unsaved_bar(): string"},
5689 "range": {
5690 "start": {"line": 1, "character": 34},
5691 "end": {"line": 1, "character": 45},
5694 powered_by="serverless_ide",
5696 .request(line=line(), method="shutdown", params={}, result=None)
5697 .notification(method="exit", params={})
5700 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5702 def test_hover_without_file_open(self) -> None:
5703 variables = dict(self.prepare_serverless_ide_environment())
5704 variables.update(self.setup_php_file("hover.php"))
5705 self.test_driver.stop_hh_server()
5707 spec = (
5708 self.initialize_spec(
5709 LspTestSpec("test_hover_without_file_open"),
5710 use_serverless_ide=True,
5711 supports_status=True,
5713 .ignore_notifications(method="textDocument/publishDiagnostics")
5714 .ignore_requests(
5715 comment="Ignore 'initializing...' messages since they're racy",
5716 method="window/showStatus",
5717 params={
5718 "type": 2,
5719 "actions": [{"title": "Restart hh_server"}],
5720 "message": "Hack IDE: initializing.\nhh_server: stopped.",
5721 "shortMessage": "Hack: initializing",
5724 .ignore_requests(
5725 comment="another racy initializing, before hh_server has even responded",
5726 method="window/showStatus",
5727 params={
5728 "type": 2,
5729 "actions": [],
5730 "message": "Hack IDE: initializing.",
5731 "shortMessage": "Hack: initializing",
5734 .ignore_requests(
5735 comment="another racy initialization to ignore, again before hh_server",
5736 method="window/showStatus",
5737 params={
5738 "type": 3,
5739 "actions": [],
5740 "message": "Hack IDE: ready.",
5741 "shortMessage": "Hack: ready",
5744 .wait_for_server_request(
5745 method="window/showStatus",
5746 params={
5747 "actions": [{"title": "Restart hh_server"}],
5748 "message": "Hack IDE: ready.\nhh_server: stopped.",
5749 "shortMessage": "Hack: ready",
5750 "type": 3,
5752 result=NoResponse(),
5754 .request(
5755 line=line(),
5756 comment="hover before file_open will fail",
5757 method="textDocument/hover",
5758 params={
5759 "textDocument": {"uri": "${php_file_uri}"},
5760 "position": {"line": 26, "character": 20},
5762 result=None,
5764 .notification(
5765 method="textDocument/didOpen",
5766 params={
5767 "textDocument": {
5768 "uri": "${php_file_uri}",
5769 "languageId": "hack",
5770 "version": 1,
5771 "text": "${php_file}",
5775 .request(
5776 line=line(),
5777 comment="hover after file_open will succeed",
5778 method="textDocument/hover",
5779 params={
5780 "textDocument": {"uri": "${php_file_uri}"},
5781 "position": {"line": 26, "character": 20},
5783 result={"contents": [{"language": "hack", "value": "string"}]},
5784 powered_by="serverless_ide",
5786 .request(
5787 line=line(),
5788 method="$test/shutdownServerlessIde",
5789 params={},
5790 result=None,
5791 powered_by="serverless_ide",
5793 .wait_for_server_request(
5794 method="window/showStatus",
5795 params={
5796 "actions": [
5797 {"title": "Restart Hack IDE"},
5798 {"title": "Restart hh_server"},
5800 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: stopped.",
5801 "shortMessage": "Hack: failed",
5802 "type": 1,
5804 result={"title": "Restart Hack IDE"},
5806 .wait_for_server_request(
5807 method="window/showStatus",
5808 params={
5809 "actions": [{"title": "Restart hh_server"}],
5810 "message": "Hack IDE: ready.\nhh_server: stopped.",
5811 "shortMessage": "Hack: ready",
5812 "type": 3,
5814 result=NoResponse(),
5816 .request(
5817 line=line(),
5818 comment="hover after restart will succeed",
5819 method="textDocument/hover",
5820 params={
5821 "textDocument": {"uri": "${php_file_uri}"},
5822 "position": {"line": 26, "character": 20},
5824 result={"contents": [{"language": "hack", "value": "string"}]},
5825 powered_by="serverless_ide",
5827 .notification(
5828 method="textDocument/didClose",
5829 params={"textDocument": {"uri": "${php_file_uri}"}},
5831 .request(
5832 line=line(),
5833 comment="hover after file_close will fail",
5834 method="textDocument/hover",
5835 params={
5836 "textDocument": {"uri": "${php_file_uri}"},
5837 "position": {"line": 26, "character": 20},
5839 result=None,
5841 .request(line=line(), method="shutdown", params={}, result=None)
5842 .notification(method="exit", params={})
5845 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5847 def test_hh_server_status_diagnostic(self) -> None:
5848 variables = dict(self.prepare_serverless_ide_environment())
5849 variables.update(self.setup_php_file("unsaved1.php"))
5850 variables.update(
5852 "unsaved2_file_uri": self.repo_file_uri("unsaved2.php"),
5853 "unsaved2_file": self.read_repo_file("unsaved2.php"),
5856 self.test_driver.stop_hh_server()
5858 spec = (
5859 self.initialize_spec(
5860 LspTestSpec("test_hh_server_status_diagnostic"), use_serverless_ide=True
5862 .ignore_status_diagnostics(False)
5863 .notification(
5864 method="textDocument/didOpen",
5865 params={
5866 "textDocument": {
5867 "uri": "${php_file_uri}",
5868 "languageId": "hack",
5869 "version": 1,
5870 "text": "${php_file}",
5874 .wait_for_notification(
5875 comment="After didOpen(file1), the hh_server_status diagnostic should appear in file1",
5876 method="textDocument/publishDiagnostics",
5877 params={
5878 "uri": "${php_file_uri}",
5879 "diagnostics": [
5881 "range": {
5882 "start": {"line": 0, "character": 0},
5883 "end": {"line": 0, "character": 1},
5885 "severity": 1,
5886 "source": "hh_server",
5887 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5888 "relatedInformation": [],
5889 "relatedLocations": [],
5892 "isStatusFB": True,
5895 .notification(
5896 method="textDocument/didOpen",
5897 params={
5898 "textDocument": {
5899 "uri": "${unsaved2_file_uri}",
5900 "languageId": "hack",
5901 "version": 1,
5902 "text": "${unsaved2_file}",
5906 .wait_for_notification(
5907 comment="After didOpen(file2), the hh_server_status diagnostic should disappear from file1",
5908 method="textDocument/publishDiagnostics",
5909 params={
5910 "uri": "${php_file_uri}",
5911 "diagnostics": [],
5912 "isStatusFB": True,
5915 .wait_for_notification(
5916 comment="After didOpen(file2), the hh_server_status diagnostic should reappear in file2",
5917 method="textDocument/publishDiagnostics",
5918 params={
5919 "uri": "${unsaved2_file_uri}",
5920 "diagnostics": [
5922 "range": {
5923 "start": {"line": 0, "character": 0},
5924 "end": {"line": 0, "character": 1},
5926 "severity": 1,
5927 "source": "hh_server",
5928 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5929 "relatedInformation": [],
5930 "relatedLocations": [],
5933 "isStatusFB": True,
5936 .notification(
5937 method="textDocument/didClose",
5938 params={"textDocument": {"uri": "${unsaved2_file_uri}"}},
5940 .wait_for_notification(
5941 comment="After didClose(file2), the hh_server_status diagnostic should disappear from file2",
5942 method="textDocument/publishDiagnostics",
5943 params={
5944 "uri": "${unsaved2_file_uri}",
5945 "diagnostics": [],
5946 "isStatusFB": True,
5949 .wait_for_notification(
5950 comment="After didClose(file2), the hh_server_status diagnostic should reappear in file1",
5951 method="textDocument/publishDiagnostics",
5952 params={
5953 "uri": "${php_file_uri}",
5954 "diagnostics": [
5956 "range": {
5957 "start": {"line": 0, "character": 0},
5958 "end": {"line": 0, "character": 1},
5960 "severity": 1,
5961 "source": "hh_server",
5962 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5963 "relatedInformation": [],
5964 "relatedLocations": [],
5967 "isStatusFB": True,
5970 .notification(
5971 method="textDocument/didClose",
5972 params={"textDocument": {"uri": "${php_file_uri}"}},
5974 .wait_for_notification(
5975 comment="After didClose(file1), the hh_server_status diagnostic should disappear from file1",
5976 method="textDocument/publishDiagnostics",
5977 params={
5978 "uri": "${php_file_uri}",
5979 "diagnostics": [],
5980 "isStatusFB": True,
5983 .request(line=line(), method="shutdown", params={}, result=None)
5984 .notification(method="exit", params={})
5987 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5989 def _sanitize_gutter_line_numbers(self, s: str) -> str:
5990 gutter_line_number_re = re.compile(r"^[ ]*[0-9]+ \|", re.MULTILINE)
5991 return re.sub(gutter_line_number_re, " XXXX |", s)
5993 def test_lsptestspec_incorrect_request_result(self) -> None:
5994 variables = dict(self.prepare_serverless_ide_environment())
5995 variables.update(self.setup_php_file("hover.php"))
5996 self.test_driver.stop_hh_server()
5998 spec = (
5999 self.initialize_spec(
6000 LspTestSpec("test_lsptestspec_incorrect_request_result"),
6001 use_serverless_ide=True,
6003 .notification(
6004 method="textDocument/didOpen",
6005 params={
6006 "textDocument": {
6007 "uri": "${php_file_uri}",
6008 "languageId": "hack",
6009 "version": 1,
6010 "text": "${php_file}",
6014 .request(
6015 line=line(),
6016 comment="hover over function invocation",
6017 method="textDocument/hover",
6018 params={
6019 "textDocument": {"uri": "${php_file_uri}"},
6020 "position": {"line": 3, "character": 16},
6022 result={
6023 "contents": [
6024 {"language": "hack", "value": "int"},
6025 "INCORRECT COMMENT HERE",
6027 "range": {
6028 "start": {"line": 3, "character": 9},
6029 "end": {"line": 3, "character": 16},
6032 powered_by="serverless_ide",
6034 .request(line=line(), method="shutdown", params={}, result=None)
6035 .notification(method="exit", params={})
6037 try:
6038 self.run_spec(
6039 spec,
6040 variables=variables,
6041 wait_for_server=False,
6042 use_serverless_ide=True,
6044 raise AssertionError("Expected an error here")
6045 except AssertionError as e:
6046 self.assertEqual(
6047 self._sanitize_gutter_line_numbers(str(e)),
6048 """\
6049 Test case test_lsptestspec_incorrect_request_result failed with 1 errors:
6051 Error 1/1:
6052 Description: Request with ID 5 (comment: 'hover over function invocation') \
6053 got an incorrect result:
6055 (- is expected lines, + is actual lines)
6056 - {'contents': [{'language': 'hack', 'value': 'int'}, 'INCORRECT COMMENT HERE'],
6057 ? ---------------------------
6059 + {'contents': [{'language': 'hack', 'value': 'int'},
6060 + 'A comment describing b_hover.'],
6061 'range': {'end': {'character': 16, 'line': 3},
6062 'start': {'character': 9, 'line': 3}}}
6064 Context:
6065 This was the associated request:
6067 hphp/hack/test/integration/test_lsp.py
6068 XXXX | .request(
6069 XXXX | line=line(),
6070 XXXX | comment="hover over function invocation",
6071 XXXX | method="textDocument/hover",
6072 XXXX | params={
6073 XXXX | "textDocument": {"uri": "${php_file_uri}"},
6074 XXXX | "position": {"line": 3, "character": 16},
6075 XXXX | },
6076 XXXX | result={
6077 XXXX | "contents": [
6078 XXXX | {"language": "hack", "value": "int"},
6079 XXXX | "INCORRECT COMMENT HERE",
6080 XXXX | ],
6081 XXXX | "range": {
6082 XXXX | "start": {"line": 3, "character": 9},
6083 XXXX | "end": {"line": 3, "character": 16},
6084 XXXX | },
6085 XXXX | },
6086 XXXX | powered_by="serverless_ide",
6087 XXXX | )
6089 Remediation:
6090 1) If this was unexpected, then the language server is buggy and should be
6091 fixed.
6093 2) If this was expected, you can update your request with the following code to
6094 make it match:
6096 .request(
6097 line=line(),
6098 comment='hover over function invocation',
6099 method='textDocument/hover',
6100 params={'textDocument': {'uri': '${php_file_uri}'}, \
6101 'position': {'line': 3, 'character': 16}},
6102 result={'contents': [{'language': 'hack', 'value': 'int'}, \
6103 'A comment describing b_hover.'], \
6104 'range': {'start': {'line': 3, 'character': 9}, \
6105 'end': {'line': 3, 'character': 16}}},
6106 powered_by='serverless_ide',
6109 If you want to examine the raw LSP logs, you can check the `.sent.log` and
6110 `.received.log` files that were generated in the template repo for this test.\
6111 """,
6114 def test_lsptestspec_unexpected_notification(self) -> None:
6115 self.prepare_server_environment()
6116 variables = self.setup_php_file("didchange.php")
6117 spec = (
6118 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
6119 .wait_for_hh_server_ready()
6120 .notification(
6121 method="textDocument/didOpen",
6122 params={
6123 "textDocument": {
6124 "uri": "${php_file_uri}",
6125 "languageId": "hack",
6126 "version": 1,
6127 "text": "${php_file}",
6131 .notification(
6132 method="textDocument/didChange",
6133 params={
6134 "textDocument": {"uri": "${php_file_uri}"},
6135 "contentChanges": [
6137 "range": {
6138 "start": {"line": 7, "character": 11},
6139 "end": {"line": 7, "character": 12},
6141 "text": "a",
6146 .wait_for_notification(
6147 method="textDocument/publishDiagnostics",
6148 params={
6149 "uri": "${php_file_uri}",
6150 "diagnostics": [
6152 "range": {
6153 "start": {"line": 7, "character": 11},
6154 "end": {"line": 7, "character": 11},
6156 "severity": 1,
6157 "code": 1002,
6158 "source": "Hack",
6159 "message": "A semicolon ; is expected here.",
6160 "relatedLocations": [],
6161 "relatedInformation": [],
6166 .request(line=line(), method="shutdown", params={}, result=None)
6167 .notification(method="exit", params={})
6169 try:
6170 self.run_spec(
6171 spec, variables, wait_for_server=True, use_serverless_ide=False
6173 raise AssertionError("Expected an error here")
6174 except AssertionError as e:
6175 self.assertEqual(
6176 self._sanitize_gutter_line_numbers(str(e)),
6177 """\
6178 Test case did_change failed with 1 errors:
6180 Error 1/1:
6181 Description: An unexpected notification of type \
6182 'textDocument/publishDiagnostics' was sent by the language server.
6183 Here is the notification payload:
6185 {'jsonrpc': '2.0',
6186 'method': 'textDocument/publishDiagnostics',
6187 'params': {'diagnostics': [],
6188 'uri': '__PHP_FILE_URI__'}}
6190 Context:
6191 This was the most recent request issued from the language client before it
6192 received the notification:
6194 hphp/hack/test/integration/test_lsp.py
6195 XXXX | .request(line=line(), method="shutdown", params={}, result=None)
6197 Remediation:
6198 1) If this was unexpected, then the language server is buggy and should be
6199 fixed.
6201 2) If all notifications of type 'textDocument/publishDiagnostics' should be \
6202 ignored, add this directive
6203 anywhere in your test:
6205 .ignore_notifications(method='textDocument/publishDiagnostics')
6207 3) If this single instance of the notification was expected, add this directive
6208 to your test to wait for it before proceeding:
6210 .wait_for_notification(
6211 method='textDocument/publishDiagnostics',
6212 params={'uri': '${php_file_uri}', 'diagnostics': []},
6215 If you want to examine the raw LSP logs, you can check the `.sent.log` and
6216 `.received.log` files that were generated in the template repo for this test.\
6218 # There's an instance of a literal `${php_file_uri}` in there
6219 # which we don't want to change, so use a different name than
6220 # that one.
6221 .replace("__PHP_FILE_URI__", variables["php_file_uri"]),
6224 def test_serverless_ide_highlight(self) -> None:
6225 variables = dict(self.prepare_serverless_ide_environment())
6226 variables.update(self.setup_php_file("highlight.php"))
6227 self.test_driver.stop_hh_server()
6229 spec = (
6230 self.initialize_spec(
6231 LspTestSpec("serverless_ide_highlight"), use_serverless_ide=True
6233 .notification(
6234 method="textDocument/didOpen",
6235 params={
6236 "textDocument": {
6237 "uri": "${php_file_uri}",
6238 "languageId": "hack",
6239 "version": 1,
6240 "text": "${php_file}",
6244 .request(
6245 line=line(),
6246 comment="document highlight, id 2",
6247 method="textDocument/documentHighlight",
6248 params={
6249 "textDocument": {"uri": "${php_file_uri}"},
6250 "position": {"line": 3, "character": 10},
6252 result=[
6254 "range": {
6255 "start": {"line": 3, "character": 9},
6256 "end": {"line": 3, "character": 20},
6260 powered_by="serverless_ide",
6262 .request(
6263 line=line(),
6264 comment="shutdown, id 3",
6265 method="shutdown",
6266 params={},
6267 result=None,
6270 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6272 def test_serverless_ide_coverage(self) -> None:
6273 variables = dict(self.prepare_serverless_ide_environment())
6274 variables.update(self.setup_php_file("coverage.php"))
6275 self.test_driver.stop_hh_server()
6277 spec = (
6278 self.initialize_spec(
6279 LspTestSpec("serverless_ide_coverage"), use_serverless_ide=True
6281 .notification(
6282 method="textDocument/didOpen",
6283 params={
6284 "textDocument": {
6285 "uri": "${php_file_uri}",
6286 "languageId": "hack",
6287 "version": 1,
6288 "text": "${php_file}",
6292 .request(
6293 line=line(),
6294 comment="Check type coverage",
6295 method="textDocument/typeCoverage",
6296 params={"textDocument": {"uri": "${php_file_uri}"}},
6297 result={
6298 "coveredPercent": 100,
6299 "uncoveredRanges": [],
6300 "defaultMessage": "Un-type checked code. Consider adding type annotations.",
6302 powered_by="serverless_ide",
6304 .request(
6305 line=line(),
6306 comment="Shutdown",
6307 method="shutdown",
6308 params={},
6309 result=None,
6312 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6314 def test_status_stopped(self) -> None:
6315 self.prepare_server_environment()
6316 variables = self.setup_php_file("hover.php")
6317 self.test_driver.stop_hh_server()
6319 spec = (
6320 self.initialize_spec(
6321 LspTestSpec("status_stopped"),
6322 use_serverless_ide=False,
6323 supports_status=True,
6325 .wait_for_server_request(
6326 method="window/showStatus",
6327 params={
6328 "shortMessage": "Hack: stopped",
6329 "message": "hh_server: stopped.",
6330 "actions": [{"title": "Restart hh_server"}],
6331 "type": 1,
6333 result=NoResponse(),
6335 .request(line=line(), method="shutdown", params={}, result=None)
6336 .notification(method="exit", params={})
6338 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=False)
6340 def test_status_running(self) -> None:
6341 self.prepare_server_environment()
6342 variables = self.setup_php_file("hover.php")
6344 spec = (
6345 self.initialize_spec(
6346 LspTestSpec("status_running"),
6347 use_serverless_ide=False,
6348 supports_status=True,
6350 .ignore_requests(
6351 comment="Ignore initializing... requests since they're racy",
6352 method="window/showStatus",
6353 params={
6354 "type": 2,
6355 "shortMessage": "Hack: initializing",
6356 "message": "hh_server initializing: processing [<test> seconds]",
6357 "actions": [],
6360 .wait_for_server_request(
6361 method="window/showStatus",
6362 params={"actions": [], "message": "hh_server: ready.", "type": 3},
6363 result=NoResponse(),
6365 .request(line=line(), method="shutdown", params={}, result=None)
6366 .notification(method="exit", params={})
6368 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
6370 def test_serverless_ide_status_stopped(self) -> None:
6371 variables = dict(self.prepare_serverless_ide_environment())
6372 variables.update(self.setup_php_file("hover.php"))
6373 self.test_driver.stop_hh_server()
6375 spec = (
6376 self.initialize_spec(
6377 LspTestSpec("serverless_ide_status_stopped"),
6378 use_serverless_ide=True,
6379 supports_status=True,
6381 .ignore_requests(
6382 comment="ignore initializing... messages since they're kind of racy",
6383 method="window/showStatus",
6384 params={
6385 "type": 2,
6386 "actions": [{"title": "Restart hh_server"}],
6387 "message": "Hack IDE: initializing.\nhh_server: stopped.",
6388 "shortMessage": "Hack: initializing",
6391 .ignore_requests(
6392 comment="another racy initialization to ignore, before hh_server has even reported its status",
6393 method="window/showStatus",
6394 params={
6395 "type": 2,
6396 "actions": [],
6397 "message": "Hack IDE: initializing.",
6398 "shortMessage": "Hack: initializing",
6401 .ignore_requests(
6402 comment="another racy initialization to ignore, again before hh_server",
6403 method="window/showStatus",
6404 params={
6405 "type": 3,
6406 "actions": [],
6407 "message": "Hack IDE: ready.",
6408 "shortMessage": "Hack: ready",
6411 .wait_for_server_request(
6412 method="window/showStatus",
6413 params={
6414 "message": "Hack IDE: ready.\nhh_server: stopped.",
6415 "shortMessage": "Hack: ready",
6416 "actions": [{"title": "Restart hh_server"}],
6417 "type": 3,
6419 result=NoResponse(),
6421 .request(line=line(), method="shutdown", params={}, result=None)
6422 .notification(method="exit", params={})
6424 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6426 def test_serverless_ide_status_restart(self) -> None:
6427 variables = dict(self.prepare_serverless_ide_environment())
6428 variables.update(self.setup_php_file("hover.php"))
6430 spec = (
6431 self.initialize_spec(
6432 LspTestSpec("serverless_ide_status_restart"),
6433 use_serverless_ide=True,
6434 supports_status=True,
6436 .ignore_requests(
6437 comment="Ignore initializing messages since they're racy",
6438 method="window/showStatus",
6439 params={
6440 "type": 2,
6441 "actions": [],
6442 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
6443 "shortMessage": "Hack: initializing",
6446 .ignore_requests(
6447 comment="Another form of initializing to ignore",
6448 method="window/showStatus",
6449 params={
6450 "type": 2,
6451 "actions": [],
6452 "message": "Hack IDE: initializing.\nhh_server: ready.",
6453 "shortMessage": "Hack: initializing",
6456 .ignore_requests(
6457 comment="Another form of initializing to ignore before we've even heard the first peep from hh_server",
6458 method="window/showStatus",
6459 params={
6460 "type": 2,
6461 "actions": [],
6462 "message": "Hack IDE: initializing.",
6463 "shortMessage": "Hack: initializing",
6466 .ignore_requests(
6467 comment="another racy initialization to ignore, again before hh_server",
6468 method="window/showStatus",
6469 params={
6470 "type": 3,
6471 "actions": [],
6472 "message": "Hack IDE: ready.",
6473 "shortMessage": "Hack: ready",
6476 .wait_for_server_request(
6477 method="window/showStatus",
6478 params={
6479 "actions": [],
6480 "message": "Hack IDE: ready.\nhh_server: ready.",
6481 "shortMessage": "Hack: ready",
6482 "type": 3,
6484 result=NoResponse(),
6486 .request(
6487 line=line(),
6488 method="$test/shutdownServerlessIde",
6489 params={},
6490 result=None,
6491 powered_by="serverless_ide",
6493 .wait_for_server_request(
6494 method="window/showStatus",
6495 params={
6496 "actions": [{"title": "Restart Hack IDE"}],
6497 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6498 "shortMessage": "Hack: failed",
6499 "type": 1,
6501 result={"title": "Restart Hack IDE"},
6503 .wait_for_server_request(
6504 method="window/showStatus",
6505 params={
6506 "actions": [],
6507 "message": "Hack IDE: ready.\nhh_server: ready.",
6508 "shortMessage": "Hack: ready",
6509 "type": 3,
6511 result=NoResponse(),
6513 .request(line=line(), method="shutdown", params={}, result=None)
6514 .notification(method="exit", params={})
6516 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6518 def test_serverless_ide_failed_to_load_saved_state(self) -> None:
6519 variables = dict(self.prepare_serverless_ide_environment())
6520 variables.update(self.setup_php_file("hover.php"))
6521 assert "naming_table_saved_state_path" in variables
6522 variables["naming_table_saved_state_path"] = "/tmp/nonexistent"
6524 spec = (
6525 self.initialize_spec(
6526 LspTestSpec("serverless_ide_status_failed_to_load_saved_state"),
6527 use_serverless_ide=True,
6528 supports_status=True,
6529 supports_init=True,
6531 .ignore_requests(
6532 comment="Ignore initializing since they're kind of racy",
6533 method="window/showStatus",
6534 params={
6535 "type": 2,
6536 "actions": [],
6537 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
6538 "shortMessage": "Hack: initializing",
6541 .ignore_requests(
6542 comment="Ignore another form of initializing",
6543 method="window/showStatus",
6544 params={
6545 "type": 2,
6546 "actions": [],
6547 "message": "Hack IDE: initializing.\nhh_server: ready.",
6548 "shortMessage": "Hack: initializing",
6551 .ignore_requests(
6552 comment="Ignore another form of initializing, from before we've even heard the first peep out of hh_server",
6553 method="window/showStatus",
6554 params={
6555 "type": 2,
6556 "actions": [],
6557 "message": "Hack IDE: initializing.",
6558 "shortMessage": "Hack: initializing",
6561 .ignore_requests(
6562 comment="Ignore another form of initializing, again before hh_server",
6563 method="window/showStatus",
6564 params={
6565 "type": 1,
6566 "actions": [{"title": "Restart Hack IDE"}],
6567 "message": "Hack IDE has failed. See Output›Hack for details.",
6568 "shortMessage": "Hack: failed",
6571 .wait_for_notification(
6572 method="window/logMessage",
6573 params={
6574 "type": 1,
6575 "message": "Hack IDE has failed.\nThis is unexpected.\nPlease file a bug within your IDE.\nMore details: http://dummy/HH_TEST_MODE",
6578 .wait_for_server_request(
6579 method="window/showStatus",
6580 params={
6581 "actions": [{"title": "Restart Hack IDE"}],
6582 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6583 "shortMessage": "Hack: failed",
6584 "type": 1,
6586 result=NoResponse(),
6588 .request(line=line(), method="shutdown", params={}, result=None)
6589 .notification(method="exit", params={})
6591 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6593 def test_workspace_symbol(self) -> None:
6594 self.prepare_server_environment()
6595 variables = self.setup_php_file("didchange.php")
6596 spec = (
6597 self.initialize_spec(
6598 LspTestSpec("test_workspace_symbol"), use_serverless_ide=False
6600 .wait_for_hh_server_ready()
6601 .request(
6602 line=line(),
6603 comment="Look up symbols",
6604 method="workspace/symbol",
6605 params={"query": "TestNS\\test"},
6606 result=[
6608 "name": "TestNS\\test_func",
6609 "kind": 12,
6610 "location": {
6611 "uri": "file://${root_path}/completion_extras_namespace.php",
6612 "range": {
6613 "start": {"line": 4, "character": 9},
6614 "end": {"line": 4, "character": 25},
6620 .request(
6621 line=line(),
6622 comment="Look up symbols starting with 'test_f' within multiple namespaces",
6623 method="workspace/symbol",
6624 params={"query": "test_f"},
6625 result=[
6627 "name": "test_function",
6628 "kind": 12,
6629 "location": {
6630 "uri": "file://${root_path}/completion.php",
6631 "range": {
6632 "start": {"line": 7, "character": 9},
6633 "end": {"line": 7, "character": 22},
6638 "name": "TestNS\\test_func",
6639 "kind": 12,
6640 "location": {
6641 "uri": "file://${root_path}/completion_extras_namespace.php",
6642 "range": {
6643 "start": {"line": 4, "character": 9},
6644 "end": {"line": 4, "character": 25},
6650 .request(line=line(), method="shutdown", params={}, result=None)
6651 .notification(method="exit", params={})
6653 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
6655 def test_serverless_ide_during_hh_server_restart(self) -> None:
6656 variables = dict(self.prepare_serverless_ide_environment())
6657 variables.update(self.setup_php_file("didchange.php"))
6658 spec = (
6659 self.initialize_spec(
6660 LspTestSpec("test_serverless_ide_during_hh_server_restart"),
6661 use_serverless_ide=True,
6663 .notification(
6664 method="textDocument/didOpen",
6665 params={
6666 "textDocument": {
6667 "uri": "${php_file_uri}",
6668 "languageId": "hack",
6669 "version": 1,
6670 "text": "${php_file}",
6674 .notification(
6675 comment="Send a 'didChange' notification before HH Server is functional.",
6676 method="textDocument/didChange",
6677 params={
6678 "textDocument": {"uri": "${php_file_uri}"},
6679 "contentChanges": [
6681 "range": {
6682 "start": {"line": 7, "character": 9},
6683 "end": {"line": 7, "character": 11},
6685 "text": "'foo'",
6690 .start_hh_server("Start HH Server; should detect the bad edit")
6691 .wait_for_notification(
6692 method="textDocument/publishDiagnostics",
6693 params={
6694 "uri": "${php_file_uri}",
6695 "diagnostics": [
6697 "code": 4110,
6698 "message": "Invalid return type",
6699 "range": {
6700 "end": {"character": 14, "line": 7},
6701 "start": {"character": 9, "line": 7},
6703 "relatedInformation": [
6705 "location": {
6706 "range": {
6707 "end": {"character": 27, "line": 6},
6708 "start": {"character": 24, "line": 6},
6710 "uri": "${php_file_uri}",
6712 "message": "Expected int",
6715 "location": {
6716 "range": {
6717 "end": {"character": 14, "line": 7},
6718 "start": {"character": 9, "line": 7},
6720 "uri": "${php_file_uri}",
6722 "message": "But got string",
6725 "relatedLocations": [
6727 "location": {
6728 "range": {
6729 "end": {"character": 27, "line": 6},
6730 "start": {"character": 24, "line": 6},
6732 "uri": "${php_file_uri}",
6734 "message": "Expected int",
6737 "location": {
6738 "range": {
6739 "end": {"character": 14, "line": 7},
6740 "start": {"character": 9, "line": 7},
6742 "uri": "${php_file_uri}",
6744 "message": "But got string",
6747 "severity": 1,
6748 "source": "Hack",
6753 .stop_hh_server("Shutdown HH Server")
6754 .start_hh_server("Restart HH Server")
6755 .wait_for_notification(
6756 comment="On startup it thinks everything is okay ...",
6757 method="textDocument/publishDiagnostics",
6758 params={"uri": "${php_file_uri}", "diagnostics": []},
6760 .wait_for_notification(
6761 comment="But then hh_server sends a hello message and it gets the edited files, which leads it to see the problem.",
6762 method="textDocument/publishDiagnostics",
6763 params={
6764 "uri": "${php_file_uri}",
6765 "diagnostics": [
6767 "code": 4110,
6768 "message": "Invalid return type",
6769 "range": {
6770 "end": {"character": 14, "line": 7},
6771 "start": {"character": 9, "line": 7},
6773 "relatedInformation": [
6775 "location": {
6776 "range": {
6777 "end": {"character": 27, "line": 6},
6778 "start": {"character": 24, "line": 6},
6780 "uri": "${php_file_uri}",
6782 "message": "Expected int",
6785 "location": {
6786 "range": {
6787 "end": {"character": 14, "line": 7},
6788 "start": {"character": 9, "line": 7},
6790 "uri": "${php_file_uri}",
6792 "message": "But got string",
6795 "relatedLocations": [
6797 "location": {
6798 "range": {
6799 "end": {"character": 27, "line": 6},
6800 "start": {"character": 24, "line": 6},
6802 "uri": "${php_file_uri}",
6804 "message": "Expected int",
6807 "location": {
6808 "range": {
6809 "end": {"character": 14, "line": 7},
6810 "start": {"character": 9, "line": 7},
6812 "uri": "${php_file_uri}",
6814 "message": "But got string",
6817 "severity": 1,
6818 "source": "Hack",
6823 .request(line=line(), method="shutdown", params={}, result=None)
6824 .notification(method="exit", params={})
6826 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6828 def test_serverless_ide_naming_error1(self) -> None:
6829 variables = dict(self.prepare_serverless_ide_environment())
6830 variables.update(self.setup_php_file("didchange.php"))
6831 variables.update(
6833 "main_file": self.repo_file("main.php"),
6834 "main_file_contents": """\
6835 <?hh
6836 function main(): int {
6837 return aaa();
6839 """,
6840 "file_a": self.repo_file("a.php"),
6841 "file_b": self.repo_file("b.php"),
6844 spec = (
6845 self.initialize_spec(
6846 LspTestSpec("serverless_ide_naming_error1"), use_serverless_ide=True
6848 .write_to_disk(
6849 uri="${main_file}", contents="${main_file_contents}", notify=True
6851 .notification(
6852 method="textDocument/didOpen",
6853 params={
6854 "textDocument": {
6855 "uri": "${main_file}",
6856 "languageId": "hack",
6857 "version": 1,
6858 "text": "${main_file_contents}",
6862 .request(
6863 line=line(),
6864 comment="Ensure that hover over `aaa` works even when the name is not yet defined",
6865 method="textDocument/hover",
6866 params={
6867 "textDocument": {"uri": "${main_file}"},
6868 "position": {"line": 2, "character": 13},
6870 result={
6871 "contents": [{"language": "hack", "value": "_"}],
6872 "range": {
6873 "start": {"line": 2, "character": 11},
6874 "end": {"line": 2, "character": 14},
6877 powered_by="serverless_ide",
6879 .write_to_disk(
6880 comment="create file A",
6881 uri="${file_a}",
6882 contents="""\
6883 <?hh
6884 function aaa(): int {
6885 return 1;
6887 """,
6888 notify=True,
6890 .request(
6891 line=line(),
6892 comment="Ensure that hover over `aaa` works when there are no naming errors",
6893 method="textDocument/hover",
6894 params={
6895 "textDocument": {"uri": "${main_file}"},
6896 "position": {"line": 2, "character": 13},
6898 result={
6899 "contents": [
6900 {"language": "hack", "value": "function aaa(): int"},
6902 "range": {
6903 "start": {"line": 2, "character": 11},
6904 "end": {"line": 2, "character": 14},
6907 powered_by="serverless_ide",
6909 .write_to_disk(
6910 comment="create file B",
6911 uri="${file_b}",
6912 contents="""\
6913 <?hh
6914 function aaa(): string {
6915 return "foo";
6917 """,
6918 notify=True,
6920 .request(
6921 line=line(),
6922 comment="Ensure that hover over `aaa` works even when there is a duplicate name",
6923 method="textDocument/hover",
6924 params={
6925 "textDocument": {"uri": "${main_file}"},
6926 "position": {"line": 2, "character": 13},
6928 result={
6929 "contents": [
6930 {"language": "hack", "value": "function aaa(): int"},
6932 "range": {
6933 "start": {"line": 2, "character": 11},
6934 "end": {"line": 2, "character": 14},
6937 powered_by="serverless_ide",
6939 .write_to_disk(
6940 comment="delete file A", uri="${file_a}", contents=None, notify=True
6942 .request(
6943 line=line(),
6944 comment="Now that we've fixed the error, hover should work.",
6945 method="textDocument/hover",
6946 params={
6947 "textDocument": {"uri": "${main_file}"},
6948 "position": {"line": 2, "character": 13},
6950 result={
6951 "contents": [
6952 {"language": "hack", "value": "function aaa(): string"},
6954 "range": {
6955 "start": {"line": 2, "character": 11},
6956 "end": {"line": 2, "character": 14},
6959 powered_by="serverless_ide",
6961 .request(line=line(), method="shutdown", params={}, result=None)
6962 .notification(method="exit", params={})
6964 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6966 def test_serverless_ide_naming_error2(self) -> None:
6967 variables = dict(self.prepare_serverless_ide_environment())
6968 self.test_driver.stop_hh_server()
6969 variables.update(self.setup_php_file("naming_error_caller.php"))
6970 variables.update(
6972 "contents": self.read_repo_file("naming_error_declaration.php"),
6973 "original": self.repo_file("naming_error_declaration.php"),
6974 "copy": self.repo_file("naming_error_copy.php"),
6977 spec = (
6978 self.initialize_spec(
6979 LspTestSpec("serverless_ide_naming_error2"), use_serverless_ide=True
6981 .notification(
6982 method="textDocument/didOpen",
6983 params={
6984 "textDocument": {
6985 "uri": "${php_file_uri}",
6986 "languageId": "hack",
6987 "version": 1,
6988 "text": "${php_file}",
6992 .write_to_disk(
6993 comment="create copy",
6994 uri="${copy}",
6995 contents="${contents}",
6996 notify=True,
6998 .write_to_disk(
6999 comment="delete copy", uri="${copy}", contents=None, notify=True
7001 .request(
7002 line=line(),
7003 comment="hover should work fine after making copy then deleting copy.",
7004 method="textDocument/hover",
7005 params={
7006 "textDocument": {"uri": "${php_file_uri}"},
7007 "position": {"line": 3, "character": 15},
7009 result={
7010 "contents": [
7012 "language": "hack",
7013 "value": "function naming_error_declaration(): void",
7016 "range": {
7017 "start": {"line": 3, "character": 2},
7018 "end": {"line": 3, "character": 26},
7021 powered_by="serverless_ide",
7023 .request(line=line(), method="shutdown", params={}, result=None)
7024 .notification(method="exit", params={})
7026 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7028 def test_serverless_ide_naming_error3(self) -> None:
7029 variables = dict(self.prepare_serverless_ide_environment())
7030 self.test_driver.stop_hh_server()
7031 variables.update(self.setup_php_file("naming_error_caller.php"))
7032 variables.update(
7034 "contents": self.read_repo_file("naming_error_declaration.php"),
7035 "original": self.repo_file("naming_error_declaration.php"),
7036 "copy": self.repo_file("naming_error_copy.php"),
7039 spec = (
7040 self.initialize_spec(
7041 LspTestSpec("serverless_ide_naming_error3"), use_serverless_ide=True
7043 .notification(
7044 method="textDocument/didOpen",
7045 params={
7046 "textDocument": {
7047 "uri": "${php_file_uri}",
7048 "languageId": "hack",
7049 "version": 1,
7050 "text": "${php_file}",
7054 .write_to_disk(
7055 comment="create copy",
7056 uri="${copy}",
7057 contents="${contents}",
7058 notify=True,
7060 .write_to_disk(
7061 comment="delete original", uri="${original}", contents=None, notify=True
7063 .request(
7064 line=line(),
7065 comment="hover should work fine after making copy then deleting original.",
7066 method="textDocument/hover",
7067 params={
7068 "textDocument": {"uri": "${php_file_uri}"},
7069 "position": {"line": 3, "character": 15},
7071 result={
7072 "contents": [
7074 "language": "hack",
7075 "value": "function naming_error_declaration(): void",
7078 "range": {
7079 "start": {"line": 3, "character": 2},
7080 "end": {"line": 3, "character": 26},
7083 powered_by="serverless_ide",
7085 .request(line=line(), method="shutdown", params={}, result=None)
7086 .notification(method="exit", params={})
7088 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7090 def test_serverless_ide_requests_before_init(self) -> None:
7091 variables = dict(self.prepare_serverless_ide_environment())
7092 variables["root_path"] = self.test_driver.repo_dir
7093 self.test_driver.stop_hh_server()
7095 spec = (
7096 self.initialize_spec(
7097 LspTestSpec("test_serverless_ide_requests_before_init"),
7098 use_serverless_ide=True,
7099 supports_status=True,
7100 supports_init=True,
7102 .ignore_notifications(method="textDocument/publishDiagnostics")
7103 .ignore_requests(
7104 comment="Ignore 'initializing...' messages since they're racy",
7105 method="window/showStatus",
7106 params={
7107 "type": 2,
7108 "actions": [{"title": "Restart hh_server"}],
7109 "message": "Hack IDE: initializing.\nhh_server: stopped.",
7110 "shortMessage": "Hack: initializing",
7113 .ignore_requests(
7114 comment="another racy initialization, before we've yet heard from hh_server",
7115 method="window/showStatus",
7116 params={
7117 "type": 2,
7118 "actions": [],
7119 "message": "Hack IDE: initializing.",
7120 "shortMessage": "Hack: initializing",
7123 .ignore_requests(
7124 comment="another racy initialization, if HackIDE is done before hh_server has yet sent status",
7125 method="window/showStatus",
7126 params={
7127 "type": 3,
7128 "actions": [],
7129 "message": "Hack IDE: ready.",
7130 "shortMessage": "Hack: ready",
7133 .write_to_disk(
7134 notify=True,
7135 wait=False,
7136 uri="file://${root_path}/beforeInit1.php",
7137 contents="<?hh // strict\nfunction beforeInit1(): int {\n return 42;\n}\n",
7139 .notification(
7140 comment="open a file before init has finished",
7141 method="textDocument/didOpen",
7142 params={
7143 "textDocument": {
7144 "uri": "file://${root_path}/beforeInit2.php",
7145 "languageId": "hack",
7146 "version": 1,
7147 "text": "<?hh // strict\nfunction beforeInit2(): void {\n $foo = beforeInit1();\n}\n",
7151 .request(
7152 line=line(),
7153 comment="hover before init will fail",
7154 method="textDocument/hover",
7155 params={
7156 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
7157 "position": {"line": 2, "character": 4},
7159 result=None,
7161 .request(
7162 line=line(),
7163 comment="documentSymbol before init will succeed",
7164 method="textDocument/documentSymbol",
7165 params={"textDocument": {"uri": "file://${root_path}/beforeInit2.php"}},
7166 result=[
7168 "name": "beforeInit2",
7169 "kind": 12,
7170 "location": {
7171 "uri": "file://${root_path}/beforeInit2.php",
7172 "range": {
7173 "start": {"line": 1, "character": 0},
7174 "end": {"line": 3, "character": 1},
7179 powered_by="serverless_ide",
7181 .wait_for_notification(
7182 comment="wait for sIDE to init",
7183 method="telemetry/event",
7184 params={"type": 4, "message": "[client-ide] Finished init: ok"},
7186 .wait_for_server_request(
7187 method="window/showStatus",
7188 params={
7189 "actions": [{"title": "Restart hh_server"}],
7190 "message": "Hack IDE: ready.\nhh_server: stopped.",
7191 "shortMessage": "Hack: ready",
7192 "type": 3,
7194 result=NoResponse(),
7196 .request(
7197 line=line(),
7198 comment="hover after init will succeed",
7199 method="textDocument/hover",
7200 params={
7201 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
7202 "position": {"line": 2, "character": 4},
7204 result={
7205 "contents": [{"language": "hack", "value": "int"}],
7206 "range": {
7207 "start": {"line": 2, "character": 2},
7208 "end": {"line": 2, "character": 6},
7211 powered_by="serverless_ide",
7213 .request(line=line(), method="shutdown", params={}, result=None)
7214 .notification(method="exit", params={})
7217 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7219 def test_serverless_ide_workspace_symbol(self) -> None:
7220 variables = dict(self.prepare_serverless_ide_environment())
7221 variables["root_path"] = self.test_driver.repo_dir
7222 self.test_driver.stop_hh_server()
7224 spec = (
7225 self.initialize_spec(
7226 LspTestSpec("serverless_ide_workspace_symbol"), use_serverless_ide=True
7228 .request(
7229 line=line(),
7230 comment="workspace symbol call, global, powered by sqlite (generated during serverless-ide-init)",
7231 method="workspace/symbol",
7232 params={"query": "TakesString"},
7233 result=[
7235 "name": "TakesString",
7236 "kind": 5,
7237 "location": {
7238 "uri": "file://${root_path}/definition.php",
7239 "range": {
7240 "start": {"line": 36, "character": 6},
7241 "end": {"line": 36, "character": 17},
7246 powered_by="serverless_ide",
7248 .request(
7249 line=line(),
7250 comment="workspace symbol call, member (derived from naming-table)",
7251 method="workspace/symbol",
7252 params={"query": "TakesString::"},
7253 result=[
7255 "name": "__construct",
7256 "kind": 6,
7257 "location": {
7258 "uri": "file://${root_path}/definition.php",
7259 "range": {
7260 "start": {"line": 37, "character": 18},
7261 "end": {"line": 37, "character": 29},
7266 powered_by="serverless_ide",
7268 .request(line=line(), method="shutdown", params={}, result=None)
7269 .notification(method="exit", params={})
7271 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)