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_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 __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)
     )
Exemple #5
0
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
Exemple #6
0
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
 def try_save_file(path, history) -> bool:
     """
     Try to save file of the user to the filesystem.
     Creates a new document, applies all the patches, then saves
     the resulting document's text to file.
     :param path: path to save
     :param history: patch history
     :type path: Path
     :type history: List[str]
     :return: True if successful, otherwise False
     """
     file_doc = Doc()
     file_doc.site = 0
     for patch in history:
         file_doc.apply_patch(patch)
     try:
         with open(path, 'w') as file:
             file.write(file_doc.text)
         return True
     except (OSError, IOError, FileNotFoundError):
         logging.info(f"Requested [{path}] was not found!")
         return False
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()