TERMINAL: Add option for partial history search (#736)

This commit is contained in:
Michael Ficocelli 2023-08-21 06:50:17 -04:00 committed by GitHub
parent 7ea0725a39
commit 86b0bd5ac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 5 deletions

@ -30,6 +30,18 @@ export const MiscPage = (): React.ReactElement => {
</> </>
} }
/> />
<OptionSwitch
checked={Settings.EnableHistorySearch}
onChange={(newValue) => (Settings.EnableHistorySearch = newValue)}
text="Enable terminal history search with arrow keys"
tooltip={
<>
If there is user-entered text in the terminal, using the up arrow will search through the terminal history
for previous commands that start with the current text, instead of navigating to the most recent history
item. Search results can be executed immediately via 'enter', or autofilled into the terminal with 'tab'.
</>
}
/>
</GameOptionsPage> </GameOptionsPage>
); );
}; };

@ -26,6 +26,8 @@ export const Settings = {
DisableOverviewProgressBars: false, DisableOverviewProgressBars: false,
/** Whether to enable bash hotkeys */ /** Whether to enable bash hotkeys */
EnableBashHotkeys: false, EnableBashHotkeys: false,
/** Whether to enable terminal history search */
EnableHistorySearch: false,
/** Timestamps format string */ /** Timestamps format string */
TimestampsFormat: "", TimestampsFormat: "",
/** Locale used for display numbers. */ /** Locale used for display numbers. */

@ -32,6 +32,16 @@ const useStyles = makeStyles((theme: Theme) =>
padding: theme.spacing(0), padding: theme.spacing(0),
height: "100%", height: "100%",
}, },
absolute: {
margin: theme.spacing(0),
position: "absolute",
bottom: "5px",
opacity: "0.75",
maxWidth: "100%",
"white-space": "nowrap break-spaces",
overflow: "hidden",
pointerEvents: "none",
},
}), }),
); );
@ -44,6 +54,9 @@ export function TerminalInput(): React.ReactElement {
const [value, setValue] = useState(command); const [value, setValue] = useState(command);
const [postUpdateValue, setPostUpdateValue] = useState<{ postUpdate: () => void } | null>(); const [postUpdateValue, setPostUpdateValue] = useState<{ postUpdate: () => void } | null>();
const [possibilities, setPossibilities] = useState<string[]>([]); const [possibilities, setPossibilities] = useState<string[]>([]);
const [searchResults, setSearchResults] = useState<string[]>([]);
const [searchResultsIndex, setSearchResultsIndex] = useState(0);
const [autofilledValue, setAutofilledValue] = useState(false);
const classes = useStyles(); const classes = useStyles();
// If we have no data in the current terminal history, let's initialize it from the player save // If we have no data in the current terminal history, let's initialize it from the player save
@ -73,6 +86,20 @@ export function TerminalInput(): React.ReactElement {
function handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void {
saveValue(event.target.value); saveValue(event.target.value);
setPossibilities([]); setPossibilities([]);
setSearchResults([]);
setAutofilledValue(false);
}
function resetSearch(isAutofilled = false) {
setSearchResults([]);
setAutofilledValue(isAutofilled);
setSearchResultsIndex(0);
}
function getSearchSuggestionPrespace() {
const currentPrefix = `[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> `;
const prefixLength = `${currentPrefix}${value}`.length;
return Array(prefixLength).fill(" ");
} }
function modifyInput(mod: string): void { function modifyInput(mod: string): void {
@ -197,10 +224,12 @@ export function TerminalInput(): React.ReactElement {
// Run command or insert newline // Run command or insert newline
if (event.key === KEY.ENTER) { if (event.key === KEY.ENTER) {
event.preventDefault(); event.preventDefault();
Terminal.print(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${value}`); const command = searchResults.length ? searchResults[searchResultsIndex] : value;
if (value) { Terminal.print(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${command}`);
Terminal.executeCommands(value); if (command) {
Terminal.executeCommands(command);
saveValue(""); saveValue("");
resetSearch();
} }
return; return;
} }
@ -208,8 +237,15 @@ export function TerminalInput(): React.ReactElement {
// Autocomplete // Autocomplete
if (event.key === KEY.TAB) { if (event.key === KEY.TAB) {
event.preventDefault(); event.preventDefault();
if (searchResults.length) {
saveValue(searchResults[searchResultsIndex]);
resetSearch(true);
return;
}
const possibilities = await getTabCompletionPossibilities(value, Terminal.cwd()); const possibilities = await getTabCompletionPossibilities(value, Terminal.cwd());
if (possibilities.length === 0) return; if (possibilities.length === 0) return;
setSearchResults([]);
if (possibilities.length === 1) { if (possibilities.length === 1) {
saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " "); saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " ");
return; return;
@ -228,7 +264,7 @@ export function TerminalInput(): React.ReactElement {
// Select previous command. // Select previous command.
if (event.key === KEY.UP_ARROW || (Settings.EnableBashHotkeys && event.key === KEY.P && event.ctrlKey)) { if (event.key === KEY.UP_ARROW || (Settings.EnableBashHotkeys && event.key === KEY.P && event.ctrlKey)) {
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys || (Settings.EnableHistorySearch && value)) {
event.preventDefault(); event.preventDefault();
} }
const i = Terminal.commandHistoryIndex; const i = Terminal.commandHistoryIndex;
@ -237,6 +273,23 @@ export function TerminalInput(): React.ReactElement {
if (len == 0) { if (len == 0) {
return; return;
} }
// If there is a partial command in the terminal, hitting "up" will filter the history
if (value && !autofilledValue && Settings.EnableHistorySearch) {
if (searchResults.length > 0) {
setSearchResultsIndex((searchResultsIndex + 1) % searchResults.length);
return;
}
const newResults = [...new Set(Terminal.commandHistory.filter((item) => item?.startsWith(value)).reverse())];
if (newResults.length) {
setSearchResults(newResults);
}
// Prevent moving through the history when the user has a search term even if there are
// no search results, to be consistent with zsh-type terminal behavior
return;
}
if (i < 0 || i > len) { if (i < 0 || i > len) {
Terminal.commandHistoryIndex = len; Terminal.commandHistoryIndex = len;
} }
@ -246,6 +299,7 @@ export function TerminalInput(): React.ReactElement {
} }
const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex]; const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
saveValue(prevCommand); saveValue(prevCommand);
resetSearch(true);
if (ref) { if (ref) {
setTimeout(function () { setTimeout(function () {
ref.selectionStart = ref.selectionEnd = 10000; ref.selectionStart = ref.selectionEnd = 10000;
@ -258,6 +312,11 @@ export function TerminalInput(): React.ReactElement {
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys) {
event.preventDefault(); event.preventDefault();
} }
if (searchResults.length > 0) {
setSearchResultsIndex(searchResultsIndex === 0 ? searchResults.length - 1 : searchResultsIndex - 1);
return;
}
const i = Terminal.commandHistoryIndex; const i = Terminal.commandHistoryIndex;
const len = Terminal.commandHistory.length; const len = Terminal.commandHistory.length;
@ -272,14 +331,20 @@ export function TerminalInput(): React.ReactElement {
if (i == len || i == len - 1) { if (i == len || i == len - 1) {
Terminal.commandHistoryIndex = len; Terminal.commandHistoryIndex = len;
saveValue(""); saveValue("");
resetSearch();
} else { } else {
++Terminal.commandHistoryIndex; ++Terminal.commandHistoryIndex;
const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex]; const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
saveValue(prevCommand); saveValue(prevCommand);
resetSearch(true);
} }
} }
if (event.key === KEY.ESC && searchResults.length) {
resetSearch();
}
// Extra Bash Emulation Hotkeys, must be enabled through options // Extra Bash Emulation Hotkeys, must be enabled through options
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys) {
if (event.code === KEYCODE.C && event.ctrlKey && ref && ref.selectionStart === ref.selectionEnd) { if (event.code === KEYCODE.C && event.ctrlKey && ref && ref.selectionStart === ref.selectionEnd) {
@ -367,7 +432,10 @@ export function TerminalInput(): React.ReactElement {
</Typography> </Typography>
), ),
spellCheck: false, spellCheck: false,
onBlur: () => setPossibilities([]), onBlur: () => {
setPossibilities([]);
resetSearch();
},
onKeyDown: onKeyDown, onKeyDown: onKeyDown,
}} }}
></TextField> ></TextField>
@ -386,6 +454,10 @@ export function TerminalInput(): React.ReactElement {
</Typography> </Typography>
</Paper> </Paper>
</Popper> </Popper>
<Typography classes={{ root: classes.absolute }} color={"primary"} paragraph={false}>
{getSearchSuggestionPrespace()}
{(searchResults[searchResultsIndex] ?? "").substring(value.length)}
</Typography>
</> </>
); );
} }