bash-language-server
Advanced tools
Comparing version 1.1.2 to 1.3.0
#!/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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
108583
47
1635
4
2
+ Addedtree-sitter@0.11.2(transitive)
+ Addedtree-sitter-bash@0.11.2(transitive)
+ Addedvscode-jsonrpc@8.2.0(transitive)
+ Addedvscode-languageserver@4.4.2(transitive)
+ Addedvscode-languageserver-protocol@3.17.5(transitive)
+ Addedvscode-languageserver-types@3.17.5(transitive)
- Removedtree-sitter@0.10.0(transitive)
- Removedtree-sitter-bash@0.6.0(transitive)
- Removedvscode-jsonrpc@3.5.0(transitive)
- Removedvscode-languageserver@3.5.1(transitive)
- Removedvscode-languageserver-protocol@3.5.1(transitive)
- Removedvscode-languageserver-types@3.5.0(transitive)
Updatedtree-sitter@^0.11.0
Updatedtree-sitter-bash@^0.11.0
Updatedvscode-languageserver@^4.1.1