Compare commits

...

127 Commits

Author SHA1 Message Date
Evan Simkowitz e4d3bfd90a save work 2024-04-25 14:48:30 -07:00
Evan Simkowitz 9073b9b31e add utils to assist in fs lookups 2024-04-25 14:47:44 -07:00
Evan Simkowitz cf28bb53ca fix path recommendations 2024-04-25 14:47:18 -07:00
Evan Simkowitz e59f70d26b fix option suggestions not showing if there's a complete match for one option and a partial for others 2024-04-25 11:01:37 -07:00
Evan Simkowitz deb9e23d53 Add ability to enabling autocomplete logging in clientsettings 2024-04-24 17:17:50 -07:00
Evan Simkowitz 3a9aa5813f fix file completion display 2024-04-24 16:41:37 -07:00
Evan Simkowitz c3aa027cc8 fix cwd resolution 2024-04-24 15:41:19 -07:00
Evan Simkowitz 7bdf332ce0 fix insertValue calculation 2024-04-24 15:22:39 -07:00
Evan Simkowitz 9483b43afa add default icon case, though it should never be hit 2024-04-23 18:22:16 -07:00
Evan Simkowitz 3fc4eb40cc remove unused log utils 2024-04-23 18:19:48 -07:00
Evan Simkowitz b142940a4c update import 2024-04-23 18:14:36 -07:00
Evan Simkowitz 3e4a22e7b8 handle -- argument properly 2024-04-23 18:08:01 -07:00
Evan Simkowitz b4928780d2 Merge branch 'main' into evan/inshellisense-newton-merge 2024-04-23 17:50:04 -07:00
Evan Simkowitz a5e092e6c5 move pos to {cursor} if present 2024-04-23 17:45:25 -07:00
Evan Simkowitz 6a7ee12c10 Pull in loadSpec functions from evan/inshellisense branch 2024-04-23 17:32:45 -07:00
Evan Simkowitz f5af7ad283 Merge branch 'main' into evan/inshellisense-newton-merge 2024-04-23 17:05:45 -07:00
Evan Simkowitz b115d04876 Merge branch 'main' into evan/inshellisense-newton-merge 2024-04-23 11:08:39 -07:00
Evan Simkowitz 5f755e6574 Merge branch 'main' into evan/inshellisense-newton-merge 2024-04-23 10:51:25 -07:00
Evan Simkowitz 0b74f4afcf bad merge 2024-04-22 19:48:02 -07:00
Evan Simkowitz 77706bf21e Merge branch 'evan/auxview-fix' into evan/inshellisense-newton-merge 2024-04-22 19:45:47 -07:00
Evan Simkowitz 6bd5931056 remove unused class 2024-04-22 19:40:48 -07:00
Evan Simkowitz e5ee4e4069 remove unnecessary change 2024-04-22 19:38:44 -07:00
Evan Simkowitz f05cb30dca fix ai chat scroll direction 2024-04-22 19:35:59 -07:00
Evan Simkowitz 260b29167d simplify condition 2024-04-22 19:32:44 -07:00
Evan Simkowitz e543538bae Make the auxView tilebar optional and make sure it flows as part of the flexbox 2024-04-22 19:30:30 -07:00
Evan Simkowitz bd7a83ba0e making auxview more flexible 2024-04-22 19:21:57 -07:00
Evan Simkowitz 5d1c92d643 prefix is pushing through well for option args with separators 2024-04-22 17:57:13 -07:00
Evan Simkowitz c6f187d65d support arg separators 2024-04-22 17:14:33 -07:00
Evan Simkowitz b476ab31c6 fixing more display issues 2024-04-22 15:46:40 -07:00
Evan Simkowitz 20b25a9272 Merge branch 'evan/inshellisense' into evan/inshellisense-newton-merge 2024-04-22 13:54:53 -07:00
Evan Simkowitz 35cf4b6a3b Merge branch 'main' into evan/inshellisense 2024-04-22 13:54:42 -07:00
Evan Simkowitz ff50e89156 save 2024-04-22 13:53:07 -07:00
Evan Simkowitz 024f46bf7b save work 2024-04-22 13:44:40 -07:00
Evan Simkowitz 73164e0152 almost there, just need to fix icon displays and reimplement the cached imports 2024-04-22 12:58:42 -07:00
Evan Simkowitz 0d40d6fff3 looking good 2024-04-22 12:15:47 -07:00
Evan Simkowitz 86cbba1f79 Got the results up to parity, next step is to work on performance 2024-04-19 19:01:11 -07:00
Evan Simkowitz bdf17f0133 move Newton parser to its own file 2024-04-19 16:27:19 -07:00
Evan Simkowitz f5f0791b32 Merge branch 'evan/inshellisense' into evan/inshellisense-newton 2024-04-19 15:08:40 -07:00
Evan Simkowitz 899347e90f Merge branch 'main' into evan/inshellisense 2024-04-19 15:08:27 -07:00
Evan Simkowitz fbc5e1606b added suggestions for options 2024-04-19 15:06:26 -07:00
Evan Simkowitz fde2ce97d2 save work 2024-04-17 10:29:32 -07:00
Evan Simkowitz 93845c405d Got mostly to parity with suggestions in the parser! 2024-04-16 18:01:18 -07:00
Evan Simkowitz f2b322c7d7 working to merge my parser into the inshellisense model 2024-04-15 20:22:47 -07:00
Evan Simkowitz c1bd5f5765 fix command execution 2024-04-15 17:22:25 -07:00
Evan Simkowitz b97c7e186d remove unnecessary debugs 2024-04-12 16:07:49 -07:00
Evan Simkowitz 0442fc84d4 update debugs to be more helpful 2024-04-12 16:04:05 -07:00
Evan Simkowitz 27efae3ad5 make the logic less gross 2024-04-12 16:03:27 -07:00
Evan Simkowitz 4d8c2cd242 remove unused import 2024-04-12 15:57:17 -07:00
Evan Simkowitz a560499b58 return filepaths when in doubt 2024-04-12 15:53:54 -07:00
Evan Simkowitz e8b489bcd9 add history and root spec 2024-04-12 15:35:45 -07:00
Evan Simkowitz 31c12c4705 remove and modify debugs 2024-04-11 18:25:35 -07:00
Evan Simkowitz 787bcbc9d8 replace logs with debugs 2024-04-11 18:23:34 -07:00
Evan Simkowitz 985304c9aa demote dotfiles and dotdirs 2024-04-11 18:19:00 -07:00
Evan Simkowitz 13cbfdeb6e fix isEnabled 2024-04-11 18:16:28 -07:00
Evan Simkowitz 53290b47e1 fix bad merge 2024-04-11 18:14:05 -07:00
Evan Simkowitz 4bdd092d33 remove lastCurLine from textareainput 2024-04-11 18:09:28 -07:00
Evan Simkowitz fb2b3dfc6a move import 2024-04-11 18:07:56 -07:00
Evan Simkowitz fa27964d49 move import 2024-04-11 18:06:58 -07:00
Evan Simkowitz 95132011e4 remove import 2024-04-11 18:06:43 -07:00
Evan Simkowitz dae4210b72 rename stuff and add comments 2024-04-11 18:03:04 -07:00
Evan Simkowitz e394ba70a6 move keybinding 2024-04-11 17:55:28 -07:00
Evan Simkowitz 483b2eff6f add right arrow to apply suggestion 2024-04-11 17:54:54 -07:00
Evan Simkowitz 2f4a6fa6e2 Merge branch 'main' into evan/inshellisense 2024-04-11 17:45:24 -07:00
Evan Simkowitz 4af1b19ffd fix lastCurLine 2024-04-11 17:41:11 -07:00
Evan Simkowitz 8b1d930cd6 move import 2024-04-11 17:38:24 -07:00
Evan Simkowitz a4e8648968 add placeholder div if no suggestions 2024-04-11 17:37:50 -07:00
Evan Simkowitz 429ea37419 change onclose 2024-04-11 17:23:16 -07:00
Evan Simkowitz a8f3e356f6 add keybindings 2024-04-11 17:22:51 -07:00
Evan Simkowitz 85235aaad8 move autocomplete to its own model, add more styling to suggestion view 2024-04-11 17:16:22 -07:00
Evan Simkowitz 619cfe4931 rudimentary show all autocomplete 2024-04-11 16:00:52 -07:00
Evan Simkowitz 11f1c3bb70 remove finished todos 2024-04-11 15:50:46 -07:00
Evan Simkowitz 8d140c9920 Add file and folder completions 2024-04-11 15:49:46 -07:00
Evan Simkowitz 205a9d1e2e Merge branch 'main' into evan/inshellisense 2024-04-10 23:11:14 -07:00
Evan Simkowitz d3139d51cf add new license 2024-04-10 22:08:08 -07:00
Evan Simkowitz 5c931de40c fix caching 2024-04-10 21:48:23 -07:00
Evan Simkowitz 497a96bfc2 adds trigger and begins to add caching 2024-04-10 17:20:51 -07:00
Evan Simkowitz 427447c592 Merge branch 'main' into evan/inshellisense 2024-04-09 17:11:25 -07:00
Evan Simkowitz 1fe5d425da set non-transparent colors for light mode, fix fuzzy letters issue with ghost prompt 2024-04-09 17:07:15 -07:00
Evan Simkowitz a395043a1c enforce setting 2024-04-09 16:40:47 -07:00
Evan Simkowitz ff9f9b43b6 Add client setting for autocomplete 2024-04-09 16:29:59 -07:00
Evan Simkowitz 98ba0b46ac clear suggestions in more places 2024-04-09 12:33:16 -07:00
Evan Simkowitz fb3253d018 cache last command 2024-04-09 12:22:27 -07:00
Evan Simkowitz 8d91d156aa handle flicker when deleting chars 2024-04-09 12:09:42 -07:00
Evan Simkowitz b7eec15853 Adding ghost text 2024-04-09 11:57:07 -07:00
Evan Simkowitz 38ff6183a2 get suggestions on typing 2024-04-08 18:47:35 -07:00
Evan Simkowitz 1b1e6edaee Merge branch 'main' into evan/inshellisense 2024-04-08 13:47:17 -07:00
Evan Simkowitz f4aeb92d32 Merge branch 'main' into evan/inshellisense 2024-04-08 13:16:01 -07:00
Evan Simkowitz b780d5b244 Merge branch 'main' into evan/inshellisense 2024-04-08 11:19:25 -07:00
Evan Simkowitz d81a6ed480 Merge branch 'main' into evan/inshellisense 2024-04-05 10:49:22 -07:00
Evan Simkowitz d8393ae50b Merge branch 'main' into evan/inshellisense 2024-04-02 18:48:59 -07:00
Evan Simkowitz 79deb01ea4 save work 2024-04-02 18:48:38 -07:00
Evan Simkowitz 12f9a50686 beginning to add autocomplete to textareainput 2024-04-02 18:35:12 -07:00
Evan Simkowitz 239933be3e Got autocomplete working! 2024-04-02 18:14:49 -07:00
Evan Simkowitz 4692cf698b hook up runtime 2024-04-02 17:13:54 -07:00
Evan Simkowitz 907e87c217 expand cwd 2024-04-02 17:05:14 -07:00
Evan Simkowitz f97bb699a1 plumb timeout to waveshell 2024-04-02 16:57:34 -07:00
Evan Simkowitz 077545ed93 bad merrge 2024-04-02 15:50:46 -07:00
Evan Simkowitz 60d0f0e976 Merge branch 'main' into evan/inshellisense 2024-04-02 15:49:32 -07:00
Evan Simkowitz cd4671e5bb store pointer to syncs 2024-04-02 14:58:39 -07:00
Evan Simkowitz d8a714dd4a ensure only one downstream writer can read from the buffer 2024-04-02 14:28:15 -07:00
Evan Simkowitz e0fa1b2560 document UsePty 2024-04-02 14:19:46 -07:00
Evan Simkowitz 2dffcc9237 add UsePty to EphemeralRunOpts 2024-04-02 14:17:12 -07:00
Evan Simkowitz 70090b8684 add package comment 2024-04-02 14:08:10 -07:00
Evan Simkowitz 3f8952caf4 more naming changes 2024-04-02 14:06:51 -07:00
Evan Simkowitz 15a2486e94 delete unused package 2024-04-02 14:01:57 -07:00
Evan Simkowitz 38aa7016b8 comment 2024-04-02 14:01:03 -07:00
Evan Simkowitz cff353ca94 formatting 2024-04-02 13:59:59 -07:00
Evan Simkowitz 5b440ace65 Making EphemeralWriteCloser into a more generic BufferedPipe 2024-04-02 13:58:56 -07:00
Evan Simkowitz 87de0dbc44 revert waveshell changes 2024-04-01 22:17:44 -07:00
Evan Simkowitz 74839b8e1b env vars are now working 2024-04-01 22:12:58 -07:00
Evan Simkowitz 8a24953a88 fix stdout/stderr 2024-04-01 20:55:23 -07:00
Evan Simkowitz a95854514a Add separate paths for stdout and stderr writers 2024-04-01 20:51:26 -07:00
Evan Simkowitz 229fcf8cd4 got cwd override working 2024-04-01 18:27:18 -07:00
Evan Simkowitz 83542ddab8 clean up code 2024-04-01 17:42:03 -07:00
Evan Simkowitz b520242f04 fixing spaghetti 2024-04-01 17:12:51 -07:00
Evan Simkowitz bc73ed1b6c save debug statements 2024-04-01 16:04:42 -07:00
Evan Simkowitz c4647945a4 fix bad merge 2024-04-01 14:22:23 -07:00
Evan Simkowitz 3db40b1c63 Merge branch 'main' into evan/inshellisense 2024-04-01 14:22:17 -07:00
Evan Simkowitz f6d7c37915 save debugging 2024-03-29 16:54:29 -07:00
Evan Simkowitz a41e866b1f Command pipes thru, triggers infinite loop 2024-03-28 18:44:37 -07:00
Evan Simkowitz a772715ae4 Add EphemeralWriteCloser 2024-03-28 18:02:27 -07:00
Evan Simkowitz d32e2aae05 Merge branch 'main' into evan/inshellisense 2024-03-28 16:24:27 -07:00
Evan Simkowitz 3f9d52a7e4 save work 2024-03-26 16:04:37 -07:00
Evan Simkowitz a239a8d70e Merge branch 'main' into evan/inshellisense 2024-03-26 13:00:21 -07:00
Evan Simkowitz a1bddd971f save work 2024-03-25 17:43:02 -07:00
Evan Simkowitz 722318f57d save work, starting to add backend types 2024-03-25 10:54:27 -07:00
Evan Simkowitz 3b633328a1 initial 2024-03-22 16:01:08 -07:00
44 changed files with 3067 additions and 101 deletions
+4
View File
@@ -24,6 +24,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",
@@ -46,6 +47,7 @@
"react-markdown": "^9.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"shell-quote": "^1.8.1",
"sprintf-js": "^1.1.2",
"throttle-debounce": "^5.0.0",
"tsx-control-statements": "^5.1.1",
@@ -76,10 +78,12 @@
"@types/papaparse": "^5.3.10",
"@types/react": "^18.0.12",
"@types/semver": "^7.5.6",
"@types/shell-quote": "^1.7.5",
"@types/sprintf-js": "^1.1.3",
"@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",
+2 -2
View File
@@ -9,8 +9,8 @@
--app-accent-color: rgb(75, 166, 57);
--app-accent-bg-color: rgba(75, 166, 57, 0.2);
--app-text-color: rgb(0, 0, 0);
--app-text-primary-color: rgb(0, 0, 0, 0.9);
--app-text-secondary-color: rgb(0, 0, 0, 0.7);
--app-text-primary-color: rgb(23, 23, 23);
--app-text-secondary-color: rgb(76, 76, 76);
--app-border-color: rgb(139 145 138);
--app-panel-bg-color: rgb(224, 224, 224);
--app-panel-bg-color-dev: rgb(224, 224, 224);
+1
View File
@@ -68,3 +68,4 @@ export const ErrorCode_InvalidCwd = "ERRCWD";
export const InputAuxView_History = "history";
export const InputAuxView_Info = "info";
export const InputAuxView_AIChat = "aichat";
export const InputAuxView_Suggestions = "suggestions";
+32 -1
View File
@@ -11,9 +11,9 @@ import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/commo
import { commandRtnHandler, isBlank } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import * as appconst from "@/app/appconst";
import { MainView } from "@/common/elements/mainview";
import "./clientsettings.less";
import { MainView } from "../common/elements/mainview";
class ClientSettingsKeybindings extends React.Component<{}, {}> {
componentDidMount() {
@@ -110,6 +110,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
GlobalModel.getElectronApi().changeAutoUpdate(val);
}
@boundMethod
handleChangeAutocompleteEnabled(val: boolean): void {
const prtn: Promise<CommandRtnType> = GlobalCommandRunner.setAutocompleteEnabled(val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeAutocompleteDebuggingEnabled(val: boolean): void {
mobx.action(() => {
GlobalModel.autocompleteModel.loggingEnabled = val;
})();
}
getFontSizes(): DropdownItem[] {
const availableFontSizes: DropdownItem[] = [];
for (let s = appconst.MinFontSize; s <= appconst.MaxFontSize; s++) {
@@ -354,6 +367,24 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Command Autocomplete</div>
<div className="settings-input">
<Toggle
checked={cdata.clientopts.autocompleteenabled ?? false}
onChange={this.handleChangeAutocompleteEnabled}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Command Autocomplete Debugging</div>
<div className="settings-input">
<Toggle
checked={GlobalModel.autocompleteModel.loggingEnabled}
onChange={this.handleChangeAutocompleteDebuggingEnabled}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
</MainView>
+2 -1
View File
@@ -88,7 +88,7 @@ class AIChat extends React.Component<{}, {}> {
}
submitChatMessage(messageStr: string) {
const curLine = GlobalModel.inputModel.getCurLine();
const curLine = GlobalModel.inputModel.curLine;
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => {
if (!rtn.success) {
@@ -107,6 +107,7 @@ class AIChat extends React.Component<{}, {}> {
mobx.action(() => {
GlobalModel.inputModel.setAuxViewFocus(true);
})();
this.onTextAreaChange(e);
}
onTextAreaBlur(e: any) {
+4
View File
@@ -19,6 +19,7 @@ import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
import { AIChat } from "./aichat";
import * as util from "@/util/util";
import * as appconst from "@/app/appconst";
import { AutocompleteSuggestionView } from "./suggestionview";
import "./cmdinput.less";
@@ -196,6 +197,9 @@ class CmdInput extends React.Component<{}, {}> {
<When condition={openView === appconst.InputAuxView_Info}>
<InfoMsg key="infomsg" />
</When>
<When condition={openView === appconst.InputAuxView_Suggestions}>
<AutocompleteSuggestionView />
</When>
</Choose>
<If condition={remote && remote.status != "connected"}>
<div className="remote-status-warning">
+1 -1
View File
@@ -225,7 +225,7 @@ class HistoryInfo extends React.Component<{}, {}> {
render() {
const inputModel = GlobalModel.inputModel;
const selItem = inputModel.getHistorySelectedItem();
const hitems = inputModel.getFilteredHistoryItems().slice().reverse();
const hitems = inputModel.filteredHistoryItems.slice().reverse();
const opts = inputModel.historyQueryOpts.get();
let hitem: HistoryItem = null;
let snames: Record<string, string> = {};
@@ -0,0 +1,25 @@
.suggestions-view .auxview-content {
display: flex;
flex-direction: column;
min-height: 1em;
.suggestion-item {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
border-radius: 5px;
&:hover {
background-color: var(--table-tr-hover-bg-color);
}
&.is-selected {
font-weight: bold;
color: var(--app-text-primary-color);
background-color: var(--table-tr-selected-bg-color);
&:hover {
background-color: var(--table-tr-selected-hover-bg-color);
}
}
}
}
@@ -0,0 +1,85 @@
import { AuxiliaryCmdView } from "./auxview";
import { GlobalModel } from "@/models";
import cn from "classnames";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
import { If } from "tsx-control-statements/components";
import "./suggestionview.less";
import { getAll, getFirst } from "@/autocomplete/runtime/utils";
export const AutocompleteSuggestionView: React.FC = observer(() => {
const inputModel = GlobalModel.inputModel;
const autocompleteModel = GlobalModel.autocompleteModel;
const [selectedSuggestion, setSelectedSuggestion] = React.useState<number>(0);
const updateScroll = (index: number) => {
setSelectedSuggestion(index);
autocompleteModel.setPrimarySuggestionIndex(index);
const element = document.getElementsByClassName("suggestion-item")[index] as HTMLElement;
if (element) {
element.scrollIntoView({ block: "nearest" });
}
};
useEffect(() => {
const keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("pane", "autocomplete", "generic:confirm", (waveEvent) => {
setSuggestion(selectedSuggestion);
return true;
});
keybindManager.registerKeybinding("pane", "autocomplete", "generic:cancel", (waveEvent) => {
closeView();
return true;
});
keybindManager.registerKeybinding("pane", "autocomplete", "generic:selectAbove", (waveEvent) => {
updateScroll(Math.max(0, selectedSuggestion - 1));
return true;
});
keybindManager.registerKeybinding("pane", "autocomplete", "generic:selectBelow", (waveEvent) => {
updateScroll(Math.min(suggestions?.length - 1, selectedSuggestion + 1));
return true;
});
keybindManager.registerKeybinding("pane", "autocomplete", "generic:tab", (waveEvent) => {
updateScroll(Math.min(suggestions?.length - 1, selectedSuggestion + 1));
return true;
});
return () => {
GlobalModel.keybindManager.unregisterDomain("autocomplete");
};
});
const suggestions: Fig.Suggestion[] = autocompleteModel.getSuggestions();
const closeView = () => {
inputModel.closeAuxView();
};
const setSuggestion = (idx: number) => {
console.log("setSuggestion", idx);
autocompleteModel.applySuggestion(idx);
autocompleteModel.loadSuggestions();
closeView();
};
return (
<AuxiliaryCmdView title="Suggestions" className="suggestions-view" onClose={closeView} scrollable={true}>
<If condition={!suggestions || suggestions.length == 0}>
<div className="no-suggestions">No suggestions</div>
</If>
{suggestions?.map((suggestion, idx) => (
<option
key={getFirst(suggestion.name)}
title={suggestion.description}
className={cn("suggestion-item", { "is-selected": selectedSuggestion === idx })}
onClick={() => {
setSuggestion(idx);
}}
>
{`${suggestion.icon} ${suggestion.displayName ?? getAll(suggestion.name).join(",")} ${
suggestion.description ? `- ${suggestion.description}` : ""
}`}
</option>
))}
</AuxiliaryCmdView>
);
});
+29 -22
View File
@@ -109,30 +109,17 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
return;
}
const inputObject = this.props.inputObject;
this.lastTab = false;
const keybindManager = GlobalModel.keybindManager;
const inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "cmdinput", "cmdinput:autocomplete", (waveEvent) => {
const lastTab = this.lastTab;
this.lastTab = true;
this.curPress = "tab";
const curLine = inputModel.getCurLine();
if (lastTab) {
GlobalModel.submitCommand(
"_compgen",
null,
[curLine],
{ comppos: String(curLine.length), compshow: "1", nohist: "1" },
true
);
if (this.lastTab) {
const curLine = inputModel.curLine;
if (curLine != "") {
inputModel.setActiveAuxView(appconst.InputAuxView_Suggestions);
}
} else {
GlobalModel.submitCommand(
"_compgen",
null,
[curLine],
{ comppos: String(curLine.length), nohist: "1" },
true
);
this.lastTab = true;
}
return true;
});
@@ -203,6 +190,10 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
const rtn = inputObject.arrowDownPressed();
return rtn;
});
keybindManager.registerKeybinding("pane", "cmdinput", "generic:selectRight", (waveEvent) => {
inputObject.arrowRightPressed();
return true;
});
keybindManager.registerKeybinding("pane", "cmdinput", "generic:selectPageAbove", (waveEvent) => {
this.curPress = "historyupdown";
inputObject.scrollPage(true);
@@ -397,6 +388,11 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
return true;
}
@boundMethod
arrowRightPressed() {
GlobalModel.autocompleteModel.applyPrimarySuggestion();
}
scrollPage(up: boolean) {
const inputModel = GlobalModel.inputModel;
const infoScroll = inputModel.hasScrollingInfoMsg();
@@ -560,7 +556,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
render() {
const model = GlobalModel;
const inputModel = model.inputModel;
const curLine = inputModel.getCurLine();
const curLine = inputModel.curLine;
let displayLines = 1;
const numLines = curLine.split("\n").length;
const maxCols = this.getTextAreaMaxCols();
@@ -606,6 +602,9 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
const renderCmdInputKeybindings = inputModel.shouldRenderAuxViewKeybindings(null);
const renderHistoryKeybindings = inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_History);
// Will be null if the feature is disabled
const primaryAutocompleteSuggestion = GlobalModel.autocompleteModel.getPrimarySuggestionCompletion();
return (
<div
className="textareainput-div control is-expanded"
@@ -613,15 +612,23 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
style={{ height: computedOuterHeight }}
>
<If condition={renderCmdInputKeybindings}>
<CmdInputKeybindings inputObject={this}></CmdInputKeybindings>
<CmdInputKeybindings inputObject={this} />
</If>
<If condition={renderHistoryKeybindings}>
<HistoryKeybindings inputObject={this}></HistoryKeybindings>
<HistoryKeybindings />
</If>
<If condition={!util.isBlank(shellType)}>
<div className="shelltag">{shellType}</div>
</If>
<If condition={primaryAutocompleteSuggestion}>
<div
className="textarea-ghost"
style={{ height: computedInnerHeight, minHeight: computedInnerHeight, fontSize: termFontSize }}
>
{`${"\xa0".repeat(curLine.length)}${primaryAutocompleteSuggestion}`}
</div>
</If>
<textarea
key="main"
ref={this.mainInputRef}
+5
View File
@@ -0,0 +1,5 @@
# Newton autocomplete parser
Newton is a Fig-compatible autocomplete parser. It builds on a lot of goodness from the [@microsoft/inshellisense project](https://github.com/microsoft/inshellisense), with heavy modifications to minimize recursion and allow for caching of intermediate states. 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`.
Any exec commands that need to be run are proxied through the Wave backend to ensure no additional permissions are required.
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
export * from "./runtime/runtime";
export * from "./utils/shell";
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/generator.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { GlobalModel } from "@/models";
import log from "../utils/log";
import { runTemplates } from "./template";
import { buildExecuteShellCommand } from "./utils";
const getGeneratorContext = async (cwd: string): Promise<Fig.GeneratorContext> => {
const resp = await GlobalModel.submitEphemeralCommand("eval", null, ["env"], null, false, {
expectsresponse: true,
overridecwd: cwd,
env: null,
});
const { stdout, stderr } = await GlobalModel.getEphemeralCommandOutput(resp);
if (stderr) {
log.debug({ msg: "failed to get environment variables", stderr });
}
const env = {};
stdout
.split("\n")
.filter((s) => s.length > 0)
.forEach((line) => {
const [key, value] = line.split("=");
env[key] = value;
});
return {
environmentVariables: env,
currentWorkingDirectory: cwd,
currentProcess: "", // TODO: define current process
sshPrefix: "", // deprecated, should be empty
isDangerous: false,
searchTerm: "", // TODO: define search term
};
};
let lastFirstToken = "";
let lastFinalToken = "";
let cachedSuggestions: Fig.Suggestion[] = [];
// TODO: add support getQueryTerm
export const runGenerator = async (
generator: Fig.Generator,
tokens: string[],
cwd: string
): Promise<Fig.Suggestion[]> => {
const { script, postProcess, scriptTimeout, splitOn, custom, template, filterTemplateSuggestions, trigger } =
generator;
const newToken = tokens.at(-1) ?? "";
if (lastFirstToken == tokens.at(0) && trigger && cachedSuggestions.length > 0) {
log.debug("trigger", trigger);
if (typeof trigger === "string") {
if (!newToken?.includes(trigger)) {
log.debug("trigger string", newToken, trigger);
return cachedSuggestions;
}
} else if (typeof trigger === "function") {
log.debug("trigger function", "newToken:", newToken, "lastToken: ", lastFinalToken);
if (!trigger(newToken, lastFinalToken ?? "")) {
log.debug("trigger function false");
return cachedSuggestions;
} else {
log.debug("trigger function true");
}
} else {
switch (trigger.on) {
case "change": {
log.debug("trigger change", newToken, lastFinalToken);
if (lastFinalToken && newToken && lastFinalToken === newToken) {
log.debug("trigger change false");
return cachedSuggestions;
} else {
log.debug("trigger change true");
}
break;
}
case "match": {
if (Array.isArray(trigger.string)) {
log.debug("trigger match array", newToken, trigger.string);
if (!trigger.string.some((t) => newToken === t)) {
log.debug("trigger match false");
return cachedSuggestions;
} else {
log.debug("trigger match true");
}
} else if (trigger.string !== newToken) {
log.debug("trigger match single true", newToken, trigger.string);
return cachedSuggestions;
} else {
log.debug("trigger match single false", newToken, trigger.string);
}
break;
}
case "threshold": {
log.debug("trigger threshold", newToken, lastFinalToken, trigger.length);
if (Math.abs(newToken.length - lastFinalToken.length) < trigger.length) {
log.debug("trigger threshold false");
return cachedSuggestions;
} else {
log.debug("trigger threshold true");
}
break;
}
}
}
} else if (lastFirstToken === tokens.at(0) && newToken && lastFinalToken === newToken) {
log.debug("lastToken === newToken", lastFinalToken, newToken);
return cachedSuggestions;
}
log.debug("lastToken !== newToken", lastFinalToken, newToken);
const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000);
const suggestions = [];
lastFinalToken = tokens[-1];
lastFirstToken = tokens[0];
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();
const scriptStderr = scriptOutput.stderr.trim();
if (scriptStderr) {
log.debug("script error, skipping processing", scriptStderr);
} else if (postProcess) {
suggestions.push(...postProcess(scriptStdout, tokens));
} else if (splitOn) {
suggestions.push(...scriptStdout.split(splitOn).map((s) => ({ name: s })));
}
}
if (custom) {
log.debug("custom", custom);
const customSuggestions = await custom(tokens, executeShellCommand, await getGeneratorContext(cwd));
log.debug("customSuggestions", customSuggestions);
suggestions.push(...customSuggestions);
}
if (template != null) {
const templateSuggestions = await runTemplates(template, cwd);
if (filterTemplateSuggestions) {
suggestions.push(...filterTemplateSuggestions(templateSuggestions));
} else {
suggestions.push(...templateSuggestions);
}
}
cachedSuggestions = suggestions;
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;
};
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/runtime.ts
// 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 log from "../utils/log";
import { buildExecuteShellCommand, mergeSubcomands } from "./utils";
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any
const specSet: any = {};
const rootSpec: Fig.Spec = {
name: "root",
filterStrategy: "prefix",
subcommands: (speclist as string[])
.filter((s) => !s.includes("/") && !s.includes("@")) // filter out versioned commands and subcommands in subdirectories
.map((s) => {
return {
name: s,
loadSpec: s,
};
}),
};
const filepathSpec: Fig.Spec = {
name: "filepaths",
args: {
name: "filepaths",
isVariadic: true,
template: "filepaths",
},
};
(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 } = {};
/**
* Loads the spec for the current command. If the spec has been loaded already, it will be returned.
* If the command defines a `loadSpec` function, that function is run and the result is set as the new spec.
* Otherwise, the spec is set to the command itself.
* @param specName The name of the spec to load.
* @param entries The entries to pass to the spec's `generateSpec` function, if it exists.
* @returns The loaded spec, or undefined if the spec could not be loaded.
*/
export const loadSpec = async (specName: string, entries: string[]): Promise<Fig.Spec | undefined> => {
if (!specName) {
log.debug("specName empty, returning undefined");
return;
}
try {
log.debug("loading spec: ", specName);
let spec: any;
if (loadedSpecs[specName]) {
log.debug("loaded spec found");
return loadedSpecs[specName];
}
if (specSet[specName]) {
log.debug("loading spec");
spec = await import(`@withfig/autocomplete/build/${specSet[specName]}`);
} else {
log.debug("no spec found, returning undefined");
return;
}
if (Object.hasOwn(spec, "getVersionCommand") && typeof spec.getVersionCommand === "function") {
log.debug("has getVersionCommand fn");
const commandVersion = await (spec.getVersionCommand as Fig.GetVersionCommand)(
buildExecuteShellCommand(5000)
);
log.debug("commandVersion: " + commandVersion);
log.debug("returning as version is not supported");
return;
}
if (typeof spec.default === "object") {
const command = spec.default as Fig.Subcommand;
log.debug("Spec is valid Subcommand", command);
if (command.generateSpec) {
log.debug("has generateSpec function");
const generatedSpec = await command.generateSpec(entries, buildExecuteShellCommand(5000));
log.debug("generatedSpec: ", generatedSpec);
spec = mergeSubcomands(command, generatedSpec);
} else {
log.debug("no generateSpec function");
spec = command;
}
loadedSpecs[specName] = spec;
return spec;
} else {
log.debug("Spec is not valid Subcommand");
return;
}
} catch (e) {
console.warn("import failed: ", e);
}
};
// this load spec function should only be used for `loadSpec` on the fly as it is cacheless
export 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
export const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise<Fig.Spec | undefined> => {
return; //TODO: implement spec location loading
};
/**
* Returns the subcommand from a spec if it exists.
* @param spec The spec to get the subcommand from.
* @returns The subcommand, or undefined if the spec does not contain a subcommand.
*/
export const getSubcommand = (spec?: Fig.Spec): Fig.Subcommand | undefined => {
// TODO: handle subcommands that are versioned
if (spec == null) return;
if (typeof spec === "function") {
const potentialSubcommand = spec();
if (Object.hasOwn(potentialSubcommand, "name")) {
return potentialSubcommand as Fig.Subcommand;
}
return;
}
return spec;
};
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
import * as shellQuote from "shell-quote";
import { Shell } from "../utils/shell";
import { Newton } from "./newton";
import { MemCache } from "@/util/memcache";
import log from "../utils/log";
const parserCache = new MemCache<string, Newton>(1000 * 60 * 5);
/**
* Starting from the end of the entry array, find the last sequence of strings, stopping when a non-string (i.e. an operand) is found.
* @param entry The entry array to search.
* @returns The last sequence of strings, i.e. the last statement. If no strings are found, returns an empty array.
*/
function findLastStmt(entry: shellQuote.ParseEntry[]): string[] {
let entries: string[] = [];
for (let i = entry.length - 1; i >= 0; i--) {
let entryValue = entry[i].valueOf();
if (typeof entryValue == "string") {
entries.unshift(entryValue);
} else {
break;
}
}
return entries;
}
export async function getSuggestions(curLine: string, cwd: string, shell: Shell): Promise<Fig.Suggestion[]> {
log.debug("getSuggestions", curLine, cwd, shell);
const entry = shellQuote.parse(curLine);
if (curLine.endsWith(" ")) {
// shell-quote doesn't include trailing space in parse. We need to know this to determine if we should suggest subcommands
entry.push(" ");
}
const lastStmt = findLastStmt(entry);
const lastStmtStr = lastStmt.slice(0, lastStmt.length - 2).join(" ");
// let parser: Newton = parserCache.get(lastStmtStr);
// if (parser) {
// console.log("Using cached parser");
// parser.cwd = cwd;
// parser.shell = shell;
// parser.entries = lastStmt;
// parser.entryIndex = parser.entryIndex - 1;
// } else {
// console.log("Creating new parser");
// parser = new Newton(undefined, lastStmt, cwd, shell);
// }
const parser: Newton = new Newton(undefined, lastStmt, cwd, shell);
const retVal = await parser.generateSuggestions();
parserCache.put(lastStmtStr, parser);
return retVal;
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/suggestion.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
enum SuggestionIcons {
File = "📄",
Folder = "📁",
Subcommand = "📦",
Option = "🔗",
Argument = "💲",
Mixin = "🏝️",
Shortcut = "🔥",
Special = "⭐",
Default = "📀",
}
export 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;
default:
return SuggestionIcons.Default;
}
};
export type FilterStrategy = "fuzzy" | "prefix" | "default";
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/template.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { GlobalModel } from "@/models";
import log from "../utils/log";
/**
* Retrieves the contents of the specified directory on the active remote machine.
* @param cwd The directory whose contents should be returned.
* @param tempType The template to use when returning the contents. If "folders" is passed, only the directories within the specified directory will be returned. Otherwise, all the contents will be returned.
* @returns The contents of the directory formatted to the specified template.
*/
export const getFileCompletionSuggestions = async (
cwd: string,
overriddenCwd: boolean,
tempType: "filepaths" | "folders"
): Promise<Fig.TemplateSuggestion[]> => {
const comptype = tempType === "filepaths" ? "file" : "directory";
if (comptype == null) return [];
log.debug("getFileCompletionSuggestions", cwd, tempType, overriddenCwd);
const crtn = await GlobalModel.submitCommand("_compfiledir", null, [], { comptype, cwd }, false, false);
if (Array.isArray(crtn?.update?.data)) {
if (crtn.update.data.length === 0) return [];
const firstData = crtn.update.data[0];
if (firstData.info?.infocomps) {
if (firstData.info.infocomps.length === 0) return [];
if (firstData.info.infocomps[0] === "(no completions)") return [];
return firstData.info.infocomps.map((comp: string) => {
const fullPath = overriddenCwd ? cwd + comp : comp;
// log.debug("getFileCompletionSuggestions", cwd, comp, fullPath);
return {
name: fullPath,
displayName: comp,
priority: comp.startsWith(".") ? 1 : 55,
context: { templateType: tempType },
type: comp.endsWith("/") ? "folder" : "file",
insertValue: fullPath,
};
});
} else {
return [];
}
}
};
const historyTemplate = (cwd: String): Fig.TemplateSuggestion[] => {
const inputModel = GlobalModel.inputModel;
const cmdLine = inputModel.curLine;
const cmdLineMinusLastToken = cmdLine.substring(0, cmdLine.lastIndexOf(" "));
log.debug("historyTemplate cmdLine", cmdLine);
inputModel.loadHistory(false, 0, "screen");
const hitems = GlobalModel.inputModel.filteredHistoryItems;
if (hitems.length > 0) {
const hmap: Map<string, Fig.TemplateSuggestion> = new Map();
hitems.forEach((h) => {
const cmdstr = h.cmdstr.trim();
if (cmdstr.startsWith(cmdLine)) {
if (hmap.has(cmdstr)) {
hmap.get(cmdstr).priority += 1;
} else {
const insertValue = cmdstr.replace(cmdLineMinusLastToken, "").trim();
hmap.set(cmdstr, {
name: cmdstr,
priority: 90,
context: {
templateType: "history",
},
icon: "🕒",
insertValue,
type: "special",
});
}
}
});
const ret = Array.from(hmap.values());
log.debug("historyTemplate ret", ret);
return ret;
}
return [];
};
// TODO: implement help template
const helpTemplate = (): Fig.TemplateSuggestion[] => {
return [];
};
export const runTemplates = async (
template: Fig.TemplateStrings[] | Fig.Template,
cwd: string,
overriddenCwd: boolean = false
): Promise<Fig.TemplateSuggestion[]> => {
const templates = template instanceof Array ? template : [template];
log.debug("runTemplates", templates, cwd);
return (
await Promise.all(
templates.map(async (t) => {
try {
switch (t) {
case "filepaths":
return await getFileCompletionSuggestions(cwd, overriddenCwd, "filepaths");
case "folders":
return await getFileCompletionSuggestions(cwd, overriddenCwd, "folders");
case "history":
return historyTemplate(cwd);
case "help":
return helpTemplate();
}
} catch (e) {
log.debug({ msg: "template failed", e, template: t, cwd });
return [];
}
})
)
).flat();
};
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/utils.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { Shell } from "../utils/shell";
import { GlobalModel, getApi } from "@/models";
import { MemCache } from "@/util/memcache";
import log from "../utils/log";
export type ExecuteShellCommandTTYResult = {
code: number | null;
};
const commandResultCache = new MemCache<Fig.ExecuteCommandInput, Fig.ExecuteCommandOutput>(1000 * 60 * 5);
export const buildExecuteShellCommand =
(timeout: number): Fig.ExecuteCommandFunction =>
async (input: Fig.ExecuteCommandInput): Promise<Fig.ExecuteCommandOutput> => {
const cachedResult = commandResultCache.get(input);
log.debug("cachedResult", cachedResult);
if (cachedResult) {
log.debug("Using cached result for", input);
return cachedResult;
}
log.debug("Executing command", input);
const { command, args, cwd, env } = input;
const resp = await GlobalModel.submitEphemeralCommand(
"eval",
null,
[[command, ...args].join(" ")],
null,
false,
{
expectsresponse: true,
overridecwd: cwd,
env: env,
timeoutms: timeout,
}
);
const { stdout, stderr } = await GlobalModel.getEphemeralCommandOutput(resp);
const output: Fig.ExecuteCommandOutput = { stdout, stderr, status: stderr?.length > 1 ? 1 : 0 };
if (output.status !== 0) {
log.debug("Command failed, skipping caching", output);
} else {
commandResultCache.put(input, output);
}
return output;
};
const pathSeps = new Map<Shell, string>();
/**
* Get the path separator for the given shell.
* @param shell The shell to get the path separator for.
* @returns The path separator.
*/
export function getPathSep(shell: Shell): string {
if (!pathSeps.has(shell)) {
const pathSep = getApi().pathSep();
pathSeps.set(shell, pathSep);
return pathSep;
}
return pathSeps.get(shell) as string;
}
/**
* Determine if the current token is a path or not. If it is an incomplete path, return the base name of the path as the new cwd to be used in downstream parsing operations. Otherwise, return the current cwd.
* @param token The token to check.
* @param cwd The current working directory.
* @param shell The shell being used.
* @returns The new cwd, whether the token is a path, and whether the path is complete.
*/
export const resolveCwdToken = async (
token: string,
cwd: string,
shell: Shell
): Promise<{ cwd: string; pathy: boolean; complete: boolean }> => {
if (token == null) return { cwd, pathy: false, complete: false };
const sep = getPathSep(shell);
if (!token.includes(sep)) return { cwd, pathy: false, complete: false };
const complete = token.endsWith(sep);
const dirname = getApi().pathDirName(token);
log.debug("resolveCwdToken", { token, cwd, complete, dirname });
return { cwd: complete ? token : dirname, pathy: true, complete };
};
/**
* Runs the comparator function on each value and returns true if any of them match
* @param values The value(s) to check
* @param comparator The function to use to compare the values
* @returns True if any of the values match the comparator
*/
export function matchAny<T>(values: Fig.SingleOrArray<T>, comparator: (a: T) => boolean) {
if (Array.isArray(values)) {
for (const value of values) {
if (comparator(value)) {
return true;
}
}
return false;
} else {
return comparator(values);
}
}
/**
* Checks if any of the values start with the input string
* @param values The value(s) to check
* @param input The input to check against
* @returns True if any of the values start with the input
*/
export function startsWithAny(values: Fig.SingleOrArray<string>, input: string) {
return matchAny(values, (a) => a.startsWith(input));
}
/**
* Checks if any of the values are not equal to the input
* @param values The value(s) to check
* @param input The input to check against
* @returns True if any of the values are not equal to the input
*/
export function equalsAny<T>(values: Fig.SingleOrArray<T>, input: T) {
return matchAny(values, (a) => a == input);
}
/**
* Checks if any of the values of the SingleOrArray are not in the array
* @param values The value(s) to check
* @param arr The array to check against
* @returns True if any of the values are not in the array
*/
export function notInAny<T>(values: Fig.SingleOrArray<T>, arr: T[]) {
return matchAny(values, (a) => !arr.includes(a));
}
/**
* Get the first element of a Fig.SingleOrArray<T>.
* @param values Either a single value or an array of values of the specified type.
* @returns The first element of the array, or the value.
*/
export function getFirst<T>(values: Fig.SingleOrArray<T>): T {
if (Array.isArray(values)) {
return values[0];
}
return values;
}
/**
* Get all elements of a Fig.SingleOrArray<T>
* @param values Either a single value or an array of values of the specified type.
* @returns The array of values, or an empty array
*/
export function getAll<T>(values: Fig.SingleOrArray<T> | undefined): T[] {
if (Array.isArray(values)) {
return values;
}
return [values as T];
}
/**
* Checks if a string is an option, i.e. starts with "--".
* @param value The string to check.
* @returns True if the string is an option.
*/
export function isOption(value: string): boolean {
return value.startsWith("--");
}
/**
* Checks if a string is a flag, i.e. starts with "-" but not "--".
* @param value The string to check.
* @returns True if the string is a flag.
*/
export function isFlag(value: string): boolean {
return value.startsWith("-") && !isOption(value);
}
/**
* Checks if a string is either a flag or an option, i.e. starts with "-".
* @param value The string to check.
* @returns True if the string is a flag or an option.
*/
export function isFlagOrOption(value: string): boolean {
return value.startsWith("-");
}
export function isPath(value: string, shell: Shell): boolean {
return value.includes(getPathSep(shell));
}
/**
* Get the flag of a Fig.SingleOrArray<string>.
* @param values Either a string or an array of strings.
* @returns The flag, or undefined if none is found.
*/
export function getFlag(values: Fig.SingleOrArray<string>): string | undefined {
if (Array.isArray(values)) {
for (const value of values) {
if (isFlag(value)) {
return value;
}
}
} else if (isFlag(values)) {
return values;
}
return undefined;
}
/**
* Checks if a string is a command, i.e. not a flag or option.
* @param value The string to check.
* @returns True if the string is a command.
*/
export function isCommand(value: string): boolean {
return !isFlag(value) && !isOption(value);
}
/**
* Checks if an option suggestion contains a flag. If so, modifies the name to include the preceding flags.
* @param option The option to modify.
* @param precedingFlags The preceding flags to prepend to the suggestion name.
* @returns The modified option suggestion.
*/
export function modifyPosixFlags(option: Fig.Option, precedingFlags: string): Fig.Option {
// We only want to modify the name if the option is a flag
if (option.name) {
// Get the name of the flag without the preceding "-"
const name = getFlag(option.name)?.slice(1);
if (name) {
// Shallow copy the option so we can modify the name without modifying the original spec.
option = { ...option };
// We want to prepend the existing flags to the name, except for the suggestion of the last flag (i.e. the `c` of an input `-abc`), which we want to replace with the existing flags.
// The end result is that we will suggest -abc instead of -a -b -c. We do not want -abb. The case of -aba should already be covered by filterFlags.
if (name === precedingFlags.at(-1)) {
option.name = "-" + precedingFlags;
} else {
option.name = "-" + precedingFlags + name;
}
}
}
return option;
}
/**
* Sort suggestions in-place by priority, then by name.
* @param suggestions The suggestions to sort.
*/
export function sortSuggestions(suggestions: Fig.Suggestion[]) {
suggestions.sort((a, b) => {
if (a.priority == b.priority) {
if (a.name) {
if (b.name) {
return getFirst(a.name).trim().localeCompare(getFirst(b.name));
} else {
return -1;
}
} else if (b.name) {
return 1;
}
}
return (b.priority ?? 0) - (a.priority ?? 0);
});
}
/**
* Merge two subcommand objects, with the second subcommand taking precedence in case of conflicts.
* @param subcommand1 The first subcommand.
* @param subcommand2 The second subcommand.
* @returns The merged subcommand.
*/
export function mergeSubcomands(subcommand1: Fig.Subcommand, subcommand2: Fig.Subcommand): Fig.Subcommand {
log.debug("merging two subcommands", subcommand1, subcommand2);
const newCommand: Fig.Subcommand = { ...subcommand1 };
// Merge the generated spec with the existing spec
for (const key in subcommand2) {
if (Array.isArray(subcommand2[key])) {
newCommand[key] = [...subcommand2[key], ...(newCommand[key] ?? [])];
continue;
} else if (typeof subcommand2[key] === "object") {
newCommand[key] = { ...subcommand2[key], ...(newCommand[key] ?? {}) };
} else {
newCommand[key] = subcommand2[key];
}
}
log.debug("merged subcommand:", newCommand);
return newCommand;
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/utils/log.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { GlobalModel } from "@/models";
export const debug = (...content) => {
if (!GlobalModel.autocompleteModel.loggingEnabled) {
return;
}
console.log("[autocomplete]", ...content);
};
export default { debug };

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