fix: suggestions ui to position above rather than forcing screen scroll (#97)

* fix: suggestion locations by doing above positioning when at bottom of the screen

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* fix: preserve color on re-renders

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* fix: ui on description only item when hitting end of line

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

---------

Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
Chapman Pendery
2023-12-04 23:20:00 -08:00
committed by GitHub
parent 4e5d7de60e
commit 8b8b9f1a48
6 changed files with 134 additions and 38 deletions
+1
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@withfig/autocomplete": "^2.633.0",
"ajv": "^8.12.0",
"ansi-styles": "^6.2.1",
"chalk": "^5.3.0",
"commander": "^11.0.0",
"find-process": "^1.4.7",
+1
View File
@@ -35,6 +35,7 @@
"dependencies": {
"@withfig/autocomplete": "^2.633.0",
"ajv": "^8.12.0",
"ansi-styles": "^6.2.1",
"chalk": "^5.3.0",
"commander": "^11.0.0",
"find-process": "^1.4.7",
+5 -5
View File
@@ -7,6 +7,8 @@ import { Shell } from "../utils/bindings.js";
import log from "../utils/log.js";
import { getConfig } from "../utils/config.js";
const maxPromptPollDistance = 10;
type TerminalCommand = {
promptStartMarker?: IMarker;
promptEndMarker?: IMarker;
@@ -75,12 +77,9 @@ export class CommandManager {
// User defined prompt
const inshellisenseConfig = getConfig();
log.debug({ inshellisenseConfig, t: "tomato" });
if (this.#shell == Shell.Bash) {
if (inshellisenseConfig.promptRegex?.bash != null) {
const customBashPrompt = lineText.match(new RegExp(inshellisenseConfig.promptRegex?.bash.regex))?.groups?.prompt;
log.debug({ customBashPrompt });
const adjustedPrompt = this._adjustPrompt(customBashPrompt, lineText, inshellisenseConfig.promptRegex?.bash.postfix);
if (adjustedPrompt) {
return adjustedPrompt;
@@ -184,7 +183,7 @@ export class CommandManager {
// if we haven't fond the prompt yet, poll over the next 5 lines searching for it
if (this.#activeCommand.promptText == null && withinPollDistance) {
for (let i = globalCursorPosition; i < promptEndMarker.line + 5; i++) {
for (let i = globalCursorPosition; i < promptEndMarker.line + maxPromptPollDistance; i++) {
if (this.#previousCommandLines.has(i)) continue;
const promptResult = this._getWindowsPrompt(i);
if (promptResult != null) {
@@ -222,8 +221,9 @@ export class CommandManager {
const cursorAtEndOfInput = (this.#activeCommand.promptText.length + command.trim().length) % this.#terminal.cols <= this.#terminal.buffer.active.cursorX;
let hasOutput = false;
let cell = undefined;
for (let i = 0; i < this.#terminal.cols; i++) {
const cell = line?.getCell(i);
cell = line?.getCell(i, cell);
if (cell == null) continue;
hasOutput = cell.getChars() != "";
if (hasOutput) {
+73 -1
View File
@@ -12,6 +12,8 @@ import xterm from "xterm-headless";
import { CommandManager, CommandState } from "./commandManager.js";
import log from "../utils/log.js";
import { gitBashPath } from "../utils/shell.js";
import ansi from "ansi-escapes";
import styles from "ansi-styles";
const ISTermOnDataEvent = "data";
@@ -122,17 +124,87 @@ export class ISTerm implements IPty {
}
getCommandState(): CommandState {
log.debug({ x: this.#term.buffer.active.cursorX, y: this.#term.buffer.active.baseY, lines: this.#term.buffer.active.length });
return this.#commandManager.getState();
}
getCursorState() {
return {
onLastLine: this.#term.buffer.active.cursorY >= this.#term.rows - 2,
remainingLines: Math.max(this.#term.rows - 2 - this.#term.buffer.active.cursorY, 0),
cursorX: this.#term.buffer.active.cursorX,
cursorY: this.#term.buffer.active.cursorY,
};
}
private _sameColor(baseCell: xterm.IBufferCell | undefined, targetCell: xterm.IBufferCell | undefined) {
return (
baseCell?.getBgColorMode() == targetCell?.getBgColorMode() &&
baseCell?.getBgColor() == targetCell?.getBgColor() &&
baseCell?.getFgColorMode() == targetCell?.getFgColorMode() &&
baseCell?.getFgColor() == targetCell?.getFgColor()
);
}
private _getAnsiColors(cell: xterm.IBufferCell | undefined): string {
if (cell == null) return "";
let bgAnsi = "";
cell.getBgColor;
cell.getFgColor;
if (cell.isBgDefault()) {
bgAnsi = "\x1b[49m";
} else if (cell.isBgPalette()) {
bgAnsi = `\x1b[48;5;${cell.getBgColor()}m`;
} else {
bgAnsi = `\x1b[48;5;${styles.hexToAnsi256(cell.getBgColor().toString(16))}m`;
}
let fgAnsi = "";
if (cell.isFgDefault()) {
fgAnsi = "\x1b[39m";
} else if (cell.isFgPalette()) {
fgAnsi = `\x1b[38;5;${cell.getFgColor()}m`;
} else {
fgAnsi = `\x1b[38;5;${styles.hexToAnsi256(cell.getFgColor().toString(16))}m`;
}
return bgAnsi + fgAnsi;
}
getCells(height: number, direction: "below" | "above") {
const currentCursorPosition = this.#term.buffer.active.cursorY + this.#term.buffer.active.baseY;
const writeLine = (y: number) => {
const line = this.#term.buffer.active.getLine(y);
const ansiLine = ["\x1b[0m"];
if (line == null) return "";
let cell = line.getCell(0);
let prevCell: xterm.IBufferCell | undefined;
for (let x = 0; x < line.length; x++) {
cell = line.getCell(x, cell);
const chars = cell?.getChars() ?? "";
if (!this._sameColor(prevCell, cell)) {
ansiLine.push(this._getAnsiColors(cell));
}
ansiLine.push(chars == "" ? " " : chars);
prevCell = cell;
}
return ansiLine.join("");
};
const lines = [];
if (direction == "above") {
const startCursorPosition = currentCursorPosition - 1;
const endCursorPosition = currentCursorPosition - 1 - height;
for (let y = startCursorPosition; y > endCursorPosition; y--) {
lines.push(writeLine(y));
}
} else {
const startCursorPosition = currentCursorPosition + 1;
const endCursorPosition = currentCursorPosition + 1 + height;
for (let y = startCursorPosition; y < endCursorPosition; y++) {
lines.push(writeLine(y));
}
}
return lines.reverse().join(ansi.cursorNextLine);
}
}
export const spawn = async (options: ISTermOptions): Promise<ISTerm> => {
+4 -3
View File
@@ -8,13 +8,14 @@ import { renderBox, truncateText, truncateMultilineText } from "./utils.js";
import ansi from "ansi-escapes";
import chalk from "chalk";
import { parseKeystroke } from "../utils/ansi.js";
const maxSuggestions = 5;
const suggestionWidth = 40;
const descriptionWidth = 30;
const descriptionHeight = 6;
const descriptionHeight = 5;
const borderWidth = 2;
const activeSuggestionBackgroundColor = "#7D56F4";
export const MAX_LINES = borderWidth + maxSuggestions;
export const MAX_LINES = borderWidth + Math.max(maxSuggestions, descriptionHeight);
type SuggestionsSequence = {
data: string;
columns: number;
@@ -81,7 +82,7 @@ export class SuggestionManager {
const wrappedPadding = this.#term.getCursorState().cursorX % this.#term.cols;
const maxPadding = activeDescription.length !== 0 ? this.#term.cols - suggestionWidth - descriptionWidth : this.#term.cols - suggestionWidth;
const swapDescription = wrappedPadding > maxPadding;
const swapDescription = wrappedPadding > maxPadding && activeDescription.length !== 0;
const swappedPadding = swapDescription ? Math.max(wrappedPadding - descriptionWidth, 0) : wrappedPadding;
const clampedLeftPadding = Math.min(Math.min(wrappedPadding, swappedPadding), maxPadding);
+50 -29
View File
@@ -5,7 +5,7 @@ import { inputModifier } from "./input.js";
import log from "../utils/log.js";
import { Shell } from "../utils/bindings.js";
import isterm from "../isterm/index.js";
import { eraseLinesBelow, scrollDown } from "../utils/ansi.js";
import { eraseLinesBelow } from "../utils/ansi.js";
import ansi from "ansi-escapes";
import { SuggestionManager, MAX_LINES } from "./suggestionManager.js";
@@ -14,7 +14,6 @@ export const render = async (shell: Shell) => {
const suggestionManager = new SuggestionManager(term);
let hasActiveSuggestions = false;
let previousSuggestionsColumns = 0;
let addedLines = 0;
process.stdin.setRawMode(true);
const writeOutput = (data: string) => {
@@ -25,60 +24,82 @@ export const render = async (shell: Shell) => {
writeOutput(ansi.clearTerminal);
term.onData((data) => {
if (term.getCursorState().onLastLine) {
// eslint-disable-next-line no-control-regex
for (const match of data.matchAll(/\x1b\[([0-9]+);([0-9]+)H/g)) {
const [cupSequence, , cursorX] = match;
data = data.replaceAll(cupSequence, ansi.cursorTo(parseInt(cursorX) - 1, term.rows - 1 - addedLines));
}
}
const commandState = term.getCommandState();
if ((commandState.hasOutput || hasActiveSuggestions) && !commandState.persistentOutput) {
writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(MAX_LINES) + ansi.cursorRestorePosition + ansi.cursorShow + data);
if (term.getCursorState().remainingLines < previousSuggestionsColumns) {
writeOutput(
ansi.cursorHide +
ansi.cursorSavePosition +
ansi.cursorPrevLine.repeat(MAX_LINES) +
term.getCells(MAX_LINES, "above") +
ansi.cursorRestorePosition +
ansi.cursorShow +
data,
);
} else {
writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(MAX_LINES + 1) + ansi.cursorRestorePosition + ansi.cursorShow + data);
}
} else {
writeOutput(data);
}
setImmediate(async () => {
const suggestion = await suggestionManager.render();
addedLines = suggestion.columns;
const commandState = term.getCommandState();
if (suggestion.data != "" && commandState.cursorTerminated && !commandState.hasOutput) {
if (hasActiveSuggestions) {
const offset = MAX_LINES - suggestion.columns;
writeOutput(
ansi.cursorHide +
ansi.cursorSavePosition +
eraseLinesBelow(MAX_LINES) +
(offset > 0 ? ansi.cursorUp(offset) : "") +
suggestion.data +
ansi.cursorRestorePosition +
ansi.cursorShow,
);
} else {
if (term.getCursorState().onLastLine) {
if (term.getCursorState().remainingLines < suggestion.columns) {
writeOutput(
ansi.cursorHide +
ansi.cursorSavePosition +
"\n".repeat(suggestion.columns) +
ansi.cursorPrevLine.repeat(MAX_LINES) +
term.getCells(MAX_LINES, "above") +
ansi.cursorRestorePosition +
ansi.cursorSavePosition +
ansi.cursorUp() +
suggestion.data +
ansi.cursorRestorePosition +
ansi.cursorUp(suggestion.columns) +
ansi.cursorShow,
);
} else {
const offset = MAX_LINES - suggestion.columns;
writeOutput(
ansi.cursorHide + ansi.cursorSavePosition + "\n".repeat(suggestion.columns) + suggestion.data + ansi.cursorRestorePosition + ansi.cursorShow,
ansi.cursorHide +
ansi.cursorSavePosition +
eraseLinesBelow(MAX_LINES) +
(offset > 0 ? ansi.cursorUp(offset) : "") +
suggestion.data +
ansi.cursorRestorePosition +
ansi.cursorShow,
);
}
} else {
if (term.getCursorState().remainingLines < suggestion.columns) {
writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorUp() + suggestion.data + ansi.cursorRestorePosition + ansi.cursorShow);
} else {
writeOutput(
ansi.cursorHide +
ansi.cursorSavePosition +
ansi.cursorNextLine.repeat(suggestion.columns) +
suggestion.data +
ansi.cursorRestorePosition +
ansi.cursorShow,
);
}
}
hasActiveSuggestions = true;
} else {
if (hasActiveSuggestions) {
if (term.getCursorState().onLastLine) {
writeOutput(scrollDown(previousSuggestionsColumns) + ansi.cursorDown(previousSuggestionsColumns));
if (term.getCursorState().remainingLines < previousSuggestionsColumns) {
writeOutput(
ansi.cursorHide +
ansi.cursorSavePosition +
ansi.cursorPrevLine.repeat(MAX_LINES) +
term.getCells(MAX_LINES, "above") +
ansi.cursorRestorePosition +
ansi.cursorShow,
);
} else {
writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(MAX_LINES) + ansi.cursorRestorePosition + ansi.cursorShow);
}