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

autocomplete unimported packages #685

Merged
merged 3 commits into from
Nov 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ install:
- go get -u github.com/rogpeppe/godef
- go get -u github.com/fatih/gomodifytags
- go get -u golang.org/x/tools/cmd/guru
- go get -u github.com/tpng/gopkgs

script:
- 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh'
Expand Down
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ install:
- go get -u github.com/rogpeppe/godef
- go get -u github.com/fatih/gomodifytags
- go get -u golang.org/x/tools/cmd/guru
- go get -u github.com/tpng/gopkgs

build_script:
- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1'))
Expand Down
97 changes: 97 additions & 0 deletions lib/autocomplete/gocodeprovider-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use babel'

import path from 'path'

const vendorString = '/vendor/'

export function wantedPackage (buffer, pos) {
// get the pkg the user tries to autocomplete from the current line
const lineTillPos = buffer.getTextInRange([[pos.row, 0], pos])
const matches = lineTillPos.match(/(\w+)\.$/)
if (!matches) {
return null
}
return matches[1]
}

export function addImport (buffer, pkg, offset) {
// find the "package ..." statement
let row = -1
buffer.scan(/^package /, (result) => {
row = result.row
})
if (row === -1) {
return null
}

const text = buffer.getText()

// import the "pkg" right after the package statement
const importStmt = `import "${pkg}"\n`
const index = buffer.characterIndexForPosition([row + 1, 0])
const newText = text.substr(0, index) + importStmt + text.substr(index)
const newOffset = offset + importStmt.length
return { text: newText, offset: newOffset }
}

export function getPackage (file, gopath, pkgs, useVendor) {
if (useVendor) {
const dir = path.dirname(file)
const workspace = getCurrentGoWorkspaceFromGOPATH(gopath, dir)
const vendorPkgs = pkgs.filter((pkg) => pkg.lastIndexOf(vendorString) > 0)
for (const vpkg of vendorPkgs) {
const relativePath = getRelativePackagePath(dir, workspace, vpkg)
if (relativePath) {
return relativePath
}
}
}

// take the first non-vendor package
return pkgs.find((pkg) => pkg.lastIndexOf(vendorString) === -1)
}

export function getRelativePackagePath (currentDir, currentWorkspace, pkg) {
let magicVendorString = vendorString
let vendorIndex = pkg.lastIndexOf(magicVendorString)
if (vendorIndex === -1) {
magicVendorString = 'vendor/'
if (pkg.startsWith(magicVendorString)) {
vendorIndex = 0
}
}
// Check if current file and the vendor pkg belong to the same root project
// If yes, then vendor pkg can be replaced with its relative path to the "vendor" folder
// If not, then the vendor pkg should not be allowed to be imported.
if (vendorIndex > -1) {
let rootProjectForVendorPkg = path.join(currentWorkspace, pkg.substr(0, vendorIndex))
let relativePathForVendorPkg = pkg.substring(vendorIndex + magicVendorString.length)

if (relativePathForVendorPkg && currentDir.startsWith(rootProjectForVendorPkg)) {
return relativePathForVendorPkg
}
return ''
}

return pkg
}

export function getCurrentGoWorkspaceFromGOPATH (gopath, currentDir) {
let workspaces = gopath.split(path.delimiter)
let currentWorkspace = ''

// Find current workspace by checking if current file is
// under any of the workspaces in $GOPATH
for (let i = 0; i < workspaces.length; i++) {
let possibleCurrentWorkspace = path.join(workspaces[i], 'src')
if (currentDir.startsWith(possibleCurrentWorkspace)) {
// In case of nested workspaces, (example: both /Users/me and /Users/me/src/a/b/c are in $GOPATH)
// both parent & child workspace in the nested workspaces pair can make it inside the above if block
// Therefore, the below check will take longer (more specific to current file) of the two
if (possibleCurrentWorkspace.length > currentWorkspace.length) {
currentWorkspace = possibleCurrentWorkspace
}
}
}
return currentWorkspace
}
125 changes: 79 additions & 46 deletions lib/autocomplete/gocodeprovider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {filter} from 'fuzzaldrin-plus'
import _ from 'lodash'
import os from 'os'
import {isValidEditor} from '../utils'
import {allPackages, isVendorSupported} from '../go'
import {wantedPackage, getPackage, addImport} from './gocodeprovider-helper'

class GocodeProvider {
constructor (goconfig) {
Expand Down Expand Up @@ -54,6 +56,8 @@ class GocodeProvider {
this.unimportedPackages = value
this.toggleGocodeConfig()
}))

this.allPkgs = allPackages(this.goconfig)
}

dispose () {
Expand Down Expand Up @@ -154,7 +158,7 @@ class GocodeProvider {
return p
}
}
if (prefix.length > 1 && this.currentSuggestions.length) {
if (prefix.length > 0 && prefix !== '.' && this.currentSuggestions.length) {
// fuzzy filter on this.currentSuggestions
const p = new Promise((resolve) => {
resolve(filter(this.currentSuggestions, prefix, {key: 'fuzzyMatch'})
Expand All @@ -166,22 +170,23 @@ class GocodeProvider {

// get a fresh set of suggestions from gocode
const p = new Promise((resolve) => {
if (!options || !this.ready() || !isValidEditor(options.editor)) {
const { editor, bufferPosition } = options
if (!options || !this.ready() || !isValidEditor(editor)) {
return resolve()
}

const buffer = options.editor.getBuffer()
if (!buffer || !options.bufferPosition) {
const buffer = editor.getBuffer()
if (!buffer || !bufferPosition) {
return resolve()
}

const index = buffer.characterIndexForPosition(options.bufferPosition)
const priorBufferPosition = options.bufferPosition.copy()
const index = buffer.characterIndexForPosition(bufferPosition)
const priorBufferPosition = bufferPosition.copy()
if (priorBufferPosition.column > 0) {
priorBufferPosition.column = priorBufferPosition.column - 1
}
const scopeDescriptor = options.editor.scopeDescriptorForBufferPosition(priorBufferPosition)
const text = options.editor.getText()
const scopeDescriptor = editor.scopeDescriptorForBufferPosition(priorBufferPosition)
const text = editor.getText()
if (!options.activatedManually && index > 0 && this.characterIsSuppressed(text[index - 1], scopeDescriptor)) {
return resolve()
}
Expand All @@ -192,33 +197,79 @@ class GocodeProvider {
resolve()
return false
}
const args = ['-f=json', 'autocomplete', buffer.getPath(), offset]
const execOptions = this.goconfig.executor.getOptions('file', options.editor)
const file = buffer.getPath()
const args = ['-f=json', 'autocomplete', file, offset]
const execOptions = this.goconfig.executor.getOptions('file', editor)
execOptions.input = text
this.goconfig.executor.exec(cmd, args, execOptions).then((r) => {
if (r.stderr && r.stderr.trim() !== '') {
console.log('autocomplete-go: (stderr) ' + r.stderr)
}
let messages
if (r.stdout && r.stdout.trim() !== '') {
messages = this.mapMessages(r.stdout, options.editor, options.bufferPosition)
}
if (!messages || messages.length < 1) {
return resolve()
}
this.currentSuggestions = messages
resolve(messages)
}).catch((e) => {
console.log(e)
resolve()
})

this.executeGocode(cmd, args, execOptions)
.then((rawSuggestions) => {
if (rawSuggestions.length === 0 && prefix === '.') {
return isVendorSupported(this.goconfig).then((useVendor) => {
const pkg = wantedPackage(buffer, bufferPosition)
if (!pkg) {
return []
}
const pkgs = this.allPkgs.get(pkg)
if (!pkgs || !pkgs.length) {
return []
}
const {GOPATH} = this.goconfig.environment()
const pkgPath = getPackage(file, GOPATH, pkgs, useVendor)
if (!pkgPath) {
return []
}
const added = addImport(buffer, pkgPath, offset)
if (!added) {
return []
}
const args = ['-f=json', 'autocomplete', file, added.offset]
const execOptions = this.goconfig.executor.getOptions('file', editor)
execOptions.input = added.text
return this.executeGocode(cmd, args, execOptions)
})
}
return rawSuggestions
})
.then((rawSuggestions) => {
const suggestions = this.mapMessages(rawSuggestions, editor, bufferPosition)
this.currentSuggestions = suggestions
resolve(suggestions)
})
})
})

this.notifySubscribers(p)
return p
}

executeGocode (cmd, args, options) {
return this.goconfig.executor.exec(cmd, args, options).then((r) => {
if (r.stderr && r.stderr.trim() !== '') {
console.log('go-plus: Failed to run gocode:', r.stderr)
}
const data = (r.stdout || '').trim()
if (!data) {
return []
}
try {
return JSON.parse(r.stdout)
} catch (e) {
if (e && e.handle) {
e.handle()
}
atom.notifications.addError('gocode error', {
detail: r.stdout,
dismissable: true
})
console.log('go-plus: Failed to parse the output of gocode:', e)
return []
}
}).catch((e) => {
console.log(e)
})
}

notifySubscribers (p) {
if (this.subscribers && this.subscribers.length > 0) {
for (const subscriber of this.subscribers) {
Expand All @@ -233,25 +284,7 @@ class GocodeProvider {
}
}

mapMessages (data, editor, position) {
if (!data) {
return []
}
let res
try {
res = JSON.parse(data)
} catch (e) {
if (e && e.handle) {
e.handle()
}
atom.notifications.addError('gocode error', {
detail: data,
dismissable: true
})
console.log(e)
return []
}

mapMessages (res, editor, position) {
const numPrefix = res[0]
const candidates = res[1]
if (!candidates) {
Expand Down
20 changes: 11 additions & 9 deletions lib/config/executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,17 @@ class Executor {
}

const bufferedprocess = new BufferedProcess({command: command, args: args, options: options, stdout: stdoutFn, stderr: stderrFn, exit: exitFn})
setTimeout(() => {
bufferedprocess.kill()
resolve({
error: null,
exitcode: 124,
stdout: stdout,
stderr: stderr
})
}, options.timeout)
if (options.timeout > 0) {
setTimeout(() => {
bufferedprocess.kill()
resolve({
error: null,
exitcode: 124,
stdout: stdout,
stderr: stderr
})
}, options.timeout)
}
bufferedprocess.onWillThrowError((err) => {
let e = err
if (err) {
Expand Down
59 changes: 59 additions & 0 deletions lib/go.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use babel'

let vendorSupported
export function isVendorSupported (goconfig) {
if (vendorSupported != null) {
return Promise.resolve(vendorSupported)
}
return goconfig.locator.runtime().then((runtime) => {
if (!runtime) {
return goconfig.environment()['GO15VENDOREXPERIMENT'] !== '0'
}
const [major, minor] = runtime.semver.split('.')

switch (major) {
case 0:
vendorSupported = false
break
case 1:
vendorSupported = (minor > 6 || ((minor === 5 || minor === 6) && goconfig.environment()['GO15VENDOREXPERIMENT'] !== '0'))
break
default:
vendorSupported = true
break
}
return vendorSupported
})
}

let pkgs
export function allPackages (goconfig) {
if (pkgs) {
return pkgs
}
pkgs = new Map()

goconfig.locator.findTool('gopkgs').then((gopkgs) => {
if (!gopkgs) {
return
}
return goconfig.executor.exec(gopkgs, [], 'project').then((r) => {
if (r.exitcode !== 0) {
console.log('go-plus: "gopkgs" returned the following errors:', r.stderr.trim() || `exitcode ${r.exitcode}`)
}

r.stdout.trim()
.split('\n')
.forEach((path) => {
const name = path.trim().split('/').pop()
const p = pkgs.get(name) || []
pkgs.set(name, p.concat(path.trim()))
})

pkgs.forEach((p) => {
p.sort()
})
})
})
return pkgs
}
3 changes: 2 additions & 1 deletion lib/package-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const goTools = new Map([
['gogetdoc', 'github.com/zmb3/gogetdoc'],
['godef', 'github.com/rogpeppe/godef'],
['guru', 'golang.org/x/tools/cmd/guru'],
['gomodifytags', 'github.com/fatih/gomodifytags']
['gomodifytags', 'github.com/fatih/gomodifytags'],
['gopkgs', 'github.com/tpng/gopkgs']
])

class PackageManager {
Expand Down
Loading