| /* eslint-disable @typescript-eslint/no-explicit-any */ |
| /*--------------------------------------------------------- |
| * Copyright (C) Microsoft Corporation. All rights reserved. |
| * Licensed under the MIT License. See LICENSE in the project root for license information. |
| *--------------------------------------------------------*/ |
| |
| 'use strict'; |
| |
| import cp = require('child_process'); |
| import path = require('path'); |
| import vscode = require('vscode'); |
| import { getGoConfig } from './config'; |
| import { toolExecutionEnvironment } from './goEnv'; |
| import { promptForMissingTool, promptForUpdatingTool } from './goInstallTools'; |
| import { getModFolderPath, promptToUpdateToolForModules } from './goModules'; |
| import { |
| byteOffsetAt, |
| getBinPath, |
| getFileArchive, |
| getModuleCache, |
| getWorkspaceFolderPath, |
| goKeywords, |
| isPositionInString, |
| runGodoc |
| } from './util'; |
| import { getCurrentGoRoot } from './utils/pathUtils'; |
| import { killProcessTree } from './utils/processUtils'; |
| |
| const missingToolMsg = 'Missing tool: '; |
| |
| export interface GoDefinitionInformation { |
| file: string; |
| line: number; |
| column: number; |
| doc: string; |
| declarationlines: string[]; |
| name: string; |
| toolUsed: string; |
| } |
| |
| interface GoDefinitionInput { |
| document: vscode.TextDocument; |
| position: vscode.Position; |
| word: string; |
| includeDocs: boolean; |
| isMod: boolean; |
| cwd: string; |
| } |
| |
| interface GoGetDocOuput { |
| name: string; |
| import: string; |
| decl: string; |
| doc: string; |
| pos: string; |
| } |
| |
| interface GuruDefinitionOuput { |
| objpos: string; |
| desc: string; |
| } |
| |
| export function definitionLocation( |
| document: vscode.TextDocument, |
| position: vscode.Position, |
| goConfig: vscode.WorkspaceConfiguration, |
| includeDocs: boolean, |
| token: vscode.CancellationToken |
| ): Promise<GoDefinitionInformation> { |
| const adjustedPos = adjustWordPosition(document, position); |
| if (!adjustedPos[0]) { |
| return Promise.resolve(null); |
| } |
| const word = adjustedPos[1]; |
| position = adjustedPos[2]; |
| |
| if (!goConfig) { |
| goConfig = getGoConfig(document.uri); |
| } |
| const toolForDocs = goConfig['docsTool'] || 'godoc'; |
| return getModFolderPath(document.uri).then((modFolderPath) => { |
| const input: GoDefinitionInput = { |
| document, |
| position, |
| word, |
| includeDocs, |
| isMod: !!modFolderPath, |
| cwd: |
| modFolderPath && modFolderPath !== getModuleCache() |
| ? modFolderPath |
| : getWorkspaceFolderPath(document.uri) || path.dirname(document.fileName) |
| }; |
| if (toolForDocs === 'godoc') { |
| return definitionLocation_godef(input, token); |
| } else if (toolForDocs === 'guru') { |
| return definitionLocation_guru(input, token); |
| } |
| return definitionLocation_gogetdoc(input, token, true); |
| }); |
| } |
| |
| export function adjustWordPosition( |
| document: vscode.TextDocument, |
| position: vscode.Position |
| ): [boolean, string, vscode.Position] { |
| const wordRange = document.getWordRangeAtPosition(position); |
| const lineText = document.lineAt(position.line).text; |
| const word = wordRange ? document.getText(wordRange) : ''; |
| if ( |
| !wordRange || |
| lineText.startsWith('//') || |
| isPositionInString(document, position) || |
| word.match(/^\d+.?\d+$/) || |
| goKeywords.indexOf(word) > 0 |
| ) { |
| return [false, null, null]; |
| } |
| if (position.isEqual(wordRange.end) && position.isAfter(wordRange.start)) { |
| position = position.translate(0, -1); |
| } |
| |
| return [true, word, position]; |
| } |
| |
| const godefImportDefinitionRegex = /^import \(.* ".*"\)$/; |
| function definitionLocation_godef( |
| input: GoDefinitionInput, |
| token: vscode.CancellationToken, |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars |
| useReceivers = true |
| ): Promise<GoDefinitionInformation> { |
| const godefTool = 'godef'; |
| const godefPath = getBinPath(godefTool); |
| if (!path.isAbsolute(godefPath)) { |
| return Promise.reject(missingToolMsg + godefTool); |
| } |
| const offset = byteOffsetAt(input.document, input.position); |
| const env = toolExecutionEnvironment(); |
| env['GOROOT'] = getCurrentGoRoot(); |
| let p: cp.ChildProcess; |
| if (token) { |
| token.onCancellationRequested(() => killProcessTree(p)); |
| } |
| |
| return new Promise<GoDefinitionInformation>((resolve, reject) => { |
| // Spawn `godef` process |
| const args = ['-t', '-i', '-f', input.document.fileName, '-o', offset.toString()]; |
| // if (useReceivers) { |
| // args.push('-r'); |
| // } |
| p = cp.execFile(godefPath, args, { env, cwd: input.cwd }, (err, stdout, stderr) => { |
| try { |
| if (err && (<any>err).code === 'ENOENT') { |
| return reject(missingToolMsg + godefTool); |
| } |
| if (err) { |
| if ( |
| input.isMod && |
| !input.includeDocs && |
| stderr && |
| stderr.startsWith('godef: no declaration found for') |
| ) { |
| promptToUpdateToolForModules( |
| 'godef', |
| 'To get the Go to Definition feature when using Go modules, please update your version of the "godef" tool.' |
| ); |
| return reject(stderr); |
| } |
| if (stderr.indexOf('flag provided but not defined: -r') !== -1) { |
| promptForUpdatingTool('godef'); |
| p = null; |
| return definitionLocation_godef(input, token, false).then(resolve, reject); |
| } |
| return reject(err.message || stderr); |
| } |
| const result = stdout.toString(); |
| const lines = result.split('\n'); |
| let match = /(.*):(\d+):(\d+)/.exec(lines[0]); |
| if (!match) { |
| // TODO: Gotodef on pkg name: |
| // /usr/local/go/src/html/template\n |
| return resolve(null); |
| } |
| const [, file, line, col] = match; |
| const pkgPath = path.dirname(file); |
| const definitionInformation: GoDefinitionInformation = { |
| file, |
| line: +line - 1, |
| column: +col - 1, |
| declarationlines: lines.slice(1), |
| toolUsed: 'godef', |
| doc: null, |
| name: null |
| }; |
| if (!input.includeDocs || godefImportDefinitionRegex.test(definitionInformation.declarationlines[0])) { |
| return resolve(definitionInformation); |
| } |
| match = /^\w+ \(\*?(\w+)\)/.exec(lines[1]); |
| runGodoc(input.cwd, pkgPath, match ? match[1] : '', input.word, token) |
| .then((doc) => { |
| if (doc) { |
| definitionInformation.doc = doc; |
| } |
| resolve(definitionInformation); |
| }) |
| .catch((runGoDocErr) => { |
| console.log(runGoDocErr); |
| resolve(definitionInformation); |
| }); |
| } catch (e) { |
| reject(e); |
| } |
| }); |
| if (p.pid) { |
| p.stdin.end(input.document.getText()); |
| } |
| }); |
| } |
| |
| function definitionLocation_gogetdoc( |
| input: GoDefinitionInput, |
| token: vscode.CancellationToken, |
| useTags: boolean |
| ): Promise<GoDefinitionInformation> { |
| const gogetdoc = getBinPath('gogetdoc'); |
| if (!path.isAbsolute(gogetdoc)) { |
| return Promise.reject(missingToolMsg + 'gogetdoc'); |
| } |
| const offset = byteOffsetAt(input.document, input.position); |
| const env = toolExecutionEnvironment(); |
| let p: cp.ChildProcess; |
| if (token) { |
| token.onCancellationRequested(() => killProcessTree(p)); |
| } |
| |
| return new Promise<GoDefinitionInformation>((resolve, reject) => { |
| const gogetdocFlagsWithoutTags = [ |
| '-u', |
| '-json', |
| '-modified', |
| '-pos', |
| input.document.fileName + ':#' + offset.toString() |
| ]; |
| const buildTags = getGoConfig(input.document.uri)['buildTags']; |
| const gogetdocFlags = |
| buildTags && useTags ? [...gogetdocFlagsWithoutTags, '-tags', buildTags] : gogetdocFlagsWithoutTags; |
| p = cp.execFile(gogetdoc, gogetdocFlags, { env, cwd: input.cwd }, (err, stdout, stderr) => { |
| try { |
| if (err && (<any>err).code === 'ENOENT') { |
| return reject(missingToolMsg + 'gogetdoc'); |
| } |
| if (stderr && stderr.startsWith('flag provided but not defined: -tags')) { |
| p = null; |
| return definitionLocation_gogetdoc(input, token, false).then(resolve, reject); |
| } |
| if (err) { |
| if (input.isMod && !input.includeDocs && stdout.startsWith("gogetdoc: couldn't get package for")) { |
| promptToUpdateToolForModules( |
| 'gogetdoc', |
| 'To get the Go to Definition feature when using Go modules, please update your version of the "gogetdoc" tool.' |
| ); |
| return resolve(null); |
| } |
| return reject(err.message || stderr); |
| } |
| const goGetDocOutput = <GoGetDocOuput>JSON.parse(stdout.toString()); |
| const match = /(.*):(\d+):(\d+)/.exec(goGetDocOutput.pos); |
| const definitionInfo: GoDefinitionInformation = { |
| file: null, |
| line: 0, |
| column: 0, |
| toolUsed: 'gogetdoc', |
| declarationlines: goGetDocOutput.decl.split('\n'), |
| doc: goGetDocOutput.doc, |
| name: goGetDocOutput.name |
| }; |
| if (!match) { |
| return resolve(definitionInfo); |
| } |
| definitionInfo.file = match[1]; |
| definitionInfo.line = +match[2] - 1; |
| definitionInfo.column = +match[3] - 1; |
| return resolve(definitionInfo); |
| } catch (e) { |
| reject(e); |
| } |
| }); |
| if (p.pid) { |
| p.stdin.end(getFileArchive(input.document)); |
| } |
| }); |
| } |
| |
| function definitionLocation_guru( |
| input: GoDefinitionInput, |
| token: vscode.CancellationToken |
| ): Promise<GoDefinitionInformation> { |
| const guru = getBinPath('guru'); |
| if (!path.isAbsolute(guru)) { |
| return Promise.reject(missingToolMsg + 'guru'); |
| } |
| const offset = byteOffsetAt(input.document, input.position); |
| const env = toolExecutionEnvironment(); |
| let p: cp.ChildProcess; |
| if (token) { |
| token.onCancellationRequested(() => killProcessTree(p)); |
| } |
| return new Promise<GoDefinitionInformation>((resolve, reject) => { |
| p = cp.execFile( |
| guru, |
| ['-json', '-modified', 'definition', input.document.fileName + ':#' + offset.toString()], |
| { env }, |
| (err, stdout, stderr) => { |
| try { |
| if (err && (<any>err).code === 'ENOENT') { |
| return reject(missingToolMsg + 'guru'); |
| } |
| if (err) { |
| return reject(err.message || stderr); |
| } |
| const guruOutput = <GuruDefinitionOuput>JSON.parse(stdout.toString()); |
| const match = /(.*):(\d+):(\d+)/.exec(guruOutput.objpos); |
| const definitionInfo: GoDefinitionInformation = { |
| file: null, |
| line: 0, |
| column: 0, |
| toolUsed: 'guru', |
| declarationlines: [guruOutput.desc], |
| doc: null, |
| name: null |
| }; |
| if (!match) { |
| return resolve(definitionInfo); |
| } |
| // const [_, file, line, col] = match; |
| definitionInfo.file = match[1]; |
| definitionInfo.line = +match[2] - 1; |
| definitionInfo.column = +match[3] - 1; |
| return resolve(definitionInfo); |
| } catch (e) { |
| reject(e); |
| } |
| } |
| ); |
| if (p.pid) { |
| p.stdin.end(getFileArchive(input.document)); |
| } |
| }); |
| } |
| |
| export function parseMissingError(err: any): [boolean, string] { |
| if (err) { |
| // Prompt for missing tool is located here so that the |
| // prompts dont show up on hover or signature help |
| if (typeof err === 'string' && err.startsWith(missingToolMsg)) { |
| return [true, err.substr(missingToolMsg.length)]; |
| } |
| } |
| return [false, null]; |
| } |
| |
| export class GoDefinitionProvider implements vscode.DefinitionProvider { |
| private goConfig: vscode.WorkspaceConfiguration = null; |
| |
| constructor(goConfig?: vscode.WorkspaceConfiguration) { |
| this.goConfig = goConfig; |
| } |
| |
| public provideDefinition( |
| document: vscode.TextDocument, |
| position: vscode.Position, |
| token: vscode.CancellationToken |
| ): Thenable<vscode.Location> { |
| return definitionLocation(document, position, this.goConfig, false, token).then( |
| (definitionInfo) => { |
| if (definitionInfo === null || definitionInfo.file === null) { |
| return null; |
| } |
| const definitionResource = vscode.Uri.file(definitionInfo.file); |
| const pos = new vscode.Position(definitionInfo.line, definitionInfo.column); |
| return new vscode.Location(definitionResource, pos); |
| }, |
| (err) => { |
| const miss = parseMissingError(err); |
| if (miss[0]) { |
| promptForMissingTool(miss[1]); |
| } else if (err) { |
| return Promise.reject(err); |
| } |
| return Promise.resolve(null); |
| } |
| ); |
| } |
| } |