diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 6ca3a636359fcc1135f0020f8b283d49220e7d78..8133de90a41ba845e0fa4bb29fa787791283fbf8 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -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? diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index a2958286adae5da6478128d3cdf2a40bfafdd2ce..7b9cecbc3da8ac6fad341a7c2f0fdc59f2d09e5f 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -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' @@ -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]) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 521f2ac1e8d5d5d6a4084a8701ed10fbc17bd5e4..6761155dcf916f06aefe68f09e7907472aa5ed9f 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -22,7 +22,7 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : 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) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index cb93c6a574a2f51aada49550217dbe619eb193b4..c1a6e16da5a42dadedda488d725a46785d8b7298 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -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? diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index ba2f12db147b0dc458e950aa4a73eefba4be404c..e21f496102d80d0d9156075648f9f3a2ff438fcd 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -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 diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 02e097eca3d56f7329473d0679a5fca4b3fbdc6c..a5b041687e318141c331b4f5a42242ef69de9e18 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -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 = {}) diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index cdeed603e23a7a0e331c2878b803cd4a166cefc7..3c66c9889baa0039ddf7f7fb23c6185f3375c2e6 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -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