Skip to content

Commit

Permalink
core/views: Add support for copying code blocks.
Browse files Browse the repository at this point in the history
Introduces support for copying code blocks in the message information
popup. Makes use of the CodeBlockButton class and its methods.
Tests added.

Co-authored-by: kingjuno <[email protected]>
Co-authored-by: yuktasarode <[email protected]>

Fixes #1123.
  • Loading branch information
rsashank committed Sep 19, 2024
1 parent 87ac1c0 commit b5e97e4
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 6 deletions.
5 changes: 5 additions & 0 deletions docs/hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
|Toggle star status of the current message|<kbd>Ctrl</kbd> + <kbd>s</kbd> / <kbd>*</kbd>|
|Show/hide message information|<kbd>i</kbd>|
|Show/hide message sender information|<kbd>u</kbd>|
|Show/hide edit history (from message information)|<kbd>e</kbd>|
|View current message in browser (from message information)|<kbd>v</kbd>|
|Show/hide full rendered message (from message information)|<kbd>f</kbd>|
|Show/hide full raw message (from message information)|<kbd>r</kbd>|
|Copy code block to clipboard (from message information)|<kbd>c</kbd>|

## Stream list actions
|Command|Key Combination|
Expand Down
136 changes: 132 additions & 4 deletions tests/ui_tools/test_popups.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
title="Full Rendered Message",
)

Expand Down Expand Up @@ -558,6 +559,7 @@ def test_keypress_show_msg_info(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)


Expand All @@ -582,6 +584,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
title="Full Raw Message",
)

Expand Down Expand Up @@ -634,6 +637,7 @@ def test_keypress_show_msg_info(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)


Expand All @@ -657,6 +661,7 @@ def mock_external_classes(self, mocker: MockerFixture) -> None:
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
title="Edit History",
)

Expand Down Expand Up @@ -705,6 +710,7 @@ def test_keypress_show_msg_info(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)

@pytest.mark.parametrize(
Expand Down Expand Up @@ -982,6 +988,7 @@ def mock_external_classes(
OrderedDict(),
OrderedDict(),
list(),
list(),
)

def test_init(self, message_fixture: Message) -> None:
Expand All @@ -1000,6 +1007,7 @@ def test_pop_up_info_order(self, message_fixture: Message) -> None:
topic_links=topic_links,
message_links=message_links,
time_mentions=list(),
code_blocks=list(),
)
msg_links = msg_info_view.button_widgets
assert msg_links == [message_links, topic_links]
Expand Down Expand Up @@ -1048,6 +1056,7 @@ def test_keypress_edit_history(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)
size = widget_size(msg_info_view)

Expand All @@ -1059,6 +1068,7 @@ def test_keypress_edit_history(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)
else:
self.controller.show_edit_history.assert_not_called()
Expand All @@ -1077,6 +1087,7 @@ def test_keypress_full_rendered_message(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)
size = widget_size(msg_info_view)

Expand All @@ -1087,6 +1098,7 @@ def test_keypress_full_rendered_message(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)

@pytest.mark.parametrize("key", keys_for_command("FULL_RAW_MESSAGE"))
Expand All @@ -1103,6 +1115,7 @@ def test_keypress_full_raw_message(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)
size = widget_size(msg_info_view)

Expand All @@ -1113,6 +1126,7 @@ def test_keypress_full_raw_message(
topic_links=OrderedDict(),
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
)

@pytest.mark.parametrize(
Expand Down Expand Up @@ -1141,13 +1155,14 @@ def test_keypress_view_in_browser(
assert self.controller.open_in_browser.called

def test_height_noreactions(self) -> None:
expected_height = 8
# 6 = 1 (date & time) +1 (sender's name) +1 (sender's email)
expected_height = 9
# 3 = 1 (date & time) +1 (sender's name) +1 (sender's email)
# +1 (display group header)
# +1 (whitespace column)
# +1 (view message in browser)
# +1 (full rendered message)
# +1 (full raw message)
# +1 (copy code block)
assert self.msg_info_view.height == expected_height

# FIXME This is the same parametrize as MessageBox:test_reactions_view
Expand Down Expand Up @@ -1214,10 +1229,11 @@ def test_height_reactions(
OrderedDict(),
OrderedDict(),
list(),
list(),
)
# 12 = 7 labels + 2 blank lines + 1 'Reactions' (category)
# 11 = 8 labels + 2 blank lines + 1 'Reactions' (category)
# + 4 reactions (excluding 'Message Links').
expected_height = 14
expected_height = 15
assert self.msg_info_view.height == expected_height

@pytest.mark.parametrize(
Expand Down Expand Up @@ -1267,6 +1283,118 @@ def test_create_link_buttons(
assert link_w._wrapped_widget.attr_map == expected_attr_map
assert link_width == expected_link_width

@pytest.mark.parametrize(
[
"initial_code_block",
"expected_code",
"expected_attr_map",
"expected_focus_map",
],
[
(
[
(
"Python",
[
("pygments:k", "def"),
("pygments:w", " "),
("pygments:nf", "main"),
("pygments:p", "()"),
("pygments:w", "\n "),
("pygments:nb", "print"),
("pygments:p", "("),
("pygments:s2", '"Hello"'),
("pygments:p", ")"),
("pygments:w", "\n"),
],
)
],
'1: Python\ndef main()\n print("Hello")...',
{None: "popup_contrast"},
{None: "selected"},
),
(
[
(
"JavaScript",
[
("pygments:nx", "console"),
("pygments:p", "."),
("pygments:nx", "log"),
("pygments:p", "("),
("pygments:s2", '"Hello, world!"'),
("pygments:p", ");"),
("pygments:w", "\n"),
],
)
],
'1: JavaScript\nconsole.log("Hello, world!");',
{None: "popup_contrast"},
{None: "selected"},
),
(
[
(
"C++",
[
("pygments:cp", "#include"),
("pygments:w", " "),
("pygments:cpf", "<iostream>"),
("pygments:w", "\n\n"),
("pygments:kt", "int"),
("pygments:w", " "),
("pygments:nf", "main"),
("pygments:p", "()"),
("pygments:w", " "),
("pygments:p", "{"),
("pygments:w", "\n"),
("pygments:w", " "),
("pygments:n", "std"),
("pygments:o", "::"),
("pygments:n", "cout"),
("pygments:w", " "),
("pygments:o", "<<"),
("pygments:w", " "),
("pygments:s", '"Hello World!"'),
("pygments:p", ";"),
("pygments:w", "\n"),
("pygments:w", " "),
("pygments:k", "return"),
("pygments:w", " "),
("pygments:mi", "0"),
("pygments:p", ";"),
("pygments:w", "\n"),
("pygments:p", "}"),
("pygments:w", "\n"),
],
)
],
"1: C++\n#include <iostream>\n\nint main() {...",
{None: "popup_contrast"},
{None: "selected"},
),
],
ids=[
"with_python_code_block_two_lines",
"with_javascript_code_block_one_line",
"with_cpp_code_block_more_than_two_lines",
],
)
def test_create_code_block_buttons(
self,
initial_code_block: List[Tuple[str, List[Tuple[str, str]]]],
expected_code: str,
expected_attr_map: Dict[None, str],
expected_focus_map: Dict[None, str],
) -> None:
[code_w], _ = self.msg_info_view.create_code_block_buttons(
self.controller, initial_code_block
)

assert code_w._wrapped_widget.original_widget.text == expected_code
assert code_w._wrapped_widget.focus_map == expected_focus_map
assert code_w._wrapped_widget.attr_map == expected_attr_map


class TestStreamInfoView:
@pytest.fixture(autouse=True)
Expand Down
2 changes: 1 addition & 1 deletion tools/lint-hotkeys
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ SCRIPT_NAME = PurePath(__file__).name
HELP_TEXT_STYLE = re.compile(r"^[a-zA-Z /()',&@#:_-]*$")

# Exclude keys from duplicate keys checking
KEYS_TO_EXCLUDE = ["q", "e", "m", "r", "Esc"]
KEYS_TO_EXCLUDE = ["q", "e", "m", "r", "Esc", "c"]


def main(fix: bool) -> None:
Expand Down
7 changes: 7 additions & 0 deletions zulipterminal/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,13 @@ class KeyBinding(TypedDict):
'help_text': 'Show/hide full raw message',
'key_category': 'msg_info',
},
'COPY_CODE_BLOCK': {
'keys': ['c'],
'help_text':
'Copy code block to clipboard (from message information)',
'excluded_from_random_tips': True,
'key_category': 'msg_actions'
},
'NEW_HINT': {
'keys': ['tab'],
'help_text': 'New footer hotkey hint',
Expand Down
8 changes: 8 additions & 0 deletions zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ def show_msg_info(
topic_links: Dict[str, Tuple[str, int, bool]],
message_links: Dict[str, Tuple[str, int, bool]],
time_mentions: List[Tuple[str, str]],
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
) -> None:
msg_info_view = MsgInfoView(
self,
Expand All @@ -276,6 +277,7 @@ def show_msg_info(
topic_links,
message_links,
time_mentions,
code_blocks,
)
self.show_pop_up(msg_info_view, "area:msg")

Expand Down Expand Up @@ -347,6 +349,7 @@ def show_full_rendered_message(
topic_links: Dict[str, Tuple[str, int, bool]],
message_links: Dict[str, Tuple[str, int, bool]],
time_mentions: List[Tuple[str, str]],
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
) -> None:
self.show_pop_up(
FullRenderedMsgView(
Expand All @@ -355,6 +358,7 @@ def show_full_rendered_message(
topic_links,
message_links,
time_mentions,
code_blocks,
f"Full rendered message {SCROLL_PROMPT}",
),
"area:msg",
Expand All @@ -366,6 +370,7 @@ def show_full_raw_message(
topic_links: Dict[str, Tuple[str, int, bool]],
message_links: Dict[str, Tuple[str, int, bool]],
time_mentions: List[Tuple[str, str]],
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
) -> None:
self.show_pop_up(
FullRawMsgView(
Expand All @@ -374,6 +379,7 @@ def show_full_raw_message(
topic_links,
message_links,
time_mentions,
code_blocks,
f"Full raw message {SCROLL_PROMPT}",
),
"area:msg",
Expand All @@ -385,6 +391,7 @@ def show_edit_history(
topic_links: Dict[str, Tuple[str, int, bool]],
message_links: Dict[str, Tuple[str, int, bool]],
time_mentions: List[Tuple[str, str]],
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
) -> None:
self.show_pop_up(
EditHistoryView(
Expand All @@ -393,6 +400,7 @@ def show_edit_history(
topic_links,
message_links,
time_mentions,
code_blocks,
f"Edit History {SCROLL_PROMPT}",
),
"area:msg",
Expand Down
7 changes: 7 additions & 0 deletions zulipterminal/ui_tools/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,13 @@ def extract_display_code(self, code_block_list: List[Tuple[str, str]]) -> None:
)
self.display_code = [("pygments:w", self.caption)] + self.display_code

def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
if is_command_key("COPY_CODE_BLOCK", key):
urwid.emit_signal(
self, "click", lambda button: self.copy_to_clipboard(self.block_list)
)
return super().keypress(size, key)


class EditModeButton(urwid.Button):
def __init__(self, *, controller: Any, width: int) -> None:
Expand Down
Loading

0 comments on commit b5e97e4

Please sign in to comment.