Skip to content

Commit df786e5

Browse files
committed
views: Add support for copying code snippets.
Fixes #1123. Introduces support for copying code snippets in the message information popup. Makes use of the CodeSnippetButton class and its methods. Tests added.
1 parent bebee81 commit df786e5

File tree

2 files changed

+142
-1
lines changed

2 files changed

+142
-1
lines changed

tests/ui_tools/test_popups.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
467467
message=self.message,
468468
topic_links=OrderedDict(),
469469
message_links=OrderedDict(),
470+
code_snippets=list(),
470471
time_mentions=list(),
471472
title="Full Rendered Message",
472473
)
@@ -519,6 +520,7 @@ def test_keypress_show_msg_info(
519520
msg=self.message,
520521
topic_links=OrderedDict(),
521522
message_links=OrderedDict(),
523+
code_snippets=list(),
522524
time_mentions=list(),
523525
)
524526

@@ -543,6 +545,7 @@ def mock_external_classes(self, mocker: MockerFixture, msg_box: MessageBox) -> N
543545
message=self.message,
544546
topic_links=OrderedDict(),
545547
message_links=OrderedDict(),
548+
code_snippets=list(),
546549
time_mentions=list(),
547550
title="Full Raw Message",
548551
)
@@ -595,6 +598,7 @@ def test_keypress_show_msg_info(
595598
msg=self.message,
596599
topic_links=OrderedDict(),
597600
message_links=OrderedDict(),
601+
code_snippets=list(),
598602
time_mentions=list(),
599603
)
600604

@@ -618,6 +622,7 @@ def mock_external_classes(self, mocker: MockerFixture) -> None:
618622
message=self.message,
619623
topic_links=OrderedDict(),
620624
message_links=OrderedDict(),
625+
code_snippets=list(),
621626
time_mentions=list(),
622627
title="Edit History",
623628
)
@@ -666,6 +671,7 @@ def test_keypress_show_msg_info(
666671
msg=self.message,
667672
topic_links=OrderedDict(),
668673
message_links=OrderedDict(),
674+
code_snippets=list(),
669675
time_mentions=list(),
670676
)
671677

@@ -944,6 +950,7 @@ def mock_external_classes(
944950
OrderedDict(),
945951
OrderedDict(),
946952
list(),
953+
list(),
947954
)
948955

949956
def test_init(self, message_fixture: Message) -> None:
@@ -961,6 +968,7 @@ def test_pop_up_info_order(self, message_fixture: Message) -> None:
961968
title="Message Information",
962969
topic_links=topic_links,
963970
message_links=message_links,
971+
code_snippets=list(),
964972
time_mentions=list(),
965973
)
966974
msg_links = msg_info_view.button_widgets
@@ -1009,6 +1017,7 @@ def test_keypress_edit_history(
10091017
title="Message Information",
10101018
topic_links=OrderedDict(),
10111019
message_links=OrderedDict(),
1020+
code_snippets=list(),
10121021
time_mentions=list(),
10131022
)
10141023
size = widget_size(msg_info_view)
@@ -1020,6 +1029,7 @@ def test_keypress_edit_history(
10201029
message=message_fixture,
10211030
topic_links=OrderedDict(),
10221031
message_links=OrderedDict(),
1032+
code_snippets=list(),
10231033
time_mentions=list(),
10241034
)
10251035
else:
@@ -1038,6 +1048,7 @@ def test_keypress_full_rendered_message(
10381048
title="Message Information",
10391049
topic_links=OrderedDict(),
10401050
message_links=OrderedDict(),
1051+
code_snippets=list(),
10411052
time_mentions=list(),
10421053
)
10431054
size = widget_size(msg_info_view)
@@ -1048,6 +1059,7 @@ def test_keypress_full_rendered_message(
10481059
message=message_fixture,
10491060
topic_links=OrderedDict(),
10501061
message_links=OrderedDict(),
1062+
code_snippets=list(),
10511063
time_mentions=list(),
10521064
)
10531065

@@ -1064,6 +1076,7 @@ def test_keypress_full_raw_message(
10641076
title="Message Information",
10651077
topic_links=OrderedDict(),
10661078
message_links=OrderedDict(),
1079+
code_snippets=list(),
10671080
time_mentions=list(),
10681081
)
10691082
size = widget_size(msg_info_view)
@@ -1074,6 +1087,7 @@ def test_keypress_full_raw_message(
10741087
message=message_fixture,
10751088
topic_links=OrderedDict(),
10761089
message_links=OrderedDict(),
1090+
code_snippets=list(),
10771091
time_mentions=list(),
10781092
)
10791093

@@ -1176,6 +1190,7 @@ def test_height_reactions(
11761190
OrderedDict(),
11771191
OrderedDict(),
11781192
list(),
1193+
list(),
11791194
)
11801195
# 12 = 7 labels + 2 blank lines + 1 'Reactions' (category)
11811196
# + 4 reactions (excluding 'Message Links').
@@ -1229,6 +1244,58 @@ def test_create_link_buttons(
12291244
assert link_w._wrapped_widget.attr_map == expected_attr_map
12301245
assert link_width == expected_link_width
12311246

1247+
@pytest.mark.parametrize(
1248+
[
1249+
"initial_code_snippet",
1250+
"expected_code",
1251+
"expected_attr_map",
1252+
"expected_focus_map",
1253+
"expected_code_width",
1254+
],
1255+
[
1256+
(
1257+
[
1258+
(
1259+
"Python",
1260+
[
1261+
("pygments:k", "def"),
1262+
("pygments:w", " "),
1263+
("pygments:nf", "main"),
1264+
("pygments:p", "()"),
1265+
("pygments:w", "\n "),
1266+
("pygments:nb", "print"),
1267+
("pygments:p", "("),
1268+
("pygments:s2", '"Hello"'),
1269+
("pygments:p", ")"),
1270+
("pygments:w", "\n"),
1271+
],
1272+
)
1273+
],
1274+
'1: Python\ndef main()\n print("Hello")...',
1275+
{None: "popup_contrast"},
1276+
{None: "selected"},
1277+
95,
1278+
)
1279+
],
1280+
ids=["with_code_snippet"],
1281+
)
1282+
def test_create_code_snippet_buttons(
1283+
self,
1284+
initial_code_snippet: List[Tuple[str, List[Tuple[str, str]]]],
1285+
expected_code: str,
1286+
expected_attr_map: Dict[None, str],
1287+
expected_focus_map: Dict[None, str],
1288+
expected_code_width: int,
1289+
) -> None:
1290+
[code_w], copy_code_width = self.msg_info_view.create_code_snippet_buttons(
1291+
self.controller, initial_code_snippet
1292+
)
1293+
1294+
assert code_w._wrapped_widget.original_widget.text == expected_code
1295+
assert code_w._wrapped_widget.focus_map == expected_focus_map
1296+
assert code_w._wrapped_widget.attr_map == expected_attr_map
1297+
assert copy_code_width == expected_code_width
1298+
12321299

12331300
class TestStreamInfoView:
12341301
@pytest.fixture(autouse=True)

zulipterminal/ui_tools/views.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from zulipterminal.server_url import near_message_url
4646
from zulipterminal.ui_tools.boxes import PanelSearchBox
4747
from zulipterminal.ui_tools.buttons import (
48+
CodeSnippetButton,
4849
EmojiButton,
4950
HomeButton,
5051
MentionedButton,
@@ -1548,11 +1549,13 @@ def __init__(
15481549
title: str,
15491550
topic_links: Dict[str, Tuple[str, int, bool]],
15501551
message_links: Dict[str, Tuple[str, int, bool]],
1552+
code_snippets: List[Tuple[str, List[Tuple[str, str]]]],
15511553
time_mentions: List[Tuple[str, str]],
15521554
) -> None:
15531555
self.msg = msg
15541556
self.topic_links = topic_links
15551557
self.message_links = message_links
1558+
self.code_snippets = code_snippets
15561559
self.time_mentions = time_mentions
15571560
self.server_url = controller.model.server_url
15581561
date_and_time = controller.model.formatted_local_time(
@@ -1568,6 +1571,9 @@ def __init__(
15681571
full_raw_message_keys = "[{}]".format(
15691572
", ".join(map(str, keys_for_command("FULL_RAW_MESSAGE")))
15701573
)
1574+
copy_code_keys = "[{}]".format(
1575+
", ".join(map(str, keys_for_command("COPY_CODE_SNIPPET")))
1576+
)
15711577
msg_info = [
15721578
(
15731579
"",
@@ -1586,6 +1592,7 @@ def __init__(
15861592
("Open in web browser", view_in_browser_keys),
15871593
("Full rendered message", full_rendered_message_keys),
15881594
("Full raw message", full_raw_message_keys),
1595+
("Copy code to clipboard", copy_code_keys),
15891596
],
15901597
)
15911598
msg_info.append(viewing_actions)
@@ -1607,6 +1614,8 @@ def __init__(
16071614
msg_info.append(("Topic Links", []))
16081615
if time_mentions:
16091616
msg_info.append(("Time mentions", time_mentions))
1617+
if code_snippets:
1618+
msg_info.append(("Code Snippets", []))
16101619
if msg["reactions"]:
16111620
reactions = sorted(
16121621
(reaction["emoji_name"], reaction["user"]["full_name"])
@@ -1644,7 +1653,6 @@ def __init__(
16441653
widgets[:slice_index] + message_link_widgets + widgets[slice_index:]
16451654
)
16461655
popup_width = max(popup_width, message_link_width)
1647-
16481656
if topic_links:
16491657
topic_link_widgets, topic_link_width = self.create_link_buttons(
16501658
controller, topic_links
@@ -1660,6 +1668,21 @@ def __init__(
16601668
widgets = widgets[:slice_index] + topic_link_widgets + widgets[slice_index:]
16611669
popup_width = max(popup_width, topic_link_width)
16621670

1671+
if code_snippets:
1672+
(
1673+
code_snippets_widgets,
1674+
code_snippets_width,
1675+
) = self.create_code_snippet_buttons(controller, code_snippets)
1676+
# slice_index = Number of labels before code snippets + 1 newline
1677+
# + 1 'Code Snippets' category label.
1678+
slice_index = len(msg_info[0][1]) + len(msg_info[1][1]) + 2 + 2
1679+
slice_index += sum([len(w) + 2 for w in self.button_widgets])
1680+
self.button_widgets.append(code_snippets)
1681+
widgets = (
1682+
widgets[:slice_index] + code_snippets_widgets + widgets[slice_index:]
1683+
)
1684+
popup_width = max(popup_width, code_snippets_width)
1685+
16631686
super().__init__(controller, widgets, "MSG_INFO", popup_width, title)
16641687

16651688
@staticmethod
@@ -1689,12 +1712,52 @@ def create_link_buttons(
16891712

16901713
return link_widgets, link_width
16911714

1715+
def create_code_snippet_buttons(
1716+
self, controller: Any, code_snippets: List[Tuple[str, List[Tuple[str, str]]]]
1717+
) -> Tuple[List[Any], int]:
1718+
code_snippet_widgets = []
1719+
code_snippet_width = 0
1720+
1721+
for index, snippet in enumerate(code_snippets):
1722+
language, snippet_list = snippet
1723+
language = "" if language is None else language
1724+
display_code, copy_code = CodeSnippetButton.get_code_from_snippet(
1725+
self, snippet_list
1726+
)
1727+
if display_code:
1728+
code_snippet_width = max(
1729+
code_snippet_width, len(max(display_code, key=len))
1730+
)
1731+
display_code[-1] = (
1732+
display_code[-1][0],
1733+
display_code[-1][1].rstrip("\n"),
1734+
)
1735+
caption = f"{str(index+1)}: {language}\n"
1736+
1737+
display_attr = None if index % 2 else "popup_contrast"
1738+
display_code = [("pygments:w", caption)] + display_code
1739+
code_snippet_widgets.append(
1740+
CodeSnippetButton(
1741+
controller=controller,
1742+
caption=caption,
1743+
display_code=display_code,
1744+
copy_code=copy_code,
1745+
display_attr=display_attr,
1746+
)
1747+
)
1748+
code = caption + str(snip[1] for snip in snippet_list)
1749+
code_snippet_width = max(
1750+
code_snippet_width, len(max(code.split("\n"), key=len))
1751+
)
1752+
return code_snippet_widgets, code_snippet_width
1753+
16921754
def keypress(self, size: urwid_Size, key: str) -> str:
16931755
if is_command_key("EDIT_HISTORY", key) and self.show_edit_history_label:
16941756
self.controller.show_edit_history(
16951757
message=self.msg,
16961758
topic_links=self.topic_links,
16971759
message_links=self.message_links,
1760+
code_snippets=self.code_snippets,
16981761
time_mentions=self.time_mentions,
16991762
)
17001763
elif is_command_key("VIEW_IN_BROWSER", key):
@@ -1705,6 +1768,7 @@ def keypress(self, size: urwid_Size, key: str) -> str:
17051768
message=self.msg,
17061769
topic_links=self.topic_links,
17071770
message_links=self.message_links,
1771+
code_snippets=self.code_snippets,
17081772
time_mentions=self.time_mentions,
17091773
)
17101774
return key
@@ -1713,6 +1777,7 @@ def keypress(self, size: urwid_Size, key: str) -> str:
17131777
message=self.msg,
17141778
topic_links=self.topic_links,
17151779
message_links=self.message_links,
1780+
code_snippets=self.code_snippets,
17161781
time_mentions=self.time_mentions,
17171782
)
17181783
return key
@@ -1762,13 +1827,15 @@ def __init__(
17621827
message: Message,
17631828
topic_links: Dict[str, Tuple[str, int, bool]],
17641829
message_links: Dict[str, Tuple[str, int, bool]],
1830+
code_snippets: List[Tuple[str, List[Tuple[str, str]]]],
17651831
time_mentions: List[Tuple[str, str]],
17661832
title: str,
17671833
) -> None:
17681834
self.controller = controller
17691835
self.message = message
17701836
self.topic_links = topic_links
17711837
self.message_links = message_links
1838+
self.code_snippets = code_snippets
17721839
self.time_mentions = time_mentions
17731840
width = 64
17741841
widgets: List[Any] = []
@@ -1867,6 +1934,7 @@ def keypress(self, size: urwid_Size, key: str) -> str:
18671934
msg=self.message,
18681935
topic_links=self.topic_links,
18691936
message_links=self.message_links,
1937+
code_snippets=self.code_snippets,
18701938
time_mentions=self.time_mentions,
18711939
)
18721940
return key
@@ -1880,13 +1948,15 @@ def __init__(
18801948
message: Message,
18811949
topic_links: Dict[str, Tuple[str, int, bool]],
18821950
message_links: Dict[str, Tuple[str, int, bool]],
1951+
code_snippets: List[Tuple[str, List[Tuple[str, str]]]],
18831952
time_mentions: List[Tuple[str, str]],
18841953
title: str,
18851954
) -> None:
18861955
self.controller = controller
18871956
self.message = message
18881957
self.topic_links = topic_links
18891958
self.message_links = message_links
1959+
self.code_snippets = code_snippets
18901960
self.time_mentions = time_mentions
18911961
max_cols, max_rows = controller.maximum_popup_dimensions()
18921962

@@ -1911,6 +1981,7 @@ def keypress(self, size: urwid_Size, key: str) -> str:
19111981
msg=self.message,
19121982
topic_links=self.topic_links,
19131983
message_links=self.message_links,
1984+
code_snippets=self.code_snippets,
19141985
time_mentions=self.time_mentions,
19151986
)
19161987
return key
@@ -1924,13 +1995,15 @@ def __init__(
19241995
message: Message,
19251996
topic_links: Dict[str, Tuple[str, int, bool]],
19261997
message_links: Dict[str, Tuple[str, int, bool]],
1998+
code_snippets: List[Tuple[str, List[Tuple[str, str]]]],
19271999
time_mentions: List[Tuple[str, str]],
19282000
title: str,
19292001
) -> None:
19302002
self.controller = controller
19312003
self.message = message
19322004
self.topic_links = topic_links
19332005
self.message_links = message_links
2006+
self.code_snippets = code_snippets
19342007
self.time_mentions = time_mentions
19352008
max_cols, max_rows = controller.maximum_popup_dimensions()
19362009

@@ -1961,6 +2034,7 @@ def keypress(self, size: urwid_Size, key: str) -> str:
19612034
msg=self.message,
19622035
topic_links=self.topic_links,
19632036
message_links=self.message_links,
2037+
code_snippets=self.code_snippets,
19642038
time_mentions=self.time_mentions,
19652039
)
19662040
return key

0 commit comments

Comments
 (0)