Skip to content

Add support for copying code snippets #1455

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
|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>|
|Copy code block to clipboard (from message information)|<kbd>c</kbd>|

## Stream list actions
|Command|Key Combination|
Expand Down
73 changes: 72 additions & 1 deletion tests/ui_tools/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ def test_soup2markup(self, content, expected_markup, mocker):
server_url=SERVER_URL,
message_links=OrderedDict(),
time_mentions=list(),
code_blocks=list(),
bq_len=0,
)

Expand Down Expand Up @@ -1585,14 +1586,84 @@ def test_keypress_EDIT_MESSAGE(
# fmt: on
],
)
def test_transform_content(self, mocker, raw_html, expected_content):
def test_transform_content(self, raw_html, expected_content):
expected_content = expected_content.replace("{}", QUOTED_TEXT_MARKER)

content, *_ = MessageBox.transform_content(raw_html, SERVER_URL)

rendered_text = Text(content)
assert rendered_text.text == expected_content

@pytest.mark.parametrize(
"raw_html, expected_code_blocks",
[
(
"""<div class="codehilite" data-code-language="Python"><pre><span></span><code><span class="k">def</span> <span class="nf">foo</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="k">return</span><span class="p">(</span><span class="n">x</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div>""", # noqa: E501
[
(
"Python",
[
("pygments:k", "def"),
("pygments:w", " "),
("pygments:nf", "foo"),
("pygments:p", "("),
("pygments:n", "x"),
("pygments:p", "):"),
("pygments:w", "\n "),
("pygments:k", "return"),
("pygments:p", "("),
("pygments:n", "x"),
("pygments:o", "+"),
("pygments:mi", "1"),
("pygments:p", ")"),
("pygments:w", "\n"),
],
)
],
),
(
"""<div class="codehilite" data-code-language="JavaScript"><pre><span></span><code><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Hello, world!"</span><span class="p">);</span>
</code></pre></div>""", # noqa: E501
[
(
"JavaScript",
[
("pygments:nx", "console"),
("pygments:p", "."),
("pygments:nx", "log"),
("pygments:p", "("),
("pygments:s2", '"Hello, world!"'),
("pygments:p", ");"),
("pygments:w", "\n"),
],
)
],
),
(
"""<div class="codehilite" data-code-language="Python"><pre><span></span><code><span class="nb">print</span><span class="p">(</span><span class="s2">"Hello, world!"</span><span class="p">)</span>
</code></pre></div>""", # noqa: E501
[
(
"Python",
[
("pygments:nb", "print"),
("pygments:p", "("),
("pygments:s2", '"Hello, world!"'),
("pygments:p", ")"),
("pygments:w", "\n"),
],
)
],
),
],
)
def test_transform_content_code_blocks(self, raw_html, expected_code_blocks):
_, _, _, code_blocks = MessageBox.transform_content(raw_html, SERVER_URL)

assert code_blocks == expected_code_blocks

# FIXME This is the same parametrize as MsgInfoView:test_height_reactions
@pytest.mark.parametrize(
"to_vary_in_each_message, expected_text, expected_attributes",
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 @@ -443,6 +443,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
Loading
Loading