@microsoft/inshellisense 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { runGenerator } from "./generator.js";
4
+ import { runTemplates } from "./template.js";
5
+ var SuggestionIcons;
6
+ (function (SuggestionIcons) {
7
+ SuggestionIcons["File"] = "\uD83D\uDCC4";
8
+ SuggestionIcons["Folder"] = "\uD83D\uDCC1";
9
+ SuggestionIcons["Subcommand"] = "\uD83D\uDCE6";
10
+ SuggestionIcons["Option"] = "\uD83D\uDD17";
11
+ SuggestionIcons["Argument"] = "\uD83D\uDCB2";
12
+ SuggestionIcons["Mixin"] = "\uD83C\uDFDD\uFE0F";
13
+ SuggestionIcons["Shortcut"] = "\uD83D\uDD25";
14
+ SuggestionIcons["Special"] = "\u2B50";
15
+ SuggestionIcons["Default"] = "\uD83D\uDCC0";
16
+ })(SuggestionIcons || (SuggestionIcons = {}));
17
+ const getIcon = (suggestionType) => {
18
+ switch (suggestionType) {
19
+ case "arg":
20
+ return SuggestionIcons.Argument;
21
+ case "file":
22
+ return SuggestionIcons.File;
23
+ case "folder":
24
+ return SuggestionIcons.Folder;
25
+ case "option":
26
+ return SuggestionIcons.Option;
27
+ case "subcommand":
28
+ return SuggestionIcons.Subcommand;
29
+ case "mixin":
30
+ return SuggestionIcons.Mixin;
31
+ case "shortcut":
32
+ return SuggestionIcons.Shortcut;
33
+ case "special":
34
+ return SuggestionIcons.Special;
35
+ }
36
+ return SuggestionIcons.Default;
37
+ };
38
+ const getLong = (suggestion) => {
39
+ return suggestion instanceof Array ? suggestion.reduce((p, c) => (p.length > c.length ? p : c)) : suggestion;
40
+ };
41
+ const toSuggestion = (suggestion, name, type) => {
42
+ if (suggestion.name == null)
43
+ return;
44
+ return {
45
+ name: name ?? getLong(suggestion.name),
46
+ description: suggestion.description,
47
+ icon: getIcon(type ?? suggestion.type),
48
+ allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name],
49
+ priority: suggestion.priority ?? 50,
50
+ insertValue: suggestion.insertValue,
51
+ };
52
+ };
53
+ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
54
+ if (!partialCmd)
55
+ return suggestions.map((s) => toSuggestion(s, undefined, suggestionType)).filter((s) => s != null);
56
+ switch (filterStrategy) {
57
+ case "fuzzy":
58
+ return suggestions
59
+ .map((s) => {
60
+ if (s.name == null)
61
+ return;
62
+ if (s.name instanceof Array) {
63
+ const matchedName = s.name.find((n) => n.toLowerCase().includes(partialCmd.toLowerCase()));
64
+ return matchedName != null
65
+ ? {
66
+ name: matchedName,
67
+ description: s.description,
68
+ icon: getIcon(s.type ?? suggestionType),
69
+ allNames: s.name,
70
+ priority: s.priority ?? 50,
71
+ insertValue: s.insertValue,
72
+ }
73
+ : undefined;
74
+ }
75
+ return s.name.toLowerCase().includes(partialCmd.toLowerCase())
76
+ ? {
77
+ name: s.name,
78
+ description: s.description,
79
+ icon: getIcon(s.type ?? suggestionType),
80
+ allNames: [s.name],
81
+ priority: s.priority ?? 50,
82
+ insertValue: s.insertValue,
83
+ }
84
+ : undefined;
85
+ })
86
+ .filter((s) => s != null);
87
+ default:
88
+ return suggestions
89
+ .map((s) => {
90
+ if (s.name == null)
91
+ return;
92
+ if (s.name instanceof Array) {
93
+ const matchedName = s.name.find((n) => n.toLowerCase().startsWith(partialCmd.toLowerCase()));
94
+ return matchedName != null
95
+ ? {
96
+ name: matchedName,
97
+ description: s.description,
98
+ icon: getIcon(s.type ?? suggestionType),
99
+ allNames: s.name,
100
+ insertValue: s.insertValue,
101
+ priority: s.priority ?? 50,
102
+ }
103
+ : undefined;
104
+ }
105
+ return s.name.toLowerCase().startsWith(partialCmd.toLowerCase())
106
+ ? {
107
+ name: s.name,
108
+ description: s.description,
109
+ icon: getIcon(s.type ?? suggestionType),
110
+ allNames: [s.name],
111
+ insertValue: s.insertValue,
112
+ priority: s.priority ?? 50,
113
+ }
114
+ : undefined;
115
+ })
116
+ .filter((s) => s != null);
117
+ }
118
+ }
119
+ const generatorSuggestions = async (generator, acceptedTokens, filterStrategy, partialCmd) => {
120
+ const generators = generator instanceof Array ? generator : generator ? [generator] : [];
121
+ const tokens = acceptedTokens.map((t) => t.token);
122
+ const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens)))).flat();
123
+ return filter(suggestions, filterStrategy, partialCmd, undefined);
124
+ };
125
+ const templateSuggestions = async (templates, filterStrategy, partialCmd) => {
126
+ return filter(await runTemplates(templates ?? []), filterStrategy, partialCmd, undefined);
127
+ };
128
+ const suggestionSuggestions = (suggestions, filterStrategy, partialCmd) => {
129
+ const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? [];
130
+ return filter(cleanedSuggestions ?? [], filterStrategy, partialCmd, undefined);
131
+ };
132
+ const subcommandSuggestions = (subcommands, filterStrategy, partialCmd) => {
133
+ return filter(subcommands ?? [], filterStrategy, partialCmd, "subcommand");
134
+ };
135
+ const optionSuggestions = (options, acceptedTokens, filterStrategy, partialCmd) => {
136
+ const usedOptions = new Set(acceptedTokens.filter((t) => t.isOption).map((t) => t.token));
137
+ const validOptions = options?.filter((o) => o.exclusiveOn?.every((exclusiveOption) => !usedOptions.has(exclusiveOption)) ?? true);
138
+ return filter(validOptions ?? [], filterStrategy, partialCmd, "option");
139
+ };
140
+ const removeDuplicateSuggestions = (suggestions, acceptedTokens) => {
141
+ const seen = new Set(acceptedTokens.map((t) => t.token));
142
+ return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n)));
143
+ };
144
+ const removeEmptySuggestion = (suggestions) => {
145
+ return suggestions.filter((s) => s.name.length > 0);
146
+ };
147
+ export const getSubcommandDrivenRecommendation = async (subcommand, persistentOptions, partialCmd, argsDepleted, argsFromSubcommand, acceptedTokens) => {
148
+ if (argsDepleted && argsFromSubcommand) {
149
+ return;
150
+ }
151
+ const suggestions = [];
152
+ const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
153
+ const allOptions = persistentOptions.concat(subcommand.options ?? []);
154
+ if (!argsFromSubcommand) {
155
+ suggestions.push(...subcommandSuggestions(subcommand.subcommands, subcommand.filterStrategy, partialCmd));
156
+ suggestions.push(...optionSuggestions(allOptions, acceptedTokens, subcommand.filterStrategy, partialCmd));
157
+ }
158
+ if (argLength != 0) {
159
+ const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args;
160
+ suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)));
161
+ suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd));
162
+ suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd)));
163
+ }
164
+ return {
165
+ suggestions: removeEmptySuggestion(removeDuplicateSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens)),
166
+ };
167
+ };
168
+ export const getArgDrivenRecommendation = async (args, subcommand, persistentOptions, partialCmd, acceptedTokens, variadicArgBound) => {
169
+ const activeArg = args[0];
170
+ const allOptions = persistentOptions.concat(subcommand.options ?? []);
171
+ const suggestions = [
172
+ ...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)),
173
+ ...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd),
174
+ ...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd)),
175
+ ];
176
+ if ((activeArg.isOptional && !activeArg.isVariadic) || (activeArg.isVariadic && activeArg.isOptional && !variadicArgBound)) {
177
+ suggestions.push(...subcommandSuggestions(subcommand.subcommands, activeArg?.filterStrategy, partialCmd));
178
+ suggestions.push(...optionSuggestions(allOptions, acceptedTokens, activeArg?.filterStrategy, partialCmd));
179
+ }
180
+ return {
181
+ suggestions: removeEmptySuggestion(removeDuplicateSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens)),
182
+ argumentDescription: activeArg.description ?? activeArg.name,
183
+ };
184
+ };
@@ -0,0 +1,35 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import fsAsync from "fs/promises";
4
+ import process from "node:process";
5
+ const filepathsTemplate = async () => {
6
+ const files = await fsAsync.readdir(process.cwd(), { withFileTypes: true });
7
+ return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90 }));
8
+ };
9
+ const foldersTemplate = async () => {
10
+ const files = await fsAsync.readdir(process.cwd(), { withFileTypes: true });
11
+ return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90 }));
12
+ };
13
+ // TODO: implement history template
14
+ const historyTemplate = () => {
15
+ return [];
16
+ };
17
+ // TODO: implement help template
18
+ const helpTemplate = () => {
19
+ return [];
20
+ };
21
+ export const runTemplates = async (template) => {
22
+ const templates = template instanceof Array ? template : [template];
23
+ return (await Promise.all(templates.map(async (t) => {
24
+ switch (t) {
25
+ case "filepaths":
26
+ return await filepathsTemplate();
27
+ case "folders":
28
+ return await foldersTemplate();
29
+ case "history":
30
+ return historyTemplate();
31
+ case "help":
32
+ return helpTemplate();
33
+ }
34
+ }))).flat();
35
+ };
@@ -0,0 +1,22 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { exec, spawn } from "node:child_process";
4
+ export const buildExecuteShellCommand = (timeout) =>
5
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use cwd in the future
6
+ async (command, cwd) => {
7
+ return new Promise((resolve) => {
8
+ exec(command, { timeout }, (_, stdout, stderr) => {
9
+ resolve(stdout || stderr);
10
+ });
11
+ });
12
+ };
13
+ export const executeShellCommandTTY = async (shell, command) => {
14
+ const child = spawn(shell, ["-c", command.trim()], { stdio: "inherit" });
15
+ return new Promise((resolve) => {
16
+ child.on("close", (code) => {
17
+ resolve({
18
+ code,
19
+ });
20
+ });
21
+ });
22
+ };
@@ -0,0 +1,55 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import React, { useState, useEffect } from "react";
4
+ import { useInput, Text } from "ink";
5
+ import chalk from "chalk";
6
+ const BlinkSpeed = 530;
7
+ const CursorColor = "#FFFFFF";
8
+ export default function Input({ value, setValue, prompt, activeSuggestion, tabCompletionDropSize, }) {
9
+ const [cursorLocation, setCursorLocation] = useState(value.length);
10
+ const [cursorBlink, setCursorBlink] = useState(true);
11
+ useEffect(() => {
12
+ setTimeout(() => {
13
+ setCursorBlink(!cursorBlink);
14
+ }, BlinkSpeed);
15
+ }, [cursorBlink]);
16
+ // TODO: arrow key navigation shortcuts (emacs & vim modes)
17
+ useInput((input, key) => {
18
+ // TODO: handle delete better on unix systems: https://github.com/vadimdemedes/ink/issues/634
19
+ const windows = process.platform === "win32";
20
+ const backspaceKey = windows ? key.backspace : key.backspace || key.delete;
21
+ const deleteKey = windows ? key.delete : false;
22
+ if (backspaceKey) {
23
+ setValue([...value].slice(0, Math.max(cursorLocation - 1, 0)).join("") + [...value].slice(cursorLocation).join(""));
24
+ setCursorLocation(Math.max(cursorLocation - 1, 0));
25
+ }
26
+ else if (deleteKey) {
27
+ setValue([...value].slice(0, cursorLocation).join("") + [...value].slice(Math.min(value.length, cursorLocation + 1)).join(""));
28
+ }
29
+ else if (key.leftArrow) {
30
+ setCursorLocation(Math.max(cursorLocation - 1, 0));
31
+ }
32
+ else if (key.rightArrow) {
33
+ setCursorLocation(Math.min(cursorLocation + 1, value.length));
34
+ }
35
+ else if (key.tab) {
36
+ if (activeSuggestion) {
37
+ // TOOD: support insertValue
38
+ const newValue = [...value].slice(0, cursorLocation - tabCompletionDropSize).join("") + activeSuggestion.name + " ";
39
+ setValue(newValue);
40
+ setCursorLocation(newValue.length);
41
+ }
42
+ }
43
+ else if (input) {
44
+ setValue([...value].slice(0, cursorLocation).join("") + input + [...value].slice(cursorLocation).join(""));
45
+ setCursorLocation(cursorLocation + input.length);
46
+ }
47
+ });
48
+ const cursoredCommand = value + " ";
49
+ const cursoredText = [...cursoredCommand].slice(0, cursorLocation).join("") +
50
+ (cursorBlink ? chalk.bgHex(CursorColor).inverse([...cursoredCommand].at(cursorLocation)) : [...cursoredCommand].at(cursorLocation)) +
51
+ [...cursoredCommand].slice(cursorLocation + 1).join("");
52
+ return (React.createElement(Text, null,
53
+ prompt,
54
+ cursoredText));
55
+ }
@@ -0,0 +1,84 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import React, { useState, useCallback, useEffect } from "react";
4
+ import { Text, useInput, Box, measureElement } from "ink";
5
+ import wrapAnsi from "wrap-ansi";
6
+ const MaxSuggestions = 5;
7
+ const SuggestionWidth = 40;
8
+ const DescriptionWidth = 30;
9
+ const DescriptionMaxHeight = 6;
10
+ const BorderWidth = 2;
11
+ const ActiveSuggestionBackgroundColor = "#7D56F4";
12
+ function Description({ description }) {
13
+ wrapAnsi(description, DescriptionWidth, { hard: true });
14
+ if (description.length !== 0) {
15
+ return (React.createElement(Box, { flexDirection: "column" },
16
+ React.createElement(Box, { borderStyle: "single", width: DescriptionWidth },
17
+ React.createElement(Text, null, truncateDescription(description, DescriptionMaxHeight)))));
18
+ }
19
+ }
20
+ function truncateDescription(description, maxHeight) {
21
+ const wrappedText = wrapAnsi(description, DescriptionWidth - BorderWidth, {
22
+ trim: false,
23
+ hard: true,
24
+ });
25
+ const lines = wrappedText.split("\n");
26
+ const truncatedLines = lines.slice(0, maxHeight);
27
+ if (lines.length > maxHeight) {
28
+ truncatedLines[maxHeight - 1] = [...truncatedLines[maxHeight - 1]].slice(0, -1).join("") + "…";
29
+ }
30
+ return truncatedLines.join("\n");
31
+ }
32
+ function SuggestionList({ suggestions, activeSuggestionIdx }) {
33
+ return (React.createElement(Box, { flexDirection: "column" },
34
+ React.createElement(Box, { borderStyle: "single", width: SuggestionWidth, flexDirection: "column" }, suggestions
35
+ .map((suggestion, idx) => {
36
+ const bgColor = idx === activeSuggestionIdx ? ActiveSuggestionBackgroundColor : undefined;
37
+ return (React.createElement(Box, { key: idx },
38
+ React.createElement(Text, { backgroundColor: bgColor, wrap: "truncate-end" }, `${suggestion.icon} ${suggestion.name}`.padEnd(SuggestionWidth - BorderWidth, " "))));
39
+ })
40
+ .filter((node) => node !== undefined))));
41
+ }
42
+ export default function Suggestions({ leftPadding, setActiveSuggestion, suggestions, }) {
43
+ const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0);
44
+ const [windowWidth, setWindowWidth] = useState(500);
45
+ const page = Math.floor(activeSuggestionIndex / MaxSuggestions) + 1;
46
+ const pagedSuggestions = suggestions.filter((_, idx) => idx < page * MaxSuggestions && idx >= (page - 1) * MaxSuggestions);
47
+ const activePagedSuggestionIndex = activeSuggestionIndex % MaxSuggestions;
48
+ const activeDescription = pagedSuggestions.at(activePagedSuggestionIndex)?.description || "";
49
+ const wrappedPadding = leftPadding % windowWidth;
50
+ const maxPadding = activeDescription.length !== 0 ? windowWidth - SuggestionWidth - DescriptionWidth : windowWidth - SuggestionWidth;
51
+ const swapDescription = wrappedPadding > maxPadding;
52
+ const swappedPadding = swapDescription ? Math.max(wrappedPadding - DescriptionWidth, 0) : wrappedPadding;
53
+ const clampedLeftPadding = Math.min(Math.min(wrappedPadding, swappedPadding), maxPadding);
54
+ useEffect(() => {
55
+ setActiveSuggestion(suggestions[activeSuggestionIndex]);
56
+ }, [activeSuggestionIndex, suggestions]);
57
+ useEffect(() => {
58
+ if (suggestions.length <= activeSuggestionIndex) {
59
+ setActiveSuggestionIndex(Math.max(suggestions.length - 1, 0));
60
+ }
61
+ }, [suggestions]);
62
+ useInput((_, key) => {
63
+ if (key.upArrow) {
64
+ setActiveSuggestionIndex(Math.max(0, activeSuggestionIndex - 1));
65
+ }
66
+ if (key.downArrow) {
67
+ setActiveSuggestionIndex(Math.min(activeSuggestionIndex + 1, suggestions.length - 1));
68
+ }
69
+ });
70
+ const measureRef = useCallback((node) => {
71
+ if (node !== null) {
72
+ const { width } = measureElement(node);
73
+ setWindowWidth(width);
74
+ }
75
+ }, []);
76
+ if (suggestions.length === 0)
77
+ return React.createElement(React.Fragment, null);
78
+ return (React.createElement(Box, { ref: measureRef },
79
+ React.createElement(Box, { paddingLeft: clampedLeftPadding }, swapDescription ? (React.createElement(React.Fragment, null,
80
+ React.createElement(Description, { description: activeDescription }),
81
+ React.createElement(SuggestionList, { suggestions: pagedSuggestions, activeSuggestionIdx: activePagedSuggestionIndex }))) : (React.createElement(React.Fragment, null,
82
+ React.createElement(SuggestionList, { suggestions: pagedSuggestions, activeSuggestionIdx: activePagedSuggestionIndex }),
83
+ React.createElement(Description, { description: activeDescription }))))));
84
+ }
@@ -0,0 +1,64 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import React, { useEffect, useState } from "react";
4
+ import { Box, Text, render as inkRender, useInput, useApp } from "ink";
5
+ import chalk from "chalk";
6
+ import { availableBindings, bind, supportedShells } from "../utils/bindings.js";
7
+ let uiResult = "";
8
+ function UI() {
9
+ const { exit } = useApp();
10
+ const [selectionIdx, setSelectionIdx] = useState(0);
11
+ const [availableShells, setAvailableShells] = useState([]);
12
+ useEffect(() => {
13
+ availableBindings().then((bindings) => {
14
+ if (bindings.length == 0) {
15
+ exit();
16
+ }
17
+ setAvailableShells(bindings);
18
+ });
19
+ }, []);
20
+ useInput(async (_, key) => {
21
+ if (key.upArrow) {
22
+ setSelectionIdx(Math.max(0, selectionIdx - 1));
23
+ }
24
+ else if (key.downArrow) {
25
+ setSelectionIdx(Math.min(availableShells.length - 1, selectionIdx + 1));
26
+ }
27
+ else if (key.return) {
28
+ await bind(availableShells[selectionIdx]);
29
+ uiResult = availableShells[selectionIdx];
30
+ exit();
31
+ }
32
+ });
33
+ return (React.createElement(Box, { flexDirection: "column" },
34
+ React.createElement(Box, null,
35
+ React.createElement(Text, { bold: true }, "Select your desired shell for keybinding creation")),
36
+ React.createElement(Box, { flexDirection: "column" },
37
+ availableShells.map((shell, idx) => {
38
+ if (idx == selectionIdx) {
39
+ return (React.createElement(Text, { color: "cyan", underline: true, key: idx },
40
+ ">",
41
+ " ",
42
+ shell));
43
+ }
44
+ return (React.createElement(Text, { key: idx },
45
+ " ",
46
+ shell));
47
+ }),
48
+ supportedShells
49
+ .filter((s) => !availableShells.includes(s))
50
+ .map((shell, idx) => (React.createElement(Text, { color: "gray", key: idx },
51
+ " ",
52
+ shell,
53
+ " (already bound)"))))));
54
+ }
55
+ export const render = async () => {
56
+ const { waitUntilExit } = inkRender(React.createElement(UI, null));
57
+ await waitUntilExit();
58
+ if (uiResult.length !== 0) {
59
+ process.stdout.write("\n" + chalk.green("✓") + " successfully created new bindings \n");
60
+ }
61
+ else {
62
+ process.stdout.write("\n");
63
+ }
64
+ };
@@ -0,0 +1,71 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import React, { useCallback, useEffect, useState } from "react";
4
+ import { Text, Box, render as inkRender, measureElement, useInput, useApp } from "ink";
5
+ import wrapAnsi from "wrap-ansi";
6
+ import { getSuggestions } from "../runtime/runtime.js";
7
+ import Suggestions from "./suggestions.js";
8
+ import Input from "./input.js";
9
+ const Prompt = "> ";
10
+ let uiResult = undefined;
11
+ function UI({ startingCommand }) {
12
+ const { exit } = useApp();
13
+ const [isExiting, setIsExiting] = useState(false);
14
+ const [command, setCommand] = useState(startingCommand);
15
+ const [activeSuggestion, setActiveSuggestion] = useState();
16
+ const [tabCompletionDropSize, setTabCompletionDropSize] = useState(0);
17
+ const [suggestions, setSuggestions] = useState([]);
18
+ const [windowWidth, setWindowWidth] = useState(500);
19
+ const leftPadding = getLeftPadding(windowWidth, command);
20
+ const measureRef = useCallback((node) => {
21
+ if (node !== null) {
22
+ const { width } = measureElement(node);
23
+ setWindowWidth(width);
24
+ }
25
+ }, []);
26
+ useInput((input, key) => {
27
+ if (key.ctrl && input.toLowerCase() == "d") {
28
+ uiResult = undefined;
29
+ exit();
30
+ }
31
+ if (key.return) {
32
+ setIsExiting(true);
33
+ }
34
+ });
35
+ useEffect(() => {
36
+ if (isExiting) {
37
+ uiResult = command;
38
+ exit();
39
+ }
40
+ }, [isExiting]);
41
+ useEffect(() => {
42
+ getSuggestions(command).then((suggestions) => {
43
+ setSuggestions(suggestions?.suggestions ?? []);
44
+ setTabCompletionDropSize(suggestions?.charactersToDrop ?? 0);
45
+ });
46
+ }, [command]);
47
+ if (isExiting) {
48
+ return (React.createElement(Text, null,
49
+ Prompt,
50
+ command));
51
+ }
52
+ return (React.createElement(Box, { flexDirection: "column", ref: measureRef },
53
+ React.createElement(Box, null,
54
+ React.createElement(Text, null,
55
+ React.createElement(Input, { value: command, setValue: setCommand, prompt: Prompt, activeSuggestion: activeSuggestion, tabCompletionDropSize: tabCompletionDropSize }))),
56
+ React.createElement(Suggestions, { leftPadding: leftPadding, setActiveSuggestion: setActiveSuggestion, suggestions: suggestions })));
57
+ }
58
+ export const render = async (command) => {
59
+ uiResult = undefined;
60
+ const { waitUntilExit } = inkRender(React.createElement(UI, { startingCommand: command ?? "" }));
61
+ await waitUntilExit();
62
+ return uiResult;
63
+ };
64
+ function getLeftPadding(windowWidth, command) {
65
+ const wrappedText = wrapAnsi(command + "", windowWidth, {
66
+ trim: false,
67
+ hard: true,
68
+ });
69
+ const lines = wrappedText.split("\n");
70
+ return (lines.length - 1) * windowWidth + lines[lines.length - 1].length + Prompt.length;
71
+ }
@@ -0,0 +1,144 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import fsAsync from "node:fs/promises";
6
+ import fs from "node:fs";
7
+ import process from "node:process";
8
+ import url from "node:url";
9
+ const __filename = url.fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const cacheFolder = ".inshellisense";
12
+ export var Shell;
13
+ (function (Shell) {
14
+ Shell["Bash"] = "bash";
15
+ Shell["Powershell"] = "powershell";
16
+ Shell["Pwsh"] = "pwsh";
17
+ Shell["Zsh"] = "zsh";
18
+ Shell["Fish"] = "fish";
19
+ })(Shell || (Shell = {}));
20
+ export const supportedShells = [Shell.Bash, Shell.Powershell, Shell.Pwsh, Shell.Zsh, Shell.Fish];
21
+ const bashScriptCommand = () => {
22
+ return `[ -f ~/${cacheFolder}/key-bindings.bash ] && source ~/${cacheFolder}/key-bindings.bash`;
23
+ };
24
+ const zshScriptCommand = () => {
25
+ return `[ -f ~/${cacheFolder}/key-bindings.zsh ] && source ~/${cacheFolder}/key-bindings.zsh`;
26
+ };
27
+ const fishScriptCommand = () => {
28
+ return `[ -f ~/${cacheFolder}/key-bindings.fish ] && source ~/${cacheFolder}/key-bindings.fish`;
29
+ };
30
+ const powershellScriptCommand = () => {
31
+ const bindingsPath = path.join(os.homedir(), cacheFolder, "key-bindings-powershell.ps1");
32
+ return `if(Test-Path '${bindingsPath}' -PathType Leaf){. ${bindingsPath}}`;
33
+ };
34
+ const pwshScriptCommand = () => {
35
+ const bindingsPath = path.join(os.homedir(), cacheFolder, "key-bindings-pwsh.ps1");
36
+ return `if(Test-Path '${bindingsPath}' -PathType Leaf){. ${bindingsPath}}`;
37
+ };
38
+ const pwshConfigPath = () => {
39
+ switch (process.platform) {
40
+ case "win32":
41
+ return path.join(os.homedir(), "Documents", "Powershell", "Microsoft.PowerShell_profile.ps1");
42
+ case "linux":
43
+ case "darwin":
44
+ return path.join(os.homedir(), ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
45
+ default:
46
+ throw new Error("Unsupported platform");
47
+ }
48
+ };
49
+ export const availableBindings = async () => {
50
+ const cliConfigPath = path.join(os.homedir(), cacheFolder);
51
+ if (!fs.existsSync(cliConfigPath)) {
52
+ await fsAsync.mkdir(cliConfigPath);
53
+ }
54
+ const bindings = [];
55
+ const bashConfigPath = path.join(os.homedir(), ".bashrc");
56
+ if (!fs.existsSync(bashConfigPath)) {
57
+ bindings.push(Shell.Bash);
58
+ }
59
+ else {
60
+ const bashConfigContent = fsAsync.readFile(bashConfigPath, { encoding: "utf-8" });
61
+ if (!(await bashConfigContent).includes(bashScriptCommand())) {
62
+ bindings.push(Shell.Bash);
63
+ }
64
+ }
65
+ const zshConfigPath = path.join(os.homedir(), ".zshrc");
66
+ if (!fs.existsSync(zshConfigPath)) {
67
+ bindings.push(Shell.Zsh);
68
+ }
69
+ else {
70
+ const zshConfigContent = fsAsync.readFile(zshConfigPath, { encoding: "utf-8" });
71
+ if (!(await zshConfigContent).includes(zshScriptCommand())) {
72
+ bindings.push(Shell.Zsh);
73
+ }
74
+ }
75
+ const fishConfigPath = path.join(os.homedir(), ".config", "fish", "config.fish");
76
+ if (!fs.existsSync(fishConfigPath)) {
77
+ bindings.push(Shell.Fish);
78
+ }
79
+ else {
80
+ const fishConfigContent = fsAsync.readFile(fishConfigPath, { encoding: "utf-8" });
81
+ if (!(await fishConfigContent).includes(fishScriptCommand())) {
82
+ bindings.push(Shell.Fish);
83
+ }
84
+ }
85
+ const powershellConfigPath = path.join(os.homedir(), "Documents", "WindowsPowershell", "Microsoft.PowerShell_profile.ps1");
86
+ if (!fs.existsSync(powershellConfigPath)) {
87
+ bindings.push(Shell.Powershell);
88
+ }
89
+ else {
90
+ const powershellConfigContent = fsAsync.readFile(powershellConfigPath, { encoding: "utf-8" });
91
+ if (!(await powershellConfigContent).includes(powershellScriptCommand())) {
92
+ bindings.push(Shell.Powershell);
93
+ }
94
+ }
95
+ if (!fs.existsSync(pwshConfigPath())) {
96
+ bindings.push(Shell.Pwsh);
97
+ }
98
+ else {
99
+ const pwshConfigContent = fsAsync.readFile(pwshConfigPath(), { encoding: "utf-8" });
100
+ if (!(await pwshConfigContent).includes(pwshScriptCommand())) {
101
+ bindings.push(Shell.Pwsh);
102
+ }
103
+ }
104
+ return bindings;
105
+ };
106
+ export const bind = async (shell) => {
107
+ const cliConfigPath = path.join(os.homedir(), cacheFolder);
108
+ if (!fs.existsSync(cliConfigPath)) {
109
+ await fsAsync.mkdir(cliConfigPath);
110
+ }
111
+ switch (shell) {
112
+ case Shell.Bash: {
113
+ const bashConfigPath = path.join(os.homedir(), ".bashrc");
114
+ await fsAsync.appendFile(bashConfigPath, `\n${bashScriptCommand()}`);
115
+ await fsAsync.copyFile(path.join(__dirname, "..", "..", "shell", "key-bindings.bash"), path.join(os.homedir(), cacheFolder, "key-bindings.bash"));
116
+ break;
117
+ }
118
+ case Shell.Zsh: {
119
+ const zshConfigPath = path.join(os.homedir(), ".zshrc");
120
+ await fsAsync.appendFile(zshConfigPath, `\n${zshScriptCommand()}`);
121
+ await fsAsync.copyFile(path.join(__dirname, "..", "..", "shell", "key-bindings.zsh"), path.join(os.homedir(), cacheFolder, "key-bindings.zsh"));
122
+ break;
123
+ }
124
+ case Shell.Fish: {
125
+ const fishConfigDirectory = path.join(os.homedir(), ".config", "fish");
126
+ const fishConfigPath = path.join(fishConfigDirectory, "config.fish");
127
+ await fsAsync.mkdir(fishConfigDirectory, { recursive: true });
128
+ await fsAsync.appendFile(fishConfigPath, `\n${fishScriptCommand()}`);
129
+ await fsAsync.copyFile(path.join(__dirname, "..", "..", "shell", "key-bindings.fish"), path.join(os.homedir(), cacheFolder, "key-bindings.fish"));
130
+ break;
131
+ }
132
+ case Shell.Powershell: {
133
+ const powershellConfigPath = path.join(os.homedir(), "Documents", "WindowsPowershell", "Microsoft.PowerShell_profile.ps1");
134
+ await fsAsync.appendFile(powershellConfigPath, `\n${powershellScriptCommand()}`);
135
+ await fsAsync.copyFile(path.join(__dirname, "..", "..", "shell", "key-bindings-powershell.ps1"), path.join(os.homedir(), cacheFolder, "key-bindings-powershell.ps1"));
136
+ break;
137
+ }
138
+ case Shell.Pwsh: {
139
+ await fsAsync.appendFile(pwshConfigPath(), `\n${pwshScriptCommand()}`);
140
+ await fsAsync.copyFile(path.join(__dirname, "..", "..", "shell", "key-bindings-pwsh.ps1"), path.join(os.homedir(), cacheFolder, "key-bindings-pwsh.ps1"));
141
+ break;
142
+ }
143
+ }
144
+ };