Skip to content

Commit 33bfce1

Browse files
committed
core/views: Add support for copying code blocks.
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.
1 parent 5f8cb1d commit 33bfce1

File tree

7 files changed

+228
-6
lines changed

7 files changed

+228
-6
lines changed

docs/hotkeys.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
|View current message in browser (from message information)|<kbd>v</kbd>|
6767
|Show/hide full rendered message (from message information)|<kbd>f</kbd>|
6868
|Show/hide full raw message (from message information)|<kbd>r</kbd>|
69+
|Copy code block to clipboard (from message information)|<kbd>c</kbd>|
6970

7071
## Stream list actions
7172
|Command|Key Combination|

tests/ui_tools/test_popups.py

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
503503
topic_links=OrderedDict(),
504504
message_links=OrderedDict(),
505505
time_mentions=list(),
506+
code_blocks=list(),
506507
title="Full Rendered Message",
507508
)
508509

@@ -555,6 +556,7 @@ def test_keypress_show_msg_info(
555556
topic_links=OrderedDict(),
556557
message_links=OrderedDict(),
557558
time_mentions=list(),
559+
code_blocks=list(),
558560
)
559561

560562

@@ -579,6 +581,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
579581
topic_links=OrderedDict(),
580582
message_links=OrderedDict(),
581583
time_mentions=list(),
584+
code_blocks=list(),
582585
title="Full Raw Message",
583586
)
584587

@@ -631,6 +634,7 @@ def test_keypress_show_msg_info(
631634
topic_links=OrderedDict(),
632635
message_links=OrderedDict(),
633636
time_mentions=list(),
637+
code_blocks=list(),
634638
)
635639

636640

@@ -654,6 +658,7 @@ def mock_external_classes(self, mocker: MockerFixture) -> None:
654658
topic_links=OrderedDict(),
655659
message_links=OrderedDict(),
656660
time_mentions=list(),
661+
code_blocks=list(),
657662
title="Edit History",
658663
)
659664

@@ -702,6 +707,7 @@ def test_keypress_show_msg_info(
702707
topic_links=OrderedDict(),
703708
message_links=OrderedDict(),
704709
time_mentions=list(),
710+
code_blocks=list(),
705711
)
706712

707713
@pytest.mark.parametrize(
@@ -979,6 +985,7 @@ def mock_external_classes(
979985
OrderedDict(),
980986
OrderedDict(),
981987
list(),
988+
list(),
982989
)
983990

984991
def test_init(self, message_fixture: Message) -> None:
@@ -997,6 +1004,7 @@ def test_pop_up_info_order(self, message_fixture: Message) -> None:
9971004
topic_links=topic_links,
9981005
message_links=message_links,
9991006
time_mentions=list(),
1007+
code_blocks=list(),
10001008
)
10011009
msg_links = msg_info_view.button_widgets
10021010
assert msg_links == [message_links, topic_links]
@@ -1045,6 +1053,7 @@ def test_keypress_edit_history(
10451053
topic_links=OrderedDict(),
10461054
message_links=OrderedDict(),
10471055
time_mentions=list(),
1056+
code_blocks=list(),
10481057
)
10491058
size = widget_size(msg_info_view)
10501059

@@ -1056,6 +1065,7 @@ def test_keypress_edit_history(
10561065
topic_links=OrderedDict(),
10571066
message_links=OrderedDict(),
10581067
time_mentions=list(),
1068+
code_blocks=list(),
10591069
)
10601070
else:
10611071
self.controller.show_edit_history.assert_not_called()
@@ -1074,6 +1084,7 @@ def test_keypress_full_rendered_message(
10741084
topic_links=OrderedDict(),
10751085
message_links=OrderedDict(),
10761086
time_mentions=list(),
1087+
code_blocks=list(),
10771088
)
10781089
size = widget_size(msg_info_view)
10791090

@@ -1084,6 +1095,7 @@ def test_keypress_full_rendered_message(
10841095
topic_links=OrderedDict(),
10851096
message_links=OrderedDict(),
10861097
time_mentions=list(),
1098+
code_blocks=list(),
10871099
)
10881100

10891101
@pytest.mark.parametrize("key", keys_for_command("FULL_RAW_MESSAGE"))
@@ -1100,6 +1112,7 @@ def test_keypress_full_raw_message(
11001112
topic_links=OrderedDict(),
11011113
message_links=OrderedDict(),
11021114
time_mentions=list(),
1115+
code_blocks=list(),
11031116
)
11041117
size = widget_size(msg_info_view)
11051118

@@ -1110,6 +1123,7 @@ def test_keypress_full_raw_message(
11101123
topic_links=OrderedDict(),
11111124
message_links=OrderedDict(),
11121125
time_mentions=list(),
1126+
code_blocks=list(),
11131127
)
11141128

11151129
@pytest.mark.parametrize(
@@ -1138,13 +1152,14 @@ def test_keypress_view_in_browser(
11381152
assert self.controller.open_in_browser.called
11391153

11401154
def test_height_noreactions(self) -> None:
1141-
expected_height = 8
1142-
# 6 = 1 (date & time) +1 (sender's name) +1 (sender's email)
1155+
expected_height = 9
1156+
# 3 = 1 (date & time) +1 (sender's name) +1 (sender's email)
11431157
# +1 (display group header)
11441158
# +1 (whitespace column)
11451159
# +1 (view message in browser)
11461160
# +1 (full rendered message)
11471161
# +1 (full raw message)
1162+
# +1 (copy code block)
11481163
assert self.msg_info_view.height == expected_height
11491164

11501165
# FIXME This is the same parametrize as MessageBox:test_reactions_view
@@ -1211,10 +1226,11 @@ def test_height_reactions(
12111226
OrderedDict(),
12121227
OrderedDict(),
12131228
list(),
1229+
list(),
12141230
)
1215-
# 12 = 7 labels + 2 blank lines + 1 'Reactions' (category)
1231+
# 11 = 8 labels + 2 blank lines + 1 'Reactions' (category)
12161232
# + 4 reactions (excluding 'Message Links').
1217-
expected_height = 14
1233+
expected_height = 15
12181234
assert self.msg_info_view.height == expected_height
12191235

12201236
@pytest.mark.parametrize(
@@ -1264,6 +1280,118 @@ def test_create_link_buttons(
12641280
assert link_w._wrapped_widget.attr_map == expected_attr_map
12651281
assert link_width == expected_link_width
12661282

1283+
@pytest.mark.parametrize(
1284+
[
1285+
"initial_code_block",
1286+
"expected_code",
1287+
"expected_attr_map",
1288+
"expected_focus_map",
1289+
],
1290+
[
1291+
(
1292+
[
1293+
(
1294+
"Python",
1295+
[
1296+
("pygments:k", "def"),
1297+
("pygments:w", " "),
1298+
("pygments:nf", "main"),
1299+
("pygments:p", "()"),
1300+
("pygments:w", "\n "),
1301+
("pygments:nb", "print"),
1302+
("pygments:p", "("),
1303+
("pygments:s2", '"Hello"'),
1304+
("pygments:p", ")"),
1305+
("pygments:w", "\n"),
1306+
],
1307+
)
1308+
],
1309+
'1: Python\ndef main()\n print("Hello")...',
1310+
{None: "popup_contrast"},
1311+
{None: "selected"},
1312+
),
1313+
(
1314+
[
1315+
(
1316+
"JavaScript",
1317+
[
1318+
("pygments:nx", "console"),
1319+
("pygments:p", "."),
1320+
("pygments:nx", "log"),
1321+
("pygments:p", "("),
1322+
("pygments:s2", '"Hello, world!"'),
1323+
("pygments:p", ");"),
1324+
("pygments:w", "\n"),
1325+
],
1326+
)
1327+
],
1328+
'1: JavaScript\nconsole.log("Hello, world!");',
1329+
{None: "popup_contrast"},
1330+
{None: "selected"},
1331+
),
1332+
(
1333+
[
1334+
(
1335+
"C++",
1336+
[
1337+
("pygments:cp", "#include"),
1338+
("pygments:w", " "),
1339+
("pygments:cpf", "<iostream>"),
1340+
("pygments:w", "\n\n"),
1341+
("pygments:kt", "int"),
1342+
("pygments:w", " "),
1343+
("pygments:nf", "main"),
1344+
("pygments:p", "()"),
1345+
("pygments:w", " "),
1346+
("pygments:p", "{"),
1347+
("pygments:w", "\n"),
1348+
("pygments:w", " "),
1349+
("pygments:n", "std"),
1350+
("pygments:o", "::"),
1351+
("pygments:n", "cout"),
1352+
("pygments:w", " "),
1353+
("pygments:o", "<<"),
1354+
("pygments:w", " "),
1355+
("pygments:s", '"Hello World!"'),
1356+
("pygments:p", ";"),
1357+
("pygments:w", "\n"),
1358+
("pygments:w", " "),
1359+
("pygments:k", "return"),
1360+
("pygments:w", " "),
1361+
("pygments:mi", "0"),
1362+
("pygments:p", ";"),
1363+
("pygments:w", "\n"),
1364+
("pygments:p", "}"),
1365+
("pygments:w", "\n"),
1366+
],
1367+
)
1368+
],
1369+
"1: C++\n#include <iostream>\n\nint main() {...",
1370+
{None: "popup_contrast"},
1371+
{None: "selected"},
1372+
),
1373+
],
1374+
ids=[
1375+
"with_python_code_block_two_lines",
1376+
"with_javascript_code_block_one_line",
1377+
"with_cpp_code_block_more_than_two_lines",
1378+
],
1379+
)
1380+
def test_create_code_block_buttons(
1381+
self,
1382+
initial_code_block: List[Tuple[str, List[Tuple[str, str]]]],
1383+
expected_code: str,
1384+
expected_attr_map: Dict[None, str],
1385+
expected_focus_map: Dict[None, str],
1386+
) -> None:
1387+
[code_w], _ = self.msg_info_view.create_code_block_buttons(
1388+
self.controller, initial_code_block
1389+
)
1390+
1391+
assert code_w._wrapped_widget.original_widget.text == expected_code
1392+
assert code_w._wrapped_widget.focus_map == expected_focus_map
1393+
assert code_w._wrapped_widget.attr_map == expected_attr_map
1394+
12671395

12681396
class TestStreamInfoView:
12691397
@pytest.fixture(autouse=True)

tools/lint-hotkeys

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ SCRIPT_NAME = PurePath(__file__).name
2323
HELP_TEXT_STYLE = re.compile(r"^[a-zA-Z /()',&@#:_-]*$")
2424

2525
# Exclude keys from duplicate keys checking
26-
KEYS_TO_EXCLUDE = ["q", "e", "m", "r", "Esc"]
26+
KEYS_TO_EXCLUDE = ["q", "e", "m", "r", "Esc", "c"]
2727

2828

2929
def main(fix: bool) -> None:

zulipterminal/config/keys.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,13 @@ class KeyBinding(TypedDict):
438438
'help_text': 'Show/hide full raw message (from message information)',
439439
'key_category': 'msg_actions',
440440
},
441+
'COPY_CODE_BLOCK': {
442+
'keys': ['c'],
443+
'help_text':
444+
'Copy code block to clipboard (from message information)',
445+
'excluded_from_random_tips': True,
446+
'key_category': 'msg_actions'
447+
},
441448
'NEW_HINT': {
442449
'keys': ['tab'],
443450
'help_text': 'New footer hotkey hint',

zulipterminal/core.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def show_msg_info(
264264
topic_links: Dict[str, Tuple[str, int, bool]],
265265
message_links: Dict[str, Tuple[str, int, bool]],
266266
time_mentions: List[Tuple[str, str]],
267+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
267268
) -> None:
268269
msg_info_view = MsgInfoView(
269270
self,
@@ -272,6 +273,7 @@ def show_msg_info(
272273
topic_links,
273274
message_links,
274275
time_mentions,
276+
code_blocks,
275277
)
276278
self.show_pop_up(msg_info_view, "area:msg")
277279

@@ -342,6 +344,7 @@ def show_full_rendered_message(
342344
topic_links: Dict[str, Tuple[str, int, bool]],
343345
message_links: Dict[str, Tuple[str, int, bool]],
344346
time_mentions: List[Tuple[str, str]],
347+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
345348
) -> None:
346349
self.show_pop_up(
347350
FullRenderedMsgView(
@@ -350,6 +353,7 @@ def show_full_rendered_message(
350353
topic_links,
351354
message_links,
352355
time_mentions,
356+
code_blocks,
353357
f"Full rendered message {SCROLL_PROMPT}",
354358
),
355359
"area:msg",
@@ -361,6 +365,7 @@ def show_full_raw_message(
361365
topic_links: Dict[str, Tuple[str, int, bool]],
362366
message_links: Dict[str, Tuple[str, int, bool]],
363367
time_mentions: List[Tuple[str, str]],
368+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
364369
) -> None:
365370
self.show_pop_up(
366371
FullRawMsgView(
@@ -369,6 +374,7 @@ def show_full_raw_message(
369374
topic_links,
370375
message_links,
371376
time_mentions,
377+
code_blocks,
372378
f"Full raw message {SCROLL_PROMPT}",
373379
),
374380
"area:msg",
@@ -380,6 +386,7 @@ def show_edit_history(
380386
topic_links: Dict[str, Tuple[str, int, bool]],
381387
message_links: Dict[str, Tuple[str, int, bool]],
382388
time_mentions: List[Tuple[str, str]],
389+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
383390
) -> None:
384391
self.show_pop_up(
385392
EditHistoryView(
@@ -388,6 +395,7 @@ def show_edit_history(
388395
topic_links,
389396
message_links,
390397
time_mentions,
398+
code_blocks,
391399
f"Edit History {SCROLL_PROMPT}",
392400
),
393401
"area:msg",

zulipterminal/ui_tools/buttons.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,13 @@ def extract_display_code(self, code_block_list: List[Tuple[str, str]]) -> None:
742742
)
743743
self.display_code = [("pygments:w", self.caption)] + self.display_code
744744

745+
def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
746+
if is_command_key("COPY_CODE_BLOCK", key):
747+
urwid.emit_signal(
748+
self, "click", lambda button: self.copy_to_clipboard(self.block_list)
749+
)
750+
return super().keypress(size, key)
751+
745752

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

0 commit comments

Comments
 (0)