mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35cf4b6a3b | |||
| 8fdcf10cae | |||
| 6336f87cf2 | |||
| 899347e90f | |||
| 39ee41921f | |||
| 2913babea7 | |||
| a787a1a934 | |||
| bf447c60ce | |||
| 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,75 @@
|
||||
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";
|
||||
|
||||
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={suggestion.name}
|
||||
title={suggestion.description}
|
||||
className={cn("suggestion-item", { "is-selected": selectedSuggestion === idx })}
|
||||
onClick={() => {
|
||||
setSuggestion(idx);
|
||||
}}
|
||||
>
|
||||
{suggestion.icon} {suggestion.name} {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;
|
||||
@@ -100,39 +102,39 @@ class HistoryKeybindings extends React.Component<{ inputObject: TextAreaInput },
|
||||
}
|
||||
}
|
||||
|
||||
const SuggestionsInfoMsg = (props: { suggestions: SuggestionBlob }) => {
|
||||
const suggestions = props.suggestions.suggestions;
|
||||
return (
|
||||
<div className="cmd-input-info">
|
||||
{suggestions.map((s) => (
|
||||
<div key={s.name} className="cmd-input-info-item">
|
||||
{s.icon} {s.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 +205,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 +246,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 +403,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 +571,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 +614,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 +625,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}
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as constants from "@/app/appconst";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { MagicLayout } from "@/app/magiclayout";
|
||||
import { TabIcon } from "@/elements/tabicon";
|
||||
import * as appconst from "@/app/appconst";
|
||||
|
||||
@mobxReact.observer
|
||||
class ScreenTab extends React.Component<
|
||||
@@ -62,6 +63,59 @@ class ScreenTab extends React.Component<
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onContextMenu(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let { screen, activeScreenId } = this.props;
|
||||
if (activeScreenId != screen.screenId) {
|
||||
// only show context menu for active tab
|
||||
GlobalCommandRunner.switchScreen(screen.screenId);
|
||||
return;
|
||||
}
|
||||
let colorSubMenu: ContextMenuItem[] = [];
|
||||
for (let color of appconst.TabColors) {
|
||||
colorSubMenu.push({
|
||||
label: color,
|
||||
click: () => {
|
||||
GlobalCommandRunner.screenSetSettings(screen.screenId, { tabcolor: color }, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
let menu: ContextMenuItem[] = [
|
||||
{
|
||||
label: "New Tab",
|
||||
click: () => {
|
||||
GlobalCommandRunner.createNewScreen();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Set Tab Color",
|
||||
submenu: colorSubMenu,
|
||||
},
|
||||
{
|
||||
label: "All Tab Settings",
|
||||
click: () => {
|
||||
GlobalModel.tabSettingsOpen.set(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Close Tab",
|
||||
click: () => {
|
||||
GlobalModel.onCloseCurrentTab();
|
||||
},
|
||||
},
|
||||
];
|
||||
GlobalModel.contextMenuModel.showContextMenu(menu, { x: e.clientX, y: e.clientY });
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen, activeScreenId, index, onSwitchScreen } = this.props;
|
||||
let archived = screen.archived.get() ? (
|
||||
@@ -83,7 +137,7 @@ class ScreenTab extends React.Component<
|
||||
"color-" + screen.getTabColor()
|
||||
)}
|
||||
onPointerDown={() => onSwitchScreen(screen.screenId)}
|
||||
onContextMenu={(event) => this.openScreenSettings(event, screen)}
|
||||
onContextMenu={this.onContextMenu}
|
||||
onDragEnd={this.handleDragEnd}
|
||||
>
|
||||
<div className="background"></div>
|
||||
|
||||
@@ -121,7 +121,8 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
if (screen == null) {
|
||||
return;
|
||||
}
|
||||
if (screen.getScreenLines().lines.length == 0) {
|
||||
let numLines = screen.getScreenLines().lines.length;
|
||||
if (numLines < 10) {
|
||||
GlobalCommandRunner.screenDelete(screen.screenId, false);
|
||||
GlobalModel.modalsModel.popModal();
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user