🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
Book a DemoInstallSign in
Socket

bash-language-server

Package Overview
Dependencies
Maintainers
1
Versions
110
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bash-language-server - npm Package Compare versions

Comparing version

to
1.3.0

out/builtins.js

2

bin/main.js
#!/usr/bin/env node
const server = require('../out/server')
const server = require('../out/index')
const package = require('../package')

@@ -5,0 +5,0 @@

# Bash Language Server
## 1.3.0
- Improved completions by adding support for
- Suggestions based on the programs on your PATH [#17][17]
- Suggestions based on the bash builtins [#33][33]
- Implemented the `onHover` message that now shows documentation for programs
and builtins when you hover your cursor over words in the document. [#17][17]
[#33][33]
- Improved outline hierarchy [#31][31]
- Upgraded tree-sitter bash and other libraries. [#28][28]
## 1.1.2

@@ -9,1 +23,6 @@

[tree-sitter/tree-sitter-bash#9](https://github.com/tree-sitter/tree-sitter-bash/issues/9)
[17]: https://github.com/mads-hartmann/bash-language-server/pull/17
[28]: https://github.com/mads-hartmann/bash-language-server/pull/28
[31]: https://github.com/mads-hartmann/bash-language-server/pull/31
[33]: https://github.com/mads-hartmann/bash-language-server/pull/33
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// tslint:disable:no-submodule-imports
const fs = require("fs");
const glob = require("glob");
const Path = require("path");
const tree_sitter_1 = require("tree-sitter");
const bash = require("tree-sitter-bash");
const main_1 = require("vscode-languageserver/lib/main");
const kinds = {
environment_variable_assignment: main_1.SymbolKind.Variable,
function_definition: main_1.SymbolKind.Function,
};
const declarations = {};
const documents = {};
const texts = {};
const LSP = require("vscode-languageserver");
const array_1 = require("./util/array");
const flatten_1 = require("./util/flatten");
const TreeSitterUtil = require("./util/tree-sitter");
/**
* Analyze the given document, cache the tree-sitter AST, and iterate over the
* tree to find declarations.
*
* Returns all, if any, syntax errors that occurred while parsing the file.
*
* The Analyzer uses the Abstract Syntax Trees (ASTs) that are provided by
* tree-sitter to find definitions, reference, etc.
*/
function analyze(uri, contents) {
const d = new tree_sitter_1.Document();
d.setLanguage(bash);
d.setInputString(contents);
d.parse();
documents[uri] = d;
declarations[uri] = {};
texts[uri] = contents;
const problems = [];
forEach(d.rootNode, n => {
if (n.type === 'ERROR') {
problems.push(main_1.Diagnostic.create(range(n), 'Failed to parse expression', main_1.DiagnosticSeverity.Error));
return;
class Analyzer {
constructor() {
this.uriToTreeSitterDocument = {};
// We need this to find the word at a given point etc.
this.uriToFileContent = {};
this.uriToDeclarations = {};
this.treeSitterTypeToLSPKind = {
// These keys are using underscores as that's the naming convention in tree-sitter.
environment_variable_assignment: LSP.SymbolKind.Variable,
function_definition: LSP.SymbolKind.Function,
};
}
/**
* Initialize the Analyzer based on a connection to the client and an optional
* root path.
*
* If the rootPath is provided it will initialize all *.sh files it can find
* anywhere on that path.
*/
static fromRoot(connection, rootPath) {
// This happens if the users opens a single bash script without having the
// 'window' associated with a specific project.
if (!rootPath) {
return Promise.resolve(new Analyzer());
}
else if (isDefinition(n)) {
const named = n.firstNamedChild;
const name = contents.slice(named.startIndex, named.endIndex);
const namedDeclarations = declarations[uri][name] || [];
namedDeclarations.push(main_1.SymbolInformation.create(name, kinds[n.type], range(named), uri));
declarations[uri][name] = namedDeclarations;
}
});
return problems;
}
exports.analyze = analyze;
/**
* Find all the locations where something named name has been defined.
*/
function findDefinition(name) {
const symbols = [];
Object.keys(declarations).forEach(uri => {
const declarationNames = declarations[uri][name] || [];
declarationNames.forEach(d => symbols.push(d));
});
return symbols.map(s => s.location);
}
exports.findDefinition = findDefinition;
/**
* Find all the locations where something named name has been defined.
*/
function findReferences(name) {
const locations = [];
Object.keys(documents).forEach(uri => {
findOccurrences(uri, name).forEach(l => locations.push(l));
});
return locations;
}
exports.findReferences = findReferences;
/**
* Find all occurrences of name in the given file.
* It's currently not scope-aware.
*/
function findOccurrences(uri, query) {
const doc = documents[uri];
const contents = texts[uri];
const locations = [];
forEach(doc.rootNode, n => {
let name = null;
let rng = null;
if (isReference(n)) {
const node = n.firstNamedChild || n;
name = contents.slice(node.startIndex, node.endIndex);
rng = range(node);
}
else if (isDefinition(n)) {
const namedNode = n.firstNamedChild;
name = contents.slice(namedNode.startIndex, namedNode.endIndex);
rng = range(n.firstNamedChild);
}
if (name === query) {
locations.push(main_1.Location.create(uri, rng));
}
});
return locations;
}
exports.findOccurrences = findOccurrences;
/**
* Find all symbol definitions in the given file.
*/
function findSymbols(uri) {
const declarationsInFile = declarations[uri] || [];
const ds = [];
Object.keys(declarationsInFile).forEach(n => {
declarationsInFile[n].forEach(d => ds.push(d));
});
return ds;
}
exports.findSymbols = findSymbols;
/**
* Find the full word at the given point.
*/
function wordAtPoint(uri, line, column) {
const document = documents[uri];
const contents = texts[uri];
const node = document.rootNode.namedDescendantForPosition({ row: line, column });
const start = node.startIndex;
const end = node.endIndex;
const name = contents.slice(start, end);
// Hack. Might be a problem with the grammar.
if (name.endsWith('=')) {
return name.slice(0, name.length - 1);
return new Promise((resolve, reject) => {
glob('**/*.sh', { cwd: rootPath }, (err, paths) => {
if (err != null) {
reject(err);
}
else {
const analyzer = new Analyzer();
paths.forEach(p => {
const absolute = Path.join(rootPath, p);
const uri = 'file://' + absolute;
connection.console.log('Analyzing ' + uri);
analyzer.analyze(uri, fs.readFileSync(absolute, 'utf8'));
});
resolve(analyzer);
}
});
});
}
return name;
}
exports.wordAtPoint = wordAtPoint;
//
// Tree sitter utility functions.
//
function forEach(node, cb) {
cb(node);
if (node.children.length) {
node.children.forEach(n => forEach(n, cb));
/**
* Find all the locations where something named name has been defined.
*/
findDefinition(name) {
const symbols = [];
Object.keys(this.uriToDeclarations).forEach(uri => {
const declarationNames = this.uriToDeclarations[uri][name] || [];
declarationNames.forEach(d => symbols.push(d));
});
return symbols.map(s => s.location);
}
}
function range(n) {
return main_1.Range.create(n.startPosition.row, n.startPosition.column, n.endPosition.row, n.endPosition.column);
}
function isDefinition(n) {
switch (n.type) {
// For now. Later we'll have a command_declaration take precedence over
// variable_assignment
case 'variable_assignment':
case 'function_definition':
return true;
default:
return false;
/**
* Find all the locations where something named name has been defined.
*/
findReferences(name) {
const uris = Object.keys(this.uriToTreeSitterDocument);
return flatten_1.flattenArray(uris.map(uri => this.findOccurrences(uri, name)));
}
}
function isReference(n) {
switch (n.type) {
case 'variable_name':
case 'command_name':
return true;
default:
return false;
/**
* Find all occurrences of name in the given file.
* It's currently not scope-aware.
*/
findOccurrences(uri, query) {
const doc = this.uriToTreeSitterDocument[uri];
const contents = this.uriToFileContent[uri];
const locations = [];
TreeSitterUtil.forEach(doc.rootNode, n => {
let name = null;
let rng = null;
if (TreeSitterUtil.isReference(n)) {
const node = n.firstNamedChild || n;
name = contents.slice(node.startIndex, node.endIndex);
rng = TreeSitterUtil.range(node);
}
else if (TreeSitterUtil.isDefinition(n)) {
const namedNode = n.firstNamedChild;
name = contents.slice(namedNode.startIndex, namedNode.endIndex);
rng = TreeSitterUtil.range(n.firstNamedChild);
}
if (name === query) {
locations.push(LSP.Location.create(uri, rng));
}
});
return locations;
}
/**
* Find all symbol definitions in the given file.
*/
findSymbols(uri) {
const declarationsInFile = this.uriToDeclarations[uri] || {};
return flatten_1.flattenObjectValues(declarationsInFile);
}
/**
* Find unique symbol completions for the given file.
*/
findSymbolCompletions(uri) {
const hashFunction = ({ name, kind }) => `${name}${kind}`;
return array_1.uniqueBasedOnHash(this.findSymbols(uri), hashFunction).map((symbol) => ({
label: symbol.name,
kind: this.symbolKindToCompletionKind(symbol.kind),
data: {
name: symbol.name,
type: 'function',
},
}));
}
/**
* Analyze the given document, cache the tree-sitter AST, and iterate over the
* tree to find declarations.
*
* Returns all, if any, syntax errors that occurred while parsing the file.
*
*/
analyze(uri, contents) {
const d = new tree_sitter_1.Document();
d.setLanguage(bash);
d.setInputString(contents);
d.parse();
this.uriToTreeSitterDocument[uri] = d;
this.uriToDeclarations[uri] = {};
this.uriToFileContent[uri] = contents;
const problems = [];
TreeSitterUtil.forEach(d.rootNode, n => {
if (n.type === 'ERROR') {
problems.push(LSP.Diagnostic.create(TreeSitterUtil.range(n), 'Failed to parse expression', LSP.DiagnosticSeverity.Error));
return;
}
else if (TreeSitterUtil.isDefinition(n)) {
const named = n.firstNamedChild;
const name = contents.slice(named.startIndex, named.endIndex);
const namedDeclarations = this.uriToDeclarations[uri][name] || [];
const parent = TreeSitterUtil.findParent(n, p => p.type === 'function_definition');
const parentName = parent
? contents.slice(parent.firstNamedChild.startIndex, parent.firstNamedChild.endIndex)
: null;
namedDeclarations.push(LSP.SymbolInformation.create(name, this.treeSitterTypeToLSPKind[n.type], TreeSitterUtil.range(n), uri, parentName));
this.uriToDeclarations[uri][name] = namedDeclarations;
}
});
return problems;
}
/**
* Find the full word at the given point.
*/
wordAtPoint(uri, line, column) {
const document = this.uriToTreeSitterDocument[uri];
const contents = this.uriToFileContent[uri];
const node = document.rootNode.namedDescendantForPosition({ row: line, column });
const start = node.startIndex;
const end = node.endIndex;
const name = contents.slice(start, end);
// Hack. Might be a problem with the grammar.
if (name.endsWith('=')) {
return name.slice(0, name.length - 1);
}
return name;
}
symbolKindToCompletionKind(s) {
switch (s) {
case LSP.SymbolKind.File:
return LSP.CompletionItemKind.File;
case LSP.SymbolKind.Module:
case LSP.SymbolKind.Namespace:
case LSP.SymbolKind.Package:
return LSP.CompletionItemKind.Module;
case LSP.SymbolKind.Class:
return LSP.CompletionItemKind.Class;
case LSP.SymbolKind.Method:
return LSP.CompletionItemKind.Method;
case LSP.SymbolKind.Property:
return LSP.CompletionItemKind.Property;
case LSP.SymbolKind.Field:
return LSP.CompletionItemKind.Field;
case LSP.SymbolKind.Constructor:
return LSP.CompletionItemKind.Constructor;
case LSP.SymbolKind.Enum:
return LSP.CompletionItemKind.Enum;
case LSP.SymbolKind.Interface:
return LSP.CompletionItemKind.Interface;
case LSP.SymbolKind.Function:
return LSP.CompletionItemKind.Function;
case LSP.SymbolKind.Variable:
return LSP.CompletionItemKind.Variable;
case LSP.SymbolKind.Constant:
return LSP.CompletionItemKind.Constant;
case LSP.SymbolKind.String:
case LSP.SymbolKind.Number:
case LSP.SymbolKind.Boolean:
case LSP.SymbolKind.Array:
case LSP.SymbolKind.Key:
case LSP.SymbolKind.Null:
return LSP.CompletionItemKind.Text;
case LSP.SymbolKind.Object:
return LSP.CompletionItemKind.Module;
case LSP.SymbolKind.EnumMember:
return LSP.CompletionItemKind.EnumMember;
case LSP.SymbolKind.Struct:
return LSP.CompletionItemKind.Struct;
case LSP.SymbolKind.Event:
return LSP.CompletionItemKind.Event;
case LSP.SymbolKind.Operator:
return LSP.CompletionItemKind.Operator;
case LSP.SymbolKind.TypeParameter:
return LSP.CompletionItemKind.TypeParameter;
default:
return LSP.CompletionItemKind.Text;
}
}
}
exports.default = Analyzer;
//# sourceMappingURL=analyser.js.map

@@ -5,29 +5,53 @@ "use strict";

const Path = require("path");
const ChildProcess = require("child_process");
const ArrayUtil = require("./util/array");
const FsUtil = require("./util/fs");
const ShUtil = require("./util/sh");
/**
* Find all programs in your PATH
* Provides information based on the programs on your PATH
*/
function findAll(path) {
// TODO: Don't do this for windows, I guess?
const paths = path.split(":");
const promises = paths.map(x => executables(x));
return Promise.all(promises).then(flatten).then(uniq);
}
exports.findAll = findAll;
/**
*
*/
function man(path) {
return new Promise((resolve) => {
ChildProcess.exec(`man ${path}`, (_err, stdout, _stderr) => {
resolve(stdout);
class Executables {
/**
* @param path is expected to to be a ':' separated list of paths.
*/
static fromPath(path) {
const paths = path.split(':');
const promises = paths.map(x => findExecutablesInPath(x));
return Promise.all(promises)
.then(ArrayUtil.flatten)
.then(ArrayUtil.uniq)
.then(executables => new Executables(executables));
}
constructor(executables) {
this.executables = new Set(executables);
}
/**
* Find all programs in your PATH
*/
list() {
return Array.from(this.executables.values());
}
/**
* Check if the the given {{executable}} exists on the PATH
*/
isExecutableOnPATH(executable) {
return this.executables.has(executable);
}
/**
* Look up documentation for the given executable.
*
* For now it simply tries to look up the MAN documentation.
*/
documentation(executable) {
return ShUtil.execShellScript(`man ${executable} | col -b`).then(doc => {
return !doc
? Promise.resolve(`No MAN page for ${executable}`)
: Promise.resolve(doc);
});
});
}
}
exports.man = man;
exports.default = Executables;
/**
* Find all the executables on the given path.
* Only returns direct children, or the path itself if it's an executable.
*/
function executables(path) {
function findExecutablesInPath(path) {
return new Promise((resolve, _) => {

@@ -40,11 +64,11 @@ Fs.lstat(path, (err, stat) => {

if (stat.isDirectory()) {
Fs.readdir(path, (err, paths) => {
if (err) {
Fs.readdir(path, (readDirErr, paths) => {
if (readDirErr) {
resolve([]);
}
else {
const files = paths.map(p => getStats(Path.join(path, p))
.then(s => s.isFile() ? [Path.basename(p)] : [])
const files = paths.map(p => FsUtil.getStats(Path.join(path, p))
.then(s => (s.isFile() ? [Path.basename(p)] : []))
.catch(() => []));
resolve(Promise.all(files).then(flatten));
resolve(Promise.all(files).then(ArrayUtil.flatten));
}

@@ -64,20 +88,2 @@ });

}
function getStats(path) {
return new Promise((resolve, reject) => {
Fs.lstat(path, (err, stat) => {
if (err) {
reject(err);
}
else {
resolve(stat);
}
});
});
}
function flatten(xs) {
return xs.reduce((a, b) => a.concat(b), []);
}
function uniq(a) {
return Array.from(new Set(a));
}
//# sourceMappingURL=executables.js.map

@@ -1,111 +0,174 @@

'use strict';
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
// tslint:disable:no-var-requires
const vscode_languageserver_1 = require("vscode-languageserver");
const glob = require('glob');
const fs = require('fs');
const Path = require("path");
const pkg = require('../package');
const Analyser = require("./analyser");
function listen() {
// Create a connection for the server.
// The connection uses stdin/stdout for communication.
const connection = vscode_languageserver_1.createConnection(new vscode_languageserver_1.StreamMessageReader(process.stdin), new vscode_languageserver_1.StreamMessageWriter(process.stdout));
// Create a simple text document manager. The text document manager
// supports full document sync only
const documents = new vscode_languageserver_1.TextDocuments();
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
connection.onInitialize((params) => {
connection.console.log(`Initialized server v. ${pkg.version} for ${params.rootUri}`);
if (params.rootPath) {
glob('**/*.sh', { cwd: params.rootPath }, (err, paths) => {
if (err != null) {
connection.console.error(err);
}
else {
paths.forEach(p => {
const absolute = Path.join(params.rootPath, p);
const uri = 'file://' + absolute;
connection.console.log('Analyzing ' + uri);
Analyser.analyze(uri, fs.readFileSync(absolute, 'utf8'));
});
}
const LSP = require("vscode-languageserver");
const analyser_1 = require("./analyser");
const Builtins = require("./builtins");
const executables_1 = require("./executables");
/**
* The BashServer glues together the separate components to implement
* the various parts of the Language Server Protocol.
*/
class BashServer {
constructor(connection, executables, analyzer) {
this.documents = new LSP.TextDocuments();
this.connection = connection;
this.executables = executables;
this.analyzer = analyzer;
}
/**
* Initialize the server based on a connection to the client and the protocols
* initialization parameters.
*/
static initialize(connection, params) {
return Promise.all([
executables_1.default.fromPath(process.env.PATH),
analyser_1.default.fromRoot(connection, params.rootPath),
]).then(xs => {
const executables = xs[0];
const analyzer = xs[1];
return new BashServer(connection, executables, analyzer);
});
}
/**
* Register handlers for the events from the Language Server Protocol that we
* care about.
*/
register(connection) {
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
this.documents.listen(this.connection);
this.documents.onDidChangeContent(change => {
const uri = change.document.uri;
const contents = change.document.getText();
const diagnostics = this.analyzer.analyze(uri, contents);
connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
});
}
});
// Register all the handlers for the LSP events.
connection.onHover(this.onHover.bind(this));
connection.onDefinition(this.onDefinition.bind(this));
connection.onDocumentSymbol(this.onDocumentSymbol.bind(this));
connection.onDocumentHighlight(this.onDocumentHighlight.bind(this));
connection.onReferences(this.onReferences.bind(this));
connection.onCompletion(this.onCompletion.bind(this));
connection.onCompletionResolve(this.onCompletionResolve.bind(this));
}
/**
* The parts of the Language Server Protocol that we are currently supporting.
*/
capabilities() {
return {
capabilities: {
// For now we're using full-sync even though tree-sitter has great support
// for partial updates.
textDocumentSync: documents.syncKind,
completionProvider: {
resolveProvider: true,
},
documentHighlightProvider: true,
definitionProvider: true,
documentSymbolProvider: true,
referencesProvider: true,
// For now we're using full-sync even though tree-sitter has great support
// for partial updates.
textDocumentSync: this.documents.syncKind,
completionProvider: {
resolveProvider: true,
},
hoverProvider: true,
documentHighlightProvider: true,
definitionProvider: true,
documentSymbolProvider: true,
referencesProvider: true,
};
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
connection.console.log('Invoked onDidChangeContent');
const uri = change.document.uri;
const contents = change.document.getText();
const diagnostics = Analyser.analyze(uri, contents);
connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
});
});
connection.onDidChangeWatchedFiles(() => {
// Monitored files have change in VSCode
connection.console.log('We received an file change event');
});
connection.onDefinition((textDocumentPosition) => {
connection.console.log(`Asked for definition at ${textDocumentPosition.position.line}:${textDocumentPosition.position.character}`);
const word = Analyser.wordAtPoint(textDocumentPosition.textDocument.uri, textDocumentPosition.position.line, textDocumentPosition.position.character);
return Analyser.findDefinition(word);
});
connection.onDocumentSymbol((params) => {
return Analyser.findSymbols(params.textDocument.uri);
});
connection.onDocumentHighlight((textDocumentPosition) => {
const word = Analyser.wordAtPoint(textDocumentPosition.textDocument.uri, textDocumentPosition.position.line, textDocumentPosition.position.character);
return Analyser.findOccurrences(textDocumentPosition.textDocument.uri, word).map(n => ({ range: n.range }));
});
connection.onReferences((params) => {
const word = Analyser.wordAtPoint(params.textDocument.uri, params.position.line, params.position.character);
return Analyser.findReferences(word);
});
connection.onCompletion((textDocumentPosition) => {
connection.console.log(`Asked for completions at ${textDocumentPosition.position.line}:${textDocumentPosition.position.character}`);
const symbols = Analyser.findSymbols(textDocumentPosition.textDocument.uri);
return symbols.map((s) => {
}
getWordAtPoint(params) {
return this.analyzer.wordAtPoint(params.textDocument.uri, params.position.line, params.position.character);
}
onHover(pos) {
this.connection.console.log(`Hovering over ${pos.position.line}:${pos.position.character}`);
const word = this.getWordAtPoint(pos);
if (Builtins.isBuiltin(word)) {
return Builtins.documentation(word).then(doc => ({
contents: {
language: 'plaintext',
value: doc,
},
}));
}
else if (this.executables.isExecutableOnPATH(word)) {
return this.executables.documentation(word).then(doc => ({
contents: {
language: 'plaintext',
value: doc,
},
}));
}
else {
return null;
}
}
onDefinition(pos) {
this.connection.console.log(`Asked for definition at ${pos.position.line}:${pos.position.character}`);
const word = this.getWordAtPoint(pos);
return this.analyzer.findDefinition(word);
}
onDocumentSymbol(params) {
return this.analyzer.findSymbols(params.textDocument.uri);
}
onDocumentHighlight(pos) {
const word = this.getWordAtPoint(pos);
return this.analyzer
.findOccurrences(pos.textDocument.uri, word)
.map(n => ({ range: n.range }));
}
onReferences(params) {
const word = this.getWordAtPoint(params);
return this.analyzer.findReferences(word);
}
onCompletion(pos) {
this.connection.console.log(`Asked for completions at ${pos.position.line}:${pos.position.character}`);
const symbolCompletions = this.analyzer.findSymbolCompletions(pos.textDocument.uri);
const programCompletions = this.executables.list().map((s) => {
return {
label: s.name,
kind: s.kind,
data: s.name,
label: s,
kind: LSP.SymbolKind.Function,
data: {
name: s,
type: 'executable',
},
};
});
});
// This handler resolve additional information for the item selected in
// the completion list.
connection.onCompletionResolve((item) => {
// TODO: Look up man pages for commands
// TODO: For builtins look up the docs.
// TODO: For functions, parse their comments?
// if (item.data === 1) {
// item.detail = 'TypeScript details',
// item.documentation = 'TypeScript documentation'
// }
return item;
});
// Listen on the connection
connection.listen();
const builtinsCompletions = Builtins.LIST.map(builtin => ({
label: builtin,
kind: LSP.SymbolKind.Method,
data: {
name: builtin,
type: 'builtin',
},
}));
return [...symbolCompletions, ...programCompletions, ...builtinsCompletions];
}
onCompletionResolve(item) {
return __awaiter(this, void 0, void 0, function* () {
const { data: { name, type } } = item;
try {
if (type === 'executable') {
const doc = yield this.executables.documentation(name);
return Object.assign({}, item, { detail: doc });
}
else if (type === 'builtin') {
const doc = yield Builtins.documentation(name);
return Object.assign({}, item, { detail: doc });
}
else {
return item;
}
}
catch (error) {
return item;
}
});
}
}
exports.listen = listen;
exports.default = BashServer;
//# sourceMappingURL=server.js.map

@@ -6,3 +6,3 @@ {

"license": "MIT",
"version": "1.1.2",
"version": "1.3.0",
"publisher": "mads-hartmann",

@@ -22,10 +22,10 @@ "main": "out/server.js",

"glob": "^7.1.2",
"tree-sitter": "^0.10.0",
"tree-sitter-bash": "^0.6.0",
"vscode-languageserver": "^3.5.0"
"tree-sitter": "^0.11.0",
"tree-sitter-bash": "^0.11.0",
"vscode-languageserver": "^4.1.1"
},
"scripts": {
"compile": "tsc -p ./",
"compile": "rm -rf out && tsc -p ./",
"compile:watch": "tsc -w -p ./"
}
}
// tslint:disable:no-submodule-imports
import { ASTNode, Document } from 'tree-sitter'
import * as fs from 'fs'
import * as glob from 'glob'
import * as Path from 'path'
import { Document } from 'tree-sitter'
import * as bash from 'tree-sitter-bash'
import {
Diagnostic,
DiagnosticSeverity,
Location,
Range,
SymbolInformation,
SymbolKind,
} from 'vscode-languageserver/lib/main'
import * as LSP from 'vscode-languageserver'
// Global mapping from tree-sitter node type to vscode SymbolKind
type Kinds = { [type: string]: SymbolKind }
const kinds: Kinds = {
environment_variable_assignment: SymbolKind.Variable,
function_definition: SymbolKind.Function,
}
import { uniqueBasedOnHash } from './util/array'
import { flattenArray, flattenObjectValues } from './util/flatten'
import * as TreeSitterUtil from './util/tree-sitter'
// Global map of all the declarations that we've seen, indexed by file
// and then name.
type Declarations = { [name: string]: SymbolInformation[] }
type Kinds = { [type: string]: LSP.SymbolKind }
type Declarations = { [name: string]: LSP.SymbolInformation[] }
type FileDeclarations = { [uri: string]: Declarations }
const declarations: FileDeclarations = {}
// Global map from uri to the tree-sitter document.
type Documents = { [uri: string]: Document }
const documents: Documents = {}
// Global mapping from uri to the contents of the file at that uri.
// We need this to find the word at a given point etc.
type Texts = { [uri: string]: string }
const texts: Texts = {}
/**
* Analyze the given document, cache the tree-sitter AST, and iterate over the
* tree to find declarations.
*
* Returns all, if any, syntax errors that occurred while parsing the file.
*
* The Analyzer uses the Abstract Syntax Trees (ASTs) that are provided by
* tree-sitter to find definitions, reference, etc.
*/
export function analyze(uri: string, contents: string): Diagnostic[] {
const d = new Document()
d.setLanguage(bash)
d.setInputString(contents)
d.parse()
export default class Analyzer {
/**
* Initialize the Analyzer based on a connection to the client and an optional
* root path.
*
* If the rootPath is provided it will initialize all *.sh files it can find
* anywhere on that path.
*/
public static fromRoot(
connection: LSP.Connection,
rootPath: string | null,
): Promise<Analyzer> {
// This happens if the users opens a single bash script without having the
// 'window' associated with a specific project.
if (!rootPath) {
return Promise.resolve(new Analyzer())
}
documents[uri] = d
declarations[uri] = {}
texts[uri] = contents
return new Promise((resolve, reject) => {
glob('**/*.sh', { cwd: rootPath }, (err, paths) => {
if (err != null) {
reject(err)
} else {
const analyzer = new Analyzer()
paths.forEach(p => {
const absolute = Path.join(rootPath, p)
const uri = 'file://' + absolute
connection.console.log('Analyzing ' + uri)
analyzer.analyze(uri, fs.readFileSync(absolute, 'utf8'))
})
resolve(analyzer)
}
})
})
}
const problems = []
private uriToTreeSitterDocument: Documents = {}
forEach(d.rootNode, n => {
if (n.type === 'ERROR') {
problems.push(
Diagnostic.create(
range(n),
'Failed to parse expression',
DiagnosticSeverity.Error,
),
)
return
} else if (isDefinition(n)) {
const named = n.firstNamedChild
const name = contents.slice(named.startIndex, named.endIndex)
const namedDeclarations = declarations[uri][name] || []
// We need this to find the word at a given point etc.
private uriToFileContent: Texts = {}
namedDeclarations.push(
SymbolInformation.create(name, kinds[n.type], range(named), uri),
)
declarations[uri][name] = namedDeclarations
}
})
private uriToDeclarations: FileDeclarations = {}
return problems
}
private treeSitterTypeToLSPKind: Kinds = {
// These keys are using underscores as that's the naming convention in tree-sitter.
environment_variable_assignment: LSP.SymbolKind.Variable,
function_definition: LSP.SymbolKind.Function,
}
/**
* Find all the locations where something named name has been defined.
*/
export function findDefinition(name: string): Location[] {
const symbols: SymbolInformation[] = []
Object.keys(declarations).forEach(uri => {
const declarationNames = declarations[uri][name] || []
declarationNames.forEach(d => symbols.push(d))
})
return symbols.map(s => s.location)
}
/**
* Find all the locations where something named name has been defined.
*/
public findDefinition(name: string): LSP.Location[] {
const symbols: LSP.SymbolInformation[] = []
Object.keys(this.uriToDeclarations).forEach(uri => {
const declarationNames = this.uriToDeclarations[uri][name] || []
declarationNames.forEach(d => symbols.push(d))
})
return symbols.map(s => s.location)
}
/**
* Find all the locations where something named name has been defined.
*/
export function findReferences(name: string): Location[] {
const locations = []
Object.keys(documents).forEach(uri => {
findOccurrences(uri, name).forEach(l => locations.push(l))
})
return locations
}
/**
* Find all the locations where something named name has been defined.
*/
public findReferences(name: string): LSP.Location[] {
const uris = Object.keys(this.uriToTreeSitterDocument)
return flattenArray(uris.map(uri => this.findOccurrences(uri, name)))
}
/**
* Find all occurrences of name in the given file.
* It's currently not scope-aware.
*/
export function findOccurrences(uri: string, query: string): Location[] {
const doc = documents[uri]
const contents = texts[uri]
/**
* Find all occurrences of name in the given file.
* It's currently not scope-aware.
*/
public findOccurrences(uri: string, query: string): LSP.Location[] {
const doc = this.uriToTreeSitterDocument[uri]
const contents = this.uriToFileContent[uri]
const locations = []
const locations = []
forEach(doc.rootNode, n => {
let name: string = null
let rng: Range = null
TreeSitterUtil.forEach(doc.rootNode, n => {
let name: string = null
let rng: LSP.Range = null
if (isReference(n)) {
const node = n.firstNamedChild || n
name = contents.slice(node.startIndex, node.endIndex)
rng = range(node)
} else if (isDefinition(n)) {
const namedNode = n.firstNamedChild
name = contents.slice(namedNode.startIndex, namedNode.endIndex)
rng = range(n.firstNamedChild)
}
if (TreeSitterUtil.isReference(n)) {
const node = n.firstNamedChild || n
name = contents.slice(node.startIndex, node.endIndex)
rng = TreeSitterUtil.range(node)
} else if (TreeSitterUtil.isDefinition(n)) {
const namedNode = n.firstNamedChild
name = contents.slice(namedNode.startIndex, namedNode.endIndex)
rng = TreeSitterUtil.range(n.firstNamedChild)
}
if (name === query) {
locations.push(Location.create(uri, rng))
}
})
if (name === query) {
locations.push(LSP.Location.create(uri, rng))
}
})
return locations
}
return locations
}
/**
* Find all symbol definitions in the given file.
*/
export function findSymbols(uri: string): SymbolInformation[] {
const declarationsInFile = declarations[uri] || []
const ds = []
Object.keys(declarationsInFile).forEach(n => {
declarationsInFile[n].forEach(d => ds.push(d))
})
return ds
}
/**
* Find all symbol definitions in the given file.
*/
public findSymbols(uri: string): LSP.SymbolInformation[] {
const declarationsInFile = this.uriToDeclarations[uri] || {}
return flattenObjectValues(declarationsInFile)
}
/**
* Find the full word at the given point.
*/
export function wordAtPoint(uri: string, line: number, column: number): string | null {
const document = documents[uri]
const contents = texts[uri]
/**
* Find unique symbol completions for the given file.
*/
public findSymbolCompletions(uri: string): LSP.CompletionItem[] {
const hashFunction = ({ name, kind }) => `${name}${kind}`
const node = document.rootNode.namedDescendantForPosition({ row: line, column })
return uniqueBasedOnHash(this.findSymbols(uri), hashFunction).map(
(symbol: LSP.SymbolInformation) => ({
label: symbol.name,
kind: this.symbolKindToCompletionKind(symbol.kind),
data: {
name: symbol.name,
type: 'function',
},
}),
)
}
const start = node.startIndex
const end = node.endIndex
const name = contents.slice(start, end)
/**
* Analyze the given document, cache the tree-sitter AST, and iterate over the
* tree to find declarations.
*
* Returns all, if any, syntax errors that occurred while parsing the file.
*
*/
public analyze(uri: string, contents: string): LSP.Diagnostic[] {
const d = new Document()
d.setLanguage(bash)
d.setInputString(contents)
d.parse()
// Hack. Might be a problem with the grammar.
if (name.endsWith('=')) {
return name.slice(0, name.length - 1)
}
this.uriToTreeSitterDocument[uri] = d
this.uriToDeclarations[uri] = {}
this.uriToFileContent[uri] = contents
return name
}
const problems = []
//
// Tree sitter utility functions.
//
TreeSitterUtil.forEach(d.rootNode, n => {
if (n.type === 'ERROR') {
problems.push(
LSP.Diagnostic.create(
TreeSitterUtil.range(n),
'Failed to parse expression',
LSP.DiagnosticSeverity.Error,
),
)
return
} else if (TreeSitterUtil.isDefinition(n)) {
const named = n.firstNamedChild
const name = contents.slice(named.startIndex, named.endIndex)
const namedDeclarations = this.uriToDeclarations[uri][name] || []
function forEach(node: ASTNode, cb: (n: ASTNode) => void) {
cb(node)
if (node.children.length) {
node.children.forEach(n => forEach(n, cb))
const parent = TreeSitterUtil.findParent(n, p => p.type === 'function_definition')
const parentName = parent
? contents.slice(
parent.firstNamedChild.startIndex,
parent.firstNamedChild.endIndex,
)
: null
namedDeclarations.push(
LSP.SymbolInformation.create(
name,
this.treeSitterTypeToLSPKind[n.type],
TreeSitterUtil.range(n),
uri,
parentName,
),
)
this.uriToDeclarations[uri][name] = namedDeclarations
}
})
return problems
}
}
function range(n: ASTNode): Range {
return Range.create(
n.startPosition.row,
n.startPosition.column,
n.endPosition.row,
n.endPosition.column,
)
}
/**
* Find the full word at the given point.
*/
public wordAtPoint(uri: string, line: number, column: number): string | null {
const document = this.uriToTreeSitterDocument[uri]
const contents = this.uriToFileContent[uri]
function isDefinition(n: ASTNode): boolean {
switch (n.type) {
// For now. Later we'll have a command_declaration take precedence over
// variable_assignment
case 'variable_assignment':
case 'function_definition':
return true
default:
return false
const node = document.rootNode.namedDescendantForPosition({ row: line, column })
const start = node.startIndex
const end = node.endIndex
const name = contents.slice(start, end)
// Hack. Might be a problem with the grammar.
if (name.endsWith('=')) {
return name.slice(0, name.length - 1)
}
return name
}
}
function isReference(n: ASTNode): boolean {
switch (n.type) {
case 'variable_name':
case 'command_name':
return true
default:
return false
private symbolKindToCompletionKind(s: LSP.SymbolKind): LSP.CompletionItemKind {
switch (s) {
case LSP.SymbolKind.File:
return LSP.CompletionItemKind.File
case LSP.SymbolKind.Module:
case LSP.SymbolKind.Namespace:
case LSP.SymbolKind.Package:
return LSP.CompletionItemKind.Module
case LSP.SymbolKind.Class:
return LSP.CompletionItemKind.Class
case LSP.SymbolKind.Method:
return LSP.CompletionItemKind.Method
case LSP.SymbolKind.Property:
return LSP.CompletionItemKind.Property
case LSP.SymbolKind.Field:
return LSP.CompletionItemKind.Field
case LSP.SymbolKind.Constructor:
return LSP.CompletionItemKind.Constructor
case LSP.SymbolKind.Enum:
return LSP.CompletionItemKind.Enum
case LSP.SymbolKind.Interface:
return LSP.CompletionItemKind.Interface
case LSP.SymbolKind.Function:
return LSP.CompletionItemKind.Function
case LSP.SymbolKind.Variable:
return LSP.CompletionItemKind.Variable
case LSP.SymbolKind.Constant:
return LSP.CompletionItemKind.Constant
case LSP.SymbolKind.String:
case LSP.SymbolKind.Number:
case LSP.SymbolKind.Boolean:
case LSP.SymbolKind.Array:
case LSP.SymbolKind.Key:
case LSP.SymbolKind.Null:
return LSP.CompletionItemKind.Text
case LSP.SymbolKind.Object:
return LSP.CompletionItemKind.Module
case LSP.SymbolKind.EnumMember:
return LSP.CompletionItemKind.EnumMember
case LSP.SymbolKind.Struct:
return LSP.CompletionItemKind.Struct
case LSP.SymbolKind.Event:
return LSP.CompletionItemKind.Event
case LSP.SymbolKind.Operator:
return LSP.CompletionItemKind.Operator
case LSP.SymbolKind.TypeParameter:
return LSP.CompletionItemKind.TypeParameter
default:
return LSP.CompletionItemKind.Text
}
}
}

@@ -1,173 +0,208 @@

'use strict'
// tslint:disable:no-var-requires
import * as LSP from 'vscode-languageserver'
import {
CompletionItem,
createConnection,
Definition,
DocumentHighlight,
DocumentSymbolParams,
IConnection,
InitializeResult,
Location,
ReferenceParams,
StreamMessageReader,
StreamMessageWriter,
SymbolInformation,
TextDocumentPositionParams,
TextDocuments,
} from 'vscode-languageserver'
import Analyzer from './analyser'
import * as Builtins from './builtins'
import Executables from './executables'
const glob = require('glob')
const fs = require('fs')
import * as Path from 'path'
/**
* The BashServer glues together the separate components to implement
* the various parts of the Language Server Protocol.
*/
export default class BashServer {
/**
* Initialize the server based on a connection to the client and the protocols
* initialization parameters.
*/
public static initialize(
connection: LSP.Connection,
params: LSP.InitializeParams,
): Promise<BashServer> {
return Promise.all([
Executables.fromPath(process.env.PATH),
Analyzer.fromRoot(connection, params.rootPath),
]).then(xs => {
const executables = xs[0]
const analyzer = xs[1]
return new BashServer(connection, executables, analyzer)
})
}
const pkg = require('../package')
import * as Analyser from './analyser'
private executables: Executables
private analyzer: Analyzer
export function listen() {
// Create a connection for the server.
// The connection uses stdin/stdout for communication.
const connection: IConnection = createConnection(
new StreamMessageReader(process.stdin),
new StreamMessageWriter(process.stdout),
)
private documents: LSP.TextDocuments = new LSP.TextDocuments()
private connection: LSP.Connection
// Create a simple text document manager. The text document manager
// supports full document sync only
const documents: TextDocuments = new TextDocuments()
private constructor(
connection: LSP.Connection,
executables: Executables,
analyzer: Analyzer,
) {
this.connection = connection
this.executables = executables
this.analyzer = analyzer
}
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection)
/**
* Register handlers for the events from the Language Server Protocol that we
* care about.
*/
public register(connection: LSP.Connection): void {
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
this.documents.listen(this.connection)
this.documents.onDidChangeContent(change => {
const uri = change.document.uri
const contents = change.document.getText()
const diagnostics = this.analyzer.analyze(uri, contents)
connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
})
})
connection.onInitialize((params): InitializeResult => {
connection.console.log(`Initialized server v. ${pkg.version} for ${params.rootUri}`)
// Register all the handlers for the LSP events.
connection.onHover(this.onHover.bind(this))
connection.onDefinition(this.onDefinition.bind(this))
connection.onDocumentSymbol(this.onDocumentSymbol.bind(this))
connection.onDocumentHighlight(this.onDocumentHighlight.bind(this))
connection.onReferences(this.onReferences.bind(this))
connection.onCompletion(this.onCompletion.bind(this))
connection.onCompletionResolve(this.onCompletionResolve.bind(this))
}
if (params.rootPath) {
glob('**/*.sh', { cwd: params.rootPath }, (err, paths) => {
if (err != null) {
connection.console.error(err)
} else {
paths.forEach(p => {
const absolute = Path.join(params.rootPath, p)
const uri = 'file://' + absolute
connection.console.log('Analyzing ' + uri)
Analyser.analyze(uri, fs.readFileSync(absolute, 'utf8'))
})
}
})
/**
* The parts of the Language Server Protocol that we are currently supporting.
*/
public capabilities(): LSP.ServerCapabilities {
return {
// For now we're using full-sync even though tree-sitter has great support
// for partial updates.
textDocumentSync: this.documents.syncKind,
completionProvider: {
resolveProvider: true,
},
hoverProvider: true,
documentHighlightProvider: true,
definitionProvider: true,
documentSymbolProvider: true,
referencesProvider: true,
}
}
return {
capabilities: {
// For now we're using full-sync even though tree-sitter has great support
// for partial updates.
textDocumentSync: documents.syncKind,
completionProvider: {
resolveProvider: true,
private getWordAtPoint(
params: LSP.ReferenceParams | LSP.TextDocumentPositionParams,
): string | null {
return this.analyzer.wordAtPoint(
params.textDocument.uri,
params.position.line,
params.position.character,
)
}
private onHover(pos: LSP.TextDocumentPositionParams): Promise<LSP.Hover> {
this.connection.console.log(
`Hovering over ${pos.position.line}:${pos.position.character}`,
)
const word = this.getWordAtPoint(pos)
if (Builtins.isBuiltin(word)) {
return Builtins.documentation(word).then(doc => ({
contents: {
language: 'plaintext',
value: doc,
},
documentHighlightProvider: true,
definitionProvider: true,
documentSymbolProvider: true,
referencesProvider: true,
},
}))
} else if (this.executables.isExecutableOnPATH(word)) {
return this.executables.documentation(word).then(doc => ({
contents: {
language: 'plaintext',
value: doc,
},
}))
} else {
return null
}
})
}
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
connection.console.log('Invoked onDidChangeContent')
const uri = change.document.uri
const contents = change.document.getText()
const diagnostics = Analyser.analyze(uri, contents)
connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
})
})
private onDefinition(pos: LSP.TextDocumentPositionParams): LSP.Definition {
this.connection.console.log(
`Asked for definition at ${pos.position.line}:${pos.position.character}`,
)
const word = this.getWordAtPoint(pos)
return this.analyzer.findDefinition(word)
}
connection.onDidChangeWatchedFiles(() => {
// Monitored files have change in VSCode
connection.console.log('We received an file change event')
})
private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] {
return this.analyzer.findSymbols(params.textDocument.uri)
}
connection.onDefinition(
(textDocumentPosition: TextDocumentPositionParams): Definition => {
connection.console.log(
`Asked for definition at ${textDocumentPosition.position.line}:${
textDocumentPosition.position.character
}`,
)
const word = Analyser.wordAtPoint(
textDocumentPosition.textDocument.uri,
textDocumentPosition.position.line,
textDocumentPosition.position.character,
)
return Analyser.findDefinition(word)
},
)
private onDocumentHighlight(
pos: LSP.TextDocumentPositionParams,
): LSP.DocumentHighlight[] {
const word = this.getWordAtPoint(pos)
return this.analyzer
.findOccurrences(pos.textDocument.uri, word)
.map(n => ({ range: n.range }))
}
connection.onDocumentSymbol((params: DocumentSymbolParams): SymbolInformation[] => {
return Analyser.findSymbols(params.textDocument.uri)
})
private onReferences(params: LSP.ReferenceParams): LSP.Location[] {
const word = this.getWordAtPoint(params)
return this.analyzer.findReferences(word)
}
connection.onDocumentHighlight(
(textDocumentPosition: TextDocumentPositionParams): DocumentHighlight[] => {
const word = Analyser.wordAtPoint(
textDocumentPosition.textDocument.uri,
textDocumentPosition.position.line,
textDocumentPosition.position.character,
)
return Analyser.findOccurrences(textDocumentPosition.textDocument.uri, word).map(
n => ({ range: n.range }),
)
},
)
connection.onReferences((params: ReferenceParams): Location[] => {
const word = Analyser.wordAtPoint(
params.textDocument.uri,
params.position.line,
params.position.character,
private onCompletion(pos: LSP.TextDocumentPositionParams): LSP.CompletionItem[] {
this.connection.console.log(
`Asked for completions at ${pos.position.line}:${pos.position.character}`,
)
return Analyser.findReferences(word)
})
const symbolCompletions = this.analyzer.findSymbolCompletions(pos.textDocument.uri)
connection.onCompletion(
(textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
connection.console.log(
`Asked for completions at ${textDocumentPosition.position.line}:${
textDocumentPosition.position.character
}`,
)
const symbols = Analyser.findSymbols(textDocumentPosition.textDocument.uri)
return symbols.map((s: SymbolInformation) => {
return {
label: s.name,
kind: s.kind,
data: s.name, // Used for later resolving more info.
}
})
},
)
const programCompletions = this.executables.list().map((s: string) => {
return {
label: s,
kind: LSP.SymbolKind.Function,
data: {
name: s,
type: 'executable',
},
}
})
// This handler resolve additional information for the item selected in
// the completion list.
connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
// TODO: Look up man pages for commands
// TODO: For builtins look up the docs.
// TODO: For functions, parse their comments?
const builtinsCompletions = Builtins.LIST.map(builtin => ({
label: builtin,
kind: LSP.SymbolKind.Method, // ??
data: {
name: builtin,
type: 'builtin',
},
}))
// if (item.data === 1) {
// item.detail = 'TypeScript details',
// item.documentation = 'TypeScript documentation'
// }
return [...symbolCompletions, ...programCompletions, ...builtinsCompletions]
}
return item
})
// Listen on the connection
connection.listen()
private async onCompletionResolve(
item: LSP.CompletionItem,
): Promise<LSP.CompletionItem> {
const { data: { name, type } } = item
try {
if (type === 'executable') {
const doc = await this.executables.documentation(name)
return {
...item,
detail: doc,
}
} else if (type === 'builtin') {
const doc = await Builtins.documentation(name)
return {
...item,
detail: doc,
}
} else {
return item
}
} catch (error) {
return item
}
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet