Skip to content
Snippets Groups Projects
Commit 8dfad143 authored by Douwe Maan's avatar Douwe Maan
Browse files

Add inline diff markers in highlighted diffs.

parent 83e4fc18
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -66,7 +66,7 @@ class Projects::BlobController < Projects::ApplicationController
 
def diff
@form = UnfoldForm.new(params)
@lines = Gitlab::Diff::Highlight.process_file(repository, @ref, @path)
@lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
@lines = @lines[@form.since - 1..@form.to - 1]
 
if @form.bottom?
Loading
Loading
Loading
Loading
@@ -13,7 +13,7 @@
= link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(left[:line_code], 'old')
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", "line_code" => left[:line_code] }= diff_line_content(left[:text])
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { "line_code" => left[:line_code] }}= diff_line_content(left[:text])
 
- if right[:type] == 'new'
- new_line_class = 'new'
Loading
Loading
@@ -26,7 +26,7 @@
= link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(right[:line_code], 'new')
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= diff_line_content(right[:text])
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { "line_code" => new_line_code }}= diff_line_content(right[:text])
 
- if @reply_allowed
- comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
Loading
Loading
Loading
Loading
@@ -22,7 +22,7 @@
= link_to_new_diff_note(line_code)
%td.new_line{data: {linenumber: line.new_pos}}
= link_to raw(type == "old" ? "&nbsp;" : line.new_pos), "##{line_code}", id: line_code
%td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text)
%td.line_content{class: "noteable_line #{type} #{line_code}", data: { "line_code" => line_code }}= diff_line_content(line.text)
 
- if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
Loading
Loading
Loading
Loading
@@ -18,7 +18,7 @@ module Gitlab
end
 
def highlighted_diff_lines
Gitlab::Diff::Highlight.process_diff_lines(self)
Gitlab::Diff::Highlight.new(self).highlight
end
 
def mode_changed?
Loading
Loading
Loading
Loading
@@ -6,59 +6,199 @@ module Gitlab
delegate :repository, :old_path, :new_path, :old_ref, :new_ref,
to: :diff_file, prefix: :diff
 
# Apply syntax highlight to provided source code
#
# diff_file - an instance of Gitlab::Diff::File
#
# Returns an Array with the processed items.
def self.process_diff_lines(diff_file)
processor = new(diff_file)
processor.highlight
def initialize(diff_file)
@diff_file = diff_file
@diff_lines = diff_file.diff_lines
@raw_lines = @diff_lines.map(&:text)
end
 
def self.process_file(repository, ref, file_name)
blob = repository.blob_at(ref, file_name)
return [] unless blob
def highlight
return [] if @diff_lines.empty?
 
Gitlab::Highlight.highlight(file_name, blob.data).lines.map!(&:html_safe)
end
find_inline_diffs
 
def initialize(diff_file)
@diff_file = diff_file
@file_name = diff_file.new_path
@lines = diff_file.diff_lines
process_lines
@diff_lines
end
 
def highlight
return [] if @lines.empty?
private
 
@lines.each_with_index do |line, i|
line_prefix = line.text.match(/\A([+-])/) ? $1 : ' '
def find_inline_diffs
@inline_diffs = []
 
local_edit_indexes.each do |index|
old_index = index
new_index = index + 1
old_line = @raw_lines[old_index][1..-1]
new_line = @raw_lines[new_index][1..-1]
# Skip inline diff if empty line was replaced with content
next if old_line == ""
lcp = longest_common_prefix(old_line, new_line)
lcs = longest_common_suffix(old_line, new_line)
old_diff_range = lcp..(old_line.length - lcs - 1)
new_diff_range = lcp..(new_line.length - lcs - 1)
@inline_diffs[old_index] = old_diff_range if old_diff_range.begin <= old_diff_range.end
@inline_diffs[new_index] = new_diff_range if new_diff_range.begin <= new_diff_range.end
end
end
def process_lines
@diff_lines.each_with_index do |diff_line, i|
# ignore highlighting for "match" lines
next if line.type == 'match'
next if diff_line.type == 'match'
rich_line = highlight_line(diff_line, i)
rich_line = mark_inline_diffs(rich_line, diff_line, i)
diff_line.text = rich_line.html_safe
end
end
def highlight_line(diff_line, index)
line_prefix = line_prefixes[index]
case diff_line.type
when 'new', nil
rich_line = new_lines[diff_line.new_pos - 1]
when 'old'
rich_line = old_lines[diff_line.old_pos - 1]
end
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
rich_line ? line_prefix + rich_line : diff_line.text
end
def mark_inline_diffs(rich_line, diff_line, index)
inline_diff = @inline_diffs[index]
return rich_line unless inline_diff
raw_line = diff_line.text
# Based on the prefixless versions
from = inline_diff.begin + 1
to = inline_diff.end + 1
position_mapping = map_character_positions(raw_line, rich_line)
inline_diff_positions = position_mapping[from..to]
marker_ranges = collapse_ranges(inline_diff_positions)
offset = 0
marker_ranges.each do |range|
offset = insert_around_range(rich_line, range, "<span class='idiff'>", "</span>", offset)
end
rich_line
end
 
case line.type
when 'new', nil
highlighted_line = new_lines[line.new_pos - 1]
when 'old'
highlighted_line = old_lines[line.old_pos - 1]
def line_prefixes
@line_prefixes ||= @raw_lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' }
end
def local_edit_indexes
@local_edit_indexes ||= begin
joined_line_prefixes = " #{line_prefixes.join} "
offset = 0
local_edit_indexes = []
while index = joined_line_prefixes.index(" -+ ", offset)
local_edit_indexes << index
offset = index + 1
end
 
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
line.text = highlighted_line.insert(0, line_prefix).html_safe if highlighted_line
local_edit_indexes
end
end
def map_character_positions(raw_line, rich_line)
mapping = []
raw_pos = 0
rich_pos = 0
(0..raw_line.length).each do |raw_pos|
raw_char = raw_line[raw_pos]
rich_char = rich_line[rich_pos]
while rich_char == '<'
until rich_char == '>'
rich_pos += 1
rich_char = rich_line[rich_pos]
end
rich_pos += 1
rich_char = rich_line[rich_pos]
end
 
@lines
mapping[raw_pos] = rich_pos
rich_pos += 1
end
mapping
end
 
def old_lines
@old_lines ||= self.class.process_file(diff_repository, diff_old_ref, diff_old_path)
@old_lines ||= Gitlab::Highlight.highlight_lines(diff_repository, diff_old_ref, diff_old_path)
end
 
def new_lines
@new_lines ||= self.class.process_file(diff_repository, diff_new_ref, diff_new_path)
@new_lines ||= Gitlab::Highlight.highlight_lines(diff_repository, diff_new_ref, diff_new_path)
end
def longest_common_suffix(a, b)
longest_common_prefix(a.reverse, b.reverse)
end
def longest_common_prefix(a, b)
max_length = [a.length, b.length].max
length = 0
(0..max_length - 1).each do |pos|
old_char = a[pos]
new_char = b[pos]
break if old_char != new_char
length += 1
end
length
end
def collapse_ranges(positions)
return [] if positions.empty?
ranges = []
start = prev = positions[0]
range = start..prev
positions[1..-1].each do |pos|
if pos == prev + 1
range = start..pos
prev = pos
else
ranges << range
start = prev = pos
range = start..prev
end
end
ranges << range
ranges
end
def insert_around_range(text, range, before, after, offset = 0)
from = range.begin
to = range.end
text.insert(offset + from, before)
offset += before.length
text.insert(offset + to + 1, after)
offset += after.length
offset
end
end
end
Loading
Loading
Loading
Loading
@@ -7,6 +7,13 @@ module Gitlab
formatter.format(lexer.lex(blob_content, continue: continue)).html_safe
end
 
def self.highlight_lines(repository, ref, file_name)
blob = repository.blob_at(ref, file_name)
return [] unless blob
highlight(file_name, blob.data).lines.map!(&:html_safe)
end
private
 
def self.rouge_formatter(options = {})
Loading
Loading
Loading
Loading
@@ -51,9 +51,9 @@ describe Gitlab::Diff::Highlight, lib: true do
end
end
 
describe '.process_file' do
describe '.highlight_lines' do
let(:lines) do
Gitlab::Diff::Highlight.process_file(project.repository, commit.id, 'files/ruby/popen.rb')
Gitlab::Diff::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb')
end
 
it 'should properly highlight all the lines' do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment