Skip to content
This repository has been archived by the owner on May 7, 2021. It is now read-only.

Commit

Permalink
Merge pull request #261 from greensnark/godef-with-context
Browse files Browse the repository at this point in the history
Better godef integration: local defs, imports
  • Loading branch information
joefitzgerald committed Aug 24, 2015
2 parents 2718caa + 12c2b54 commit ab2ae56
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 94 deletions.
2 changes: 2 additions & 0 deletions keymaps/go-plus.cson
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
'atom-text-editor[data-grammar="source go"]:not(.mini)':
'ctrl-alt-c': 'golang:gocover'
'ctrl-alt-shift-c': 'golang:cleargocover'
# Shouldn't these be ctrl-alt for portability?
'alt-cmd-g': 'golang:godef'
'alt-cmd-shift-g': 'golang:godef-return'
2 changes: 1 addition & 1 deletion lib/executor.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,5 @@ class Executor
err.handle()
callback(127, output, error, messages)

if input
if input?
bufferedprocess.process.stdin.end(input)
140 changes: 102 additions & 38 deletions lib/godef.coffee
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
{Point} = require('atom')
{Emitter, Subscriber} = require('emissary')
path = require('path')
fs = require('fs')

EditorLocationStack = require('./util/editor-location-stack')

module.exports =
class Godef
Subscriber.includeInto(this)
Emitter.includeInto(this)

constructor: (@dispatch) ->
@commandName = "golang:godef"
@godefCommand = "golang:godef"
@returnCommand = "golang:godef-return"
@name = 'def'
@didCompleteNotification = "#{@name}-complete"
atom.commands.add 'atom-workspace', 'golang:godef': => @gotoDefinitionForWordAtCursor()
@godefLocationStack = new EditorLocationStack()
atom.commands.add 'atom-workspace', "golang:godef": => @gotoDefinitionForWordAtCursor()
atom.commands.add 'atom-workspace', "golang:godef-return": => @godefReturn()
@cursorOnChangeSubscription = null

destroy: ->
Expand All @@ -22,64 +28,49 @@ class Godef
@emit('reset', @editor)
@cursorOnChangeSubscription?.dispose()

clearReturnHistory: ->
@godefLocationStack.reset()

# new pattern as per http://blog.atom.io/2014/09/16/new-event-subscription-api.html
# (but so far unable to get event-kit subscriptions to work, so keeping emissary)
onDidComplete: (callback) =>
onDidComplete: (callback) ->
@on(@didCompleteNotification, callback)

godefReturn: ->
@godefLocationStack.restorePreviousLocation().then =>
@emitDidComplete()

gotoDefinitionForWordAtCursor: ->
@editor = atom?.workspace?.getActiveTextEditor()
done = (err, messages) =>
@dispatch.resetAndDisplayMessages(@editor, messages)
@dispatch?.resetAndDisplayMessages(@editor, messages)

unless @dispatch?.isValidEditor(@editor)
@emit(@didCompleteNotification, @editor, false)
@emitDidComplete()
return
if @editor.hasMultipleCursors()
@bailWithWarning('Godef only works with a single cursor', done)
return
{word, range} = @wordAtCursor()
unless word.length > 0
@bailWithWarning('No word under cursor to define', done)
return

editorCursorOffset = (e) ->
e.getBuffer().characterIndexForPosition(e.getCursorBufferPosition())

offset = editorCursorOffset(@editor)
@reset(@editor)
@gotoDefinitionForWord(word, done)
@gotoDefinitionWithParameters(['-o', offset, '-i'], @editor.getText(), done)

gotoDefinitionForWord: (word, callback = -> undefined) ->
@gotoDefinitionWithParameters([word], undefined, callback)

gotoDefinitionWithParameters: (cmdArgs, cmdInput = undefined, callback = -> undefined) ->
message = null
done = (exitcode, stdout, stderr, messages) =>
unless exitcode is 0
# little point parsing the error further, given godef bugs eg
# "godef: cannot parse expression: <arg>:1:1: expected operand, found 'return'"
@bailWithWarning(stderr, callback)
return
outputs = stdout.split(':')
if process.platform is 'win32'
targetFilePath = "#{outputs[0]}:#{outputs[1]}"
rowNumber = outputs[2]
colNumber = outputs[3]
else
targetFilePath = outputs[0]
rowNumber = outputs[1]
colNumber = outputs[2]

unless fs.existsSync(targetFilePath)
@bailWithWarning("godef suggested a file path (\"#{targetFilePath}\") that does not exist)", callback)
return
# atom's cursors 0-based; godef uses diff-like 1-based
row = parseInt(rowNumber, 10) - 1
col = parseInt(colNumber, 10) - 1
if @editor.getPath() is targetFilePath
@editor.setCursorBufferPosition [row, col]
@cursorOnChangeSubscription = @highlightWordAtCursor()
@emit(@didCompleteNotification, @editor, false)
callback(null, [message])
else
atom.workspace.open(targetFilePath, {initialLine: row, initialColumn: col}).then (e) =>
@cursorOnChangeSubscription = @highlightWordAtCursor(atom.workspace.getActiveTextEditor())
@emit(@didCompleteNotification, @editor, false)
callback(null, [message])
@visitLocation(@parseGodefLocation(stdout), callback)

go = @dispatch.goexecutable.current()
cmd = go.godef()
Expand All @@ -94,9 +85,81 @@ class Godef
env['GOPATH'] = gopath
filePath = @editor.getPath()
cwd = path.dirname(filePath)
args = ['-f', filePath, word]
args = ['-f', filePath, cmdArgs...]
@dispatch.executor.exec(cmd, cwd, env, done, args, cmdInput)

parseGodefLocation: (godefStdout) ->
outputs = godefStdout.trim().split(':')
# Windows paths may have DriveLetter: prefix, or be UNC paths, so
# handle both cases:
[targetFilePathSegments..., rowNumber, colNumber] = outputs
targetFilePath = targetFilePathSegments.join(':')

# godef on an import returns the imported package directory with no
# row and column information: handle this appropriately
if targetFilePath.length is 0 and rowNumber
targetFilePath = [rowNumber, colNumber].filter((x) -> x).join(':')
rowNumber = colNumber = undefined

# atom's cursors are 0-based; godef uses diff-like 1-based
p = (rawPosition) -> parseInt(rawPosition, 10) - 1

filepath: targetFilePath
pos: if rowNumber? and colNumber? then new Point(p(rowNumber), p(colNumber))
raw: godefStdout

visitLocation: (loc, callback) ->
unless loc.filepath
@bailWithWarning("godef returned malformed output: #{JSON.stringify(loc.raw)}", callback)
return

@dispatch.executor.exec(cmd, cwd, env, done, args)
fs.stat loc.filepath, (err, stats) =>
if err
@bailWithWarning("godef returned invalid file path: \"#{loc.filepath}\"", callback)
return

@godefLocationStack.pushCurrentLocation()
if stats.isDirectory()
@visitDirectory(loc, callback)
else
@visitFile(loc, callback)

visitFile: (loc, callback) ->
atom.workspace.open(loc.filepath).then (@editor) =>
if loc.pos
@editor.scrollToBufferPosition(loc.pos)
@editor.setCursorBufferPosition(loc.pos)
@cursorOnChangeSubscription = @highlightWordAtCursor(atom.workspace.getActiveTextEditor())
@emitDidComplete()
callback(null, [])

visitDirectory: (loc, callback) ->
success = (goFile) =>
@visitFile({filepath: goFile, raw: loc.raw}, callback)
failure = (err) =>
@bailWithWarning("godef return invalid directory #{loc.filepath}: #{err}", callback)
@findFirstGoFile(loc.filepath).then(success).catch(failure)

findFirstGoFile: (dir) ->
new Promise (resolve, reject) =>
fs.readdir dir, (err, files) =>
if err
reject(err)
goFilePath = @firstGoFilePath(dir, files.sort())
if goFilePath
resolve(goFilePath)
else
reject("#{dir} has no non-test .go file")

firstGoFilePath: (dir, files) ->
isGoSourceFile = (file) ->
file.endsWith('.go') and file.indexOf('_test') is -1
for file in files
return path.join(dir, file) if isGoSourceFile(file)
return

emitDidComplete: ->
@emit(@didCompleteNotification, @editor, false)

bailWithWarning: (warning, callback) ->
@bailWithMessage('warning', warning, callback)
Expand All @@ -112,6 +175,7 @@ class Godef
type: type
source: @name
callback(null, [message])
@emitDidComplete()

wordAtCursor: (editor = @editor) ->
options =
Expand Down
44 changes: 44 additions & 0 deletions lib/util/editor-location-stack.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
doneAlready = ->
new Promise (resolve, reject) ->
resolve()

module.exports =
class EditorLocationStack
constructor: (@maxSize = 500) ->
@maxSize = 1 if @maxSize < 1
@stack = []

isEmpty: ->
not @stack.length

reset: ->
@stack = []

pushCurrentLocation: ->
editor = atom.workspace.getActiveTextEditor()
return unless editor
loc =
position: editor.getCursorBufferPosition()
file: editor.getURI()
return unless loc.file and loc.position?.row and loc.position?.column
@push(loc)
return

##
# Returns a promise that is complete when navigation is done.
restorePreviousLocation: ->
return doneAlready() if @isEmpty()
lastLocation = @stack.pop()
atom.workspace.open(lastLocation.file).then (editor) =>
@moveEditorCursorTo(editor, lastLocation.position)

moveEditorCursorTo: (editor, pos) ->
return unless editor
editor.scrollToBufferPosition(pos)
editor.setCursorBufferPosition(pos)
return

push: (loc) ->
@stack.push(loc)
@stack.splice(0, @stack.length - @maxSize) if @stack.length > @maxSize
return
Loading

0 comments on commit ab2ae56

Please sign in to comment.