Skip to content

Commit dfb2e39

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 a74e4cf commit dfb2e39

File tree

7 files changed

+232
-6
lines changed

7 files changed

+232
-6
lines changed

docs/hotkeys.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
|Toggle star status of the current message|<kbd>Ctrl</kbd> + <kbd>s</kbd> / <kbd>*</kbd>|
6262
|Show/hide message information|<kbd>i</kbd>|
6363
|Show/hide message sender information|<kbd>u</kbd>|
64+
|Show/hide edit history (from message information)|<kbd>e</kbd>|
65+
|View current message in browser (from message information)|<kbd>v</kbd>|
66+
|Show/hide full rendered message (from message information)|<kbd>f</kbd>|
67+
|Show/hide full raw message (from message information)|<kbd>r</kbd>|
68+
|Copy code block to clipboard (from message information)|<kbd>c</kbd>|
6469

6570
## Stream list actions
6671
|Command|Key Combination|

tests/ui_tools/test_popups.py

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
506506
topic_links=OrderedDict(),
507507
message_links=OrderedDict(),
508508
time_mentions=list(),
509+
code_blocks=list(),
509510
title="Full Rendered Message",
510511
)
511512

@@ -558,6 +559,7 @@ def test_keypress_show_msg_info(
558559
topic_links=OrderedDict(),
559560
message_links=OrderedDict(),
560561
time_mentions=list(),
562+
code_blocks=list(),
561563
)
562564

563565

@@ -582,6 +584,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
582584
topic_links=OrderedDict(),
583585
message_links=OrderedDict(),
584586
time_mentions=list(),
587+
code_blocks=list(),
585588
title="Full Raw Message",
586589
)
587590

@@ -634,6 +637,7 @@ def test_keypress_show_msg_info(
634637
topic_links=OrderedDict(),
635638
message_links=OrderedDict(),
636639
time_mentions=list(),
640+
code_blocks=list(),
637641
)
638642

639643

@@ -657,6 +661,7 @@ def mock_external_classes(self, mocker: MockerFixture) -> None:
657661
topic_links=OrderedDict(),
658662
message_links=OrderedDict(),
659663
time_mentions=list(),
664+
code_blocks=list(),
660665
title="Edit History",
661666
)
662667

@@ -705,6 +710,7 @@ def test_keypress_show_msg_info(
705710
topic_links=OrderedDict(),
706711
message_links=OrderedDict(),
707712
time_mentions=list(),
713+
code_blocks=list(),
708714
)
709715

710716
@pytest.mark.parametrize(
@@ -982,6 +988,7 @@ def mock_external_classes(
982988
OrderedDict(),
983989
OrderedDict(),
984990
list(),
991+
list(),
985992
)
986993

987994
def test_init(self, message_fixture: Message) -> None:
@@ -1000,6 +1007,7 @@ def test_pop_up_info_order(self, message_fixture: Message) -> None:
10001007
topic_links=topic_links,
10011008
message_links=message_links,
10021009
time_mentions=list(),
1010+
code_blocks=list(),
10031011
)
10041012
msg_links = msg_info_view.button_widgets
10051013
assert msg_links == [message_links, topic_links]
@@ -1048,6 +1056,7 @@ def test_keypress_edit_history(
10481056
topic_links=OrderedDict(),
10491057
message_links=OrderedDict(),
10501058
time_mentions=list(),
1059+
code_blocks=list(),
10511060
)
10521061
size = widget_size(msg_info_view)
10531062

@@ -1059,6 +1068,7 @@ def test_keypress_edit_history(
10591068
topic_links=OrderedDict(),
10601069
message_links=OrderedDict(),
10611070
time_mentions=list(),
1071+
code_blocks=list(),
10621072
)
10631073
else:
10641074
self.controller.show_edit_history.assert_not_called()
@@ -1077,6 +1087,7 @@ def test_keypress_full_rendered_message(
10771087
topic_links=OrderedDict(),
10781088
message_links=OrderedDict(),
10791089
time_mentions=list(),
1090+
code_blocks=list(),
10801091
)
10811092
size = widget_size(msg_info_view)
10821093

@@ -1087,6 +1098,7 @@ def test_keypress_full_rendered_message(
10871098
topic_links=OrderedDict(),
10881099
message_links=OrderedDict(),
10891100
time_mentions=list(),
1101+
code_blocks=list(),
10901102
)
10911103

10921104
@pytest.mark.parametrize("key", keys_for_command("FULL_RAW_MESSAGE"))
@@ -1103,6 +1115,7 @@ def test_keypress_full_raw_message(
11031115
topic_links=OrderedDict(),
11041116
message_links=OrderedDict(),
11051117
time_mentions=list(),
1118+
code_blocks=list(),
11061119
)
11071120
size = widget_size(msg_info_view)
11081121

@@ -1113,6 +1126,7 @@ def test_keypress_full_raw_message(
11131126
topic_links=OrderedDict(),
11141127
message_links=OrderedDict(),
11151128
time_mentions=list(),
1129+
code_blocks=list(),
11161130
)
11171131

11181132
@pytest.mark.parametrize(
@@ -1141,13 +1155,14 @@ def test_keypress_view_in_browser(
11411155
assert self.controller.open_in_browser.called
11421156

11431157
def test_height_noreactions(self) -> None:
1144-
expected_height = 8
1145-
# 6 = 1 (date & time) +1 (sender's name) +1 (sender's email)
1158+
expected_height = 9
1159+
# 3 = 1 (date & time) +1 (sender's name) +1 (sender's email)
11461160
# +1 (display group header)
11471161
# +1 (whitespace column)
11481162
# +1 (view message in browser)
11491163
# +1 (full rendered message)
11501164
# +1 (full raw message)
1165+
# +1 (copy code block)
11511166
assert self.msg_info_view.height == expected_height
11521167

11531168
# FIXME This is the same parametrize as MessageBox:test_reactions_view
@@ -1214,10 +1229,11 @@ def test_height_reactions(
12141229
OrderedDict(),
12151230
OrderedDict(),
12161231
list(),
1232+
list(),
12171233
)
1218-
# 12 = 7 labels + 2 blank lines + 1 'Reactions' (category)
1234+
# 11 = 8 labels + 2 blank lines + 1 'Reactions' (category)
12191235
# + 4 reactions (excluding 'Message Links').
1220-
expected_height = 14
1236+
expected_height = 15
12211237
assert self.msg_info_view.height == expected_height
12221238

12231239
@pytest.mark.parametrize(
@@ -1267,6 +1283,118 @@ def test_create_link_buttons(
12671283
assert link_w._wrapped_widget.attr_map == expected_attr_map
12681284
assert link_width == expected_link_width
12691285

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

12711399
class TestStreamInfoView:
12721400
@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
@@ -451,6 +451,13 @@ class KeyBinding(TypedDict):
451451
'help_text': 'Show/hide full raw message',
452452
'key_category': 'msg_info',
453453
},
454+
'COPY_CODE_BLOCK': {
455+
'keys': ['c'],
456+
'help_text':
457+
'Copy code block to clipboard (from message information)',
458+
'excluded_from_random_tips': True,
459+
'key_category': 'msg_actions'
460+
},
454461
'NEW_HINT': {
455462
'keys': ['tab'],
456463
'help_text': 'New footer hotkey hint',

zulipterminal/core.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ def show_msg_info(
268268
topic_links: Dict[str, Tuple[str, int, bool]],
269269
message_links: Dict[str, Tuple[str, int, bool]],
270270
time_mentions: List[Tuple[str, str]],
271+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
271272
) -> None:
272273
msg_info_view = MsgInfoView(
273274
self,
@@ -276,6 +277,7 @@ def show_msg_info(
276277
topic_links,
277278
message_links,
278279
time_mentions,
280+
code_blocks,
279281
)
280282
self.show_pop_up(msg_info_view, "area:msg")
281283

@@ -347,6 +349,7 @@ def show_full_rendered_message(
347349
topic_links: Dict[str, Tuple[str, int, bool]],
348350
message_links: Dict[str, Tuple[str, int, bool]],
349351
time_mentions: List[Tuple[str, str]],
352+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
350353
) -> None:
351354
self.show_pop_up(
352355
FullRenderedMsgView(
@@ -355,6 +358,7 @@ def show_full_rendered_message(
355358
topic_links,
356359
message_links,
357360
time_mentions,
361+
code_blocks,
358362
f"Full rendered message {SCROLL_PROMPT}",
359363
),
360364
"area:msg",
@@ -366,6 +370,7 @@ def show_full_raw_message(
366370
topic_links: Dict[str, Tuple[str, int, bool]],
367371
message_links: Dict[str, Tuple[str, int, bool]],
368372
time_mentions: List[Tuple[str, str]],
373+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
369374
) -> None:
370375
self.show_pop_up(
371376
FullRawMsgView(
@@ -374,6 +379,7 @@ def show_full_raw_message(
374379
topic_links,
375380
message_links,
376381
time_mentions,
382+
code_blocks,
377383
f"Full raw message {SCROLL_PROMPT}",
378384
),
379385
"area:msg",
@@ -385,6 +391,7 @@ def show_edit_history(
385391
topic_links: Dict[str, Tuple[str, int, bool]],
386392
message_links: Dict[str, Tuple[str, int, bool]],
387393
time_mentions: List[Tuple[str, str]],
394+
code_blocks: List[Tuple[str, List[Tuple[str, str]]]],
388395
) -> None:
389396
self.show_pop_up(
390397
EditHistoryView(
@@ -393,6 +400,7 @@ def show_edit_history(
393400
topic_links,
394401
message_links,
395402
time_mentions,
403+
code_blocks,
396404
f"Edit History {SCROLL_PROMPT}",
397405
),
398406
"area:msg",

zulipterminal/ui_tools/buttons.py

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

755+
def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
756+
if is_command_key("COPY_CODE_BLOCK", key):
757+
urwid.emit_signal(
758+
self, "click", lambda button: self.copy_to_clipboard(self.block_list)
759+
)
760+
return super().keypress(size, key)
761+
755762

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

0 commit comments

Comments
 (0)