diff --git a/pylspclient/__init__.py b/pylspclient/__init__.py index 1cb55c7..b640986 100644 --- a/pylspclient/__init__.py +++ b/pylspclient/__init__.py @@ -5,4 +5,4 @@ from pylspclient.json_rpc_endpoint import JsonRpcEndpoint from pylspclient.lsp_client import LspClient from pylspclient.lsp_endpoint import LspEndpoint -from pylspclient import lsp_structs +from pylspclient import lsp_errors diff --git a/pylspclient/json_rpc_endpoint.py b/pylspclient/json_rpc_endpoint.py index 04f2e39..73ba20b 100644 --- a/pylspclient/json_rpc_endpoint.py +++ b/pylspclient/json_rpc_endpoint.py @@ -1,6 +1,6 @@ from __future__ import print_function import json -from pylspclient import lsp_structs +from pylspclient.lsp_errors import ErrorCodes, ResponseError import threading JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}" @@ -70,7 +70,7 @@ def recv_response(self): return None line = line.decode("utf-8") if not line.endswith("\r\n"): - raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: missing newline") + raise ResponseError(ErrorCodes.ParseError, "Bad header: missing newline") #remove the "\r\n" line = line[:-2] if line == "": @@ -79,15 +79,15 @@ def recv_response(self): elif line.startswith(LEN_HEADER): line = line[len(LEN_HEADER):] if not line.isdigit(): - raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: size is not int") + raise ResponseError(ErrorCodes.ParseError, "Bad header: size is not int") message_size = int(line) elif line.startswith(TYPE_HEADER): # nothing todo with type for now. pass else: - raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: unkown header") + raise ResponseError(ErrorCodes.ParseError, "Bad header: unkown header") if not message_size: - raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: missing size") + raise ResponseError(ErrorCodes.ParseError, "Bad header: missing size") jsonrpc_res = self.stdout.read(message_size).decode("utf-8") return json.loads(jsonrpc_res) diff --git a/pylspclient/lsp_client.py b/pylspclient/lsp_client.py index d61a301..35aaa59 100644 --- a/pylspclient/lsp_client.py +++ b/pylspclient/lsp_client.py @@ -1,10 +1,9 @@ from typing import Optional from pydantic import ValidationError -from pylspclient import lsp_structs from pylspclient.lsp_endpoint import LspEndpoint from pylspclient.lsp_pydantic_strcuts import TextDocumentItem, TextDocumentIdentifier, DocumentSymbol, SymbolInformation, LocationLink, Location -from pylspclient.lsp_pydantic_strcuts import Position +from pylspclient.lsp_pydantic_strcuts import Position, SignatureHelp, CompletionContext, CompletionItem, CompletionList class LspClient(object): def __init__(self, lsp_endpoint: LspEndpoint): @@ -89,10 +88,10 @@ def didOpen(self, textDocument: TextDocumentItem): :param TextDocumentItem textDocument: The document that was opened. """ - return self.lsp_endpoint.send_notification("textDocument/didOpen", textDocument=textDocument) + self.lsp_endpoint.send_notification("textDocument/didOpen", textDocument=textDocument) - def didChange(self, textDocument, contentChanges): + def didChange(self, textDocument: TextDocumentItem, contentChanges): """ The document change notification is sent from the client to the server to signal changes to a text document. In 2.0 the shape of the params has changed to include proper version numbers and language ids. @@ -134,7 +133,13 @@ def typeDefinition( return [Location.parse_obj(result) for result in result_dict] - def signatureHelp(self, textDocument, position): + def signatureHelp( + self, + textDocument: TextDocumentIdentifier, + position: Position, + workDoneToken: Optional[str] = None, + partialResultToken: Optional[str] = None + ) -> SignatureHelp: """ The signature help request is sent from the client to the server to request signature information at a given cursor position. @@ -142,23 +147,28 @@ def signatureHelp(self, textDocument, position): :param Position position: The position inside the text document. """ result_dict = self.lsp_endpoint.call_method("textDocument/signatureHelp", textDocument=textDocument, position=position) - return lsp_structs.SignatureHelp(**result_dict) + return SignatureHelp.parse_obj(result_dict) - def completion(self, textDocument, position, context): + def completion( + self, + textDocument: TextDocumentIdentifier, + position: Position, + context: CompletionContext + ) -> CompletionItem | CompletionList: """ The signature help request is sent from the client to the server to request signature information at a given cursor position. :param TextDocumentItem textDocument: The text document. :param Position position: The position inside the text document. :param CompletionContext context: The completion context. This is only available if the client specifies - to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` + CompletionContext to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` """ result_dict = self.lsp_endpoint.call_method("textDocument/completion", textDocument=textDocument, position=position, context=context) if "isIncomplete" in result_dict: - return lsp_structs.CompletionList(**result_dict) + return CompletionList.parse_obj(result_dict) - return [lsp_structs.CompletionItem(**result) for result in result_dict] + return [CompletionItem.parse_obj(result) for result in result_dict] def declaration( diff --git a/pylspclient/lsp_endpoint.py b/pylspclient/lsp_endpoint.py index 3df745d..184ceaa 100644 --- a/pylspclient/lsp_endpoint.py +++ b/pylspclient/lsp_endpoint.py @@ -1,6 +1,6 @@ from __future__ import print_function import threading -from pylspclient import lsp_structs +from pylspclient.lsp_errors import ErrorCodes, ResponseError class LspEndpoint(threading.Thread): @@ -45,7 +45,7 @@ def run(self): if rpc_id: # a call for method if method not in self.method_callbacks: - raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.MethodNotFound, "Method not found: {method}".format(method=method)) + raise ResponseError(ErrorCodes.MethodNotFound, "Method not found: {method}".format(method=method)) result = self.method_callbacks[method](params) self.send_response(rpc_id, result, None) else: @@ -57,7 +57,7 @@ def run(self): self.notify_callbacks[method](params) else: self.handle_result(rpc_id, result, error) - except lsp_structs.ResponseError as e: + except ResponseError as e: self.send_response(rpc_id, None, e) @@ -101,7 +101,7 @@ def call_method(self, method_name, **kwargs): self.event_dict.pop(current_id) result, error = self.response_dict.pop(current_id) if error: - raise lsp_structs.ResponseError(error.get("code"), error.get("message"), error.get("data")) + raise ResponseError(error.get("code"), error.get("message"), error.get("data")) return result diff --git a/pylspclient/lsp_errors.py b/pylspclient/lsp_errors.py new file mode 100644 index 0000000..3c66ecd --- /dev/null +++ b/pylspclient/lsp_errors.py @@ -0,0 +1,27 @@ +from typing import Any +from enum import IntEnum + + +class ErrorCodes(IntEnum): + # Defined by JSON RPC + ParseError = -32700 + InvalidRequest = -32600 + MethodNotFound = -32601 + InvalidParams = -32602 + InternalError = -32603 + serverErrorStart = -32099 + serverErrorEnd = -32000 + ServerNotInitialized = -32002 + UnknownErrorCode = -32001 + + # Defined by the protocol. + RequestCancelled = -32800 + ContentModified = -32801 + + +class ResponseError(Exception): + def __init__(self, code: ErrorCodes, message: str, data: Any = None): + self.code = code + self.message = message + if data: + self.data = data diff --git a/pylspclient/lsp_pydantic_strcuts.py b/pylspclient/lsp_pydantic_strcuts.py index 1ba2f8b..fe54870 100644 --- a/pylspclient/lsp_pydantic_strcuts.py +++ b/pylspclient/lsp_pydantic_strcuts.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Union from enum import Enum, IntEnum from pydantic import BaseModel, HttpUrl @@ -119,7 +119,7 @@ class SymbolKind(IntEnum): Operator = 25 TypeParameter = 26 -class SymbolTag(Enum): +class SymbolTag(IntEnum): """Represents additional information about the symbol.""" # Define the symbol tags as per your specification, for example: Deprecated = 1 @@ -173,3 +173,70 @@ class LocationLink(BaseModel): targetUri: HttpUrl targetRange: Range targetSelectionRange: Range + +class SignatureInformation(BaseModel): + label: str + documentation: Optional[Union[str, dict]] = None + parameters: Optional[List[dict]] = None + +class SignatureHelp(BaseModel): + signatures: List[SignatureInformation] + activeSignature: Optional[int] = None + activeParameter: Optional[int] = None + +class CompletionTriggerKind(IntEnum): + """ + Specifies how the completion was triggered. + """ + Invoked = 1 # Completion was triggered by typing an identifier, manual invocation, etc. + TriggerCharacter = 2 # Completion was triggered by a trigger character. + TriggerForIncompleteCompletions = 3 # Completion was re-triggered as the current completion list is incomplete. + +class CompletionContext(BaseModel): + """ + Contains additional information about the context in which a completion request is triggered. + """ + triggerKind: CompletionTriggerKind + triggerCharacter: Optional[str] = None # Character that triggered the completion request. + +class CompletionItemKind(IntEnum): + """ + Defines the kind of completion item. + """ + Text = 1 + Method = 2 + Function = 3 + Constructor = 4 + Field = 5 + Variable = 6 + Class = 7 + Interface = 8 + Module = 9 + Property = 10 + Unit = 11 + Value = 12 + Enum = 13 + Keyword = 14 + Snippet = 15 + Color = 16 + File = 17 + Reference = 18 + Folder = 19 + EnumMember = 20 + Constant = 21 + Struct = 22 + Event = 23 + Operator = 24 + TypeParameter = 25 + +class CompletionItem(BaseModel): + label: str + kind: Optional[CompletionItemKind] = None + detail: Optional[str] = None + documentation: Optional[Union[str, dict]] = None + insertText: Optional[str] = None + insertTextFormat: Optional[int] = None # 1: PlainText, 2: Snippet + +class CompletionList(BaseModel): + isIncomplete: bool + items: List[CompletionItem] diff --git a/pylspclient/lsp_structs.py b/pylspclient/lsp_structs.py deleted file mode 100644 index d6dbdd2..0000000 --- a/pylspclient/lsp_structs.py +++ /dev/null @@ -1,434 +0,0 @@ -import enum - - -def to_type(o, new_type): - ''' - Helper function that receives an object or a dict and convert it to a new given type. - - :param object|dict o: The object to convert - :param Type new_type: The type to convert to. - ''' - if new_type == type(o): - return o - else: - return new_type(**o) - - -class Position(object): - def __init__(self, line, character): - """ - Constructs a new Position instance. - - :param int line: Line position in a document (zero-based). - :param int character: Character offset on a line in a document (zero-based). - """ - self.line = line - self.character = character - - -class Range(object): - def __init__(self, start, end): - """ - Constructs a new Range instance. - - :param Position start: The range's start position. - :param Position end: The range's end position. - """ - self.start = to_type(start, Position) - self.end = to_type(end, Position) - - -class Location(object): - """ - Represents a location inside a resource, such as a line inside a text file. - """ - def __init__(self, uri, range): - """ - Constructs a new Location instance. - - :param str uri: Resource file. - :param Range range: The range inside the file - """ - self.uri = uri - self.range = to_type(range, Range) - - -class LocationLink(object): - """ - Represents a link between a source and a target location. - """ - def __init__(self, originSelectionRange, targetUri, targetRange, targetSelectionRange): - """ - Constructs a new LocationLink instance. - - :param Range originSelectionRange: Span of the origin of this link. - Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position. - :param str targetUri: The target resource identifier of this link. - :param Range targetRange: The full target range of this link. If the target for example is a symbol then target - range is the range enclosing this symbol not including leading/trailing whitespace but everything else - like comments. This information is typically used to highlight the range in the editor. - :param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed, - e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range` - """ - self.originSelectionRange = to_type(originSelectionRange, Range) - self.targetUri = targetUri - self.targetRange = to_type(targetRange, Range) - self.targetSelectionRange = to_type(targetSelectionRange, Range) - - -class Diagnostic(object): - def __init__(self, range, severity, code, source, message, relatedInformation): - """ - Constructs a new Diagnostic instance. - :param Range range: The range at which the message applies.Resource file. - :param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the - client to interpret diagnostics as error, warning, info or hint. - :param str code: The diagnostic's code, which might appear in the user interface. - :param str source: A human-readable string describing the source of this - diagnostic, e.g. 'typescript' or 'super lint'. - :param str message: The diagnostic's message. - :param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within - a scope collide all definitions can be marked via this property. - """ - self.range = range - self.severity = severity - self.code = code - self.source = source - self.message = message - self.relatedInformation = relatedInformation - - -class DiagnosticSeverity(object): - Error = 1 - Warning = 2 # TODO: warning is known in python - Information = 3 - Hint = 4 - - -class DiagnosticRelatedInformation(object): - def __init__(self, location, message): - """ - Constructs a new Diagnostic instance. - :param Location location: The location of this related diagnostic information. - :param str message: The message of this related diagnostic information. - """ - self.location = location - self.message = message - - -class Command(object): - def __init__(self, title, command, arguments): - """ - Constructs a new Diagnostic instance. - :param str title: Title of the command, like `save`. - :param str command: The identifier of the actual command handler. - :param list argusments: Arguments that the command handler should be invoked with. - """ - self.title = title - self.command = command - self.arguments = arguments - - -class TextDocumentItem(object): - """ - An item to transfer a text document from the client to the server. - """ - def __init__(self, uri, languageId, version, text): - """ - Constructs a new Diagnostic instance. - - :param DocumentUri uri: Title of the command, like `save`. - :param str languageId: The identifier of the actual command handler. - :param int version: Arguments that the command handler should be invoked with. - :param str text: Arguments that the command handler should be invoked with. - """ - self.uri = uri - self.languageId = languageId - self.version = version - self.text = text - - -class TextDocumentIdentifier(object): - """ - Text documents are identified using a URI. On the protocol level, URIs are passed as strings. - """ - def __init__(self, uri): - """ - Constructs a new TextDocumentIdentifier instance. - - :param DocumentUri uri: The text document's URI. - """ - self.uri = uri - - -class VersionedTextDocumentIdentifier(TextDocumentIdentifier): - """ - An identifier to denote a specific version of a text document. - """ - def __init__(self, uri, version): - """ - Constructs a new TextDocumentIdentifier instance. - - :param DocumentUri uri: The text document's URI. - :param int version: The version number of this document. If a versioned - text document identifier is sent from the server to the client and - the file is not open in the editor (the server has not received an - open notification before) the server can send `null` to indicate - that the version is known and the content on disk is the truth (as - speced with document content ownership). - The version number of a document will increase after each change, including - undo/redo. The number doesn't need to be consecutive. - """ - super(VersionedTextDocumentIdentifier, self).__init__(uri) - self.version = version - - -class TextDocumentContentChangeEvent(object): - """ - An event describing a change to a text document. If range and rangeLength are omitted - the new text is considered to be the full content of the document. - """ - def __init__(self, range, rangeLength, text): - """ - Constructs a new TextDocumentContentChangeEvent instance. - - :param Range range: The range of the document that changed. - :param int rangeLength: The length of the range that got replaced. - :param str text: The new text of the range/document. - """ - self.range = range - self.rangeLength = rangeLength - self.text = text - - -class TextDocumentPositionParams(object): - """ - A parameter literal used in requests to pass a text document and a position inside that document. - """ - def __init__(self, textDocument, position): - """ - Constructs a new TextDocumentPositionParams instance. - - :param TextDocumentIdentifier textDocument: The text document. - :param Position position: The position inside the text document. - """ - self.textDocument = textDocument - self.position = position - - -class ParameterInformation(object): - """ - Represents a parameter of a callable-signature. A parameter can - have a label and a doc-comment. - """ - def __init__(self, label, documentation=""): - """ - Constructs a new ParameterInformation instance. - - :param str label: The label of this parameter. Will be shown in the UI. - :param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted. - """ - self.label = label - self.documentation = documentation - - -class SignatureInformation(object): - """ - Represents the signature of something callable. A signature - can have a label, like a function-name, a doc-comment, and - a set of parameters. - """ - def __init__(self, label, documentation="", parameters=[]): - """ - Constructs a new SignatureInformation instance. - - :param str label: The label of this signature. Will be shown in the UI. - :param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted. - :param ParameterInformation[] parameters: The parameters of this signature. - """ - self.label = label - self.documentation = documentation - self.parameters = [to_type(parameter, ParameterInformation) for parameter in parameters] - - -class SignatureHelp(object): - """ - Signature help represents the signature of something - callable. There can be multiple signature but only one - active and only one active parameter. - """ - def __init__(self, signatures, activeSignature=0, activeParameter=0): - """ - Constructs a new SignatureHelp instance. - - :param SignatureInformation[] signatures: One or more signatures. - :param int activeSignature: - :param int activeParameter: - """ - self.signatures = [to_type(signature, SignatureInformation) for signature in signatures] - self.activeSignature = activeSignature - self.activeParameter = activeParameter - - -class CompletionTriggerKind(object): - Invoked = 1 - TriggerCharacter = 2 - TriggerForIncompleteCompletions = 3 - - -class CompletionContext(object): - """ - Contains additional information about the context in which a completion request is triggered. - """ - def __init__(self, triggerKind, triggerCharacter=None): - """ - Constructs a new CompletionContext instance. - - :param CompletionTriggerKind triggerKind: How the completion was triggered. - :param str triggerCharacter: The trigger character (a single character) that has trigger code complete. - Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` - """ - self.triggerKind = triggerKind - if triggerCharacter: - self.triggerCharacter = triggerCharacter - - -class TextEdit(object): - """ - A textual edit applicable to a text document. - """ - def __init__(self, range, newText): - """ - :param Range range: The range of the text document to be manipulated. To insert - text into a document create a range where start === end. - :param str newText: The string to be inserted. For delete operations use an empty string. - """ - self.range = range - self.newText = newText - - -class InsertTextFormat(object): - PlainText = 1 - Snippet = 2 - - -class CompletionItem(object): - """ - """ - def __init__(self, label, kind=None, detail=None, documentation=None, deprecated=None, preselect=None, sortText=None, filterText=None, insertText=None, insertTextFormat=None, textEdit=None, additionalTextEdits=None, commitCharacters=None, command=None, data=None, score=0.0): - """ - :param str label: The label of this completion item. By default also the text that is inserted when selecting - this completion. - :param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor. - :param str detail: A human-readable string with additional information about this item, like type or symbol information. - :param tr ocumentation: A human-readable string that represents a doc-comment. - :param bool deprecated: Indicates if this item is deprecated. - :param bool preselect: Select this item when showing. Note: that only one completion item can be selected and that the - tool / client decides which item that is. The rule is that the first item of those that match best is selected. - :param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used. - :param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used. - :param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used. - The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example - VS Code when code complete is requested in this example `con` and a completion item with an `insertText` of `console` is provided it - will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation. - @deprecated Use textEdit instead. - :param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property - and the `newText` property of a provided `textEdit`. - :param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored. - Note:* The range of the edit must be a single line range and it must contain the position at which completion - has been requested. - :param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion. - Edits must not overlap (including the same insert position) with the main edit nor with themselves. - Additional text edits should be used to change text unrelated to the current cursor position - (for example adding an import statement at the top of the file if the completion item will - insert an unqualified type). - :param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and - then type that character. *Note* that all commit characters should have `length=1` and that superfluous - characters will be ignored. - :param Command command: An optional command that is executed *after* inserting this completion. Note: that - additional modifications to the current document should be described with the additionalTextEdits-property. - :param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request. - :param float score: Score of the code completion item. - """ - self.label = label - self.kind = kind - self.detail = detail - self.documentation = documentation - self.deprecated = deprecated - self.preselect = preselect - self.sortText = sortText - self.filterText = filterText - self.insertText = insertText - self.insertTextFormat = insertTextFormat - self.textEdit = textEdit - self.additionalTextEdits = additionalTextEdits - self.commitCharacters = commitCharacters - self.command = command - self.data = data - self.score = score - - -class CompletionItemKind(enum.Enum): - Text = 1 - Method = 2 - Function = 3 - Constructor = 4 - Field = 5 - Variable = 6 - Class = 7 - Interface = 8 - Module = 9 - Property = 10 - Unit = 11 - Value = 12 - Enum = 13 - Keyword = 14 - Snippet = 15 - Color = 16 - File = 17 - Reference = 18 - Folder = 19 - EnumMember = 20 - Constant = 21 - Struct = 22 - Event = 23 - Operator = 24 - TypeParameter = 25 - - -class CompletionList(object): - """ - Represents a collection of [completion items](#CompletionItem) to be presented in the editor. - """ - def __init__(self, isIncomplete, items): - """ - Constructs a new CompletionContext instance. - - :param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list. - :param CompletionItem items: The completion items. - """ - self.isIncomplete = isIncomplete - self.items = [to_type(i, CompletionItem) for i in items] - -class ErrorCodes(enum.Enum): - # Defined by JSON RPC - ParseError = -32700 - InvalidRequest = -32600 - MethodNotFound = -32601 - InvalidParams = -32602 - InternalError = -32603 - serverErrorStart = -32099 - serverErrorEnd = -32000 - ServerNotInitialized = -32002 - UnknownErrorCode = -32001 - - # Defined by the protocol. - RequestCancelled = -32800 - ContentModified = -32801 - -class ResponseError(Exception): - def __init__(self, code, message, data = None): - self.code = code - self.message = message - if data: - self.data = data diff --git a/tests/test-workspace/lsp_client.py b/tests/test-workspace/lsp_client.py index efd7ffb..7219f4e 100644 --- a/tests/test-workspace/lsp_client.py +++ b/tests/test-workspace/lsp_client.py @@ -66,7 +66,7 @@ def exit(self): but before the client is sending any other request or notification to the server. The server can use the initialized notification for example to dynamically register capabilities. The initialized notification may only be sent once. """ - self.lsp_endpoint.send_notification("exit") + self.lsp_endpoint.send_ # notification("exit") def didOpen(self, textDocument): diff --git a/tests/test_json_rpc_endpoint.py b/tests/test_json_rpc_endpoint.py index 4999c4d..ea43a1e 100644 --- a/tests/test_json_rpc_endpoint.py +++ b/tests/test_json_rpc_endpoint.py @@ -51,7 +51,7 @@ def test_recv_wrong_header(): json_rpc_endpoint = pylspclient.JsonRpcEndpoint(None, pipein) pipeout.write('Contentength: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode("utf-8")) pipeout.flush() - with pytest.raises(pylspclient.lsp_structs.ResponseError): + with pytest.raises(pylspclient.lsp_errors.ResponseError): result = json_rpc_endpoint.recv_response() print("should never get here", result) @@ -63,7 +63,7 @@ def test_recv_missing_size(): json_rpc_endpoint = pylspclient.JsonRpcEndpoint(None, pipein) pipeout.write('Content-Type: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode("utf-8")) pipeout.flush() - with pytest.raises(pylspclient.lsp_structs.ResponseError): + with pytest.raises(pylspclient.lsp_errors.ResponseError): result = json_rpc_endpoint.recv_response() print("should never get here", result) diff --git a/tests/test_pylsp_integration.py b/tests/test_pylsp_integration.py index e1a46e3..68a8147 100644 --- a/tests/test_pylsp_integration.py +++ b/tests/test_pylsp_integration.py @@ -5,7 +5,7 @@ import threading import pylspclient -from pylspclient.lsp_pydantic_strcuts import TextDocumentIdentifier, TextDocumentItem, LanguageIdentifier, Position, Range +from pylspclient.lsp_pydantic_strcuts import TextDocumentIdentifier, TextDocumentItem, LanguageIdentifier, Position, Range, CompletionTriggerKind, CompletionContext def to_uri(path: str) -> str: @@ -174,3 +174,17 @@ def test_definition(lsp_client: pylspclient.LspClient): result_file_content = open(result_path, "r").read() result_definition = range_in_text_to_string(result_file_content, definitions[0].range) assert result_definition == "send_notification" + + +def test_completion(lsp_client: pylspclient.LspClient): + add_dir(lsp_client, DEFAULT_ROOT) + file_path = "lsp_client.py" + relative_file_path = path.join(DEFAULT_ROOT, file_path) + uri = to_uri(relative_file_path) + file_content = open(relative_file_path, "r").read() + to_complete = "send_" + position = string_in_text_to_position(file_content, to_complete + " ") + position.character += len(to_complete) + context = CompletionContext(triggerKind=CompletionTriggerKind.Invoked) + completion_result = lsp_client.completion(TextDocumentIdentifier(uri=uri), position, context) + assert all([i.insertText.startswith(to_complete) for i in completion_result.items])