mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d40d6fff3 | |||
| 86cbba1f79 | |||
| bdf17f0133 | |||
| f5f0791b32 | |||
| 899347e90f | |||
| fbc5e1606b | |||
| 39ee41921f | |||
| fde2ce97d2 | |||
| 93845c405d | |||
| 2913babea7 | |||
| a787a1a934 | |||
| bf447c60ce | |||
| f2b322c7d7 | |||
| c1bd5f5765 | |||
| b97c7e186d | |||
| 0442fc84d4 | |||
| 27efae3ad5 | |||
| 4d8c2cd242 | |||
| a560499b58 | |||
| e8b489bcd9 | |||
| 31c12c4705 | |||
| 787bcbc9d8 | |||
| 985304c9aa | |||
| 13cbfdeb6e | |||
| 53290b47e1 | |||
| 4bdd092d33 | |||
| fb2b3dfc6a | |||
| fa27964d49 | |||
| 95132011e4 | |||
| dae4210b72 | |||
| e394ba70a6 | |||
| 483b2eff6f | |||
| 2f4a6fa6e2 | |||
| 4af1b19ffd | |||
| 8b1d930cd6 | |||
| a4e8648968 | |||
| 429ea37419 | |||
| a8f3e356f6 | |||
| 85235aaad8 | |||
| 619cfe4931 | |||
| 11f1c3bb70 | |||
| 8d140c9920 | |||
| 205a9d1e2e | |||
| d3139d51cf | |||
| 5c931de40c | |||
| 497a96bfc2 | |||
| 427447c592 | |||
| 1fe5d425da | |||
| a395043a1c | |||
| ff9f9b43b6 | |||
| 98ba0b46ac | |||
| fb3253d018 | |||
| 8d91d156aa | |||
| b7eec15853 | |||
| 38ff6183a2 | |||
| 1b1e6edaee | |||
| f4aeb92d32 | |||
| b780d5b244 | |||
| d81a6ed480 | |||
| d8393ae50b | |||
| 79deb01ea4 | |||
| 12f9a50686 | |||
| 239933be3e | |||
| 4692cf698b | |||
| 907e87c217 | |||
| f97bb699a1 | |||
| 077545ed93 | |||
| 60d0f0e976 | |||
| cd4671e5bb | |||
| d8a714dd4a | |||
| e0fa1b2560 | |||
| 2dffcc9237 | |||
| 70090b8684 | |||
| 3f8952caf4 | |||
| 15a2486e94 | |||
| 38aa7016b8 | |||
| cff353ca94 | |||
| 5b440ace65 | |||
| 87de0dbc44 | |||
| 74839b8e1b | |||
| 8a24953a88 | |||
| a95854514a | |||
| 229fcf8cd4 | |||
| 83542ddab8 | |||
| b520242f04 | |||
| bc73ed1b6c | |||
| c4647945a4 | |||
| 3db40b1c63 | |||
| f6d7c37915 | |||
| a41e866b1f | |||
| a772715ae4 | |||
| d32e2aae05 | |||
| 3f9d52a7e4 | |||
| a239a8d70e | |||
| a1bddd971f | |||
| 722318f57d | |||
| 3b633328a1 |
@@ -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",
|
||||
@@ -79,6 +80,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,12 @@ 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);
|
||||
}
|
||||
|
||||
getFontSizes(): DropdownItem[] {
|
||||
const availableFontSizes: DropdownItem[] = [];
|
||||
for (let s = appconst.MinFontSize; s <= appconst.MaxFontSize; s++) {
|
||||
@@ -354,6 +360,15 @@ 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>
|
||||
<SettingsError errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</MainView>
|
||||
|
||||
@@ -58,4 +58,8 @@
|
||||
.term-prompt-python {
|
||||
color: var(--term-magenta);
|
||||
}
|
||||
|
||||
.term-prompt-k8s {
|
||||
color: var(--term-magenta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ class Prompt extends React.Component<
|
||||
let branchElem = null;
|
||||
let pythonElem = null;
|
||||
let condaElem = null;
|
||||
let k8sElem = null;
|
||||
if (!isBlank(festate["PROMPTVAR_GITBRANCH"])) {
|
||||
const branchName = festate["PROMPTVAR_GITBRANCH"];
|
||||
branchElem = (
|
||||
@@ -171,9 +172,19 @@ class Prompt extends React.Component<
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (!isBlank(festate["K8SCONTEXT"])) {
|
||||
const k8sContext = festate["K8SCONTEXT"];
|
||||
const k8sNs = festate["K8SNAMESPACE"];
|
||||
k8sElem = (
|
||||
<span title="k8s context:namespace" className="term-prompt-k8s">
|
||||
k8s:({k8sContext}
|
||||
{isBlank(k8sNs) ? "" : ":" + k8sNs}){" "}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className={termClassNames}>
|
||||
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem}
|
||||
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem} {k8sElem}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,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) {
|
||||
|
||||
@@ -4,47 +4,45 @@
|
||||
import * as React from "react";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import "./auxview.less";
|
||||
|
||||
export class AuxiliaryCmdView extends React.Component<
|
||||
{
|
||||
title: string;
|
||||
className?: string;
|
||||
iconClass?: string;
|
||||
titleBarContents?: React.ReactElement[];
|
||||
children?: React.ReactNode;
|
||||
onClose?: React.MouseEventHandler<HTMLDivElement>;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
render() {
|
||||
const { title, className, iconClass, titleBarContents, children, onClose } = this.props;
|
||||
interface AuxiliaryCmdViewProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
iconClass?: string;
|
||||
titleBarContents?: React.ReactElement[];
|
||||
children?: React.ReactNode;
|
||||
onClose?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("auxview", className)}>
|
||||
<div className="auxview-titlebar">
|
||||
<If condition={iconClass != null}>
|
||||
<div className="title-icon">
|
||||
<i className={iconClass} />
|
||||
</div>
|
||||
</If>
|
||||
<div className="title-string">{title}</div>
|
||||
export const AuxiliaryCmdView: React.FC<AuxiliaryCmdViewProps> = observer((props) => {
|
||||
const { title, className, iconClass, titleBarContents, children, onClose } = props;
|
||||
|
||||
<If condition={titleBarContents != null}>{titleBarContents}</If>
|
||||
return (
|
||||
<div className={cn("auxview", className)}>
|
||||
<div className="auxview-titlebar">
|
||||
<If condition={iconClass != null}>
|
||||
<div className="title-icon">
|
||||
<i className={iconClass} />
|
||||
</div>
|
||||
</If>
|
||||
<div className="title-string">{title}</div>
|
||||
|
||||
<div className="flex-spacer"></div>
|
||||
<If condition={titleBarContents != null}>{titleBarContents}</If>
|
||||
|
||||
<If condition={onClose != null}>
|
||||
<div className="close-button" title="Close (ESC)" onClick={onClose}>
|
||||
<i className="fa-sharp fa-solid fa-xmark-large" />
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
<If condition={children != null}>
|
||||
<div className="auxview-content">{children}</div>
|
||||
<div className="flex-spacer"></div>
|
||||
|
||||
<If condition={onClose != null}>
|
||||
<div className="close-button" title="Close (ESC)" onClick={onClose}>
|
||||
<i className="fa-sharp fa-solid fa-xmark-large" />
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<If condition={children != null}>
|
||||
<div className="auxview-content">{children}</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
// TODO: use a generic selector for this
|
||||
&.has-aichat,
|
||||
&.has-history,
|
||||
&.has-info {
|
||||
&.has-info,
|
||||
&.has-suggestions {
|
||||
.base-cmdinput {
|
||||
border-top: 1px solid var(--app-border-color);
|
||||
}
|
||||
@@ -109,9 +110,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
color: var(--app-text-primary-color);
|
||||
background-color: var(--app-bg-color);
|
||||
.textarea,
|
||||
.textarea-ghost {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: var(--termpad) 0;
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
@@ -119,10 +122,21 @@
|
||||
font-family: var(--termfontfamily);
|
||||
line-height: var(--termlineheight);
|
||||
font-size: var(--termfontsize);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
color: var(--app-text-primary-color);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.textarea-ghost {
|
||||
color: var(--app-text-secondary-color);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input.history-input {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -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 { SuggestionView } from "./suggestionview";
|
||||
|
||||
import "./cmdinput.less";
|
||||
|
||||
@@ -181,6 +182,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
hidePrompt = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={this.cmdInputRef} className={cn("cmd-input", hasOpenView, { active: focusVal })}>
|
||||
<Choose>
|
||||
@@ -195,6 +197,9 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
<When condition={openView === appconst.InputAuxView_Info}>
|
||||
<InfoMsg key="infomsg" />
|
||||
</When>
|
||||
<When condition={openView === appconst.InputAuxView_Suggestions}>
|
||||
<SuggestionView />
|
||||
</When>
|
||||
</Choose>
|
||||
<If condition={remote && remote.status != "connected"}>
|
||||
<div className="remote-status-warning">
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
.history-clickable-opt {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--app-text-primary-color);
|
||||
|
||||
@@ -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,77 @@
|
||||
import { SuggestionBlob } from "@/autocomplete/runtime/model";
|
||||
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 SuggestionView: React.FC = observer(() => {
|
||||
const [selectedSuggestion, setSelectedSuggestion] = React.useState<number>(0);
|
||||
useEffect(() => {
|
||||
const keybindManager = GlobalModel.keybindManager;
|
||||
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
|
||||
setSuggestion(selectedSuggestion);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
|
||||
closeView();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
|
||||
setSelectedSuggestion(Math.max(0, selectedSuggestion - 1));
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
|
||||
setSelectedSuggestion(Math.min(suggestions?.suggestions.length - 1, selectedSuggestion + 1));
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:tab", (waveEvent) => {
|
||||
setSelectedSuggestion(Math.min(suggestions?.suggestions.length - 1, selectedSuggestion + 1));
|
||||
return true;
|
||||
});
|
||||
|
||||
return () => {
|
||||
GlobalModel.keybindManager.unregisterDomain("aichat");
|
||||
};
|
||||
});
|
||||
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
const autocompleteModel = GlobalModel.autocompleteModel;
|
||||
const suggestions: SuggestionBlob = autocompleteModel.getSuggestions();
|
||||
|
||||
const closeView = () => {
|
||||
inputModel.closeAuxView();
|
||||
};
|
||||
|
||||
const setSuggestion = (idx: number) => {
|
||||
autocompleteModel.applySuggestion(idx);
|
||||
autocompleteModel.loadSuggestions();
|
||||
closeView();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuxiliaryCmdView title="Suggestions" className="suggestions-view" onClose={closeView}>
|
||||
<If condition={!suggestions}>
|
||||
<div className="no-suggestions">No suggestions</div>
|
||||
</If>
|
||||
{suggestions?.suggestions.map((suggestion, idx) => (
|
||||
<div
|
||||
key={getFirst(suggestion.name)}
|
||||
title={suggestion.description}
|
||||
className={cn("suggestion-item", { "is-selected": selectedSuggestion === idx })}
|
||||
onClick={() => {
|
||||
setSuggestion(idx);
|
||||
}}
|
||||
>
|
||||
{suggestion.icon} {getAll(suggestion.name).join(",")}{" "}
|
||||
{suggestion.description ? `- ${suggestion.description}` : ""}
|
||||
</div>
|
||||
))}
|
||||
</AuxiliaryCmdView>
|
||||
);
|
||||
});
|
||||
@@ -11,6 +11,8 @@ import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
|
||||
import { getMonoFontSize } from "@/util/textmeasure";
|
||||
import * as appconst from "@/app/appconst";
|
||||
import { Shell, getSuggestions } from "@/autocomplete";
|
||||
import { SuggestionBlob } from "@/autocomplete/runtime/model";
|
||||
|
||||
type OV<T> = mobx.IObservableValue<T>;
|
||||
|
||||
@@ -38,7 +40,7 @@ function scrollDiv(div: any, amt: number) {
|
||||
div.scrollTo({ top: newScrollTop, behavior: "smooth" });
|
||||
}
|
||||
|
||||
class HistoryKeybindings extends React.Component<{ inputObject: TextAreaInput }, {}> {
|
||||
class HistoryKeybindings extends React.Component<{}, {}> {
|
||||
componentDidMount(): void {
|
||||
if (GlobalModel.activeMainView != "session") {
|
||||
return;
|
||||
@@ -101,38 +103,25 @@ class HistoryKeybindings extends React.Component<{ inputObject: TextAreaInput },
|
||||
}
|
||||
|
||||
class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }, {}> {
|
||||
lastTab: boolean;
|
||||
curPress: string;
|
||||
lastTab: boolean;
|
||||
|
||||
componentDidMount() {
|
||||
if (GlobalModel.activeMainView != "session") {
|
||||
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 +192,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);
|
||||
@@ -240,9 +233,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
|
||||
|
||||
@mobxReact.observer
|
||||
class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () => void }, {}> {
|
||||
lastTab: boolean = false;
|
||||
lastHistoryUpDown: boolean = false;
|
||||
lastTabCurLine: OV<string> = mobx.observable.box(null);
|
||||
lastFocusType: string = null;
|
||||
mainInputRef: React.RefObject<HTMLTextAreaElement> = React.createRef();
|
||||
historyInputRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
@@ -399,6 +390,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();
|
||||
@@ -562,7 +558,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();
|
||||
@@ -605,6 +601,10 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
}
|
||||
}
|
||||
const isHistoryFocused = auxViewFocused && inputModel.getActiveAuxView() == appconst.InputAuxView_History;
|
||||
|
||||
// Will be null if the feature is disabled
|
||||
const primaryAutocompleteSuggestion = GlobalModel.autocompleteModel.getPrimarySuggestionCompletion();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="textareainput-div control is-expanded"
|
||||
@@ -612,15 +612,23 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
style={{ height: computedOuterHeight }}
|
||||
>
|
||||
<If condition={!auxViewFocused}>
|
||||
<CmdInputKeybindings inputObject={this}></CmdInputKeybindings>
|
||||
<CmdInputKeybindings inputObject={this} />
|
||||
</If>
|
||||
<If condition={isHistoryFocused}>
|
||||
<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}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# 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`.
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from "./runtime/runtime";
|
||||
export * from "./utils/shell";
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/model.ts
|
||||
// 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: Fig.Suggestion[];
|
||||
argumentDescription?: string;
|
||||
charactersToDrop?: number;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user