Reorder expected/actual in LSP assertions
[hiphop-php.git] / hphp / hack / test / integration / test_lsp.py
blob20e208c481c9c939769152789913b12d47810ea1
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 'call_via_label#'",
1872 method="textDocument/didChange",
1873 params={
1874 "textDocument": {"uri": "${php_file_uri}"},
1875 "contentChanges": [
1877 "range": {
1878 "start": {"line": 35, "character": 0},
1879 "end": {"line": 35, "character": 0},
1881 "text": " call_via_label#",
1886 .request(
1887 line=line(),
1888 comment="autocomplete results for 'call_via_label#'",
1889 method="textDocument/completion",
1890 params={
1891 "textDocument": {"uri": "${php_file_uri}"},
1892 "position": {"line": 35, "character": 17},
1894 result={
1895 "isIncomplete": False,
1896 "items": [
1898 "label": "First",
1899 "kind": 21,
1900 "detail": "HH\\MemberOf<MyEnumClass, MyEnumClassKind>",
1901 "inlineDetail": "HH\\MemberOf<MyEnumClass, MyEnumClassKind>",
1902 "sortText": "First",
1903 "insertText": "First",
1904 "insertTextFormat": 1,
1905 "data": {
1906 "fullname": "First",
1907 "filename": "${root_path}/definition.php",
1908 "line": 51,
1909 "char": 19,
1910 "base_class": "\\MyEnumClass",
1914 "label": "Second",
1915 "kind": 21,
1916 "detail": "HH\\MemberOf<MyEnumClass, MyEnumClassKind>",
1917 "inlineDetail": "HH\\MemberOf<MyEnumClass, MyEnumClassKind>",
1918 "sortText": "Second",
1919 "insertText": "Second",
1920 "insertTextFormat": 1,
1921 "data": {
1922 "fullname": "Second",
1923 "filename": "${root_path}/definition.php",
1924 "line": 52,
1925 "char": 19,
1926 "base_class": "\\MyEnumClass",
1931 powered_by="serverless_ide",
1933 .notification(
1934 comment="Add '<xhp:enum-attribute enum-attribute={}'",
1935 method="textDocument/didChange",
1936 params={
1937 "textDocument": {"uri": "${php_file_uri}"},
1938 "contentChanges": [
1940 "range": {
1941 "start": {"line": 3, "character": 0},
1942 "end": {"line": 3, "character": 17},
1944 "text": "<xhp:enum-attribute enum-attribute={}",
1949 .request(
1950 line=line(),
1951 comment="autocomplete after '<xhp:enum-attribute enum-attribute={'",
1952 method="textDocument/completion",
1953 params={
1954 "textDocument": {"uri": "${php_file_uri}"},
1955 "position": {"line": 3, "character": 36},
1956 "context": {"triggerKind": 2, "triggerCharacter": "{"},
1958 result={
1959 "isIncomplete": False,
1960 "items": [
1962 "label": "MyEnum::TYPE_C",
1963 "kind": 13,
1964 "detail": "enum",
1965 "inlineDetail": "enum",
1966 "sortText": "MyEnum::TYPE_C",
1967 "insertText": "MyEnum::TYPE_C",
1968 "insertTextFormat": 1,
1969 "data": {
1970 "fullname": "MyEnum::TYPE_C",
1971 "filename": "${root_path}/xhp_class_definitions.php",
1972 "line": 13,
1973 "char": 14,
1974 "base_class": "\\MyEnum",
1978 "label": "MyEnum::TYPE_A",
1979 "kind": 13,
1980 "detail": "enum",
1981 "inlineDetail": "enum",
1982 "sortText": "MyEnum::TYPE_A",
1983 "insertTextFormat": 1,
1984 "insertText": "MyEnum::TYPE_A",
1985 "data": {
1986 "fullname": "MyEnum::TYPE_A",
1987 "filename": "${root_path}/xhp_class_definitions.php",
1988 "line": 13,
1989 "char": 14,
1990 "base_class": "\\MyEnum",
1994 "label": "MyEnum::TYPE_B",
1995 "kind": 13,
1996 "detail": "enum",
1997 "inlineDetail": "enum",
1998 "sortText": "MyEnum::TYPE_B",
1999 "insertTextFormat": 1,
2000 "insertText": "MyEnum::TYPE_B",
2001 "data": {
2002 "fullname": "MyEnum::TYPE_B",
2003 "filename": "${root_path}/xhp_class_definitions.php",
2004 "line": 13,
2005 "char": 14,
2006 "base_class": "\\MyEnum",
2011 powered_by="serverless_ide",
2013 .notification(
2014 comment="Add '1 is strin'",
2015 method="textDocument/didChange",
2016 params={
2017 "textDocument": {"uri": "${php_file_uri}"},
2018 "contentChanges": [
2020 "range": {
2021 "start": {"line": 3, "character": 0},
2022 "end": {"line": 3, "character": 37},
2024 "text": "1 is strin",
2029 .request(
2030 line=line(),
2031 comment="autocomplete after '1 is strin'",
2032 method="textDocument/completion",
2033 params={
2034 "textDocument": {"uri": "${php_file_uri}"},
2035 "position": {"line": 3, "character": 10},
2037 result={
2038 "isIncomplete": False,
2039 "items": [
2041 "data": {"fullname": "string"},
2042 "detail": "builtin",
2043 "documentation": {
2044 "kind": "markdown",
2045 "value": "A sequence of zero or more characters. Strings are usually manipulated with functions from the `Str\\` namespace",
2047 "inlineDetail": "builtin",
2048 "insertText": "string",
2049 "insertTextFormat": 1,
2050 "kind": 25,
2051 "label": "string",
2052 "sortText": "string",
2055 "data": {"fullname": "StringBuffer"},
2056 "detail": "class",
2057 "inlineDetail": "class",
2058 "insertText": "StringBuffer",
2059 "insertTextFormat": 1,
2060 "kind": 7,
2061 "label": "StringBuffer",
2062 "sortText": "StringBuffer",
2065 "data": {"fullname": "Stringish"},
2066 "detail": "interface",
2067 "inlineDetail": "interface",
2068 "insertText": "Stringish",
2069 "insertTextFormat": 1,
2070 "kind": 8,
2071 "label": "Stringish",
2072 "sortText": "Stringish",
2075 "data": {"fullname": "StringishObject"},
2076 "detail": "interface",
2077 "inlineDetail": "interface",
2078 "insertText": "StringishObject",
2079 "insertTextFormat": 1,
2080 "kind": 8,
2081 "label": "StringishObject",
2082 "sortText": "StringishObject",
2086 powered_by="serverless_ide",
2088 .request(
2089 line=line(),
2090 comment="autocomplete resolving after '1 is strin'",
2091 method="completionItem/resolve",
2092 params={
2093 "data": {"fullname": "string"},
2094 "detail": "builtin",
2095 "documentation": {
2096 "kind": "markdown",
2097 "value": "A sequence of zero or more characters. Strings are usually manipulated with functions from the `Str\\` namespace",
2099 "inlineDetail": "builtin",
2100 "insertText": "string",
2101 "insertTextFormat": 1,
2102 "kind": 25,
2103 "label": "string",
2104 "sortText": "string",
2106 result={
2107 "data": {"fullname": "string"},
2108 "detail": "builtin",
2109 "documentation": {
2110 "kind": "markdown",
2111 "value": "A sequence of zero or more characters. Strings are usually manipulated with functions from the `Str\\` namespace",
2113 "inlineDetail": "builtin",
2114 "insertText": "string",
2115 "insertTextFormat": 1,
2116 "kind": 25,
2117 "label": "string",
2118 "sortText": "string",
2120 powered_by="serverless_ide",
2122 .request(line=line(), method="shutdown", params={}, result=None)
2123 .notification(method="exit", params={})
2125 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
2127 def test_serverless_ide_completion_legacy(self) -> None:
2128 variables = dict(self.prepare_serverless_ide_environment())
2129 variables.update(self.setup_php_file("completion.php"))
2130 self.test_driver.stop_hh_server()
2132 spec = (
2133 self.initialize_spec(
2134 LspTestSpec("serverless_ide_completion_legacy"), use_serverless_ide=True
2136 .notification(
2137 method="textDocument/didOpen",
2138 params={
2139 "textDocument": {
2140 "uri": "${php_file_uri}",
2141 "languageId": "hack",
2142 "version": 1,
2143 "text": "${php_file}",
2147 .notification(
2148 comment="Add '$x = <'",
2149 method="textDocument/didChange",
2150 params={
2151 "textDocument": {"uri": "${php_file_uri}"},
2152 "contentChanges": [
2154 "range": {
2155 "start": {"line": 3, "character": 0},
2156 "end": {"line": 3, "character": 0},
2158 "text": "$x = <",
2163 .request(
2164 line=line(),
2165 comment="autocomplete after '$x = <'",
2166 method="textDocument/completion",
2167 params={
2168 "textDocument": {"uri": "${php_file_uri}"},
2169 "position": {"line": 3, "character": 6},
2171 result={
2172 "isIncomplete": False,
2173 "items": [
2175 "label": "ab:cd:alpha",
2176 "kind": 7,
2177 "detail": "class",
2178 "inlineDetail": "class",
2179 "sortText": "ab:cd:alpha",
2180 "insertText": "ab:cd:alpha",
2181 "insertTextFormat": InsertTextFormat.PlainText.value,
2182 "data": {"fullname": ":ab:cd:alpha"},
2185 "label": "ab:cd:text",
2186 "kind": 7,
2187 "detail": "class",
2188 "inlineDetail": "class",
2189 "sortText": "ab:cd:text",
2190 "insertText": "ab:cd:text",
2191 "insertTextFormat": InsertTextFormat.PlainText.value,
2192 "data": {"fullname": ":ab:cd:text"},
2195 "label": "xhp:enum-attribute",
2196 "kind": 7,
2197 "detail": "class",
2198 "inlineDetail": "class",
2199 "sortText": "xhp:enum-attribute",
2200 "insertText": "xhp:enum-attribute",
2201 "insertTextFormat": InsertTextFormat.PlainText.value,
2202 "data": {"fullname": ":xhp:enum-attribute"},
2205 "label": "xhp:generic",
2206 "kind": 7,
2207 "detail": "class",
2208 "inlineDetail": "class",
2209 "sortText": "xhp:generic",
2210 "insertText": "xhp:generic",
2211 "insertTextFormat": InsertTextFormat.PlainText.value,
2212 "data": {"fullname": ":xhp:generic"},
2216 powered_by="serverless_ide",
2218 .notification(
2219 comment="Add '$x = <a'",
2220 method="textDocument/didChange",
2221 params={
2222 "textDocument": {"uri": "${php_file_uri}"},
2223 "contentChanges": [
2225 "range": {
2226 "start": {"line": 3, "character": 0},
2227 "end": {"line": 3, "character": 6},
2229 "text": "$x = <a",
2234 .request(
2235 line=line(),
2236 comment="autocomplete after '$x = <a'",
2237 method="textDocument/completion",
2238 params={
2239 "textDocument": {"uri": "${php_file_uri}"},
2240 "position": {"line": 3, "character": 7},
2242 result={
2243 "isIncomplete": False,
2244 "items": [
2246 "label": "ab:cd:alpha",
2247 "kind": 7,
2248 "detail": "class",
2249 "inlineDetail": "class",
2250 "sortText": "ab:cd:alpha",
2251 "insertText": "ab:cd:alpha",
2252 "insertTextFormat": InsertTextFormat.PlainText.value,
2253 "data": {"fullname": ":ab:cd:alpha"},
2256 "label": "ab:cd:text",
2257 "kind": 7,
2258 "detail": "class",
2259 "inlineDetail": "class",
2260 "sortText": "ab:cd:text",
2261 "insertText": "ab:cd:text",
2262 "insertTextFormat": InsertTextFormat.PlainText.value,
2263 "data": {"fullname": ":ab:cd:text"},
2267 powered_by="serverless_ide",
2269 .notification(
2270 comment="Add '$x = <ab:'",
2271 method="textDocument/didChange",
2272 params={
2273 "textDocument": {"uri": "${php_file_uri}"},
2274 "contentChanges": [
2276 "range": {
2277 "start": {"line": 3, "character": 0},
2278 "end": {"line": 3, "character": 7},
2280 "text": "$x = <ab:",
2285 .request(
2286 line=line(),
2287 comment="autocomplete after '$x = <ab:'.",
2288 method="textDocument/completion",
2289 params={
2290 "textDocument": {"uri": "${php_file_uri}"},
2291 "position": {"line": 3, "character": 9},
2293 result={
2294 "isIncomplete": False,
2295 "items": [
2297 "label": "ab:cd:alpha",
2298 "kind": 7,
2299 "detail": "class",
2300 "inlineDetail": "class",
2301 "sortText": "ab:cd:alpha",
2302 "insertText": "ab:cd:alpha",
2303 "insertTextFormat": InsertTextFormat.PlainText.value,
2304 "data": {"fullname": ":ab:cd:alpha"},
2307 "label": "ab:cd:text",
2308 "kind": 7,
2309 "detail": "class",
2310 "inlineDetail": "class",
2311 "sortText": "ab:cd:text",
2312 "insertText": "ab:cd:text",
2313 "insertTextFormat": InsertTextFormat.PlainText.value,
2314 "data": {"fullname": ":ab:cd:text"},
2318 powered_by="serverless_ide",
2320 .notification(
2321 comment="Add '$x = <ab:cd:text '",
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": 9},
2331 "text": "$x = <ab:cd:text ",
2336 .request(
2337 line=line(),
2338 comment="autocomplete after '$x = <ab:cd:text '",
2339 method="textDocument/completion",
2340 params={
2341 "textDocument": {"uri": "${php_file_uri}"},
2342 "position": {"line": 3, "character": 17},
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 = <ab:cd:text w'",
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": 17},
2394 "text": "$x = <ab:cd:text w",
2399 .request(
2400 line=line(),
2401 comment="autocomplete after '$x = <ab:cd:text w'",
2402 method="textDocument/completion",
2403 params={
2404 "textDocument": {"uri": "${php_file_uri}"},
2405 "position": {"line": 3, "character": 18},
2407 result={
2408 "isIncomplete": False,
2409 "items": [
2411 "label": "width",
2412 "kind": 10,
2413 "detail": "?int",
2414 "inlineDetail": "?int",
2415 "sortText": "width",
2416 "insertText": "width",
2417 "insertTextFormat": InsertTextFormat.PlainText.value,
2418 "data": {
2419 "fullname": ":width",
2420 "filename": "${root_path}/xhp_class_definitions.php",
2421 "line": 5,
2422 "char": 27,
2423 "base_class": "\\:ab:cd:text",
2427 "label": "color",
2428 "kind": 10,
2429 "detail": "?string",
2430 "inlineDetail": "?string",
2431 "sortText": "color",
2432 "insertText": "color",
2433 "insertTextFormat": InsertTextFormat.PlainText.value,
2434 "data": {
2435 "fullname": ":color",
2436 "filename": "${root_path}/xhp_class_definitions.php",
2437 "line": 5,
2438 "char": 13,
2439 "base_class": "\\:ab:cd:text",
2444 powered_by="serverless_ide",
2446 .notification(
2447 comment="Add '$x = new :''",
2448 method="textDocument/didChange",
2449 params={
2450 "textDocument": {"uri": "${php_file_uri}"},
2451 "contentChanges": [
2453 "range": {
2454 "start": {"line": 3, "character": 0},
2455 "end": {"line": 3, "character": 18},
2457 "text": "$x = new :",
2462 .request(
2463 line=line(),
2464 comment="autocomplete after '$x = new :'",
2465 method="textDocument/completion",
2466 params={
2467 "textDocument": {"uri": "${php_file_uri}"},
2468 "position": {"line": 3, "character": 10},
2470 result={
2471 "isIncomplete": False,
2472 "items": [
2474 "label": ":ab:cd:alpha",
2475 "kind": 7,
2476 "detail": "class",
2477 "inlineDetail": "class",
2478 "sortText": ":ab:cd:alpha",
2479 "insertText": ":ab:cd:alpha",
2480 "insertTextFormat": InsertTextFormat.PlainText.value,
2481 "data": {"fullname": ":ab:cd:alpha"},
2484 "label": ":ab:cd:text",
2485 "kind": 7,
2486 "detail": "class",
2487 "inlineDetail": "class",
2488 "sortText": ":ab:cd:text",
2489 "insertText": ":ab:cd:text",
2490 "insertTextFormat": InsertTextFormat.PlainText.value,
2491 "data": {"fullname": ":ab:cd:text"},
2494 "label": ":xhp:enum-attribute",
2495 "kind": 7,
2496 "detail": "class",
2497 "inlineDetail": "class",
2498 "sortText": ":xhp:enum-attribute",
2499 "insertText": ":xhp:enum-attribute",
2500 "insertTextFormat": InsertTextFormat.PlainText.value,
2501 "data": {"fullname": ":xhp:enum-attribute"},
2504 "label": ":xhp:generic",
2505 "kind": 7,
2506 "detail": "class",
2507 "inlineDetail": "class",
2508 "sortText": ":xhp:generic",
2509 "insertText": ":xhp:generic",
2510 "insertTextFormat": InsertTextFormat.PlainText.value,
2511 "data": {"fullname": ":xhp:generic"},
2515 powered_by="serverless_ide",
2517 .notification(
2518 comment="Add '$x = new :a'",
2519 method="textDocument/didChange",
2520 params={
2521 "textDocument": {"uri": "${php_file_uri}"},
2522 "contentChanges": [
2524 "range": {
2525 "start": {"line": 3, "character": 0},
2526 "end": {"line": 3, "character": 10},
2528 "text": "$x = new :a",
2533 .request(
2534 line=line(),
2535 comment="autocomplete after '$x = new :a'",
2536 method="textDocument/completion",
2537 params={
2538 "textDocument": {"uri": "${php_file_uri}"},
2539 "position": {"line": 3, "character": 11},
2541 result={
2542 "isIncomplete": False,
2543 "items": [
2545 "label": ":ab:cd:alpha",
2546 "kind": 7,
2547 "detail": "class",
2548 "inlineDetail": "class",
2549 "sortText": ":ab:cd:alpha",
2550 "insertText": ":ab:cd:alpha",
2551 "insertTextFormat": InsertTextFormat.PlainText.value,
2552 "data": {"fullname": ":ab:cd:alpha"},
2555 "label": ":ab:cd:text",
2556 "kind": 7,
2557 "detail": "class",
2558 "inlineDetail": "class",
2559 "sortText": ":ab:cd:text",
2560 "insertText": ":ab:cd:text",
2561 "insertTextFormat": InsertTextFormat.PlainText.value,
2562 "data": {"fullname": ":ab:cd:text"},
2566 powered_by="serverless_ide",
2568 # Note that this request sent should match the result given in the previous example
2569 .request(
2570 line=line(),
2571 comment="autocomplete resolving after '$x = new :a'",
2572 method="completionItem/resolve",
2573 params={
2574 "label": ":ab:cd:alpha",
2575 "kind": 7,
2576 "detail": "class",
2577 "inlineDetail": "class",
2578 "itemType": ":ab:cd:alpha",
2579 "insertText": ":ab:cd:alpha",
2580 "insertTextFormat": InsertTextFormat.PlainText.value,
2581 "data": {"fullname": ":ab:cd:alpha"},
2583 result={
2584 "label": ":ab:cd:alpha",
2585 "kind": 7,
2586 "detail": "class",
2587 "inlineDetail": "class",
2588 "itemType": ":ab:cd:alpha",
2589 "documentation": {
2590 "kind": "markdown",
2591 "value": ":ab:cd:alpha docblock",
2593 "insertText": ":ab:cd:alpha",
2594 "insertTextFormat": InsertTextFormat.PlainText.value,
2595 "data": {"fullname": ":ab:cd:alpha"},
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": 11},
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": 29},
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 '$x = <ab:cd:text/>; $y = $x->:'",
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": 29},
2673 "text": "$x = <ab:cd:text/>; $y = $x->:",
2678 .request(
2679 line=line(),
2680 comment="autocomplete after '$x = <ab:cd:text/>; $y = $x->:'",
2681 method="textDocument/completion",
2682 params={
2683 "textDocument": {"uri": "${php_file_uri}"},
2684 "position": {"line": 3, "character": 30},
2686 result={
2687 "isIncomplete": False,
2688 "items": [
2690 "label": ":width",
2691 "kind": 10,
2692 "detail": "?int",
2693 "inlineDetail": "?int",
2694 "sortText": ":width",
2695 "insertText": ":width",
2696 "insertTextFormat": InsertTextFormat.PlainText.value,
2697 "data": {
2698 "fullname": ":width",
2699 "filename": "${root_path}/xhp_class_definitions.php",
2700 "line": 5,
2701 "char": 27,
2702 "base_class": "\\:ab:cd:text",
2706 "label": ":color",
2707 "kind": 10,
2708 "detail": "?string",
2709 "inlineDetail": "?string",
2710 "sortText": ":color",
2711 "insertText": ":color",
2712 "insertTextFormat": InsertTextFormat.PlainText.value,
2713 "data": {
2714 "fullname": ":color",
2715 "filename": "${root_path}/xhp_class_definitions.php",
2716 "line": 5,
2717 "char": 13,
2718 "base_class": "\\:ab:cd:text",
2723 powered_by="serverless_ide",
2725 .notification(
2726 comment="Add 'test_fun'",
2727 method="textDocument/didChange",
2728 params={
2729 "textDocument": {"uri": "${php_file_uri}"},
2730 "contentChanges": [
2732 "range": {
2733 "start": {"line": 3, "character": 0},
2734 "end": {"line": 3, "character": 30},
2736 "text": "test_fun",
2741 .request(
2742 line=line(),
2743 comment="autocomplete after 'test_fun'",
2744 method="textDocument/completion",
2745 params={
2746 "textDocument": {"uri": "${php_file_uri}"},
2747 "position": {"line": 3, "character": 8},
2749 result={
2750 "isIncomplete": False,
2751 "items": [
2753 "label": "test_function",
2754 "kind": 3,
2755 "detail": "function",
2756 "inlineDetail": "function",
2757 "sortText": "test_function",
2758 "insertText": "test_function",
2759 "insertTextFormat": InsertTextFormat.PlainText.value,
2760 "data": {"fullname": "test_function"},
2764 powered_by="serverless_ide",
2766 .request(
2767 line=line(),
2768 comment="autocomplete resolving after 'test_fun'",
2769 method="completionItem/resolve",
2770 params={
2771 "label": "test_function",
2772 "kind": 3,
2773 "detail": "function(): void",
2774 "inlineDetail": "()",
2775 "itemType": "void",
2776 "insertText": "test_function",
2777 "insertTextFormat": InsertTextFormat.PlainText.value,
2778 "data": {
2779 "filename": "${root_path}/completion.php",
2780 "line": 8,
2781 "char": 10,
2784 result={
2785 "label": "test_function",
2786 "kind": 3,
2787 "detail": "function(): void",
2788 "inlineDetail": "()",
2789 "itemType": "void",
2790 "documentation": {
2791 "kind": "markdown",
2792 "value": "test_function docblock.",
2794 "insertText": "test_function",
2795 "insertTextFormat": InsertTextFormat.PlainText.value,
2796 "data": {
2797 "filename": "${root_path}/completion.php",
2798 "line": 8,
2799 "char": 10,
2802 powered_by="serverless_ide",
2804 .notification(
2805 comment="Add 'switch (Elsa::Alonso) { case Elsa:'",
2806 method="textDocument/didChange",
2807 params={
2808 "textDocument": {"uri": "${php_file_uri}"},
2809 "contentChanges": [
2811 "range": {
2812 "start": {"line": 3, "character": 0},
2813 "end": {"line": 3, "character": 8},
2815 "text": "switch (Elsa::Alonso) { case Elsa:",
2820 .request(
2821 line=line(),
2822 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa:'",
2823 method="textDocument/completion",
2824 params={
2825 "textDocument": {"uri": "${php_file_uri}"},
2826 "position": {"line": 3, "character": 34},
2828 result={"isIncomplete": False, "items": []},
2829 powered_by="serverless_ide",
2831 .notification(
2832 comment="Add 'switch (Elsa::Alonso) { case Elsa::'",
2833 method="textDocument/didChange",
2834 params={
2835 "textDocument": {"uri": "${php_file_uri}"},
2836 "contentChanges": [
2838 "range": {
2839 "start": {"line": 3, "character": 0},
2840 "end": {"line": 3, "character": 34},
2842 "text": "switch (Elsa::Alonso) { case Elsa::",
2847 .request(
2848 line=line(),
2849 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::'",
2850 method="textDocument/completion",
2851 params={
2852 "textDocument": {"uri": "${php_file_uri}"},
2853 "position": {"line": 3, "character": 35},
2855 result={
2856 "isIncomplete": False,
2857 "items": [
2859 "label": "class",
2860 "kind": 21,
2861 "detail": "classname<this>",
2862 "inlineDetail": "classname<this>",
2863 "sortText": "class",
2864 "insertText": "class",
2865 "insertTextFormat": InsertTextFormat.PlainText.value,
2866 "data": {
2867 "fullname": "class",
2868 "filename": "${root_path}/completion_extras.php",
2869 "line": 3,
2870 "char": 6,
2871 "base_class": "\\Elsa",
2875 "label": "Bard",
2876 "kind": 21,
2877 "detail": "Elsa",
2878 "inlineDetail": "Elsa",
2879 "sortText": "Bard",
2880 "insertText": "Bard",
2881 "insertTextFormat": InsertTextFormat.PlainText.value,
2882 "data": {
2883 "fullname": "Bard",
2884 "filename": "${root_path}/completion_extras.php",
2885 "line": 3,
2886 "char": 12,
2887 "base_class": "\\Elsa",
2891 "label": "Alonso",
2892 "kind": 21,
2893 "detail": "Elsa",
2894 "inlineDetail": "Elsa",
2895 "sortText": "Alonso",
2896 "insertText": "Alonso",
2897 "insertTextFormat": InsertTextFormat.PlainText.value,
2898 "data": {
2899 "fullname": "Alonso",
2900 "filename": "${root_path}/completion_extras.php",
2901 "line": 3,
2902 "char": 12,
2903 "base_class": "\\Elsa",
2907 "label": "isValid",
2908 "kind": 2,
2909 "detail": "function(mixed $value): bool",
2910 "inlineDetail": "(mixed $value)",
2911 "itemType": "bool",
2912 "sortText": "isValid",
2913 "insertText": "isValid(${1:\\$value})",
2914 "insertTextFormat": InsertTextFormat.Snippet.value,
2915 "data": {
2916 "fullname": "isValid",
2917 "filename": "${hhi_path}/BuiltinEnum.hhi",
2918 "line": 46,
2919 "char": 32,
2920 "base_class": "\\Elsa",
2924 "label": "getValues",
2925 "kind": 2,
2926 "detail": "function(): dict<string, Elsa>",
2927 "inlineDetail": "()",
2928 "itemType": "dict<string, Elsa>",
2929 "sortText": "getValues",
2930 "insertText": "getValues()",
2931 "insertTextFormat": InsertTextFormat.Snippet.value,
2932 "data": {
2933 "fullname": "getValues",
2934 "filename": "${hhi_path}/BuiltinEnum.hhi",
2935 "line": 33,
2936 "char": 32,
2937 "base_class": "\\Elsa",
2941 "label": "getNames",
2942 "kind": 2,
2943 "detail": "function(): dict<Elsa, string>",
2944 "inlineDetail": "()",
2945 "itemType": "dict<Elsa, string>",
2946 "sortText": "getNames",
2947 "insertText": "getNames()",
2948 "insertTextFormat": InsertTextFormat.Snippet.value,
2949 "data": {
2950 "fullname": "getNames",
2951 "filename": "${hhi_path}/BuiltinEnum.hhi",
2952 "line": 41,
2953 "char": 32,
2954 "base_class": "\\Elsa",
2958 "label": "coerce",
2959 "kind": 2,
2960 "detail": "function(mixed $value): ?Elsa",
2961 "inlineDetail": "(mixed $value)",
2962 "itemType": "?Elsa",
2963 "sortText": "coerce",
2964 "insertText": "coerce(${1:\\$value})",
2965 "insertTextFormat": InsertTextFormat.Snippet.value,
2966 "data": {
2967 "fullname": "coerce",
2968 "filename": "${hhi_path}/BuiltinEnum.hhi",
2969 "line": 52,
2970 "char": 32,
2971 "base_class": "\\Elsa",
2975 "label": "assertAll",
2976 "kind": 2,
2977 "detail": "function(Traversable<mixed> $values): Container<Elsa>",
2978 "inlineDetail": "(Traversable<mixed> $values)",
2979 "itemType": "Container<Elsa>",
2980 "sortText": "assertAll",
2981 "insertText": "assertAll(${1:\\$values})",
2982 "insertTextFormat": InsertTextFormat.Snippet.value,
2983 "data": {
2984 "fullname": "assertAll",
2985 "filename": "${hhi_path}/BuiltinEnum.hhi",
2986 "line": 64,
2987 "char": 32,
2988 "base_class": "\\Elsa",
2992 "label": "assert",
2993 "kind": 2,
2994 "detail": "function(mixed $value): Elsa",
2995 "inlineDetail": "(mixed $value)",
2996 "itemType": "Elsa",
2997 "sortText": "assert",
2998 "insertText": "assert(${1:\\$value})",
2999 "insertTextFormat": InsertTextFormat.Snippet.value,
3000 "data": {
3001 "fullname": "assert",
3002 "filename": "${hhi_path}/BuiltinEnum.hhi",
3003 "line": 58,
3004 "char": 32,
3005 "base_class": "\\Elsa",
3010 powered_by="serverless_ide",
3012 .notification(
3013 comment="Add 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
3014 method="textDocument/didChange",
3015 params={
3016 "textDocument": {"uri": "${php_file_uri}"},
3017 "contentChanges": [
3019 "range": {
3020 "start": {"line": 3, "character": 0},
3021 "end": {"line": 3, "character": 35},
3023 "text": "switch (Elsa::Alonso) { case Elsa::Alonso:",
3028 .request(
3029 line=line(),
3030 comment="autocomplete after 'switch (Elsa::Alonso) { case Elsa::Alonso:'",
3031 method="textDocument/completion",
3032 params={
3033 "textDocument": {"uri": "${php_file_uri}"},
3034 "position": {"line": 3, "character": 42},
3036 result={"isIncomplete": False, "items": []},
3037 powered_by="serverless_ide",
3039 .notification(
3040 comment="Add 'DeprecatedClass::'",
3041 method="textDocument/didChange",
3042 params={
3043 "textDocument": {"uri": "${php_file_uri}"},
3044 "contentChanges": [
3046 "range": {
3047 "start": {"line": 3, "character": 0},
3048 "end": {"line": 3, "character": 41},
3050 "text": "DeprecatedClass::",
3055 .request(
3056 line=line(),
3057 comment="autocomplete after 'DeprecatedClass::'",
3058 method="textDocument/completion",
3059 params={
3060 "textDocument": {"uri": "${php_file_uri}"},
3061 "position": {"line": 3, "character": 17},
3063 result={
3064 "isIncomplete": False,
3065 "items": [
3067 "label": "class",
3068 "kind": 21,
3069 "detail": "classname<this>",
3070 "inlineDetail": "classname<this>",
3071 "sortText": "class",
3072 "insertText": "class",
3073 "insertTextFormat": InsertTextFormat.PlainText.value,
3074 "data": {
3075 "fullname": "class",
3076 "filename": "${root_path}/completion_extras.php",
3077 "line": 8,
3078 "char": 13,
3079 "base_class": "\\DeprecatedClass",
3083 "label": "test_do_not_use",
3084 "kind": 2,
3085 "detail": "function(): void",
3086 "inlineDetail": "()",
3087 "itemType": "void",
3088 "sortText": "~test_do_not_use",
3089 "insertText": "test_do_not_use()",
3090 "insertTextFormat": InsertTextFormat.Snippet.value,
3091 "data": {
3092 "fullname": "test_do_not_use",
3093 "filename": "${root_path}/completion_extras.php",
3094 "line": 12,
3095 "char": 26,
3096 "base_class": "\\DeprecatedClass",
3100 "label": "getName",
3101 "kind": 2,
3102 "detail": "function(): void",
3103 "inlineDetail": "()",
3104 "itemType": "void",
3105 "sortText": "getName",
3106 "insertText": "getName()",
3107 "insertTextFormat": InsertTextFormat.Snippet.value,
3108 "data": {
3109 "fullname": "getName",
3110 "filename": "${root_path}/completion_extras.php",
3111 "line": 9,
3112 "char": 26,
3113 "base_class": "\\DeprecatedClass",
3117 "label": "getAttributes_DO_NOT_USE",
3118 "kind": 2,
3119 "detail": "function(): void",
3120 "inlineDetail": "()",
3121 "itemType": "void",
3122 "sortText": "~getAttributes_DO_NOT_USE",
3123 "insertText": "getAttributes_DO_NOT_USE()",
3124 "insertTextFormat": InsertTextFormat.Snippet.value,
3125 "data": {
3126 "fullname": "getAttributes_DO_NOT_USE",
3127 "filename": "${root_path}/completion_extras.php",
3128 "line": 11,
3129 "char": 26,
3130 "base_class": "\\DeprecatedClass",
3134 "label": "__getLoader",
3135 "kind": 2,
3136 "detail": "function(): void",
3137 "inlineDetail": "()",
3138 "itemType": "void",
3139 "sortText": "~__getLoader",
3140 "insertText": "__getLoader()",
3141 "insertTextFormat": InsertTextFormat.Snippet.value,
3142 "data": {
3143 "fullname": "__getLoader",
3144 "filename": "${root_path}/completion_extras.php",
3145 "line": 10,
3146 "char": 26,
3147 "base_class": "\\DeprecatedClass",
3152 powered_by="serverless_ide",
3154 .request(line=line(), method="shutdown", params={}, result=None)
3155 .notification(method="exit", params={})
3157 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3159 def test_serverless_ide_definition(self) -> None:
3160 variables = dict(self.prepare_serverless_ide_environment())
3161 variables.update(self.setup_php_file("definition.php"))
3162 self.test_driver.stop_hh_server()
3164 spec = (
3165 self.initialize_spec(
3166 LspTestSpec("serverless_ide_definition"), use_serverless_ide=True
3168 .notification(
3169 method="textDocument/didOpen",
3170 params={
3171 "textDocument": {
3172 "uri": "${php_file_uri}",
3173 "languageId": "hack",
3174 "version": 1,
3175 "text": "${php_file}",
3179 .request(
3180 line=line(),
3181 comment="call to `b_definition`",
3182 method="textDocument/definition",
3183 params={
3184 "textDocument": {"uri": "${php_file_uri}"},
3185 "position": {"line": 3, "character": 10},
3187 result=[
3189 "uri": "file://${root_path}/definition.php",
3190 "range": {
3191 "start": {"line": 6, "character": 9},
3192 "end": {"line": 6, "character": 21},
3194 "title": "b_definition",
3197 powered_by="serverless_ide",
3199 .request(
3200 line=line(),
3201 comment="call to `new BB(1)`",
3202 method="textDocument/definition",
3203 params={
3204 "textDocument": {"uri": "${php_file_uri}"},
3205 "position": {"line": 29, "character": 13},
3207 result=[
3209 "uri": "file://${root_path}/definition.php",
3210 "range": {
3211 "start": {"line": 11, "character": 18},
3212 "end": {"line": 11, "character": 29},
3214 "title": "BB::__construct",
3217 powered_by="serverless_ide",
3219 .request(
3220 line=line(),
3221 comment="call to `new CC(1)`",
3222 method="textDocument/definition",
3223 params={
3224 "textDocument": {"uri": "${php_file_uri}"},
3225 "position": {"line": 30, "character": 13},
3227 result=[
3229 "uri": "file://${root_path}/definition.php",
3230 "range": {
3231 "start": {"line": 14, "character": 6},
3232 "end": {"line": 14, "character": 8},
3234 "title": "CC",
3237 "uri": "file://${root_path}/definition.php",
3238 "range": {
3239 "start": {"line": 11, "character": 18},
3240 "end": {"line": 11, "character": 29},
3242 "title": "BB::__construct",
3245 powered_by="serverless_ide",
3247 .request(
3248 line=line(),
3249 comment="call to `new DD(1)`",
3250 method="textDocument/definition",
3251 params={
3252 "textDocument": {"uri": "${php_file_uri}"},
3253 "position": {"line": 31, "character": 13},
3255 result=[
3257 "uri": "file://${root_path}/definition.php",
3258 "range": {
3259 "start": {"line": 17, "character": 6},
3260 "end": {"line": 17, "character": 8},
3262 "title": "DD",
3265 "uri": "file://${root_path}/definition.php",
3266 "range": {
3267 "start": {"line": 11, "character": 18},
3268 "end": {"line": 11, "character": 29},
3270 "title": "BB::__construct",
3273 powered_by="serverless_ide",
3275 .request(
3276 line=line(),
3277 comment="call to `new EE(1)`",
3278 method="textDocument/definition",
3279 params={
3280 "textDocument": {"uri": "${php_file_uri}"},
3281 "position": {"line": 32, "character": 13},
3283 result=[
3285 "uri": "file://${root_path}/definition.php",
3286 "range": {
3287 "start": {"line": 21, "character": 18},
3288 "end": {"line": 21, "character": 29},
3290 "title": "EE::__construct",
3293 powered_by="serverless_ide",
3295 .request(
3296 line=line(),
3297 comment="call to `new FF(1)`",
3298 method="textDocument/definition",
3299 params={
3300 "textDocument": {"uri": "${php_file_uri}"},
3301 "position": {"line": 33, "character": 13},
3303 result=[
3305 "uri": "file://${root_path}/definition.php",
3306 "range": {
3307 "start": {"line": 26, "character": 6},
3308 "end": {"line": 26, "character": 8},
3310 "title": "FF",
3313 powered_by="serverless_ide",
3315 .request(
3316 line=line(),
3317 comment="call to `new TakesString(HasString::MyString)`",
3318 method="textDocument/definition",
3319 params={
3320 "textDocument": {"uri": "${php_file_uri}"},
3321 "position": {"line": 45, "character": 23},
3323 result=[
3325 "uri": "file://${root_path}/definition.php",
3326 "range": {
3327 "start": {"line": 40, "character": 6},
3328 "end": {"line": 40, "character": 15},
3330 "title": "HasString",
3333 powered_by="serverless_ide",
3335 .notification(
3336 comment="make local, unsaved change to the file",
3337 method="textDocument/didChange",
3338 params={
3339 "textDocument": {"uri": "${php_file_uri}", "version": 2},
3340 "contentChanges": [
3342 "text": "test",
3343 "range": {
3344 "start": {"line": 3, "character": 9},
3345 "end": {"line": 3, "character": 21},
3351 .request(
3352 line=line(),
3353 comment="call to `test` instead of `b_definition`",
3354 method="textDocument/definition",
3355 params={
3356 "textDocument": {"uri": "${php_file_uri}"},
3357 "position": {"line": 3, "character": 10},
3359 result=[
3361 "uri": "file://${root_path}/definition.php",
3362 "range": {
3363 "start": {"line": 28, "character": 9},
3364 "end": {"line": 28, "character": 13},
3366 "title": "test",
3369 powered_by="serverless_ide",
3371 .request(line=line(), method="shutdown", params={}, result=None)
3372 .notification(method="exit", params={})
3374 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3376 def test_serverless_ide_overridden_definition(self) -> None:
3377 variables = dict(self.prepare_serverless_ide_environment())
3378 variables.update(self.setup_php_file("override.php"))
3379 self.test_driver.stop_hh_server()
3381 spec = (
3382 self.initialize_spec(
3383 LspTestSpec("serverless_ide_overridden_definition"),
3384 use_serverless_ide=True,
3386 .notification(
3387 method="textDocument/didOpen",
3388 params={
3389 "textDocument": {
3390 "uri": "${php_file_uri}",
3391 "languageId": "hack",
3392 "version": 1,
3393 "text": "${php_file}",
3397 .request(
3398 line=line(),
3399 comment="find overridden method from trait. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3400 method="textDocument/definition",
3401 params={
3402 "textDocument": {"uri": "${php_file_uri}"},
3403 "position": {"line": 13, "character": 5},
3405 result=[
3407 "uri": "file://${root_path}/override.php",
3408 "range": {
3409 "start": {"line": 7, "character": 18},
3410 "end": {"line": 7, "character": 21},
3412 "title": "MyTrait::foo",
3415 powered_by="serverless_ide",
3417 .request(
3418 line=line(),
3419 comment="find overridden static method. It's arbitrary which one we pick. This test embodies current (alphabetical) implementation.",
3420 method="textDocument/definition",
3421 params={
3422 "textDocument": {"uri": "${php_file_uri}"},
3423 "position": {"line": 26, "character": 5},
3425 result=[
3427 "uri": "file://${root_path}/override.php",
3428 "range": {
3429 "start": {"line": 23, "character": 25},
3430 "end": {"line": 23, "character": 28},
3432 "title": "C2::bar",
3435 powered_by="serverless_ide",
3437 .request(
3438 line=line(),
3439 comment="find overridden interface method",
3440 method="textDocument/definition",
3441 params={
3442 "textDocument": {"uri": "${php_file_uri}"},
3443 "position": {"line": 35, "character": 5},
3445 result=[
3447 "uri": "file://${root_path}/override.php",
3448 "range": {
3449 "start": {"line": 32, "character": 18},
3450 "end": {"line": 32, "character": 22},
3452 "title": "I1::quux",
3455 powered_by="serverless_ide",
3457 .request(line=line(), method="shutdown", params={}, result=None)
3458 .notification(method="exit", params={})
3460 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3462 def test_serverless_ide_document_symbol(self) -> None:
3463 variables = dict(self.prepare_serverless_ide_environment())
3464 variables.update(self.setup_php_file("definition.php"))
3465 self.test_driver.stop_hh_server()
3467 spec = (
3468 self.initialize_spec(
3469 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide=True
3471 .notification(
3472 method="textDocument/didOpen",
3473 params={
3474 "textDocument": {
3475 "uri": "${php_file_uri}",
3476 "languageId": "hack",
3477 "version": 1,
3478 "text": "${php_file}",
3482 .request(
3483 line=line(),
3484 comment="documentSymbol call",
3485 method="textDocument/documentSymbol",
3486 params={"textDocument": {"uri": "${php_file_uri}"}},
3487 result=[
3489 "name": "First",
3490 "kind": 14,
3491 "location": {
3492 "uri": "file://${root_path}/definition.php",
3493 "range": {
3494 "start": {"line": 50, "character": 18},
3495 "end": {"line": 50, "character": 47},
3498 "containerName": "MyEnumClass",
3501 "name": "MyEnumClass",
3502 "kind": 10,
3503 "location": {
3504 "uri": "file://${root_path}/definition.php",
3505 "range": {
3506 "start": {"line": 49, "character": 0},
3507 "end": {"line": 52, "character": 1},
3512 "name": "testClassMemberInsideConstructorInvocation",
3513 "kind": 12,
3514 "location": {
3515 "uri": "file://${root_path}/definition.php",
3516 "range": {
3517 "start": {"line": 44, "character": 0},
3518 "end": {"line": 46, "character": 1},
3523 "name": "MyString",
3524 "kind": 14,
3525 "location": {
3526 "uri": "file://${root_path}/definition.php",
3527 "range": {
3528 "start": {"line": 41, "character": 8},
3529 "end": {"line": 41, "character": 29},
3532 "containerName": "HasString",
3535 "name": "HasString",
3536 "kind": 5,
3537 "location": {
3538 "uri": "file://${root_path}/definition.php",
3539 "range": {
3540 "start": {"line": 40, "character": 0},
3541 "end": {"line": 42, "character": 1},
3546 "name": "__construct",
3547 "kind": 6,
3548 "location": {
3549 "uri": "file://${root_path}/definition.php",
3550 "range": {
3551 "start": {"line": 37, "character": 2},
3552 "end": {"line": 37, "character": 43},
3555 "containerName": "TakesString",
3558 "name": "TakesString",
3559 "kind": 5,
3560 "location": {
3561 "uri": "file://${root_path}/definition.php",
3562 "range": {
3563 "start": {"line": 36, "character": 0},
3564 "end": {"line": 38, "character": 1},
3569 "name": "FF",
3570 "kind": 5,
3571 "location": {
3572 "uri": "file://${root_path}/definition.php",
3573 "range": {
3574 "start": {"line": 26, "character": 0},
3575 "end": {"line": 26, "character": 11},
3580 "name": "__construct",
3581 "kind": 6,
3582 "location": {
3583 "uri": "file://${root_path}/definition.php",
3584 "range": {
3585 "start": {"line": 21, "character": 2},
3586 "end": {"line": 23, "character": 3},
3589 "containerName": "EE",
3592 "name": "EE",
3593 "kind": 5,
3594 "location": {
3595 "uri": "file://${root_path}/definition.php",
3596 "range": {
3597 "start": {"line": 20, "character": 0},
3598 "end": {"line": 24, "character": 1},
3603 "name": "CC",
3604 "kind": 5,
3605 "location": {
3606 "uri": "file://${root_path}/definition.php",
3607 "range": {
3608 "start": {"line": 14, "character": 0},
3609 "end": {"line": 15, "character": 1},
3614 "name": "__construct",
3615 "kind": 6,
3616 "location": {
3617 "uri": "file://${root_path}/definition.php",
3618 "range": {
3619 "start": {"line": 11, "character": 2},
3620 "end": {"line": 11, "character": 40},
3623 "containerName": "BB",
3626 "name": "BB",
3627 "kind": 5,
3628 "location": {
3629 "uri": "file://${root_path}/definition.php",
3630 "range": {
3631 "start": {"line": 10, "character": 0},
3632 "end": {"line": 12, "character": 1},
3637 "name": "a_definition",
3638 "kind": 12,
3639 "location": {
3640 "uri": "file://${root_path}/definition.php",
3641 "range": {
3642 "start": {"line": 2, "character": 0},
3643 "end": {"line": 4, "character": 1},
3648 "name": "b_definition",
3649 "kind": 12,
3650 "location": {
3651 "uri": "file://${root_path}/definition.php",
3652 "range": {
3653 "start": {"line": 6, "character": 0},
3654 "end": {"line": 8, "character": 1},
3659 "name": "DD",
3660 "kind": 5,
3661 "location": {
3662 "uri": "file://${root_path}/definition.php",
3663 "range": {
3664 "start": {"line": 17, "character": 0},
3665 "end": {"line": 18, "character": 1},
3670 "name": "test",
3671 "kind": 12,
3672 "location": {
3673 "uri": "file://${root_path}/definition.php",
3674 "range": {
3675 "start": {"line": 28, "character": 0},
3676 "end": {"line": 34, "character": 1},
3681 "name": "MyEnumClassKind",
3682 "kind": 5,
3683 "location": {
3684 "uri": "file://${root_path}/definition.php",
3685 "range": {
3686 "start": {"line": 48, "character": 0},
3687 "end": {"line": 48, "character": 24},
3692 "name": "Second",
3693 "kind": 14,
3694 "location": {
3695 "uri": "${php_file_uri}",
3696 "range": {
3697 "start": {"line": 51, "character": 18},
3698 "end": {"line": 51, "character": 48},
3701 "containerName": "MyEnumClass",
3704 powered_by="serverless_ide",
3706 .request(line=line(), method="shutdown", params={}, result=None)
3707 .notification(method="exit", params={})
3709 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3711 def initialize_spec(
3712 self,
3713 spec: LspTestSpec,
3714 use_serverless_ide: bool,
3715 supports_status: bool = False, # does the caller wish to see all status messages?
3716 supports_init: bool = False, # do we wish to interact with init, rather than waiting for init ok?
3717 ) -> LspTestSpec:
3718 if use_serverless_ide:
3719 initialization_options = {
3720 "namingTableSavedStatePath": "${naming_table_saved_state_path}",
3721 "namingTableSavedStateTestDelay": 0.0,
3723 if supports_init:
3724 # A small delay, since otherwise init completes immediately
3725 # This isn't very racy. All we need is a tiny delay so that
3726 # other things which are in the queue get processed, rather
3727 # than continuing synchronously
3728 initialization_options["namingTableSavedStateTestDelay"] = 0.5
3729 else:
3730 initialization_options = {}
3732 window_capabilities = {}
3733 if supports_status:
3734 window_capabilities["status"] = {"dynamicRegistration": False}
3736 spec = spec.ignore_notifications(method="telemetry/event").request(
3737 line=line(),
3738 method="initialize",
3739 params={
3740 "initializationOptions": initialization_options,
3741 "processId": None,
3742 "rootPath": "${root_path}",
3743 "capabilities": {
3744 "window": window_capabilities,
3745 "textDocument": {
3746 "completion": {"completionItem": {"snippetSupport": True}}
3750 result={
3751 "capabilities": {
3752 "textDocumentSync": {
3753 "openClose": True,
3754 "change": 2,
3755 "willSave": False,
3756 "willSaveWaitUntil": True,
3757 "save": {"includeText": False},
3759 "hoverProvider": True,
3760 "completionProvider": {
3761 "resolveProvider": True,
3762 "triggerCharacters": [
3763 "$",
3764 ">",
3765 "\\",
3766 ":",
3767 "<",
3768 "[",
3769 "'",
3770 '"',
3771 "{",
3772 "#",
3775 "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
3776 "definitionProvider": True,
3777 "typeDefinitionProvider": True,
3778 "referencesProvider": True,
3779 "documentHighlightProvider": True,
3780 "documentSymbolProvider": True,
3781 "workspaceSymbolProvider": True,
3782 "codeActionProvider": True,
3783 "documentFormattingProvider": True,
3784 "documentRangeFormattingProvider": True,
3785 "documentOnTypeFormattingProvider": {
3786 "firstTriggerCharacter": ";",
3787 "moreTriggerCharacter": ["}"],
3789 "renameProvider": True,
3790 "implementationProvider": True,
3791 "typeCoverageProvider": True,
3792 "rageProvider": True,
3796 if use_serverless_ide:
3797 spec = spec.wait_for_server_request(
3798 method="client/registerCapability",
3799 params={
3800 "registrations": [
3802 "id": "did-change-watched-files",
3803 "method": "workspace/didChangeWatchedFiles",
3804 "registerOptions": {
3805 "watchers": [
3807 "globPattern": "**/*.{php,phpt,hack,hackpartial,hck,hh,hhi,xhp}",
3808 "kind": 7,
3815 result=None,
3817 if not supports_status:
3818 spec = spec.ignore_status_diagnostics(True)
3820 if use_serverless_ide and not supports_init:
3821 spec = spec.wait_for_notification(
3822 comment="wait for sIDE to finish init",
3823 method="telemetry/event",
3824 params={"type": 4, "message": "[client-ide] Finished init: ok"},
3827 return spec
3829 def test_serverless_ide_type_definition(self) -> None:
3830 variables = dict(self.prepare_serverless_ide_environment())
3831 variables.update(self.setup_php_file("type_definition.php"))
3832 self.test_driver.stop_hh_server()
3834 spec = (
3835 self.initialize_spec(
3836 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide=True
3838 .notification(
3839 method="textDocument/didOpen",
3840 params={
3841 "textDocument": {
3842 "uri": "${php_file_uri}",
3843 "languageId": "hack",
3844 "version": 1,
3845 "text": "${php_file}",
3849 .request(
3850 line=line(),
3851 comment="Conditional Type Definition of HH or II",
3852 method="textDocument/typeDefinition",
3853 params={
3854 "textDocument": {"uri": "${php_file_uri}"},
3855 "position": {"line": 32, "character": 2},
3857 result=[
3859 "uri": "${php_file_uri}",
3860 "range": {
3861 "start": {"line": 2, "character": 6},
3862 "end": {"line": 2, "character": 8},
3864 "title": "\\HH",
3867 "uri": "${php_file_uri}",
3868 "range": {
3869 "start": {"line": 12, "character": 6},
3870 "end": {"line": 12, "character": 8},
3872 "title": "\\LL",
3875 powered_by="serverless_ide",
3877 .request(
3878 line=line(),
3879 comment="Standard Class Definition",
3880 method="textDocument/typeDefinition",
3881 params={
3882 "textDocument": {"uri": "${php_file_uri}"},
3883 "position": {"line": 40, "character": 2},
3885 result=[
3887 "uri": "${php_file_uri}",
3888 "range": {
3889 "start": {"line": 2, "character": 6},
3890 "end": {"line": 2, "character": 8},
3892 "title": "\\HH",
3895 powered_by="serverless_ide",
3897 .request(
3898 line=line(),
3899 comment="Class Type Definition with Casting",
3900 method="textDocument/typeDefinition",
3901 params={
3902 "textDocument": {"uri": "${php_file_uri}"},
3903 "position": {"line": 41, "character": 2},
3905 result=[
3907 "uri": "${php_file_uri}",
3908 "range": {
3909 "start": {"line": 2, "character": 6},
3910 "end": {"line": 2, "character": 8},
3912 "title": "\\HH",
3915 powered_by="serverless_ide",
3917 .request(
3918 line=line(),
3919 comment="Primitive Type Definition",
3920 method="textDocument/typeDefinition",
3921 params={
3922 "textDocument": {"uri": "${php_file_uri}"},
3923 "position": {"line": 42, "character": 2},
3925 result=[],
3926 powered_by="serverless_ide",
3928 .request(
3929 line=line(),
3930 comment="Function Return Type Definition",
3931 method="textDocument/typeDefinition",
3932 params={
3933 "textDocument": {"uri": "${php_file_uri}"},
3934 "position": {"line": 43, "character": 2},
3936 result=[
3938 "uri": "${php_file_uri}",
3939 "range": {
3940 "start": {"line": 12, "character": 6},
3941 "end": {"line": 12, "character": 8},
3943 "title": "\\LL",
3946 powered_by="serverless_ide",
3948 .request(
3949 line=line(),
3950 comment="Function definition with primitive return type",
3951 method="textDocument/typeDefinition",
3952 params={
3953 "textDocument": {"uri": "${php_file_uri}"},
3954 "position": {"line": 44, "character": 2},
3956 result=[
3958 "uri": "${php_file_uri}",
3959 "range": {
3960 "start": {"line": 22, "character": 9},
3961 "end": {"line": 22, "character": 29},
3963 "title": "(function(): int)",
3966 powered_by="serverless_ide",
3968 .request(line=line(), method="shutdown", params={}, result=None)
3969 .notification(method="exit", params={})
3971 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
3973 def test_serverless_ide_hover(self) -> None:
3974 variables = dict(self.prepare_serverless_ide_environment())
3975 variables.update(self.setup_php_file("hover.php"))
3976 self.test_driver.stop_hh_server()
3978 spec = (
3979 self.initialize_spec(
3980 LspTestSpec("serverless_ide_hover"), use_serverless_ide=True
3982 .notification(
3983 method="textDocument/didOpen",
3984 params={
3985 "textDocument": {
3986 "uri": "${php_file_uri}",
3987 "languageId": "hack",
3988 "version": 1,
3989 "text": "${php_file}",
3993 .request(
3994 line=line(),
3995 comment="hover over function invocation",
3996 method="textDocument/hover",
3997 params={
3998 "textDocument": {"uri": "${php_file_uri}"},
3999 "position": {"line": 3, "character": 16},
4001 result={
4002 "contents": [
4003 {"language": "hack", "value": "int"},
4004 "A comment describing b_hover.",
4006 "range": {
4007 "start": {"line": 3, "character": 9},
4008 "end": {"line": 3, "character": 16},
4011 powered_by="serverless_ide",
4013 .request(
4014 line=line(),
4015 comment="hover over string literal outside call",
4016 method="textDocument/hover",
4017 params={
4018 "textDocument": {"uri": "${php_file_uri}"},
4019 "position": {"line": 25, "character": 12}, # 9 - 16
4021 result={"contents": [{"language": "hack", "value": "string"}]},
4022 powered_by="serverless_ide",
4024 .request(
4025 line=line(),
4026 comment="hover over string literal inside call",
4027 method="textDocument/hover",
4028 params={
4029 "textDocument": {"uri": "${php_file_uri}"},
4030 "position": {"line": 26, "character": 20}, # 16 - 29
4032 result={"contents": [{"language": "hack", "value": "string"}]},
4033 powered_by="serverless_ide",
4035 .request(
4036 line=line(),
4037 comment="hover over int literal inside call",
4038 method="textDocument/hover",
4039 params={
4040 "textDocument": {"uri": "${php_file_uri}"},
4041 "position": {"line": 26, "character": 32}, # 31 - 33
4043 result={"contents": [{"language": "hack", "value": "int"}]},
4044 powered_by="serverless_ide",
4046 .request(
4047 line=line(),
4048 comment="hover over constant reference",
4049 method="textDocument/hover",
4050 params={
4051 "textDocument": {"uri": "${php_file_uri}"},
4052 "position": {"line": 15, "character": 19},
4054 result={
4055 "contents": [
4056 {"language": "hack", "value": "THE_ANSWER"},
4057 "A comment describing THE_ANSWER",
4058 "int THE_ANSWER = 42",
4060 "range": {
4061 "start": {"line": 15, "character": 9},
4062 "end": {"line": 15, "character": 19},
4065 powered_by="serverless_ide",
4067 .request(
4068 line=line(),
4069 comment="hover over whitespace",
4070 method="textDocument/hover",
4071 params={
4072 "textDocument": {"uri": "${php_file_uri}"},
4073 "position": {"line": 3, "character": 1},
4075 result=None,
4076 powered_by="serverless_ide",
4078 .request(
4079 line=line(),
4080 comment="hover over a keyword",
4081 method="textDocument/hover",
4082 params={
4083 "textDocument": {"uri": "${php_file_uri}"},
4084 "position": {"line": 2, "character": 1},
4086 result=None,
4087 powered_by="serverless_ide",
4089 .request(
4090 line=line(),
4091 comment="hover over a comment",
4092 method="textDocument/hover",
4093 params={
4094 "textDocument": {"uri": "${php_file_uri}"},
4095 "position": {"line": 1, "character": 4},
4097 result=None,
4098 powered_by="serverless_ide",
4100 .request(
4101 line=line(),
4102 comment="hover past the end of a line",
4103 method="textDocument/hover",
4104 params={
4105 "textDocument": {"uri": "${php_file_uri}"},
4106 "position": {"line": 3, "character": 100},
4108 result=None,
4109 powered_by="serverless_ide",
4111 .request(
4112 line=line(),
4113 comment="hover past the end of a file",
4114 method="textDocument/hover",
4115 params={
4116 "textDocument": {"uri": "${php_file_uri}"},
4117 "position": {"line": 300, "character": 0},
4119 result=None,
4120 powered_by="serverless_ide",
4122 .request(
4123 line=line(),
4124 comment="hover over class with copyright docblock",
4125 method="textDocument/hover",
4126 params={
4127 "textDocument": {"uri": "${php_file_uri}"},
4128 "position": {"line": 37, "character": 15},
4130 result={
4131 "contents": [
4132 {"language": "hack", "value": "final class CopyrightClass"},
4133 "Testing copyright removal",
4135 "range": {
4136 "start": {"line": 37, "character": 2},
4137 "end": {"line": 37, "character": 16},
4140 powered_by="serverless_ide",
4142 .request(
4143 line=line(),
4144 comment="hover over class with generated docblock",
4145 method="textDocument/hover",
4146 params={
4147 "textDocument": {"uri": "${php_file_uri}"},
4148 "position": {"line": 58, "character": 15},
4150 result={
4151 "contents": [
4152 {"language": "hack", "value": "final class GeneratedClass"},
4153 "Testing generated text removal",
4155 "range": {
4156 "start": {"line": 58, "character": 2},
4157 "end": {"line": 58, "character": 16},
4160 powered_by="serverless_ide",
4162 .request(
4163 line=line(),
4164 comment="hover over an primitive attribute in an xhp literal",
4165 method="textDocument/hover",
4166 params={
4167 "textDocument": {"uri": "${php_file_uri}"},
4168 "position": {"line": 62, "character": 25},
4170 result={
4171 "contents": [
4172 {"language": "hack", "value": "public ?string name"},
4173 ":xhp:enum-attribute::name docblock",
4175 "range": {
4176 "start": {"line": 62, "character": 22},
4177 "end": {"line": 62, "character": 26},
4180 powered_by="serverless_ide",
4182 .request(
4183 line=line(),
4184 comment="hover over a nonprimitive attribute in an xhp literal",
4185 method="textDocument/hover",
4186 params={
4187 "textDocument": {"uri": "${php_file_uri}"},
4188 "position": {"line": 62, "character": 36},
4190 result={
4191 "contents": [
4192 {"language": "hack", "value": "public ?MyEnum enum-attribute"}
4194 "range": {
4195 "start": {"line": 62, "character": 33},
4196 "end": {"line": 62, "character": 47},
4199 powered_by="serverless_ide",
4201 .request(
4202 line=line(),
4203 comment="hover over a generic attribute in an xhp literal",
4204 method="textDocument/hover",
4205 params={
4206 "textDocument": {"uri": "${php_file_uri}"},
4207 "position": {"line": 63, "character": 16},
4209 result={
4210 "contents": [
4211 {"language": "hack", "value": "public ?ID<EntSomething> id"}
4213 "range": {
4214 "start": {"line": 63, "character": 15},
4215 "end": {"line": 63, "character": 17},
4218 powered_by="serverless_ide",
4220 .notification(
4221 comment="Add '<xhp:enum-attribute name' to test hover for incomplete xhp attribute",
4222 method="textDocument/didChange",
4223 params={
4224 "textDocument": {"uri": "${php_file_uri}"},
4225 "contentChanges": [
4227 "range": {
4228 "start": {"line": 69, "character": 0},
4229 "end": {"line": 69, "character": 0},
4231 "text": "<xhp:enum-attribute name",
4236 .request(
4237 line=line(),
4238 comment="hover over an attribute in an xhp literal without a value",
4239 method="textDocument/hover",
4240 params={
4241 "textDocument": {"uri": "${php_file_uri}"},
4242 "position": {"line": 69, "character": 22},
4244 result={
4245 "contents": [
4246 {"language": "hack", "value": "public ?string name"},
4247 ":xhp:enum-attribute::name docblock",
4249 "range": {
4250 "start": {"line": 69, "character": 20},
4251 "end": {"line": 69, "character": 24},
4254 powered_by="serverless_ide",
4256 .request(line=line(), method="shutdown", params={}, result=None)
4257 .notification(method="exit", params={})
4259 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4261 def test_serverless_ide_file_touched_on_disk(self) -> None:
4262 variables = dict(self.prepare_serverless_ide_environment())
4263 variables.update(self.setup_php_file("hover.php"))
4264 self.test_driver.stop_hh_server()
4266 spec = (
4267 self.initialize_spec(
4268 LspTestSpec("serverless_ide_file_on_disk_change"),
4269 use_serverless_ide=True,
4271 .notification(
4272 method="textDocument/didOpen",
4273 params={
4274 "textDocument": {
4275 "uri": "${php_file_uri}",
4276 "languageId": "hack",
4277 "version": 1,
4278 "text": "${php_file}",
4282 .notification(
4283 method="workspace/didChangeWatchedFiles",
4284 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
4286 .wait_for_notification(
4287 comment="wait for sIDE to process file change",
4288 method="telemetry/event",
4289 params={
4290 "type": 4,
4291 "message": "[client-ide] Done processing file changes",
4294 .request(
4295 line=line(),
4296 method="textDocument/hover",
4297 params={
4298 "textDocument": {"uri": "${php_file_uri}"},
4299 "position": {"line": 3, "character": 16},
4301 result={
4302 "contents": [
4303 {"language": "hack", "value": "int"},
4304 "A comment describing b_hover.",
4306 "range": {
4307 "start": {"line": 3, "character": 9},
4308 "end": {"line": 3, "character": 16},
4311 powered_by="serverless_ide",
4313 .request(line=line(), method="shutdown", params={}, result=None)
4314 .notification(method="exit", params={})
4316 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4318 def test_serverless_ide_file_hover_with_errors(self) -> None:
4319 variables = dict(self.prepare_serverless_ide_environment())
4320 variables.update(self.setup_php_file("hover_with_errors.php"))
4321 self.test_driver.stop_hh_server()
4323 spec = (
4324 self.initialize_spec(
4325 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide=True
4327 .notification(
4328 method="textDocument/didOpen",
4329 params={
4330 "textDocument": {
4331 "uri": "${php_file_uri}",
4332 "languageId": "hack",
4333 "version": 1,
4334 "text": "${php_file}",
4338 .notification(
4339 method="workspace/didChangeWatchedFiles",
4340 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
4342 .wait_for_notification(
4343 comment="wait for sIDE to process file change",
4344 method="telemetry/event",
4345 params={
4346 "type": 4,
4347 "message": "[client-ide] Done processing file changes",
4350 .request(
4351 line=line(),
4352 comment="Totally normal hover",
4353 method="textDocument/hover",
4354 params={
4355 "textDocument": {"uri": "${php_file_uri}"},
4356 "position": {"line": 14, "character": 37},
4358 result={
4359 "contents": [
4361 "language": "hack",
4362 "value": "public static function staticMethod(string $z): void",
4364 'During testing, we\'ll remove the "public" tag from this '
4365 "method\n"
4366 "to ensure that we can still get IDE services",
4367 "Return type: `void`",
4368 "Full name: `HoverWithErrorsClass::staticMethod`",
4370 "range": {
4371 "end": {"character": 39, "line": 14},
4372 "start": {"character": 27, "line": 14},
4375 powered_by="serverless_ide",
4377 .notification(
4378 comment="Remove the 'public' visibility modifier which triggers AST->AAST errors",
4379 method="textDocument/didChange",
4380 params={
4381 "textDocument": {"uri": "${php_file_uri}"},
4382 "contentChanges": [
4384 "range": {
4385 "start": {"line": 10, "character": 2},
4386 "end": {"line": 10, "character": 8},
4388 "text": "",
4393 .request(
4394 line=line(),
4395 comment="Hover should still work even if visibility modifier has been removed",
4396 method="textDocument/hover",
4397 params={
4398 "textDocument": {"uri": "${php_file_uri}"},
4399 "position": {"line": 14, "character": 37},
4401 result={
4402 "contents": [
4404 "language": "hack",
4405 "value": "public static function staticMethod(string $z): void",
4407 'During testing, we\'ll remove the "public" tag from this '
4408 "method\n"
4409 "to ensure that we can still get IDE services",
4410 "Return type: `void`",
4411 "Full name: `HoverWithErrorsClass::staticMethod`",
4413 "range": {
4414 "end": {"character": 39, "line": 14},
4415 "start": {"character": 27, "line": 14},
4418 powered_by="serverless_ide",
4420 .request(line=line(), method="shutdown", params={}, result=None)
4421 .notification(method="exit", params={})
4423 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4425 def test_serverless_ide_formatting(self) -> None:
4426 # This test will fail if hackfmt can't be found
4427 if not self.test_driver.run_hackfmt_check():
4428 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4430 variables = dict(self.prepare_serverless_ide_environment())
4431 variables.update(self.setup_php_file("messy.php"))
4433 self.test_driver.stop_hh_server()
4435 spec = (
4436 self.initialize_spec(LspTestSpec("formatting"), use_serverless_ide=True)
4437 .notification(
4438 method="textDocument/didOpen",
4439 params={
4440 "textDocument": {
4441 "uri": "${php_file_uri}",
4442 "languageId": "hack",
4443 "version": 1,
4444 "text": "${php_file}",
4448 .request(
4449 line=line(),
4450 method="textDocument/formatting",
4451 params={
4452 "textDocument": {"uri": "${php_file_uri}"},
4453 "options": {"tabSize": 5, "insertSpaces": True},
4455 result=[
4457 "range": {
4458 "start": {"line": 0, "character": 0},
4459 "end": {"line": 12, "character": 0},
4461 "newText": "<?hh //strict\n\nfunction x(): string {\n"
4462 + ' $a = "this";\n\n'
4463 + ' $b = "is";\n\n'
4464 + ' $c = "messy";\n\n'
4465 + ' $d = ".";\n'
4466 + ' return "$a"."$b"."$c"."d";\n}\n',
4470 .request(line=line(), method="shutdown", params={}, result=None)
4471 .notification(method="exit", params={})
4473 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4475 def test_serverless_ide_rangeformatting(self) -> None:
4476 # This test will fail if hackfmt can't be found
4477 if not self.test_driver.run_hackfmt_check():
4478 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4480 variables = dict(self.prepare_serverless_ide_environment())
4481 variables.update(self.setup_php_file("messy.php"))
4483 self.test_driver.stop_hh_server()
4485 spec = (
4486 self.initialize_spec(
4487 LspTestSpec("range_formatting"), use_serverless_ide=True
4489 .notification(
4490 method="textDocument/didOpen",
4491 params={
4492 "textDocument": {
4493 "uri": "${php_file_uri}",
4494 "languageId": "hack",
4495 "version": 1,
4496 "text": "${php_file}",
4500 .request(
4501 line=line(),
4502 method="textDocument/rangeFormatting",
4503 params={
4504 "textDocument": {"uri": "${php_file_uri}"},
4505 "range": {
4506 "start": {"line": 3, "character": 0},
4507 "end": {"line": 4, "character": 0},
4509 "options": {"tabSize": 5, "insertSpaces": True},
4511 result=[
4513 "range": {
4514 "start": {"line": 3, "character": 0},
4515 "end": {"line": 4, "character": 0},
4517 "newText": ' $a = "this";\n',
4521 .request(line=line(), method="shutdown", params={}, result=None)
4522 .notification(method="exit", params={})
4524 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4526 def test_serverless_ide_ontypeformatting(self) -> None:
4527 # This test will fail if hackfmt can't be found
4528 if not self.test_driver.run_hackfmt_check():
4529 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
4531 variables = dict(self.prepare_serverless_ide_environment())
4532 variables.update(self.setup_php_file("ontypeformatting.php"))
4534 spec = (
4535 self.initialize_spec(
4536 LspTestSpec("ontypeformatting"), use_serverless_ide=True
4538 .notification(
4539 method="textDocument/didOpen",
4540 params={
4541 "textDocument": {
4542 "uri": "${php_file_uri}",
4543 "languageId": "hack",
4544 "version": 1,
4545 "text": "${php_file}",
4549 .request(
4550 line=line(),
4551 method="textDocument/onTypeFormatting",
4552 params={
4553 "textDocument": {"uri": "${php_file_uri}"},
4554 "position": {"line": 9, "character": 58},
4555 "ch": ";",
4556 "options": {"tabSize": 2, "insertSpaces": True},
4558 result=[
4560 "range": {
4561 "start": {"line": 5, "character": 23},
4562 "end": {"line": 9, "character": 58},
4564 "newText": "{\n test_otf(\n"
4565 + " '1234567890',\n"
4566 + " '1234567890',\n"
4567 + " '1234567890',\n"
4568 + " '1234567890',\n"
4569 + " '1234567890',\n"
4570 + " '1234567890',\n );",
4574 .request(
4575 line=line(),
4576 method="textDocument/onTypeFormatting",
4577 params={
4578 "textDocument": {"uri": "${php_file_uri}"},
4579 "position": {"line": 15, "character": 23},
4580 "ch": "}",
4581 "options": {"tabSize": 2, "insertSpaces": True},
4583 result=[
4585 "range": {
4586 "start": {"line": 15, "character": 0},
4587 "end": {"line": 15, "character": 23},
4589 "newText": "function otf(): void {}",
4593 .request(line=line(), method="shutdown", params={}, result=None)
4594 .notification(method="exit", params={})
4597 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
4599 def test_did_change(self) -> None:
4600 self.prepare_server_environment()
4601 variables = self.setup_php_file("didchange.php")
4602 spec = (
4603 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
4604 .wait_for_hh_server_ready()
4605 .notification(
4606 method="textDocument/didOpen",
4607 params={
4608 "textDocument": {
4609 "uri": "${php_file_uri}",
4610 "languageId": "hack",
4611 "version": 1,
4612 "text": "${php_file}",
4616 .notification(
4617 method="textDocument/didChange",
4618 params={
4619 "textDocument": {"uri": "${php_file_uri}"},
4620 "contentChanges": [
4622 "range": {
4623 "start": {"line": 7, "character": 11},
4624 "end": {"line": 7, "character": 12},
4626 "text": "a",
4631 .wait_for_notification(
4632 method="textDocument/publishDiagnostics",
4633 params={
4634 "uri": "${php_file_uri}",
4635 "diagnostics": [
4637 "range": {
4638 "start": {"line": 7, "character": 11},
4639 "end": {"line": 7, "character": 11},
4641 "severity": 1,
4642 "code": 1002,
4643 "source": "Hack",
4644 "message": "A semicolon ; is expected here.",
4645 "relatedLocations": [],
4646 "relatedInformation": [],
4651 .request(line=line(), method="shutdown", params={}, result=None)
4652 .wait_for_notification(
4653 comment="Hack appears to clear out diagnostics before shutting down",
4654 method="textDocument/publishDiagnostics",
4655 params={"uri": "${php_file_uri}", "diagnostics": []},
4657 .notification(method="exit", params={})
4659 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
4661 def test_go_to_implementation(self) -> None:
4662 self.prepare_server_environment()
4663 variables = self.setup_php_file("go_to_implementation.php")
4664 spec = (
4665 self.initialize_spec(
4666 LspTestSpec("test_go_to_implementation"), use_serverless_ide=False
4668 .wait_for_hh_server_ready()
4669 .notification(
4670 method="textDocument/didOpen",
4671 params={
4672 "textDocument": {
4673 "uri": "${php_file_uri}",
4674 "languageId": "hack",
4675 "version": 1,
4676 "text": "${php_file}",
4680 .request(
4681 line=line(),
4682 comment="go to implemenetation: abstract class",
4683 method="textDocument/implementation",
4684 params={
4685 "textDocument": {"uri": "${php_file_uri}"},
4686 "position": {"line": 1, "character": 17},
4688 result=[
4690 "uri": "${php_file_uri}",
4691 "range": {
4692 "start": {"line": 7, "character": 6},
4693 "end": {"line": 7, "character": 9},
4698 .request(
4699 line=line(),
4700 comment="go to implemenetation: interface",
4701 method="textDocument/implementation",
4702 params={
4703 "textDocument": {"uri": "${php_file_uri}"},
4704 "position": {"line": 13, "character": 13},
4706 result=[
4708 "uri": "${php_file_uri}",
4709 "range": {
4710 "start": {"line": 17, "character": 6},
4711 "end": {"line": 17, "character": 9},
4716 .request(
4717 line=line(),
4718 comment="go to implemenetation: trait",
4719 method="textDocument/implementation",
4720 params={
4721 "textDocument": {"uri": "${php_file_uri}"},
4722 "position": {"line": 23, "character": 10},
4724 result=[
4726 "uri": "${php_file_uri}",
4727 "range": {
4728 "start": {"line": 30, "character": 6},
4729 "end": {"line": 30, "character": 16},
4734 .request(
4735 line=line(),
4736 comment="go to implemenetation: method",
4737 method="textDocument/implementation",
4738 params={
4739 "textDocument": {"uri": "${php_file_uri}"},
4740 "position": {"line": 19, "character": 18},
4742 result=[
4744 "uri": "${php_file_uri}",
4745 "range": {
4746 "start": {"line": 8, "character": 18},
4747 "end": {"line": 8, "character": 22},
4752 .request(line=line(), method="shutdown", params={}, result=None)
4753 .notification(method="exit", params={})
4755 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
4757 def test_signature_help(self) -> None:
4758 self.prepare_server_environment()
4759 variables = self.setup_php_file("signaturehelp.php")
4760 spec = (
4761 self.initialize_spec(
4762 LspTestSpec("test_signature_help"), use_serverless_ide=False
4764 .wait_for_hh_server_ready()
4765 .notification(
4766 method="textDocument/didOpen",
4767 params={
4768 "textDocument": {
4769 "uri": "${php_file_uri}",
4770 "languageId": "hack",
4771 "version": 1,
4772 "text": "${php_file}",
4776 .request(
4777 line=line(),
4778 comment="signature help for 0-argument constructor"
4779 " (left of opening paren)",
4780 method="textDocument/signatureHelp",
4781 params={
4782 "textDocument": {"uri": "${php_file_uri}"},
4783 "position": {"line": 16, "character": 18},
4785 result=None,
4787 .request(
4788 line=line(),
4789 comment="signature help for 0-argument constructor",
4790 method="textDocument/signatureHelp",
4791 params={
4792 "textDocument": {"uri": "${php_file_uri}"},
4793 "position": {"line": 16, "character": 19},
4795 result={
4796 "signatures": [
4798 "label": "public function __construct(): void",
4799 "documentation": "Constructor with doc block",
4800 "parameters": [],
4803 "activeSignature": 0,
4804 "activeParameter": 0,
4807 .request(
4808 line=line(),
4809 comment="signature help for 0-argument constructor"
4810 " (right of closing paren)",
4811 method="textDocument/signatureHelp",
4812 params={
4813 "textDocument": {"uri": "${php_file_uri}"},
4814 "position": {"line": 16, "character": 20},
4816 result=None,
4818 .request(
4819 line=line(),
4820 comment="signature help for 2-argument instance method"
4821 " (left of opening paren)",
4822 method="textDocument/signatureHelp",
4823 params={
4824 "textDocument": {"uri": "${php_file_uri}"},
4825 "position": {"line": 17, "character": 20},
4827 result=None,
4829 .request(
4830 line=line(),
4831 comment="signature help for 2-argument instance method"
4832 " (right of opening paren)",
4833 method="textDocument/signatureHelp",
4834 params={
4835 "textDocument": {"uri": "${php_file_uri}"},
4836 "position": {"line": 17, "character": 21},
4838 result={
4839 "signatures": [
4841 "label": "public function instanceMethod"
4842 "(int $x1, int $x2): void",
4843 "documentation": "Instance method with doc block",
4844 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4847 "activeSignature": 0,
4848 "activeParameter": 0,
4851 .request(
4852 line=line(),
4853 comment="signature help for 2-argument instance method"
4854 " (left of first comma)",
4855 method="textDocument/signatureHelp",
4856 params={
4857 "textDocument": {"uri": "${php_file_uri}"},
4858 "position": {"line": 17, "character": 22},
4860 result={
4861 "signatures": [
4863 "label": "public function instanceMethod"
4864 "(int $x1, int $x2): void",
4865 "documentation": "Instance method with doc block",
4866 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4869 "activeSignature": 0,
4870 "activeParameter": 1,
4873 .request(
4874 line=line(),
4875 comment="signature help for 2-argument instance method"
4876 " (right of first comma)",
4877 method="textDocument/signatureHelp",
4878 params={
4879 "textDocument": {"uri": "${php_file_uri}"},
4880 "position": {"line": 17, "character": 23},
4882 result={
4883 "signatures": [
4885 "label": "public function instanceMethod"
4886 "(int $x1, int $x2): void",
4887 "documentation": "Instance method with doc block",
4888 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4891 "activeSignature": 0,
4892 "activeParameter": 1,
4895 .request(
4896 line=line(),
4897 comment="signature help for 2-argument instance method"
4898 " (left of closing paren)",
4899 method="textDocument/signatureHelp",
4900 params={
4901 "textDocument": {"uri": "${php_file_uri}"},
4902 "position": {"line": 17, "character": 24},
4904 result={
4905 "signatures": [
4907 "label": "public function instanceMethod"
4908 "(int $x1, int $x2): void",
4909 "documentation": "Instance method with doc block",
4910 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
4913 "activeSignature": 0,
4914 "activeParameter": 1,
4917 .request(
4918 line=line(),
4919 comment="signature help for 2-argument instance method"
4920 " (right of closing paren)",
4921 method="textDocument/signatureHelp",
4922 params={
4923 "textDocument": {"uri": "${php_file_uri}"},
4924 "position": {"line": 17, "character": 25},
4926 result=None,
4928 .request(
4929 line=line(),
4930 comment="signature help for 1-argument static method"
4931 " (left of open paren)",
4932 method="textDocument/signatureHelp",
4933 params={
4934 "textDocument": {"uri": "${php_file_uri}"},
4935 "position": {"line": 18, "character": 23},
4937 result=None,
4939 .request(
4940 line=line(),
4941 comment="signature help for 1-argument static method"
4942 " (right of open paren)",
4943 method="textDocument/signatureHelp",
4944 params={
4945 "textDocument": {"uri": "${php_file_uri}"},
4946 "position": {"line": 18, "character": 24},
4948 result={
4949 "signatures": [
4951 "label": "public static function staticMethod"
4952 "(string $z): void",
4953 "documentation": "Static method with doc block",
4954 "parameters": [{"label": "$z"}],
4957 "activeSignature": 0,
4958 "activeParameter": 0,
4961 .request(
4962 line=line(),
4963 comment="signature help for 2-argument global function"
4964 " (left of open paren)",
4965 method="textDocument/signatureHelp",
4966 params={
4967 "textDocument": {"uri": "${php_file_uri}"},
4968 "position": {"line": 19, "character": 17},
4970 result=None,
4972 .request(
4973 line=line(),
4974 comment="signature help for 2-argument global function"
4975 " (right of open paren)",
4976 method="textDocument/signatureHelp",
4977 params={
4978 "textDocument": {"uri": "${php_file_uri}"},
4979 "position": {"line": 19, "character": 18},
4981 result={
4982 "signatures": [
4984 "label": "function global_function"
4985 "(string $s, int $x): void",
4986 "documentation": "Global function with doc block",
4987 "parameters": [{"label": "$s"}, {"label": "$x"}],
4990 "activeSignature": 0,
4991 "activeParameter": 0,
4994 .request(
4995 line=line(),
4996 comment="signature help for 1-argument namespace-aliased global"
4997 " function (right of open paren)",
4998 method="textDocument/signatureHelp",
4999 params={
5000 "textDocument": {"uri": "${php_file_uri}"},
5001 "position": {"line": 20, "character": 26},
5003 result=None,
5005 .request(
5006 line=line(),
5007 comment="signature help for 1-argument namespace-aliased global"
5008 " function (right of open paren)",
5009 method="textDocument/signatureHelp",
5010 params={
5011 "textDocument": {"uri": "${php_file_uri}"},
5012 "position": {"line": 20, "character": 26},
5014 result=None,
5016 .request(
5017 line=line(),
5018 comment="signature help for 1-argument namespace-aliased global"
5019 " function (right of open paren)",
5020 method="textDocument/signatureHelp",
5021 params={
5022 "textDocument": {"uri": "${php_file_uri}"},
5023 "position": {"line": 20, "character": 27},
5025 result={
5026 "signatures": [
5028 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
5029 "documentation": "Namespace-aliased function with doc block",
5030 "parameters": [{"label": "$s"}],
5033 "activeSignature": 0,
5034 "activeParameter": 0,
5037 .request(
5038 line=line(),
5039 comment="signature help for 1-argument namespace-aliased global"
5040 " function (right of open paren)",
5041 method="textDocument/signatureHelp",
5042 params={
5043 "textDocument": {"uri": "${php_file_uri}"},
5044 "position": {"line": 20, "character": 28},
5046 result={
5047 "signatures": [
5049 "label": "function Derp\\Lib\\Herp\\aliased_global_func(string $s): void",
5050 "documentation": "Namespace-aliased function with doc block",
5051 "parameters": [{"label": "$s"}],
5054 "activeSignature": 0,
5055 "activeParameter": 0,
5058 .request(
5059 line=line(),
5060 comment="signature help for 2-argument function with params"
5061 " (right of open paren)",
5062 method="textDocument/signatureHelp",
5063 params={
5064 "textDocument": {"uri": "${php_file_uri}"},
5065 "position": {"line": 21, "character": 30},
5067 result={
5068 "signatures": [
5070 "label": "function test_signature_help_params1("
5071 "\n string $param1,\n string $param2\n): void",
5072 "documentation": "comment describing the method"
5073 "\n@param $param1 info1"
5074 "\n@param param2 info2",
5075 "parameters": [
5076 {"label": "$param1", "documentation": "info1"},
5077 {"label": "$param2", "documentation": "info2"},
5081 "activeSignature": 0,
5082 "activeParameter": 0,
5085 .request(
5086 line=line(),
5087 comment="signature help for 2-argument function with params"
5088 " (right of open paren)",
5089 method="textDocument/signatureHelp",
5090 params={
5091 "textDocument": {"uri": "${php_file_uri}"},
5092 "position": {"line": 22, "character": 30},
5094 result={
5095 "signatures": [
5097 "label": "function test_signature_help_params2("
5098 "\n string $param1,\n string $param2\n): void",
5099 "documentation": "comment describing the method"
5100 "\n@param $param1 info1",
5101 "parameters": [
5102 {"label": "$param1", "documentation": "info1"},
5103 {"label": "$param2"},
5107 "activeSignature": 0,
5108 "activeParameter": 0,
5111 .request(
5112 line=line(),
5113 comment="signature help for 2-argument function with params"
5114 " (right of open paren)",
5115 method="textDocument/signatureHelp",
5116 params={
5117 "textDocument": {"uri": "${php_file_uri}"},
5118 "position": {"line": 23, "character": 30},
5120 result={
5121 "signatures": [
5123 "label": "function test_signature_help_params3("
5124 "\n string $param1,\n string $param2\n): string",
5125 "documentation": "@param $param1 info1"
5126 "\n for param1"
5127 "\n@param $param2 info2"
5128 "\n@return the string"
5129 "\n 'hack'",
5130 "parameters": [
5132 "label": "$param1",
5133 "documentation": "info1 for param1",
5135 {"label": "$param2", "documentation": "info2"},
5139 "activeSignature": 0,
5140 "activeParameter": 0,
5143 .request(line=line(), method="shutdown", params={}, result=None)
5144 .notification(method="exit", params={})
5146 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5148 def test_signature_help_lambda(self) -> None:
5149 self.prepare_server_environment()
5150 variables = self.setup_php_file("signaturehelp_lambda.php")
5151 spec = (
5152 self.initialize_spec(
5153 LspTestSpec("test_serverless_ide_signature_help_lambda"),
5154 use_serverless_ide=False,
5156 .wait_for_hh_server_ready()
5157 .notification(
5158 method="textDocument/didOpen",
5159 params={
5160 "textDocument": {
5161 "uri": "${php_file_uri}",
5162 "languageId": "hack",
5163 "version": 1,
5164 "text": "${php_file}",
5168 .request(
5169 line=line(),
5170 comment="signature help for a normal function call",
5171 method="textDocument/signatureHelp",
5172 params={
5173 "textDocument": {"uri": "${php_file_uri}"},
5174 "position": {"line": 8, "character": 29},
5176 result={
5177 "activeParameter": 0,
5178 "activeSignature": 0,
5179 "signatures": [
5181 "label": "function test_lambda_sighelp(\n"
5182 " string $str,\n"
5183 " (function(string): int) $f\n"
5184 "): int",
5185 "parameters": [{"label": "$str"}, {"label": "$f"}],
5190 .request(
5191 line=line(),
5192 comment="signature help for normal function call within a lambda",
5193 method="textDocument/signatureHelp",
5194 params={
5195 "textDocument": {"uri": "${php_file_uri}"},
5196 "position": {"line": 9, "character": 21},
5198 result={
5199 "activeParameter": 0,
5200 "activeSignature": 0,
5201 "signatures": [
5203 "label": "function normal_test_func(string $str): void",
5204 "parameters": [{"label": "$str"}],
5209 .request(
5210 line=line(),
5211 comment="signature help for text within a lambda, left side of an open paren",
5212 method="textDocument/signatureHelp",
5213 params={
5214 "textDocument": {"uri": "${php_file_uri}"},
5215 "position": {"line": 10, "character": 15},
5217 result=None,
5219 .request(
5220 line=line(),
5221 comment="signature help for text within a lambda, right side of an open paren",
5222 method="textDocument/signatureHelp",
5223 params={
5224 "textDocument": {"uri": "${php_file_uri}"},
5225 "position": {"line": 10, "character": 16},
5227 result=None,
5229 .request(line=line(), method="shutdown", params={}, result=None)
5230 .notification(method="exit", params={})
5232 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5234 def test_rename(self) -> None:
5235 self.prepare_server_environment()
5236 variables = self.setup_php_file("rename.php")
5237 self.load_and_run("rename", variables)
5239 def test_references(self) -> None:
5240 self.prepare_server_environment()
5241 variables = self.setup_php_file("references.php")
5242 self.load_and_run("references", variables)
5244 def test_non_existing_method(self) -> None:
5245 self.prepare_server_environment()
5246 variables = self.setup_php_file("nomethod.php")
5247 self.load_and_run("nomethod", variables)
5249 def test_bad_call(self) -> None:
5250 self.prepare_server_environment()
5251 variables = self.setup_php_file("bad_call.php")
5252 self.load_and_run("bad_call", variables)
5254 def test_code_action_missing_method(self) -> None:
5255 variables = dict(self.prepare_serverless_ide_environment())
5256 variables.update(self.setup_php_file("code_action_missing_method.php"))
5257 self.test_driver.stop_hh_server()
5259 spec = (
5260 self.initialize_spec(
5261 LspTestSpec("code_action_missing_method"), use_serverless_ide=True
5263 .notification(
5264 method="textDocument/didOpen",
5265 params={
5266 "textDocument": {
5267 "uri": "${php_file_uri}",
5268 "languageId": "hack",
5269 "version": 1,
5270 "text": "${php_file}",
5274 .notification(
5275 comment="make local, unsaved change to the file",
5276 method="textDocument/didChange",
5277 params={
5278 "textDocument": {"uri": "${php_file_uri}", "version": 2},
5279 "contentChanges": [
5281 "text": """\
5282 <?hh
5284 class ClassWithFooBar {
5285 public function foobar(): void {}
5288 function call_method(ClassWithFooBar $mc): void {
5289 $mc->foobaz();
5296 .request(
5297 line=line(),
5298 comment="get actions",
5299 method="textDocument/codeAction",
5300 params={
5301 "textDocument": {"uri": "${php_file_uri}"},
5302 "range": {
5303 "start": {"line": 7, "character": 7},
5304 "end": {"line": 7, "character": 13},
5306 "context": {
5307 "diagnostics": [
5309 "range": {
5310 "start": {"line": 7, "character": 7},
5311 "end": {"line": 7, "character": 13},
5313 "severity": 1,
5314 "code": 4053,
5315 "source": "Hack",
5316 "message": "No instance method foobaz in ClassWithFooBar",
5317 "relatedInformation": [
5319 "location": {
5320 "uri": "${php_file_uri}",
5321 "range": {
5322 "start": {"line": 3, "character": 18},
5323 "end": {"line": 3, "character": 24},
5326 "message": "Did you mean foobar instead?",
5329 "location": {
5330 "uri": "${php_file_uri}",
5331 "range": {
5332 "start": {"line": 6, "character": 21},
5333 "end": {"line": 6, "character": 36},
5336 "message": "This is why I think it is an object of type ClassWithFooBar",
5339 "location": {
5340 "uri": "${php_file_uri}",
5341 "range": {
5342 "start": {"line": 2, "character": 6},
5343 "end": {"line": 2, "character": 21},
5346 "message": "Declaration of ClassWithFooBar is here",
5349 "relatedLocations": [
5351 "location": {
5352 "uri": "${php_file_uri}",
5353 "range": {
5354 "start": {"line": 3, "character": 18},
5355 "end": {"line": 3, "character": 24},
5358 "message": "Did you mean foobar instead?",
5361 "location": {
5362 "uri": "${php_file_uri}",
5363 "range": {
5364 "start": {"line": 6, "character": 21},
5365 "end": {"line": 6, "character": 36},
5368 "message": "This is why I think it is an object of type ClassWithFooBar",
5371 "location": {
5372 "uri": "${php_file_uri}",
5373 "range": {
5374 "start": {"line": 2, "character": 6},
5375 "end": {"line": 2, "character": 21},
5378 "message": "Declaration of ClassWithFooBar is here",
5385 result=[
5387 "title": "Change to ->foobar",
5388 "kind": "quickfix",
5389 "diagnostics": [],
5390 "edit": {
5391 "changes": {
5392 "${root_path}/code_action_missing_method.php": [
5394 "range": {
5395 "start": {"line": 7, "character": 7},
5396 "end": {"line": 7, "character": 13},
5398 "newText": "foobar",
5405 powered_by="serverless_ide",
5407 .request(line=line(), method="shutdown", params={}, result=None)
5408 .notification(method="exit", params={})
5410 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5412 def test_non_blocking(self) -> None:
5413 self.prepare_server_environment()
5414 variables = self.setup_php_file("non_blocking.php")
5415 self.test_driver.start_hh_loop_forever_assert_timeout()
5416 spec = (
5417 self.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide=False)
5418 .wait_for_hh_server_ready()
5419 .request(
5420 line=line(),
5421 method="textDocument/definition",
5422 params={
5423 "textDocument": {"uri": "${php_file_uri}"},
5424 "position": {"line": 7, "character": 11},
5426 result=[
5428 "uri": "file://${root_path}/non_blocking.php",
5429 "range": {
5430 "start": {"line": 2, "character": 9},
5431 "end": {"line": 2, "character": 32},
5433 "title": "non_blocking_definition",
5436 wait_id="definition request",
5438 .notification(
5439 comment="remove hh_loop_forever() invocation to break the infinite loop",
5440 method="textDocument/didOpen",
5441 params={
5442 "textDocument": {
5443 "uri": "${root_path}/__hh_loop_forever_foo.php",
5444 "languageId": "hack",
5445 "version": 1,
5446 "text": """\
5447 <?hh // strict
5449 function __hh_loop_forever_foo(): int {
5450 return 4;
5452 """,
5456 .wait_for_response(wait_id="definition request")
5457 .request(line=line(), method="shutdown", params={}, result=None)
5458 .notification(method="exit", params={})
5460 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
5462 def test_serverless_ide_hierarchy_file_change_on_disk(self) -> None:
5463 variables = dict(self.prepare_serverless_ide_environment())
5464 variables.update(self.setup_php_file("incremental_derived.php"))
5465 changed_php_file_uri = self.repo_file("incremental_base.php")
5466 variables.update({"changed_php_file_uri": changed_php_file_uri})
5467 self.test_driver.stop_hh_server()
5469 spec = (
5470 self.initialize_spec(
5471 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
5472 use_serverless_ide=True,
5474 .notification(
5475 method="textDocument/didOpen",
5476 params={
5477 "textDocument": {
5478 "uri": "${php_file_uri}",
5479 "languageId": "hack",
5480 "version": 1,
5481 "text": "${php_file}",
5485 .request(
5486 line=line(),
5487 comment="hover before change to class hierarchy should be `int`",
5488 method="textDocument/hover",
5489 params={
5490 "textDocument": {"uri": "${php_file_uri}"},
5491 "position": {"line": 7, "character": 14},
5493 result={
5494 "contents": [
5495 {"language": "hack", "value": "public function foo(): int"},
5496 "Return type: `int`",
5497 "Full name: `BaseClassIncremental::foo`",
5499 "range": {
5500 "start": {"line": 7, "character": 12},
5501 "end": {"line": 7, "character": 15},
5504 powered_by="serverless_ide",
5506 .write_to_disk(
5507 uri=changed_php_file_uri,
5508 contents="""\
5509 <?hh // strict
5510 class BaseClassIncremental {
5511 public function foo(): string { return ''; }
5513 """,
5514 notify=True,
5516 .request(
5517 line=line(),
5518 comment="hover after change to class hierarchy should be `string`",
5519 method="textDocument/hover",
5520 params={
5521 "textDocument": {"uri": "${php_file_uri}"},
5522 "position": {"line": 7, "character": 14},
5524 result={
5525 "contents": [
5526 {"language": "hack", "value": "public function foo(): string"},
5527 "Return type: `string`",
5528 "Full name: `BaseClassIncremental::foo`",
5530 "range": {
5531 "start": {"line": 7, "character": 12},
5532 "end": {"line": 7, "character": 15},
5535 powered_by="serverless_ide",
5537 .request(line=line(), method="shutdown", params={}, result=None)
5538 .notification(method="exit", params={})
5541 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5543 def test_serverless_ide_decl_in_unsaved_buffer_changed(self) -> None:
5544 variables = dict(self.prepare_serverless_ide_environment())
5545 variables.update(self.setup_php_file("hover.php"))
5546 self.test_driver.stop_hh_server()
5548 spec = (
5549 self.initialize_spec(
5550 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
5551 use_serverless_ide=True,
5553 .notification(
5554 method="textDocument/didOpen",
5555 params={
5556 "textDocument": {
5557 "uri": "${php_file_uri}",
5558 "languageId": "hack",
5559 "version": 1,
5560 "text": "${php_file}",
5564 .request(
5565 line=line(),
5566 comment="hover over function invocation",
5567 method="textDocument/hover",
5568 params={
5569 "textDocument": {"uri": "${php_file_uri}"},
5570 "position": {"line": 3, "character": 16},
5572 result={
5573 "contents": [
5574 {"language": "hack", "value": "int"},
5575 "A comment describing b_hover.",
5577 "range": {
5578 "start": {"line": 3, "character": 9},
5579 "end": {"line": 3, "character": 16},
5582 powered_by="serverless_ide",
5584 .notification(
5585 comment="make local, unsaved change to the file",
5586 method="textDocument/didChange",
5587 params={
5588 "textDocument": {"uri": "${php_file_uri}", "version": 2},
5589 "contentChanges": [
5591 "text": """\
5592 <?hh // strict
5593 // comment
5594 function a_hover(): int {
5595 return b_hover();
5597 // A comment describing b_hover differently.
5598 function b_hover(): string {
5599 return 42;
5606 .request(
5607 line=line(),
5608 comment="another hover over function invocation, should be string now",
5609 method="textDocument/hover",
5610 params={
5611 "textDocument": {"uri": "${php_file_uri}"},
5612 "position": {"line": 3, "character": 16},
5614 result={
5615 "contents": [
5616 {"language": "hack", "value": "string"},
5617 "A comment describing b_hover differently.",
5619 "range": {
5620 "start": {"line": 3, "character": 9},
5621 "end": {"line": 3, "character": 16},
5624 powered_by="serverless_ide",
5626 .request(line=line(), method="shutdown", params={}, result=None)
5627 .notification(method="exit", params={})
5630 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5632 def test_serverless_ide_decl_two_unsaved_buffers(self) -> None:
5633 variables = dict(self.prepare_serverless_ide_environment())
5634 variables.update(self.setup_php_file("unsaved1.php"))
5635 variables.update({"unsaved2_file_uri": self.repo_file_uri("unsaved2.php")})
5636 self.test_driver.stop_hh_server()
5638 spec = (
5639 self.initialize_spec(
5640 LspTestSpec("test_serverless_ide_decl_two_unsaved_buffers"),
5641 use_serverless_ide=True,
5643 .notification(
5644 comment="open 'unsaved1.php', since we'll be hovering in it",
5645 method="textDocument/didOpen",
5646 params={
5647 "textDocument": {
5648 "uri": "${php_file_uri}",
5649 "languageId": "hack",
5650 "version": 1,
5651 "text": "${php_file}",
5655 .notification(
5656 comment="open 'unsaved2.php' with a bool-returning signature, different from disk",
5657 method="textDocument/didOpen",
5658 params={
5659 "textDocument": {
5660 "uri": "${unsaved2_file_uri}",
5661 "languageId": "hack",
5662 "version": 1,
5663 "text": """\
5664 <?hh //strict
5665 function unsaved_bar(): bool { return true; }
5666 """,
5670 .request(
5671 line=line(),
5672 comment="hover 'unsaved1.php' is with respect to disk contents of 'unsaved2.php'",
5673 method="textDocument/hover",
5674 params={
5675 "textDocument": {"uri": "${php_file_uri}"},
5676 "position": {"line": 1, "character": 39},
5678 result={
5679 "contents": [
5680 {"language": "hack", "value": "function unsaved_bar(): int"},
5681 "Return type: `int`",
5683 "range": {
5684 "start": {"line": 1, "character": 34},
5685 "end": {"line": 1, "character": 45},
5688 powered_by="serverless_ide",
5690 .notification(
5691 comment="change signature in 'unsaved2.php' to return string",
5692 method="textDocument/didChange",
5693 params={
5694 "textDocument": {"uri": "${unsaved2_file_uri}", "version": 2},
5695 "contentChanges": [
5697 "text": """\
5698 <?hh //strict
5699 function unsaved_bar(): string { return "hello"; }
5705 .request(
5706 line=line(),
5707 comment="this is a dummy hover in 'unsaved2.php' just to ensure its decl is cached",
5708 method="textDocument/hover",
5709 params={
5710 "textDocument": {"uri": "${unsaved2_file_uri}"},
5711 "position": {"line": 0, "character": 0},
5713 result=None,
5714 powered_by="serverless_ide",
5716 .request(
5717 line=line(),
5718 comment="hover 'unsaved1.php' is still with respect to disk contents of 'unsaved2.php'",
5719 method="textDocument/hover",
5720 params={
5721 "textDocument": {"uri": "${php_file_uri}"},
5722 "position": {"line": 1, "character": 39},
5724 result={
5725 "contents": [
5726 {"language": "hack", "value": "function unsaved_bar(): int"},
5727 "Return type: `int`",
5729 "range": {
5730 "start": {"line": 1, "character": 34},
5731 "end": {"line": 1, "character": 45},
5734 powered_by="serverless_ide",
5736 .write_to_disk(
5737 comment="save signature in 'unsaved2' to return string",
5738 uri=variables["unsaved2_file_uri"],
5739 contents="""\
5740 <?hh // strict
5741 function unsaved_bar(): string { return "hello"; }
5742 """,
5743 notify=True,
5745 .request(
5746 line=line(),
5747 comment="hover 'unsaved1.php' gets new disk contents of 'unsaved2.php'",
5748 method="textDocument/hover",
5749 params={
5750 "textDocument": {"uri": "${php_file_uri}"},
5751 "position": {"line": 1, "character": 39},
5753 result={
5754 "contents": [
5755 {"language": "hack", "value": "function unsaved_bar(): string"},
5756 "Return type: `string`",
5758 "range": {
5759 "start": {"line": 1, "character": 34},
5760 "end": {"line": 1, "character": 45},
5763 powered_by="serverless_ide",
5765 .request(line=line(), method="shutdown", params={}, result=None)
5766 .notification(method="exit", params={})
5769 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5771 def test_hover_without_file_open(self) -> None:
5772 variables = dict(self.prepare_serverless_ide_environment())
5773 variables.update(self.setup_php_file("hover.php"))
5774 self.test_driver.stop_hh_server()
5776 spec = (
5777 self.initialize_spec(
5778 LspTestSpec("test_hover_without_file_open"),
5779 use_serverless_ide=True,
5780 supports_status=True,
5782 .ignore_notifications(method="textDocument/publishDiagnostics")
5783 .ignore_requests(
5784 comment="Ignore 'initializing...' messages since they're racy",
5785 method="window/showStatus",
5786 params={
5787 "type": 2,
5788 "actions": [{"title": "Restart hh_server"}],
5789 "message": "Hack IDE: initializing.\nhh_server: stopped.",
5790 "shortMessage": "Hack: initializing",
5793 .ignore_requests(
5794 comment="another racy initializing, before hh_server has even responded",
5795 method="window/showStatus",
5796 params={
5797 "type": 2,
5798 "actions": [],
5799 "message": "Hack IDE: initializing.",
5800 "shortMessage": "Hack: initializing",
5803 .ignore_requests(
5804 comment="another racy initialization to ignore, again before hh_server",
5805 method="window/showStatus",
5806 params={
5807 "type": 3,
5808 "actions": [],
5809 "message": "Hack IDE: ready.",
5810 "shortMessage": "Hack: ready",
5813 .wait_for_server_request(
5814 method="window/showStatus",
5815 params={
5816 "actions": [{"title": "Restart hh_server"}],
5817 "message": "Hack IDE: ready.\nhh_server: stopped.",
5818 "shortMessage": "Hack: ready",
5819 "type": 3,
5821 result=NoResponse(),
5823 .request(
5824 line=line(),
5825 comment="hover before file_open will fail",
5826 method="textDocument/hover",
5827 params={
5828 "textDocument": {"uri": "${php_file_uri}"},
5829 "position": {"line": 26, "character": 20},
5831 result=None,
5833 .notification(
5834 method="textDocument/didOpen",
5835 params={
5836 "textDocument": {
5837 "uri": "${php_file_uri}",
5838 "languageId": "hack",
5839 "version": 1,
5840 "text": "${php_file}",
5844 .request(
5845 line=line(),
5846 comment="hover after file_open will succeed",
5847 method="textDocument/hover",
5848 params={
5849 "textDocument": {"uri": "${php_file_uri}"},
5850 "position": {"line": 26, "character": 20},
5852 result={"contents": [{"language": "hack", "value": "string"}]},
5853 powered_by="serverless_ide",
5855 .request(
5856 line=line(),
5857 method="$test/shutdownServerlessIde",
5858 params={},
5859 result=None,
5860 powered_by="serverless_ide",
5862 .wait_for_server_request(
5863 method="window/showStatus",
5864 params={
5865 "actions": [
5866 {"title": "Restart Hack IDE"},
5867 {"title": "Restart hh_server"},
5869 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: stopped.",
5870 "shortMessage": "Hack: failed",
5871 "type": 1,
5873 result={"title": "Restart Hack IDE"},
5875 .wait_for_server_request(
5876 method="window/showStatus",
5877 params={
5878 "actions": [{"title": "Restart hh_server"}],
5879 "message": "Hack IDE: ready.\nhh_server: stopped.",
5880 "shortMessage": "Hack: ready",
5881 "type": 3,
5883 result=NoResponse(),
5885 .request(
5886 line=line(),
5887 comment="hover after restart will succeed",
5888 method="textDocument/hover",
5889 params={
5890 "textDocument": {"uri": "${php_file_uri}"},
5891 "position": {"line": 26, "character": 20},
5893 result={"contents": [{"language": "hack", "value": "string"}]},
5894 powered_by="serverless_ide",
5896 .notification(
5897 method="textDocument/didClose",
5898 params={"textDocument": {"uri": "${php_file_uri}"}},
5900 .request(
5901 line=line(),
5902 comment="hover after file_close will fail",
5903 method="textDocument/hover",
5904 params={
5905 "textDocument": {"uri": "${php_file_uri}"},
5906 "position": {"line": 26, "character": 20},
5908 result=None,
5910 .request(line=line(), method="shutdown", params={}, result=None)
5911 .notification(method="exit", params={})
5914 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
5916 def test_hh_server_status_diagnostic(self) -> None:
5917 variables = dict(self.prepare_serverless_ide_environment())
5918 variables.update(self.setup_php_file("unsaved1.php"))
5919 variables.update(
5921 "unsaved2_file_uri": self.repo_file_uri("unsaved2.php"),
5922 "unsaved2_file": self.read_repo_file("unsaved2.php"),
5925 self.test_driver.stop_hh_server()
5927 spec = (
5928 self.initialize_spec(
5929 LspTestSpec("test_hh_server_status_diagnostic"), use_serverless_ide=True
5931 .ignore_status_diagnostics(False)
5932 .notification(
5933 method="textDocument/didOpen",
5934 params={
5935 "textDocument": {
5936 "uri": "${php_file_uri}",
5937 "languageId": "hack",
5938 "version": 1,
5939 "text": "${php_file}",
5943 .wait_for_notification(
5944 comment="After didOpen(file1), the hh_server_status diagnostic should appear in file1",
5945 method="textDocument/publishDiagnostics",
5946 params={
5947 "uri": "${php_file_uri}",
5948 "diagnostics": [
5950 "range": {
5951 "start": {"line": 0, "character": 0},
5952 "end": {"line": 0, "character": 1},
5954 "severity": 1,
5955 "source": "hh_server",
5956 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5957 "relatedInformation": [],
5958 "relatedLocations": [],
5961 "isStatusFB": True,
5964 .notification(
5965 method="textDocument/didOpen",
5966 params={
5967 "textDocument": {
5968 "uri": "${unsaved2_file_uri}",
5969 "languageId": "hack",
5970 "version": 1,
5971 "text": "${unsaved2_file}",
5975 .wait_for_notification(
5976 comment="After didOpen(file2), the hh_server_status diagnostic should disappear from file1",
5977 method="textDocument/publishDiagnostics",
5978 params={
5979 "uri": "${php_file_uri}",
5980 "diagnostics": [],
5981 "isStatusFB": True,
5984 .wait_for_notification(
5985 comment="After didOpen(file2), the hh_server_status diagnostic should reappear in file2",
5986 method="textDocument/publishDiagnostics",
5987 params={
5988 "uri": "${unsaved2_file_uri}",
5989 "diagnostics": [
5991 "range": {
5992 "start": {"line": 0, "character": 0},
5993 "end": {"line": 0, "character": 1},
5995 "severity": 1,
5996 "source": "hh_server",
5997 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
5998 "relatedInformation": [],
5999 "relatedLocations": [],
6002 "isStatusFB": True,
6005 .notification(
6006 method="textDocument/didClose",
6007 params={"textDocument": {"uri": "${unsaved2_file_uri}"}},
6009 .wait_for_notification(
6010 comment="After didClose(file2), the hh_server_status diagnostic should disappear from file2",
6011 method="textDocument/publishDiagnostics",
6012 params={
6013 "uri": "${unsaved2_file_uri}",
6014 "diagnostics": [],
6015 "isStatusFB": True,
6018 .wait_for_notification(
6019 comment="After didClose(file2), the hh_server_status diagnostic should reappear in file1",
6020 method="textDocument/publishDiagnostics",
6021 params={
6022 "uri": "${php_file_uri}",
6023 "diagnostics": [
6025 "range": {
6026 "start": {"line": 0, "character": 0},
6027 "end": {"line": 0, "character": 1},
6029 "severity": 1,
6030 "source": "hh_server",
6031 "message": "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... hh_server: stopped.",
6032 "relatedInformation": [],
6033 "relatedLocations": [],
6036 "isStatusFB": True,
6039 .notification(
6040 method="textDocument/didClose",
6041 params={"textDocument": {"uri": "${php_file_uri}"}},
6043 .wait_for_notification(
6044 comment="After didClose(file1), the hh_server_status diagnostic should disappear from file1",
6045 method="textDocument/publishDiagnostics",
6046 params={
6047 "uri": "${php_file_uri}",
6048 "diagnostics": [],
6049 "isStatusFB": True,
6052 .request(line=line(), method="shutdown", params={}, result=None)
6053 .notification(method="exit", params={})
6056 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6058 def _sanitize_gutter_line_numbers(self, s: str) -> str:
6059 gutter_line_number_re = re.compile(r"^[ ]*[0-9]+ \|", re.MULTILINE)
6060 return re.sub(gutter_line_number_re, " XXXX |", s)
6062 def test_lsptestspec_incorrect_request_result(self) -> None:
6063 variables = dict(self.prepare_serverless_ide_environment())
6064 variables.update(self.setup_php_file("hover.php"))
6065 self.test_driver.stop_hh_server()
6067 spec = (
6068 self.initialize_spec(
6069 LspTestSpec("test_lsptestspec_incorrect_request_result"),
6070 use_serverless_ide=True,
6072 .notification(
6073 method="textDocument/didOpen",
6074 params={
6075 "textDocument": {
6076 "uri": "${php_file_uri}",
6077 "languageId": "hack",
6078 "version": 1,
6079 "text": "${php_file}",
6083 .request(
6084 line=line(),
6085 comment="hover over function invocation",
6086 method="textDocument/hover",
6087 params={
6088 "textDocument": {"uri": "${php_file_uri}"},
6089 "position": {"line": 3, "character": 16},
6091 result={
6092 "contents": [
6093 {"language": "hack", "value": "int"},
6094 "INCORRECT COMMENT HERE",
6096 "range": {
6097 "start": {"line": 3, "character": 9},
6098 "end": {"line": 3, "character": 16},
6101 powered_by="serverless_ide",
6103 .request(line=line(), method="shutdown", params={}, result=None)
6104 .notification(method="exit", params={})
6106 try:
6107 self.run_spec(
6108 spec,
6109 variables=variables,
6110 wait_for_server=False,
6111 use_serverless_ide=True,
6113 raise AssertionError("Expected an error here")
6114 except AssertionError as e:
6115 self.assertEqual(
6116 self._sanitize_gutter_line_numbers(str(e)),
6117 """\
6118 Test case test_lsptestspec_incorrect_request_result failed with 1 errors:
6120 Error 1/1:
6121 Description: Request with ID 5 (comment: 'hover over function invocation') \
6122 got an incorrect result:
6124 (- is expected lines, + is actual lines)
6125 - {'contents': [{'language': 'hack', 'value': 'int'}, 'INCORRECT COMMENT HERE'],
6126 ? ---------------------------
6128 + {'contents': [{'language': 'hack', 'value': 'int'},
6129 + 'A comment describing b_hover.'],
6130 'range': {'end': {'character': 16, 'line': 3},
6131 'start': {'character': 9, 'line': 3}}}
6133 Context:
6134 This was the associated request:
6136 hphp/hack/test/integration/test_lsp.py
6137 XXXX | .request(
6138 XXXX | line=line(),
6139 XXXX | comment="hover over function invocation",
6140 XXXX | method="textDocument/hover",
6141 XXXX | params={
6142 XXXX | "textDocument": {"uri": "${php_file_uri}"},
6143 XXXX | "position": {"line": 3, "character": 16},
6144 XXXX | },
6145 XXXX | result={
6146 XXXX | "contents": [
6147 XXXX | {"language": "hack", "value": "int"},
6148 XXXX | "INCORRECT COMMENT HERE",
6149 XXXX | ],
6150 XXXX | "range": {
6151 XXXX | "start": {"line": 3, "character": 9},
6152 XXXX | "end": {"line": 3, "character": 16},
6153 XXXX | },
6154 XXXX | },
6155 XXXX | powered_by="serverless_ide",
6156 XXXX | )
6158 Remediation:
6159 1) If this was unexpected, then the language server is buggy and should be
6160 fixed.
6162 2) If this was expected, you can update your request with the following code to
6163 make it match:
6165 .request(
6166 line=line(),
6167 comment='hover over function invocation',
6168 method='textDocument/hover',
6169 params={'textDocument': {'uri': '${php_file_uri}'}, \
6170 'position': {'line': 3, 'character': 16}},
6171 result={'contents': [{'language': 'hack', 'value': 'int'}, \
6172 'A comment describing b_hover.'], \
6173 'range': {'start': {'line': 3, 'character': 9}, \
6174 'end': {'line': 3, 'character': 16}}},
6175 powered_by='serverless_ide',
6178 If you want to examine the raw LSP logs, you can check the `.sent.log` and
6179 `.received.log` files that were generated in the template repo for this test.\
6180 """,
6183 def test_lsptestspec_unexpected_notification(self) -> None:
6184 self.prepare_server_environment()
6185 variables = self.setup_php_file("didchange.php")
6186 spec = (
6187 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
6188 .wait_for_hh_server_ready()
6189 .notification(
6190 method="textDocument/didOpen",
6191 params={
6192 "textDocument": {
6193 "uri": "${php_file_uri}",
6194 "languageId": "hack",
6195 "version": 1,
6196 "text": "${php_file}",
6200 .notification(
6201 method="textDocument/didChange",
6202 params={
6203 "textDocument": {"uri": "${php_file_uri}"},
6204 "contentChanges": [
6206 "range": {
6207 "start": {"line": 7, "character": 11},
6208 "end": {"line": 7, "character": 12},
6210 "text": "a",
6215 .wait_for_notification(
6216 method="textDocument/publishDiagnostics",
6217 params={
6218 "uri": "${php_file_uri}",
6219 "diagnostics": [
6221 "range": {
6222 "start": {"line": 7, "character": 11},
6223 "end": {"line": 7, "character": 11},
6225 "severity": 1,
6226 "code": 1002,
6227 "source": "Hack",
6228 "message": "A semicolon ; is expected here.",
6229 "relatedLocations": [],
6230 "relatedInformation": [],
6235 .request(line=line(), method="shutdown", params={}, result=None)
6236 .notification(method="exit", params={})
6238 try:
6239 self.run_spec(
6240 spec, variables, wait_for_server=True, use_serverless_ide=False
6242 raise AssertionError("Expected an error here")
6243 except AssertionError as e:
6244 self.assertEqual(
6245 self._sanitize_gutter_line_numbers(str(e)),
6246 """\
6247 Test case did_change failed with 1 errors:
6249 Error 1/1:
6250 Description: An unexpected notification of type \
6251 'textDocument/publishDiagnostics' was sent by the language server.
6252 Here is the notification payload:
6254 {'jsonrpc': '2.0',
6255 'method': 'textDocument/publishDiagnostics',
6256 'params': {'diagnostics': [],
6257 'uri': '__PHP_FILE_URI__'}}
6259 Context:
6260 This was the most recent request issued from the language client before it
6261 received the notification:
6263 hphp/hack/test/integration/test_lsp.py
6264 XXXX | .request(line=line(), method="shutdown", params={}, result=None)
6266 Remediation:
6267 1) If this was unexpected, then the language server is buggy and should be
6268 fixed.
6270 2) If all notifications of type 'textDocument/publishDiagnostics' should be \
6271 ignored, add this directive
6272 anywhere in your test:
6274 .ignore_notifications(method='textDocument/publishDiagnostics')
6276 3) If this single instance of the notification was expected, add this directive
6277 to your test to wait for it before proceeding:
6279 .wait_for_notification(
6280 method='textDocument/publishDiagnostics',
6281 params={'uri': '${php_file_uri}', 'diagnostics': []},
6284 If you want to examine the raw LSP logs, you can check the `.sent.log` and
6285 `.received.log` files that were generated in the template repo for this test.\
6287 # There's an instance of a literal `${php_file_uri}` in there
6288 # which we don't want to change, so use a different name than
6289 # that one.
6290 .replace("__PHP_FILE_URI__", variables["php_file_uri"]),
6293 def test_serverless_ide_highlight(self) -> None:
6294 variables = dict(self.prepare_serverless_ide_environment())
6295 variables.update(self.setup_php_file("highlight.php"))
6296 self.test_driver.stop_hh_server()
6298 spec = (
6299 self.initialize_spec(
6300 LspTestSpec("serverless_ide_highlight"), use_serverless_ide=True
6302 .notification(
6303 method="textDocument/didOpen",
6304 params={
6305 "textDocument": {
6306 "uri": "${php_file_uri}",
6307 "languageId": "hack",
6308 "version": 1,
6309 "text": "${php_file}",
6313 .request(
6314 line=line(),
6315 comment="document highlight, id 2",
6316 method="textDocument/documentHighlight",
6317 params={
6318 "textDocument": {"uri": "${php_file_uri}"},
6319 "position": {"line": 3, "character": 10},
6321 result=[
6323 "range": {
6324 "start": {"line": 3, "character": 9},
6325 "end": {"line": 3, "character": 20},
6329 powered_by="serverless_ide",
6331 .request(
6332 line=line(),
6333 comment="shutdown, id 3",
6334 method="shutdown",
6335 params={},
6336 result=None,
6339 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6341 def test_serverless_ide_coverage(self) -> None:
6342 variables = dict(self.prepare_serverless_ide_environment())
6343 variables.update(self.setup_php_file("coverage.php"))
6344 self.test_driver.stop_hh_server()
6346 spec = (
6347 self.initialize_spec(
6348 LspTestSpec("serverless_ide_coverage"), use_serverless_ide=True
6350 .notification(
6351 method="textDocument/didOpen",
6352 params={
6353 "textDocument": {
6354 "uri": "${php_file_uri}",
6355 "languageId": "hack",
6356 "version": 1,
6357 "text": "${php_file}",
6361 .request(
6362 line=line(),
6363 comment="Check type coverage",
6364 method="textDocument/typeCoverage",
6365 params={"textDocument": {"uri": "${php_file_uri}"}},
6366 result={
6367 "coveredPercent": 100,
6368 "uncoveredRanges": [],
6369 "defaultMessage": "Un-type checked code. Consider adding type annotations.",
6371 powered_by="serverless_ide",
6373 .request(
6374 line=line(),
6375 comment="Shutdown",
6376 method="shutdown",
6377 params={},
6378 result=None,
6381 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6383 def test_status_stopped(self) -> None:
6384 self.prepare_server_environment()
6385 variables = self.setup_php_file("hover.php")
6386 self.test_driver.stop_hh_server()
6388 spec = (
6389 self.initialize_spec(
6390 LspTestSpec("status_stopped"),
6391 use_serverless_ide=False,
6392 supports_status=True,
6394 .wait_for_server_request(
6395 method="window/showStatus",
6396 params={
6397 "shortMessage": "Hack: stopped",
6398 "message": "hh_server: stopped.",
6399 "actions": [{"title": "Restart hh_server"}],
6400 "type": 1,
6402 result=NoResponse(),
6404 .request(line=line(), method="shutdown", params={}, result=None)
6405 .notification(method="exit", params={})
6407 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=False)
6409 def test_status_running(self) -> None:
6410 self.prepare_server_environment()
6411 variables = self.setup_php_file("hover.php")
6413 spec = (
6414 self.initialize_spec(
6415 LspTestSpec("status_running"),
6416 use_serverless_ide=False,
6417 supports_status=True,
6419 .ignore_requests(
6420 comment="Ignore initializing... requests since they're racy",
6421 method="window/showStatus",
6422 params={
6423 "type": 2,
6424 "shortMessage": "Hack: initializing",
6425 "message": "hh_server initializing: processing [<test> seconds]",
6426 "actions": [],
6429 .wait_for_server_request(
6430 method="window/showStatus",
6431 params={"actions": [], "message": "hh_server: ready.", "type": 3},
6432 result=NoResponse(),
6434 .request(line=line(), method="shutdown", params={}, result=None)
6435 .notification(method="exit", params={})
6437 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
6439 def test_serverless_ide_status_stopped(self) -> None:
6440 variables = dict(self.prepare_serverless_ide_environment())
6441 variables.update(self.setup_php_file("hover.php"))
6442 self.test_driver.stop_hh_server()
6444 spec = (
6445 self.initialize_spec(
6446 LspTestSpec("serverless_ide_status_stopped"),
6447 use_serverless_ide=True,
6448 supports_status=True,
6450 .ignore_requests(
6451 comment="ignore initializing... messages since they're kind of racy",
6452 method="window/showStatus",
6453 params={
6454 "type": 2,
6455 "actions": [{"title": "Restart hh_server"}],
6456 "message": "Hack IDE: initializing.\nhh_server: stopped.",
6457 "shortMessage": "Hack: initializing",
6460 .ignore_requests(
6461 comment="another racy initialization to ignore, before hh_server has even reported its status",
6462 method="window/showStatus",
6463 params={
6464 "type": 2,
6465 "actions": [],
6466 "message": "Hack IDE: initializing.",
6467 "shortMessage": "Hack: initializing",
6470 .ignore_requests(
6471 comment="another racy initialization to ignore, again before hh_server",
6472 method="window/showStatus",
6473 params={
6474 "type": 3,
6475 "actions": [],
6476 "message": "Hack IDE: ready.",
6477 "shortMessage": "Hack: ready",
6480 .wait_for_server_request(
6481 method="window/showStatus",
6482 params={
6483 "message": "Hack IDE: ready.\nhh_server: stopped.",
6484 "shortMessage": "Hack: ready",
6485 "actions": [{"title": "Restart hh_server"}],
6486 "type": 3,
6488 result=NoResponse(),
6490 .request(line=line(), method="shutdown", params={}, result=None)
6491 .notification(method="exit", params={})
6493 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
6495 def test_serverless_ide_status_restart(self) -> None:
6496 variables = dict(self.prepare_serverless_ide_environment())
6497 variables.update(self.setup_php_file("hover.php"))
6499 spec = (
6500 self.initialize_spec(
6501 LspTestSpec("serverless_ide_status_restart"),
6502 use_serverless_ide=True,
6503 supports_status=True,
6505 .ignore_requests(
6506 comment="Ignore initializing messages since they're racy",
6507 method="window/showStatus",
6508 params={
6509 "type": 2,
6510 "actions": [],
6511 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
6512 "shortMessage": "Hack: initializing",
6515 .ignore_requests(
6516 comment="Another form of initializing to ignore",
6517 method="window/showStatus",
6518 params={
6519 "type": 2,
6520 "actions": [],
6521 "message": "Hack IDE: initializing.\nhh_server: ready.",
6522 "shortMessage": "Hack: initializing",
6525 .ignore_requests(
6526 comment="Another form of initializing to ignore before we've even heard the first peep from hh_server",
6527 method="window/showStatus",
6528 params={
6529 "type": 2,
6530 "actions": [],
6531 "message": "Hack IDE: initializing.",
6532 "shortMessage": "Hack: initializing",
6535 .ignore_requests(
6536 comment="another racy initialization to ignore, again before hh_server",
6537 method="window/showStatus",
6538 params={
6539 "type": 3,
6540 "actions": [],
6541 "message": "Hack IDE: ready.",
6542 "shortMessage": "Hack: ready",
6545 .wait_for_server_request(
6546 method="window/showStatus",
6547 params={
6548 "actions": [],
6549 "message": "Hack IDE: ready.\nhh_server: ready.",
6550 "shortMessage": "Hack: ready",
6551 "type": 3,
6553 result=NoResponse(),
6555 .request(
6556 line=line(),
6557 method="$test/shutdownServerlessIde",
6558 params={},
6559 result=None,
6560 powered_by="serverless_ide",
6562 .wait_for_server_request(
6563 method="window/showStatus",
6564 params={
6565 "actions": [{"title": "Restart Hack IDE"}],
6566 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6567 "shortMessage": "Hack: failed",
6568 "type": 1,
6570 result={"title": "Restart Hack IDE"},
6572 .wait_for_server_request(
6573 method="window/showStatus",
6574 params={
6575 "actions": [],
6576 "message": "Hack IDE: ready.\nhh_server: ready.",
6577 "shortMessage": "Hack: ready",
6578 "type": 3,
6580 result=NoResponse(),
6582 .request(line=line(), method="shutdown", params={}, result=None)
6583 .notification(method="exit", params={})
6585 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6587 def test_serverless_ide_failed_to_load_saved_state(self) -> None:
6588 variables = dict(self.prepare_serverless_ide_environment())
6589 variables.update(self.setup_php_file("hover.php"))
6590 assert "naming_table_saved_state_path" in variables
6591 variables["naming_table_saved_state_path"] = "/tmp/nonexistent"
6593 spec = (
6594 self.initialize_spec(
6595 LspTestSpec("serverless_ide_status_failed_to_load_saved_state"),
6596 use_serverless_ide=True,
6597 supports_status=True,
6598 supports_init=True,
6600 .ignore_requests(
6601 comment="Ignore initializing since they're kind of racy",
6602 method="window/showStatus",
6603 params={
6604 "type": 2,
6605 "actions": [],
6606 "message": "Hack IDE: initializing.\nhh_server initializing: processing [<test> seconds]",
6607 "shortMessage": "Hack: initializing",
6610 .ignore_requests(
6611 comment="Ignore another form of initializing",
6612 method="window/showStatus",
6613 params={
6614 "type": 2,
6615 "actions": [],
6616 "message": "Hack IDE: initializing.\nhh_server: ready.",
6617 "shortMessage": "Hack: initializing",
6620 .ignore_requests(
6621 comment="Ignore another form of initializing, from before we've even heard the first peep out of hh_server",
6622 method="window/showStatus",
6623 params={
6624 "type": 2,
6625 "actions": [],
6626 "message": "Hack IDE: initializing.",
6627 "shortMessage": "Hack: initializing",
6630 .ignore_requests(
6631 comment="Ignore another form of initializing, again before hh_server",
6632 method="window/showStatus",
6633 params={
6634 "type": 1,
6635 "actions": [{"title": "Restart Hack IDE"}],
6636 "message": "Hack IDE has failed. See Output›Hack for details.",
6637 "shortMessage": "Hack: failed",
6640 .wait_for_notification(
6641 method="window/logMessage",
6642 params={
6643 "type": 1,
6644 "message": "Hack IDE has failed.\nThis is unexpected.\nPlease file a bug within your IDE.\nMore details: http://dummy/HH_TEST_MODE",
6647 .wait_for_server_request(
6648 method="window/showStatus",
6649 params={
6650 "actions": [{"title": "Restart Hack IDE"}],
6651 "message": "Hack IDE has failed. See Output›Hack for details.\nhh_server: ready.",
6652 "shortMessage": "Hack: failed",
6653 "type": 1,
6655 result=NoResponse(),
6657 .request(line=line(), method="shutdown", params={}, result=None)
6658 .notification(method="exit", params={})
6660 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6662 def test_workspace_symbol(self) -> None:
6663 self.prepare_server_environment()
6664 variables = self.setup_php_file("didchange.php")
6665 spec = (
6666 self.initialize_spec(
6667 LspTestSpec("test_workspace_symbol"), use_serverless_ide=False
6669 .wait_for_hh_server_ready()
6670 .request(
6671 line=line(),
6672 comment="Look up symbols",
6673 method="workspace/symbol",
6674 params={"query": "TestNS\\test"},
6675 result=[
6677 "name": "TestNS\\test_func",
6678 "kind": 12,
6679 "location": {
6680 "uri": "file://${root_path}/completion_extras_namespace.php",
6681 "range": {
6682 "start": {"line": 4, "character": 9},
6683 "end": {"line": 4, "character": 25},
6689 .request(
6690 line=line(),
6691 comment="Look up symbols starting with 'test_f' within multiple namespaces",
6692 method="workspace/symbol",
6693 params={"query": "test_f"},
6694 result=[
6696 "name": "test_function",
6697 "kind": 12,
6698 "location": {
6699 "uri": "file://${root_path}/completion.php",
6700 "range": {
6701 "start": {"line": 7, "character": 9},
6702 "end": {"line": 7, "character": 22},
6707 "name": "TestNS\\test_func",
6708 "kind": 12,
6709 "location": {
6710 "uri": "file://${root_path}/completion_extras_namespace.php",
6711 "range": {
6712 "start": {"line": 4, "character": 9},
6713 "end": {"line": 4, "character": 25},
6719 .request(line=line(), method="shutdown", params={}, result=None)
6720 .notification(method="exit", params={})
6722 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
6724 def test_serverless_ide_during_hh_server_restart(self) -> None:
6725 variables = dict(self.prepare_serverless_ide_environment())
6726 variables.update(self.setup_php_file("didchange.php"))
6727 spec = (
6728 self.initialize_spec(
6729 LspTestSpec("test_serverless_ide_during_hh_server_restart"),
6730 use_serverless_ide=True,
6732 .notification(
6733 method="textDocument/didOpen",
6734 params={
6735 "textDocument": {
6736 "uri": "${php_file_uri}",
6737 "languageId": "hack",
6738 "version": 1,
6739 "text": "${php_file}",
6743 .notification(
6744 comment="Send a 'didChange' notification before HH Server is functional.",
6745 method="textDocument/didChange",
6746 params={
6747 "textDocument": {"uri": "${php_file_uri}"},
6748 "contentChanges": [
6750 "range": {
6751 "start": {"line": 7, "character": 9},
6752 "end": {"line": 7, "character": 11},
6754 "text": "'foo'",
6759 .start_hh_server("Start HH Server; should detect the bad edit")
6760 .wait_for_notification(
6761 method="textDocument/publishDiagnostics",
6762 params={
6763 "uri": "${php_file_uri}",
6764 "diagnostics": [
6766 "code": 4110,
6767 "message": "Invalid return type",
6768 "range": {
6769 "end": {"character": 14, "line": 7},
6770 "start": {"character": 9, "line": 7},
6772 "relatedInformation": [
6774 "location": {
6775 "range": {
6776 "end": {"character": 27, "line": 6},
6777 "start": {"character": 24, "line": 6},
6779 "uri": "${php_file_uri}",
6781 "message": "Expected int",
6784 "location": {
6785 "range": {
6786 "end": {"character": 14, "line": 7},
6787 "start": {"character": 9, "line": 7},
6789 "uri": "${php_file_uri}",
6791 "message": "But got string",
6794 "relatedLocations": [
6796 "location": {
6797 "range": {
6798 "end": {"character": 27, "line": 6},
6799 "start": {"character": 24, "line": 6},
6801 "uri": "${php_file_uri}",
6803 "message": "Expected int",
6806 "location": {
6807 "range": {
6808 "end": {"character": 14, "line": 7},
6809 "start": {"character": 9, "line": 7},
6811 "uri": "${php_file_uri}",
6813 "message": "But got string",
6816 "severity": 1,
6817 "source": "Hack",
6822 .stop_hh_server("Shutdown HH Server")
6823 .start_hh_server("Restart HH Server")
6824 .wait_for_notification(
6825 comment="On startup it thinks everything is okay ...",
6826 method="textDocument/publishDiagnostics",
6827 params={"uri": "${php_file_uri}", "diagnostics": []},
6829 .wait_for_notification(
6830 comment="But then hh_server sends a hello message and it gets the edited files, which leads it to see the problem.",
6831 method="textDocument/publishDiagnostics",
6832 params={
6833 "uri": "${php_file_uri}",
6834 "diagnostics": [
6836 "code": 4110,
6837 "message": "Invalid return type",
6838 "range": {
6839 "end": {"character": 14, "line": 7},
6840 "start": {"character": 9, "line": 7},
6842 "relatedInformation": [
6844 "location": {
6845 "range": {
6846 "end": {"character": 27, "line": 6},
6847 "start": {"character": 24, "line": 6},
6849 "uri": "${php_file_uri}",
6851 "message": "Expected int",
6854 "location": {
6855 "range": {
6856 "end": {"character": 14, "line": 7},
6857 "start": {"character": 9, "line": 7},
6859 "uri": "${php_file_uri}",
6861 "message": "But got string",
6864 "relatedLocations": [
6866 "location": {
6867 "range": {
6868 "end": {"character": 27, "line": 6},
6869 "start": {"character": 24, "line": 6},
6871 "uri": "${php_file_uri}",
6873 "message": "Expected int",
6876 "location": {
6877 "range": {
6878 "end": {"character": 14, "line": 7},
6879 "start": {"character": 9, "line": 7},
6881 "uri": "${php_file_uri}",
6883 "message": "But got string",
6886 "severity": 1,
6887 "source": "Hack",
6892 .request(line=line(), method="shutdown", params={}, result=None)
6893 .notification(method="exit", params={})
6895 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=True)
6897 def test_serverless_ide_naming_error1(self) -> None:
6898 variables = dict(self.prepare_serverless_ide_environment())
6899 variables.update(self.setup_php_file("didchange.php"))
6900 variables.update(
6902 "main_file": self.repo_file("main.php"),
6903 "main_file_contents": """\
6904 <?hh
6905 function main(): int {
6906 return aaa();
6908 """,
6909 "file_a": self.repo_file("a.php"),
6910 "file_b": self.repo_file("b.php"),
6913 spec = (
6914 self.initialize_spec(
6915 LspTestSpec("serverless_ide_naming_error1"), use_serverless_ide=True
6917 .write_to_disk(
6918 uri="${main_file}", contents="${main_file_contents}", notify=True
6920 .notification(
6921 method="textDocument/didOpen",
6922 params={
6923 "textDocument": {
6924 "uri": "${main_file}",
6925 "languageId": "hack",
6926 "version": 1,
6927 "text": "${main_file_contents}",
6931 .request(
6932 line=line(),
6933 comment="Ensure that hover over `aaa` works even when the name is not yet defined",
6934 method="textDocument/hover",
6935 params={
6936 "textDocument": {"uri": "${main_file}"},
6937 "position": {"line": 2, "character": 13},
6939 result={
6940 "contents": [{"language": "hack", "value": "_"}],
6941 "range": {
6942 "start": {"line": 2, "character": 11},
6943 "end": {"line": 2, "character": 14},
6946 powered_by="serverless_ide",
6948 .write_to_disk(
6949 comment="create file A",
6950 uri="${file_a}",
6951 contents="""\
6952 <?hh
6953 function aaa(): int {
6954 return 1;
6956 """,
6957 notify=True,
6959 .request(
6960 line=line(),
6961 comment="Ensure that hover over `aaa` works when there are no naming errors",
6962 method="textDocument/hover",
6963 params={
6964 "textDocument": {"uri": "${main_file}"},
6965 "position": {"line": 2, "character": 13},
6967 result={
6968 "contents": [
6969 {"language": "hack", "value": "function aaa(): int"},
6970 "Return type: `int`",
6972 "range": {
6973 "start": {"line": 2, "character": 11},
6974 "end": {"line": 2, "character": 14},
6977 powered_by="serverless_ide",
6979 .write_to_disk(
6980 comment="create file B",
6981 uri="${file_b}",
6982 contents="""\
6983 <?hh
6984 function aaa(): string {
6985 return "foo";
6987 """,
6988 notify=True,
6990 .request(
6991 line=line(),
6992 comment="Ensure that hover over `aaa` works even when there is a duplicate name",
6993 method="textDocument/hover",
6994 params={
6995 "textDocument": {"uri": "${main_file}"},
6996 "position": {"line": 2, "character": 13},
6998 result={
6999 "contents": [
7000 {"language": "hack", "value": "function aaa(): int"},
7001 "Return type: `int`",
7003 "range": {
7004 "start": {"line": 2, "character": 11},
7005 "end": {"line": 2, "character": 14},
7008 powered_by="serverless_ide",
7010 .write_to_disk(
7011 comment="delete file A", uri="${file_a}", contents=None, notify=True
7013 .request(
7014 line=line(),
7015 comment="Now that we've fixed the error, hover should work.",
7016 method="textDocument/hover",
7017 params={
7018 "textDocument": {"uri": "${main_file}"},
7019 "position": {"line": 2, "character": 13},
7021 result={
7022 "contents": [
7023 {"language": "hack", "value": "function aaa(): string"},
7024 "Return type: `string`",
7026 "range": {
7027 "start": {"line": 2, "character": 11},
7028 "end": {"line": 2, "character": 14},
7031 powered_by="serverless_ide",
7033 .request(line=line(), method="shutdown", params={}, result=None)
7034 .notification(method="exit", params={})
7036 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7038 def test_serverless_ide_naming_error2(self) -> None:
7039 variables = dict(self.prepare_serverless_ide_environment())
7040 self.test_driver.stop_hh_server()
7041 variables.update(self.setup_php_file("naming_error_caller.php"))
7042 variables.update(
7044 "contents": self.read_repo_file("naming_error_declaration.php"),
7045 "original": self.repo_file("naming_error_declaration.php"),
7046 "copy": self.repo_file("naming_error_copy.php"),
7049 spec = (
7050 self.initialize_spec(
7051 LspTestSpec("serverless_ide_naming_error2"), use_serverless_ide=True
7053 .notification(
7054 method="textDocument/didOpen",
7055 params={
7056 "textDocument": {
7057 "uri": "${php_file_uri}",
7058 "languageId": "hack",
7059 "version": 1,
7060 "text": "${php_file}",
7064 .write_to_disk(
7065 comment="create copy",
7066 uri="${copy}",
7067 contents="${contents}",
7068 notify=True,
7070 .write_to_disk(
7071 comment="delete copy", uri="${copy}", contents=None, notify=True
7073 .request(
7074 line=line(),
7075 comment="hover should work fine after making copy then deleting copy.",
7076 method="textDocument/hover",
7077 params={
7078 "textDocument": {"uri": "${php_file_uri}"},
7079 "position": {"line": 3, "character": 15},
7081 result={
7082 "contents": [
7084 "language": "hack",
7085 "value": "function naming_error_declaration(): void",
7087 "Return type: `void`",
7089 "range": {
7090 "start": {"line": 3, "character": 2},
7091 "end": {"line": 3, "character": 26},
7094 powered_by="serverless_ide",
7096 .request(line=line(), method="shutdown", params={}, result=None)
7097 .notification(method="exit", params={})
7099 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7101 def test_serverless_ide_naming_error3(self) -> None:
7102 variables = dict(self.prepare_serverless_ide_environment())
7103 self.test_driver.stop_hh_server()
7104 variables.update(self.setup_php_file("naming_error_caller.php"))
7105 variables.update(
7107 "contents": self.read_repo_file("naming_error_declaration.php"),
7108 "original": self.repo_file("naming_error_declaration.php"),
7109 "copy": self.repo_file("naming_error_copy.php"),
7112 spec = (
7113 self.initialize_spec(
7114 LspTestSpec("serverless_ide_naming_error3"), use_serverless_ide=True
7116 .notification(
7117 method="textDocument/didOpen",
7118 params={
7119 "textDocument": {
7120 "uri": "${php_file_uri}",
7121 "languageId": "hack",
7122 "version": 1,
7123 "text": "${php_file}",
7127 .write_to_disk(
7128 comment="create copy",
7129 uri="${copy}",
7130 contents="${contents}",
7131 notify=True,
7133 .write_to_disk(
7134 comment="delete original", uri="${original}", contents=None, notify=True
7136 .request(
7137 line=line(),
7138 comment="hover should work fine after making copy then deleting original.",
7139 method="textDocument/hover",
7140 params={
7141 "textDocument": {"uri": "${php_file_uri}"},
7142 "position": {"line": 3, "character": 15},
7144 result={
7145 "contents": [
7147 "language": "hack",
7148 "value": "function naming_error_declaration(): void",
7150 "Return type: `void`",
7152 "range": {
7153 "start": {"line": 3, "character": 2},
7154 "end": {"line": 3, "character": 26},
7157 powered_by="serverless_ide",
7159 .request(line=line(), method="shutdown", params={}, result=None)
7160 .notification(method="exit", params={})
7162 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7164 def test_serverless_ide_requests_before_init(self) -> None:
7165 variables = dict(self.prepare_serverless_ide_environment())
7166 variables["root_path"] = self.test_driver.repo_dir
7167 self.test_driver.stop_hh_server()
7169 spec = (
7170 self.initialize_spec(
7171 LspTestSpec("test_serverless_ide_requests_before_init"),
7172 use_serverless_ide=True,
7173 supports_status=True,
7174 supports_init=True,
7176 .ignore_notifications(method="textDocument/publishDiagnostics")
7177 .ignore_requests(
7178 comment="Ignore 'initializing...' messages since they're racy",
7179 method="window/showStatus",
7180 params={
7181 "type": 2,
7182 "actions": [{"title": "Restart hh_server"}],
7183 "message": "Hack IDE: initializing.\nhh_server: stopped.",
7184 "shortMessage": "Hack: initializing",
7187 .ignore_requests(
7188 comment="another racy initialization, before we've yet heard from hh_server",
7189 method="window/showStatus",
7190 params={
7191 "type": 2,
7192 "actions": [],
7193 "message": "Hack IDE: initializing.",
7194 "shortMessage": "Hack: initializing",
7197 .ignore_requests(
7198 comment="another racy initialization, if HackIDE is done before hh_server has yet sent status",
7199 method="window/showStatus",
7200 params={
7201 "type": 3,
7202 "actions": [],
7203 "message": "Hack IDE: ready.",
7204 "shortMessage": "Hack: ready",
7207 .write_to_disk(
7208 notify=True,
7209 wait=False,
7210 uri="file://${root_path}/beforeInit1.php",
7211 contents="<?hh // strict\nfunction beforeInit1(): int {\n return 42;\n}\n",
7213 .notification(
7214 comment="open a file before init has finished",
7215 method="textDocument/didOpen",
7216 params={
7217 "textDocument": {
7218 "uri": "file://${root_path}/beforeInit2.php",
7219 "languageId": "hack",
7220 "version": 1,
7221 "text": "<?hh // strict\nfunction beforeInit2(): void {\n $foo = beforeInit1();\n}\n",
7225 .request(
7226 line=line(),
7227 comment="hover before init will fail",
7228 method="textDocument/hover",
7229 params={
7230 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
7231 "position": {"line": 2, "character": 4},
7233 result=None,
7235 .request(
7236 line=line(),
7237 comment="documentSymbol before init will succeed",
7238 method="textDocument/documentSymbol",
7239 params={"textDocument": {"uri": "file://${root_path}/beforeInit2.php"}},
7240 result=[
7242 "name": "beforeInit2",
7243 "kind": 12,
7244 "location": {
7245 "uri": "file://${root_path}/beforeInit2.php",
7246 "range": {
7247 "start": {"line": 1, "character": 0},
7248 "end": {"line": 3, "character": 1},
7253 powered_by="serverless_ide",
7255 .wait_for_notification(
7256 comment="wait for sIDE to init",
7257 method="telemetry/event",
7258 params={"type": 4, "message": "[client-ide] Finished init: ok"},
7260 .wait_for_server_request(
7261 method="window/showStatus",
7262 params={
7263 "actions": [{"title": "Restart hh_server"}],
7264 "message": "Hack IDE: ready.\nhh_server: stopped.",
7265 "shortMessage": "Hack: ready",
7266 "type": 3,
7268 result=NoResponse(),
7270 .request(
7271 line=line(),
7272 comment="hover after init will succeed",
7273 method="textDocument/hover",
7274 params={
7275 "textDocument": {"uri": "file://${root_path}/beforeInit2.php"},
7276 "position": {"line": 2, "character": 4},
7278 result={
7279 "contents": [{"language": "hack", "value": "int"}],
7280 "range": {
7281 "start": {"line": 2, "character": 2},
7282 "end": {"line": 2, "character": 6},
7285 powered_by="serverless_ide",
7287 .request(line=line(), method="shutdown", params={}, result=None)
7288 .notification(method="exit", params={})
7291 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
7293 def test_serverless_ide_workspace_symbol(self) -> None:
7294 variables = dict(self.prepare_serverless_ide_environment())
7295 variables["root_path"] = self.test_driver.repo_dir
7296 self.test_driver.stop_hh_server()
7298 spec = (
7299 self.initialize_spec(
7300 LspTestSpec("serverless_ide_workspace_symbol"), use_serverless_ide=True
7302 .request(
7303 line=line(),
7304 comment="workspace symbol call, global, powered by sqlite (generated during serverless-ide-init)",
7305 method="workspace/symbol",
7306 params={"query": "TakesString"},
7307 result=[
7309 "name": "TakesString",
7310 "kind": 5,
7311 "location": {
7312 "uri": "file://${root_path}/definition.php",
7313 "range": {
7314 "start": {"line": 36, "character": 6},
7315 "end": {"line": 36, "character": 17},
7320 powered_by="serverless_ide",
7322 .request(
7323 line=line(),
7324 comment="workspace symbol call, member (derived from naming-table)",
7325 method="workspace/symbol",
7326 params={"query": "TakesString::"},
7327 result=[
7329 "name": "__construct",
7330 "kind": 6,
7331 "location": {
7332 "uri": "file://${root_path}/definition.php",
7333 "range": {
7334 "start": {"line": 37, "character": 18},
7335 "end": {"line": 37, "character": 29},
7340 powered_by="serverless_ide",
7342 .request(line=line(), method="shutdown", params={}, result=None)
7343 .notification(method="exit", params={})
7345 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)