def test_docengine_insert(): """ test Doc line insertion """ test_str = "test insert of line" doc = Doc() for c in test_str: doc.insert(0, c) assert doc.text == test_str[::-1]
def test_document_editor_do_copy(mock_msg_service, mock_get_app): # get sample patches doc = Doc() doc.site = 0 test_str = "Test string, quite a long one! Yeah, sure..." patches = [] for idx, c in enumerate(test_str): patches.append(doc.insert(idx, c)) msg_srv_instance = mock_msg_service.return_value() mock_msg_service.return_value.prepare_send_request.return_value = None mock_msg_service.return_value.put_message.return_value = None document_editor = DocumentEditor(msg_srv_instance) for patch in patches: document_editor.update_text(patch) document_editor.text_field.buffer.selection_state = \ SelectionState(4, SelectionType.CHARACTERS) document_editor.do_copy() # assert that document remains unchanged assert document_editor.doc.text == "Test string, quite a long one!" \ " Yeah, sure..." copied_data = mock_get_app.return_value.clipboard.set_data \ .call_args[0][0] # check if correct text was sent to clipboard assert copied_data.text == ' string, quite a long one! Yeah, sure...'
async def test_client_handler_new_patch(user_svc, file_svc): mock_client = MagicMock() doc = Doc() patch = doc.insert(0, "A") file_id = FileService.get_file_id("r", "test") msg = { "username": "******", "password": "******", "filename": "test", "type": "patch", "content": patch, "file_id": file_id } raw_msg = json.dumps(msg).encode("utf-8") mock_client.__aiter__.return_value = [raw_msg] mock_client.return_value.send.return_value = Future() mock_client.return_value.send.return_value.set_result("123") user_svc.auth_user.return_value = False MagicMock.__await__ = lambda x: async_magic().__await__() user_svc_instance = user_svc.return_value() user_svc_instance.auth_user.return_value = True user_svc_instance.has_access.return_value = True file_svc_instance = file_svc.return_value() file_svc_instance.register_patch.return_value = None client_handler = ClientHandler(user_svc_instance, file_svc_instance) client_handler.active_authors.append({ "connection": mock_client, "current_file": file_id }) await client_handler.handle_client(mock_client, None) response = mock_client.send.call_args.args[0] assert json.loads(response)["content"] == patch
def test_author_lexer_single_author(): """ Test that lexer returns single color and original string if there is only one author """ doc = Doc() doc.site = 0 test_str = "Test string" for idx, c in enumerate(test_str): doc.insert(idx, c) lexer = AuthorLexer(doc) lex_func = lexer.lex_document(None) colors, chars = zip(*lex_func(0)) assert len(set(colors)) == 1 assert "".join(chars) == test_str
def test_author_lexer_multiple_authors(): """ Test that lexer returns three colors for three different authors. :return: """ doc = Doc() doc.site = 0 author_inserts = ["first", "second", "third"] for insert in author_inserts: for idx, c in enumerate(insert): doc.insert(idx, c) doc.site += 1 lexer = AuthorLexer(doc) lex_func = lexer.lex_document(None) colors = [item[0] for item in lex_func(0)] assert len(set(colors)) == 3
def test_document_editor_do_del(mock_msg_service): # get sample patches doc = Doc() doc.site = 0 test_str = "Test string, quite a long one! Yeah, sure..." patches = [] for idx, c in enumerate(test_str): patches.append(doc.insert(idx, c)) msg_srv_instance = mock_msg_service.return_value() mock_msg_service.return_value.prepare_send_request.return_value = None mock_msg_service.return_value.put_message.return_value = None document_editor = DocumentEditor(msg_srv_instance) for patch in patches: document_editor.update_text(patch) document_editor.text_field.buffer.selection_state = \ SelectionState(4, SelectionType.CHARACTERS) document_editor.do_delete() assert document_editor.doc.text == "Test"
def try_load_file(path) -> List[str] or None: """ Try to load file and return it as list of encoded patches :param path: path to file :type path: Path :return: list of file patches encoded to string """ try: with open(path, 'r') as file: file_doc = Doc() file_doc.site = 0 pos = 0 result = [] for line in file: for char in line: result.append(file_doc.insert(pos, char)) pos += 1 return result except (OSError, IOError, FileNotFoundError): logging.info(f"Requested [{path}] was not found!") return None
class DocumentEditor: """ Document Editor responds to keypress events and copy/cut/paste/delete events by applying patches to internal CRDT document and then updating the changed text on outer TextEditor document object. """ WINDOWS_LINE_ENDING = '\r\n' UNIX_LINE_ENDING = '\n' CR_CHAR = '\r' def __init__(self, msg_service: MessageService): self.doc = Doc() self.doc.site = int(random.getrandbits(32)) self.patch_set = [] self.msg_service = msg_service self.text_field = TextEditor( scrollbar=True, line_numbers=False, search_field=SearchToolbar(), key_bindings=self.__init_bindings(), lexer=AuthorLexer(self.doc) ) def __register_patch(self, patch) -> None: """ Put provided patch to internal patch set and send patch to the server using Message Service :type patch: str """ self.patch_set.append(patch) message = self.msg_service.prepare_send_request({"type": "patch", "content": patch}) self.msg_service.put_message(message) def do_cut(self) -> None: """ Handle selection cut. """ new_doc, cut_data = self.__get_selection(cut=True) get_app().clipboard.set_data(cut_data) self.text_field.document = new_doc def do_copy(self) -> None: """ Handle selection copy """ _, cut_data = self.__get_selection(cut=False) get_app().clipboard.set_data(cut_data) def do_delete(self) -> None: """ Handle selection delete """ self.text_field.document, _ = self.__get_selection(cut=True) def do_paste(self) -> None: """ Handle paste from clipboard """ paste_text = get_app().clipboard.get_data().text # replace CRLF with LF paste_text = paste_text.replace(self.WINDOWS_LINE_ENDING, self.UNIX_LINE_ENDING) paste_text = paste_text.replace(self.CR_CHAR, self.UNIX_LINE_ENDING) cursor_pos = self.text_field.buffer.cursor_position for idx, char in enumerate(paste_text): self.text_field.buffer.text += str(idx) patch = self.doc.insert(cursor_pos + idx, char) self.__register_patch(patch) self.text_field.buffer.text = self.doc.text self.text_field.buffer.cursor_position += len(paste_text) def __get_selection(self, cut=False) -> Tuple[Document, ClipboardData]: """ Get selection from document selection and the resulting document. If cut is true, remove selection from internal CRDT document immediately. :param cut: cut selected part of document :type cut: bool :return: resulting document and selection text fragment """ if self.text_field.document.selection: cut_parts = [] remaining_parts = [] new_cursor_position = self.text_field.document.cursor_position last_end = 0 for start, end in self.text_field.document.selection_ranges(): if cut: # remove from internal doc for pos in range(end, start, -1): patch = self.doc.delete(pos - 1) self.__register_patch(patch) if last_end == 0: new_cursor_position = start remaining_parts.append( self.text_field.document.text[last_end:start]) cut_parts.append(self.text_field.document.text[start:end]) last_end = end remaining_parts.append(self.text_field.document.text[last_end:]) cut_text = "\n".join(cut_parts) return ( Document(text=self.doc.text, cursor_position=new_cursor_position), ClipboardData(cut_text, self.text_field.document.selection.type), ) else: return self.text_field.document, ClipboardData("") def __init_bindings(self) -> KeyBindings: """ Generate bindings that handles KeyPress events, make changes to internal doc and then displaying them in outer TextEdit buffer. :return: """ bindings = KeyBindings() @bindings.add('delete') def handle_delete(event: KeyPressEvent) -> None: """ Captures Delete KeyPress event and removes char next to cursor pos from internal Doc """ if not self.text_field.buffer.text: return if self.text_field.buffer.selection_state: self.do_delete() elif self.text_field.buffer.cursor_position \ != len(self.text_field.buffer.text): cursor_pos = self.text_field.buffer.cursor_position patch = self.doc.delete(cursor_pos) self.text_field.buffer.text = self.doc.text self.__register_patch(patch) @bindings.add('c-h') def handle_backspace(event: KeyPressEvent) -> None: """ Captures Backspace KeyPress event and removes char before cursor pos from internal Doc """ if not self.text_field.buffer.text or \ self.text_field.buffer.cursor_position == 0: return if self.text_field.buffer.selection_state: self.do_delete() else: cursor_pos = self.text_field.buffer.cursor_position patch = self.doc.delete(cursor_pos - 1) if cursor_pos != len(self.text_field.buffer.text): self.text_field.buffer.cursor_position -= 1 self.text_field.buffer.text = self.doc.text self.__register_patch(patch) @bindings.add('c-m') def handle_enter(event: KeyPressEvent) -> None: """ Captures Enter KeyPress events and applies it to internal Doc """ if self.text_field.buffer.selection_state: self.do_delete() cursor_pos = self.text_field.buffer.cursor_position patch = self.doc.insert(cursor_pos, self.UNIX_LINE_ENDING) self.text_field.buffer.text = self.doc.text self.text_field.buffer.cursor_position += 1 self.__register_patch(patch) @bindings.add('c-i') @bindings.add('<any>') def handle_text_enter(event: KeyPressEvent) -> None: """ Captures General / Tab KeyPress events and applies it to internal Doc """ if self.text_field.buffer.selection_state: self.do_delete() cursor_pos = self.text_field.buffer.cursor_position patch = self.doc.insert(cursor_pos, event.data) self.text_field.buffer.text = self.doc.text self.text_field.buffer.cursor_right() self.__register_patch(patch) @bindings.add(Keys.BracketedPaste) def handle_paste(event: KeyPressEvent) -> None: """ Handle paste event from terminal. :param event: :return: """ self.do_paste() return bindings def update_text(self, patch) -> None: """ Apply patch to internal document and update TextEdit window buffer. :param patch: raw patch :type patch: str """ json_patch = json.loads(patch) operation = json_patch["op"] if operation == "d": patch_pos = self.doc.get_real_position(patch) self.doc.apply_patch(patch) else: self.doc.apply_patch(patch) patch_pos = self.doc.get_real_position(patch) old_pos = self.text_field.buffer.cursor_position self.text_field.buffer.text = self.doc.text if patch_pos == -1 or patch_pos > old_pos + 1: self.text_field.buffer.cursor_position = old_pos else: if operation == "i": self.text_field.buffer.cursor_right() if operation == "d": self.text_field.buffer.cursor_left()