]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5877 Unified source viewer
authorStas Vilchik <vilchiks@gmail.com>
Thu, 27 Nov 2014 08:06:20 +0000 (09:06 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Thu, 27 Nov 2014 15:15:41 +0000 (16:15 +0100)
36 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/coffee/common/overlay.coffee
server/sonar-web/src/main/coffee/component-viewer/mixins/main-coverage.coffee
server/sonar-web/src/main/coffee/component-viewer/source.coffee
server/sonar-web/src/main/coffee/issue/issue-view.coffee
server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee
server/sonar-web/src/main/coffee/issues/controller.coffee
server/sonar-web/src/main/coffee/issues/layout.coffee
server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee [new file with mode: 0644]
server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee [new file with mode: 0644]
server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee [new file with mode: 0644]
server/sonar-web/src/main/coffee/source-viewer/source.coffee [new file with mode: 0644]
server/sonar-web/src/main/coffee/source-viewer/viewer.coffee [new file with mode: 0644]
server/sonar-web/src/main/hbs/component-viewer/cw-code-expand.hbs
server/sonar-web/src/main/hbs/component-viewer/cw-source.hbs
server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/common/handlebars-extensions.js
server/sonar-web/src/main/js/common/jquery-isolated-scroll.js [new file with mode: 0644]
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-coverage-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-duplications-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-favorite-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-issues-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-lines-filters-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-link-to-raw-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-select-tab-by-metric-spec.js
server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-spec.js
server/sonar-web/src/main/less/component-viewer-source-colorizer.less
server/sonar-web/src/main/less/components.less
server/sonar-web/src/main/less/components/code-source.less [deleted file]
server/sonar-web/src/main/less/components/issues.less
server/sonar-web/src/main/less/components/source.less [new file with mode: 0644]
server/sonar-web/src/main/less/issues.less
server/sonar-web/src/main/less/mixins.less

index d5a53f5350ea2ed93cbad30fcb984ad687207eca..1127dbaf1cf3af2874d68ddef7b604343e2d3194 100644 (file)
@@ -98,6 +98,7 @@ module.exports = (grunt) ->
             '<%= pkg.assets %>js/common/inputs.js'
             '<%= pkg.assets %>js/common/dialogs.js'
             '<%= pkg.assets %>js/common/processes.js'
+            '<%= pkg.assets %>js/common/jquery-isolated-scroll.js'
             '<%= pkg.assets %>js/application.js'
             '<%= pkg.assets %>js/csv.js'
             '<%= pkg.assets %>js/dashboard.js'
@@ -133,6 +134,7 @@ module.exports = (grunt) ->
             '<%= pkg.assets %>js/common/inputs.js'
             '<%= pkg.assets %>js/common/dialogs.js'
             '<%= pkg.assets %>js/common/processes.js'
+            '<%= pkg.assets %>js/common/jquery-isolated-scroll.js'
             '<%= pkg.assets %>js/application.js'
             '<%= pkg.assets %>js/csv.js'
             '<%= pkg.assets %>js/dashboard.js'
@@ -251,6 +253,9 @@ module.exports = (grunt) ->
           '<%= pkg.assets %>js/templates/component-viewer.js': [
             '<%= pkg.sources %>hbs/component-viewer/**/*.hbs'
           ]
+          '<%= pkg.assets %>js/templates/source-viewer.js': [
+            '<%= pkg.sources %>hbs/source-viewer/**/*.hbs'
+          ]
           '<%= pkg.assets %>js/templates/issue.js': [
             '<%= pkg.sources %>hbs/common/**/*.hbs'
             '<%= pkg.sources %>hbs/issue/**/*.hbs'
@@ -316,6 +321,7 @@ module.exports = (grunt) ->
         options:
           test: true
           verbose: true
+          'fail-fast': true
         src: ['<%= pkg.sources %>js/tests/e2e/tests/<%= grunt.option("spec") %>-spec.js']
 
 
index 9abcf4d6d96db2180fb3941497dc1ab0ce7f5af0..70574bef9aef4a0aceee600a41b5afdc1cc7e4c9 100644 (file)
@@ -6,14 +6,6 @@ define [
 
   $ = jQuery
 
-  $.fn.isolatedScroll = ->
-    @on 'wheel', (e) ->
-      delta = -e.originalEvent.deltaY
-      bottomOverflow = @scrollTop + $(@).outerHeight() - @scrollHeight >= 0
-      topOverflow = @scrollTop <= 0
-      e.preventDefault() if (delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)
-    @
-
 
   class extends Marionette.ItemView
     className: 'overlay-popup'
index f0223d27486d1271b8536ec278b498b2805ee361..ac7b29ffd9ec7e6c23360b4357c93919699e01b1 100644 (file)
@@ -25,10 +25,13 @@ define [], () ->
           testCases: c[2]
           branches: c[3]
           coveredBranches: c[4]
+        line.coverageStatus = 'partially-covered'
+        line.coverageStatus = 'covered' if c[1] && c[3] == c[4]
+        line.coverageStatus = 'uncovered' if !c[1] || c[4] == 0
         if line.coverage.branches? && line.coverage.coveredBranches?
-          line.coverage.branchCoverageStatus = 'green' if line.coverage.branches == line.coverage.coveredBranches
-          line.coverage.branchCoverageStatus = 'orange' if line.coverage.branches > line.coverage.coveredBranches
-          line.coverage.branchCoverageStatus = 'red' if line.coverage.coveredBranches == 0
+          line.coverage.branchCoverageStatus = 'covered' if line.coverage.branches == line.coverage.coveredBranches
+          line.coverage.branchCoverageStatus = 'partially-covered' if line.coverage.branches > line.coverage.coveredBranches
+          line.coverage.branchCoverageStatus = 'uncovered' if line.coverage.coveredBranches == 0
       @source.set 'formattedSource', formattedSource
 
 
index 17b0b11296d77ae8be1a2b132a032d96f79a2286..9bdc668be20a83aaee6c95df73a8ab2edeb23fe4 100644 (file)
@@ -41,11 +41,10 @@ define [
 
       'click .js-line-actions': 'highlightLine'
 
-      'click .coverage-tests': 'showCoveragePopup'
+      'click .source-line-covered': 'showCoveragePopup'
+      'click .source-line-partially-covered': 'showCoveragePopup'
 
-      'click .duplication-exists': 'showDuplicationPopup'
-      'mouseenter .duplication-exists': 'duplicationMouseEnter'
-      'mouseleave .duplication-exists': 'duplicationMouseLeave'
+      'click .source-line-duplications-extra': 'showDuplicationPopup'
 
       'click .js-expand': 'expandBlock'
       'click .js-expand-all': 'expandAll'
@@ -78,24 +77,36 @@ define [
 
 
     renderExpandButtons: ->
-      rows = @$('.row[data-line-number]')
+      rows = @$('.source-line[data-line-number]')
       rows.get().forEach (row) =>
         line = $(row).data 'line-number'
         linePrev = $(row).prev('[data-line-number]').data 'line-number'
         if line? && linePrev? && (linePrev + 1) < line
-          expand = @expandTemplate from: linePrev, to: line, settings: @options.main.settings.toJSON()
+          expand = @expandTemplate
+            from: linePrev
+            to: line
+            settings: @options.main.settings.toJSON()
+            baseDuplications: @getBaseDuplications()
           $(expand).insertBefore $(row)
 
       firstShown = rows.first().data('line-number')
       if firstShown > 1
-        expand = @expandTemplate from: firstShown - EXPAND_LINES, to: firstShown, settings: @options.main.settings.toJSON()
+        expand = @expandTemplate
+          from: firstShown - EXPAND_LINES
+          to: firstShown
+          settings: @options.main.settings.toJSON()
+          baseDuplications: @getBaseDuplications()
         $(expand).insertBefore rows.first()
 
       lines = _.size @model.get 'source'
       lines = Math.min lines, LINES_LIMIT
       lastShown = rows.last().data('line-number')
       if lastShown < lines
-        expand = @expandTemplate from: lastShown, to: lines, settings: @options.main.settings.toJSON()
+        expand = @expandTemplate
+          from: lastShown
+          to: lines
+          settings: @options.main.settings.toJSON()
+          baseDuplications: @getBaseDuplications()
         $(expand).insertAfter rows.last()
 
       @delegateEvents()
@@ -114,8 +125,8 @@ define [
           row = @$("##{@cid}-#{line}")
         if row.length > 0
           rendered += 1
-          row.removeClass 'row-hidden'
-          container = row.children('.line')
+          row.removeClass 'hidden'
+          container = row.children('.source-line-code')
           container.addClass 'has-issues' if line > 0
           if rendered < ISSUES_LIMIT
             issueModel = new Issue issue
@@ -124,6 +135,7 @@ define [
             if issues.length == 0
               issues = $('<div class="issue-list"></div>').appendTo container
             issueView.render().$el.appendTo issues
+            issueView.$el.prop('id', "issue-#{issue.key}").data('issue-key', issue.key)
             issueView.on 'reset', =>
               @updateIssue issueModel
               @options.main.requestComponent(@options.main.key, false, false).done =>
@@ -155,12 +167,12 @@ define [
       popup = new LineActionsPopupView
         triggerEl: $(e.currentTarget)
         main: @options.main
-        row: $(e.currentTarget).closest '.row'
+        row: $(e.currentTarget).closest '.source-line'
       popup.render()
 
 
     highlightLine: (e) ->
-      row = $(e.currentTarget).closest('.row')
+      row = $(e.currentTarget).closest('.source-line')
       highlighted = row.is ".#{HIGHLIGHTED_ROW_CLASS}"
       @$(".#{HIGHLIGHTED_ROW_CLASS}").removeClass HIGHLIGHTED_ROW_CLASS
       @highlightedLine = null
@@ -195,7 +207,7 @@ define [
     showCoveragePopup: (e) ->
       e.stopPropagation()
       $('body').click()
-      line = $(e.currentTarget).closest('.row').data 'line-number'
+      line = $(e.currentTarget).closest('.source-line').data 'line-number'
       $.get API_COVERAGE_TESTS, key: @options.main.component.get('key'), line: line, (data) =>
         popup = new CoveragePopupView
           model: new Backbone.Model data
@@ -308,6 +320,14 @@ define [
       r
 
 
+    getBaseDuplications: ->
+      source = @model.get 'formattedSource'
+      baseDuplications = []
+      if source? && source.length > 0 && _.first(source).duplications?
+        baseDuplications = _.first(source).duplications
+      baseDuplications
+
+
     serializeData: ->
       uid: @cid
       source: @prepareSource()
@@ -321,3 +341,4 @@ define [
       issuesLimitReached: @model.get('activeIssues')?.length > ISSUES_LIMIT
       linesLimit: LINES_LIMIT
       linesLimitReached: _.size(@model.get 'source') > LINES_LIMIT
+      baseDuplications: @getBaseDuplications()
index 2af05daf721cdd2fb4c139728fe95ff0b7d54194..24ea55f22de7f0b40d3690b78266aa93f15141bb 100644 (file)
@@ -64,7 +64,6 @@ define [
 
 
     onRender: ->
-      @$el.prop('id', "issue-#{@model.id}").data('issue-key', @model.id)
 
 
     resetIssue: (options, p) ->
index 738826ecc0e1e67a2cb6fe32465a63c9a1d51ef1..e3143ceebdd898991877821f31607a3e309810b8 100644 (file)
@@ -2,48 +2,44 @@ define [
   'backbone'
   'backbone.marionette'
   'templates/issues'
+  'source-viewer/viewer'
   'issues/models/issues'
   'issues/component-viewer/issue-view'
 ], (
   Backbone
   Marionette
   Templates
+  SourceViewer
   Issues
   IssueView
 ) ->
 
   $ = jQuery
 
-  API_SOURCES = "#{baseUrl}/api/sources/lines"
-  LINES_AROUND = 200
   TOP_OFFSET = 38
   BOTTOM_OFFSET = 10
-  ISSUES_LIMIT = 100
 
 
-  class extends Marionette.ItemView
-    template: Templates['issues-component-viewer']
+  class extends SourceViewer
 
-
-    ui:
-      sourceBeforeSpinner: '.js-component-viewer-source-before'
-      sourceAfterSpinner: '.js-component-viewer-source-after'
-
-
-    events:
-      'click .js-close-component-viewer': 'closeComponentViewer'
-      'click .sym': 'highlightUsages'
-      'click .code-issue': 'selectIssue'
+    events: ->
+      _.extend super,
+        'click .js-close-component-viewer': 'closeComponentViewer'
+        'click .sym': 'highlightUsages'
+        'click .code-issue': 'selectIssue'
 
 
     initialize: (options) ->
-      @component = new Backbone.Model()
-      @issues = new Issues()
-      @issueViews = []
+      super
+      @listenTo @, 'loaded', @onLoaded
       @listenTo options.app.state, 'change:selectedIndex', @select
-      @loadSourceBeforeThrottled = _.throttle @loadSourceBefore, 1000
-      @loadSourceAfterThrottled = _.throttle @loadSourceAfter, 1000
-      @scrollTimer = null
+
+
+    onLoaded: ->
+      @bindScrollEvents()
+      @bindShortcuts()
+      if @baseIssue?
+        @scrollToLine @baseIssue.get 'line'
 
 
     bindShortcuts: ->
@@ -85,106 +81,12 @@ define [
       key.deleteScope 'componentViewer'
 
 
-    bindScrollEvents: ->
-      $(window).on 'scroll.issues-component-viewer', (=> @onScroll())
-
-
-    unbindScrollEvents: ->
-      $(window).off 'scroll.issues-component-viewer'
-
-
-    disablePointerEvents: ->
-      clearTimeout @scrollTimer
-      $('body').addClass 'disabled-pointer-events'
-      @scrollTimer = setTimeout (-> $('body').removeClass 'disabled-pointer-events'), 250
-
-
-    onScroll: ->
-      @disablePointerEvents()
-
-      if @model.get('hasSourceBefore') && $(window).scrollTop() <= @ui.sourceBeforeSpinner.offset().top
-        @loadSourceBeforeThrottled()
-
-      if @model.get('hasSourceAfter') && $(window).scrollTop() + $(window).height() >= @ui.sourceAfterSpinner.offset().top
-        @loadSourceAfterThrottled()
-
-
-    loadSourceBefore: ->
-      @unbindScrollEvents()
-      source = @model.get 'source'
-      firstLine = _.first(source).line
-      @requestSources(firstLine - LINES_AROUND, firstLine - 1).done (data) =>
-        source = data.sources.concat source
-        @model.set
-          source: source
-          hasSourceBefore: data.sources.length == LINES_AROUND
-        @render()
-        @scrollToLine firstLine
-        @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
-
-
-    loadSourceAfter: ->
-      @unbindScrollEvents()
-      source = @model.get 'source'
-      lastLine = _.last(source).line
-      @requestSources(lastLine + 1, lastLine + LINES_AROUND)
-      .done (data) =>
-        source = source.concat data.sources
-        @model.set
-          source: source
-          hasSourceAfter: data.sources.length == LINES_AROUND
-        @render()
-        @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
-      .fail =>
-        @model.set
-          source: []
-          hasSourceAfter: false
-        @render()
-
-
     onClose: ->
-      @issueViews.forEach (view) -> view.close()
-      @issuesView = []
+      super
       @unbindScrollEvents()
       @unbindShortcuts()
 
 
-    onRender: ->
-      @renderIssues()
-
-
-    renderIssues: ->
-      @hiddenIssues = false
-      @issues.forEach (issue) =>
-        @renderIssue issue
-      if @hiddenIssues
-        issues = @$('.issue').length
-        warn = $('<div class="process-spinner shown">' + tp('issues.issues_limit_reached', issues) + '</div>')
-        $('body').append warn
-        setTimeout (-> warn.remove()), 3000
-
-
-    addIssue: (issue) ->
-      @renderIssue issue
-
-
-    renderIssue: (issue) ->
-      line = issue.get('line') || 0
-      row = @$("[data-line-number=#{line}]")
-      issueView = new IssueView app: @options.app, model: issue
-      @issueViews.push issueView
-      showBox = Math.abs(issue.get('index') - @model.get('issueIndex')) < ISSUES_LIMIT / 2
-      @hiddenIssues = true unless showBox
-      if showBox && row.length > 0
-        row.removeClass 'hidden'
-        line = row.find '.line'
-        line.addClass 'has-issues'
-        issues = line.find '.issue-list'
-        if issues.length == 0
-          issues = $('<div class="issue-list"></div>').appendTo line
-        issueView.render().$el.appendTo issues
-
-
     select: ->
       selected = @options.app.state.get 'selectedIndex'
       selectedIssue = @options.app.issues.at selected
@@ -231,49 +133,48 @@ define [
 
 
     openFileByIssue: (issue) ->
+      @baseIssue = issue
       componentKey = issue.get 'component'
       componentUuid = issue.get 'componentUuid'
+      @open componentUuid, componentKey
+
+
+    linesLimit: ->
+      line = @LINES_LIMIT / 2
+      if @baseIssue? && @baseIssue.has('line')
+        line = Math.max line, @baseIssue.get('line')
+      from: line - @LINES_LIMIT / 2 + 1
+      to: line + @LINES_LIMIT / 2
 
-      line = issue.get('line') || 0
-      @model.set
-        id: componentUuid
-        key: componentKey
-        issueLine: line
-        issueIndex: issue.get 'index'
-
-      @requestSources(line - LINES_AROUND, line + LINES_AROUND)
-      .done (data) =>
-        @model.set source: data.sources
-        firstLine = _.first(data.sources).line
-        lastLine = _.last(data.sources).line
-        @model.set
-          hasSourceBefore: firstLine > 1
-          hasSourceAfter: lastLine == line + LINES_AROUND
-        @requestIssues().done =>
-          @issues.reset @options.app.issues.filter (issue) => issue.get('component') == componentKey
-          @render()
-          @bindScrollEvents()
-          @bindShortcuts()
-          @scrollToLine issue.get 'line'
-      .fail =>
-        @model.set source: []
-        @model.set hasSourceBefore: false, hasSourceAfter: false
-        @issues.reset @options.app.issues.filter (issue) => issue.get('component') == componentKey
-        @render()
-        @bindShortcuts()
-        @scrollToLine issue.get 'line'
-
-
-
-    requestSources: (lineFrom, lineTo) ->
-      lineFrom = Math.max 0, lineFrom
-      $.get API_SOURCES, uuid: @model.id, from: lineFrom, to: lineTo
+
+    limitIssues: (issues) ->
+      index = @ISSUES_LIMIT / 2
+      if @baseIssue? && @baseIssue.has('index')
+        index = Math.max index, @baseIssue.get('index')
+      x = issues.filter (issue) =>
+        Math.abs(issue.get('index') - index) <= @ISSUES_LIMIT / 2
+      x
 
 
     requestIssues: ->
+      console.log 'Request issues'
       if @options.app.issues.last().get('component') == @model.get('key')
-        @options.app.controller.fetchNextPage()
-      else $.Deferred().resolve().promise()
+        r = @options.app.controller.fetchNextPage()
+      else r = $.Deferred().resolve().promise()
+      r.done =>
+        @issues.reset @options.app.issues.filter (issue) => issue.get('component') == @model.key()
+        @issues.reset @limitIssues @issues
+        @addIssuesPerLineMeta @issues
+        console.log 'Issues loaded'
+
+
+    renderIssue: (issue) ->
+      issueView = new IssueView
+        el: '#issue-' + issue.get('key')
+        model: issue
+        app: @options.app
+      @issueViews.push issueView
+      issueView.render()
 
 
     addNextIssuesPage: ->
@@ -298,8 +199,3 @@ define [
       @$('.sym.highlighted').removeClass 'highlighted'
       @$(".sym.#{key}").addClass 'highlighted' unless highlighted
 
-
-    serializeData: ->
-      hasSCM = _.some @model.get('source'), (row) -> row.scmAuthor?
-      _.extend super,
-        hasSCM: hasSCM
index 6022dac9b9ab02a339ad8be6097ea8f67d245030..6d8d43b094f1fab8348f6790b9652a5ca378b450 100644 (file)
@@ -202,9 +202,7 @@ define [
       key.setScope 'componentViewer'
       @options.app.issuesView.unbindScrollEvents()
       @options.app.state.set 'component', @_prepareComponent(issue)
-      @options.app.componentViewer = new ComponentViewer
-        app: @options.app
-        model: new ComponentViewerState()
+      @options.app.componentViewer = new ComponentViewer app: @options.app
       @options.app.layout.workspaceComponentViewerRegion.show @options.app.componentViewer
       @options.app.layout.showComponentViewer()
       @options.app.componentViewer.openFileByIssue issue
index 4e6d76e0fb6b0a1a64bde952633e2df42ca79716..a39d515384bc42df2431b37e379a45c124fbe8ab 100644 (file)
@@ -8,14 +8,6 @@ define [
 
   $ = jQuery
 
-  $.fn.isolatedScroll = ->
-    @on 'wheel', (e) ->
-      delta = -e.originalEvent.deltaY
-      bottomOverflow = @scrollTop + $(@).outerHeight() - @scrollHeight >= 0
-      topOverflow = @scrollTop <= 0
-      e.preventDefault() if (delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)
-    @
-
 
   class extends Marionette.Layout
     template: Templates['issues-layout']
diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee
new file mode 100644 (file)
index 0000000..66b97c2
--- /dev/null
@@ -0,0 +1,42 @@
+define [
+  'backbone.marionette'
+  'templates/source-viewer'
+  'common/popup'
+  'component-viewer/utils'
+], (
+  Marionette
+  Templates
+  Popup
+  utils
+) ->
+
+  $ = jQuery
+
+
+  class extends Popup
+    template: Templates['source-viewer-coverage-popup']
+
+
+    events:
+      'click a[data-key]': 'goToFile'
+
+
+    onRender: ->
+      super
+      @$('.bubble-popup-container').isolatedScroll()
+
+
+    goToFile: (e) ->
+      el = $(e.currentTarget)
+      key = el.data 'key'
+      method = el.data 'method'
+      files = @model.get 'files'
+
+
+    serializeData: ->
+      files = @model.get 'files'
+      tests = _.groupBy @model.get('tests'), '_ref'
+      testFiles = _.map tests, (testSet, fileRef) ->
+        file: files[fileRef]
+        tests: testSet
+      testFiles: testFiles
diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee
new file mode 100644 (file)
index 0000000..378ae8d
--- /dev/null
@@ -0,0 +1,55 @@
+define [
+  'backbone.marionette'
+  'templates/component-viewer'
+  'common/popup'
+  'component-viewer/utils'
+], (
+  Marionette
+  Templates
+  Popup
+  utils
+) ->
+
+  $ = jQuery
+
+
+  class extends Popup
+    template: Templates['source-viewer-duplication-popup']
+
+
+    events:
+      'click a[data-key]': 'goToFile'
+
+
+    goToFile: (e) ->
+      key = $(e.currentTarget).data 'key'
+      line = $(e.currentTarget).data 'line'
+      files = @options.main.source.get('duplicationFiles')
+      options =  @collection.map (item) ->
+        file = files[item.get('_ref')]
+        x = utils.splitLongName file.name
+        key: file.key
+        name: x.name
+        subname: x.dir
+        component:
+          projectName: file.projectName
+          subProjectName: file.subProjectName
+        active: file.key == key
+      options = _.uniq options, (item) -> item.key
+
+
+    serializeData: ->
+      files = @model.get 'duplicationFiles'
+      groupedBlocks = _.groupBy @collection.toJSON(), '_ref'
+      duplications = _.map groupedBlocks, (blocks, fileRef) ->
+        blocks: blocks
+        file: files[fileRef]
+
+      duplications = _.sortBy duplications, (d) =>
+        a = d.file.projectName != @model.get 'projectName'
+        b = d.file.subProjectName != @model.get 'subProjectName'
+        c = d.file.key != @model.get 'key'
+        '' + a + b + c
+
+      component: @model.toJSON()
+      duplications: duplications
diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee
new file mode 100644 (file)
index 0000000..f1e96f0
--- /dev/null
@@ -0,0 +1,40 @@
+define [
+  'backbone.marionette'
+  'templates/source-viewer'
+  'common/popup'
+  'issue/manual-issue-view'
+], (
+  Marionette
+  Templates
+  Popup
+  ManualIssueView
+) ->
+
+
+  class extends Popup
+    template: Templates['source-viewer-line-options-popup']
+
+
+    events:
+      'click .js-get-permalink': 'getPermalink'
+      'click .js-add-manual-issue': 'addManualIssue'
+
+
+    getPermalink: (e) ->
+      e.preventDefault()
+      url = "#{baseUrl}/component/index#component=#{encodeURIComponent(@model.key())}&line=#{@options.line}"
+      windowParams = 'resizable=1,scrollbars=1,status=1'
+      window.open url, @model.get('name'), windowParams
+
+
+    addManualIssue: (e) ->
+      e.preventDefault()
+      line = @options.line
+      component = @model.key()
+      manualIssueView = new ManualIssueView
+        line: line
+        component: component
+        rules: @model.get 'manual_rules'
+      manualIssueView.render().$el.appendTo @options.row.find('.source-line-code')
+      manualIssueView.on 'add', (issue) =>
+        @trigger 'onManualIssueAdded', issue
diff --git a/server/sonar-web/src/main/coffee/source-viewer/source.coffee b/server/sonar-web/src/main/coffee/source-viewer/source.coffee
new file mode 100644 (file)
index 0000000..7badb90
--- /dev/null
@@ -0,0 +1,51 @@
+define [
+  'backbone'
+], (
+  Backbone
+) ->
+
+  class extends Backbone.Model
+    idAttribute: 'uuid'
+
+    defaults: ->
+      hasSource: false
+      hasCoverage: false
+      hasDuplications: false
+      hasSCM: false
+
+
+    key: ->
+      @get 'key'
+
+
+    addMeta: (meta) ->
+      source = @get 'source'
+      metaIdx = 0
+      metaLine = meta[metaIdx]
+      source.forEach (line) ->
+        while metaLine? && line.line > metaLine.line
+          metaIdx++
+          metaLine = meta[metaIdx]
+        if metaLine? && line.line == metaLine.line
+          _.extend line, metaLine
+          metaIdx++
+          metaLine = meta[metaIdx]
+      @set source: source
+
+
+    addDuplications: (duplications) ->
+      source = @get 'source'
+      return unless source
+      source.forEach (line) ->
+        lineDuplications = []
+        duplications.forEach (d, i) ->
+          duplicated = false
+          d.blocks.forEach (b) ->
+            if b._ref == '1'
+              lineFrom = b.from
+              lineTo = b.from + b.size
+              duplicated = true if line.line >= lineFrom && line.line <= lineTo
+          lineDuplications.push if duplicated then i + 1 else false
+        line.duplications = lineDuplications
+      @set source: source
+
diff --git a/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee b/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee
new file mode 100644 (file)
index 0000000..17d94c9
--- /dev/null
@@ -0,0 +1,360 @@
+define [
+  'backbone.marionette'
+  'templates/source-viewer'
+  'source-viewer/source'
+  'issue/models/issue'
+  'issue/collections/issues'
+  'issue/issue-view'
+
+  'source-viewer/popups/coverage-popup'
+  'source-viewer/popups/duplication-popup'
+  'source-viewer/popups/line-actions-popup'
+], (
+  Marionette
+  Templates
+  Source
+  Issue
+  Issues
+  IssueView
+
+  CoveragePopupView
+  DuplicationPopupView
+  LineActionsPopupView
+) ->
+
+  $ = jQuery
+
+  HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted'
+
+  log = (message) ->
+    console.log 'Source Viewer:', message
+
+
+  class extends Marionette.ItemView
+    className: 'source'
+    template: Templates['source-viewer']
+
+    ISSUES_LIMIT: 100
+    LINES_LIMIT: 1000
+    LINES_AROUND: 500
+
+
+    ui:
+      sourceBeforeSpinner: '.js-component-viewer-source-before'
+      sourceAfterSpinner: '.js-component-viewer-source-after'
+
+
+    events: ->
+      'click .source-line-covered': 'showCoveragePopup'
+      'click .source-line-partially-covered': 'showCoveragePopup'
+
+      'click .source-line-duplications': 'showDuplications'
+      'click .source-line-duplications-extra': 'showDuplicationPopup'
+
+      'click .source-line-number[data-line-number]': 'highlightLine'
+
+
+    initialize: ->
+      @model = new Source() unless @model?
+      @issues = new Issues()
+      @issueViews = []
+      @loadSourceBeforeThrottled = _.throttle @loadSourceBefore, 1000
+      @loadSourceAfterThrottled = _.throttle @loadSourceAfter, 1000
+      @scrollTimer = null
+
+
+    onRender: ->
+      log 'Render'
+      @renderIssues()
+
+
+    onClose: ->
+      @issueViews.forEach (view) -> view.close()
+      @issueViews = []
+
+
+    open: (id, key) ->
+      @model.clear()
+      @model.set uuid: id, key: key
+      @requestComponent().done =>
+        @requestSource()
+        .done =>
+          @requestCoverage().done =>
+            @requestDuplications().done =>
+              @requestIssues().done =>
+                @render()
+                @trigger 'loaded'
+        .fail =>
+          @model.set source: [{ line: 0 }]
+          @requestIssues().done =>
+            @render()
+            @trigger 'loaded'
+
+
+    requestComponent: ->
+      log 'Request component details...'
+      url = "#{baseUrl}/api/components/app"
+      options = key: @model.key()
+      $.get url, options, (data) =>
+        @model.set data
+        log 'Component loaded'
+
+
+    linesLimit: ->
+      from: 1
+      to: @LINES_LIMIT
+
+
+    requestSource: ->
+      log 'Request source...'
+      url = "#{baseUrl}/api/sources/lines"
+      options = _.extend { uuid: @model.id }, @linesLimit()
+      $.get url, options, (data) =>
+        source = data.sources || []
+        if source.length == 0 || (source.length > 0 && _.first(source).line == 1)
+          source.unshift { line: 0 }
+        firstLine = _.first(source).line
+        @model.set
+          source: source
+          hasSourceBefore: firstLine > 1
+          hasSourceAfter: true
+        log 'Source loaded'
+
+
+    requestCoverage: ->
+      log 'Request coverage'
+      url = "#{baseUrl}/api/coverage/show"
+      options = key: @model.key()
+      $.get url, options, (data) =>
+        hasCoverage = data? && data.coverage?
+        @model.set hasCoverage: hasCoverage
+        if hasCoverage
+          coverage = data.coverage.map (c) ->
+            status = 'partially-covered'
+            status = 'covered' if c[1] && c[3] == c[4]
+            status = 'uncovered' if !c[1] || c[4] == 0
+            line: +c[0]
+            covered: status
+        else coverage = []
+        @model.addMeta coverage
+        log 'Coverage loaded'
+
+
+    requestDuplications: ->
+      log 'Request duplications'
+      url = "#{baseUrl}/api/duplications/show"
+      options = key: @model.key()
+      $.get url, options, (data) =>
+        hasDuplications = data? && data.duplications?
+        if hasDuplications
+          duplications = {}
+          data.duplications.forEach (d, i) ->
+            d.blocks.forEach (b) ->
+              if b._ref == '1'
+                lineFrom = b.from
+                lineTo = b.from + b.size
+                duplications[i] = true for i in [lineFrom..lineTo]
+          duplications = _.pairs(duplications).map (line) ->
+            line: +line[0]
+            duplicated: line[1]
+        else duplications = []
+        @model.addMeta duplications
+        @model.addDuplications data.duplications
+        @model.set
+          duplications: data.duplications
+          duplicationFiles: data.files
+        log 'Duplications loaded'
+
+
+    requestIssues: ->
+      log 'Request issues'
+      options = data:
+        componentUuids: @model.id
+        extra_fields: 'actions,transitions,assigneeName,actionPlanName'
+        resolved: false
+        s: 'FILE_LINE'
+        asc: true
+      @issues.fetch(options).done =>
+        @issues.reset @limitIssues @issues
+        @addIssuesPerLineMeta @issues
+        log 'Issues loaded'
+
+
+    addIssuesPerLineMeta: (issues) ->
+      lines = {}
+      issues.forEach (issue) ->
+        line = issue.get('line') || 0
+        lines[line] = [] unless _.isArray lines[line]
+        lines[line].push issue.toJSON()
+      issuesPerLine = _.pairs(lines).map (line) ->
+        line: +line[0]
+        issues: line[1]
+      @model.addMeta issuesPerLine
+
+
+    limitIssues: (issues) ->
+      issues.first @ISSUES_LIMIT
+
+
+    renderIssues: ->
+      log 'Render issues'
+      @issues.forEach @renderIssue, @
+      log 'Issues rendered'
+
+
+    renderIssue: (issue) ->
+      issueView = new IssueView
+        el: '#issue-' + issue.get('key')
+        model: issue
+      @issueViews.push issueView
+      issueView.render()
+
+
+    addIssue: (issue) ->
+      line = issue.get('line') || 0
+      code = @$(".source-line-code[data-line-number=#{line}]")
+      issueList = code.find('.issue-list')
+      unless issueList.length > 0
+        issueList = $('<div class="issue-list"></div>')
+        code.append issueList
+      issueList.append "<div class=\"issue\" id=\"issue-#{issue.id}\"></div>"
+      @renderIssue issue
+
+
+    showSpinner: ->
+    hideSpinner: ->
+    resetShowBlocks: ->
+
+
+    showCoveragePopup: (e) ->
+      r = window.process.addBackgroundProcess()
+      e.stopPropagation()
+      $('body').click()
+      line = $(e.currentTarget).data 'line-number'
+      url = "#{baseUrl}/api/tests/test_cases"
+      options =
+        key: @model.key()
+        line: line
+      $.get url, options
+      .done (data) =>
+        popup = new CoveragePopupView
+          model: new Backbone.Model data
+          triggerEl: $(e.currentTarget)
+        popup.render()
+        window.process.finishBackgroundProcess r
+      .fail ->
+        window.process.failBackgroundProcess r
+
+
+    showDuplications: ->
+      @$('.source-line-duplications').addClass 'hidden'
+      @$('.source-line-duplications-extra').removeClass 'hidden'
+
+
+    showDuplicationPopup: (e) ->
+      e.stopPropagation()
+      $('body').click()
+      index = $(e.currentTarget).data 'index'
+      line = $(e.currentTarget).data 'line-number'
+      blocks = @model.get('duplications')[index - 1].blocks
+      blocks = _.filter blocks, (b) ->
+        (b._ref != '1') || (b._ref == '1' && b.from > line) || (b._ref == '1' && b.from + b.size < line)
+      popup = new DuplicationPopupView
+        triggerEl: $(e.currentTarget)
+        model: @model
+        collection: new Backbone.Collection blocks
+      popup.render()
+
+
+    showLineActionsPopup: (e) ->
+      e.stopPropagation()
+      $('body').click()
+      line = $(e.currentTarget).data 'line-number'
+      popup = new LineActionsPopupView
+        triggerEl: $(e.currentTarget)
+        model: @model
+        line: line
+        row: $(e.currentTarget).closest '.source-line'
+      popup.on 'onManualIssueAdded', (data) =>
+        @addIssue new Issue(data)
+      popup.render()
+
+
+    highlightLine: (e) ->
+      row = $(e.currentTarget).closest('.source-line')
+      highlighted = row.is ".#{HIGHLIGHTED_ROW_CLASS}"
+      @$(".#{HIGHLIGHTED_ROW_CLASS}").removeClass HIGHLIGHTED_ROW_CLASS
+      unless highlighted
+        row.addClass HIGHLIGHTED_ROW_CLASS
+        @showLineActionsPopup(e)
+
+
+    bindScrollEvents: ->
+      @$el.scrollParent().on 'scroll.source-viewer', (=> @onScroll())
+
+
+    unbindScrollEvents: ->
+      @$el.scrollParent().off 'scroll.source-viewer'
+
+
+    disablePointerEvents: ->
+      clearTimeout @scrollTimer
+      $('body').addClass 'disabled-pointer-events'
+      @scrollTimer = setTimeout (-> $('body').removeClass 'disabled-pointer-events'), 250
+
+
+    onScroll: ->
+      @disablePointerEvents()
+
+      p = @$el.scrollParent()
+      p = $(window) if p.is(document)
+      pTopOffset = if p.offset()? then p.offset().top else 0
+      if @model.get('hasSourceBefore') && (p.scrollTop() + pTopOffset <= @ui.sourceBeforeSpinner.offset().top)
+        @loadSourceBeforeThrottled()
+
+      if @model.get('hasSourceAfter') && (p.scrollTop() + pTopOffset + p.height() >= @ui.sourceAfterSpinner.offset().top)
+        @loadSourceAfterThrottled()
+
+
+    loadSourceBefore: ->
+      @unbindScrollEvents()
+      source = @model.get 'source'
+      firstLine = _.first(source).line
+      url = "#{baseUrl}/api/sources/lines"
+      options =
+        uuid: @model.id
+        from: firstLine - @LINES_AROUND
+        to: firstLine - 1
+      $.get url, options, (data) =>
+        source = (data.sources || []).concat source
+        if source.length == 0 || (source.length > 0 && _.first(source).line == 1)
+          source.unshift { line: 0 }
+        @model.set
+          source: source
+          hasSourceBefore: data.sources.length == @LINES_AROUND
+        @render()
+        @scrollToLine firstLine
+        @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
+
+
+    loadSourceAfter: ->
+      @unbindScrollEvents()
+      source = @model.get 'source'
+      lastLine = _.last(source).line
+      url = "#{baseUrl}/api/sources/lines"
+      options =
+        uuid: @model.id
+        from: lastLine + 1
+        to: lastLine + @LINES_AROUND
+      $.get url, options
+      .done (data) =>
+        source = source.concat data.sources
+        @model.set
+          source: source
+          hasSourceAfter: data.sources.length == @LINES_AROUND
+        @render()
+        @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
+      .fail =>
+        @model.set hasSourceAfter: false
+        @render()
+        @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
index 0bfd74281462e88891d37f26dade0e72416999ce..10d494cd69536565f81d9f520324dcecb25682e1 100644 (file)
@@ -1,16 +1,17 @@
-<tr class="row row-expand">
+<tr class="source-line source-line-expand">
+  <td class="source-meta source-line-number"></td>
+  {{#if settings.scm}}
+    <td class="source-meta source-line-scm"></td>
+  {{/if}}
   {{#if settings.coverage}}
-    <td class="stat coverage-tests"></td>
-    <td class="stat coverage-conditions"></td>
+    <td class="source-meta source-line-coverage"></td>
   {{/if}}
   {{#if settings.duplications}}
-    <td class="stat"></td>
-  {{/if}}
-  {{#if settings.scm}}
-    <td class="stat"></td>
+    {{#each baseDuplications}}
+      <td class="source-meta source-line-duplications-extra"></td>
+    {{/each}}
   {{/if}}
-  <td class="stat lid">
+  <td class="source-line-code">
     <button class="button-clean js-expand" data-from="{{from}}" data-to="{{to}}"><i class="icon-expand"></i></button>
   </td>
-  <td class="line"></td>
 </tr>
index 2498a590d5cc12f6ca403b1ebc605c7450ff2ffe..47a371da8dfc1add0e3e55a30a12d9a9b946c874 100644 (file)
     <p class="marginbottom10 js-duplications-in-deleted-files">{{t 'duplications.dups_found_on_deleted_resource'}}</p>
   {{/if}}
 
-  <table class="code">
-    {{#if showZeroLine}}
-      <tr class="row row-hidden" data-line-number="0" id="{{uid}}-0">
-        {{#if settings.coverage}}
-          <td class="stat coverage-tests"></td>
-          <td class="stat coverage-conditions"></td>
-        {{/if}}
-        {{#if settings.duplications}}
-          <td class="stat"></td>
-        {{/if}}
-        {{#if settings.scm}}
-          <td class="stat"></td>
-        {{/if}}
-        <td class="stat lid"></td>
-        <td class="line"></td>
-      </tr>
-    {{/if}}
-
-    {{#each source}}
-      {{#if show}}
-        <tr class="row" data-line-number="{{lineNumber}}" id="{{../../uid}}-{{lineNumber}}">
-
-          <td class="stat lid js-line-actions" title="{{t 'component_viewer.line_actions'}}">{{lineNumber}}</td>
-
-          {{#if ../../settings.coverage}}
-            <td class="stat {{#if coverage}}coverage-{{#if coverage.covered}}green{{else}}red{{/if}}{{/if}}">
-              {{#if coverage}}
-                <span class="coverage-tests" title="{{tp 'coverage_viewer.line_covered_by_x_tests' coverage.testCases}}">
-                  {{coverage.testCases}}
-                </span>
-              {{/if}}
-            </td>
+  <div class="source">
+    <table class="source-table">
+      {{#if showZeroLine}}
+        <tr class="source-line hidden" data-line-number="0" id="{{uid}}-0">
+          <td class="source-meta source-line-number" title="{{t 'component_viewer.line_actions'}}"></td>
 
-            <td class="stat {{#if coverage}}{{#if coverage.branchCoverageStatus}}coverage-{{coverage.branchCoverageStatus}}{{/if}}{{/if}}">
-              {{#if coverage}}
-                {{#if coverage.branches}}
-                  <span class="coverage-branches" title="{{tp 'coverage_viewer.x_covered_conditions' coverage.coveredBranches}}">
-                    {{coverage.coveredBranches}}/{{coverage.branches}}
-                  </span>
-                {{/if}}
-              {{/if}}
-            </td>
+          {{#if settings.scm}}
+            <td class="source-meta source-line-scm"></td>
           {{/if}}
 
-          {{#if ../../settings.duplications}}
-            <td class="stat duplications">
-              {{#each duplications}}
-                <span class="duplication {{#if this}}duplication-exists{{/if}}" data-index="{{this}}"></span>
-              {{/each}}
+          {{#if settings.coverage}}
+            <td class="source-meta source-line-coverage">
+              <div class="source-line-bar"></div>
             </td>
           {{/if}}
 
-          {{#if ../../settings.scm}}
-            <td class="stat {{#if scm}}scm{{/if}}">
-              {{#if scm}}
-                {{#ifSCMChanged ../../../../source ../../../lineNumber}}
-                  <span class="scm-author" title="{{dt scm.date}}&#013;{{scm.author}}">{{scm.author}}</span>
-                {{/ifSCMChanged}}
-              {{/if}}
-            </td>
+          {{#if settings.duplications}}
+            {{#each baseDuplications}}
+              <td class="source-meta source-line-duplications-extra">
+                <div class="source-line-bar"></div>
+              </td>
+            {{/each}}
           {{/if}}
 
-          <td class="line"><pre>{{{code}}}</pre></td>
+          <td class="source-line-code"></td>
         </tr>
       {{/if}}
-    {{/each}}
-  </table>
+
+      {{#each source}}
+        {{#if show}}
+          <tr class="source-line" data-line-number="{{lineNumber}}" id="{{../../uid}}-{{lineNumber}}">
+            <td class="source-meta source-line-number js-line-actions" title="{{t 'component_viewer.line_actions'}}"
+                {{#if lineNumber}}data-line-number="{{lineNumber}}"{{/if}}></td>
+
+            {{#if ../../settings.scm}}
+              <td class="source-meta {{#if scm}}source-line-scm{{/if}}">
+                {{#if scm}}
+                  {{#ifSCMChanged ../../../../source ../../../lineNumber}}
+                    <div class="source-line-scm-inner" title="{{scm.author}}&#013;{{dt scm.date}}" data-author="{{scm.author}}"></div>
+                  {{/ifSCMChanged}}
+                {{/if}}
+              </td>
+            {{/if}}
+
+            {{#if ../../settings.coverage}}
+              <td class="source-meta source-line-coverage {{#if coverage}}source-line-{{coverageStatus}}{{/if}}">
+                <div class="source-line-bar"></div>
+              </td>
+            {{/if}}
+
+            {{#if ../../settings.duplications}}
+              {{#each duplications}}
+                <td class="source-meta source-line-duplications-extra {{#if this}}source-line-duplicated{{/if}}"
+                    data-index="{{this}}" data-line-number="{{line}}">
+                  <div class="source-line-bar"></div>
+                </td>
+              {{/each}}
+            {{/if}}
+
+            <td class="source-line-code code"><pre>{{{code}}}</pre></td>
+          </tr>
+        {{/if}}
+      {{/each}}
+    </table>
+  </div>
 
 {{/if}}
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs
new file mode 100644 (file)
index 0000000..be7d99c
--- /dev/null
@@ -0,0 +1,27 @@
+<div class="bubble-popup-container">
+  <div class="bubble-popup-title">{{t 'component_viewer.transition.coverage'}}</div>
+
+  {{#each testFiles}}
+    <div class="bubble-popup-section">
+      <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}">
+        {{file.longName}}
+      </a>
+      <ul class="bubble-popup-list">
+        {{#each tests}}
+          <li class="component-viewer-popup-test" title="{{name}}">
+            <i class="component-viewer-popup-test-status {{testStatusIconClass status}}"></i>
+            <span class="component-viewer-popup-test-name">
+              <a class="component-viewer-popup-test-file link-action" title="{{name}}"
+                 data-key="{{../file.key}}" data-method="{{name}}">
+                {{name}}
+              </a>
+            </span>
+            <span class="component-viewer-popup-test-duration">{{durationInMs}}ms</span>
+          </li>
+        {{/each}}
+      </ul>
+    </div>
+  {{/each}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs
new file mode 100644 (file)
index 0000000..bf134d7
--- /dev/null
@@ -0,0 +1,31 @@
+<div class="bubble-popup-container">
+  <div class="bubble-popup-title">{{t 'component_viewer.transition.duplication'}}</div>
+  {{#each duplications}}
+    <div class="bubble-popup-section">
+      {{#notEqComponents file ../component}}
+        <div class="component-viewer-popup-label" title="{{projectFullName file}}">
+          <i class="icon-qualifier-trk"></i> {{projectFullName file}}
+        </div>
+      {{/notEqComponents}}
+
+      {{#notEq file.key ../component.key}}
+        <a class="link-action" data-key="{{file.key}}" title="{{file.name}}">
+          {{file.name}}
+        </a>
+      {{/notEq}}
+
+      <div class="component-viewer-popup-duplications">
+        Lines:
+        {{#joinEach blocks ','}}
+          <a class="link-action" data-key="{{../file.key}}" data-line="{{from}}">
+            {{from}} â€“ {{sum from size}}
+          </a>
+        {{/joinEach}}
+      </div>
+    </div>
+  {{else}}
+    {{t 'duplications.block_was_duplicated_by_a_deleted_resource'}}
+  {{/each}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs
new file mode 100644 (file)
index 0000000..facf640
--- /dev/null
@@ -0,0 +1,13 @@
+<div class="bubble-popup-container">
+  <div class="bubble-popup-section">
+    <a href="#" class="js-get-permalink link-action">{{t 'component_viewer.get_permalink'}}</a>
+  </div>
+
+  {{#if canCreateManualIssue}}
+    <div class="bubble-popup-section">
+      <a href="#" class="js-add-manual-issue link-action">{{t 'component_viewer.add_manual_issue'}}</a>
+    </div>
+  {{/if}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs
new file mode 100644 (file)
index 0000000..3d56acd
--- /dev/null
@@ -0,0 +1,51 @@
+{{#if hasSourceBefore}}
+  <i class="spinner js-component-viewer-source-before"></i>
+{{/if}}
+
+<table class="source-table">
+  {{#each source}}
+    <tr class="source-line {{#eq line 0}}{{#empty issues}}hidden{{/empty}}{{/eq}}">
+      <td class="source-meta source-line-number" {{#if line}}data-line-number="{{line}}"{{/if}}></td>
+
+      <td class="source-meta source-line-scm">
+        {{#ifSCMChanged2 ../source line}}
+          <div class="source-line-scm-inner" title="{{scmAuthor}} {{scmDate}}" data-author="{{scmAuthor}}"></div>
+        {{/ifSCMChanged2}}
+      </td>
+
+      <td class="source-meta source-line-coverage {{#notNull covered}}source-line-{{covered}}{{/notNull}}"
+          data-line-number="{{line}}">
+        <div class="source-line-bar"></div>
+      </td>
+
+      <td class="source-meta source-line-duplications {{#if duplicated}}source-line-duplicated{{/if}}">
+        <div class="source-line-bar"></div>
+      </td>
+
+      {{#each duplications}}
+        <td class="source-meta source-line-duplications-extra hidden {{#if this}}source-line-duplicated{{/if}}"
+            data-index="{{this}}" data-line-number="{{line}}">
+          <div class="source-line-bar"></div>
+        </td>
+      {{/each}}
+
+      <td class="source-line-code" data-line-number="{{line}}">
+        {{#notNull code}}
+          <pre>{{#if code}}{{{code}}}{{else}}&nbsp;{{/if}}</pre>
+        {{/notNull}}
+
+        {{#notEmpty issues}}
+          <div class="issue-list">
+            {{#each issues}}
+              <div class="issue" id="issue-{{key}}"></div>
+            {{/each}}
+          </div>
+        {{/notEmpty}}
+      </td>
+    </tr>
+  {{/each}}
+</table>
+
+{{#if hasSourceAfter}}
+  <i class="spinner js-component-viewer-source-after"></i>
+{{/if}}
index 5f19ef8529848a7048b91a61c8ed57138777872f..2c851270ee92daaba17ce880889e7452dd7ea1f4 100644 (file)
@@ -147,6 +147,11 @@ define(['handlebars'], function (Handlebars) {
     return cond ? options.fn(this) : options.inverse(this);
   });
 
+  Handlebars.registerHelper('empty', function(array, options) {
+    var cond = _.isArray(array) && array.length > 0;
+    return cond ? options.inverse(this) : options.fn(this);
+  });
+
   Handlebars.registerHelper('all', function() {
     var args = Array.prototype.slice.call(arguments, 0, -1),
         options = arguments[arguments.length - 1],
diff --git a/server/sonar-web/src/main/js/common/jquery-isolated-scroll.js b/server/sonar-web/src/main/js/common/jquery-isolated-scroll.js
new file mode 100644 (file)
index 0000000..1eeb1c4
--- /dev/null
@@ -0,0 +1,14 @@
+;(function ($) {
+
+  $.fn.isolatedScroll = function () {
+      this.on('wheel', function (e) {
+        var delta = -e.originalEvent.deltaY;
+        var bottomOverflow = this.scrollTop + $(this).outerHeight() - this.scrollHeight >= 0;
+        var topOverflow = this.scrollTop <= 0;
+        if ((delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)) {
+          e.preventDefault();
+        }
+      });
+  };
+
+})(window.jQuery);
index fe39602530479d68b485a19d88c4f58605d903d4..bac92d8b5d1a7d4344599228c545d65d356c8afd 100644 (file)
@@ -17,7 +17,7 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -27,41 +27,41 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
 
       .then(function () {
         casper.click('.js-filter-lines-to-cover');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 149);
-          test.assertElementCount('.coverage-red', 51);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertElementCount('.component-viewer-source .row', 369);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 142);
+          test.assertElementCount('.source-line-uncovered', 50);
+          test.assertElementCount('.source-line-partially-covered', 2);
+          test.assertElementCount('.component-viewer-source .source-line', 369);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-uncovered-lines');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 18);
-          test.assertElementCount('.coverage-red', 51);
-          test.assertElementCount('.coverage-orange', 0);
-          test.assertElementCount('.component-viewer-source .row', 136);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 18);
+          test.assertElementCount('.source-line-uncovered', 50);
+          test.assertElementCount('.source-line-partially-covered', 0);
+          test.assertElementCount('.component-viewer-source .source-line', 136);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-branches-to-cover');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 26);
-          test.assertElementCount('.coverage-red', 4);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertElementCount('.component-viewer-source .row', 33);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 19);
+          test.assertElementCount('.source-line-uncovered', 3);
+          test.assertElementCount('.source-line-partially-covered', 2);
+          test.assertElementCount('.component-viewer-source .source-line', 33);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-uncovered-branches');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 6);
-          test.assertElementCount('.coverage-red', 4);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertElementCount('.component-viewer-source .row', 13);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 4);
+          test.assertElementCount('.source-line-uncovered', 3);
+          test.assertElementCount('.source-line-partially-covered', 2);
+          test.assertElementCount('.component-viewer-source .source-line', 13);
         });
       })
 
@@ -83,13 +83,13 @@ casper.test.begin(testName('Go From Coverage to Test File'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
         casper.click('.js-toggle-coverage');
-        casper.waitForSelector('.coverage-green', function () {
-          casper.click('.coverage-green .coverage-tests');
+        casper.waitForSelector('.source-line-covered', function () {
+          casper.click('.source-line-covered');
           casper.waitForSelector('.bubble-popup', function () {
             test.assertSelectorContains('.bubble-popup', 'src/test/java/org/sonar/batch/issue/IssueCacheTest.java');
             test.assertSelectorContains('.bubble-popup', 'should_update_existing_issue');
index a0fc453373263eacca5cb7bc7414630d9cc8a312..fbce585badd6735db82632f96e0e9507ba89720d 100644 (file)
@@ -17,20 +17,20 @@ casper.test.begin(testName('Filters'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
         casper.click('.js-header-tab-duplications');
-        casper.waitForSelector('.duplication-exists', function () {
+        casper.waitForSelector('.source-line-duplicated', function () {
           test.assertExists('.js-filter-duplications.active');
-          test.assertElementCount('.component-viewer-source .row', 39);
+          test.assertElementCount('.component-viewer-source .source-line', 39);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-duplications');
-        test.assertElementCount('.component-viewer-source .row', 520);
+        test.assertElementCount('.component-viewer-source .source-line', 520);
       })
 
       .run(function () {
@@ -53,15 +53,15 @@ casper.test.begin(testName('Cross-Project'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
         casper.click('.js-header-tab-duplications');
         casper.waitForSelector('.js-filter-duplications', function () {
           casper.click('.js-filter-duplications');
-          casper.waitForSelector('.duplication-exists', function () {
-            casper.click('.duplication-exists');
+          casper.waitForSelector('.source-line-duplicated', function () {
+            casper.click('.source-line-duplicated');
             casper.waitForSelector('.bubble-popup', function () {
               test.assertSelectorContains('.bubble-popup', 'JavaScript');
               test.assertSelectorContains('.bubble-popup', 'JavaScript :: Sonar Plugin');
@@ -92,12 +92,12 @@ casper.test.begin(testName('In Deleted Files'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
         casper.click('.js-toggle-duplications');
-        casper.waitForSelector('.duplication-exists', function () {
+        casper.waitForSelector('.source-line-duplicated', function () {
           test.assertExists('.js-duplications-in-deleted-files');
         });
       })
index 8776419f7b0337325563552a004eb6fbc09e1cfc..b3cce4f92c2dfdf8e6d2442e2ee1faf2d0e5b599 100644 (file)
@@ -17,7 +17,7 @@ casper.test.begin(testName('Mark as Favorite'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -49,7 +49,7 @@ casper.test.begin(testName('Don\'t Show Favorite If Not Logged In'), function (t
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
index a1c716ba5dec25eb176ec45bd64a66b06621f287..3ea8cb5d1cd8cd8cea8ce5c801640ee7875257c5 100644 (file)
@@ -17,7 +17,7 @@ casper.test.begin(testName('Filters'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -116,7 +116,7 @@ casper.test.begin(testName('On File Level'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -130,7 +130,7 @@ casper.test.begin(testName('On File Level'), function (test) {
       })
 
       .then(function () {
-        test.assertVisible('.component-viewer-source .row[data-line-number="0"]');
+        test.assertVisible('.component-viewer-source .source-line[data-line-number="0"]');
         test.assertExists('#issue-20002ec7-b647-44da-bdf5-4d9fbf4b7c58');
       })
 
@@ -152,7 +152,7 @@ casper.test.begin(testName('Bulk Change Link Exists'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
index 2960f784d25460fa9d406203f960b345fccfc4ba..92b0e485907f39efb326a9885b09c70f161b2e50 100644 (file)
@@ -16,7 +16,7 @@ casper.test.begin(testName('Lines Filters'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -26,12 +26,12 @@ casper.test.begin(testName('Lines Filters'), function (test) {
 
       .then(function () {
         casper.click('.js-filter-ncloc');
-        test.assertElementCount('.component-viewer-source .row', 451);
+        test.assertElementCount('.component-viewer-source .source-line', 451);
       })
 
       .then(function () {
         casper.click('.js-filter-ncloc');
-        test.assertElementCount('.component-viewer-source .row', 520);
+        test.assertElementCount('.component-viewer-source .source-line', 520);
       })
 
       .run(function () {
@@ -51,7 +51,7 @@ casper.test.begin(testName('Do Not Show Ncloc Filter If No Data'), function (tes
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
index 4249ea35d7d035bd7a0f944909ab848b915ddb61..019cdca2879ed7136d7777085b28b9c3718935fd 100644 (file)
@@ -15,7 +15,7 @@ casper.test.begin(testName('Link to Raw'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
index 57ea4a016fc088981e5c48dbddefc426d6357339..f47a365a41e67c91f93b98737b2c4d3b6e13ff05 100644 (file)
@@ -21,7 +21,7 @@ casper.test.begin(testName('sqale_index'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -29,7 +29,7 @@ casper.test.begin(testName('sqale_index'), function (test) {
           test.assertExists('.js-toggle-issues.active');
           test.assertExists('.component-viewer-header-expanded-bar.active');
           test.assertExists('.js-filter-unresolved-issues.active');
-          test.assertElementCount('.component-viewer-source .row', 56);
+          test.assertElementCount('.component-viewer-source .source-line', 56);
         });
       })
 
@@ -55,7 +55,7 @@ casper.test.begin(testName('minor_violations'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -63,7 +63,7 @@ casper.test.begin(testName('minor_violations'), function (test) {
           test.assertExists('.js-toggle-issues.active');
           test.assertExists('.component-viewer-header-expanded-bar.active');
           test.assertExists('.js-filter-MINOR-issues.active');
-          test.assertElementCount('.component-viewer-source .row', 11);
+          test.assertElementCount('.component-viewer-source .source-line', 11);
         });
       })
 
@@ -89,15 +89,15 @@ casper.test.begin(testName('line_coverage'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
-        casper.waitForSelector('.coverage-green', function () {
+        casper.waitForSelector('.source-line-covered', function () {
           test.assertExists('.js-toggle-coverage.active');
           test.assertExists('.component-viewer-header-expanded-bar.active');
           test.assertExists('.js-filter-lines-to-cover.active');
-          test.assertElementCount('.component-viewer-source .row', 369);
+          test.assertElementCount('.component-viewer-source .source-line', 369);
         });
       })
 
@@ -123,15 +123,15 @@ casper.test.begin(testName('duplicated_lines'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
-        casper.waitForSelector('.duplication-exists', function () {
+        casper.waitForSelector('.source-line-duplicated', function () {
           test.assertExists('.js-toggle-duplications.active');
           test.assertExists('.component-viewer-header-expanded-bar.active');
           test.assertExists('.js-filter-duplications.active');
-          test.assertElementCount('.component-viewer-source .row', 39);
+          test.assertElementCount('.component-viewer-source .source-line', 39);
         });
       })
 
index a296d84cb84f300f616072a40d08cd565d90edef..8aee5a10dce0e13053c448d53655573a1560136f 100644 (file)
@@ -16,7 +16,7 @@ casper.test.begin(testName('Base'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row', function () {
+        casper.waitForSelector('.component-viewer-source .source-line', function () {
           // Check header elements
           test.assertExists('.component-viewer-header');
           test.assertSelectorContains('.component-viewer-header-component-project', 'SonarQube');
@@ -35,7 +35,7 @@ casper.test.begin(testName('Base'), function (test) {
           test.assertExists('.js-header-tab-scm');
 
           // Check source
-          test.assertElementCount('.component-viewer-source .row', 520);
+          test.assertElementCount('.component-viewer-source .source-line', 520);
           test.assertSelectorContains('.component-viewer-source', 'public class Cache');
 
           // Check workspace
@@ -64,7 +64,7 @@ casper.test.begin(testName('Decoration'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -84,16 +84,14 @@ casper.test.begin(testName('Decoration'), function (test) {
       .then(function () {
         // Check coverage decoration
         casper.click('.js-toggle-coverage');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 149);
-          test.assertSelectorContains('.coverage-green', '27');
-          test.assertElementCount('.coverage-red', 51);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertSelectorContains('.coverage-orange', '1/2');
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 142);
+          test.assertElementCount('.source-line-uncovered', 50);
+          test.assertElementCount('.source-line-partially-covered', 2);
 
           casper.click('.js-toggle-coverage');
-          casper.waitWhileSelector('.coverage-green', function () {
-            test.assertDoesntExist('.coverage-green');
+          casper.waitWhileSelector('.source-line-covered', function () {
+            test.assertDoesntExist('.source-line-covered');
           });
         });
       })
@@ -101,12 +99,12 @@ casper.test.begin(testName('Decoration'), function (test) {
       .then(function () {
         // Check duplications decoration
         casper.click('.js-toggle-duplications');
-        casper.waitForSelector('.duplication-exists', function () {
-          test.assertElementCount('.duplication-exists', 32);
+        casper.waitForSelector('.source-line-duplicated', function () {
+          test.assertElementCount('.source-line-duplicated', 32);
 
           casper.click('.js-toggle-duplications');
-          casper.waitWhileSelector('.duplication-exists', function () {
-            test.assertDoesntExist('.duplication-exists');
+          casper.waitWhileSelector('.source-line-duplicated', function () {
+            test.assertDoesntExist('.source-line-duplicated');
           });
         });
       })
@@ -114,14 +112,14 @@ casper.test.begin(testName('Decoration'), function (test) {
       .then(function () {
         // Check scm decoration
         casper.click('.js-toggle-scm');
-        casper.waitForSelector('.scm-author', function () {
-          test.assertElementCount('.scm-author', 182);
-          test.assertSelectorContains('.scm-author', 'simon.brandhof@gmail.com');
-          test.assertSelectorContains('.scm-author', 'julien.henry@sonarsource.com');
+        casper.waitForSelector('.source-line-scm-inner', function () {
+          test.assertElementCount('.source-line-scm-inner', 182);
+          test.assertExists('.source-line-scm-inner[data-author="simon.brandhof@gmail.com"]');
+          test.assertExists('.source-line-scm-inner[data-author="julien.henry@sonarsource.com"]');
 
           casper.click('.js-toggle-scm');
-          casper.waitWhileSelector('.scm-author', function () {
-            test.assertDoesntExist('.scm-author');
+          casper.waitWhileSelector('.source-line-scm-inner', function () {
+            test.assertDoesntExist('.source-line-scm-inner');
           });
         });
       })
@@ -141,10 +139,14 @@ casper.test.begin(testName('Header'), function (test) {
         lib.mockRequestFromFile('/api/components/app', 'app.json');
         lib.mockRequestFromFile('/api/sources/show', 'source.json');
         lib.mockRequestFromFile('/api/resources', 'resources.json');
+        lib.mockRequestFromFile('/api/issues/search', 'issues.json');
+        lib.mockRequestFromFile('/api/coverage/show', 'coverage.json');
+        lib.mockRequestFromFile('/api/duplications/show', 'duplications.json');
+        lib.mockRequestFromFile('/api/sources/scm', 'scm.json');
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -244,7 +246,7 @@ casper.test.begin(testName('Test File'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -284,13 +286,13 @@ casper.test.begin(testName('Go From Coverage to Test File'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
         casper.click('.js-toggle-coverage');
-        casper.waitForSelector('.coverage-green', function () {
-          casper.click('.coverage-green .coverage-tests');
+        casper.waitForSelector('.source-line-covered', function () {
+          casper.click('.source-line-covered');
           casper.waitForSelector('.bubble-popup', function () {
             test.assertSelectorContains('.bubble-popup', 'src/test/java/org/sonar/batch/issue/IssueCacheTest.java');
             test.assertSelectorContains('.bubble-popup', 'should_update_existing_issue');
@@ -329,7 +331,7 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
@@ -339,41 +341,41 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
 
       .then(function () {
         casper.click('.js-filter-lines-to-cover');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 149);
-          test.assertElementCount('.coverage-red', 51);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertElementCount('.component-viewer-source .row', 369);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 142);
+          test.assertElementCount('.source-line-uncovered', 50);
+          test.assertElementCount('.source-line-partially-covered', 2);
+          test.assertElementCount('.component-viewer-source .source-line', 369);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-uncovered-lines');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 18);
-          test.assertElementCount('.coverage-red', 51);
-          test.assertElementCount('.coverage-orange', 0);
-          test.assertElementCount('.component-viewer-source .row', 136);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 18);
+          test.assertElementCount('.source-line-uncovered', 50);
+          test.assertElementCount('.source-line-partially-covered', 0);
+          test.assertElementCount('.component-viewer-source .source-line', 136);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-branches-to-cover');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 26);
-          test.assertElementCount('.coverage-red', 4);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertElementCount('.component-viewer-source .row', 33);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 19);
+          test.assertElementCount('.source-line-uncovered', 3);
+          test.assertElementCount('.source-line-partially-covered', 2);
+          test.assertElementCount('.component-viewer-source .source-line', 33);
         });
       })
 
       .then(function () {
         casper.click('.js-filter-uncovered-branches');
-        casper.waitForSelector('.coverage-green', function () {
-          test.assertElementCount('.coverage-green', 6);
-          test.assertElementCount('.coverage-red', 4);
-          test.assertElementCount('.coverage-orange', 2);
-          test.assertElementCount('.component-viewer-source .row', 13);
+        casper.waitForSelector('.source-line-covered', function () {
+          test.assertElementCount('.source-line-covered', 4);
+          test.assertElementCount('.source-line-uncovered', 3);
+          test.assertElementCount('.source-line-partially-covered', 2);
+          test.assertElementCount('.component-viewer-source .source-line', 13);
         });
       })
 
@@ -397,19 +399,19 @@ casper.test.begin(testName('Ability to Deselect Filters'), function (test) {
       })
 
       .then(function () {
-        casper.waitForSelector('.component-viewer-source .row');
+        casper.waitForSelector('.component-viewer-source .source-line');
       })
 
       .then(function () {
         casper.click('.js-header-tab-issues');
         var testFilter = '.js-filter-unresolved-issues';
         casper.waitForSelector(testFilter + '.active', function () {
-          lib.waitForElementCount('.component-viewer-source .row', 56, function () {
+          lib.waitForElementCount('.component-viewer-source .source-line', 56, function () {
             casper.click(testFilter);
-            lib.waitForElementCount('.component-viewer-source .row', 520, function () {
+            lib.waitForElementCount('.component-viewer-source .source-line', 520, function () {
               test.assertDoesntExist(testFilter + '.active');
               casper.click(testFilter);
-              lib.waitForElementCount('.component-viewer-source .row', 56, function () {
+              lib.waitForElementCount('.component-viewer-source .source-line', 56, function () {
                 test.assertExists(testFilter + '.active');
               });
             });
@@ -421,12 +423,12 @@ casper.test.begin(testName('Ability to Deselect Filters'), function (test) {
         casper.click('.js-header-tab-coverage');
         var testFilter = '.js-filter-lines-to-cover';
         casper.waitForSelector(testFilter + '.active', function () {
-          lib.waitForElementCount('.component-viewer-source .row', 369, function () {
+          lib.waitForElementCount('.component-viewer-source .source-line', 369, function () {
             casper.click(testFilter);
-            lib.waitForElementCount('.component-viewer-source .row', 520, function () {
+            lib.waitForElementCount('.component-viewer-source .source-line', 520, function () {
               test.assertDoesntExist(testFilter + '.active');
               casper.click(testFilter);
-              lib.waitForElementCount('.component-viewer-source .row', 369, function () {
+              lib.waitForElementCount('.component-viewer-source .source-line', 369, function () {
                 test.assertExists(testFilter + '.active');
               });
             });
@@ -438,12 +440,12 @@ casper.test.begin(testName('Ability to Deselect Filters'), function (test) {
         casper.click('.js-header-tab-duplications');
         var testFilter = '.js-filter-duplications';
         casper.waitForSelector(testFilter + '.active', function () {
-          lib.waitForElementCount('.component-viewer-source .row', 39, function () {
+          lib.waitForElementCount('.component-viewer-source .source-line', 39, function () {
             casper.click(testFilter);
-            lib.waitForElementCount('.component-viewer-source .row', 520, function () {
+            lib.waitForElementCount('.component-viewer-source .source-line', 520, function () {
               test.assertDoesntExist(testFilter + '.active');
               casper.click(testFilter);
-              lib.waitForElementCount('.component-viewer-source .row', 39, function () {
+              lib.waitForElementCount('.component-viewer-source .source-line', 39, function () {
                 test.assertExists(testFilter + '.active');
               });
             });
index 8ca1788ef74e9741cb625c2dce1137eab2d48794..9c5d4027839b8fc7f7adbd8c9234359b473915d2 100644 (file)
@@ -1,13 +1,6 @@
 @import (reference) 'variables';
 @import (reference) 'mixins';
 
-.code pre {
-  padding: 0;
-  font-family: @monoFontFamily;
-  font-size: 12px;
-  line-height: 16px;
-}
-
 /* constants */
 .code .c {
   font-style: normal;
index f53ed457f0ede6e780d5560dd8056ecf7caa950f..834d9d6912bef35faab5ae665e438d72b8801eef 100644 (file)
@@ -1,5 +1,5 @@
 @import "components/component-issues";
-@import "components/code-source";
+@import "components/source";
 @import "components/facets";
 @import "components/modals";
 @import "components/issues";
diff --git a/server/sonar-web/src/main/less/components/code-source.less b/server/sonar-web/src/main/less/components/code-source.less
deleted file mode 100644 (file)
index a657c5b..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-@import (reference) "../mixins";
-@import (reference) "../variables";
-
-.code-source {
-
-  .code {
-    width: 100%;
-    border: 1px solid @barBorderColor;
-  }
-
-  .code th {
-    height: 30px;
-    .box-sizing(border-box);
-    background-color: @barBackgroundColor;
-
-    &.lid {
-      border-right: 1px solid @barBorderColor;
-    }
-
-    &.stat {
-      padding-top: 4px;
-      padding-bottom: 4px;
-      border-left: none;
-      border-right: none;
-      border-bottom: 1px solid @barBorderColor;
-    }
-  }
-
-  .code .stat {
-    vertical-align: top;
-    min-width: 12px;
-    padding: 1px 5px;
-    background-color: @barBackgroundColor;
-    color: #888;
-    font-size: @smallFontSize;
-    line-height: 16px;
-    text-align: right;
-    cursor: default;
-    white-space: nowrap;
-  }
-
-  .code .lid {
-    min-width: 18px;
-    padding-left: 10px;
-    padding-right: 10px;
-    cursor: pointer;
-  }
-
-  .code .scm {
-    line-height: 16px;
-    padding-top: 0;
-    padding-bottom: 0;
-    text-align: left;
-
-    .scm-date {
-      display: inline-block;
-      vertical-align: middle;
-      padding: 2px 4px;
-      line-height: 1;
-      background-color: @barBorderColor;
-    }
-
-    .scm-author {
-      display: inline-block;
-      vertical-align: middle;
-      max-width: 40px;
-      padding: 2px 0;
-      line-height: 1;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-
-  }
-
-  .code .row:hover {
-    td.stat { background-color: @barBorderColor; }
-    td.line { background-color: @barBackgroundColor; }
-  }
-
-  .code .row-highlighted,
-  .code .row-highlighted:hover {
-    td.stat { background-color: #fdf190; }
-    td.line, .code-issues, .code-issue { background-color: #fff8c2; }
-  }
-
-  .code td.line {
-    width: 100%;
-    padding: 1px 5px;
-  }
-
-}
index a709e9ab9a870c0978379de71cc17d8acef4baad..dd439dbccca3e8534a0c8e7cfd8a6ea1d76f2f1f 100644 (file)
@@ -9,17 +9,19 @@
 
 
 .issue-list {
-  margin: 5px -5px;
-  border-top: 1px solid @barBorderColor;
-  border-bottom: 1px solid @barBorderColor;
+  margin: 10px 0;
+  background-color: #ffeaea;
 }
 
 
 .issue {
   position: relative;
+  max-width: 900px;
   padding-top: @topPadding;
   padding-bottom: @bottomPadding;
-  background-color: @barBackgroundColor;
+  border: 1px solid transparent;
+  border-left-width: 3px;
+  background-color: #ffeaea;
 }
 
 .issue.selected {
diff --git a/server/sonar-web/src/main/less/components/source.less b/server/sonar-web/src/main/less/components/source.less
new file mode 100644 (file)
index 0000000..7ef086a
--- /dev/null
@@ -0,0 +1,146 @@
+@import (reference) "../mixins";
+@import (reference) "../variables";
+
+@lineHeight: 18px;
+@duplicationColor: #f3ca8e;
+
+.source {
+  width: 100%;
+  border: 1px solid @barBorderColor;
+  overflow-x: auto;
+  overflow-y: hidden;
+}
+
+.source-table {
+  width: 100%;
+  border: none;
+  border-collapse: collapse;
+}
+
+.source-line:hover {
+  .source-line-number,
+  .source-line-coverage,
+  .source-line-duplications,
+  .source-line-duplications-extra,
+  .source-line-scm {
+    border-color: darken(@barBackgroundColor, 4%);
+    background-color: darken(@barBackgroundColor, 4%);
+  }
+
+  .source-line-code {
+    background-color: darken(#fff, 4%);
+  }
+}
+
+.source-line-highlighted,
+.source-line-highlighted:hover {
+  .source-line-number,
+  .source-line-coverage,
+  .source-line-duplications,
+  .source-line-duplications-extra,
+  .source-line-scm {
+    border-color: #fdf190 !important;
+    background-color: #fdf190;
+  }
+
+  .source-line-code {
+    background-color: #fff8c2;
+  }
+}
+
+.source-line-expand {
+  .source-line-code {
+    background: url(../images/gray-stripes.png) repeat;
+  }
+}
+
+.source pre {
+  height: @lineHeight;
+  padding: 0;
+}
+
+.source pre,
+.source-meta {
+  line-height: @lineHeight;
+  font-family: @monoFontFamily;
+  font-size: 12px;
+}
+
+.source-line-code {
+  padding: 0 10px;
+}
+
+.source-meta {
+  vertical-align: top;
+  width: 1px;
+  .user-select(none);
+}
+
+.source-meta + .source-meta {
+  border-left: 1px solid @barBackgroundColor;
+}
+
+.source-line-number {
+  min-width: 18px;
+  padding: 0 10px;
+  background-color: @barBackgroundColor;
+  color: @secondFontColor;
+  text-align: right;
+
+  &[data-line-number] {
+    cursor: pointer;
+  }
+
+  &:before {
+    content: attr(data-line-number);
+  }
+}
+
+.source-line-coverage {
+  background-color: @barBackgroundColor;
+}
+
+.source-line-duplications,
+.source-line-duplications-extra {
+  background-color: @barBackgroundColor;
+}
+
+.source-line-scm {
+  padding: 0 5px;
+  background-color: @barBackgroundColor;
+}
+
+.source-line-scm-inner {
+  max-width: 40px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  &:before {
+    content: attr(data-author);
+  }
+}
+
+.source-line-bar {
+  width: 5px;
+  height: @lineHeight;
+}
+
+.source-line-covered {
+  background-color: @green !important;
+  cursor: pointer;
+}
+
+.source-line-uncovered {
+  background-color: @red !important;
+}
+
+.source-line-partially-covered {
+  background-color: @orange !important;
+  cursor: pointer;
+}
+
+.source-line-duplicated {
+  background-color: @duplicationColor !important;
+  cursor: pointer;
+}
index cd20bb1d1da9cafda5cdd612b0b159eaaef8b965..845f16328586ddc8cddbafe637676a32529196f1 100644 (file)
@@ -35,6 +35,7 @@
 
 .issues-side {
   position: fixed;
+  z-index: 100;
   width: @sideWidth;
   top: 30px; left: 0; bottom: 0;
   .box-sizing(border-box);
 
 .issues-workspace-list {
   padding: 0 5px;
+
+  .issue {
+    max-width: none;
+  }
 }
 
 .issues-workspace-list-more {
index 66349f4b6559ad3339d3c91ea20afb988f15bde6..efe411f7d72a824ac91db48bb0e4708d1b202333 100644 (file)
     background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
 }
 
+.user-select(@mode) {
+  -webkit-user-select: @mode;
+     -moz-user-select: @mode;
+      -ms-user-select: @mode;
+          user-select: @mode;
+}
+
 .trans(@property: all, @options: @defaultTransitionOptions) {
   transition: @property @options;
 }