blob: 13aaafe206d320eb0e87990b2a65f6b9c79282c8 [file] [log] [blame]
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------*/
'use strict';
import cp = require('child_process');
import vscode = require('vscode');
import { promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
import {
getBinPath,
getFileArchive,
getGoConfig,
getToolsEnvVars,
killProcess,
makeMemoizedByteOffsetConverter
} from './util';
// Keep in sync with https://github.com/ramya-rao-a/go-outline
export interface GoOutlineRange {
start: number;
end: number;
}
export interface GoOutlineDeclaration {
label: string;
type: string;
receiverType?: string;
icon?: string; // icon class or null to use the default images based on the type
start: number;
end: number;
children?: GoOutlineDeclaration[];
signature?: GoOutlineRange;
comment?: GoOutlineRange;
}
export enum GoOutlineImportsOptions {
Include,
Exclude,
Only
}
export interface GoOutlineOptions {
/**
* Path of the file for which outline is needed
*/
fileName: string;
/**
* Option to decide if the output includes, excludes or only includes imports
* If the option is to only include imports, then the file will be parsed only till imports are collected
*/
importsOption: GoOutlineImportsOptions;
/**
* Document to be parsed. If not provided, saved contents of the given fileName is used
*/
document?: vscode.TextDocument;
}
export async function documentSymbols(
options: GoOutlineOptions,
token: vscode.CancellationToken
): Promise<vscode.DocumentSymbol[]> {
const decls = await runGoOutline(options, token);
return convertToCodeSymbols(
options.document,
decls,
options.importsOption !== GoOutlineImportsOptions.Exclude,
makeMemoizedByteOffsetConverter(Buffer.from(options.document.getText()))
);
}
export function runGoOutline(
options: GoOutlineOptions,
token: vscode.CancellationToken
): Promise<GoOutlineDeclaration[]> {
return new Promise<GoOutlineDeclaration[]>((resolve, reject) => {
const gooutline = getBinPath('go-outline');
const gooutlineFlags = ['-f', options.fileName];
if (options.importsOption === GoOutlineImportsOptions.Only) {
gooutlineFlags.push('-imports-only');
}
if (options.document) {
gooutlineFlags.push('-modified');
}
let p: cp.ChildProcess;
if (token) {
token.onCancellationRequested(() => killProcess(p));
}
// Spawn `go-outline` process
p = cp.execFile(gooutline, gooutlineFlags, { env: getToolsEnvVars() }, (err, stdout, stderr) => {
try {
if (err && (<any>err).code === 'ENOENT') {
promptForMissingTool('go-outline');
}
if (stderr && stderr.startsWith('flag provided but not defined: ')) {
promptForUpdatingTool('go-outline');
if (stderr.startsWith('flag provided but not defined: -imports-only')) {
options.importsOption = GoOutlineImportsOptions.Include;
}
if (stderr.startsWith('flag provided but not defined: -modified')) {
options.document = null;
}
p = null;
return runGoOutline(options, token).then((results) => {
return resolve(results);
});
}
if (err) {
return resolve(null);
}
const result = stdout.toString();
const decls = <GoOutlineDeclaration[]>JSON.parse(result);
return resolve(decls);
} catch (e) {
reject(e);
}
});
if (options.document && p.pid) {
p.stdin.end(getFileArchive(options.document));
}
});
}
const goKindToCodeKind: { [key: string]: vscode.SymbolKind } = {
package: vscode.SymbolKind.Package,
import: vscode.SymbolKind.Namespace,
variable: vscode.SymbolKind.Variable,
constant: vscode.SymbolKind.Constant,
type: vscode.SymbolKind.TypeParameter,
function: vscode.SymbolKind.Function,
struct: vscode.SymbolKind.Struct,
interface: vscode.SymbolKind.Interface
};
function convertToCodeSymbols(
document: vscode.TextDocument,
decls: GoOutlineDeclaration[],
includeImports: boolean,
byteOffsetToDocumentOffset: (byteOffset: number) => number
): vscode.DocumentSymbol[] {
const symbols: vscode.DocumentSymbol[] = [];
(decls || []).forEach((decl) => {
if (!includeImports && decl.type === 'import') {
return;
}
if (decl.label === '_' && decl.type === 'variable') {
return;
}
const label = decl.receiverType ? `(${decl.receiverType}).${decl.label}` : decl.label;
const start = byteOffsetToDocumentOffset(decl.start - 1);
const end = byteOffsetToDocumentOffset(decl.end - 1);
const startPosition = document.positionAt(start);
const endPosition = document.positionAt(end);
const symbolRange = new vscode.Range(startPosition, endPosition);
const selectionRange =
startPosition.line === endPosition.line
? symbolRange
: new vscode.Range(startPosition, document.lineAt(startPosition.line).range.end);
if (decl.type === 'type') {
const line = document.lineAt(document.positionAt(start));
const regexStruct = new RegExp(`^\\s*type\\s+${decl.label}\\s+struct\\b`);
const regexInterface = new RegExp(`^\\s*type\\s+${decl.label}\\s+interface\\b`);
decl.type = regexStruct.test(line.text) ? 'struct' : regexInterface.test(line.text) ? 'interface' : 'type';
}
const symbolInfo = new vscode.DocumentSymbol(
label,
decl.type,
goKindToCodeKind[decl.type],
symbolRange,
selectionRange
);
symbols.push(symbolInfo);
if (decl.children) {
symbolInfo.children = convertToCodeSymbols(
document,
decl.children,
includeImports,
byteOffsetToDocumentOffset
);
}
});
return symbols;
}
export class GoDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
constructor(private includeImports?: boolean) {}
public provideDocumentSymbols(
document: vscode.TextDocument,
token: vscode.CancellationToken
): Thenable<vscode.DocumentSymbol[]> {
if (typeof this.includeImports !== 'boolean') {
const gotoSymbolConfig = getGoConfig(document.uri)['gotoSymbol'];
this.includeImports = gotoSymbolConfig ? gotoSymbolConfig['includeImports'] : false;
}
const options: GoOutlineOptions = {
fileName: document.fileName,
document,
importsOption: this.includeImports ? GoOutlineImportsOptions.Include : GoOutlineImportsOptions.Exclude
};
return documentSymbols(options, token);
}
}