From 34657b821ae597de76ffd5a70d2b0b298dc270ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= <Ruben@GitLab.com>
Date: Tue, 15 Dec 2015 18:09:09 -0500
Subject: [PATCH] Add syntax highlighting to diff view. #3945

---
 app/helpers/application_helper.rb             |  6 ++++
 app/helpers/blob_helper.rb                    | 29 ++++++++++++++++---
 .../projects/diffs/_parallel_view.html.haml   |  8 +++--
 app/views/projects/diffs/_text_file.html.haml |  6 ++--
 lib/rouge/lexers/gitlab_diff.rb               | 20 +++++++++++++
 spec/helpers/blob_helper_spec.rb              | 11 +++++++
 6 files changed, 71 insertions(+), 9 deletions(-)
 create mode 100644 lib/rouge/lexers/gitlab_diff.rb

diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0b00b9a0702..bc4b6ec0327 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -326,4 +326,10 @@ module ApplicationHelper
   def truncate_first_line(message, length = 50)
     truncate(message.each_line.first.chomp, length: length) if message
   end
+
+  def unescape_html(content)
+    text = CGI.unescapeHTML(content)
+    text.gsub!('&nbsp;', ' ')
+    text
+  end
 end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index d31d4cde08f..bf18673972c 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,11 +1,17 @@
 module BlobHelper
-  def highlight(blob_name, blob_content, nowrap: false, continue: false)
-    @formatter ||= Rouge::Formatters::HTMLGitlab.new(
-      nowrap: nowrap,
+  def rouge_formatter(options = {})
+    default_options = {
+      nowrap: false,
       cssclass: 'code highlight',
       lineanchors: true,
       lineanchorsid: 'LC'
-    )
+    }
+
+    Rouge::Formatters::HTMLGitlab.new(default_options.merge!(options))
+  end
+
+  def highlight(blob_name, blob_content, nowrap: false, continue: false)
+    @formatter ||= rouge_formatter(nowrap: nowrap)
 
     begin
       @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
@@ -18,6 +24,21 @@ module BlobHelper
     result
   end
 
+  def highlight_line(blob_name, content, continue: false)
+    if @previous_blob_name != blob_name
+      @parent  = Rouge::Lexer.guess(filename: blob_name, source: content).new rescue Rouge::Lexers::PlainText.new
+      @lexer   = Rouge::Lexers::GitlabDiff.new(parent_lexer: @parent)
+      @options = Rouge::Lexers::PlainText === @parent ? {} : { continue: continue }
+    end
+
+    @previous_blob_name = blob_name
+    @formatter ||= rouge_formatter(nowrap: true)
+
+    content.sub!(/\A((?:\+|-)\s*)/, '') # Don't format '+' or '-' indicators.
+
+    "#{$1}#{@formatter.format(@lexer.lex(content, @options))}".html_safe
+  end
+
   def no_highlight_files
     %w(credits changelog news copying copyright license authors)
   end
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 37fd1b1ec8a..c6a9d71e789 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,5 +1,5 @@
 / Side-by-side diff view
-%div.text-file.diff-wrap-lines
+%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ class: user_color_scheme }
   %table
     - parallel_diff(diff_file, index).each do |line|
       - type_left = line[0]
@@ -20,7 +20,8 @@
             = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left
             - if @comments_allowed && can?(current_user, :create_note, @project)
               = link_to_new_diff_note(line_code_left, 'old')
-            %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw line_content_left
+            %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }<
+              = highlight_line(diff_file.new_path, unescape_html(line_content_left))
 
           - if type_right == 'new'
             - new_line_class = 'new'
@@ -33,7 +34,8 @@
             = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code
             - if @comments_allowed && can?(current_user, :create_note, @project)
               = link_to_new_diff_note(line_code_right, 'new')
-            %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw line_content_right
+            %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}<
+              = highlight_line(diff_file.new_path, unescape_html(line_content_right))
 
       - if @reply_allowed
         - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right)
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 977ca423f75..78c66a6291e 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,7 +3,8 @@
   .suppressed-container
     %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
 
-%table.text-file{class: "#{'hide' if too_big}"}
+%table.text-file.code.js-syntax-highlight{ class: [user_color_scheme, too_big ? 'hide' : ''] }
+
   - last_line = 0
   - diff_file.diff_lines.each_with_index do |line, index|
     - type = line.type
@@ -21,7 +22,8 @@
             = 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}= raw diff_line_content(line.text)
+        %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}<
+          = highlight_line(diff_file.new_path, unescape_html(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/rouge/lexers/gitlab_diff.rb b/lib/rouge/lexers/gitlab_diff.rb
new file mode 100644
index 00000000000..e136d47df00
--- /dev/null
+++ b/lib/rouge/lexers/gitlab_diff.rb
@@ -0,0 +1,20 @@
+Rouge::Token::Tokens.token(:InlineDiff, 'idiff')
+
+module Rouge
+  module Lexers
+    class GitlabDiff < RegexLexer
+      title "GitLab Diff"
+      tag 'gitlab_diff'
+
+      state :root do
+        rule %r{<span class='idiff'>(.*?)</span>} do |match|
+          token InlineDiff, match[1]
+        end
+
+        rule /(?:(?!<span).)*/ do
+          delegate option(:parent_lexer)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index b8bba36439a..74edef3a2b4 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -64,4 +64,15 @@ describe BlobHelper do
       end
     end
   end
+
+  describe 'highlight_line' do
+    let(:expected) do
+      %q(<span id="LC1" class="line"><span class="nb">puts</span> <span class="s1">&#39;Hello&#39;</span> <span class="idiff">world</span></span>)
+    end
+
+    it 'should respect the inline diff markup' do
+      result = highlight_line('demo.rb', "puts 'Hello' <span class='idiff'>world</span>")
+      expect(result).to eq(expected)
+    end
+  end
 end
-- 
GitLab