Skip to content

Commit 42c0d9d

Browse files
committed
Copy selection to clipboard in diff view
1 parent f88ecd5 commit 42c0d9d

File tree

7 files changed

+208
-38
lines changed

7 files changed

+208
-38
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ jobs:
3232
profile: minimal
3333
components: clippy
3434

35+
- name: Install dependencies for clipboard access
36+
if: matrix.os == 'ubuntu-latest'
37+
run: |
38+
sudo apt-get -qq install libxcb-shape0-dev libxcb-xfixes0-dev
39+
3540
- name: Build Debug
3641
run: |
3742
rustc --version
@@ -59,6 +64,10 @@ jobs:
5964
profile: minimal
6065
target: x86_64-unknown-linux-musl
6166

67+
- name: Install dependencies for clipboard access
68+
run: |
69+
sudo apt-get -qq install libxcb-shape0-dev libxcb-xfixes0-dev
70+
6271
- name: Setup MUSL
6372
run: |
6473
sudo apt-get -qq install musl-tools

Cargo.lock

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ serde = "1.0"
4040
anyhow = "1.0.32"
4141
unicode-width = "0.1"
4242
textwrap = "0.12"
43+
clipboard = "0.5"
4344

4445
[target.'cfg(not(windows))'.dependencies]
4546
pprof = { version = "0.3", features = ["flamegraph"], optional = true }

src/components/diff.rs

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{
2-
CommandBlocking, DrawableComponent, ExtendType, ScrollType,
2+
CommandBlocking, Direction, DrawableComponent, ScrollType,
33
};
44
use crate::{
55
components::{CommandInfo, Component},
@@ -10,6 +10,7 @@ use crate::{
1010
};
1111
use asyncgit::{hash, sync, DiffLine, DiffLineType, FileDiff, CWD};
1212
use bytesize::ByteSize;
13+
use clipboard::{ClipboardContext, ClipboardProvider};
1314
use crossterm::event::Event;
1415
use std::{borrow::Cow, cell::Cell, cmp, path::Path};
1516
use tui::{
@@ -49,11 +50,44 @@ impl Selection {
4950
}
5051
}
5152

53+
fn get_top(&self) -> usize {
54+
match self {
55+
Self::Single(start) => *start,
56+
Self::Multiple(start, end) => cmp::min(*start, *end),
57+
}
58+
}
59+
60+
fn get_bottom(&self) -> usize {
61+
match self {
62+
Self::Single(start) => *start,
63+
Self::Multiple(start, end) => cmp::max(*start, *end),
64+
}
65+
}
66+
67+
fn modify(&mut self, direction: Direction, max: usize) {
68+
let start = self.get_start();
69+
let old_end = self.get_end();
70+
71+
*self = match direction {
72+
Direction::Up => {
73+
Self::Multiple(start, old_end.saturating_sub(1))
74+
}
75+
76+
Direction::Down => {
77+
Self::Multiple(start, cmp::min(old_end + 1, max))
78+
}
79+
};
80+
}
81+
5282
fn contains(&self, index: usize) -> bool {
5383
match self {
5484
Self::Single(start) => index == *start,
5585
Self::Multiple(start, end) => {
56-
*start <= index && index <= *end
86+
if start <= end {
87+
*start <= index && index <= *end
88+
} else {
89+
*end <= index && index <= *start
90+
}
5791
}
5892
}
5993
}
@@ -147,56 +181,79 @@ impl DiffComponent {
147181
move_type: ScrollType,
148182
) -> Result<()> {
149183
if let Some(diff) = &self.diff {
150-
let old_start = self.selection.get_start();
151-
152184
let max = diff.lines.saturating_sub(1) as usize;
153185

154186
let new_start = match move_type {
155-
ScrollType::Down => old_start.saturating_add(1),
156-
ScrollType::Up => old_start.saturating_sub(1),
187+
ScrollType::Down => {
188+
self.selection.get_bottom().saturating_add(1)
189+
}
190+
ScrollType::Up => {
191+
self.selection.get_top().saturating_sub(1)
192+
}
157193
ScrollType::Home => 0,
158194
ScrollType::End => max,
159-
ScrollType::PageDown => old_start.saturating_add(
160-
self.current_size.get().1.saturating_sub(1)
161-
as usize,
162-
),
163-
ScrollType::PageUp => old_start.saturating_sub(
164-
self.current_size.get().1.saturating_sub(1)
165-
as usize,
166-
),
195+
ScrollType::PageDown => {
196+
self.selection.get_bottom().saturating_add(
197+
self.current_size.get().1.saturating_sub(1)
198+
as usize,
199+
)
200+
}
201+
ScrollType::PageUp => {
202+
self.selection.get_top().saturating_sub(
203+
self.current_size.get().1.saturating_sub(1)
204+
as usize,
205+
)
206+
}
167207
};
168208

169209
self.selection =
170210
Selection::Single(cmp::min(max, new_start));
171211

172-
if new_start != old_start {
173-
self.selected_hunk =
174-
Self::find_selected_hunk(diff, new_start)?;
175-
}
212+
self.selected_hunk =
213+
Self::find_selected_hunk(diff, new_start)?;
176214
}
177215
Ok(())
178216
}
179217

180-
fn extend_selection(
218+
fn modify_selection(
181219
&mut self,
182-
extend_type: ExtendType,
220+
direction: Direction,
183221
) -> Result<()> {
184222
if let Some(diff) = &self.diff {
185223
let max = diff.lines.saturating_sub(1) as usize;
186-
let start = self.selection.get_start();
187-
let old_end = self.selection.get_end();
188-
189-
self.selection = match extend_type {
190-
ExtendType::Up => Selection::Multiple(
191-
start,
192-
cmp::max(start, old_end.saturating_sub(1)),
193-
),
194-
195-
ExtendType::Down => Selection::Multiple(
196-
start,
197-
cmp::min(old_end + 1, max),
198-
),
199-
};
224+
225+
self.selection.modify(direction, max);
226+
}
227+
228+
Ok(())
229+
}
230+
231+
fn copy_selection(&self) -> Result<()> {
232+
if let Some(diff) = &self.diff {
233+
let lines_to_copy: Vec<&str> = diff
234+
.hunks
235+
.iter()
236+
.flat_map(|hunk| hunk.lines.iter())
237+
.enumerate()
238+
.filter_map(|(i, line)| {
239+
if self.selection.contains(i) {
240+
Some(
241+
line.content
242+
.trim_matches(|c| {
243+
c == '\n' || c == '\r'
244+
})
245+
.as_ref(),
246+
)
247+
} else {
248+
None
249+
}
250+
})
251+
.collect();
252+
253+
let mut ctx: ClipboardContext = ClipboardProvider::new()
254+
.expect("failed to get access to clipboard");
255+
ctx.set_contents(lines_to_copy.join("\n"))
256+
.expect("failed to set clipboard contents");
200257
}
201258

202259
Ok(())
@@ -489,7 +546,7 @@ impl DrawableComponent for DiffComponent {
489546
self.scroll_top.set(calc_scroll_top(
490547
self.scroll_top.get(),
491548
self.current_size.get().1 as usize,
492-
self.selection.get_start(),
549+
self.selection.get_end(),
493550
));
494551

495552
let title =
@@ -531,6 +588,12 @@ impl Component for DiffComponent {
531588
self.focused,
532589
));
533590

591+
out.push(CommandInfo::new(
592+
commands::COPY,
593+
true,
594+
self.focused,
595+
));
596+
534597
out.push(
535598
CommandInfo::new(
536599
commands::DIFF_HOME_END,
@@ -570,11 +633,11 @@ impl Component for DiffComponent {
570633
Ok(true)
571634
}
572635
keys::SHIFT_DOWN => {
573-
self.extend_selection(ExtendType::Down)?;
636+
self.modify_selection(Direction::Down)?;
574637
Ok(true)
575638
}
576639
keys::SHIFT_UP => {
577-
self.extend_selection(ExtendType::Up)?;
640+
self.modify_selection(Direction::Up)?;
578641
Ok(true)
579642
}
580643
keys::END => {
@@ -618,6 +681,10 @@ impl Component for DiffComponent {
618681
}
619682
Ok(true)
620683
}
684+
keys::COPY => {
685+
self.copy_selection()?;
686+
Ok(true)
687+
}
621688
_ => Ok(false),
622689
};
623690
}

src/components/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub enum ScrollType {
106106
}
107107

108108
#[derive(Copy, Clone)]
109-
pub enum ExtendType {
109+
pub enum Direction {
110110
Up,
111111
Down,
112112
}

src/keys.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub const SHIFT_UP: KeyEvent =
5050
pub const SHIFT_DOWN: KeyEvent =
5151
with_mod(KeyCode::Down, KeyModifiers::SHIFT);
5252
pub const ENTER: KeyEvent = no_mod(KeyCode::Enter);
53+
pub const COPY: KeyEvent = no_mod(KeyCode::Char('y'));
5354
pub const EDIT_FILE: KeyEvent = no_mod(KeyCode::Char('e'));
5455
pub const STATUS_STAGE_FILE: KeyEvent = no_mod(KeyCode::Enter);
5556
pub const STATUS_STAGE_ALL: KeyEvent = no_mod(KeyCode::Char('a'));

0 commit comments

Comments
 (0)