Skip to content

Commit

Permalink
feat: copy data to clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
tconbeer committed Dec 21, 2023
1 parent ce3bd3b commit 8fc5921
Show file tree
Hide file tree
Showing 6 changed files with 853 additions and 98 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ All notable changes to this project will be documented in this file.

### Features

- Very long values in the data table are now truncated, with an elipsis (``). The full value is shown in a tooltip when hovering over a truncated value.
- Select a range of cells in the Results Viewer by clicking and dragging or by holding <kbd>shift</kbd> while moving the cursor with the keyboard.
- Copy selected cells from the Results Viewer by pressing <kbd>ctrl+c</kbd>.
- Very long values in the Results Viewer are now truncated, with an elipsis (``). The full value is shown in a tooltip when hovering over a truncated value. (The full value will also be copied to the clipboard).
- The BigQuery adapter is now installable as an extra; use `pip install harlequin[bigquery]`.

### Bug Fixes

- Fixes an issue on Windows where pressing shift or control would hide the member autocomplete menu.
- Fixes an issue on Windows where pressing <kbd>shift</kbd> or <kbd>ctrl</kbd> would hide the member autocomplete menu.
- Fixes flaky query execution behavior on some platforms.

### Testing
Expand Down
16 changes: 15 additions & 1 deletion src/harlequin/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import os
import time
from functools import partial
from typing import Dict, List, Optional, Type, Union
Expand All @@ -19,6 +20,7 @@
from textual.widget import AwaitMount, Widget
from textual.widgets import Button, Footer, Input
from textual.worker import Worker, WorkerState
from textual_fastdatatable import DataTable
from textual_fastdatatable.backend import AutoBackendType

from harlequin.adapter import HarlequinAdapter, HarlequinCursor
Expand Down Expand Up @@ -260,7 +262,19 @@ def submit_query_if_limit_valid(self, message: Input.Submitted) -> None:
)
)

# @on(DataTable.CellHighlighted)
@on(DataTable.SelectionCopied)
def copy_data_to_clipboard(self, message: DataTable.SelectionCopied) -> None:
message.stop()
# Excel, sheets, and Snowsight all use a TSV format for copying tabular data
text = os.linesep.join("\t".join(map(str, row)) for row in message.values)
self.editor.text_input.clipboard = text
if self.editor.use_system_clipboard:
try:
self.editor.text_input.system_copy(text)
except Exception:
self.notify("Error copying data to system clipboard.", severity="error")
else:
self.notify("Selected data copied to clipboard.")

@on(Worker.StateChanged)
def handle_worker_error(self, message: Worker.StateChanged) -> None:
Expand Down
4 changes: 4 additions & 0 deletions src/harlequin/components/help_screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,21 @@

### Results Viewer Bindings

- ctrl+c: Copy selected data to the clipboard.

#### Switching Tabs

- j: Switch to the previous tab.
- k: Switch to the next tab.

#### Moving the Cursor

- up,down,left,right: Move the cursor one cell.
- home: Move the cursor to the top of the current column.
- end: Move the cursor to the bottom of the current column.
- PgUp: Move the cursor up one screen.
- PgDn: Move the cursor down one screen.
- shift+[any]: Select range while moving the cursor.


### Data Catalog Bindings
Expand Down
190 changes: 95 additions & 95 deletions tests/functional_tests/__snapshots__/test_help_screen.ambr

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/functional_tests/__snapshots__/test_results_viewer.ambr

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions tests/functional_tests/test_results_viewer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from __future__ import annotations

from typing import Awaitable, Callable

import pytest
from harlequin import Harlequin
from harlequin.components.results_viewer import ResultsViewer
from textual.message import Message
from textual_fastdatatable import DataTable


@pytest.mark.asyncio
Expand All @@ -22,3 +27,66 @@ async def test_dupe_column_names(
await app.workers.wait_for_complete()
await pilot.pause()
assert await app_snapshot(app, "dupe columns")


@pytest.mark.asyncio
async def test_copy_data(
app_all_adapters: Harlequin, app_snapshot: Callable[..., Awaitable[bool]]
) -> None:
app = app_all_adapters
query = "select 3, 'rosberg', 6, 'ROS', 'Nico', 'Rosberg', '1985-06-27', 'German', 'http://en.wikipedia.org/wiki/Nico_Rosberg'"
expected = "3 rosberg 6 ROS Nico Rosberg 1985-06-27 German http://en.wikipedia.org/wiki/Nico_Rosberg"
messages: list[Message] = []
async with app.run_test(message_hook=messages.append) as pilot:
await app.workers.wait_for_complete()
await pilot.pause()
app.editor.text = query
await pilot.press("ctrl+j")
await app.workers.wait_for_complete()
await pilot.pause()
await app.workers.wait_for_complete()
await pilot.pause()
await app.workers.wait_for_complete()
await pilot.pause()

assert app.results_viewer._has_focus_within
keys = ["shift+right"] * 8
await pilot.press(*keys)
await pilot.wait_for_scheduled_animations()
await pilot.press("ctrl+c")
await pilot.pause()

copied_message = list(
filter(lambda m: isinstance(m, DataTable.SelectionCopied), messages)
)[0]
assert isinstance(copied_message, DataTable.SelectionCopied)
assert isinstance(copied_message.values, list)

app.editor.text = ""
app.editor.focus()
await pilot.press("ctrl+v") # paste
assert app.editor.text == expected
assert await app_snapshot(app, "paste values from table")


@pytest.mark.asyncio
async def test_data_truncated_with_tooltip(
app_all_adapters: Harlequin, app_snapshot: Callable[..., Awaitable[bool]]
) -> None:
app = app_all_adapters
query = "select 'supercalifragilisticexpialidocious'"
async with app.run_test(tooltips=True) as pilot:
await app.workers.wait_for_complete()
await pilot.pause()
app.editor.text = query
await pilot.press("ctrl+j")
await app.workers.wait_for_complete()
await pilot.pause()
await app.workers.wait_for_complete()
await pilot.pause()
await app.workers.wait_for_complete()
await pilot.pause()

await pilot.hover(ResultsViewer, (2, 2))
await pilot.pause(0.5)
assert await app_snapshot(app, "hover over truncated value")

0 comments on commit 8fc5921

Please sign in to comment.