Only copy old/new code when selecting left/right side of parallel diff

This commit is contained in:
Douwe Maan 2017-09-28 16:55:25 +02:00
parent e0e49f2f71
commit b74c643c66
10 changed files with 208 additions and 100 deletions

View file

@ -298,7 +298,7 @@ class CopyAsGFM {
const documentFragment = getSelectedFragment(); const documentFragment = getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) return;
const el = transformer(documentFragment.cloneNode(true)); const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
if (!el) return; if (!el) return;
e.preventDefault(); e.preventDefault();
@ -338,55 +338,64 @@ class CopyAsGFM {
} }
static transformGFMSelection(documentFragment) { static transformGFMSelection(documentFragment) {
const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
switch (gfmEls.length) { switch (gfmElements.length) {
case 0: { case 0: {
return documentFragment; return documentFragment;
} }
case 1: { case 1: {
return gfmEls[0]; return gfmElements[0];
} }
default: { default: {
const allGfmEl = document.createElement('div'); const allGfmElement = document.createElement('div');
for (let i = 0; i < gfmEls.length; i += 1) { for (let i = 0; i < gfmElements.length; i += 1) {
const lineEl = gfmEls[i]; const gfmElement = gfmElements[i];
allGfmEl.appendChild(lineEl); allGfmElement.appendChild(gfmElement);
allGfmEl.appendChild(document.createTextNode('\n\n')); allGfmElement.appendChild(document.createTextNode('\n\n'));
} }
return allGfmEl; return allGfmElement;
} }
} }
} }
static transformCodeSelection(documentFragment) { static transformCodeSelection(documentFragment, target) {
const lineEls = documentFragment.querySelectorAll('.line'); let lineSelector = '.line';
let codeEl; if (target) {
if (lineEls.length > 1) { const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
codeEl = document.createElement('pre'); if (lineClass) {
codeEl.className = 'code highlight'; lineSelector = `.line_content.${lineClass} ${lineSelector}`;
}
}
const lang = lineEls[0].getAttribute('lang'); const lineElements = documentFragment.querySelectorAll(lineSelector);
let codeElement;
if (lineElements.length > 1) {
codeElement = document.createElement('pre');
codeElement.className = 'code highlight';
const lang = lineElements[0].getAttribute('lang');
if (lang) { if (lang) {
codeEl.setAttribute('lang', lang); codeElement.setAttribute('lang', lang);
} }
} else { } else {
codeEl = document.createElement('code'); codeElement = document.createElement('code');
} }
if (lineEls.length > 0) { if (lineElements.length > 0) {
for (let i = 0; i < lineEls.length; i += 1) { for (let i = 0; i < lineElements.length; i += 1) {
const lineEl = lineEls[i]; const lineElement = lineElements[i];
codeEl.appendChild(lineEl); codeElement.appendChild(lineElement);
codeEl.appendChild(document.createTextNode('\n')); codeElement.appendChild(document.createTextNode('\n'));
} }
} else { } else {
codeEl.appendChild(documentFragment); codeElement.appendChild(documentFragment);
} }
return codeEl; return codeElement;
} }
static nodeToGFM(node, respectWhitespaceParam = false) { static nodeToGFM(node, respectWhitespaceParam = false) {

View file

@ -24,7 +24,8 @@ class Diff {
if (!isBound) { if (!isBound) {
$(document) $(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this))
.on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this));
isBound = true; isBound = true;
} }
@ -100,6 +101,18 @@ class Diff {
this.highlightSelectedLine(); this.highlightSelectedLine();
} }
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
table.removeClass('left-side-selected right-side-selected');
const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0];
if (lineClass) {
table.addClass(`${lineClass}-selected`);
}
}
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
} }

View file

@ -77,6 +77,18 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
&.left-side-selected {
td.line_content.parallel.right-side {
@include user-select(none);
}
}
&.right-side-selected {
td.line_content.parallel.left-side {
@include user-select(none);
}
}
} }
tr.line_holder.parallel { tr.line_holder.parallel {

View file

@ -33,19 +33,21 @@ module DiffHelper
end end
def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" content_line_class = %w[line_content match]
cls = ['diff-line-num', 'unfold', 'js-unfold'] content_line_class << 'parallel' if view == :parallel
cls << 'js-unfold-bottom' if bottom
line_num_class = %w[diff-line-num unfold js-unfold]
line_num_class << 'js-unfold-bottom' if bottom
html = '' html = ''
if old_pos if old_pos
html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content unless view == :inline html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
end end
if new_pos if new_pos
html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
html << content html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end end
html.html_safe html.html_safe

View file

@ -5,25 +5,24 @@
= diff_match_line @form.since, @form.since, text: @match_line, view: diff_view = diff_match_line @form.since, @form.since, text: @match_line, view: diff_view
- @lines.each_with_index do |line, index| - @lines.each_with_index do |line, index|
- line_new = index + @form.since - line_number_new = index + @form.since
- line_old = line_new - @form.offset - line_number_old = line_number_new - @form.offset
- line_content = capture do - line[0, 0] = ' ' * @form.indent
%td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line} %tr.line_holder.diff-expanded{ id: line_number_old, class: line_class }
%tr.line_holder.diff-expanded{ id: line_old, class: line_class }
- case diff_view - case diff_view
- when :inline - when :inline
%td.old_line.diff-line-num{ data: { linenumber: line_old } } %td.old_line.diff-line-num{ data: { linenumber: line_number_old } }
%a{ href: "#", data: { linenumber: line_old }, disabled: true } %a{ href: "#", data: { linenumber: line_number_old }, disabled: true }
%td.new_line.diff-line-num{ data: { linenumber: line_new } } %td.new_line.diff-line-num{ data: { linenumber: line_number_new } }
%a{ href: "#", data: { linenumber: line_new }, disabled: true } %a{ href: "#", data: { linenumber: line_number_new }, disabled: true }
= line_content %td.line_content.noteable_line{ class: line_class }= line
- when :parallel - when :parallel
%td.old_line.diff-line-num{ data: { linenumber: line_old } } %td.old_line.diff-line-num{ data: { linenumber: line_number_old } }
%a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true } %a{ href: "##{line_number_old}", data: { linenumber: line_number_old }, disabled: true }
= line_content %td.line_content.noteable_line.left-side{ class: line_class }= line
%td.new_line.diff-line-num{ data: { linenumber: line_new } } %td.new_line.diff-line-num{ data: { linenumber: line_number_new } }
%a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true } %a{ href: "##{line_number_new}", data: { linenumber: line_number_new }, disabled: true }
= line_content %td.line_content.noteable_line.right-side{ class: line_class }= line
- if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
%tr.line_holder{ id: @form.to, class: line_class } %tr.line_holder{ id: @form.to, class: line_class }

View file

@ -14,20 +14,20 @@
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel = diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- when 'old-nonewline', 'new-nonewline' - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num %td.old_line.diff-line-num
%td.line_content.match= left.text %td.line_content.match.left-side= left.text
- else - else
- left_line_code = diff_file.line_code(left) - left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left) - left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old') = add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first) - discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable? - if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id } %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text) %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel.left-side
- if right - if right
- case right.type - case right.type
@ -35,20 +35,20 @@
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel = diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- when 'old-nonewline', 'new-nonewline' - when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.match= right.text %td.line_content.match.right-side= right.text
- else - else
- right_line_code = diff_file.line_code(right) - right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right) - right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new') = add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first) - discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable? - if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id } %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text) %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel.right-side
- if discussions_left || discussions_right - if discussions_left || discussions_right
= render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right = render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right

View file

@ -0,0 +1,5 @@
---
title: Only copy old/new code when selecting left/right side of parallel diff
merge_request:
author:
type: added

View file

@ -232,7 +232,7 @@ module SharedDiffNote
end end
def click_parallel_diff_line(code, line_type) def click_parallel_diff_line(code, line_type)
find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover'
find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
end end
end end

View file

@ -446,7 +446,7 @@ describe 'Copy as GFM', js: true do
def verify(label, *gfms) def verify(label, *gfms)
aggregate_failures(label) do aggregate_failures(label) do
gfms.each do |gfm| gfms.each do |gfm|
html = gfm_to_html(gfm) html = gfm_to_html(gfm).gsub(/\A&#x000A;|&#x000A;\z/, '')
output_gfm = html_to_gfm(html) output_gfm = html_to_gfm(html)
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
@ -463,42 +463,98 @@ describe 'Copy as GFM', js: true do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
context 'from a diff' do context 'from a diff' do
before do shared_examples 'copying code from a diff' do
visit project_commit_path(project, sample_commit.id) context 'selecting one word of text' do
end it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
context 'selecting one word of text' do '`RuntimeError`',
it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
'`RuntimeError`' target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
) )
end
end
context 'selecting one line of text' do
it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]',
'`raise RuntimeError, "System commands must be given as an array of strings"`',
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
)
end
end
context 'selecting multiple lines of text' do
it 'copies as a code block' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
<<-GFM.strip_heredoc,
```ruby
raise RuntimeError, "System commands must be given as an array of strings"
end
```
GFM
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
)
end
end end
end end
context 'selecting one line of text' do context 'inline diff' do
it 'copies as inline code' do before do
verify( visit project_commit_path(project, sample_commit.id, view: 'inline')
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
'`raise RuntimeError, "System commands must be given as an array of strings"`'
)
end end
it_behaves_like 'copying code from a diff'
end end
context 'selecting multiple lines of text' do context 'parallel diff' do
it 'copies as a code block' do before do
verify( visit project_commit_path(project, sample_commit.id, view: 'parallel')
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', end
<<-GFM.strip_heredoc, it_behaves_like 'copying code from a diff'
```ruby
raise RuntimeError, "System commands must be given as an array of strings" context 'selecting code on the left' do
end it 'copies as a code block' do
``` verify(
GFM '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
)
<<-GFM.strip_heredoc,
```ruby
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
```
GFM
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].left-side'
)
end
end
context 'selecting code on the right' do
it 'copies as a code block' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
<<-GFM.strip_heredoc,
```ruby
unless cmd.is_a?(Array)
raise RuntimeError, "System commands must be given as an array of strings"
end
```
GFM
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].right-side'
)
end
end end
end end
end end
@ -587,9 +643,9 @@ describe 'Copy as GFM', js: true do
end end
end end
def verify(selector, gfm) def verify(selector, gfm, target: nil)
html = html_for_selector(selector) html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection') output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
end end
@ -605,15 +661,21 @@ describe 'Copy as GFM', js: true do
page.evaluate_script(js) page.evaluate_script(js)
end end
def html_to_gfm(html, transformer = 'transformGFMSelection') def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<-JS.strip_heredoc js = <<-JS.strip_heredoc
(function(html) { (function(html) {
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}]; var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div'); var node = document.createElement('div');
node.innerHTML = html; $(html).each(function() { node.appendChild(this) });
node = transformer(node); var targetSelector = #{target.to_json};
var target;
if (targetSelector) {
target = document.querySelector(targetSelector);
}
node = transformer(node, target);
if (!node) return null; if (!node) return null;
return window.gl.CopyAsGFM.nodeToGFM(node); return window.gl.CopyAsGFM.nodeToGFM(node);

View file

@ -84,7 +84,7 @@ feature 'Diff note avatars', js: true do
end end
it 'shows note avatar' do it 'shows note avatar' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
@ -92,7 +92,7 @@ feature 'Diff note avatars', js: true do
end end
it 'shows comment on note avatar' do it 'shows comment on note avatar' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
@ -100,13 +100,13 @@ feature 'Diff note avatars', js: true do
end end
it 'toggles comments when clicking avatar' do it 'toggles comments when clicking avatar' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
end end
expect(page).to have_selector('.notes_holder', visible: false) expect(page).to have_selector('.notes_holder', visible: false)
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
first('img.js-diff-comment-avatar').click first('img.js-diff-comment-avatar').click
end end
@ -122,7 +122,7 @@ feature 'Diff note avatars', js: true do
wait_for_requests wait_for_requests
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
expect(page).not_to have_selector('img.js-diff-comment-avatar') expect(page).not_to have_selector('img.js-diff-comment-avatar')
end end
end end
@ -138,7 +138,7 @@ feature 'Diff note avatars', js: true do
wait_for_requests wait_for_requests
end end
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').trigger('click') find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
@ -158,7 +158,7 @@ feature 'Diff note avatars', js: true do
end end
end end
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').trigger('click') find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
@ -176,7 +176,7 @@ feature 'Diff note avatars', js: true do
end end
it 'shows extra comment count' do it 'shows extra comment count' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(find('.diff-comments-more-count')).to have_content '+1' expect(find('.diff-comments-more-count')).to have_content '+1'
@ -185,4 +185,10 @@ feature 'Diff note avatars', js: true do
end end
end end
end end
def find_line(line_code)
line = find("[id='#{line_code}']")
line = line.find(:xpath, 'preceding-sibling::*[1][self::td]') if line.tag_name == 'td'
line
end
end end