mirror of
https://github.com/wavetermdev/inshellisense.git
synced 2026-08-05 13:43:30 -07:00
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:
Generated
+1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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> => {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user