Skip to content

Commit 4d0c60b

Browse files
committed
Moved around some items to prepare for the intellisense implementation
1 parent 6b60049 commit 4d0c60b

9 files changed

+1057
-4437
lines changed

package-lock.json

+645-67
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"vscode.git"
2121
],
2222
"main": "./out/extension/index.js",
23+
"l10n": "./l10n",
2324
"contributes": {
2425
"viewsWelcome": [
2526
{
@@ -192,7 +193,6 @@
192193
"format": "prettier . --write --config ./.prettier.mjs"
193194
},
194195
"devDependencies": {
195-
"@oclif/config": "^1.18.17",
196196
"@types/mocha": "^10.0.7",
197197
"@types/mvdan-sh": "^0.10.9",
198198
"@types/node": "18.x",
@@ -222,8 +222,9 @@
222222
"@heroku-cli/schema": "file:heroku-cli-schema-1.0.25.tgz",
223223
"@microsoft/fast-element": "^1.13.0",
224224
"@vscode/codicons": "^0.0.36",
225-
"@vscode/l10n": "0.0.x",
225+
"@vscode/l10n": "0.0.18",
226226
"@vscode/webview-ui-toolkit": "^1.4.0",
227+
"@oclif/core": "2.16.0",
227228
"marked": "^13.0.3",
228229
"mvdan-sh": "^0.10.1",
229230
"resolve.exports": "^2.0.2",

package.nls.json

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
"message": "It looks like this project is not a Heroku app.\n[Get started on Heroku](https://devcenter.heroku.com/start)"
88
},
99

10+
"heroku.cli.description": {
11+
"message": "The Heroku CLI is used to manage Heroku apps from the command line. It is built using [oclif](https://oclif.io).\nFor more about Heroku see <https://www.heroku.com/home>\nTo get started see <https://devcenter.heroku.com/start>"
12+
},
13+
1014
"view.heroku.name": {
1115
"message": "Heroku"
1216
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Command } from '@oclif/core/lib/command';
2+
import { herokuCommand } from '../../meta/command';
3+
import { HerokuCommand } from '../heroku-command';
4+
5+
export declare namespace GetHerokuCommandsJson {
6+
export type ManifestMeta = Map<Command.Cached['id'], Command.Cached>;
7+
}
8+
9+
@herokuCommand()
10+
/**
11+
* The GetHerokuCommandsJson retrieves command meta data
12+
* from the heroku cli and returns it as a Map of
13+
* command id to command meta data.
14+
*
15+
* This data is used for the hover, intellisense, autocomplete and
16+
* error checking when authoring a shell script that uses the
17+
* heroku cli.
18+
*/
19+
export class GetHerokuCommandsJson extends HerokuCommand<GetHerokuCommandsJson.ManifestMeta> {
20+
public static COMMAND_ID = 'heroku:cli:get-commands' as const;
21+
private static manifestPromise: Promise<GetHerokuCommandsJson.ManifestMeta>;
22+
23+
/**
24+
* Gets the heroku commands json based on
25+
* the current heroku cli installed on the
26+
* user's machine.
27+
*
28+
* @returns Command.Cached
29+
*/
30+
public async run(): Promise<GetHerokuCommandsJson.ManifestMeta> {
31+
if (GetHerokuCommandsJson.manifestPromise !== undefined) {
32+
return GetHerokuCommandsJson.manifestPromise;
33+
}
34+
GetHerokuCommandsJson.manifestPromise = this.getManifest();
35+
return GetHerokuCommandsJson.manifestPromise;
36+
}
37+
38+
/**
39+
* Gets the manifest of the heroku cli commands.
40+
*
41+
* @returns Promise<GetHerokuCommandsJson.ManifestMeta>
42+
*/
43+
private async getManifest(): Promise<GetHerokuCommandsJson.ManifestMeta> {
44+
using commandsJsonProcess = HerokuCommand.exec('heroku commands --json', {
45+
signal: this.signal,
46+
timeout: 120 * 1000
47+
});
48+
const result = await HerokuCommand.waitForCompletion(commandsJsonProcess);
49+
const json = JSON.parse(result.output) as Command.Cached[];
50+
const manifest = new Map<Command.Cached['id'], Command.Cached>();
51+
for (const element of json) {
52+
manifest.set(element.id, element);
53+
}
54+
55+
return manifest;
56+
}
57+
}

src/extension/lexers/lexer-utils.ts

+22
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ export function isHerokuCallExpression(node: sh.Node | undefined): node is sh.Ca
2121
);
2222
}
2323

24+
/**
25+
* Determines if the specified node is a partial heroku
26+
* command call expression. This test is true
27+
* if the node is a sh.CallExpr and contains a
28+
* string literal as the first argument which
29+
* is a partial match to "heroku" and contains
30+
* at least the chars "her".
31+
*
32+
* @param node The node to test.
33+
* @returns boolean
34+
*/
35+
export function isPartialHerokuCallExpression(node: sh.Node | undefined): node is sh.CallExpr {
36+
return (
37+
!!node &&
38+
isCallExpr(node) &&
39+
!!node.Args.length &&
40+
!!node.Args[0]?.Parts?.length &&
41+
isLiteral(node.Args[0].Parts[0]) &&
42+
/^(?:her)(?:o|$)(?:k|$)(?:u|$)/.test(node.Args[0].Parts[0].Value)
43+
);
44+
}
45+
2446
/**
2547
* Determines if the specified node is a sh.CallExpr.
2648
*

src/extension/lexers/shell-script-lexer.ts

+95-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import sh from 'mvdan-sh';
22
import * as vscode from 'vscode';
3-
import { isInsideRangeBoundary } from './lexer-utils.js';
3+
import { isHerokuCallExpression, isInsideRangeBoundary, isPartialHerokuCallExpression } from './lexer-utils.js';
4+
5+
export type HerokuCommandEntity =
6+
| {
7+
type: 'heroku' | 'command';
8+
node: sh.Node;
9+
command?: never;
10+
flagKey?: never;
11+
}
12+
| {
13+
node: sh.Node;
14+
type: 'flag';
15+
flagKey: string;
16+
command: sh.Word;
17+
}
18+
| {
19+
node: sh.Node;
20+
type: 'arg';
21+
command: sh.Word;
22+
argIndex: number;
23+
};
424

525
/**
626
* The ShellScriptLexer class provides functionality
@@ -47,4 +67,78 @@ export class ShellScriptLexer {
4767
});
4868
return targets;
4969
}
70+
71+
/**
72+
* Finds the heroku command entity at the specified position.
73+
*
74+
* @param position The position of the cursor in the document.
75+
* @param includePartialHerokuCallExpression Whether to include partial heroku call expressions.
76+
* @returns sh.Node | null
77+
*/
78+
public findEntityAtPosition(
79+
position: vscode.Position,
80+
includePartialHerokuCallExpression = false
81+
): HerokuCommandEntity | null {
82+
const nodes = this.findNodesAtPosition(position);
83+
const node = nodes.find(
84+
includePartialHerokuCallExpression ? isPartialHerokuCallExpression : isHerokuCallExpression
85+
);
86+
if (!node) {
87+
return null;
88+
}
89+
const { Args: args } = node;
90+
const [heroku, command, ...flagsOrArgs] = args;
91+
// -----------------------------------
92+
// We're over a heroku call expression
93+
// -----------------------------------
94+
if (isInsideRangeBoundary(heroku, position)) {
95+
return { type: 'heroku', node: heroku as sh.Word };
96+
}
97+
// ---------------------------
98+
// We're over a heroku command
99+
// ---------------------------
100+
if (isInsideRangeBoundary(command, position)) {
101+
return { type: 'command', node: command as sh.Word };
102+
}
103+
// -------------------------------
104+
// we're over a heroku flag or arg
105+
// -------------------------------
106+
const [targetFlagOrArg] = flagsOrArgs.filter((flagOrArg) => isInsideRangeBoundary(flagOrArg, position));
107+
if (!targetFlagOrArg) {
108+
return null;
109+
}
110+
111+
const id = targetFlagOrArg.Lit();
112+
let flagKey: string = '';
113+
// This is either a command argument or a flag value.
114+
if (!id.startsWith('-')) {
115+
const idx = flagsOrArgs.indexOf(targetFlagOrArg);
116+
// back up in the list of flags or args
117+
// until we find the associated flag or
118+
// we determine this is a command arg.
119+
let i = idx;
120+
while (i-- > -1) {
121+
const maybeFlag = flagsOrArgs[i];
122+
const maybeFladId = maybeFlag?.Lit() ?? '';
123+
if (maybeFladId.startsWith('-')) {
124+
flagKey = maybeFladId.replace(/^[-]+/, '');
125+
break;
126+
}
127+
}
128+
// We didn't find a flag so this
129+
// must be a command argument
130+
if (!flagKey) {
131+
// args occur in the order in which the
132+
// properties are defined. Indexed
133+
// keys will be accurately mapped.
134+
135+
return { node: targetFlagOrArg, type: 'arg', command: command as sh.Word, argIndex: idx };
136+
}
137+
} else if (id.startsWith('-')) {
138+
// might be in the format --flag=flag-value
139+
const [flagId] = id.split('=');
140+
flagKey = flagId.replace(/^[-]+/, '');
141+
}
142+
return { node: targetFlagOrArg, type: 'flag', flagKey, command: command as sh.Word };
143+
}
50144
}

0 commit comments

Comments
 (0)