Compare commits

..

12 Commits

Author SHA1 Message Date
sawka f1a6dfb249 testing frame component 2024-04-15 22:59:13 -07:00
sawka 6319b26924 basic waveapp json -> react/html functionality sorta working 2024-04-12 16:25:52 -07:00
sawka 41fe49a54c implement ijson commands 2024-04-12 13:16:26 -07:00
sawka e044487489 tested ts ijson 2024-04-12 12:58:53 -07:00
sawka 0fdcd27fee go ijson implementation (tested), ts ijson implemented (untested) 2024-04-12 01:18:05 -07:00
Mike Sawka d923de412a add submenu support, add signal submenu to line context menu (#572) 2024-04-11 10:57:14 -07:00
Mike Sawka 15485d7235 New Context Menu Model (and implement custom block context menu) (#569)
* starting work on new dynamic context menu system

* untested contextmenu model integrated with electron api

* implement custom line context menu, copy visible output + copy full output

* implement minimize/maximize, restart, and delete
2024-04-10 23:47:33 -07:00
dependabot[bot] 59aef86e77 Bump tar from 6.2.0 to 6.2.1 (#568)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:46:43 -07:00
Mike Sawka 5353f40a20 new sidebar UI (#567) 2024-04-10 22:15:11 -07:00
Knox Lively f86f010a34 updated the OpenAICloudCompletionTelemetryOffErrorMsg to include instructions for enabling telemetry (#564) 2024-04-09 22:14:42 -06:00
Mike Sawka 73e5515e17 when the window gets focus, if our mainview is session (and no modals are open), refocus either the cmdinput or the cmd (#562) 2024-04-09 11:48:34 -07:00
Mike Sawka 6919dbfb5f force our exit trap to always run (for rtnstate commands) (#556)
* add command validation to shellapi.  mock out bash/zsh versions

* implement validate command fn bash and zsh

* test validate command

* change rtnstate commands to always end with a builtin, so we always get our exit trap to run

* simplify the rtnstate modification, don't add the 'wait' (as this is a different problem/feature)

* update schema
2024-04-09 11:33:23 -07:00
57 changed files with 2394 additions and 1723 deletions
+5 -4
View File
@@ -18,12 +18,12 @@
"appId": "dev.commandline.waveterm"
},
"dependencies": {
"@lexical/react": "^0.14.3",
"@monaco-editor/react": "^4.5.1",
"@table-nav/core": "^0.0.7",
"@table-nav/react": "^0.0.7",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.3",
"@withfig/autocomplete": "^2.652.3",
"autobind-decorator": "^2.4.0",
"base64-js": "^1.5.1",
"classnames": "^2.3.1",
@@ -32,6 +32,7 @@
"electron-squirrel-startup": "^1.0.0",
"electron-updater": "^6.1.8",
"framer-motion": "^10.16.16",
"lexical": "^0.14.3",
"mobx": "6.12",
"mobx-react": "^7.5.0",
"monaco-editor": "^0.44.0",
@@ -42,6 +43,7 @@
"papaparse": "^5.4.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-frame-component": "^5.2.6",
"react-markdown": "^9.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
@@ -51,8 +53,8 @@
"uuid": "^9.0.0",
"winston": "^3.8.2",
"xterm": "^5.3.0",
"xterm-addon-web-links": "^0.9.0",
"xterm-addon-serialize": "^0.11.0",
"xterm-addon-web-links": "^0.9.0",
"xterm-addon-webgl": "^0.16.0"
},
"devDependencies": {
@@ -78,7 +80,6 @@
"@types/throttle-debounce": "^5.0.1",
"@types/uuid": "^9.0.7",
"@types/webpack-env": "^1.18.3",
"@withfig/autocomplete-types": "^1.30.0",
"babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^12.0.0",
@@ -107,4 +108,4 @@
"scripts": {
"postinstall": "electron-builder install-app-deps"
}
}
}
+1 -1
View File
@@ -146,7 +146,7 @@ class App extends React.Component<{}, {}> {
<If condition={GlobalModel.isDev && rightSidebarCollapsed && activeMainView == "session"}>
<div className="right-sidebar-triggers">
<Button className="secondary ghost right-sidebar-trigger" onClick={this.openRightSidebar}>
<i className="fa-sharp fa-regular fa-lightbulb"></i>
<i className="fa-sharp fa-solid fa-sidebar-flip"></i>
</Button>
</div>
</If>
+5 -5
View File
@@ -6,14 +6,14 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown, Button } from "@/common/elements";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
import { commandRtnHandler, isBlank } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import * as appconst from "@/app/appconst";
import "./clientsettings.less";
import { MainView } from "@/common/elements/mainview";
import { MainView } from "../common/elements/mainview";
class ClientSettingsKeybindings extends React.Component<{}, {}> {
componentDidMount() {
@@ -70,7 +70,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return;
}
const prtn = GlobalCommandRunner.setTheme(themeSource, false);
getApi().setNativeThemeSource(themeSource);
GlobalModel.getElectronApi().setNativeThemeSource(themeSource);
commandRtnHandler(prtn, this.errorMessage);
}
@@ -107,7 +107,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
prtn = GlobalCommandRunner.releaseCheckAutoOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
getApi().changeAutoUpdate(val);
GlobalModel.getElectronApi().changeAutoUpdate(val);
}
getFontSizes(): DropdownItem[] {
+2 -2
View File
@@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel, getApi } from "@/models";
import { GlobalModel } from "@/models";
import { Modal, LinkButton } from "@/elements";
import * as util from "@/util/util";
import * as appconst from "@/app/appconst";
@@ -26,7 +26,7 @@ class AboutModal extends React.Component<{}, {}> {
@boundMethod
updateApp(): void {
getApi().installAppUpdate();
GlobalModel.getElectronApi().installAppUpdate();
}
@boundMethod
+3 -4
View File
@@ -226,13 +226,12 @@
}
.ts,
.termopts,
.renderer {
.termopts {
display: flex;
}
.renderer .renderer-icon {
margin-right: 0.5em;
.renderer-icon {
margin-right: 0.2em;
}
.metapart-mono {
+80 -2
View File
@@ -291,10 +291,10 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="meta-divider">|</div>
<div className="renderer">
<div className="renderer-icon">
<i className="fa-sharp fa-solid fa-fill renderer-icon" />
{renderer}
</div>
<div className="renderer-name">{renderer}</div>
</If>
</div>
);
@@ -665,6 +665,83 @@ class LineCmd extends React.Component<
};
};
@boundMethod
handleContextMenu(e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
let { line, screen } = this.props;
const containerType = screen.getContainerType();
const isMainContainer = containerType == appconst.LineContainer_Main;
const cmd = screen.getCmd(line);
let menu: ContextMenuItem[] = [
{ role: "copy", label: "Copy", type: "normal" },
{ role: "paste", label: "Paste", type: "normal" },
{ type: "separator" },
{ label: "Copy Command", click: () => this.copyCommandStr() },
];
const isTerminal = isBlank(line.renderer) || line.renderer == "terminal";
if (isTerminal) {
menu.push({ label: "Copy Visible Output", click: () => this.copyOutput(false) });
menu.push({ label: "Copy Full Output", click: () => this.copyOutput(true) });
}
if (isMainContainer) {
menu.push({ type: "separator" });
const isMinimized = line.linestate["wave:min"];
if (isMinimized) {
menu.push({
label: "Show Block Output",
click: () => GlobalCommandRunner.lineMinimize(line.lineid, false, true),
});
} else {
menu.push({
label: "Hide Block Output",
click: () => GlobalCommandRunner.lineMinimize(line.lineid, true, true),
});
}
if (cmd?.isRunning()) {
menu.push({ type: "separator" });
menu.push({
label: "Send Signal",
type: "submenu",
submenu: [
{ label: "SIGINT", click: () => GlobalCommandRunner.lineSignal(line.lineid, "SIGINT", true) },
{ label: "SIGTERM", click: () => GlobalCommandRunner.lineSignal(line.lineid, "SIGTERM", true) },
{ label: "SIGKILL", click: () => GlobalCommandRunner.lineSignal(line.lineid, "SIGKILL", true) },
{ type: "separator" },
{ label: "SIGUSR1", click: () => GlobalCommandRunner.lineSignal(line.lineid, "SIGUSR1", true) },
{ label: "SIGUSR2", click: () => GlobalCommandRunner.lineSignal(line.lineid, "SIGUSR2", true) },
],
});
}
menu.push({ type: "separator" });
menu.push({ label: "Restart Line", click: () => GlobalCommandRunner.lineRestart(line.lineid, true) });
menu.push({ type: "separator" });
menu.push({ label: "Delete Block", click: () => GlobalCommandRunner.lineDelete(line.lineid, true) });
}
GlobalModel.contextMenuModel.showContextMenu(menu, { x: e.clientX, y: e.clientY });
}
copyCommandStr() {
const { line, screen } = this.props;
const cmd: Cmd = screen.getCmd(line);
if (cmd != null) {
navigator.clipboard.writeText(cmd.getCmdStr());
}
}
copyOutput(fullOutput: boolean) {
const { line, screen } = this.props;
let termWrap = screen.getTermWrap(line.lineid);
if (termWrap == null) {
return;
}
let outputStr = termWrap.getOutput(fullOutput);
if (fullOutput != null) {
navigator.clipboard.writeText(outputStr);
}
}
render() {
const { screen, line, width, staticRender, visible } = this.props;
const isVisible = visible.get();
@@ -750,6 +827,7 @@ class LineCmd extends React.Component<
data-lineid={line.lineid}
data-linenum={line.linenum}
data-screenid={line.screenid}
onContextMenu={this.handleContextMenu}
>
<If condition={isSelected || cmdError}>
<div key="mask" className={cn("line-mask", { "error-mask": cmdError })}></div>
+17
View File
@@ -22,6 +22,23 @@
border-bottom: 1px solid var(--app-border-color);
}
.rsb-modes {
display: flex;
flex-direction: row;
padding: 5px 10px;
gap: 5px;
border-bottom: 1px solid var(--app-border-color);
.icon-container {
padding: 2px;
cursor: pointer;
}
.icon-container:hover {
background-color: var(--app-selected-mask-color);
}
}
&.collapsed {
display: none;
}
+34 -2
View File
@@ -1,14 +1,16 @@
// Copyright 2023, Command Line Inc.
// Copyright 2023-2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import dayjs from "dayjs";
import { If, For } from "tsx-control-statements/components";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
import { ResizableSidebar, Button } from "@/elements";
import { WaveBookDisplay } from "./wavebook";
import "./right.less";
@@ -55,6 +57,14 @@ class KeybindDevPane extends React.Component<{}, {}> {
@mobxReact.observer
class RightSideBar extends React.Component<RightSideBarProps, {}> {
mode: OV<string> = mobx.observable.box(null, { name: "RightSideBar-mode" });
setMode(mode: string) {
mobx.action(() => {
this.mode.set(mode);
})();
}
render() {
return (
<ResizableSidebar
@@ -71,9 +81,31 @@ class RightSideBar extends React.Component<RightSideBarProps, {}> {
<i className="fa-sharp fa-regular fa-xmark"></i>
</Button>
</div>
<If condition={GlobalModel.isDev}>
<div className="rsb-modes">
<div className="flex-spacer" />
<If condition={GlobalModel.isDev}>
<div
className="icon-container"
title="Show Keybinding Debugger"
onClick={() => this.setMode("keybind")}
>
<i className="fa-fw fa-sharp fa-keyboard fa-solid" />
</div>
</If>
<div
className="icon-container"
title="Show Keybinding Debugger"
onClick={() => this.setMode("wavebook")}
>
<i className="fa-sharp fa-solid fa-book-sparkles"></i>
</div>
</div>
<If condition={this.mode.get() == "keybind"}>
<KeybindDevPane></KeybindDevPane>
</If>
<If condition={this.mode.get() == "wavebook"}>
<WaveBookDisplay></WaveBookDisplay>
</If>
</React.Fragment>
)}
</ResizableSidebar>
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { If, For } from "tsx-control-statements/components";
import { GlobalModel } from "@/models";
import * as lexical from "lexical";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { $convertFromMarkdownString, $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown";
import { CodeNode, CodeHighlightNode } from "@lexical/code";
import { LinkNode } from "@lexical/link";
import { ListNode, ListItemNode } from "@lexical/list";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
import { KeybindManager } from "@/util/keyutil";
const theme = {
// Theme styling goes here
};
class WaveBookKeybindings extends React.Component<{}, {}> {
componentDidMount(): void {
const keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("pane", "wavebook", "generic:confirm", (waveEvent) => {
return true;
});
}
componentWillUnmount(): void {
const keybindManager = GlobalModel.keybindManager;
keybindManager.unregisterDomain("wavebook");
}
render() {
return null;
}
}
@mobxReact.observer
class WaveBookDisplay extends React.Component<{}, {}> {
render() {
return "playbooks";
}
}
export { WaveBookDisplay };
@@ -1,28 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { SuggestionBlob } from "@/autocomplete/runtime/model";
import { GlobalModel } from "@/models";
import cn from "classnames";
import { useMemo } from "react";
export const AutocompleteSuggestions = async (props: { curLine: string }) => {
const inputModel = GlobalModel.inputModel;
const suggestions = await useMemo(async () => await inputModel.getSuggestions(), [props.curLine]);
if (!suggestions) {
return null;
}
const items = suggestions.suggestions.map((s) => `${s.icon} ${s.name}`);
return (
<div className="autocomplete-suggestions">
{items.map((item, idx) => (
<div key={idx} className={cn("autocomplete-item")}>
{item}
</div>
))}
</div>
);
};
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
# Autocomplete parser
This is the autocomplete parser for Wave. Much of the runtime for this parser is forked from the [@microsoft/inshellisense project](https://github.com/microsoft/inshellisense). We've modified the exec code to proxy commands to the active `waveshell` instance. All suggestions, as with inshellisense, come from the [@withfig/autocomplete project](https://github.com/withfig/autocomplete). We will be supplementing these with some of our own autocomplete for our own `/slashcommands` and `metacommands`.
-2
View File
@@ -1,2 +0,0 @@
export * from "./runtime/runtime";
export * from "./utils/shell";
-65
View File
@@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import log from "../utils/log";
import { runTemplates } from "./template";
import { buildExecuteShellCommand } from "./utils";
const getGeneratorContext = (cwd: string): Fig.GeneratorContext => {
return {
environmentVariables: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] != null)
),
currentWorkingDirectory: cwd,
currentProcess: "", // TODO: define current process
sshPrefix: "", // deprecated, should be empty
isDangerous: false,
searchTerm: "", // TODO: define search term
};
};
// TODO: add support for caching, trigger, & getQueryTerm
export const runGenerator = async (
generator: Fig.Generator,
tokens: string[],
cwd: string
): Promise<Fig.Suggestion[]> => {
// TODO: support trigger
const { script, postProcess, scriptTimeout, splitOn, custom, template, filterTemplateSuggestions } = generator;
const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000);
const suggestions = [];
try {
if (script) {
const shellInput = typeof script === "function" ? script(tokens) : script;
const scriptOutput = Array.isArray(shellInput)
? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1), cwd })
: await executeShellCommand({ ...shellInput, cwd });
const scriptStdout = scriptOutput.stdout.trim();
if (postProcess) {
suggestions.push(...postProcess(scriptStdout, tokens));
} else if (splitOn) {
suggestions.push(...scriptStdout.split(splitOn).map((s) => ({ name: s })));
}
}
if (custom) {
suggestions.push(...(await custom(tokens, executeShellCommand, getGeneratorContext(cwd))));
}
if (template != null) {
const templateSuggestions = await runTemplates(template, cwd);
if (filterTemplateSuggestions) {
suggestions.push(...filterTemplateSuggestions(templateSuggestions));
} else {
suggestions.push(...templateSuggestions);
}
}
return suggestions;
} catch (e) {
const err = typeof e === "string" ? e : e instanceof Error ? e.message : e;
log.debug({ msg: "generator failed", err, script, splitOn, template });
}
return suggestions;
};
-17
View File
@@ -1,17 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export type Suggestion = {
name: string;
allNames: string[];
description?: string;
icon: string;
priority: number;
insertValue?: string;
};
export type SuggestionBlob = {
suggestions: Suggestion[];
argumentDescription?: string;
charactersToDrop?: number;
};
-75
View File
@@ -1,75 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export type CommandToken = {
token: string;
complete: boolean;
isOption: boolean;
isPersistent?: boolean;
isPath?: boolean;
isPathComplete?: boolean;
};
const cmdDelim = /(\|\|)|(&&)|(;)|(\|)/;
const spaceRegex = /\s/;
export const parseCommand = (command: string): CommandToken[] => {
const lastCommand = command.split(cmdDelim).at(-1)?.trimStart();
return lastCommand ? lex(lastCommand) : [];
};
const lex = (command: string): CommandToken[] => {
const tokens: CommandToken[] = [];
let [readingQuotedString, readingFlag, readingCmd] = [false, false, false];
let readingIdx = 0;
let readingQuoteChar = "";
[...command].forEach((char, idx) => {
const reading = readingQuotedString || readingFlag || readingCmd;
if (!reading && (char === `'` || char === `"`)) {
[readingQuotedString, readingIdx, readingQuoteChar] = [true, idx, char];
return;
} else if (!reading && char === `-`) {
[readingFlag, readingIdx] = [true, idx];
return;
} else if (!reading && !spaceRegex.test(char)) {
[readingCmd, readingIdx] = [true, idx];
return;
}
if (readingQuotedString && char === readingQuoteChar && command.at(idx - 1) !== "\\") {
readingQuotedString = false;
const complete = idx + 1 < command.length && spaceRegex.test(command[idx + 1]);
tokens.push({
token: command.slice(readingIdx, idx + 1),
complete,
isOption: false,
});
} else if ((readingFlag && spaceRegex.test(char)) || char === "=") {
readingFlag = false;
tokens.push({
token: command.slice(readingIdx, idx),
complete: true,
isOption: true,
});
} else if (readingCmd && spaceRegex.test(char)) {
readingCmd = false;
tokens.push({
token: command.slice(readingIdx, idx),
complete: true,
isOption: false,
});
}
});
const reading = readingQuotedString || readingFlag || readingCmd;
if (reading) {
tokens.push({
token: command.slice(readingIdx),
complete: false,
isOption: readingFlag,
});
}
return tokens;
};
-393
View File
@@ -1,393 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import speclist, {
diffVersionedCompletions as versionedSpeclist,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from "@withfig/autocomplete/build/index";
import { parseCommand, CommandToken } from "./parser";
import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion";
import { SuggestionBlob } from "./model";
import { buildExecuteShellCommand, resolveCwd } from "./utils";
import { Shell } from "../utils/shell";
import { getApi } from "@/models";
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any
const specSet: any = {};
(speclist as string[]).forEach((s) => {
let activeSet = specSet;
const specRoutes = s.split("/");
specRoutes.forEach((route, idx) => {
if (typeof activeSet !== "object") {
return;
}
if (idx === specRoutes.length - 1) {
const prefix = versionedSpeclist.includes(s) ? "/index.js" : `.js`;
activeSet[route] = `${s}${prefix}`;
} else {
activeSet[route] = activeSet[route] || {};
activeSet = activeSet[route];
}
});
});
const loadedSpecs: { [key: string]: Fig.Spec } = {};
const loadSpec = async (cmd: CommandToken[]): Promise<Fig.Spec | undefined> => {
const rootToken = cmd.at(0);
if (!rootToken?.complete) {
return;
}
if (loadedSpecs[rootToken.token]) {
return loadedSpecs[rootToken.token];
}
if (specSet[rootToken.token]) {
const spec = (await import(`@withfig/autocomplete/build/${specSet[rootToken.token]}`)).default;
loadedSpecs[rootToken.token] = spec;
return spec;
}
};
// this load spec function should only be used for `loadSpec` on the fly as it is cacheless
const lazyLoadSpec = async (key: string): Promise<Fig.Spec | undefined> => {
return (await import(`@withfig/autocomplete/build/${key}.js`)).default;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be implemented in below TODO
const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise<Fig.Spec | undefined> => {
return; //TODO: implement spec location loading
};
export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Promise<SuggestionBlob | undefined> => {
const activeCmd = parseCommand(cmd);
const rootToken = activeCmd.at(0);
if (activeCmd.length === 0 || !rootToken?.complete) {
return;
}
const spec = await loadSpec(activeCmd);
if (spec == null) return;
const subcommand = getSubcommand(spec);
if (subcommand == null) return;
const lastCommand = activeCmd.at(-1);
const { cwd: resolvedCwd, pathy, complete: pathyComplete } = await resolveCwd(lastCommand, cwd, shell);
if (pathy && lastCommand) {
lastCommand.isPath = true;
lastCommand.isPathComplete = pathyComplete;
}
const result = await runSubcommand(activeCmd.slice(1), subcommand, resolvedCwd);
if (result == null) return;
let charactersToDrop = lastCommand?.complete ? 0 : lastCommand?.token.length ?? 0;
if (pathy) {
charactersToDrop = pathyComplete ? 0 : getApi().pathBaseName(lastCommand?.token ?? "").length;
}
return { ...result, charactersToDrop };
};
const getPersistentOptions = (persistentOptions: Fig.Option[], options?: Fig.Option[]) => {
const persistentOptionNames = new Set(
persistentOptions.map((o) => (typeof o.name === "string" ? [o.name] : o.name)).flat()
);
return persistentOptions.concat(
(options ?? []).filter(
(o) =>
(typeof o.name == "string"
? !persistentOptionNames.has(o.name)
: o.name.some((n) => !persistentOptionNames.has(n))) && o.isPersistent === true
)
);
};
// TODO: handle subcommands that are versioned
const getSubcommand = (spec?: Fig.Spec): Fig.Subcommand | undefined => {
if (spec == null) return;
if (typeof spec === "function") {
const potentialSubcommand = spec();
if (Object.prototype.hasOwnProperty.call(potentialSubcommand, "name")) {
return potentialSubcommand as Fig.Subcommand;
}
return;
}
return spec;
};
const executeShellCommand = buildExecuteShellCommand(5000);
const genSubcommand = async (command: string, parentCommand: Fig.Subcommand): Promise<Fig.Subcommand | undefined> => {
if (!parentCommand.subcommands || parentCommand.subcommands.length === 0) return;
const subcommandIdx = parentCommand.subcommands.findIndex((s) =>
Array.isArray(s.name) ? s.name.includes(command) : s.name === command
);
if (subcommandIdx === -1) return;
const subcommand = parentCommand.subcommands[subcommandIdx];
// this pulls in the spec from the load spec and overwrites the subcommand in the parent with the loaded spec.
// then it returns the subcommand and clears the loadSpec field so that it doesn't get called again
switch (typeof subcommand.loadSpec) {
case "function": {
const partSpec = await subcommand.loadSpec(command, executeShellCommand);
if (partSpec instanceof Array) {
const locationSpecs = (await Promise.all(partSpec.map((s) => lazyLoadSpecLocation(s)))).filter(
(s) => s != null
) as Fig.Spec[];
const subcommands = locationSpecs
.map((s) => getSubcommand(s))
.filter((s) => s != null) as Fig.Subcommand[];
(parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = {
...subcommand,
...(subcommands.find((s) => s?.name == command) ?? []),
loadSpec: undefined,
};
return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx];
} else if (Object.prototype.hasOwnProperty.call(partSpec, "type")) {
const locationSingleSpec = await lazyLoadSpecLocation(partSpec as Fig.SpecLocation);
(parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = {
...subcommand,
...(getSubcommand(locationSingleSpec) ?? []),
loadSpec: undefined,
};
return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx];
} else {
(parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = {
...subcommand,
...partSpec,
loadSpec: undefined,
};
return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx];
}
}
case "string": {
const spec = await lazyLoadSpec(subcommand.loadSpec as string);
(parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = {
...subcommand,
...(getSubcommand(spec) ?? []),
loadSpec: undefined,
};
return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx];
}
case "object": {
(parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = {
...subcommand,
...(subcommand.loadSpec ?? {}),
loadSpec: undefined,
};
return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx];
}
case "undefined": {
return subcommand;
}
}
};
const getOption = (activeToken: CommandToken, options: Fig.Option[]): Fig.Option | undefined => {
return options.find((o) =>
typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token)
);
};
const getPersistentTokens = (tokens: CommandToken[]): CommandToken[] => {
return tokens.filter((t) => t.isPersistent === true);
};
const getArgs = (args: Fig.SingleOrArray<Fig.Arg> | undefined): Fig.Arg[] => {
return args instanceof Array ? args : args != null ? [args] : [];
};
const runOption = async (
tokens: CommandToken[],
option: Fig.Option,
subcommand: Fig.Subcommand,
cwd: string,
persistentOptions: Fig.Option[],
acceptedTokens: CommandToken[]
): Promise<SuggestionBlob | undefined> => {
if (tokens.length === 0) {
throw new Error("invalid state reached, option expected but no tokens found");
}
const activeToken = tokens[0];
const isPersistent = persistentOptions.some((o) =>
typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token)
);
if ((option.args instanceof Array && option.args.length > 0) || option.args != null) {
const args = option.args instanceof Array ? option.args : [option.args];
return runArg(
tokens.slice(1),
args,
subcommand,
cwd,
persistentOptions,
acceptedTokens.concat(activeToken),
true,
false
);
}
return runSubcommand(
tokens.slice(1),
subcommand,
cwd,
persistentOptions,
acceptedTokens.concat({ ...activeToken, isPersistent })
);
};
const runArg = async (
tokens: CommandToken[],
args: Fig.Arg[],
subcommand: Fig.Subcommand,
cwd: string,
persistentOptions: Fig.Option[],
acceptedTokens: CommandToken[],
fromOption: boolean,
fromVariadic: boolean
): Promise<SuggestionBlob | undefined> => {
if (args.length === 0) {
return runSubcommand(tokens, subcommand, cwd, persistentOptions, acceptedTokens, true, !fromOption);
} else if (tokens.length === 0) {
return await getArgDrivenRecommendation(
args,
subcommand,
persistentOptions,
undefined,
acceptedTokens,
fromVariadic,
cwd
);
} else if (!tokens.at(0)?.complete) {
return await getArgDrivenRecommendation(
args,
subcommand,
persistentOptions,
tokens[0],
acceptedTokens,
fromVariadic,
cwd
);
}
const activeToken = tokens[0];
if (args.every((a) => a.isOptional)) {
if (activeToken.isOption) {
const option = getOption(activeToken, persistentOptions.concat(subcommand.options ?? []));
if (option != null) {
return runOption(tokens, option, subcommand, cwd, persistentOptions, acceptedTokens);
}
return;
}
const nextSubcommand = await genSubcommand(activeToken.token, subcommand);
if (nextSubcommand != null) {
return runSubcommand(
tokens.slice(1),
nextSubcommand,
cwd,
persistentOptions,
getPersistentTokens(acceptedTokens.concat(activeToken))
);
}
}
const activeArg = args[0];
if (activeArg.isVariadic) {
return runArg(
tokens.slice(1),
args,
subcommand,
cwd,
persistentOptions,
acceptedTokens.concat(activeToken),
fromOption,
true
);
} else if (activeArg.isCommand) {
if (tokens.length <= 0) {
return;
}
const spec = await loadSpec(tokens);
if (spec == null) return;
const subcommand = getSubcommand(spec);
if (subcommand == null) return;
return runSubcommand(tokens.slice(1), subcommand, cwd);
}
return runArg(
tokens.slice(1),
args.slice(1),
subcommand,
cwd,
persistentOptions,
acceptedTokens.concat(activeToken),
fromOption,
false
);
};
const runSubcommand = async (
tokens: CommandToken[],
subcommand: Fig.Subcommand,
cwd: string,
persistentOptions: Fig.Option[] = [],
acceptedTokens: CommandToken[] = [],
argsDepleted = false,
argsUsed = false
): Promise<SuggestionBlob | undefined> => {
if (tokens.length === 0) {
return getSubcommandDrivenRecommendation(
subcommand,
persistentOptions,
undefined,
argsDepleted,
argsUsed,
acceptedTokens,
cwd
);
} else if (!tokens.at(0)?.complete) {
return getSubcommandDrivenRecommendation(
subcommand,
persistentOptions,
tokens[0],
argsDepleted,
argsUsed,
acceptedTokens,
cwd
);
}
const activeToken = tokens[0];
const activeArgsLength = subcommand.args instanceof Array ? subcommand.args.length : 1;
const allOptions = [...persistentOptions, ...(subcommand.options ?? [])];
if (activeToken.isOption) {
const option = getOption(activeToken, allOptions);
if (option != null) {
return runOption(tokens, option, subcommand, cwd, persistentOptions, acceptedTokens);
}
return;
}
const nextSubcommand = await genSubcommand(activeToken.token, subcommand);
if (nextSubcommand != null) {
return runSubcommand(
tokens.slice(1),
nextSubcommand,
cwd,
getPersistentOptions(persistentOptions, subcommand.options),
getPersistentTokens(acceptedTokens.concat(activeToken))
);
}
if (activeArgsLength <= 0) {
return; // not subcommand or option & no args exist
}
const args = getArgs(subcommand.args);
if (args.length != 0) {
return runArg(tokens, args, subcommand, cwd, allOptions, acceptedTokens, false, false);
}
// if the subcommand has no args specified, fallback to the subcommand and ignore this item
return runSubcommand(tokens.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken));
};
-311
View File
@@ -1,311 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { CommandToken } from "./parser";
import { runGenerator } from "./generator";
import { runTemplates } from "./template";
import { Suggestion, SuggestionBlob } from "./model";
import { getApi } from "@/models";
enum SuggestionIcons {
File = "📄",
Folder = "📁",
Subcommand = "📦",
Option = "🔗",
Argument = "💲",
Mixin = "🏝️",
Shortcut = "🔥",
Special = "⭐",
Default = "📀",
}
const getIcon = (icon: string | undefined, suggestionType: Fig.SuggestionType | undefined): string => {
// TODO: enable fig icons once spacing is better
// if (icon && /[^\u0000-\u00ff]/.test(icon)) {
// return icon;
// }
switch (suggestionType) {
case "arg":
return SuggestionIcons.Argument;
case "file":
return SuggestionIcons.File;
case "folder":
return SuggestionIcons.Folder;
case "option":
return SuggestionIcons.Option;
case "subcommand":
return SuggestionIcons.Subcommand;
case "mixin":
return SuggestionIcons.Mixin;
case "shortcut":
return SuggestionIcons.Shortcut;
case "special":
return SuggestionIcons.Special;
}
return SuggestionIcons.Default;
};
const getLong = (suggestion: Fig.SingleOrArray<string>): string => {
return suggestion instanceof Array ? suggestion.reduce((p, c) => (p.length > c.length ? p : c)) : suggestion;
};
const toSuggestion = (suggestion: Fig.Suggestion, name?: string, type?: Fig.SuggestionType): Suggestion | undefined => {
if (suggestion.name == null) return;
return {
name: name ?? getLong(suggestion.name),
description: suggestion.description,
icon: getIcon(suggestion.icon, type ?? suggestion.type),
allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name],
priority: suggestion.priority ?? 50,
insertValue: suggestion.insertValue,
};
};
function filter<
T extends Fig.BaseSuggestion & { name?: Fig.SingleOrArray<string>; type?: Fig.SuggestionType | undefined }
>(
suggestions: T[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
suggestionType: Fig.SuggestionType | undefined
): Suggestion[] {
if (!partialCmd)
return suggestions
.map((s) => toSuggestion(s, undefined, suggestionType))
.filter((s) => s != null) as Suggestion[];
switch (filterStrategy) {
case "fuzzy":
return suggestions
.map((s) => {
if (s.name == null) return;
if (s.name instanceof Array) {
const matchedName = s.name.find((n) => n.toLowerCase().includes(partialCmd.toLowerCase()));
return matchedName != null
? {
name: matchedName,
description: s.description,
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: s.name,
priority: s.priority ?? 50,
insertValue: s.insertValue,
}
: undefined;
}
return s.name.toLowerCase().includes(partialCmd.toLowerCase())
? {
name: s.name,
description: s.description,
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: [s.name],
priority: s.priority ?? 50,
insertValue: s.insertValue,
}
: undefined;
})
.filter((s) => s != null) as Suggestion[];
default:
return suggestions
.map((s) => {
if (s.name == null) return;
if (s.name instanceof Array) {
const matchedName = s.name.find((n) => n.toLowerCase().startsWith(partialCmd.toLowerCase()));
return matchedName != null
? {
name: matchedName,
description: s.description,
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: s.name,
insertValue: s.insertValue,
priority: s.priority ?? 50,
}
: undefined;
}
return s.name.toLowerCase().startsWith(partialCmd.toLowerCase())
? {
name: s.name,
description: s.description,
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: [s.name],
insertValue: s.insertValue,
priority: s.priority ?? 50,
}
: undefined;
})
.filter((s) => s != null) as Suggestion[];
}
}
type FilterStrategy = "fuzzy" | "prefix" | "default";
const generatorSuggestions = async (
generator: Fig.SingleOrArray<Fig.Generator> | undefined,
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
cwd: string
): Promise<Suggestion[]> => {
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
const tokens = acceptedTokens.map((t) => t.token);
if (partialCmd) tokens.push(partialCmd);
const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat();
return filter<Fig.Suggestion>(
suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })),
filterStrategy,
partialCmd,
undefined
);
};
const templateSuggestions = async (
templates: Fig.Template | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
cwd: string
): Promise<Suggestion[]> => {
return filter<Fig.Suggestion>(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined);
};
const suggestionSuggestions = (
suggestions: (string | Fig.Suggestion)[] | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Suggestion[] => {
const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? [];
return filter<Fig.Suggestion>(cleanedSuggestions ?? [], filterStrategy, partialCmd, undefined);
};
const subcommandSuggestions = (
subcommands: Fig.Subcommand[] | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Suggestion[] => {
return filter<Fig.Subcommand>(subcommands ?? [], filterStrategy, partialCmd, "subcommand");
};
const optionSuggestions = (
options: Fig.Option[] | undefined,
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Suggestion[] => {
const usedOptions = new Set(acceptedTokens.filter((t) => t.isOption).map((t) => t.token));
const validOptions = options?.filter(
(o) => o.exclusiveOn?.every((exclusiveOption) => !usedOptions.has(exclusiveOption)) ?? true
);
return filter<Fig.Option>(validOptions ?? [], filterStrategy, partialCmd, "option");
};
const removeAcceptedSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => {
const seen = new Set<string>(acceptedTokens.map((t) => t.token));
return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n)));
};
const removeDuplicateSuggestion = (suggestions: Suggestion[]): Suggestion[] => {
const seen = new Set<string>();
return suggestions
.map((s) => {
if (seen.has(s.name)) return null;
seen.add(s.name);
return s;
})
.filter((s): s is Suggestion => s != null);
};
const removeEmptySuggestion = (suggestions: Suggestion[]): Suggestion[] => {
return suggestions.filter((s) => s.name.length > 0);
};
export const getSubcommandDrivenRecommendation = async (
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
partialToken: CommandToken | undefined,
argsDepleted: boolean,
argsFromSubcommand: boolean,
acceptedTokens: CommandToken[],
cwd: string
): Promise<SuggestionBlob | undefined> => {
if (argsDepleted && argsFromSubcommand) {
return;
}
let partialCmd = partialToken?.token;
if (partialToken?.isPath) {
partialCmd = partialToken.isPathComplete ? "" : getApi().pathBaseName(partialCmd ?? "");
}
const suggestions: Suggestion[] = [];
const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
const allOptions = persistentOptions.concat(subcommand.options ?? []);
if (!argsFromSubcommand) {
suggestions.push(...subcommandSuggestions(subcommand.subcommands, subcommand.filterStrategy, partialCmd));
suggestions.push(...optionSuggestions(allOptions, acceptedTokens, subcommand.filterStrategy, partialCmd));
}
if (argLength != 0) {
const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args;
suggestions.push(
...(await generatorSuggestions(
activeArg?.generators,
acceptedTokens,
activeArg?.filterStrategy,
partialCmd,
cwd
))
);
suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd));
suggestions.push(
...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd, cwd))
);
}
return {
suggestions: removeDuplicateSuggestion(
removeEmptySuggestion(
removeAcceptedSuggestions(
suggestions.sort((a, b) => b.priority - a.priority),
acceptedTokens
)
)
),
};
};
export const getArgDrivenRecommendation = async (
args: Fig.Arg[],
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
partialToken: CommandToken | undefined,
acceptedTokens: CommandToken[],
variadicArgBound: boolean,
cwd: string
): Promise<SuggestionBlob | undefined> => {
let partialCmd = partialToken?.token;
if (partialToken?.isPath) {
partialCmd = partialToken.isPathComplete ? "" : getApi().pathBaseName(partialCmd ?? "");
}
const activeArg = args[0];
const allOptions = persistentOptions.concat(subcommand.options ?? []);
const suggestions = [
...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd)),
...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd),
...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd, cwd)),
];
if (activeArg.isOptional || (activeArg.isVariadic && variadicArgBound)) {
suggestions.push(...subcommandSuggestions(subcommand.subcommands, activeArg?.filterStrategy, partialCmd));
suggestions.push(...optionSuggestions(allOptions, acceptedTokens, activeArg?.filterStrategy, partialCmd));
}
return {
suggestions: removeDuplicateSuggestion(
removeEmptySuggestion(
removeAcceptedSuggestions(
suggestions.sort((a, b) => b.priority - a.priority),
acceptedTokens
)
)
),
argumentDescription: activeArg.description ?? activeArg.name,
};
};
-60
View File
@@ -1,60 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// const fsAsync = window.require("fs/promises");
import log from "../utils/log";
// TODO: implement filepaths template
const filepathsTemplate = async (cwd: string): Promise<Fig.TemplateSuggestion[]> => {
// const files = await fsAsync.readdir(cwd, { withFileTypes: true });
// return files
// .filter((f) => f.isFile() || f.isDirectory())
// .map((f) => ({ name: f.name, priority: 55, context: { templateType: "filepaths" } }));
return [];
};
// TODO: implement folders template
const foldersTemplate = async (cwd: string): Promise<Fig.TemplateSuggestion[]> => {
// const files = await fsAsync.readdir(cwd, { withFileTypes: true });
// return files
// .filter((f) => f.isDirectory())
// .map((f) => ({ name: f.name, priority: 55, context: { templateType: "folders" } }));
return [];
};
// TODO: implement history template
const historyTemplate = (): Fig.TemplateSuggestion[] => {
return [];
};
// TODO: implement help template
const helpTemplate = (): Fig.TemplateSuggestion[] => {
return [];
};
export const runTemplates = async (
template: Fig.TemplateStrings[] | Fig.Template,
cwd: string
): Promise<Fig.TemplateSuggestion[]> => {
const templates = template instanceof Array ? template : [template];
return (
await Promise.all(
templates.map(async (t) => {
try {
switch (t) {
case "filepaths":
return await filepathsTemplate(cwd);
case "folders":
return await foldersTemplate(cwd);
case "history":
return historyTemplate();
case "help":
return helpTemplate();
}
} catch (e) {
log.debug({ msg: "template failed", e, template: t, cwd });
return [];
}
})
)
).flat();
};

Some files were not shown because too many files have changed in this diff Show More