forked from microsoft/monitors4codegen
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlanguage_server.py
805 lines (677 loc) · 35.7 KB
/
language_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
"""
This file contains the main interface and the public API for multilspy.
The abstract class LanguageServer provides a factory method, creator that is
intended for creating instantiations of language specific clients.
The details of Language Specific configuration are not exposed to the user.
"""
import asyncio
import dataclasses
import json
import logging
import os
import pathlib
import threading
from contextlib import asynccontextmanager, contextmanager
from .lsp_protocol_handler.lsp_constants import LSPConstants
from .lsp_protocol_handler import lsp_types as LSPTypes
from . import multilspy_types
from .multilspy_logger import MultilspyLogger
from .lsp_protocol_handler.server import (
LanguageServerHandler,
ProcessLaunchInfo,
)
from .multilspy_config import MultilspyConfig, Language
from .multilspy_exceptions import MultilspyException
from .multilspy_utils import PathUtils, FileUtils, TextUtils
from pathlib import PurePath
from typing import AsyncIterator, Iterator, List, Dict, Union, Tuple
from .type_helpers import ensure_all_methods_implemented
@dataclasses.dataclass
class LSPFileBuffer:
"""
This class is used to store the contents of an open LSP file in memory.
"""
# uri of the file
uri: str
# The contents of the file
contents: str
# The version of the file
version: int
# The language id of the file
language_id: str
# reference count of the file
ref_count: int
class LanguageServer:
"""
The LanguageServer class provides a language agnostic interface to the Language Server Protocol.
It is used to communicate with Language Servers of different programming languages.
"""
@classmethod
def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer":
"""
Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory.
:param repository_root_path: The root path of the repository.
:param config: The Multilspy configuration.
:param logger: The logger to use.
:return LanguageServer: A language specific LanguageServer instance.
"""
if config.code_language == Language.PYTHON:
from monitors4codegen.multilspy.language_servers.jedi_language_server.jedi_server import (
JediServer,
)
return JediServer(config, logger, repository_root_path)
elif config.code_language == Language.JAVA:
from monitors4codegen.multilspy.language_servers.eclipse_jdtls.eclipse_jdtls import (
EclipseJDTLS,
)
return EclipseJDTLS(config, logger, repository_root_path)
elif config.code_language == Language.RUST:
from monitors4codegen.multilspy.language_servers.rust_analyzer.rust_analyzer import (
RustAnalyzer,
)
return RustAnalyzer(config, logger, repository_root_path)
elif config.code_language == Language.CSHARP:
from monitors4codegen.multilspy.language_servers.omnisharp.omnisharp import OmniSharp
return OmniSharp(config, logger, repository_root_path)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise MultilspyException(f"Language {config.code_language} is not supported")
def __init__(
self,
config: MultilspyConfig,
logger: MultilspyLogger,
repository_root_path: str,
process_launch_info: ProcessLaunchInfo,
language_id: str,
):
"""
Initializes a LanguageServer instance.
Do not instantiate this class directly. Use `LanguageServer.create` method instead.
:param config: The Multilspy configuration.
:param logger: The logger to use.
:param repository_root_path: The root path of the repository.
:param cmd: Each language server has a specific command used to start the server.
This parameter is the command to launch the language server process.
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
as opposed to HTTP, TCP modes supported by some language servers.
"""
if type(self) == LanguageServer:
raise MultilspyException(
"LanguageServer is an abstract class and cannot be instantiated directly. Use LanguageServer.create method instead."
)
self.logger = logger
self.server_started = False
self.repository_root_path: str = repository_root_path
self.completions_available = asyncio.Event()
if config.trace_lsp_communication:
def logging_fn(source, target, msg):
self.logger.log(f"LSP: {source} -> {target}: {str(msg)}", logging.DEBUG)
else:
def logging_fn(source, target, msg):
pass
# cmd is obtained from the child classes, which provide the language specific command to start the language server
# LanguageServerHandler provides the functionality to start the language server and communicate with it
self.server: LanguageServerHandler = LanguageServerHandler(process_launch_info, logger=logging_fn)
self.language_id = language_id
self.open_file_buffers: Dict[str, LSPFileBuffer] = {}
@asynccontextmanager
async def start_server(self) -> AsyncIterator["LanguageServer"]:
"""
Starts the Language Server and yields the LanguageServer instance.
Usage:
```
async with lsp.start_server():
# LanguageServer has been initialized and ready to serve requests
await lsp.request_definition(...)
await lsp.request_references(...)
# Shutdown the LanguageServer on exit from scope
# LanguageServer has been shutdown
```
"""
self.server_started = True
yield self
self.server_started = False
# TODO: Add support for more LSP features
@contextmanager
def open_file(self, relative_file_path: str) -> Iterator[None]:
"""
Open a file in the Language Server. This is required before making any requests to the Language Server.
:param relative_file_path: The relative path of the file to open.
"""
if not self.server_started:
self.logger.log(
"open_file called before Language Server started",
logging.ERROR,
)
raise MultilspyException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
if uri in self.open_file_buffers:
assert self.open_file_buffers[uri].uri == uri
assert self.open_file_buffers[uri].ref_count >= 1
self.open_file_buffers[uri].ref_count += 1
yield
self.open_file_buffers[uri].ref_count -= 1
else:
contents = FileUtils.read_file(self.logger, absolute_file_path)
version = 0
self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, self.language_id, 1)
self.server.notify.did_open_text_document(
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: uri,
LSPConstants.LANGUAGE_ID: self.language_id,
LSPConstants.VERSION: 0,
LSPConstants.TEXT: contents,
}
}
)
yield
self.open_file_buffers[uri].ref_count -= 1
if self.open_file_buffers[uri].ref_count == 0:
self.server.notify.did_close_text_document(
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: uri,
}
}
)
del self.open_file_buffers[uri]
def insert_text_at_position(
self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str
) -> multilspy_types.Position:
"""
Insert text at the given line and column in the given file and return
the updated cursor position after inserting the text.
:param relative_file_path: The relative path of the file to open.
:param line: The line number at which text should be inserted.
:param column: The column number at which text should be inserted.
:param text_to_be_inserted: The text to insert.
"""
if not self.server_started:
self.logger.log(
"insert_text_at_position called before Language Server started",
logging.ERROR,
)
raise MultilspyException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
# Ensure the file is open
assert uri in self.open_file_buffers
file_buffer = self.open_file_buffers[uri]
file_buffer.version += 1
change_index = TextUtils.get_index_from_line_col(file_buffer.contents, line, column)
file_buffer.contents = (
file_buffer.contents[:change_index] + text_to_be_inserted + file_buffer.contents[change_index:]
)
self.server.notify.did_change_text_document(
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.VERSION: file_buffer.version,
LSPConstants.URI: file_buffer.uri,
},
LSPConstants.CONTENT_CHANGES: [
{
LSPConstants.RANGE: {
"start": {"line": line, "character": column},
"end": {"line": line, "character": column},
},
"text": text_to_be_inserted,
}
],
}
)
new_l, new_c = TextUtils.get_updated_position_from_line_and_column_and_edit(line, column, text_to_be_inserted)
return multilspy_types.Position(line=new_l, character=new_c)
def delete_text_between_positions(
self,
relative_file_path: str,
start: multilspy_types.Position,
end: multilspy_types.Position,
) -> str:
"""
Delete text between the given start and end positions in the given file and return the deleted text.
"""
if not self.server_started:
self.logger.log(
"insert_text_at_position called before Language Server started",
logging.ERROR,
)
raise MultilspyException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
# Ensure the file is open
assert uri in self.open_file_buffers
file_buffer = self.open_file_buffers[uri]
file_buffer.version += 1
del_start_idx = TextUtils.get_index_from_line_col(file_buffer.contents, start["line"], start["character"])
del_end_idx = TextUtils.get_index_from_line_col(file_buffer.contents, end["line"], end["character"])
deleted_text = file_buffer.contents[del_start_idx:del_end_idx]
file_buffer.contents = file_buffer.contents[:del_start_idx] + file_buffer.contents[del_end_idx:]
self.server.notify.did_change_text_document(
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.VERSION: file_buffer.version,
LSPConstants.URI: file_buffer.uri,
},
LSPConstants.CONTENT_CHANGES: [{LSPConstants.RANGE: {"start": start, "end": end}, "text": ""}],
}
)
return deleted_text
def get_open_file_text(self, relative_file_path: str) -> str:
"""
Get the contents of the given opened file as per the Language Server.
:param relative_file_path: The relative path of the file to open.
"""
if not self.server_started:
self.logger.log(
"get_open_file_text called before Language Server started",
logging.ERROR,
)
raise MultilspyException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
# Ensure the file is open
assert uri in self.open_file_buffers
file_buffer = self.open_file_buffers[uri]
return file_buffer.contents
async def request_definition(
self, relative_file_path: str, line: int, column: int
) -> List[multilspy_types.Location]:
"""
Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server
for the symbol at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return List[multilspy_types.Location]: A list of locations where the symbol is defined
"""
if not self.server_started:
self.logger.log(
"find_function_definition called before Language Server started",
logging.ERROR,
)
raise MultilspyException("Language Server not started")
with self.open_file(relative_file_path):
# sending request to the language server and waiting for response
response = await self.server.send.definition(
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: pathlib.Path(
str(PurePath(self.repository_root_path, relative_file_path))
).as_uri()
},
LSPConstants.POSITION: {
LSPConstants.LINE: line,
LSPConstants.CHARACTER: column,
},
}
)
ret: List[multilspy_types.Location] = []
if isinstance(response, list):
# response is either of type Location[] or LocationLink[]
for item in response:
assert isinstance(item, dict)
if LSPConstants.URI in item and LSPConstants.RANGE in item:
new_item: multilspy_types.Location = {}
new_item.update(item)
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = str(
PurePath(os.path.relpath(new_item["absolutePath"], self.repository_root_path))
)
ret.append(multilspy_types.Location(new_item))
elif (
LSPConstants.ORIGIN_SELECTION_RANGE in item
and LSPConstants.TARGET_URI in item
and LSPConstants.TARGET_RANGE in item
and LSPConstants.TARGET_SELECTION_RANGE in item
):
new_item: multilspy_types.Location = {}
new_item["uri"] = item[LSPConstants.TARGET_URI]
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = str(
PurePath(os.path.relpath(new_item["absolutePath"], self.repository_root_path))
)
new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE]
ret.append(multilspy_types.Location(**new_item))
else:
assert False, f"Unexpected response from Language Server: {item}"
elif isinstance(response, dict):
# response is of type Location
assert LSPConstants.URI in response
assert LSPConstants.RANGE in response
new_item: multilspy_types.Location = {}
new_item.update(response)
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = str(
PurePath(os.path.relpath(new_item["absolutePath"], self.repository_root_path))
)
ret.append(multilspy_types.Location(**new_item))
else:
assert False, f"Unexpected response from Language Server: {response}"
return ret
async def request_references(
self, relative_file_path: str, line: int, column: int
) -> List[multilspy_types.Location]:
"""
Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server
to find references to the symbol at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbol for which references should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return List[multilspy_types.Location]: A list of locations where the symbol is referenced
"""
if not self.server_started:
self.logger.log(
"find_all_callers_of_function called before Language Server started",
logging.ERROR,
)
raise MultilspyException("Language Server not started")
with self.open_file(relative_file_path):
# sending request to the language server and waiting for response
response = await self.server.send.references(
{
"context": {"includeDeclaration": False},
"textDocument": {
"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
},
"position": {"line": line, "character": column},
}
)
ret: List[multilspy_types.Location] = []
assert isinstance(response, list)
for item in response:
assert isinstance(item, dict)
assert LSPConstants.URI in item
assert LSPConstants.RANGE in item
new_item: multilspy_types.Location = {}
new_item.update(item)
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = str(
PurePath(os.path.relpath(new_item["absolutePath"], self.repository_root_path))
)
ret.append(multilspy_types.Location(**new_item))
return ret
async def request_completions(
self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False
) -> List[multilspy_types.CompletionItem]:
"""
Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server
to find completions at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return List[multilspy_types.CompletionItem]: A list of completions
"""
with self.open_file(relative_file_path):
open_file_buffer = self.open_file_buffers[
pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
]
completion_params: LSPTypes.CompletionParams = {
"position": {"line": line, "character": column},
"textDocument": {"uri": open_file_buffer.uri},
"context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked},
}
response: Union[List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None] = None
num_retries = 0
while response is None or (response["isIncomplete"] and num_retries < 30):
await self.completions_available.wait()
response: Union[
List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None
] = await self.server.send.completion(completion_params)
if isinstance(response, list):
response = {"items": response, "isIncomplete": False}
num_retries += 1
# TODO: Understand how to appropriately handle `isIncomplete`
if response is None or (response["isIncomplete"] and not(allow_incomplete)):
return []
if "items" in response:
response = response["items"]
response: List[LSPTypes.CompletionItem] = response
# TODO: Handle the case when the completion is a keyword
items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword]
completions_list: List[multilspy_types.CompletionItem] = []
for item in items:
assert "insertText" in item or "textEdit" in item
assert "kind" in item
completion_item = {}
if "detail" in item:
completion_item["detail"] = item["detail"]
if "label" in item:
completion_item["completionText"] = item["label"]
completion_item["kind"] = item["kind"]
elif "insertText" in item:
completion_item["completionText"] = item["insertText"]
completion_item["kind"] = item["kind"]
elif "textEdit" in item and "newText" in item["textEdit"]:
completion_item["completionText"] = item["textEdit"]["newText"]
completion_item["kind"] = item["kind"]
elif "textEdit" in item and "range" in item["textEdit"]:
new_dot_lineno, new_dot_colno = (
completion_params["position"]["line"],
completion_params["position"]["character"],
)
assert all(
(
item["textEdit"]["range"]["start"]["line"] == new_dot_lineno,
item["textEdit"]["range"]["start"]["character"] == new_dot_colno,
item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"],
item["textEdit"]["range"]["start"]["character"]
== item["textEdit"]["range"]["end"]["character"],
)
)
completion_item["completionText"] = item["textEdit"]["newText"]
completion_item["kind"] = item["kind"]
elif "textEdit" in item and "insert" in item["textEdit"]:
assert False
else:
assert False
completion_item = multilspy_types.CompletionItem(**completion_item)
completions_list.append(completion_item)
return [
json.loads(json_repr)
for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list])
]
async def request_document_symbols(self, relative_file_path: str) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], Union[List[multilspy_types.TreeRepr], None]]:
"""
Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server
to find symbols in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbols
:return Tuple[List[multilspy_types.UnifiedSymbolInformation], Union[List[multilspy_types.TreeRepr], None]]: A list of symbols in the file, and the tree representation of the symbols
"""
with self.open_file(relative_file_path):
response = await self.server.send.document_symbol(
{
"textDocument": {
"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
}
}
)
ret: List[multilspy_types.UnifiedSymbolInformation] = []
l_tree = None
assert isinstance(response, list)
for item in response:
assert isinstance(item, dict)
assert LSPConstants.NAME in item
assert LSPConstants.KIND in item
if LSPConstants.CHILDREN in item:
# TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well
def visit_tree_nodes_and_build_tree_repr(tree: LSPTypes.DocumentSymbol) -> List[multilspy_types.UnifiedSymbolInformation]:
l: List[multilspy_types.UnifiedSymbolInformation] = []
children = tree['children'] if 'children' in tree else []
if 'children' in tree:
del tree['children']
l.append(multilspy_types.UnifiedSymbolInformation(**tree))
for child in children:
l.extend(visit_tree_nodes_and_build_tree_repr(child))
return l
ret.extend(visit_tree_nodes_and_build_tree_repr(item))
else:
ret.append(multilspy_types.UnifiedSymbolInformation(**item))
return ret, l_tree
async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]:
"""
Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server
to find the hover information at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the hover information
:param line: The line number of the symbol
:param column: The column number of the symbol
:return None
"""
with self.open_file(relative_file_path):
response = await self.server.send.hover(
{
"textDocument": {
"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
},
"position": {
"line": line,
"character": column,
},
}
)
if response is None:
return None
assert isinstance(response, dict)
return multilspy_types.Hover(**response)
@ensure_all_methods_implemented(LanguageServer)
class SyncLanguageServer:
"""
The SyncLanguageServer class provides a language agnostic interface to the Language Server Protocol.
It is used to communicate with Language Servers of different programming languages.
"""
def __init__(self, language_server: LanguageServer) -> None:
self.language_server = language_server
self.loop = None
self.loop_thread = None
@classmethod
def create(
cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str
) -> "SyncLanguageServer":
"""
Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory.
:param repository_root_path: The root path of the repository.
:param config: The Multilspy configuration.
:param logger: The logger to use.
:return SyncLanguageServer: A language specific LanguageServer instance.
"""
return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path))
@contextmanager
def open_file(self, relative_file_path: str) -> Iterator[None]:
"""
Open a file in the Language Server. This is required before making any requests to the Language Server.
:param relative_file_path: The relative path of the file to open.
"""
with self.language_server.open_file(relative_file_path):
yield
def insert_text_at_position(
self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str
) -> multilspy_types.Position:
"""
Insert text at the given line and column in the given file and return
the updated cursor position after inserting the text.
:param relative_file_path: The relative path of the file to open.
:param line: The line number at which text should be inserted.
:param column: The column number at which text should be inserted.
:param text_to_be_inserted: The text to insert.
"""
return self.language_server.insert_text_at_position(relative_file_path, line, column, text_to_be_inserted)
def delete_text_between_positions(
self,
relative_file_path: str,
start: multilspy_types.Position,
end: multilspy_types.Position,
) -> str:
"""
Delete text between the given start and end positions in the given file and return the deleted text.
"""
return self.language_server.delete_text_between_positions(relative_file_path, start, end)
def get_open_file_text(self, relative_file_path: str) -> str:
"""
Get the contents of the given opened file as per the Language Server.
:param relative_file_path: The relative path of the file to open.
"""
return self.language_server.get_open_file_text(relative_file_path)
@contextmanager
def start_server(self) -> Iterator["SyncLanguageServer"]:
"""
Starts the language server process and connects to it.
:return: None
"""
self.loop = asyncio.new_event_loop()
loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
loop_thread.start()
ctx = self.language_server.start_server()
asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result()
yield self
asyncio.run_coroutine_threadsafe(ctx.__aexit__(None, None, None), loop=self.loop).result()
self.loop.call_soon_threadsafe(self.loop.stop)
loop_thread.join()
def request_definition(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]:
"""
Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server
for the symbol at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return List[multilspy_types.Location]: A list of locations where the symbol is defined
"""
result = asyncio.run_coroutine_threadsafe(
self.language_server.request_definition(file_path, line, column), self.loop
).result()
return result
def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]:
"""
Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server
to find references to the symbol at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbol for which references should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return List[multilspy_types.Location]: A list of locations where the symbol is referenced
"""
result = asyncio.run_coroutine_threadsafe(
self.language_server.request_references(file_path, line, column), self.loop
).result()
return result
def request_completions(
self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False
) -> List[multilspy_types.CompletionItem]:
"""
Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server
to find completions at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return List[multilspy_types.CompletionItem]: A list of completions
"""
result = asyncio.run_coroutine_threadsafe(
self.language_server.request_completions(relative_file_path, line, column, allow_incomplete),
self.loop,
).result()
return result
def request_document_symbols(self, relative_file_path: str) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], Union[List[multilspy_types.TreeRepr], None]]:
"""
Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server
to find symbols in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the symbols
:return Tuple[List[multilspy_types.UnifiedSymbolInformation], Union[List[multilspy_types.TreeRepr], None]]: A list of symbols in the file, and the tree representation of the symbols
"""
result = asyncio.run_coroutine_threadsafe(
self.language_server.request_document_symbols(relative_file_path), self.loop
).result()
return result
def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]:
"""
Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server
to find the hover information at the given line and column in the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file that has the hover information
:param line: The line number of the symbol
:param column: The column number of the symbol
:return None
"""
result = asyncio.run_coroutine_threadsafe(
self.language_server.request_hover(relative_file_path, line, column), self.loop
).result()
return result