diff --git a/extensions/SSHQuickOpen/src/SSHQuickOpenExtension.ts b/extensions/SSHQuickOpen/src/SSHQuickOpenExtension.ts index ac4e69e3..618db208 100644 --- a/extensions/SSHQuickOpen/src/SSHQuickOpenExtension.ts +++ b/extensions/SSHQuickOpen/src/SSHQuickOpenExtension.ts @@ -1,22 +1,38 @@ /* - * Copyright 2024 Simon Edwards + * Copyright 2025 Simon Edwards * * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. */ import { ExtensionContext, Logger, SessionConfiguration } from '@extraterm/extraterm-extension-api'; import { createUuid } from "extraterm-uuid"; +// Most Recently Used list length. +const MRU_LENGTH = 10; +interface Config { + mru: string[]; +} let log: Logger = null; let context: ExtensionContext = null; +let config: Config = null; export function activate(_context: ExtensionContext): any { context = _context; log = context.logger; + loadConfig(); context.commands.registerCommand("ssh-quick-open:open", quickOpenCommand); } +function loadConfig(): void { + config = context.configuration.get(); + if (config == null) { + config = { + mru: [] + }; + } +} + // Note: This is mostly duplicated in SSHSessionEditorExtension.ts. interface SSHSessionConfiguration extends SessionConfiguration { host?: string; @@ -29,15 +45,31 @@ async function quickOpenCommand(): Promise { const sshConnectionString = await context.activeTab.showTextInput({ message: "Enter a SSH connection string:", value: "", + suggestions: config.mru }); if (sshConnectionString == null) { return; } + updateMRU(sshConnectionString); + const sshSessionConfiguration = parseConnectionString(sshConnectionString); context.commands.executeCommand("extraterm:window.newTerminal", {sessionConfiguration: sshSessionConfiguration}); } +function updateMRU(sshConnectionString: string): void { + const index = config.mru.indexOf(sshConnectionString); + if (index !== -1) { + config.mru.splice(index, 1); + } + config.mru.unshift(sshConnectionString); + + if (config.mru.length > MRU_LENGTH) { + config.mru = config.mru.slice(0, MRU_LENGTH); + } + context.configuration.set(config); +} + function parseConnectionString(sshConnectionString: string): SSHSessionConfiguration { let username: string = null; diff --git a/main/src/ThemeTestUtility.ts b/main/src/ThemeTestUtility.ts index 035399fe..0c8af620 100644 --- a/main/src/ThemeTestUtility.ts +++ b/main/src/ThemeTestUtility.ts @@ -107,6 +107,7 @@ function main(): void { ComboBox({items: ["One", "Two"]}), ComboBox({items: ["Disabled One", "Disabled Two"], enabled: false}), + ComboBox({editable: true, items: []}), Widget({layout: BoxLayout({ diff --git a/main/src/extension/TextInputPopOver.ts b/main/src/extension/TextInputPopOver.ts index 81f5696f..627de1fa 100644 --- a/main/src/extension/TextInputPopOver.ts +++ b/main/src/extension/TextInputPopOver.ts @@ -1,16 +1,16 @@ /* - * Copyright 2023 Simon Edwards + * Copyright 2025 Simon Edwards * * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. */ import * as ExtensionApi from "@extraterm/extraterm-extension-api"; -import { Logger, log, getLogger } from "extraterm-logging"; +import { Logger, getLogger } from "extraterm-logging"; import { doLater } from "extraterm-timeoutqt"; -import { Label, LineEdit } from "qt-construct"; +import { ComboBox, Label, LineEdit } from "qt-construct"; import { Window } from "../Window.js"; import { UiStyle } from "../ui/UiStyle.js"; import { WindowPopOver } from "../ui/WindowPopOver.js"; -import { Direction, Key, QKeyEvent, QLabel, QLineEdit, QPushButton, QRect, TextFormat } from "@nodegui/nodegui"; +import { Direction, Key, QComboBox, QKeyEvent, QLabel, QLineEdit, QRect, TextFormat } from "@nodegui/nodegui"; import { BoxLayout, Widget } from "qt-construct"; @@ -19,11 +19,18 @@ export interface TextInputPopOverOptions extends ExtensionApi.TextInputOptions { aroundRect?: QRect; } +enum TextInputPopOverMode { + TEXT, + COMBO_BOX +} + export class TextInputPopOver { private _log: Logger = null; #uiStyle: UiStyle = null; #messageLabel: QLabel = null; + #mode = TextInputPopOverMode.TEXT; #lineEdit: QLineEdit = null; + #comboBox: QComboBox = null; #windowPopOver: WindowPopOver = null; #containingRect: QRect = null; @@ -47,6 +54,13 @@ export class TextInputPopOver { wordWrap: true, openExternalLinks: true }), + this.#comboBox = ComboBox({ + items: [], + editable: true, + onKeyPress: (nativeEvent) => { + this.#handleKeyPress(new QKeyEvent(nativeEvent)); + }, + }), this.#lineEdit = LineEdit({ onKeyPress: (nativeEvent) => { this.#handleKeyPress(new QKeyEvent(nativeEvent)); @@ -67,9 +81,13 @@ export class TextInputPopOver { const key = event.key(); if (key === Key.Key_Enter || key === Key.Key_Return) { event.accept(); - this.#lineEdit.setEventProcessed(true); - this.#sendResult(this.#lineEdit.text()); - return; + if (this.#mode === TextInputPopOverMode.COMBO_BOX) { + this.#comboBox.setEventProcessed(true); + this.#sendResult(this.#comboBox.currentText()); + } else { + this.#lineEdit.setEventProcessed(true); + this.#sendResult(this.#lineEdit.text()); + } } } @@ -102,7 +120,24 @@ export class TextInputPopOver { this.#messageLabel.setTextFormat(TextFormat.RichText); } - this.#lineEdit.setText(options.value ?? ""); + this.#mode = (options.suggestions ?? []).length === 0 ? TextInputPopOverMode.TEXT :TextInputPopOverMode.COMBO_BOX; + const isComboBox = this.#mode === TextInputPopOverMode.COMBO_BOX; + + if (isComboBox) { + this.#lineEdit.hide(); + this.#comboBox.show(); + + this.#comboBox.clear(); + for (const item of options.suggestions) { + this.#comboBox.addItem(undefined, item, undefined); + } + this.#comboBox.setCurrentText(options.value ?? ""); + + } else { + this.#comboBox.hide(); + this.#lineEdit.show(); + this.#lineEdit.setText(options.value ?? ""); + } if (options.aroundRect != null) { const screenGeometry = window.getWidget().windowHandle().screen().geometry(); @@ -118,7 +153,12 @@ export class TextInputPopOver { }); this.#windowPopOver.show(); - this.#lineEdit.setFocus(); + + if (isComboBox) { + this.#comboBox.setFocus(); + } else { + this.#lineEdit.setFocus(); + } return new Promise((resolve, reject) => { this.#resolveFunc = resolve; diff --git a/main/src/ui/styles/DarkTwo.ts b/main/src/ui/styles/DarkTwo.ts index 6daede3b..90dfe2e7 100644 --- a/main/src/ui/styles/DarkTwo.ts +++ b/main/src/ui/styles/DarkTwo.ts @@ -9,7 +9,7 @@ import { blue, darken, green, hsl, lighten, lightness, mix, red, rgba, saturate, import { createIcon } from "../Icons.js"; import { IconPair, UiStyle } from "../UiStyle.js"; import { TitleBarStyle } from "../../config/Config.js"; -import { Color } from "packages/extraterm-color-utilities/dist/ColorUtilities.js"; +import { Color } from "extraterm-color-utilities"; function toRgba(color: string): number { diff --git a/packages/extraterm-extension-api/src/Tab.ts b/packages/extraterm-extension-api/src/Tab.ts index 1ccb8d6b..02056c99 100644 --- a/packages/extraterm-extension-api/src/Tab.ts +++ b/packages/extraterm-extension-api/src/Tab.ts @@ -123,4 +123,5 @@ export interface TextInputOptions { value: string; placeholder?: string; password?: boolean; + suggestions?: string[]; }