Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

bash-language-server

Package Overview
Dependencies
Maintainers
1
Versions
107
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 1.1.2 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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc