diff --git a/README.md b/README.md index d6c5fdc..d6f6ef8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,10 @@ In the Command Line, paste the following: `pip install salve_ipc` ## Description -Salve is an IPC library that can be used by code editors to get autocompletions, replacements, and syntax highlighting. +Salve is an IPC library that can be used by code editors to easily get autocompletions, replacements, and syntax highlighting. + +> **Note** +> The first time that the system is loaded or a new server needs to be started it will take a fair bit longer than if it is simply kept alive by pinging regularly. ## Documentation @@ -33,7 +36,7 @@ The `Token` dataclass gives easy type checking for tokens returned from the high The `hidden_chars` (`dict[str, str]`) dictionary holds a bunch of hidden (zero width) characters as keys and then names for them as values. `Token`'s of type "Hidden_Char" give the index to hidden characters and allow the user to display hidden characters to them that they may not see. These characters appear in code posted on forums or blogs by those hoping to prevent others from simply copy-pasting their code along with many other places. -### `Request` and `Response` TypedDict classes +### `Response` TypedDict classes The `Request` and `Response` TypedDict classes allow for type checking when handling output from salve_ipc. @@ -47,61 +50,42 @@ The `tokens_from_result()` function takes the results from a `highlight` command ### `IPC` Class -| Method | Description | Arguments | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `.ping()` | Pings the server. After five seconds the server closes if not pinged so it is better for performance to keep it alive but it will be reopened either way | None | -| `.get_response()` | Gets a response of the requested command | `command`: str | -| `.request()` | Makes a request to the server | `command`: str, `file`: str, `expected_keywords`: list[str] ("autocomple" or "replacements"), `current_word`: str ("autocomple" or "replacements"), `language`: str ("highlight") | -| `.cancel_request()` | Cancels request of command type and removes reponse if it was recieved. Must be called before `.get_response()` to work | `command`: str | -| `.update_file()` | Updates files stored on the server that are used to get responses | `filename`: str, `current_state`: str (just the text of the file) | -| `.remove_file()` | Removes a file of the name given if any exists. Note that a file should only be removed when sure that all other requests using the file are completed. If you delete a file right after a request you run the risk of it removing the file before the task could be run and causing a server crash. | `filename`: str | -| `.kill_IPC()` | This kills the IPC process and acts as a precaution against wasted CPU when the main thread no longer needs the IPC | None | +| Method | Description | Arguments | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.ping()` | Pings the server. After five seconds the server closes if not pinged so it is better for performance to keep it alive but it will be reopened either way | None | +| `.get_response()` | Gets a response of the requested command | `command`: str | +| `.request()` | Makes a request to the server | `command`: str, `file`: str, `expected_keywords`: list[str] ("autocomple" or "replacements"), `current_word`: str ("autocomple" or "replacements"), `language`: str ("highlight") | +| `.cancel_request()` | Cancels request of command type and removes reponse if it was recieved. Must be called before `.get_response()` to work | `command`: str | +| `.update_file()` | Updates files stored on the server that are used to get responses | `filename`: str, `current_state`: str (just the text of the file) | +| `.remove_file()` | Removes a file of the name given if any exists. Note that a file should only be removed when sure that all other requests using the file are completed. If you delete a file right after a request you run the risk of it removing the file before the task could be run and causing a server crash (`Request`'s go after `Notification`'s and `Ping`'s). | `filename`: str | +| `.kill_IPC()` | This kills the IPC process and acts as a precaution against wasted CPU when the main thread no longer needs the IPC | None | ### Basic Usage: ```python -from os import set_blocking -from selectors import EVENT_READ, DefaultSelector -from sys import stdin, stdout - -from salve_ipc import IPC, Response - -autocompleter = IPC() - -set_blocking(stdin.fileno(), False) -set_blocking(stdin.fileno(), False) -selector = DefaultSelector() -selector.register(stdin, EVENT_READ) - -stdout.write("Code: \n") -stdout.flush() - -while True: - # Keep IPC alive - autocompleter.ping() - - # Add file - autocompleter.add_file("test", "") - - # Check input - events = selector.select(0.025) - if events: - # Make requests - for line in stdin: - autocompleter.update_file("test", line) - autocompleter.request( - "autocomplete", - expected_keywords=[], - full_text=line, - current_word=line[-2], - ) - - # Check output - output: Response | None = autocompleter.get_response() - if not output: - continue - stdout.write(str(output) + "\n") - stdout.flush() +from time import sleep + +from salve_ipc import IPC, Response, tokens_from_result + +context = IPC() + +context.update_file( + "test", + open(__file__, "r+").read(), +) + +context.request( + "autocomplete", + file="test", + expected_keywords=[], + current_word="t", +) + +sleep(1) + +output: Response = context.get_response("autocomplete") # type: ignore +print(tokens_from_result(output["result"])) # type: ignore +context.kill_IPC() ``` ## How to run and contribute diff --git a/examples/example_usage.py b/examples/example_usage.py index 7f22e19..951bcef 100644 --- a/examples/example_usage.py +++ b/examples/example_usage.py @@ -46,3 +46,5 @@ # Write response stdout.write(str(output) + "\n") stdout.flush() + +context.kill_IPC() diff --git a/examples/gui_client.py b/examples/gui_client.py index 26553eb..50494f6 100644 --- a/examples/gui_client.py +++ b/examples/gui_client.py @@ -42,3 +42,4 @@ def ping_loop() -> None: root.after_idle(ping_loop) root.mainloop() +context.kill_IPC() diff --git a/examples/simple_autocomplete_example.py b/examples/simple_autocomplete_example.py new file mode 100644 index 0000000..90eb319 --- /dev/null +++ b/examples/simple_autocomplete_example.py @@ -0,0 +1,23 @@ +from time import sleep + +from salve_ipc import IPC, Response, tokens_from_result + +context = IPC() + +context.update_file( + "test", + open(__file__, "r+").read(), +) + +context.request( + "autocomplete", + file="test", + expected_keywords=[], + current_word="t", +) + +sleep(1) + +output: Response = context.get_response("autocomplete") # type: ignore +print(tokens_from_result(output["result"])) # type: ignore +context.kill_IPC() diff --git a/examples/simple_example.py b/examples/simple_example.py deleted file mode 100644 index 991a1b4..0000000 --- a/examples/simple_example.py +++ /dev/null @@ -1,22 +0,0 @@ -from time import sleep - -from salve_ipc import IPC, Response - -context = IPC() - -context.update_file( - "test", - "test file with testing words which should return test then testing", -) - -context.request( - "autocomplete", - file="test", - expected_keywords=[], - current_word="t", -) - -sleep(1) - -output: Response | None = context.get_response("autocomplete") -print(output) diff --git a/examples/simple_highlight_example.py b/examples/simple_highlight_example.py new file mode 100644 index 0000000..09dff52 --- /dev/null +++ b/examples/simple_highlight_example.py @@ -0,0 +1,17 @@ +from time import sleep + +from salve_ipc import IPC, Response, tokens_from_result + +context = IPC() + +context.update_file( + "test", + open(__file__, "r+").read(), +) + +context.request("highlight", file="test", language="python") + +sleep(1) +output: Response | None = context.get_response("highlight") +print(tokens_from_result(output["result"])) # type: ignore +context.kill_IPC() diff --git a/examples/simple_replacements_example.py b/examples/simple_replacements_example.py new file mode 100644 index 0000000..c978fbf --- /dev/null +++ b/examples/simple_replacements_example.py @@ -0,0 +1,19 @@ +from time import sleep + +from salve_ipc import IPC, Response + +context = IPC() + +context.update_file( + "test", + open(__file__, "r+").read(), +) + +context.request( + "replacements", file="test", expected_keywords=[], current_word="contest" +) + +sleep(1) +output: Response | None = context.get_response("replacements") +print(output["result"]) # type: ignore +context.kill_IPC() diff --git a/salve_ipc/highlight/highlight.py b/salve_ipc/highlight/highlight.py index 8013e90..68e4c81 100644 --- a/salve_ipc/highlight/highlight.py +++ b/salve_ipc/highlight/highlight.py @@ -7,7 +7,8 @@ from pygments.token import _TokenType default_tokens: list[str] = [ - "Token.Text" "Token.Text.Whitespace", + "Token.Text.Whitespace", + "Token.Text", "Token.Error", "Token.Keyword", "Token.Name", @@ -20,8 +21,8 @@ "Token.Generic", ] generic_tokens: list[str] = [ - "Text", "Whitespace", + "Text", "Error", "Keyword", "Name", @@ -67,7 +68,7 @@ def get_new_token_type(old_token: str) -> str: """Turns pygments token types into a generic predefined Token""" new_type: str = generic_tokens[0] for index, token in enumerate(default_tokens): - if token.startswith(old_token): + if old_token.startswith(token): new_type = generic_tokens[index] break return new_type @@ -162,25 +163,24 @@ def find_hidden_chars(lines: list[str]) -> list[Token]: def get_highlights(full_text: str, language: str = "text") -> list[Token]: """Gets pygments tokens from text provided in language proved and converts them to Token's""" lexer: Lexer = get_lexer_by_name(language) + split_text: list[str] = full_text.splitlines() new_tokens: list[Token] = [] - og_tokens: list[tuple[_TokenType, str]] = list(lex(full_text, lexer)) start_index: tuple[int, int] = (1, 0) - for token in og_tokens: - new_type: str = get_new_token_type(str(token[0])) - token_str: str = token[1] - token_len: int = len(token_str) - new_token = Token(start_index, token_len, new_type) - new_tokens.append(new_token) + for line in split_text: + og_tokens: list[tuple[_TokenType, str]] = list(lex(line, lexer)) + for token in og_tokens: + new_type: str = get_new_token_type(str(token[0])) + token_str: str = token[1] - if token_str == "\n": - start_index = (start_index[0] + 1, 0) - continue + token_len: int = len(token_str) + new_token = Token(start_index, token_len, new_type) + new_tokens.append(new_token) - start_index = (start_index[0], start_index[1] + token_len) + start_index = (start_index[0], start_index[1] + token_len) + start_index = (start_index[0] + 1, 0) # Add extra token types - split_text: list[str] = full_text.splitlines() new_tokens += get_urls(split_text) new_tokens += find_hidden_chars(split_text) diff --git a/salve_ipc/ipc.py b/salve_ipc/ipc.py index 6fee77f..4e0f563 100644 --- a/salve_ipc/ipc.py +++ b/salve_ipc/ipc.py @@ -1,11 +1,20 @@ from json import dumps, loads -from os import set_blocking +from os import remove, set_blocking from pathlib import Path from random import randint from subprocess import PIPE, Popen +from tempfile import NamedTemporaryFile, _TemporaryFileWrapper from typing import IO -from .misc import COMMANDS, Message, Notification, Ping, Request, Response +from .misc import ( + COMMANDS, + Details, + Message, + Notification, + Ping, + Request, + Response, +) class IPC: @@ -19,7 +28,7 @@ class IPC: """ def __init__(self, id_max: int = 15_000) -> None: - self.used_ids: list[int] = [] + self.all_ids: dict[int, str] = {} # id, tempfile path self.id_max = id_max self.current_ids: dict[str, int] = {} self.newest_responses: dict[str, Response | None] = {} @@ -57,6 +66,10 @@ def get_server_file(self, file: str) -> IO: return self.main_server.stdout # type: ignore return self.main_server.stdin # type: ignore + def write_tmp_file(self, details: Details, tmp_path: str) -> None: + with open(tmp_path, "r+") as file: + file.write(dumps(details)) + def send_message(self, message: Message) -> None: """Sends a Message to the main_server as provided by the argument message - internal API""" json_request: str = dumps(message) @@ -68,38 +81,49 @@ def send_message(self, message: Message) -> None: def create_message(self, type: str, **kwargs) -> None: """Creates a Message based on the args and kwawrgs provided. Highly flexible. - internal API""" id = randint(1, self.id_max) # 0 is reserved for the empty case - while id in self.used_ids: + while id in list(self.all_ids.keys()): id = randint(1, self.id_max) - self.used_ids.append(id) + tmp: _TemporaryFileWrapper[str] = NamedTemporaryFile( + prefix="salve_ipc", suffix=".tmp", delete=False, mode="r+" + ) + self.all_ids[id] = tmp.name match type: case "ping": - ping: Ping = {"id": id, "type": "ping"} + ping: Ping = {"id": id, "type": "ping", "tmp_file": tmp.name} self.send_message(ping) case "request": command = kwargs.get("command", "") self.current_ids[command] = id - request: Request = { - "id": id, - "type": type, + request_details: Request = { "command": command, "file": kwargs.get("file"), "expected_keywords": kwargs.get("expected_keywords"), "current_word": kwargs.get("current_word"), "language": kwargs.get("language"), } # type: ignore - self.send_message(request) - case "notification": - notification: Notification = { + request: Message = { "id": id, "type": type, + "tmp_file": tmp.name, + } + self.write_tmp_file(request_details, tmp.name) + self.send_message(request) + case "notification": + notification_details: Notification = { "remove": kwargs.get("remove", False), - "filename": kwargs.get("filename", ""), + "file": kwargs.get("filename", ""), "contents": kwargs.get("contents", ""), } + notification: Message = { + "id": id, + "type": type, + "tmp_file": tmp.name, + } + self.write_tmp_file(notification_details, tmp.name) self.send_message(notification) case _: - ping: Ping = {"id": id, "type": "ping"} + ping: Ping = {"id": id, "type": "ping", "tmp_file": tmp.name} self.send_message(ping) def ping(self) -> None: @@ -121,6 +145,10 @@ def request( f"Command {command} not in builtin commands. Those are {COMMANDS}!" ) + if file not in self.files: + self.kill_IPC() + raise Exception(f"File {file} does not exist in system!") + self.create_message( type="request", command=command, @@ -142,19 +170,21 @@ def cancel_request(self, command: str): def parse_line(self, line: str) -> None: """Parses main_server output line and discards useless responses - internal API""" - response_json: Response = loads(line) + response_json: Message = loads(line) + details: Response = loads(open(response_json["tmp_file"], "r+").read()) id = response_json["id"] - self.used_ids.remove(id) + self.all_ids.pop(id) + remove(response_json["tmp_file"]) - if "command" not in response_json: + if "command" not in details: return - command = response_json["command"] + command = details["command"] if id != self.current_ids[command]: return self.current_ids[command] = 0 - self.newest_responses[command] = response_json + self.newest_responses[command] = details def check_responses(self) -> None: """Checks all main_server output by calling IPC.parse_line() on each response - internal API""" diff --git a/salve_ipc/misc.py b/salve_ipc/misc.py index 1c63b82..2710a6a 100644 --- a/salve_ipc/misc.py +++ b/salve_ipc/misc.py @@ -7,10 +7,23 @@ class Message(TypedDict): """Base class for messages in and out of the server""" id: int - type: str # Can be "ping", "request", "response", "cancelled", "notification" + type: str # Can be "ping", "request", "response", "notification" + tmp_file: str # Not checked on pings -class Request(Message): +class Ping(Message): + """Not really different from a standard Message but the Ping type allows for nice differentiation""" + + ... + + +class Details(TypedDict): + """These are the details held by the tmp_file""" + + ... + + +class Request(Details): """Request results/output from the server with command specific input""" command: str # Can only be commands in COMMANDS @@ -20,23 +33,17 @@ class Request(Message): language: NotRequired[str] # highlight -class Ping(Message): - """Not really different from a standard Message but the Ping type allows for nice differentiation""" - - ... - - -class Notification(Message): +class Notification(Details): """Notifies the server to add/update/remove a file for usage in fulfilling commands""" - filename: str + file: str remove: bool contents: NotRequired[str] -class Response(Message): +class Response(Details): """Server responses to requests, notifications, and pings""" cancelled: bool command: NotRequired[str] - result: NotRequired[list[str] | tuple[tuple[int, int], int, str]] + result: NotRequired[list[str | tuple[tuple[int, int], int, str]]] diff --git a/salve_ipc/server.py b/salve_ipc/server.py index f50cb55..c320cbf 100644 --- a/salve_ipc/server.py +++ b/salve_ipc/server.py @@ -5,7 +5,7 @@ from time import time from highlight import Token, get_highlights -from misc import COMMANDS, Message, Request, Response +from misc import COMMANDS, Details, Message, Notification, Request, Response from server_functions import find_autocompletions, get_replacements @@ -18,7 +18,7 @@ def __init__(self) -> None: self.selector = DefaultSelector() self.selector.register(stdin, EVENT_READ) - self.id_list: list[int] = [] + self.all_ids: dict[int, str] = {} # id, tmp path self.newest_ids: dict[str, int] = {} self.newest_requests: dict[str, Request | None] = {} for command in COMMANDS: @@ -29,53 +29,69 @@ def __init__(self) -> None: self.old_time = time() - def write_response(self, response: Response) -> None: - stdout.write(dumps(response) + "\n") + def write_tmp_file(self, details: Details, tmp_path: str) -> None: + with open(tmp_path, "r+") as file: + file.truncate() + file.write(dumps(details)) + file.flush() + + def write_message(self, message: Message) -> None: + stdout.write(dumps(message) + "\n") stdout.flush() - def cancel_id(self, id: int) -> None: - response: Response = {"id": id, "type": "response", "cancelled": True} - self.write_response(response) + def simple_id_response( + self, id: int, tmp_path: str, cancelled: bool = True + ) -> None: + response_details: Response = { + "cancelled": cancelled, + } + response: Message = { + "id": id, + "type": "response", + "tmp_file": tmp_path, + } + self.write_tmp_file(response_details, tmp_path) + self.write_message(response) def parse_line(self, line: str) -> None: json_input: Message = loads(line) id: int = json_input["id"] match json_input["type"]: case "ping": - self.cancel_id(id) + self.simple_id_response(id, json_input["tmp_file"], False) case "notification": - filename: str = json_input["filename"] # type: ignore - if json_input["remove"]: # type: ignore + notification_details: Notification = loads( + open(json_input["tmp_file"], "r+").read() + ) + filename: str = notification_details["file"] + if notification_details["remove"]: self.files.pop(filename) return - contents: str = json_input["contents"] # type: ignore + contents: str = notification_details["contents"] # type: ignore self.files[filename] = contents + self.simple_id_response(id, json_input["tmp_file"], False) case _: - self.id_list.append(id) - command: str = json_input["command"] # type: ignore + request_details: Request = loads( + open(json_input["tmp_file"], "r+").read() + ) + self.all_ids[id] = json_input["tmp_file"] + command: str = request_details["command"] # type: ignore self.newest_ids[command] = id - self.newest_requests[command] = json_input # type: ignore + self.newest_requests[command] = request_details # type: ignore def cancel_all_ids_except_newest(self) -> None: - for id in self.id_list: + for id in list(self.all_ids.keys()): if id in list(self.newest_ids.values()): continue - self.cancel_id(id) + self.simple_id_response(id, self.all_ids.pop(id)) def handle_request(self, request: Request) -> None: + command: str = request["command"] + id: int = self.newest_ids[command] file: str = request["file"] result: list[str | tuple[tuple[int, int], int, str]] = [] - command: str = request["command"] cancelled: bool = False - - if file not in self.files: - response: Response = { - "id": request["id"], - "type": "response", - "cancelled": True, - "command": command, - "result": result, # type: ignore - } + tmp_file: str = self.all_ids.pop(id) match request["command"]: case "autocomplete": @@ -94,19 +110,22 @@ def handle_request(self, request: Request) -> None: pre_refined_result: list[Token] = get_highlights( full_text=self.files[file], language=request["language"] # type: ignore ) - result = [] - result += [token.to_tuple() for token in pre_refined_result] + result += [token.to_tuple() for token in pre_refined_result] # type: ignore case _: cancelled = True - response: Response = { - "id": request["id"], - "type": "response", + response_details: Response = { "cancelled": cancelled, "command": command, "result": result, # type: ignore } - self.write_response(response) + response_message: Message = { + "id": id, + "type": "response", + "tmp_file": tmp_file, + } + self.write_tmp_file(response_details, response_message["tmp_file"]) + self.write_message(response_message) self.newest_ids[command] = 0 def run_tasks(self) -> None: @@ -140,8 +159,6 @@ def run_tasks(self) -> None: command: str = request["command"] self.newest_requests[command] = None - self.id_list = [] - if __name__ == "__main__": handler = Handler() diff --git a/setup.py b/setup.py index cdf8b7e..7cbfdd0 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="salve_ipc", - version="0.2.2", + version="0.3.0", description="A module that makes easily provides autocompletions, replacement suggestions, and syntax highlighting to your code editor", author="Moosems", author_email="moosems.j@gmail.com", diff --git a/tests/highlight_output.json b/tests/highlight_output.json new file mode 100644 index 0000000..46ee912 --- /dev/null +++ b/tests/highlight_output.json @@ -0,0 +1,53 @@ +{ + "cancelled": false, + "command": "highlight", + "result": [ + [[1, 0], 4, "Keyword"], + [[1, 4], 1, "Text"], + [[1, 5], 4, "Name"], + [[1, 9], 1, "Text"], + [[1, 10], 6, "Keyword"], + [[1, 16], 1, "Text"], + [[1, 17], 1, "Name"], + [[1, 18], 2, "Text"], + [[1, 20], 12, "Comment"], + [[1, 32], 1, "Whitespace"], + [[2, 0], 1, "Whitespace"], + [[3, 0], 3, "Name"], + [[3, 3], 1, "Text"], + [[3, 4], 1, "Operator"], + [[3, 5], 1, "Text"], + [[3, 6], 3, "Name"], + [[3, 9], 2, "Text"], + [[3, 11], 7, "Comment"], + [[3, 18], 1, "Whitespace"], + [[4, 0], 1, "Whitespace"], + [[5, 0], 1, "Whitespace"], + [[6, 0], 5, "Keyword"], + [[6, 5], 1, "Text"], + [[6, 6], 3, "Name"], + [[6, 9], 1, "Punctuation"], + [[6, 10], 3, "Name"], + [[6, 13], 1, "Punctuation"], + [[6, 14], 1, "Punctuation"], + [[6, 15], 1, "Whitespace"], + [[7, 0], 4, "Text"], + [[7, 4], 3, "Keyword"], + [[7, 7], 1, "Text"], + [[7, 8], 8, "Name"], + [[7, 16], 1, "Punctuation"], + [[7, 17], 4, "Name"], + [[7, 21], 1, "Punctuation"], + [[7, 22], 1, "Punctuation"], + [[7, 23], 1, "Whitespace"], + [[8, 0], 8, "Text"], + [[8, 8], 4, "Keyword"], + [[8, 12], 1, "Whitespace"], + [[9, 0], 1, "Whitespace"], + [[10, 0], 1, "Whitespace"], + [[11, 0], 3, "Name"], + [[11, 3], 1, "Punctuation"], + [[11, 4], 1, "Punctuation"], + [[11, 5], 1, "Whitespace"] + ] +} diff --git a/tests/test_file.py b/tests/test_file.py new file mode 100644 index 0000000..e11998d --- /dev/null +++ b/tests/test_file.py @@ -0,0 +1,11 @@ +from this import s # noqa: F401 + +Bar = int # alias + + +class Foo(Bar): + def __init__(self): + pass + + +Foo() diff --git a/tests/test_ipc.py b/tests/test_ipc.py index 7ddfed5..0704b71 100644 --- a/tests/test_ipc.py +++ b/tests/test_ipc.py @@ -1,36 +1,53 @@ +from json import loads from time import sleep from salve_ipc import IPC, Response def test_IPC(): - autocompleter = IPC() + context = IPC() - autocompleter.ping() + context.ping() - autocompleter.update_file("test.py", "testy\n") + context.update_file("test", open("tests/test_file.py", "r+").read()) - autocompleter.request( + context.request( "autocomplete", + file="test", expected_keywords=[], - file="test.py", current_word="t", ) + context.request( + "replacements", + file="test", + expected_keywords=[], + current_word="thid", + ) + context.request("highlight", file="test", language="python") sleep(1) # Check output - output: Response = autocompleter.get_response("autocomplete") # type: ignore - output["id"] = 0 - assert output == { - "id": 0, - "type": "response", + autocomplete_output: Response | None = context.get_response("autocomplete") + assert autocomplete_output == { "cancelled": False, "command": "autocomplete", - "result": ["testy"], + "result": ["this"], + } + + replacements_output: Response | None = context.get_response("replacements") + assert replacements_output == { + "cancelled": False, + "command": "replacements", + "result": ["this"], } - autocompleter.remove_file("test.py") + highlight_output: Response | None = context.get_response("highlight") + assert highlight_output == loads( + open("tests/highlight_output.json").read() + ) + context.remove_file("test") + context.kill_IPC() test_IPC()