New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

bash-language-server

Package Overview
Dependencies
Maintainers
2
Versions
108
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 5.0.0 to 5.1.0

src/__tests__/__snapshots__/server.test.ts.snap

4

CHANGELOG.md
# Bash Language Server
## 5.1.0
- Support for renaming symbol! https://github.com/bash-lsp/bash-language-server/pull/915
## 5.0.0

@@ -4,0 +8,0 @@

import * as LSP from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as Parser from 'web-tree-sitter';
import { FindDeclarationParams } from './util/declarations';
/**

@@ -63,2 +64,10 @@ * The Analyzer uses the Abstract Syntax Trees (ASTs) that are provided by

/**
* Find a symbol's original declaration and parent scope based on its original
* definition with respect to its scope.
*/
findOriginalDeclaration(params: FindDeclarationParams['symbolInfo']): {
declaration: LSP.Location | null;
parent: LSP.Location | null;
};
/**
* Find all the locations where the given word was defined or referenced.

@@ -81,2 +90,13 @@ * This will include commands, functions, variables, etc.

findOccurrences(uri: string, word: string): LSP.Location[];
/**
* A more scope-aware version of findOccurrences that differentiates between
* functions and variables.
*/
findOccurrencesWithin({ uri, word, kind, start, scope, }: {
uri: string;
word: string;
kind: LSP.SymbolKind;
start?: LSP.Position;
scope?: LSP.Range;
}): LSP.Range[];
getAllVariables({ position, uri, }: {

@@ -117,5 +137,16 @@ position: LSP.Position;

wordAtPointFromTextPosition(params: LSP.TextDocumentPositionParams): string | null;
symbolAtPointFromTextPosition(params: LSP.TextDocumentPositionParams): {
word: string;
range: LSP.Range;
kind: LSP.SymbolKind;
} | null;
setEnableSourceErrorDiagnostics(enableSourceErrorDiagnostics: boolean): void;
setIncludeAllWorkspaceSymbols(includeAllWorkspaceSymbols: boolean): void;
/**
* If includeAllWorkspaceSymbols is true, this returns all URIs from the
* background analysis, else, it returns the URIs of the files that are
* linked to `uri` via sourcing.
*/
findAllLinkedUris(uri: string): string[];
/**
* Returns all reachable URIs from the given URI based on sourced commands

@@ -126,2 +157,10 @@ * If no URI is given, all URIs from the background analysis are returned.

private getReachableUris;
/**
* Returns all reachable URIs from `fromUri` based on source commands in
* descending order starting from the top of the sourcing tree, this list
* includes `fromUri`. If includeAllWorkspaceSymbols is true, other URIs from
* the background analysis are also included after the ordered URIs in no
* particular order.
*/
private getOrderedReachableUris;
private getAnalyzedReachableUris;

@@ -141,5 +180,13 @@ private ensureUrisAreAnalyzed;

/**
* Returns the parent `subshell` or `function_definition` of the given `node`.
* To disambiguate between regular `subshell`s and `subshell`s that serve as a
* `function_definition`'s body, this only returns a `function_definition` if
* its body is a `compound_statement`.
*/
private parentScope;
/**
* Find the node at the given point.
*/
private nodeAtPoint;
private nodeAtPoints;
}

@@ -186,2 +186,82 @@ "use strict";

/**
* Find a symbol's original declaration and parent scope based on its original
* definition with respect to its scope.
*/
findOriginalDeclaration(params) {
var _a;
const node = this.nodeAtPoint(params.uri, params.position.line, params.position.character);
if (!node) {
return { declaration: null, parent: null };
}
const otherInfo = {
currentUri: params.uri,
boundary: params.position.line,
};
let parent = this.parentScope(node);
let declaration;
let continueSearching = false;
// Search for local declaration within parents
while (parent) {
if (params.kind === LSP.SymbolKind.Variable &&
parent.type === 'function_definition' &&
parent.lastChild) {
;
({ declaration, continueSearching } = (0, declarations_1.findDeclarationUsingLocalSemantics)({
baseNode: parent.lastChild,
symbolInfo: params,
otherInfo,
}));
}
else if (parent.type === 'subshell') {
;
({ declaration, continueSearching } = (0, declarations_1.findDeclarationUsingGlobalSemantics)({
baseNode: parent,
symbolInfo: params,
otherInfo,
}));
}
if (declaration && !continueSearching) {
break;
}
// Update boundary since any other instance within or below the current
// parent can now be considered local to that parent or out of scope.
otherInfo.boundary = parent.startPosition.row;
parent = this.parentScope(parent);
}
// Search for global declaration within files
if (!parent && (!declaration || continueSearching)) {
for (const uri of this.getOrderedReachableUris({ fromUri: params.uri })) {
const root = (_a = this.uriToAnalyzedDocument[uri]) === null || _a === void 0 ? void 0 : _a.tree.rootNode;
if (!root) {
continue;
}
otherInfo.currentUri = uri;
otherInfo.boundary =
uri === params.uri
? // Reset boundary so globally defined variables within any
// functions already searched can be found.
params.position.line
: // Set boundary to EOF since any position taken from the original
// URI/file does not apply to other URIs/files.
root.endPosition.row;
({ declaration, continueSearching } = (0, declarations_1.findDeclarationUsingGlobalSemantics)({
baseNode: root,
symbolInfo: params,
otherInfo,
}));
if (declaration && !continueSearching) {
break;
}
}
}
return {
declaration: declaration
? LSP.Location.create(otherInfo.currentUri, TreeSitterUtil.range(declaration))
: null,
parent: parent
? LSP.Location.create(params.uri, TreeSitterUtil.range(parent))
: null,
};
}
/**
* Find all the locations where the given word was defined or referenced.

@@ -234,2 +314,92 @@ * This will include commands, functions, variables, etc.

}
/**
* A more scope-aware version of findOccurrences that differentiates between
* functions and variables.
*/
findOccurrencesWithin({ uri, word, kind, start, scope, }) {
var _a;
const scopeNode = scope
? this.nodeAtPoints(uri, { row: scope.start.line, column: scope.start.character }, { row: scope.end.line, column: scope.end.character })
: null;
const baseNode = scopeNode && (kind === LSP.SymbolKind.Variable || scopeNode.type === 'subshell')
? scopeNode
: (_a = this.uriToAnalyzedDocument[uri]) === null || _a === void 0 ? void 0 : _a.tree.rootNode;
if (!baseNode) {
return [];
}
const typeOfDescendants = kind === LSP.SymbolKind.Variable
? 'variable_name'
: ['function_definition', 'command_name'];
const startPosition = start
? { row: start.line, column: start.character }
: baseNode.startPosition;
const ignoredRanges = [];
const filterVariables = (n) => {
var _a;
if (n.text !== word) {
return false;
}
const definition = TreeSitterUtil.findParentOfType(n, 'variable_assignment');
const definedVariable = definition === null || definition === void 0 ? void 0 : definition.descendantsOfType('variable_name').at(0);
// For self-assignment `var=$var` cases; this decides whether `$var` is an
// occurrence or not.
if ((definedVariable === null || definedVariable === void 0 ? void 0 : definedVariable.text) === word && !n.equals(definedVariable)) {
// `start.line` is assumed to be the same as the variable's original
// declaration line; handles cases where `$var` shouldn't be considered
// an occurrence.
if ((definition === null || definition === void 0 ? void 0 : definition.startPosition.row) === (start === null || start === void 0 ? void 0 : start.line)) {
return false;
}
// Returning true here is a good enough heuristic for most cases. It
// breaks down when redeclaration happens in multiple nested scopes,
// handling those more complex situations can be done later on if use
// cases arise.
return true;
}
const parent = this.parentScope(n);
if (!parent || baseNode.equals(parent)) {
return true;
}
const includeDeclaration = !ignoredRanges.some((r) => n.startPosition.row > r.start.line && n.endPosition.row < r.end.line);
const declarationCommand = TreeSitterUtil.findParentOfType(n, 'declaration_command');
const isLocal = ((definedVariable === null || definedVariable === void 0 ? void 0 : definedVariable.text) === word || !!(!definition && declarationCommand)) &&
(parent.type === 'subshell' ||
['local', 'declare', 'typeset'].includes((_a = declarationCommand === null || declarationCommand === void 0 ? void 0 : declarationCommand.firstChild) === null || _a === void 0 ? void 0 : _a.text));
if (isLocal) {
if (includeDeclaration) {
ignoredRanges.push(TreeSitterUtil.range(parent));
}
return false;
}
return includeDeclaration;
};
const filterFunctions = (n) => {
var _a;
const text = n.type === 'function_definition' ? (_a = n.firstNamedChild) === null || _a === void 0 ? void 0 : _a.text : n.text;
if (text !== word) {
return false;
}
const parentScope = TreeSitterUtil.findParentOfType(n, 'subshell');
if (!parentScope || baseNode.equals(parentScope)) {
return true;
}
const includeDeclaration = !ignoredRanges.some((r) => n.startPosition.row > r.start.line && n.endPosition.row < r.end.line);
if (n.type === 'function_definition') {
if (includeDeclaration) {
ignoredRanges.push(TreeSitterUtil.range(parentScope));
}
return false;
}
return includeDeclaration;
};
return baseNode
.descendantsOfType(typeOfDescendants, startPosition)
.filter(kind === LSP.SymbolKind.Variable ? filterVariables : filterFunctions)
.map((n) => {
if (n.type === 'function_definition' && n.firstNamedChild) {
return TreeSitterUtil.range(n.firstNamedChild);
}
return TreeSitterUtil.range(n);
});
}
getAllVariables({ position, uri, }) {

@@ -373,2 +543,21 @@ return this.getAllDeclarations({ uri, position }).filter((symbol) => symbol.kind === LSP.SymbolKind.Variable);

}
symbolAtPointFromTextPosition(params) {
var _a;
const node = this.nodeAtPoint(params.textDocument.uri, params.position.line, params.position.character);
if (!node) {
return null;
}
if (node.type === 'variable_name' ||
(node.type === 'word' &&
['function_definition', 'command_name'].includes((_a = node.parent) === null || _a === void 0 ? void 0 : _a.type))) {
return {
word: node.text,
range: TreeSitterUtil.range(node),
kind: node.type === 'variable_name'
? LSP.SymbolKind.Variable
: LSP.SymbolKind.Function,
};
}
return null;
}
setEnableSourceErrorDiagnostics(enableSourceErrorDiagnostics) {

@@ -380,2 +569,32 @@ this.enableSourceErrorDiagnostics = enableSourceErrorDiagnostics;

}
/**
* If includeAllWorkspaceSymbols is true, this returns all URIs from the
* background analysis, else, it returns the URIs of the files that are
* linked to `uri` via sourcing.
*/
findAllLinkedUris(uri) {
if (this.includeAllWorkspaceSymbols) {
return Object.keys(this.uriToAnalyzedDocument).filter((u) => u !== uri);
}
const uriToAnalyzedDocument = Object.entries(this.uriToAnalyzedDocument);
const uris = [];
let continueSearching = true;
while (continueSearching) {
continueSearching = false;
for (const [analyzedUri, analyzedDocument] of uriToAnalyzedDocument) {
if (!analyzedDocument) {
continue;
}
for (const sourcedUri of analyzedDocument.sourcedUris.values()) {
if ((sourcedUri === uri || uris.includes(sourcedUri)) &&
!uris.includes(analyzedUri)) {
uris.push(analyzedUri);
continueSearching = true;
break;
}
}
}
}
return uris;
}
// Private methods

@@ -402,2 +621,27 @@ /**

}
/**
* Returns all reachable URIs from `fromUri` based on source commands in
* descending order starting from the top of the sourcing tree, this list
* includes `fromUri`. If includeAllWorkspaceSymbols is true, other URIs from
* the background analysis are also included after the ordered URIs in no
* particular order.
*/
getOrderedReachableUris({ fromUri }) {
let uris = this.findAllSourcedUris({ uri: fromUri });
for (const u1 of uris) {
for (const u2 of this.findAllSourcedUris({ uri: u1 })) {
if (uris.has(u2)) {
uris.delete(u2);
uris.add(u2);
}
}
}
uris = Array.from(uris);
uris.reverse();
uris.push(fromUri);
if (this.includeAllWorkspaceSymbols) {
uris.push(...Object.keys(this.uriToAnalyzedDocument).filter((u) => !uris.includes(u)));
}
return uris;
}
getAnalyzedReachableUris({ fromUri } = {}) {

@@ -497,2 +741,15 @@ return this.ensureUrisAreAnalyzed(this.getReachableUris({ fromUri }));

/**
* Returns the parent `subshell` or `function_definition` of the given `node`.
* To disambiguate between regular `subshell`s and `subshell`s that serve as a
* `function_definition`'s body, this only returns a `function_definition` if
* its body is a `compound_statement`.
*/
parentScope(node) {
return TreeSitterUtil.findParent(node, (n) => {
var _a;
return n.type === 'subshell' ||
(n.type === 'function_definition' && ((_a = n.lastChild) === null || _a === void 0 ? void 0 : _a.type) === 'compound_statement');
});
}
/**
* Find the node at the given point.

@@ -509,4 +766,12 @@ */

}
nodeAtPoints(uri, start, end) {
var _a;
const rootNode = (_a = this.uriToAnalyzedDocument[uri]) === null || _a === void 0 ? void 0 : _a.tree.rootNode;
if (!rootNode) {
return null;
}
return rootNode.descendantForPosition(start, end);
}
}
exports.default = Analyzer;
//# sourceMappingURL=analyser.js.map

4

out/config.d.ts

@@ -17,3 +17,3 @@ import { z } from 'zod';

explainshellEndpoint: string;
logLevel: "error" | "warning" | "debug" | "info";
logLevel: "debug" | "info" | "warning" | "error";
shellcheckArguments: string[];

@@ -26,3 +26,3 @@ shellcheckPath: string;

explainshellEndpoint?: string | undefined;
logLevel?: "error" | "warning" | "debug" | "info" | undefined;
logLevel?: "debug" | "info" | "warning" | "error" | undefined;
includeAllWorkspaceSymbols?: boolean | undefined;

@@ -29,0 +29,0 @@ shellcheckArguments?: unknown;

@@ -18,3 +18,3 @@ "use strict";

logLevel: zod_1.z.enum(logger_1.LOG_LEVELS).default(logger_1.DEFAULT_LOG_LEVEL),
// Controls how symbols (e.g. variables and functions) are included and used for completion and documentation.
// Controls how symbols (e.g. variables and functions) are included and used for completion, documentation, and renaming.
// If false, then we only include symbols from sourced files (i.e. using non dynamic statements like 'source file.sh' or '. file.sh' or following ShellCheck directives).

@@ -21,0 +21,0 @@ // If true, then all symbols from the workspace are included.

@@ -41,2 +41,3 @@ import * as LSP from 'vscode-languageserver/node';

private getDocumentationForSymbol;
private throwResponseError;
private onCodeAction;

@@ -51,3 +52,5 @@ private onCompletion;

private onWorkspaceSymbol;
private onPrepareRename;
private onRenameRequest;
}
export declare function getCommandOptions(name: string, word: string): string[];

@@ -104,2 +104,3 @@ "use strict";

},
renameProvider: { prepareProvider: true },
};

@@ -139,2 +140,4 @@ }

connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this));
connection.onPrepareRename(this.onPrepareRename.bind(this));
connection.onRenameRequest(this.onRenameRequest.bind(this));
/**

@@ -292,2 +295,5 @@ * The initialized notification is sent from the client to the server after

}
throwResponseError(message, code = LSP.LSPErrorCodes.RequestFailed) {
throw new LSP.ResponseError(code, message);
}
// ==============================

@@ -556,2 +562,70 @@ // Language server event handlers

}
onPrepareRename(params) {
const symbol = this.analyzer.symbolAtPointFromTextPosition(params);
this.logRequest({ request: 'onPrepareRename', params, word: symbol === null || symbol === void 0 ? void 0 : symbol.word });
if (!symbol ||
(symbol.kind === LSP.SymbolKind.Variable &&
(symbol.word === '_' || !/^[a-z_][\w]*$/i.test(symbol.word)))) {
return null;
}
return symbol.range;
}
onRenameRequest(params) {
const symbol = this.analyzer.symbolAtPointFromTextPosition(params);
this.logRequest({ request: 'onRenameRequest', params, word: symbol === null || symbol === void 0 ? void 0 : symbol.word });
if (!symbol) {
return null;
}
if (symbol.kind === LSP.SymbolKind.Variable &&
(params.newName === '_' || !/^[a-z_][\w]*$/i.test(params.newName))) {
this.throwResponseError('Invalid variable name given.');
}
if (symbol.kind === LSP.SymbolKind.Function && params.newName.includes('$')) {
this.throwResponseError('Invalid function name given.');
}
const { declaration, parent } = this.analyzer.findOriginalDeclaration({
position: params.position,
uri: params.textDocument.uri,
word: symbol.word,
kind: symbol.kind,
});
// File-wide rename
if (!declaration || parent) {
return {
changes: {
[params.textDocument.uri]: this.analyzer
.findOccurrencesWithin({
uri: params.textDocument.uri,
word: symbol.word,
kind: symbol.kind,
start: declaration === null || declaration === void 0 ? void 0 : declaration.range.start,
scope: parent === null || parent === void 0 ? void 0 : parent.range,
})
.map((r) => LSP.TextEdit.replace(r, params.newName)),
},
};
}
// Workspace-wide rename
const edits = {};
edits.changes = {
[declaration.uri]: this.analyzer
.findOccurrencesWithin({
uri: declaration.uri,
word: symbol.word,
kind: symbol.kind,
start: declaration.range.start,
})
.map((r) => LSP.TextEdit.replace(r, params.newName)),
};
for (const uri of this.analyzer.findAllLinkedUris(declaration.uri)) {
edits.changes[uri] = this.analyzer
.findOccurrencesWithin({
uri,
word: symbol.word,
kind: symbol.kind,
})
.map((r) => LSP.TextEdit.replace(r, params.newName));
}
return edits;
}
}

@@ -558,0 +632,0 @@ exports.default = BashServer;

@@ -11,4 +11,4 @@ import { z } from 'zod';

}, "strip", z.ZodTypeAny, {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -20,4 +20,4 @@ column: number;

}, {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -50,4 +50,4 @@ column: number;

}, "strip", z.ZodTypeAny, {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -59,4 +59,4 @@ column: number;

}, {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -70,4 +70,4 @@ column: number;

replacements: {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -81,4 +81,4 @@ column: number;

replacements: {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -92,5 +92,4 @@ column: number;

}, "strip", z.ZodTypeAny, {
code: number;
message: string;
code: number;
file: string;
line: number;

@@ -100,7 +99,8 @@ endLine: number;

endColumn: number;
level: "error" | "warning" | "style" | "info";
file: string;
level: "info" | "warning" | "error" | "style";
fix: {
replacements: {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -114,5 +114,4 @@ column: number;

}, {
code: number;
message: string;
code: number;
file: string;
line: number;

@@ -122,7 +121,8 @@ endLine: number;

endColumn: number;
level: "error" | "warning" | "style" | "info";
file: string;
level: "info" | "warning" | "error" | "style";
fix: {
replacements: {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -138,5 +138,4 @@ column: number;

comments: {
code: number;
message: string;
code: number;
file: string;
line: number;

@@ -146,7 +145,8 @@ endLine: number;

endColumn: number;
level: "error" | "warning" | "style" | "info";
file: string;
level: "info" | "warning" | "error" | "style";
fix: {
replacements: {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -162,5 +162,4 @@ column: number;

comments: {
code: number;
message: string;
code: number;
file: string;
line: number;

@@ -170,7 +169,8 @@ endLine: number;

endColumn: number;
level: "error" | "warning" | "style" | "info";
file: string;
level: "info" | "warning" | "error" | "style";
fix: {
replacements: {
precedence: number;
line: number;
precedence: number;
endLine: number;

@@ -177,0 +177,0 @@ column: number;

@@ -21,6 +21,6 @@ "use strict";

file: zod_1.z.string(),
line: zod_1.z.number(),
endLine: zod_1.z.number(),
column: zod_1.z.number(),
endColumn: zod_1.z.number(),
line: zod_1.z.number(), // 1-based
endLine: zod_1.z.number(), // 1-based
column: zod_1.z.number(), // 1-based
endColumn: zod_1.z.number(), // 1-based
level: LevelSchema,

@@ -27,0 +27,0 @@ code: zod_1.z.number(),

@@ -7,6 +7,4 @@ "use strict";

* - for Bash operators it's '<operator> operator'
* - for Bash parameter expansions it's '<expansion> expansion'
* - for Bash documentation it's 'documentation definition' or '"<documentation>" documentation definition'
* - for Bash functions it's 'function definition' or '"<function>" function definition'
* - for Bash builtins it's '"<builtin>" invocation'
* - for Bash character classes it's any string with optional mnemonics depicted via square brackets

@@ -17,3 +15,3 @@ * - for shell shebang it's 'shebang'

* Naming convention for `label`:
* - for shell shebang it's 'shebang'
* - for shell shebang it's 'shebang' or 'shebang-with-arguments'
* - for Bash operators it's '<operator>[<nested-operator>]', where:

@@ -29,9 +27,4 @@ * - <operator> is Bash operator

* - term delimiter: dash, like 'set-if-unset-or-null'
* - for Bash brace expansion it's 'range'
* - for Bash documentation it's one of 'documentation'/'<documentation>'
* - for Bash functions it's one of 'function'/'<function>'
* - for Bash builtins it's '<builtin>'
* - for Bash character classes it's '<character-class>'
* - for Sed it's 'sed:<expression>'
* - for Awk it's 'awk:<expression>'
* - for anything else it's any string

@@ -48,17 +41,38 @@ */

{
documentation: 'shebang-with-arguments',
label: 'shebang-with-arguments',
insertText: '#!/usr/bin/env ${1|-S,--split-string|} ${2|bash,sh|} ${3|argument ...|}',
},
{
label: 'and',
documentation: 'and operator',
insertText: '${1:first-expression} && ${2:second-expression}',
},
{
label: 'or',
documentation: 'or operator',
insertText: '${1:first-expression} || ${2:second-expression}',
},
{
label: 'if',
documentation: 'if operator',
label: 'if',
insertText: ['if ${1:command}; then', '\t$0', 'fi'].join('\n'),
insertText: ['if ${1:condition}; then', '\t${2:command ...}', 'fi'].join('\n'),
},
{
documentation: 'if else operator',
label: 'if-else',
insertText: ['if ${1:command}; then', '\t${2:echo}', 'else', '\t$0', 'fi'].join('\n'),
documentation: 'if-else operator',
insertText: [
'if ${1:condition}; then',
'\t${2:command ...}',
'else',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
documentation: 'if operator',
label: 'if-test',
label: 'if-less',
documentation: 'if with number comparison',
insertText: [
'if [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; then',
'\t$0',
'if (( "${1:first-expression}" < "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',

@@ -68,8 +82,7 @@ ].join('\n'),

{
documentation: 'if else operator',
label: 'if-else-test',
label: 'if-greater',
documentation: 'if with number comparison',
insertText: [
'if [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; then',
'else',
'\t$0',
'if (( "${1:first-expression}" > "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',

@@ -79,12 +92,88 @@ ].join('\n'),

{
label: 'if-less-or-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" <= "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-greater-or-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" >= "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" == "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-not-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" != "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-string-equal',
documentation: 'if with string comparison',
insertText: [
'if [[ "${1:first-expression}" == "${2:second-expression}" ]]; then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-string-not-equal',
documentation: 'if with string comparison',
insertText: [
'if [[ "${1:first-expression}" != "${2:second-expression}" ]]; then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-string-empty',
documentation: 'if with string comparison (has [z]ero length)',
insertText: ['if [[ -z "${1:expression}" ]]; then', '\t${2:command ...}', 'fi'].join('\n'),
},
{
label: 'if-string-not-empty',
documentation: 'if with string comparison ([n]ot empty)',
insertText: ['if [[ -n "${1:expression}" ]]; then', '\t${2:command ...}', 'fi'].join('\n'),
},
{
label: 'if-defined',
documentation: 'if with variable existence check',
insertText: ['if [[ -n "${${1:variable}+x}" ]]', '\t${2:command ...}', 'fi'].join('\n'),
},
{
label: 'if-not-defined',
documentation: 'if with variable existence check',
insertText: ['if [[ -z "${${1:variable}+x}" ]]', '\t${2:command ...}', 'fi'].join('\n'),
},
{
label: 'while',
documentation: 'while operator',
label: 'while',
insertText: ['while ${1:command}; do', '\t$0', 'done'].join('\n'),
insertText: ['while ${1:condition}; do', '\t${2:command ...}', 'done'].join('\n'),
},
{
documentation: 'while operator',
label: 'while-test',
label: 'while-else',
documentation: 'while-else operator',
insertText: [
'while [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; do',
'\t$0',
'while ${1:condition}; do',
'\t${2:command ...}',
'else',
'\t${3:command ...}',
'done',

@@ -94,12 +183,16 @@ ].join('\n'),

{
documentation: 'until operator',
label: 'until',
insertText: ['until ${1:command}; do', '\t$0', 'done'].join('\n'),
label: 'while-less',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" < "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'until operator',
label: 'until-test',
label: 'while-greater',
documentation: 'while with number comparison',
insertText: [
'until [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; do',
'\t$0',
'while (( "${1:first-expression}" > "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',

@@ -109,316 +202,457 @@ ].join('\n'),

{
documentation: 'for operator',
label: 'for',
insertText: ['for ${1:variable} in ${2:list}; do', '\t$0', 'done'].join('\n'),
label: 'while-less-or-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" <= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'for operator',
label: 'for.range',
insertText: ['for ${1:variable} in $(seq ${2:to}); do', '\t$0', 'done'].join('\n'),
label: 'while-greater-or-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" >= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'for operator',
label: 'for.file',
insertText: ['for ${1:variable} in *; do', '\t$0', 'done'].join('\n'),
label: 'while-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" == "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'for operator',
label: 'for.directory',
insertText: ['for ${1:variable} in */; do', '\t$0', 'done'].join('\n'),
label: 'while-not-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" != "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'function definition',
label: 'function',
insertText: ['${1:function_name}() {', '\t$0', '}'].join('\n'),
label: 'while-string-equal',
documentation: 'while with string comparison',
insertText: [
'while [[ "${1:first-expression}" == "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '"main" function definition',
label: 'main',
insertText: ['main() {', '\t$0', '}'].join('\n'),
label: 'while-string-not-equal',
documentation: 'while with string comparison',
insertText: [
'while [[ "${1:first-expression}" != "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'documentation definition',
label: 'documentation',
label: 'while-string-empty',
documentation: 'while with string comparison (has [z]ero length)',
insertText: [
'# ${1:function_name} ${2:function_parameters}',
'# ${3:function_description}',
'#',
'# Output:',
'# ${4:function_output}',
'#',
'# Return:',
'# - ${5:0} when ${6:all parameters are correct}',
'# - ${7:1} ${8:otherwise}',
'while [[ -z "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: ':- expansion',
label: 'if-unset-or-null',
insertText: '"\\${${1:variable}:-${2:default}}"',
label: 'while-string-not-empty',
documentation: 'while with string comparison ([n]ot empty)',
insertText: [
'while [[ -n "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '- expansion',
label: 'if-unset',
insertText: '"\\${${1:variable}-${2:default}}"',
label: 'while-defined',
documentation: 'while with variable existence check',
insertText: [
'while [[ -n "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: ':= expansion',
label: 'set-if-unset-or-null',
insertText: '"\\${${1:variable}:=${2:default}}"',
label: 'while-not-defined',
documentation: 'while with variable existence check',
insertText: [
'while [[ -z "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '= expansion',
label: 'set-if-unset',
insertText: '"\\${${1:variable}=${2:default}}"',
label: 'until',
documentation: 'until operator',
insertText: ['until ${1:condition}; do', '\t${2:command ...}', 'done'].join('\n'),
},
{
documentation: ':? expansion',
label: 'error-if-unset-or-null',
insertText: '"\\${${1:variable}:?${2:error_message}}"',
label: 'until-else',
documentation: 'until-else operator',
insertText: [
'until ${1:condition}; do',
'\t${2:command ...}',
'else',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '? expansion',
label: 'error-if-unset',
insertText: '"\\${${1:variable}?${2:error_message}}"',
label: 'until-less',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" < "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: ':+ expansion',
label: 'if-set-or-not-null',
insertText: '"\\${${1:variable}:+${2:alternative}}"',
label: 'until-greater',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" > "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '+ expansion',
label: 'if-set',
insertText: '"\\${${1:variable}+${2:alternative}}"',
label: 'until-less-or-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" <= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '# expansion',
label: 'without-shortest-leading-pattern',
insertText: '"\\${${1:variable}#${2:pattern}}"',
label: 'until-greater-or-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" >= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '## expansion',
label: 'without-longest-leading-pattern',
insertText: '"\\${${1:variable}##${2:pattern}}"',
label: 'until-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" == "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '% expansion',
label: 'without-shortest-trailing-pattern',
insertText: '"\\${${1:variable}%${2:pattern}}"',
label: 'until-not-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" != "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '%% expansion',
label: 'without-longest-trailing-pattern',
insertText: '"\\${${1:variable}%%${2:pattern}}"',
label: 'until-string-equal',
documentation: 'until with string comparison',
insertText: [
'until [[ "${1:first-expression}" == "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '.. expansion',
label: 'range',
insertText: '{${1:from}..${2:to}}',
label: 'until-string-not-equal',
documentation: 'until with string comparison',
insertText: [
'until [[ "${1:first-expression}" != "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '"echo" invocation',
label: 'echo',
insertText: 'echo "${1:message}"',
label: 'until-string-empty',
documentation: 'until with string comparison (has [z]ero length)',
insertText: [
'until [[ -z "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"printf" invocation',
label: 'printf',
insertText: 'printf \'${1|%c,%s,%d,%f,%15c,%15s,%15d,%15f,%.5s,%.5d,%.5f|}\' "${2:message}"',
label: 'until-string-not-empty',
documentation: 'until with string comparison ([n]ot empty)',
insertText: [
'until [[ -n "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"source" invocation',
label: 'source',
insertText: '${1|source,.|} "${2:path/to/file}"',
label: 'until-defined',
documentation: 'until with variable existence check',
insertText: [
'until [[ -n "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"alias" invocation',
label: 'alias',
insertText: 'alias ${1:name}=${2:value}',
label: 'until-not-defined',
documentation: 'until with variable existence check',
insertText: [
'until [[ -z "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"cd" invocation',
label: 'cd',
insertText: 'cd "${1:path/to/directory}"',
label: 'for',
documentation: 'for operator',
insertText: [
'for ${1:item} in ${2:expression}; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '"getopts" invocation',
label: 'getopts',
insertText: 'getopts ${1:optstring} ${2:name}',
label: 'for-range',
documentation: 'for with range',
insertText: [
'for ${1:item} in $(seq ${2:from} ${3:to}); do',
'\t${4:command ...}',
'done',
].join('\n'),
},
{
documentation: '"jobs" invocation',
label: 'jobs',
insertText: 'jobs -x ${1:command}',
label: 'for-stepped-range',
documentation: 'for with stepped range',
insertText: [
'for ${1:item} in $(seq ${2:from} ${3:step} ${4:to}); do',
'\t${5:command ...}',
'done',
].join('\n'),
},
{
documentation: '"kill" invocation',
label: 'kill',
insertText: 'kill ${1|-l,-L|}',
label: 'for-files',
documentation: 'for with files',
insertText: [
'for ${1:item} in *.${2:extension}; do',
'\t${4:command ...}',
'done',
].join('\n'),
},
{
documentation: '"let" invocation',
label: 'let',
insertText: 'let ${1:argument}',
label: 'case',
documentation: 'case operator',
insertText: [
'case "${1:expression}" in',
'\t${2:pattern})',
'\t\t${3:command ...}',
'\t\t;;',
'\t*)',
'\t\t${4:command ...}',
'\t\t;;',
'end',
].join('\n'),
},
{
documentation: '"test" invocation',
label: 'test',
insertText: '[[ ${1:argument1} ${2|-ef,-nt,-ot,==,=,!=,=~,<,>,-eq,-ne,-lt,-le,-gt,-ge|} ${3:argument2} ]]',
label: 'function',
documentation: 'function definition',
insertText: ['${1:name}() {', '\t${2:command ...}', '}'].join('\n'),
},
{
documentation: 'line print',
label: 'sed:print',
insertText: "sed '' ${1:path/to/file}",
documentation: 'documentation definition',
label: 'documentation',
insertText: [
'# ${1:function_name} ${2:function_parameters}',
'# ${3:function_description}',
'#',
'# Output:',
'# ${4:function_output}',
'#',
'# Return:',
'# - ${5:0} when ${6:all parameters are correct}',
'# - ${7:1} ${8:otherwise}',
].join('\n'),
},
{
documentation: 'line pattern filter',
label: 'sed:filter-by-line-pattern',
insertText: "sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '/${3:pattern}/p' ${4:path/to/file}",
documentation: 'block',
label: 'block',
insertText: ['{', '\t${1:command ...}', '}'].join('\n'),
},
{
documentation: 'line number filter',
label: 'sed:filter-by-line-number',
insertText: "sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '${3:number}p' ${4:path/to/file}",
documentation: 'block redirected',
label: 'block-redirected',
insertText: ['{', '\t${1:command ...}', '} > ${2:file}'].join('\n'),
},
{
documentation: 'line number filter',
label: 'sed:filter-by-line-numbers',
insertText: "sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '${3:from},${4:to}p' ${5:path/to/file}",
documentation: 'block stderr redirected',
label: 'block-stderr-redirected',
insertText: ['{', '\t${1:command ...}', '} 2> ${2:file}'].join('\n'),
},
{
documentation: 'single replacement',
label: 'sed:replace-first',
insertText: "sed ${1|--regexp-extended,-E|} 's/${2:pattern}/${3:replacement}/' ${4:path/to/file}",
documentation: 'variable',
label: 'variable',
insertText: 'declare ${1:variable}=${2:value}',
},
{
documentation: 'global replacement',
label: 'sed:replace-all',
insertText: "sed ${1|--regexp-extended,-E|} 's/${2:pattern}/${3:replacement}/g' ${4:path/to/file}",
documentation: 'variable index',
label: 'variable-index',
insertText: '${1:variable}[${2:index}]=${3:value}',
},
{
documentation: 'transliteration',
label: 'sed:transliterate',
insertText: "sed ${1|--regexp-extended,-E|} 'y/${2:source-characters}/${3:replacement-characters}/g' ${4:path/to/file}",
documentation: 'variable append',
label: 'variable-append',
insertText: '${1:variable}+=${2:value}',
},
{
documentation: 'whole file read',
label: 'sed:read-all',
insertText: "sed ${1|--regexp-extended,-E|} ':${2:x} N $! b$2 ${3:command}' ${4:path/to/file}",
documentation: 'variable-prepend',
label: 'variable-prepend',
insertText: '${1:variable}=${2:value}\\$$1',
},
{
documentation: 'line print',
label: 'awk:print',
insertText: "awk '/./' ${1:path/to/file}",
documentation: 'if unset or null',
label: 'if-unset-or-null',
insertText: '"\\${${1:variable}:-${2:default}}"',
},
{
documentation: 'line pattern filter',
label: 'awk:filter-by-line-pattern',
insertText: "awk '/${1:pattern}/' ${2:path/to/file}",
documentation: 'if unset',
label: 'if-unset',
insertText: '"\\${${1:variable}-${2:default}}"',
},
{
documentation: 'line number filter',
label: 'awk:filter-by-line-number',
insertText: "awk 'NR == ${1:number}' ${2:path/to/file}",
documentation: 'set if unset or null',
label: 'set-if-unset-or-null',
insertText: '"\\${${1:variable}:=${2:default}}"',
},
{
documentation: 'line number filter',
label: 'awk:filter-by-line-numbers',
insertText: "awk 'NR >= ${1:from} && NR <= ${2:to}' ${3:path/to/file}",
documentation: 'set if unset',
label: 'set-if-unset',
insertText: '"\\${${1:variable}=${2:default}}"',
},
{
documentation: 'single replacement',
label: 'awk:replace-first',
insertText: 'awk \'{ sub("${1:pattern}", "${2:replacement}") }\' ${3:path/to/file}',
documentation: 'error if unset or null',
label: 'error-if-unset-or-null',
insertText: '"\\${${1:variable}:?${2:error_message}}"',
},
{
documentation: 'global replacement',
label: 'awk:replace-all',
insertText: 'awk \'{ gsub("${1:pattern}", "${2:replacement}") }\' ${3:path/to/file}',
documentation: 'error if unset',
label: 'error-if-unset',
insertText: '"\\${${1:variable}?${2:error_message}}"',
},
{
documentation: 'whole file read',
label: 'awk:read-all',
insertText: "awk RS='^$' '{ ${1:command} }' ${2:path/to/file}",
documentation: 'if set or not null',
label: 'if-set-or-not-null',
insertText: '"\\${${1:variable}:+${2:alternative}}"',
},
{
documentation: 'node print',
label: 'jq:print',
insertText: "jq '.${1:path/to/node}' ${2:path/to/file}",
documentation: 'if set',
label: 'if-set',
insertText: '"\\${${1:variable}+${2:alternative}}"',
},
{
documentation: 'node print',
label: 'yq:print',
insertText: "yq '.${1:path/to/node}' ${2:path/to/file}",
documentation: 'string shortest leading replacement',
label: 'string-remove-leading',
insertText: '"\\${${1:variable}#${2:pattern}}"',
},
{
documentation: 'home directory',
label: '~',
insertText: '$HOME',
documentation: 'string shortest trailing replacement',
label: 'string-remove-trailing',
insertText: '"\\${${1:variable}%${2:pattern}}"',
},
{
documentation: '[dev]ice name',
label: 'dev',
insertText: '/dev/${1|null,stdin,stdout,stderr|}',
documentation: 'string filtering',
label: 'string-match',
insertText: "sed ${1|-E -n,--regexp-extended --quiet|} '/${2:pattern}/p'",
},
{
documentation: '[al]pha[num]eric characters',
label: 'alnum',
insertText: '[[:alnum:]]',
documentation: 'string replacement',
label: 'string-replace',
insertText: "sed ${1|-E,--regexp-extended|} 's/${2:pattern}/${3:replacement}/'",
},
{
documentation: '[alpha]betic characters',
label: 'alpha',
insertText: '[[:alpha:]]',
documentation: 'string replacement',
label: 'string-replace-all',
insertText: "sed ${1|-E,--regexp-extended|} 's/${2:pattern}/${3:replacement}/g'",
},
{
documentation: '[blank] characters',
label: 'blank',
insertText: '[[:blank:]]',
documentation: 'string transliterate',
label: 'string-transliterate',
insertText: "sed ${1|-E,--regexp-extended|} 'y/${2:source-characters}/${3:replacement-characters}/g'",
},
{
documentation: '[c]o[nt]ro[l] characters',
label: 'cntrl',
insertText: '[[:cntrl:]]',
documentation: 'file print',
label: 'file-print',
insertText: "sed '' ${1:file}",
},
{
documentation: '[digit] characters',
label: 'digit',
insertText: '[[:digit:]]',
documentation: 'file read',
label: 'file-read',
insertText: "sed ${1|-E,--regexp-extended|} ':${2:x} N $! b$2 ${3:command}' ${4:file}",
},
{
documentation: '[graph]ical characters',
label: 'graph',
insertText: '[[:graph:]]',
documentation: 'skip first',
label: 'skip-first',
insertText: 'tail ${1|-n,-c,--lines,--bytes|} +${2:count}',
},
{
documentation: '[lower] characters',
label: 'lower',
insertText: '[[:lower:]]',
documentation: 'skip last',
label: 'skip-last',
insertText: 'head ${1|-n,-c,--lines,--bytes|} -${2:count}',
},
{
documentation: '[print]able characters',
label: 'print',
insertText: '[[:print:]]',
documentation: 'take first',
label: 'take-first',
insertText: 'head ${1|-n,-c,--lines,--bytes|} ${2:count}',
},
{
documentation: '[punct]uation characters',
label: 'punct',
insertText: '[[:punct:]]',
documentation: 'take last',
label: 'take-last',
insertText: 'tail ${1|-n,-c,--lines,--bytes|} ${2:count}',
},
{
documentation: '[space] characters',
label: 'space',
insertText: '[[:space:]]',
documentation: 'take range',
label: 'take-range',
insertText: "sed ${1|-n,--quiet|} '${2:from},${3:to}p'",
},
{
documentation: '[upper] characters',
label: 'upper',
insertText: '[[:upper:]]',
documentation: 'take stepped range',
label: 'take-stepped-range',
insertText: "sed ${1|-n,--quiet|} '${2:from},${3:to}p' | sed $1 '1~${4:step}p'",
},
{
documentation: 'hexadecimal characters',
label: 'xdigit',
insertText: '[[:xdigit:]]',
documentation: 'json print',
label: 'json-print',
insertText: "jq '.${1:node}' ${2:file}",
},
{
documentation: 'device',
label: 'device',
insertText: '/dev/${1|null,stdin,stdout,stderr|}',
},
{
documentation: 'completion',
label: 'completion definition',
insertText: [
'_$1_completions()',
'{',
'\treadarray -t COMPREPLY < <(compgen -W "-h --help -v --version" "\\${COMP_WORDS[1]}")',
'}',
'',
'complete -F _$1_completions ${1:command}',
].join('\n'),
},
{
documentation: 'comment',
label: 'comment definition',
insertText: '# ${1:description}',
},
].map((item) => (Object.assign(Object.assign({}, item), { documentation: {

@@ -425,0 +659,0 @@ value: [

@@ -46,1 +46,44 @@ import * as LSP from 'vscode-languageserver/node';

}): Declarations;
export type FindDeclarationParams = {
/**
* The node where the search will start.
*/
baseNode: Parser.SyntaxNode;
symbolInfo: {
position: LSP.Position;
uri: string;
word: string;
kind: LSP.SymbolKind;
};
otherInfo: {
/**
* The current URI being searched.
*/
currentUri: string;
/**
* The line (LSP semantics) or row (tree-sitter semantics) at which to stop
* searching.
*/
boundary: LSP.uinteger;
};
};
/**
* Searches for the original declaration of `symbol`. Global semantics here
* means that the symbol is not local to a function, hence, `baseNode` should
* either be a `subshell` or a `program` and `symbolInfo` should contain data
* about a variable or a function.
*/
export declare function findDeclarationUsingGlobalSemantics({ baseNode, symbolInfo: { position, uri, word, kind }, otherInfo: { currentUri, boundary }, }: FindDeclarationParams): {
declaration: Parser.SyntaxNode | null | undefined;
continueSearching: boolean;
};
/**
* Searches for the original declaration of `symbol`. Local semantics here
* means that the symbol is local to a function, hence, `baseNode` should
* be the `compound_statement` of a `function_definition` and `symbolInfo`
* should contain data about a variable.
*/
export declare function findDeclarationUsingLocalSemantics({ baseNode, symbolInfo: { position, word }, otherInfo: { boundary }, }: FindDeclarationParams): {
declaration: Parser.SyntaxNode | null | undefined;
continueSearching: boolean;
};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLocalDeclarations = exports.getAllDeclarationsInTree = exports.getGlobalDeclarations = void 0;
exports.findDeclarationUsingLocalSemantics = exports.findDeclarationUsingGlobalSemantics = exports.getLocalDeclarations = exports.getAllDeclarationsInTree = exports.getGlobalDeclarations = void 0;
const LSP = require("vscode-languageserver/node");

@@ -160,2 +160,131 @@ const TreeSitterUtil = require("./tree-sitter");

}
/**
* Searches for the original declaration of `symbol`. Global semantics here
* means that the symbol is not local to a function, hence, `baseNode` should
* either be a `subshell` or a `program` and `symbolInfo` should contain data
* about a variable or a function.
*/
function findDeclarationUsingGlobalSemantics({ baseNode, symbolInfo: { position, uri, word, kind }, otherInfo: { currentUri, boundary }, }) {
let declaration;
let continueSearching = false;
TreeSitterUtil.forEach(baseNode, (n) => {
var _a, _b, _c;
if ((declaration && !continueSearching) ||
n.startPosition.row > boundary ||
(n.type === 'subshell' && !n.equals(baseNode))) {
return false;
}
// `declaration_command`s are handled separately from `variable_assignment`s
// because `declaration_command`s can declare variables without defining
// them, while `variable_assignment`s require both declaration and
// definition, so, there can be `variable_name`s within
// `declaration_command`s that are not children of `variable_assignment`s.
if (kind === LSP.SymbolKind.Variable && n.type === 'declaration_command') {
const functionDefinition = TreeSitterUtil.findParentOfType(n, 'function_definition');
const isLocalDeclaration = !!functionDefinition &&
((_a = functionDefinition.lastChild) === null || _a === void 0 ? void 0 : _a.type) === 'compound_statement' &&
['local', 'declare', 'typeset'].includes((_b = n.firstChild) === null || _b === void 0 ? void 0 : _b.text) &&
(baseNode.type !== 'subshell' ||
baseNode.startPosition.row < functionDefinition.startPosition.row);
for (const v of n.descendantsOfType('variable_name')) {
if (v.text !== word ||
TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])) {
continue;
}
if (isLocalDeclaration) {
// Update boundary since any other instance below `n` can now be
// considered local to a function or out of scope.
boundary = n.startPosition.row;
break;
}
if (uri !== currentUri || !isDefinedVariableInExpression(n, v, position)) {
declaration = v;
continueSearching = false;
break;
}
}
// This return is important as it makes sure that the next if statement
// only catches `variable_assignment`s outside of `declaration_command`s.
return false;
}
if (kind === LSP.SymbolKind.Variable &&
(['variable_assignment', 'for_statement'].includes(n.type) ||
(n.type === 'command' && n.text.includes(':=')))) {
const definedVariable = n.descendantsOfType('variable_name').at(0);
const definedVariableInExpression = uri === currentUri &&
n.type === 'variable_assignment' &&
!!definedVariable &&
isDefinedVariableInExpression(n, definedVariable, position);
if ((definedVariable === null || definedVariable === void 0 ? void 0 : definedVariable.text) === word && !definedVariableInExpression) {
declaration = definedVariable;
continueSearching = baseNode.type === 'subshell' && n.type === 'command';
// The original declaration could be inside a `for_statement`, so only
// return false when the original declaration is found.
return false;
}
return true;
}
if (kind === LSP.SymbolKind.Function &&
n.type === 'function_definition' &&
((_c = n.firstNamedChild) === null || _c === void 0 ? void 0 : _c.text) === word) {
declaration = n.firstNamedChild;
continueSearching = false;
return false;
}
return true;
});
return { declaration, continueSearching };
}
exports.findDeclarationUsingGlobalSemantics = findDeclarationUsingGlobalSemantics;
/**
* Searches for the original declaration of `symbol`. Local semantics here
* means that the symbol is local to a function, hence, `baseNode` should
* be the `compound_statement` of a `function_definition` and `symbolInfo`
* should contain data about a variable.
*/
function findDeclarationUsingLocalSemantics({ baseNode, symbolInfo: { position, word }, otherInfo: { boundary }, }) {
let declaration;
let continueSearching = false;
TreeSitterUtil.forEach(baseNode, (n) => {
var _a;
if ((declaration && !continueSearching) ||
n.startPosition.row > boundary ||
['function_definition', 'subshell'].includes(n.type)) {
return false;
}
if (n.type !== 'declaration_command') {
return true;
}
if (!['local', 'declare', 'typeset'].includes((_a = n.firstChild) === null || _a === void 0 ? void 0 : _a.text)) {
return false;
}
for (const v of n.descendantsOfType('variable_name')) {
if (v.text !== word ||
TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])) {
continue;
}
if (!isDefinedVariableInExpression(n, v, position)) {
declaration = v;
continueSearching = false;
break;
}
}
return false;
});
return { declaration, continueSearching };
}
exports.findDeclarationUsingLocalSemantics = findDeclarationUsingLocalSemantics;
/**
* This is used in checking self-assignment `var=$var` edge cases where
* `position` is within `$var`. Based on the `definition` node (should be
* `declaration_command` or `variable_assignment`) and `variable` node (should
* be `variable_name`) given, estimates if `position` is within the expressiion
* (after the equals sign) of an assignment. If it is, then `var` should be
* skipped and a higher scope should be checked for the original declaration.
*/
function isDefinedVariableInExpression(definition, variable, position) {
return (definition.endPosition.row >= position.line &&
(variable.endPosition.column < position.character ||
variable.endPosition.row < position.line));
}
//# sourceMappingURL=declarations.js.map

@@ -14,1 +14,2 @@ import * as LSP from 'vscode-languageserver/node';

export declare function findParent(start: SyntaxNode, predicate: (n: SyntaxNode) => boolean): SyntaxNode | null;
export declare function findParentOfType(start: SyntaxNode, type: string | string[]): SyntaxNode | null;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findParent = exports.isReference = exports.isDefinition = exports.range = exports.forEach = void 0;
exports.findParentOfType = exports.findParent = exports.isReference = exports.isDefinition = exports.range = exports.forEach = void 0;
const LSP = require("vscode-languageserver/node");

@@ -53,2 +53,9 @@ /**

exports.findParent = findParent;
function findParentOfType(start, type) {
if (typeof type === 'string') {
return findParent(start, (n) => n.type === type);
}
return findParent(start, (n) => type.includes(n.type));
}
exports.findParentOfType = findParentOfType;
//# sourceMappingURL=tree-sitter.js.map

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

"license": "MIT",
"version": "5.0.0",
"version": "5.1.0",
"main": "./out/server.js",

@@ -21,10 +21,10 @@ "typings": "./out/server.d.ts",

"dependencies": {
"fast-glob": "3.3.0",
"fast-glob": "3.3.2",
"fuzzy-search": "3.2.1",
"node-fetch": "2.6.12",
"node-fetch": "2.7.0",
"turndown": "7.1.2",
"vscode-languageserver": "8.0.2",
"vscode-languageserver-textdocument": "1.0.8",
"vscode-languageserver-textdocument": "1.0.11",
"web-tree-sitter": "0.20.8",
"zod": "3.21.4"
"zod": "3.22.4"
},

@@ -35,7 +35,7 @@ "scripts": {

"devDependencies": {
"@types/fuzzy-search": "2.1.2",
"@types/node-fetch": "2.6.4",
"@types/turndown": "5.0.1",
"@types/urijs": "1.19.19"
"@types/fuzzy-search": "2.1.5",
"@types/node-fetch": "2.6.9",
"@types/turndown": "5.0.4",
"@types/urijs": "1.19.25"
}
}

@@ -5,4 +5,2 @@ # Bash Language Server

We strongly recommend that you install [shellcheck][shellcheck] to enable linting: https://github.com/koalaman/shellcheck#installing
Documentation around configuration variables can be found in the [config.ts](https://github.com/bash-lsp/bash-language-server/blob/main/server/src/config.ts) file.

@@ -20,6 +18,6 @@

- Workspace symbols
- Rename symbol
To be implemented:
- Rename symbol
- Better jump to declaration and find references based on scope

@@ -29,5 +27,11 @@

### Dependencies
As a dependency, we recommend that you first install shellcheck [shellcheck][shellcheck] to enable linting: https://github.com/koalaman/shellcheck#installing . If shellcheck is installed, bash-language-server will automatically call it to provide linting and code analysis each time the file is updated (with debounce time or 500ms).
### Bash language server
Usually you want to install a client for your editor (see the section below).
But if you want to install the server binary:
But if you want to install the server binary (for examples for editors, like helix, where a generic LSP client is built in), you can install from npm registry as:

@@ -38,3 +42,3 @@ ```bash

On Fedora based distros:
Alternatively, bash-language-server may also be distributed directly by your Linux distro, for example on Fedora based distros:

@@ -45,2 +49,8 @@ ```bash

Or on Ubuntu with snap:
```bash
sudo snap install bash-language-server --classic
```
To verify that everything is working:

@@ -47,0 +57,0 @@

@@ -19,3 +19,3 @@ import { pathToFileURL } from 'node:url'

// if you add a .sh file to testing/fixtures, update this value
const FIXTURE_FILES_MATCHING_GLOB = 16
const FIXTURE_FILES_MATCHING_GLOB = 17

@@ -466,2 +466,19 @@ const defaultConfig = getDefaultConfiguration()

},
{
"kind": 13,
"location": {
"range": {
"end": {
"character": 26,
"line": 97,
},
"start": {
"character": 0,
"line": 97,
},
},
"uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/renaming.sh",
},
"name": "npm_config_loglevel",
},
]

@@ -468,0 +485,0 @@ `)

@@ -0,1 +1,2 @@

import { join } from 'node:path'
import { pathToFileURL } from 'node:url'

@@ -91,2 +92,5 @@

"referencesProvider": true,
"renameProvider": {
"prepareProvider": true,
},
"textDocumentSync": 1,

@@ -110,2 +114,4 @@ "workspaceSymbolProvider": true,

expect(connection.onWorkspaceSymbol).toHaveBeenCalledTimes(1)
expect(connection.onPrepareRename).toHaveBeenCalledTimes(1)
expect(connection.onRenameRequest).toHaveBeenCalledTimes(1)
})

@@ -1245,2 +1251,28 @@

"end": {
"character": 19,
"line": 97,
},
"start": {
"character": 0,
"line": 97,
},
},
"uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/renaming.sh",
},
{
"range": {
"end": {
"character": 25,
"line": 98,
},
"start": {
"character": 6,
"line": 98,
},
},
"uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/renaming.sh",
},
{
"range": {
"end": {
"character": 26,

@@ -1290,2 +1322,28 @@ "line": 42,

"end": {
"character": 19,
"line": 97,
},
"start": {
"character": 0,
"line": 97,
},
},
"uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/renaming.sh",
},
{
"range": {
"end": {
"character": 25,
"line": 98,
},
"start": {
"character": 6,
"line": 98,
},
},
"uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/renaming.sh",
},
{
"range": {
"end": {
"character": 26,

@@ -1333,2 +1391,13 @@ "line": 42,

},
{
kind: expect.any(Number),
location: {
range: {
end: { character: 26, line: 97 },
start: { character: 0, line: 97 },
},
uri: expect.stringContaining('/testing/fixtures/renaming.sh'),
},
name: 'npm_config_loglevel',
},
])

@@ -1342,2 +1411,385 @@ }

})
describe('onPrepareRename', () => {
async function getPrepareRenameResult(line: LSP.uinteger, character: LSP.uinteger) {
const { connection } = await initializeServer()
return connection.onPrepareRename.mock.calls[0][0](
{ textDocument: { uri: FIXTURE_URI.RENAMING }, position: { line, character } },
{} as any,
)
}
it.each([
['comment', 0, 17],
['empty line', 1, 0],
['special variable', 4, 7],
['underscore', 5, 0],
['positional parameter', 6, 7],
['invalidly named variable', 7, 2],
['string', 32, 24],
['reserved word', 32, 33],
['regular word', 88, 11],
// Documents some of tree-sitter-bash's limitations when parsing
// constructs that affect renaming; these may fail in the future when
// parsing gets better.
['variable in let expression', 110, 4],
['variable in binary expression', 111, 9],
['variable in postfix expression', 111, 17],
['variable in arithmetic expansion', 112, 14],
])('returns null for non-renamable symbol: %s', async (_, line, character) => {
expect(await getPrepareRenameResult(line, character)).toBeNull()
})
it('returns Range for renamable symbol', async () => {
const HOME = await getPrepareRenameResult(23, 10)
expect(HOME).toMatchInlineSnapshot(`
{
"end": {
"character": 11,
"line": 23,
},
"start": {
"character": 7,
"line": 23,
},
}
`)
const ls = await getPrepareRenameResult(24, 0)
expect(ls).toMatchInlineSnapshot(`
{
"end": {
"character": 2,
"line": 24,
},
"start": {
"character": 0,
"line": 24,
},
}
`)
const somefunc = await getPrepareRenameResult(28, 6)
expect(somefunc).toMatchInlineSnapshot(`
{
"end": {
"character": 8,
"line": 28,
},
"start": {
"character": 0,
"line": 28,
},
}
`)
})
})
describe('onRenameRequest', () => {
async function getRenameRequestResult(
line: LSP.uinteger,
character: LSP.uinteger,
{
rootPath = '',
includeAllWorkspaceSymbols = false,
uri = FIXTURE_URI.RENAMING,
newName = 'newName',
} = {},
) {
const { connection } = await initializeServer({
rootPath: rootPath ? rootPath : undefined,
capabilities: { workspace: { configuration: true } },
configurationObject: { includeAllWorkspaceSymbols },
})
return updateSnapshotUris(
await connection.onRenameRequest.mock.calls[0][0](
{
textDocument: { uri },
position: { line, character },
newName,
},
{} as any,
{} as any,
),
)
}
async function getFirstChangeRanges(
promise: ReturnType<typeof getRenameRequestResult>,
) {
return Object.values(
((await promise) as LSP.WorkspaceEdit).changes as {
[uri: LSP.DocumentUri]: LSP.TextEdit[]
},
)[0].map((c) => c.range)
}
async function getChangeUris(promise: ReturnType<typeof getRenameRequestResult>) {
return Object.keys(
((await promise) as LSP.WorkspaceEdit).changes as {
[uri: LSP.DocumentUri]: LSP.TextEdit[]
},
)
}
function getRenameRequestResults(
...args: Parameters<typeof getRenameRequestResult>[]
) {
return Promise.all(args.map((a) => getRenameRequestResult(...a)))
}
it.each(['_', '2', '1abc', 'ab%c'])(
'throws an error for invalid variable name: %s',
async (newName) => {
await expect(getRenameRequestResult(11, 7, { newName })).rejects.toThrow()
},
)
it.each(['$', 'new$name'])(
'throws an error for invalid function name: %s',
async (newName) => {
await expect(getRenameRequestResult(12, 24, { newName })).rejects.toThrow()
},
)
it('differentiates between variables and functions with the same name', async () => {
const variableRanges = await getFirstChangeRanges(getRenameRequestResult(11, 7))
const functionRanges = await getFirstChangeRanges(getRenameRequestResult(12, 24))
expect(variableRanges).toHaveLength(4)
expect(functionRanges).toHaveLength(2)
expect(variableRanges).not.toContainEqual(functionRanges[0])
expect(variableRanges).not.toContainEqual(functionRanges[1])
})
describe('File-wide rename', () => {
it('returns correct WorkspaceEdits for undeclared symbols', async () => {
const [HOME, ...HOMEs] = await getRenameRequestResults([23, 10], [24, 5])
expect(HOME).toMatchSnapshot()
for (const h of HOMEs) {
expect(HOME).toStrictEqual(h)
}
const [ls, ...lss] = await getRenameRequestResults([24, 0], [29, 12], [30, 18])
expect(ls).toMatchSnapshot()
for (const l of lss) {
expect(ls).toStrictEqual(l)
}
})
it('returns correct WorkspaceEdits for globally scoped declarations', async () => {
const [somefunc, ...somefuncs] = await getRenameRequestResults(
[28, 5],
[32, 11],
[46, 8],
[49, 3],
)
expect(somefunc).toMatchSnapshot()
for (const s of somefuncs) {
expect(somefunc).toStrictEqual(s)
}
const [somevar, ...somevars] = await getRenameRequestResults(
[29, 2],
[33, 12],
[40, 9],
[41, 7],
[43, 23],
[44, 9],
[64, 10],
[65, 13],
[66, 3],
)
expect(somevar).toMatchSnapshot()
for (const s of somevars) {
expect(somevar).toStrictEqual(s)
}
const [othervar, ...othervars] = await getRenameRequestResults([30, 9], [34, 12])
expect(othervar).toMatchSnapshot()
for (const o of othervars) {
expect(othervar).toStrictEqual(o)
}
})
it('returns correct WorkspaceEdits for function-scoped declarations', async () => {
const [somevar, ...somevars] = await getRenameRequestResults(
[43, 11],
[47, 2],
[52, 15],
[58, 14],
)
expect(somevar).toMatchSnapshot()
for (const s of somevars) {
expect(somevar).toStrictEqual(s)
}
const [somevarInsideSubshell, ...somevarsInsideSubshell] =
await getRenameRequestResults([53, 17], [54, 13])
expect(somevarInsideSubshell).toMatchSnapshot()
for (const s of somevarsInsideSubshell) {
expect(somevarInsideSubshell).toStrictEqual(s)
}
})
it('returns correct WorkspaceEdits for subshell-scoped declarations', async () => {
const [somevar, ...somevars] = await getRenameRequestResults(
[65, 3],
[68, 7],
[76, 18],
[83, 8],
)
expect(somevar).toMatchSnapshot()
for (const s of somevars) {
expect(somevar).toStrictEqual(s)
}
const [somevarInsideSubshell, ...somevarsInsideSubshell] =
await getRenameRequestResults([71, 4], [72, 11])
expect(somevarInsideSubshell).toMatchSnapshot()
for (const s of somevarsInsideSubshell) {
expect(somevarInsideSubshell).toStrictEqual(s)
}
const [somefunc, ...somefuncs] = await getRenameRequestResults([75, 10], [81, 5])
expect(somefunc).toMatchSnapshot()
for (const s of somefuncs) {
expect(somefunc).toStrictEqual(s)
}
const [somevarInsideSomefunc, ...somevarsInsideSomefunc] =
await getRenameRequestResults([77, 16], [78, 17])
expect(somevarInsideSomefunc).toMatchSnapshot()
for (const s of somevarsInsideSomefunc) {
expect(somevarInsideSomefunc).toStrictEqual(s)
}
})
})
describe('Workspace-wide rename', () => {
it('returns correct WorkspaceEdits for sourced symbols', async () => {
const [RED, ...REDs] = await getRenameRequestResults(
[90, 0],
[91, 8],
[4, 7, { uri: FIXTURE_URI.SOURCING }],
[4, 2, { uri: FIXTURE_URI.EXTENSION_INC }],
[22, 3, { uri: FIXTURE_URI.EXTENSION_INC }],
)
expect(RED).toMatchSnapshot()
for (const r of REDs) {
expect(RED).toStrictEqual(r)
}
const [tagRelease, ...tagReleases] = await getRenameRequestResults(
[93, 6, { rootPath: REPO_ROOT_FOLDER }],
[94, 7, { rootPath: REPO_ROOT_FOLDER }],
[18, 1, { rootPath: REPO_ROOT_FOLDER, uri: FIXTURE_URI.SOURCING }],
[
4,
18,
{
rootPath: REPO_ROOT_FOLDER,
uri: `file://${join(REPO_ROOT_FOLDER, 'scripts', 'tag-release.inc')}`,
},
],
)
expect(tagRelease).toMatchSnapshot()
for (const t of tagReleases) {
expect(tagRelease).toStrictEqual(t)
}
})
it('returns correct WorkspaceEdits for unsourced symbols when includeAllWorkspaceSymbols is false', async () => {
const [npm_config_loglevel, ...npm_config_loglevels] =
await getRenameRequestResults([97, 3], [98, 16])
expect(npm_config_loglevel).toMatchSnapshot()
for (const n of npm_config_loglevels) {
expect(npm_config_loglevel).toStrictEqual(n)
}
const [f, ...fs] = await getRenameRequestResults([101, 0], [102, 0])
expect(f).toMatchSnapshot()
for (const instance of fs) {
expect(f).toStrictEqual(instance)
}
})
it('returns correct WorkspaceEdits for unsourced symbols when includeAllWorkspaceSymbols is true', async () => {
const [npm_config_loglevel, ...npm_config_loglevels] =
await getRenameRequestResults(
[97, 3, { includeAllWorkspaceSymbols: true }],
[98, 16, { includeAllWorkspaceSymbols: true }],
[42, 9, { includeAllWorkspaceSymbols: true, uri: FIXTURE_URI.SCOPE }],
[40, 6, { includeAllWorkspaceSymbols: true, uri: FIXTURE_URI.INSTALL }],
[48, 14, { includeAllWorkspaceSymbols: true, uri: FIXTURE_URI.INSTALL }],
[50, 24, { includeAllWorkspaceSymbols: true, uri: FIXTURE_URI.INSTALL }],
)
expect(npm_config_loglevel).toMatchSnapshot()
for (const n of npm_config_loglevels) {
expect(npm_config_loglevel).toStrictEqual(n)
}
const [f, ...fs] = await getRenameRequestResults(
[101, 0, { includeAllWorkspaceSymbols: true }],
[102, 0, { includeAllWorkspaceSymbols: true }],
[7, 0, { includeAllWorkspaceSymbols: true, uri: FIXTURE_URI.SCOPE }],
[33, 0, { includeAllWorkspaceSymbols: true, uri: FIXTURE_URI.SCOPE }],
)
expect(f).toMatchSnapshot()
for (const instance of fs) {
expect(f).toStrictEqual(instance)
}
})
})
// These may fail in the future when tree-sitter-bash's parsing gets better
// or when the rename symbol implementation is improved.
describe('Edge or not covered cases', () => {
it('only includes variables typed as variable_name', async () => {
const iRanges = await getFirstChangeRanges(getRenameRequestResult(106, 4))
// This should be 6 if all instances within let and arithmetic
// expressions are included.
expect(iRanges.length).toBe(2)
const lineRanges = await getFirstChangeRanges(getRenameRequestResult(118, 10))
// This should be 2 if the declaration of `line` is included.
expect(lineRanges.length).toBe(1)
})
it('includes incorrect number of symbols for complex scopes and nesting', async () => {
const varRanges = await getFirstChangeRanges(getRenameRequestResult(124, 8))
// This should only be 2 if `$var` from `3` is not included.
expect(varRanges.length).toBe(3)
const localFuncRanges = await getFirstChangeRanges(getRenameRequestResult(144, 5))
// This should be 2 if the instance of `localFunc` in `callerFunc` is
// also included.
expect(localFuncRanges.length).toBe(1)
})
it('only takes into account subshells created with ( and )', async () => {
const pipelinevarRanges = await getFirstChangeRanges(
getRenameRequestResult(150, 7),
)
// This should only be 1 if pipeline subshell scoping is recognized.
expect(pipelinevarRanges.length).toBe(2)
})
it('does not take into account sourcing location and scope', async () => {
const FOOUris = await getChangeUris(getRenameRequestResult(154, 8))
// This should only be 1 if sourcing after a symbol does not affect it.
expect(FOOUris.length).toBe(2)
const hello_worldUris = await getChangeUris(getRenameRequestResult(160, 6))
// This should only be 1 if sourcing inside an uncalled function does
// not affect symbols outside of it.
expect(hello_worldUris.length).toBe(2)
const PATH_INPUTUris = await getChangeUris(getRenameRequestResult(163, 9))
// This should only be 1 if sourcing inside a subshell does not affect
// symbols outside of it.
expect(PATH_INPUTUris.length).toBe(2)
})
})
})
})

@@ -12,2 +12,5 @@ import * as fs from 'fs'

import {
FindDeclarationParams,
findDeclarationUsingGlobalSemantics,
findDeclarationUsingLocalSemantics,
getAllDeclarationsInTree,

@@ -280,2 +283,98 @@ getGlobalDeclarations,

/**
* Find a symbol's original declaration and parent scope based on its original
* definition with respect to its scope.
*/
public findOriginalDeclaration(params: FindDeclarationParams['symbolInfo']): {
declaration: LSP.Location | null
parent: LSP.Location | null
} {
const node = this.nodeAtPoint(
params.uri,
params.position.line,
params.position.character,
)
if (!node) {
return { declaration: null, parent: null }
}
const otherInfo: FindDeclarationParams['otherInfo'] = {
currentUri: params.uri,
boundary: params.position.line,
}
let parent = this.parentScope(node)
let declaration: Parser.SyntaxNode | null | undefined
let continueSearching = false
// Search for local declaration within parents
while (parent) {
if (
params.kind === LSP.SymbolKind.Variable &&
parent.type === 'function_definition' &&
parent.lastChild
) {
;({ declaration, continueSearching } = findDeclarationUsingLocalSemantics({
baseNode: parent.lastChild,
symbolInfo: params,
otherInfo,
}))
} else if (parent.type === 'subshell') {
;({ declaration, continueSearching } = findDeclarationUsingGlobalSemantics({
baseNode: parent,
symbolInfo: params,
otherInfo,
}))
}
if (declaration && !continueSearching) {
break
}
// Update boundary since any other instance within or below the current
// parent can now be considered local to that parent or out of scope.
otherInfo.boundary = parent.startPosition.row
parent = this.parentScope(parent)
}
// Search for global declaration within files
if (!parent && (!declaration || continueSearching)) {
for (const uri of this.getOrderedReachableUris({ fromUri: params.uri })) {
const root = this.uriToAnalyzedDocument[uri]?.tree.rootNode
if (!root) {
continue
}
otherInfo.currentUri = uri
otherInfo.boundary =
uri === params.uri
? // Reset boundary so globally defined variables within any
// functions already searched can be found.
params.position.line
: // Set boundary to EOF since any position taken from the original
// URI/file does not apply to other URIs/files.
root.endPosition.row
;({ declaration, continueSearching } = findDeclarationUsingGlobalSemantics({
baseNode: root,
symbolInfo: params,
otherInfo,
}))
if (declaration && !continueSearching) {
break
}
}
}
return {
declaration: declaration
? LSP.Location.create(otherInfo.currentUri, TreeSitterUtil.range(declaration))
: null,
parent: parent
? LSP.Location.create(params.uri, TreeSitterUtil.range(parent))
: null,
}
}
/**
* Find all the locations where the given word was defined or referenced.

@@ -337,2 +436,135 @@ * This will include commands, functions, variables, etc.

/**
* A more scope-aware version of findOccurrences that differentiates between
* functions and variables.
*/
public findOccurrencesWithin({
uri,
word,
kind,
start,
scope,
}: {
uri: string
word: string
kind: LSP.SymbolKind
start?: LSP.Position
scope?: LSP.Range
}): LSP.Range[] {
const scopeNode = scope
? this.nodeAtPoints(
uri,
{ row: scope.start.line, column: scope.start.character },
{ row: scope.end.line, column: scope.end.character },
)
: null
const baseNode =
scopeNode && (kind === LSP.SymbolKind.Variable || scopeNode.type === 'subshell')
? scopeNode
: this.uriToAnalyzedDocument[uri]?.tree.rootNode
if (!baseNode) {
return []
}
const typeOfDescendants =
kind === LSP.SymbolKind.Variable
? 'variable_name'
: ['function_definition', 'command_name']
const startPosition = start
? { row: start.line, column: start.character }
: baseNode.startPosition
const ignoredRanges: LSP.Range[] = []
const filterVariables = (n: Parser.SyntaxNode) => {
if (n.text !== word) {
return false
}
const definition = TreeSitterUtil.findParentOfType(n, 'variable_assignment')
const definedVariable = definition?.descendantsOfType('variable_name').at(0)
// For self-assignment `var=$var` cases; this decides whether `$var` is an
// occurrence or not.
if (definedVariable?.text === word && !n.equals(definedVariable)) {
// `start.line` is assumed to be the same as the variable's original
// declaration line; handles cases where `$var` shouldn't be considered
// an occurrence.
if (definition?.startPosition.row === start?.line) {
return false
}
// Returning true here is a good enough heuristic for most cases. It
// breaks down when redeclaration happens in multiple nested scopes,
// handling those more complex situations can be done later on if use
// cases arise.
return true
}
const parent = this.parentScope(n)
if (!parent || baseNode.equals(parent)) {
return true
}
const includeDeclaration = !ignoredRanges.some(
(r) => n.startPosition.row > r.start.line && n.endPosition.row < r.end.line,
)
const declarationCommand = TreeSitterUtil.findParentOfType(n, 'declaration_command')
const isLocal =
(definedVariable?.text === word || !!(!definition && declarationCommand)) &&
(parent.type === 'subshell' ||
['local', 'declare', 'typeset'].includes(
declarationCommand?.firstChild?.text as any,
))
if (isLocal) {
if (includeDeclaration) {
ignoredRanges.push(TreeSitterUtil.range(parent))
}
return false
}
return includeDeclaration
}
const filterFunctions = (n: Parser.SyntaxNode) => {
const text = n.type === 'function_definition' ? n.firstNamedChild?.text : n.text
if (text !== word) {
return false
}
const parentScope = TreeSitterUtil.findParentOfType(n, 'subshell')
if (!parentScope || baseNode.equals(parentScope)) {
return true
}
const includeDeclaration = !ignoredRanges.some(
(r) => n.startPosition.row > r.start.line && n.endPosition.row < r.end.line,
)
if (n.type === 'function_definition') {
if (includeDeclaration) {
ignoredRanges.push(TreeSitterUtil.range(parentScope))
}
return false
}
return includeDeclaration
}
return baseNode
.descendantsOfType(typeOfDescendants, startPosition)
.filter(kind === LSP.SymbolKind.Variable ? filterVariables : filterFunctions)
.map((n) => {
if (n.type === 'function_definition' && n.firstNamedChild) {
return TreeSitterUtil.range(n.firstNamedChild)
}
return TreeSitterUtil.range(n)
})
}
public getAllVariables({

@@ -531,2 +763,33 @@ position,

public symbolAtPointFromTextPosition(
params: LSP.TextDocumentPositionParams,
): { word: string; range: LSP.Range; kind: LSP.SymbolKind } | null {
const node = this.nodeAtPoint(
params.textDocument.uri,
params.position.line,
params.position.character,
)
if (!node) {
return null
}
if (
node.type === 'variable_name' ||
(node.type === 'word' &&
['function_definition', 'command_name'].includes(node.parent?.type as any))
) {
return {
word: node.text,
range: TreeSitterUtil.range(node),
kind:
node.type === 'variable_name'
? LSP.SymbolKind.Variable
: LSP.SymbolKind.Function,
}
}
return null
}
public setEnableSourceErrorDiagnostics(enableSourceErrorDiagnostics: boolean): void {

@@ -540,2 +803,40 @@ this.enableSourceErrorDiagnostics = enableSourceErrorDiagnostics

/**
* If includeAllWorkspaceSymbols is true, this returns all URIs from the
* background analysis, else, it returns the URIs of the files that are
* linked to `uri` via sourcing.
*/
public findAllLinkedUris(uri: string): string[] {
if (this.includeAllWorkspaceSymbols) {
return Object.keys(this.uriToAnalyzedDocument).filter((u) => u !== uri)
}
const uriToAnalyzedDocument = Object.entries(this.uriToAnalyzedDocument)
const uris: string[] = []
let continueSearching = true
while (continueSearching) {
continueSearching = false
for (const [analyzedUri, analyzedDocument] of uriToAnalyzedDocument) {
if (!analyzedDocument) {
continue
}
for (const sourcedUri of analyzedDocument.sourcedUris.values()) {
if (
(sourcedUri === uri || uris.includes(sourcedUri)) &&
!uris.includes(analyzedUri)
) {
uris.push(analyzedUri)
continueSearching = true
break
}
}
}
}
return uris
}
// Private methods

@@ -567,2 +868,36 @@

/**
* Returns all reachable URIs from `fromUri` based on source commands in
* descending order starting from the top of the sourcing tree, this list
* includes `fromUri`. If includeAllWorkspaceSymbols is true, other URIs from
* the background analysis are also included after the ordered URIs in no
* particular order.
*/
private getOrderedReachableUris({ fromUri }: { fromUri: string }): string[] {
let uris: Set<string> | string[] = this.findAllSourcedUris({ uri: fromUri })
for (const u1 of uris) {
for (const u2 of this.findAllSourcedUris({ uri: u1 })) {
if (uris.has(u2)) {
uris.delete(u2)
uris.add(u2)
}
}
}
uris = Array.from(uris)
uris.reverse()
uris.push(fromUri)
if (this.includeAllWorkspaceSymbols) {
uris.push(
...Object.keys(this.uriToAnalyzedDocument).filter(
(u) => !(uris as string[]).includes(u),
),
)
}
return uris
}
private getAnalyzedReachableUris({ fromUri }: { fromUri?: string } = {}): string[] {

@@ -682,2 +1017,17 @@ return this.ensureUrisAreAnalyzed(this.getReachableUris({ fromUri }))

/**
* Returns the parent `subshell` or `function_definition` of the given `node`.
* To disambiguate between regular `subshell`s and `subshell`s that serve as a
* `function_definition`'s body, this only returns a `function_definition` if
* its body is a `compound_statement`.
*/
private parentScope(node: Parser.SyntaxNode): Parser.SyntaxNode | null {
return TreeSitterUtil.findParent(
node,
(n) =>
n.type === 'subshell' ||
(n.type === 'function_definition' && n.lastChild?.type === 'compound_statement'),
)
}
/**
* Find the node at the given point.

@@ -699,2 +1049,16 @@ */

}
private nodeAtPoints(
uri: string,
start: Parser.Point,
end: Parser.Point,
): Parser.SyntaxNode | null {
const rootNode = this.uriToAnalyzedDocument[uri]?.tree.rootNode
if (!rootNode) {
return null
}
return rootNode.descendantForPosition(start, end)
}
}

@@ -22,3 +22,3 @@ import { z } from 'zod'

// Controls how symbols (e.g. variables and functions) are included and used for completion and documentation.
// Controls how symbols (e.g. variables and functions) are included and used for completion, documentation, and renaming.
// If false, then we only include symbols from sourced files (i.e. using non dynamic statements like 'source file.sh' or '. file.sh' or following ShellCheck directives).

@@ -25,0 +25,0 @@ // If true, then all symbols from the workspace are included.

@@ -132,2 +132,3 @@ import { spawnSync } from 'node:child_process'

},
renameProvider: { prepareProvider: true },
}

@@ -173,2 +174,4 @@ }

connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this))
connection.onPrepareRename(this.onPrepareRename.bind(this))
connection.onRenameRequest(this.onRenameRequest.bind(this))

@@ -394,2 +397,6 @@ /**

private throwResponseError(message: string, code = LSP.LSPErrorCodes.RequestFailed) {
throw new LSP.ResponseError(code, message)
}
// ==============================

@@ -723,2 +730,84 @@ // Language server event handlers

}
private onPrepareRename(params: LSP.PrepareRenameParams): LSP.Range | null {
const symbol = this.analyzer.symbolAtPointFromTextPosition(params)
this.logRequest({ request: 'onPrepareRename', params, word: symbol?.word })
if (
!symbol ||
(symbol.kind === LSP.SymbolKind.Variable &&
(symbol.word === '_' || !/^[a-z_][\w]*$/i.test(symbol.word)))
) {
return null
}
return symbol.range
}
private onRenameRequest(params: LSP.RenameParams): LSP.WorkspaceEdit | null {
const symbol = this.analyzer.symbolAtPointFromTextPosition(params)
this.logRequest({ request: 'onRenameRequest', params, word: symbol?.word })
if (!symbol) {
return null
}
if (
symbol.kind === LSP.SymbolKind.Variable &&
(params.newName === '_' || !/^[a-z_][\w]*$/i.test(params.newName))
) {
this.throwResponseError('Invalid variable name given.')
}
if (symbol.kind === LSP.SymbolKind.Function && params.newName.includes('$')) {
this.throwResponseError('Invalid function name given.')
}
const { declaration, parent } = this.analyzer.findOriginalDeclaration({
position: params.position,
uri: params.textDocument.uri,
word: symbol.word,
kind: symbol.kind,
})
// File-wide rename
if (!declaration || parent) {
return <LSP.WorkspaceEdit>{
changes: {
[params.textDocument.uri]: this.analyzer
.findOccurrencesWithin({
uri: params.textDocument.uri,
word: symbol.word,
kind: symbol.kind,
start: declaration?.range.start,
scope: parent?.range,
})
.map((r) => LSP.TextEdit.replace(r, params.newName)),
},
}
}
// Workspace-wide rename
const edits: LSP.WorkspaceEdit = {}
edits.changes = {
[declaration.uri]: this.analyzer
.findOccurrencesWithin({
uri: declaration.uri,
word: symbol.word,
kind: symbol.kind,
start: declaration.range.start,
})
.map((r) => LSP.TextEdit.replace(r, params.newName)),
}
for (const uri of this.analyzer.findAllLinkedUris(declaration.uri)) {
edits.changes[uri] = this.analyzer
.findOccurrencesWithin({
uri,
word: symbol.word,
kind: symbol.kind,
})
.map((r) => LSP.TextEdit.replace(r, params.newName))
}
return edits
}
}

@@ -725,0 +814,0 @@

/**
* Naming convention for `documentation`:
* - for Bash operators it's '<operator> operator'
* - for Bash parameter expansions it's '<expansion> expansion'
* - for Bash documentation it's 'documentation definition' or '"<documentation>" documentation definition'
* - for Bash functions it's 'function definition' or '"<function>" function definition'
* - for Bash builtins it's '"<builtin>" invocation'
* - for Bash character classes it's any string with optional mnemonics depicted via square brackets

@@ -13,3 +11,3 @@ * - for shell shebang it's 'shebang'

* Naming convention for `label`:
* - for shell shebang it's 'shebang'
* - for shell shebang it's 'shebang' or 'shebang-with-arguments'
* - for Bash operators it's '<operator>[<nested-operator>]', where:

@@ -25,9 +23,4 @@ * - <operator> is Bash operator

* - term delimiter: dash, like 'set-if-unset-or-null'
* - for Bash brace expansion it's 'range'
* - for Bash documentation it's one of 'documentation'/'<documentation>'
* - for Bash functions it's one of 'function'/'<function>'
* - for Bash builtins it's '<builtin>'
* - for Bash character classes it's '<character-class>'
* - for Sed it's 'sed:<expression>'
* - for Awk it's 'awk:<expression>'
* - for anything else it's any string

@@ -46,17 +39,38 @@ */

{
documentation: 'shebang-with-arguments',
label: 'shebang-with-arguments',
insertText: '#!/usr/bin/env ${1|-S,--split-string|} ${2|bash,sh|} ${3|argument ...|}',
},
{
label: 'and',
documentation: 'and operator',
insertText: '${1:first-expression} && ${2:second-expression}',
},
{
label: 'or',
documentation: 'or operator',
insertText: '${1:first-expression} || ${2:second-expression}',
},
{
label: 'if',
documentation: 'if operator',
label: 'if',
insertText: ['if ${1:command}; then', '\t$0', 'fi'].join('\n'),
insertText: ['if ${1:condition}; then', '\t${2:command ...}', 'fi'].join('\n'),
},
{
documentation: 'if else operator',
label: 'if-else',
insertText: ['if ${1:command}; then', '\t${2:echo}', 'else', '\t$0', 'fi'].join('\n'),
documentation: 'if-else operator',
insertText: [
'if ${1:condition}; then',
'\t${2:command ...}',
'else',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
documentation: 'if operator',
label: 'if-test',
label: 'if-less',
documentation: 'if with number comparison',
insertText: [
'if [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; then',
'\t$0',
'if (( "${1:first-expression}" < "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',

@@ -66,8 +80,7 @@ ].join('\n'),

{
documentation: 'if else operator',
label: 'if-else-test',
label: 'if-greater',
documentation: 'if with number comparison',
insertText: [
'if [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; then',
'else',
'\t$0',
'if (( "${1:first-expression}" > "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',

@@ -77,12 +90,96 @@ ].join('\n'),

{
label: 'if-less-or-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" <= "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-greater-or-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" >= "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" == "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-not-equal',
documentation: 'if with number comparison',
insertText: [
'if (( "${1:first-expression}" != "${2:second-expression}" )); then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-string-equal',
documentation: 'if with string comparison',
insertText: [
'if [[ "${1:first-expression}" == "${2:second-expression}" ]]; then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-string-not-equal',
documentation: 'if with string comparison',
insertText: [
'if [[ "${1:first-expression}" != "${2:second-expression}" ]]; then',
'\t${3:command ...}',
'fi',
].join('\n'),
},
{
label: 'if-string-empty',
documentation: 'if with string comparison (has [z]ero length)',
insertText: ['if [[ -z "${1:expression}" ]]; then', '\t${2:command ...}', 'fi'].join(
'\n',
),
},
{
label: 'if-string-not-empty',
documentation: 'if with string comparison ([n]ot empty)',
insertText: ['if [[ -n "${1:expression}" ]]; then', '\t${2:command ...}', 'fi'].join(
'\n',
),
},
{
label: 'if-defined',
documentation: 'if with variable existence check',
insertText: ['if [[ -n "${${1:variable}+x}" ]]', '\t${2:command ...}', 'fi'].join(
'\n',
),
},
{
label: 'if-not-defined',
documentation: 'if with variable existence check',
insertText: ['if [[ -z "${${1:variable}+x}" ]]', '\t${2:command ...}', 'fi'].join(
'\n',
),
},
{
label: 'while',
documentation: 'while operator',
label: 'while',
insertText: ['while ${1:command}; do', '\t$0', 'done'].join('\n'),
insertText: ['while ${1:condition}; do', '\t${2:command ...}', 'done'].join('\n'),
},
{
documentation: 'while operator',
label: 'while-test',
label: 'while-else',
documentation: 'while-else operator',
insertText: [
'while [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; do',
'\t$0',
'while ${1:condition}; do',
'\t${2:command ...}',
'else',
'\t${3:command ...}',
'done',

@@ -92,12 +189,16 @@ ].join('\n'),

{
documentation: 'until operator',
label: 'until',
insertText: ['until ${1:command}; do', '\t$0', 'done'].join('\n'),
label: 'while-less',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" < "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'until operator',
label: 'until-test',
label: 'while-greater',
documentation: 'while with number comparison',
insertText: [
'until [[ ${1:variable} ${2|-ef,-nt,-ot,==,=~,!=,<,>,-lt,-le,-gt,-ge|} ${3:variable} ]]; do',
'\t$0',
'while (( "${1:first-expression}" > "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',

@@ -107,325 +208,459 @@ ].join('\n'),

{
documentation: 'for operator',
label: 'for',
insertText: ['for ${1:variable} in ${2:list}; do', '\t$0', 'done'].join('\n'),
label: 'while-less-or-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" <= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'for operator',
label: 'for.range',
insertText: ['for ${1:variable} in $(seq ${2:to}); do', '\t$0', 'done'].join('\n'),
label: 'while-greater-or-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" >= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'for operator',
label: 'for.file',
insertText: ['for ${1:variable} in *; do', '\t$0', 'done'].join('\n'),
label: 'while-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" == "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'for operator',
label: 'for.directory',
insertText: ['for ${1:variable} in */; do', '\t$0', 'done'].join('\n'),
label: 'while-not-equal',
documentation: 'while with number comparison',
insertText: [
'while (( "${1:first-expression}" != "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'function definition',
label: 'function',
insertText: ['${1:function_name}() {', '\t$0', '}'].join('\n'),
label: 'while-string-equal',
documentation: 'while with string comparison',
insertText: [
'while [[ "${1:first-expression}" == "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '"main" function definition',
label: 'main',
insertText: ['main() {', '\t$0', '}'].join('\n'),
label: 'while-string-not-equal',
documentation: 'while with string comparison',
insertText: [
'while [[ "${1:first-expression}" != "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: 'documentation definition',
label: 'documentation',
label: 'while-string-empty',
documentation: 'while with string comparison (has [z]ero length)',
insertText: [
'# ${1:function_name} ${2:function_parameters}',
'# ${3:function_description}',
'#',
'# Output:',
'# ${4:function_output}',
'#',
'# Return:',
'# - ${5:0} when ${6:all parameters are correct}',
'# - ${7:1} ${8:otherwise}',
'while [[ -z "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: ':- expansion',
label: 'if-unset-or-null',
insertText: '"\\${${1:variable}:-${2:default}}"',
label: 'while-string-not-empty',
documentation: 'while with string comparison ([n]ot empty)',
insertText: [
'while [[ -n "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '- expansion',
label: 'if-unset',
insertText: '"\\${${1:variable}-${2:default}}"',
label: 'while-defined',
documentation: 'while with variable existence check',
insertText: [
'while [[ -n "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: ':= expansion',
label: 'set-if-unset-or-null',
insertText: '"\\${${1:variable}:=${2:default}}"',
label: 'while-not-defined',
documentation: 'while with variable existence check',
insertText: [
'while [[ -z "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '= expansion',
label: 'set-if-unset',
insertText: '"\\${${1:variable}=${2:default}}"',
label: 'until',
documentation: 'until operator',
insertText: ['until ${1:condition}; do', '\t${2:command ...}', 'done'].join('\n'),
},
{
documentation: ':? expansion',
label: 'error-if-unset-or-null',
insertText: '"\\${${1:variable}:?${2:error_message}}"',
label: 'until-else',
documentation: 'until-else operator',
insertText: [
'until ${1:condition}; do',
'\t${2:command ...}',
'else',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '? expansion',
label: 'error-if-unset',
insertText: '"\\${${1:variable}?${2:error_message}}"',
label: 'until-less',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" < "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: ':+ expansion',
label: 'if-set-or-not-null',
insertText: '"\\${${1:variable}:+${2:alternative}}"',
label: 'until-greater',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" > "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '+ expansion',
label: 'if-set',
insertText: '"\\${${1:variable}+${2:alternative}}"',
label: 'until-less-or-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" <= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '# expansion',
label: 'without-shortest-leading-pattern',
insertText: '"\\${${1:variable}#${2:pattern}}"',
label: 'until-greater-or-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" >= "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '## expansion',
label: 'without-longest-leading-pattern',
insertText: '"\\${${1:variable}##${2:pattern}}"',
label: 'until-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" == "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '% expansion',
label: 'without-shortest-trailing-pattern',
insertText: '"\\${${1:variable}%${2:pattern}}"',
label: 'until-not-equal',
documentation: 'until with number comparison',
insertText: [
'until (( "${1:first-expression}" != "${2:second-expression}" )); do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '%% expansion',
label: 'without-longest-trailing-pattern',
insertText: '"\\${${1:variable}%%${2:pattern}}"',
label: 'until-string-equal',
documentation: 'until with string comparison',
insertText: [
'until [[ "${1:first-expression}" == "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '.. expansion',
label: 'range',
insertText: '{${1:from}..${2:to}}',
label: 'until-string-not-equal',
documentation: 'until with string comparison',
insertText: [
'until [[ "${1:first-expression}" != "${2:second-expression}" ]]; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '"echo" invocation',
label: 'echo',
insertText: 'echo "${1:message}"',
label: 'until-string-empty',
documentation: 'until with string comparison (has [z]ero length)',
insertText: [
'until [[ -z "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"printf" invocation',
label: 'printf',
insertText:
'printf \'${1|%c,%s,%d,%f,%15c,%15s,%15d,%15f,%.5s,%.5d,%.5f|}\' "${2:message}"',
label: 'until-string-not-empty',
documentation: 'until with string comparison ([n]ot empty)',
insertText: [
'until [[ -n "${1:expression}" ]]; do',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"source" invocation',
label: 'source',
insertText: '${1|source,.|} "${2:path/to/file}"',
label: 'until-defined',
documentation: 'until with variable existence check',
insertText: [
'until [[ -n "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"alias" invocation',
label: 'alias',
insertText: 'alias ${1:name}=${2:value}',
label: 'until-not-defined',
documentation: 'until with variable existence check',
insertText: [
'until [[ -z "${${1:variable}+x}" ]]',
'\t${2:command ...}',
'done',
].join('\n'),
},
{
documentation: '"cd" invocation',
label: 'cd',
insertText: 'cd "${1:path/to/directory}"',
label: 'for',
documentation: 'for operator',
insertText: [
'for ${1:item} in ${2:expression}; do',
'\t${3:command ...}',
'done',
].join('\n'),
},
{
documentation: '"getopts" invocation',
label: 'getopts',
insertText: 'getopts ${1:optstring} ${2:name}',
label: 'for-range',
documentation: 'for with range',
insertText: [
'for ${1:item} in $(seq ${2:from} ${3:to}); do',
'\t${4:command ...}',
'done',
].join('\n'),
},
{
documentation: '"jobs" invocation',
label: 'jobs',
insertText: 'jobs -x ${1:command}',
label: 'for-stepped-range',
documentation: 'for with stepped range',
insertText: [
'for ${1:item} in $(seq ${2:from} ${3:step} ${4:to}); do',
'\t${5:command ...}',
'done',
].join('\n'),
},
{
documentation: '"kill" invocation',
label: 'kill',
insertText: 'kill ${1|-l,-L|}',
label: 'for-files',
documentation: 'for with files',
insertText: [
'for ${1:item} in *.${2:extension}; do',
'\t${4:command ...}',
'done',
].join('\n'),
},
{
documentation: '"let" invocation',
label: 'let',
insertText: 'let ${1:argument}',
label: 'case',
documentation: 'case operator',
insertText: [
'case "${1:expression}" in',
'\t${2:pattern})',
'\t\t${3:command ...}',
'\t\t;;',
'\t*)',
'\t\t${4:command ...}',
'\t\t;;',
'end',
].join('\n'),
},
{
documentation: '"test" invocation',
label: 'test',
insertText:
'[[ ${1:argument1} ${2|-ef,-nt,-ot,==,=,!=,=~,<,>,-eq,-ne,-lt,-le,-gt,-ge|} ${3:argument2} ]]',
label: 'function',
documentation: 'function definition',
insertText: ['${1:name}() {', '\t${2:command ...}', '}'].join('\n'),
},
{
documentation: 'line print',
label: 'sed:print',
insertText: "sed '' ${1:path/to/file}",
documentation: 'documentation definition',
label: 'documentation',
insertText: [
'# ${1:function_name} ${2:function_parameters}',
'# ${3:function_description}',
'#',
'# Output:',
'# ${4:function_output}',
'#',
'# Return:',
'# - ${5:0} when ${6:all parameters are correct}',
'# - ${7:1} ${8:otherwise}',
].join('\n'),
},
{
documentation: 'line pattern filter',
label: 'sed:filter-by-line-pattern',
insertText:
"sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '/${3:pattern}/p' ${4:path/to/file}",
documentation: 'block',
label: 'block',
insertText: ['{', '\t${1:command ...}', '}'].join('\n'),
},
{
documentation: 'line number filter',
label: 'sed:filter-by-line-number',
insertText:
"sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '${3:number}p' ${4:path/to/file}",
documentation: 'block redirected',
label: 'block-redirected',
insertText: ['{', '\t${1:command ...}', '} > ${2:file}'].join('\n'),
},
{
documentation: 'line number filter',
label: 'sed:filter-by-line-numbers',
insertText:
"sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '${3:from},${4:to}p' ${5:path/to/file}",
documentation: 'block stderr redirected',
label: 'block-stderr-redirected',
insertText: ['{', '\t${1:command ...}', '} 2> ${2:file}'].join('\n'),
},
{
documentation: 'single replacement',
label: 'sed:replace-first',
insertText:
"sed ${1|--regexp-extended,-E|} 's/${2:pattern}/${3:replacement}/' ${4:path/to/file}",
documentation: 'variable',
label: 'variable',
insertText: 'declare ${1:variable}=${2:value}',
},
{
documentation: 'global replacement',
label: 'sed:replace-all',
insertText:
"sed ${1|--regexp-extended,-E|} 's/${2:pattern}/${3:replacement}/g' ${4:path/to/file}",
documentation: 'variable index',
label: 'variable-index',
insertText: '${1:variable}[${2:index}]=${3:value}',
},
{
documentation: 'transliteration',
label: 'sed:transliterate',
insertText:
"sed ${1|--regexp-extended,-E|} 'y/${2:source-characters}/${3:replacement-characters}/g' ${4:path/to/file}",
documentation: 'variable append',
label: 'variable-append',
insertText: '${1:variable}+=${2:value}',
},
{
documentation: 'whole file read',
label: 'sed:read-all',
insertText:
"sed ${1|--regexp-extended,-E|} ':${2:x} N $! b$2 ${3:command}' ${4:path/to/file}",
documentation: 'variable-prepend',
label: 'variable-prepend',
insertText: '${1:variable}=${2:value}\\$$1',
},
{
documentation: 'line print',
label: 'awk:print',
insertText: "awk '/./' ${1:path/to/file}",
documentation: 'if unset or null',
label: 'if-unset-or-null',
insertText: '"\\${${1:variable}:-${2:default}}"',
},
{
documentation: 'line pattern filter',
label: 'awk:filter-by-line-pattern',
insertText: "awk '/${1:pattern}/' ${2:path/to/file}",
documentation: 'if unset',
label: 'if-unset',
insertText: '"\\${${1:variable}-${2:default}}"',
},
{
documentation: 'line number filter',
label: 'awk:filter-by-line-number',
insertText: "awk 'NR == ${1:number}' ${2:path/to/file}",
documentation: 'set if unset or null',
label: 'set-if-unset-or-null',
insertText: '"\\${${1:variable}:=${2:default}}"',
},
{
documentation: 'line number filter',
label: 'awk:filter-by-line-numbers',
insertText: "awk 'NR >= ${1:from} && NR <= ${2:to}' ${3:path/to/file}",
documentation: 'set if unset',
label: 'set-if-unset',
insertText: '"\\${${1:variable}=${2:default}}"',
},
{
documentation: 'single replacement',
label: 'awk:replace-first',
insertText: 'awk \'{ sub("${1:pattern}", "${2:replacement}") }\' ${3:path/to/file}',
documentation: 'error if unset or null',
label: 'error-if-unset-or-null',
insertText: '"\\${${1:variable}:?${2:error_message}}"',
},
{
documentation: 'global replacement',
label: 'awk:replace-all',
insertText: 'awk \'{ gsub("${1:pattern}", "${2:replacement}") }\' ${3:path/to/file}',
documentation: 'error if unset',
label: 'error-if-unset',
insertText: '"\\${${1:variable}?${2:error_message}}"',
},
{
documentation: 'whole file read',
label: 'awk:read-all',
insertText: "awk RS='^$' '{ ${1:command} }' ${2:path/to/file}",
documentation: 'if set or not null',
label: 'if-set-or-not-null',
insertText: '"\\${${1:variable}:+${2:alternative}}"',
},
{
documentation: 'node print',
label: 'jq:print',
insertText: "jq '.${1:path/to/node}' ${2:path/to/file}",
documentation: 'if set',
label: 'if-set',
insertText: '"\\${${1:variable}+${2:alternative}}"',
},
{
documentation: 'node print',
label: 'yq:print',
insertText: "yq '.${1:path/to/node}' ${2:path/to/file}",
documentation: 'string shortest leading replacement',
label: 'string-remove-leading',
insertText: '"\\${${1:variable}#${2:pattern}}"',
},
{
documentation: 'home directory',
label: '~',
insertText: '$HOME',
documentation: 'string shortest trailing replacement',
label: 'string-remove-trailing',
insertText: '"\\${${1:variable}%${2:pattern}}"',
},
{
documentation: '[dev]ice name',
label: 'dev',
insertText: '/dev/${1|null,stdin,stdout,stderr|}',
documentation: 'string filtering',
label: 'string-match',
insertText: "sed ${1|-E -n,--regexp-extended --quiet|} '/${2:pattern}/p'",
},
{
documentation: '[al]pha[num]eric characters',
label: 'alnum',
insertText: '[[:alnum:]]',
documentation: 'string replacement',
label: 'string-replace',
insertText: "sed ${1|-E,--regexp-extended|} 's/${2:pattern}/${3:replacement}/'",
},
{
documentation: '[alpha]betic characters',
label: 'alpha',
insertText: '[[:alpha:]]',
documentation: 'string replacement',
label: 'string-replace-all',
insertText: "sed ${1|-E,--regexp-extended|} 's/${2:pattern}/${3:replacement}/g'",
},
{
documentation: '[blank] characters',
label: 'blank',
insertText: '[[:blank:]]',
documentation: 'string transliterate',
label: 'string-transliterate',
insertText:
"sed ${1|-E,--regexp-extended|} 'y/${2:source-characters}/${3:replacement-characters}/g'",
},
{
documentation: '[c]o[nt]ro[l] characters',
label: 'cntrl',
insertText: '[[:cntrl:]]',
documentation: 'file print',
label: 'file-print',
insertText: "sed '' ${1:file}",
},
{
documentation: '[digit] characters',
label: 'digit',
insertText: '[[:digit:]]',
documentation: 'file read',
label: 'file-read',
insertText:
"sed ${1|-E,--regexp-extended|} ':${2:x} N $! b$2 ${3:command}' ${4:file}",
},
{
documentation: '[graph]ical characters',
label: 'graph',
insertText: '[[:graph:]]',
documentation: 'skip first',
label: 'skip-first',
insertText: 'tail ${1|-n,-c,--lines,--bytes|} +${2:count}',
},
{
documentation: '[lower] characters',
label: 'lower',
insertText: '[[:lower:]]',
documentation: 'skip last',
label: 'skip-last',
insertText: 'head ${1|-n,-c,--lines,--bytes|} -${2:count}',
},
{
documentation: '[print]able characters',
label: 'print',
insertText: '[[:print:]]',
documentation: 'take first',
label: 'take-first',
insertText: 'head ${1|-n,-c,--lines,--bytes|} ${2:count}',
},
{
documentation: '[punct]uation characters',
label: 'punct',
insertText: '[[:punct:]]',
documentation: 'take last',
label: 'take-last',
insertText: 'tail ${1|-n,-c,--lines,--bytes|} ${2:count}',
},
{
documentation: '[space] characters',
label: 'space',
insertText: '[[:space:]]',
documentation: 'take range',
label: 'take-range',
insertText: "sed ${1|-n,--quiet|} '${2:from},${3:to}p'",
},
{
documentation: '[upper] characters',
label: 'upper',
insertText: '[[:upper:]]',
documentation: 'take stepped range',
label: 'take-stepped-range',
insertText: "sed ${1|-n,--quiet|} '${2:from},${3:to}p' | sed $1 '1~${4:step}p'",
},
{
documentation: 'hexadecimal characters',
label: 'xdigit',
insertText: '[[:xdigit:]]',
documentation: 'json print',
label: 'json-print',
insertText: "jq '.${1:node}' ${2:file}",
},
{
documentation: 'device',
label: 'device',
insertText: '/dev/${1|null,stdin,stdout,stderr|}',
},
{
documentation: 'completion',
label: 'completion definition',
insertText: [
'_$1_completions()',
'{',
'\treadarray -t COMPREPLY < <(compgen -W "-h --help -v --version" "\\${COMP_WORDS[1]}")',
'}',
'',
'complete -F _$1_completions ${1:command}',
].join('\n'),
},
{
documentation: 'comment',
label: 'comment definition',
insertText: '# ${1:description}',
},
].map((item) => ({

@@ -432,0 +667,0 @@ ...item,

@@ -248,1 +248,204 @@ import * as LSP from 'vscode-languageserver/node'

}
// The functions that follow search for a single declaration based on the
// original definition NOT the latest.
export type FindDeclarationParams = {
/**
* The node where the search will start.
*/
baseNode: Parser.SyntaxNode
symbolInfo: {
position: LSP.Position
uri: string
word: string
kind: LSP.SymbolKind
}
otherInfo: {
/**
* The current URI being searched.
*/
currentUri: string
/**
* The line (LSP semantics) or row (tree-sitter semantics) at which to stop
* searching.
*/
boundary: LSP.uinteger
}
}
/**
* Searches for the original declaration of `symbol`. Global semantics here
* means that the symbol is not local to a function, hence, `baseNode` should
* either be a `subshell` or a `program` and `symbolInfo` should contain data
* about a variable or a function.
*/
export function findDeclarationUsingGlobalSemantics({
baseNode,
symbolInfo: { position, uri, word, kind },
otherInfo: { currentUri, boundary },
}: FindDeclarationParams) {
let declaration: Parser.SyntaxNode | null | undefined
let continueSearching = false
TreeSitterUtil.forEach(baseNode, (n) => {
if (
(declaration && !continueSearching) ||
n.startPosition.row > boundary ||
(n.type === 'subshell' && !n.equals(baseNode))
) {
return false
}
// `declaration_command`s are handled separately from `variable_assignment`s
// because `declaration_command`s can declare variables without defining
// them, while `variable_assignment`s require both declaration and
// definition, so, there can be `variable_name`s within
// `declaration_command`s that are not children of `variable_assignment`s.
if (kind === LSP.SymbolKind.Variable && n.type === 'declaration_command') {
const functionDefinition = TreeSitterUtil.findParentOfType(n, 'function_definition')
const isLocalDeclaration =
!!functionDefinition &&
functionDefinition.lastChild?.type === 'compound_statement' &&
['local', 'declare', 'typeset'].includes(n.firstChild?.text as any) &&
(baseNode.type !== 'subshell' ||
baseNode.startPosition.row < functionDefinition.startPosition.row)
for (const v of n.descendantsOfType('variable_name')) {
if (
v.text !== word ||
TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])
) {
continue
}
if (isLocalDeclaration) {
// Update boundary since any other instance below `n` can now be
// considered local to a function or out of scope.
boundary = n.startPosition.row
break
}
if (uri !== currentUri || !isDefinedVariableInExpression(n, v, position)) {
declaration = v
continueSearching = false
break
}
}
// This return is important as it makes sure that the next if statement
// only catches `variable_assignment`s outside of `declaration_command`s.
return false
}
if (
kind === LSP.SymbolKind.Variable &&
(['variable_assignment', 'for_statement'].includes(n.type) ||
(n.type === 'command' && n.text.includes(':=')))
) {
const definedVariable = n.descendantsOfType('variable_name').at(0)
const definedVariableInExpression =
uri === currentUri &&
n.type === 'variable_assignment' &&
!!definedVariable &&
isDefinedVariableInExpression(n, definedVariable, position)
if (definedVariable?.text === word && !definedVariableInExpression) {
declaration = definedVariable
continueSearching = baseNode.type === 'subshell' && n.type === 'command'
// The original declaration could be inside a `for_statement`, so only
// return false when the original declaration is found.
return false
}
return true
}
if (
kind === LSP.SymbolKind.Function &&
n.type === 'function_definition' &&
n.firstNamedChild?.text === word
) {
declaration = n.firstNamedChild
continueSearching = false
return false
}
return true
})
return { declaration, continueSearching }
}
/**
* Searches for the original declaration of `symbol`. Local semantics here
* means that the symbol is local to a function, hence, `baseNode` should
* be the `compound_statement` of a `function_definition` and `symbolInfo`
* should contain data about a variable.
*/
export function findDeclarationUsingLocalSemantics({
baseNode,
symbolInfo: { position, word },
otherInfo: { boundary },
}: FindDeclarationParams) {
let declaration: Parser.SyntaxNode | null | undefined
let continueSearching = false
TreeSitterUtil.forEach(baseNode, (n) => {
if (
(declaration && !continueSearching) ||
n.startPosition.row > boundary ||
['function_definition', 'subshell'].includes(n.type)
) {
return false
}
if (n.type !== 'declaration_command') {
return true
}
if (!['local', 'declare', 'typeset'].includes(n.firstChild?.text as any)) {
return false
}
for (const v of n.descendantsOfType('variable_name')) {
if (
v.text !== word ||
TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])
) {
continue
}
if (!isDefinedVariableInExpression(n, v, position)) {
declaration = v
continueSearching = false
break
}
}
return false
})
return { declaration, continueSearching }
}
/**
* This is used in checking self-assignment `var=$var` edge cases where
* `position` is within `$var`. Based on the `definition` node (should be
* `declaration_command` or `variable_assignment`) and `variable` node (should
* be `variable_name`) given, estimates if `position` is within the expressiion
* (after the equals sign) of an assignment. If it is, then `var` should be
* skipped and a higher scope should be checked for the original declaration.
*/
function isDefinedVariableInExpression(
definition: Parser.SyntaxNode,
variable: Parser.SyntaxNode,
position: LSP.Position,
): boolean {
return (
definition.endPosition.row >= position.line &&
(variable.endPosition.column < position.character ||
variable.endPosition.row < position.line)
)
}

@@ -59,1 +59,9 @@ import * as LSP from 'vscode-languageserver/node'

}
export function findParentOfType(start: SyntaxNode, type: string | string[]) {
if (typeof type === 'string') {
return findParent(start, (n) => n.type === type)
}
return findParent(start, (n) => type.includes(n.type))
}

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

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

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

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

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

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

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