Skip to content

Commit

Permalink
use linter-indie to update comments in file async
Browse files Browse the repository at this point in the history
  • Loading branch information
philschatz committed Nov 27, 2015
1 parent c355264 commit 058626f
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 173 deletions.
40 changes: 20 additions & 20 deletions lib/gh-client.coffee
@@ -1,8 +1,8 @@
{CompositeDisposable} = require 'atom'
{CompositeDisposable, Emitter} = require 'atom'
_ = require 'underscore-plus'
Octokat = require 'octokat'
{getRepoInfo} = require './helpers'

Polling = require './polling'

CONFIG_POLLING_INTERVAL = 'pull-requests.githubPollingInterval'
CONFIG_AUTHORIZATION_TOKEN = 'pull-requests.githubAuthorizationToken'
Expand All @@ -19,11 +19,13 @@ getRepoNameWithOwner = (repo) ->

module.exports = new class GitHubClient

cachedPromise: null
lastPolled: null
octo: null

initialize: ->
@emitter = new Emitter
@polling = new Polling
@polling.initialize()

@URL_TEST_NODE ?= document.createElement('a')

@activeItemSubscription = atom.workspace.onDidChangeActivePaneItem =>
Expand All @@ -34,8 +36,10 @@ module.exports = new class GitHubClient
@subscribeToActiveItem()
@subscribeToConfigChanges()

@updatePollingInterval()
@updateConfig()
@polling.onDidTick => @_tick()
@updatePollingInterval()
@polling.start()

destroy: ->
@URL_TEST_NODE = null
Expand Down Expand Up @@ -103,9 +107,11 @@ module.exports = new class GitHubClient
rootURL = null

@octo = new Octokat({token, rootURL})
@polling.forceIfStarted()

updatePollingInterval: ->
@pollingInterval = atom.config.get(CONFIG_POLLING_INTERVAL)
interval = atom.config.get(CONFIG_POLLING_INTERVAL)
@polling.set(interval * 1000)

updateRepoBranch: ->
repo = @getRepositoryForActiveItem()
Expand All @@ -115,11 +121,10 @@ module.exports = new class GitHubClient
@branchName = branchName
@repoOwner = repoOwner
@repoName = repoName
@resetCache()
@polling.forceIfStarted()

resetCache: ->
@lastPolled = null
@cachedPromise = null
onDidUpdate: (cb) ->
@emitter.on('did-update', cb)

_fetchComments: ->
repo = @octo.repos(@repoOwner, @repoName)
Expand All @@ -145,23 +150,18 @@ module.exports = new class GitHubClient

# Reset the hasShownConnectionError flag because we succeeded
@hasShownConnectionError = false

comments


getCommentsPromise: ->
_tick: ->
@updateRepoBranch() # Sometimes the branch name does not update

now = Date.now()
if @cachedPromise and @lastPolled + @pollingInterval * 1000 > now
return @cachedPromise
@lastPolled = now

unless @repoOwner and @repoName and @branchName
return Promise.resolve([])
@emit 'did-update', []

# Return a promise
return @cachedPromise = @_fetchComments()
@_fetchComments()
.then (comments) =>
@emitter.emit('did-update', comments)
.then undefined, (err) ->
unless @hasShownConnectionError
@hasShownConnectionError = true
Expand Down
21 changes: 19 additions & 2 deletions lib/init.coffee
@@ -1,3 +1,5 @@
{CompositeDisposable} = require 'atom'

fs = require 'fs-plus'
path = require 'path'

Expand Down Expand Up @@ -27,15 +29,30 @@ module.exports = new class PullRequests

activate: ->
require('atom-package-deps').install('pull-requests')
@subscriptions = new CompositeDisposable
@ghClient ?= require('./gh-client')
@ghClient.initialize()

@treeViewDecorator ?= require('./tree-view-decorator')
@treeViewDecorator.initialize()

@pullRequestLinter ?= require('./pr-linter')
@pullRequestLinter.initialize()

deactivate: ->
@ghClient?.destroy()
@treeViewDecorator?.destroy()
@pullRequestLinter.destroy()
@subscriptions.destroy()

consumeLinter: (registry) ->
atom.packages.activate('linter').then =>

registry = atom.packages.getLoadedPackage('linter').mainModule.provideIndie()

# HACK because of bug in `linter` package
registry.emit = registry.emitter.emit.bind(registry.emitter)

provideLinter: ->
return require('./pull-request-linter')
linter = registry.register {name: 'Pull Request'}
@pullRequestLinter.setLinter(linter)
@subscriptions.add(linter)
30 changes: 30 additions & 0 deletions lib/polling.coffee
@@ -0,0 +1,30 @@
{Emitter} = require 'atom'

module.exports = class Polling
initialize: ->
@emitter = new Emitter

destroy: ->
clearTimeout(@_timeout)

poll: ->
@emitter.emit('did-tick')
@_timeout = setTimeout(@poll.bind(@), @interval)

start: ->
@poll()

stop: ->
clearTimeout(@_timeout)
@_timeout = null

forceIfStarted: ->
@poll() if @_timeout

set: (@interval) ->
if @_timeout
@stop()
@start()

onDidTick: (cb) ->
@emitter.on 'did-tick', cb
167 changes: 167 additions & 0 deletions lib/pr-linter.coffee
@@ -0,0 +1,167 @@
fs = require 'fs'
path = require 'path'
{TextBuffer} = require 'atom'
_ = require 'underscore-plus'
ultramarked = require 'ultramarked'
linkify = require 'gfm-linkify'

ghClient = require './gh-client'
{getRepoInfo} = require './helpers'

simplifyHtml = (html) ->
DIV = document.createElement('div')
DIV.innerHTML = html
first = DIV.firstElementChild
if first and first is DIV.lastElementChild and first.tagName.toLowerCase() is 'p'
DIV.firstElementChild.innerHTML
else
DIV.innerHTML

getNameWithOwner = (repo) ->
url = repo.getOriginURL()
return null unless url?
return /([^\/:]+)\/([^\/]+)$/.exec(url.replace(/\.git$/, ''))[0]


# GitHub comments do not directly contain the line number of the comment.
# Instead, it needs to be calculated from the `.diffHunk`.
# This determines the line number by parsing the `diffHunk` and then adding
# or subtracting the line number depending on if the diff line has a `-`, ` `, or `+`.

# As an example:
# The following should end up with position=98
#
# ```
# @@ -90,6 +91,26 @@ DashboardChapter = React.createClass
# someCode += 1;
# someCode += 1;
#
# +
# +moreCode += 1;
# +
# +
# +thisIsTheLineWithTheComment = true;
# ```
parseHunkToGetPosition = (diffHunk) ->
LINE_RE = /^@@\ -\d+,\d+\ \+(\d+)/ # Use the start line number in the new file

diffLines = diffHunk.split('\n')

throw new Error('weird hunk format') unless diffLines[0].startsWith('@@ -')

# oldPosition = parseInt(diffLines[0].substring('@@ -'.length, diffLines[0].indexOf(',')))
position = parseInt(LINE_RE.exec(diffLines[0])?[1])
position -= 1 # because diff line numbers are 1-based

diffLines.shift() # skip the 1st line
_.each diffLines, (line) ->
if line[0] isnt '-'
position += 1
position


module.exports = new class # This only needs to be a class to bind lint()

initialize: ->
ghClient.onDidUpdate (comments) => @poll(comments)

destroy: ->

setLinter: (@linter) ->

poll: (allComments) ->
if allComments.length is 0
@linter.deleteMessages()
return
repo = atom.project.getRepositories()[0]

rootPath = path.join(repo.getPath(), '..')

# Combine the comments by file
filesMap = {}
allComments.forEach (comment) ->
filesMap[comment.path] ?= []
filesMap[comment.path].push(comment)

allMessages = []

for filePath, comments of filesMap
do (filePath, comments) =>

fileAbsolutePath = path.join(rootPath, filePath)

# Get all the diffs since the last commit (TODO: Do not assume people push their commits immediately)
# These are used to shift/remove comments in the gutter

fileText = fs.readFileSync(fileAbsolutePath, 'utf-8') # HACK: Assumes the file is utf-8

# Contains an {Array} of hunk {Object}s with the following keys:
# * `oldStart` The line {Number} of the old hunk.
# * `newStart` The line {Number} of the new hunk.
# * `oldLines` The {Number} of lines in the old hunk.
# * `newLines` The {Number} of lines in the new hunk
diffs = repo.getLineDiffs(filePath, fileText)
{ahead, behind} = repo.getCachedUpstreamAheadBehindCount(filePath)

if ahead or behind
outOfDateText = 'marker line number may be off\n'
else
outOfDateText = ''


# Sort all the comments and combine multiple comments
# that were made on the same line
lineMap = {}
_.forEach comments, (comment) ->
{diffHunk, body, user} = comment
position = parseHunkToGetPosition(diffHunk)
lineMap[position] ?= []
lineMap[position].push("#{user.login}: #{body}")

# Collapse multiple comments on the same line
# into 1 message with newlines
editorBuffer = new TextBuffer {text: fileText}
lintWarningsOrNull = _.map lineMap, (commentsOnLine, position) =>

position = parseInt(position)

# Adjust the line numbers for any diffs so they still line up
diffs.forEach ({oldStart, newStart, oldLines, newLines}) ->
return if position < oldStart
# If the comment is in the range of edited text then do something (maybe hide it?)
if oldStart <= position and position <= oldStart + oldLines
position = -1
else
position = position - oldLines + newLines

# HACK: figure out why position can be -1
if position is 0
position = 1

if position is -1
return null

# Put a squiggly across the entire line by finding the line length
if editorBuffer.getLineCount() <= position - 1
# TODO: Keep track of local diffs to adjust where the comments are
lineLength = 1
else
lineLength = editorBuffer.lineLengthForRow(position - 1)

text = outOfDateText + commentsOnLine.join('\n\n')
context = ghClient.repoOwner + '/' + ghClient.repoName
textStripped = text.replace(/<!--[\s\S]*?-->/g, '')
# textEmojis = this.replaceEmojis(textStripped)
textEmojis = textStripped
html = ultramarked(linkify(textEmojis, context))

{
type: 'Info'
html: simplifyHtml(html)
range: [[position - 1, 0], [position - 1, lineLength]]
filePath: fileAbsolutePath
}

# Filter out all the comments that no longer apply since the source was changed
allMessages = allMessages.concat(lintWarningsOrNull.filter (lintWarning) -> !!lintWarning)
@linter.setMessages(allMessages)

0 comments on commit 058626f

Please sign in to comment.