mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
@@ -1035,12 +1035,6 @@ export interface ITerminalInstance extends IBaseTerminalInstance {
|
||||
*/
|
||||
changeColor(color?: string, skipQuickPick?: boolean): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Triggers a quick pick that displays recent commands or cwds. Selecting one will
|
||||
* rerun it in the active terminal.
|
||||
*/
|
||||
runRecent(type: 'command' | 'cwd'): Promise<void>;
|
||||
|
||||
/**
|
||||
* Attempts to detect and kill the process listening on specified port.
|
||||
* If successful, places commandToRun on the command line
|
||||
|
||||
@@ -47,7 +47,6 @@ import { AbstractVariableResolverService } from '../../../services/configuration
|
||||
import { ITerminalQuickPickItem } from './terminalProfileQuickpick.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { getIconId, getColorClass, getUriClasses } from './terminalIcon.js';
|
||||
import { clearShellFileHistory, getCommandHistory } from '../common/history.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { ILanguageService } from '../../../../editor/common/languages/language.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
@@ -433,33 +432,6 @@ export function registerTerminalActions() {
|
||||
}
|
||||
});
|
||||
|
||||
registerActiveInstanceAction({
|
||||
id: TerminalCommandId.RunRecentCommand,
|
||||
title: localize2('workbench.action.terminal.runRecentCommand', 'Run Recent Command...'),
|
||||
precondition: sharedWhenClause.terminalAvailable,
|
||||
keybinding: [
|
||||
{
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyR,
|
||||
when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ContextKeyExpr.or(TerminalContextKeys.focus, ContextKeyExpr.and(accessibleViewIsShown, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Terminal)))),
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
{
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyR,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KeyR },
|
||||
when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()),
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
],
|
||||
run: async (activeInstance, c) => {
|
||||
await activeInstance.runRecent('command');
|
||||
if (activeInstance?.target === TerminalLocation.Editor) {
|
||||
await c.editorService.revealActiveEditor();
|
||||
} else {
|
||||
await c.groupService.showPanel(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerActiveInstanceAction({
|
||||
id: TerminalCommandId.CopyLastCommand,
|
||||
title: localize2('workbench.action.terminal.copyLastCommand', "Copy Last Command"),
|
||||
@@ -520,29 +492,6 @@ export function registerTerminalActions() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerActiveInstanceAction({
|
||||
id: TerminalCommandId.GoToRecentDirectory,
|
||||
title: localize2('workbench.action.terminal.goToRecentDirectory', 'Go to Recent Directory...'),
|
||||
metadata: {
|
||||
description: localize2('goToRecentDirectory.metadata', 'Goes to a recent folder'),
|
||||
},
|
||||
precondition: sharedWhenClause.terminalAvailable,
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyG,
|
||||
when: TerminalContextKeys.focus,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
run: async (activeInstance, c) => {
|
||||
await activeInstance.runRecent('cwd');
|
||||
if (activeInstance?.target === TerminalLocation.Editor) {
|
||||
await c.editorService.revealActiveEditor();
|
||||
} else {
|
||||
await c.groupService.showPanel(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerTerminalAction({
|
||||
id: TerminalCommandId.ResizePaneLeft,
|
||||
title: localize2('workbench.action.terminal.resizePaneLeft', 'Resize Terminal Left'),
|
||||
@@ -1502,17 +1451,6 @@ export function registerTerminalActions() {
|
||||
},
|
||||
run: (instance) => instance.toggleSizeToContentWidth()
|
||||
});
|
||||
|
||||
registerTerminalAction({
|
||||
id: TerminalCommandId.ClearPreviousSessionHistory,
|
||||
title: localize2('workbench.action.terminal.clearPreviousSessionHistory', 'Clear Previous Session History'),
|
||||
precondition: sharedWhenClause.terminalAvailable,
|
||||
run: async (c, accessor) => {
|
||||
getCommandHistory(accessor).clear();
|
||||
clearShellFileHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// Some commands depend on platform features
|
||||
if (BrowserFeatures.clipboard.writeText) {
|
||||
registerActiveXtermAction({
|
||||
|
||||
@@ -22,7 +22,7 @@ export type ITerminalContributionDescription = { readonly id: string } & (
|
||||
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals?: false): void;
|
||||
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessInfo, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals: true): void;
|
||||
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals = false): void {
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
// TODO: Pass in a context object containing instance, process manager, widgetmanager
|
||||
TerminalContributionRegistry.INSTANCE.registerTerminalContribution({ id, ctor, canRunInDetachedTerminals } as ITerminalContributionDescription);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,14 +64,13 @@ import { TerminalEditorInput } from './terminalEditorInput.js';
|
||||
import { TerminalExtensionsRegistry } from './terminalExtensions.js';
|
||||
import { getColorClass, createColorStyleElement, getStandardColors } from './terminalIcon.js';
|
||||
import { TerminalProcessManager } from './terminalProcessManager.js';
|
||||
import { showRunRecentQuickPick } from './terminalRunRecentQuickPick.js';
|
||||
import { ITerminalStatusList, TerminalStatus, TerminalStatusList } from './terminalStatusList.js';
|
||||
import { getTerminalResourcesFromDragEvent, getTerminalUri } from './terminalUri.js';
|
||||
import { TerminalWidgetManager } from './widgets/widgetManager.js';
|
||||
import { LineDataEventAddon } from './xterm/lineDataEventAddon.js';
|
||||
import { XtermTerminal, getXtermScaledDimensions } from './xterm/xtermTerminal.js';
|
||||
import { IEnvironmentVariableInfo } from '../common/environmentVariable.js';
|
||||
import { getCommandHistory, getDirectoryHistory } from '../common/history.js';
|
||||
import { getDirectoryHistory } from '../common/history.js';
|
||||
import { DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID, TerminalCommandId } from '../common/terminal.js';
|
||||
import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js';
|
||||
import { TerminalContextKeys } from '../common/terminalContextKey.js';
|
||||
@@ -362,7 +361,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
|
||||
constructor(
|
||||
private readonly _terminalShellTypeContextKey: IContextKey<string>,
|
||||
private readonly _terminalInRunCommandPicker: IContextKey<boolean>,
|
||||
private _shellLaunchConfig: IShellLaunchConfig,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@@ -450,6 +448,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._terminalShellIntegrationEnabledContextKey = TerminalContextKeys.terminalShellIntegrationEnabled.bindTo(scopedContextKeyService);
|
||||
|
||||
this._logService.trace(`terminalInstance#ctor (instanceId: ${this.instanceId})`, this._shellLaunchConfig);
|
||||
this._register(this.capabilities.onDidAddCapabilityType(e => this._logService.debug('terminalInstance added capability', e)));
|
||||
this._register(this.capabilities.onDidRemoveCapabilityType(e => this._logService.debug('terminalInstance removed capability', e)));
|
||||
this._register(this.capabilities.onDidAddCapabilityType(e => {
|
||||
this._logService.debug('terminalInstance added capability', e);
|
||||
if (e === TerminalCapability.CwdDetection) {
|
||||
@@ -458,16 +458,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._setTitle(this.title, TitleEventSource.Config);
|
||||
this._scopedInstantiationService.invokeFunction(getDirectoryHistory)?.add(e, { remoteAuthority: this.remoteAuthority });
|
||||
});
|
||||
} else if (e === TerminalCapability.CommandDetection) {
|
||||
const commandCapability = this.capabilities.get(TerminalCapability.CommandDetection);
|
||||
commandCapability?.onCommandFinished(e => {
|
||||
if (e.command.trim().length > 0) {
|
||||
this._scopedInstantiationService.invokeFunction(getCommandHistory)?.add(e.command, { shellType: this._shellType });
|
||||
}
|
||||
});
|
||||
// } else if (e === TerminalCapability.CommandDetection) {
|
||||
// const commandCapability = this.capabilities.get(TerminalCapability.CommandDetection);
|
||||
// commandCapability?.onCommandFinished(e => {
|
||||
// if (e.command.trim().length > 0) {
|
||||
// this._scopedInstantiationService.invokeFunction(getCommandHistory)?.add(e.command, { shellType: this._shellType });
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}));
|
||||
this._register(this.capabilities.onDidRemoveCapabilityType(e => this._logService.debug('terminalInstance removed capability', e)));
|
||||
|
||||
// Resolve just the icon ahead of time so that it shows up immediately in the tabs. This is
|
||||
// disabled in remote because this needs to be sync and the OS may differ on the remote
|
||||
@@ -898,12 +897,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
await this.sendText(commandLine, shouldExecute, !shouldExecute);
|
||||
}
|
||||
|
||||
async runRecent(type: 'command' | 'cwd', filterMode?: 'fuzzy' | 'contiguous', value?: string): Promise<void> {
|
||||
return this._scopedInstantiationService.invokeFunction(
|
||||
showRunRecentQuickPick, this, this._terminalInRunCommandPicker, type, filterMode, value
|
||||
);
|
||||
}
|
||||
|
||||
detachFromElement(): void {
|
||||
this._wrapperElement.remove();
|
||||
this._container = undefined;
|
||||
|
||||
@@ -20,7 +20,6 @@ import { promiseWithResolvers } from '../../../../base/common/async.js';
|
||||
export class TerminalInstanceService extends Disposable implements ITerminalInstanceService {
|
||||
declare _serviceBrand: undefined;
|
||||
private _terminalShellTypeContextKey: IContextKey<string>;
|
||||
private _terminalInRunCommandPicker: IContextKey<boolean>;
|
||||
private _backendRegistration = new Map<string | undefined, { promise: Promise<void>; resolve: () => void }>();
|
||||
|
||||
private readonly _onDidCreateInstance = this._register(new Emitter<ITerminalInstance>());
|
||||
@@ -36,7 +35,6 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
|
||||
) {
|
||||
super();
|
||||
this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService);
|
||||
this._terminalInRunCommandPicker = TerminalContextKeys.inTerminalRunCommandPicker.bindTo(this._contextKeyService);
|
||||
|
||||
for (const remoteAuthority of [undefined, environmentService.remoteAuthority]) {
|
||||
const { promise, resolve } = promiseWithResolvers<void>();
|
||||
@@ -48,11 +46,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
|
||||
createInstance(shellLaunchConfig: IShellLaunchConfig, target: TerminalLocation): ITerminalInstance;
|
||||
createInstance(config: IShellLaunchConfig | ITerminalProfile, target: TerminalLocation): ITerminalInstance {
|
||||
const shellLaunchConfig = this.convertProfileToShellLaunchConfig(config);
|
||||
const instance = this._instantiationService.createInstance(TerminalInstance,
|
||||
this._terminalShellTypeContextKey,
|
||||
this._terminalInRunCommandPicker,
|
||||
shellLaunchConfig
|
||||
);
|
||||
const instance = this._instantiationService.createInstance(TerminalInstance, this._terminalShellTypeContextKey, shellLaunchConfig);
|
||||
instance.target = target;
|
||||
this._onDidCreateInstance.fire(instance);
|
||||
return instance;
|
||||
|
||||
@@ -3,19 +3,12 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { env } from '../../../../base/common/process.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { LRUCache } from '../../../../base/common/map.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { FileOperationError, FileOperationResult, IFileContent, IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { GeneralShellType, PosixShellType, TerminalSettingId, TerminalShellType } from '../../../../platform/terminal/common/terminal.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
|
||||
import { Schemas } from '../../../../base/common/network.js';
|
||||
import { isWindows, OperatingSystem } from '../../../../base/common/platform.js';
|
||||
import { join } from '../../../../base/common/path.js';
|
||||
import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js';
|
||||
|
||||
/**
|
||||
* Tracks a list of generic entries.
|
||||
@@ -52,14 +45,6 @@ const enum StorageKeys {
|
||||
Timestamp = 'terminal.history.timestamp'
|
||||
}
|
||||
|
||||
let commandHistory: ITerminalPersistedHistory<{ shellType: TerminalShellType }> | undefined = undefined;
|
||||
export function getCommandHistory(accessor: ServicesAccessor): ITerminalPersistedHistory<{ shellType: TerminalShellType | undefined }> {
|
||||
if (!commandHistory) {
|
||||
commandHistory = accessor.get(IInstantiationService).createInstance(TerminalPersistedHistory, 'commands') as TerminalPersistedHistory<{ shellType: TerminalShellType }>;
|
||||
}
|
||||
return commandHistory;
|
||||
}
|
||||
|
||||
let directoryHistory: ITerminalPersistedHistory<{ remoteAuthority?: string }> | undefined = undefined;
|
||||
export function getDirectoryHistory(accessor: ServicesAccessor): ITerminalPersistedHistory<{ remoteAuthority?: string }> {
|
||||
if (!directoryHistory) {
|
||||
@@ -68,46 +53,6 @@ export function getDirectoryHistory(accessor: ServicesAccessor): ITerminalPersis
|
||||
return directoryHistory;
|
||||
}
|
||||
|
||||
// Shell file history loads once per shell per window
|
||||
const shellFileHistory: Map<TerminalShellType | undefined, string[] | null> = new Map();
|
||||
export async function getShellFileHistory(accessor: ServicesAccessor, shellType: TerminalShellType | undefined): Promise<string[]> {
|
||||
const cached = shellFileHistory.get(shellType);
|
||||
if (cached === null) {
|
||||
return [];
|
||||
}
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
let result: IterableIterator<string> | undefined;
|
||||
switch (shellType) {
|
||||
case PosixShellType.Bash:
|
||||
result = await fetchBashHistory(accessor);
|
||||
break;
|
||||
case GeneralShellType.PowerShell:
|
||||
result = await fetchPwshHistory(accessor);
|
||||
break;
|
||||
case PosixShellType.Zsh:
|
||||
result = await fetchZshHistory(accessor);
|
||||
break;
|
||||
case PosixShellType.Fish:
|
||||
result = await fetchFishHistory(accessor);
|
||||
break;
|
||||
case GeneralShellType.Python:
|
||||
result = await fetchPythonHistory(accessor);
|
||||
break;
|
||||
default: return [];
|
||||
}
|
||||
if (result === undefined) {
|
||||
shellFileHistory.set(shellType, null);
|
||||
return [];
|
||||
}
|
||||
const array = Array.from(result);
|
||||
shellFileHistory.set(shellType, array);
|
||||
return array;
|
||||
}
|
||||
export function clearShellFileHistory() {
|
||||
shellFileHistory.clear();
|
||||
}
|
||||
|
||||
export class TerminalPersistedHistory<T> extends Disposable implements ITerminalPersistedHistory<T> {
|
||||
private readonly _entries: LRUCache<string, T>;
|
||||
@@ -228,281 +173,3 @@ export class TerminalPersistedHistory<T> extends Disposable implements ITerminal
|
||||
return `${StorageKeys.Entries}.${this._storageDataKey}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBashHistory(accessor: ServicesAccessor): Promise<IterableIterator<string> | undefined> {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) {
|
||||
return undefined;
|
||||
}
|
||||
const content = await fetchFileContents(env['HOME'], '.bash_history', false, fileService, remoteAgentService);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// .bash_history does not differentiate wrapped commands from multiple commands. Parse
|
||||
// the output to get the
|
||||
const fileLines = content.split('\n');
|
||||
const result: Set<string> = new Set();
|
||||
let currentLine: string;
|
||||
let currentCommand: string | undefined = undefined;
|
||||
let wrapChar: string | undefined = undefined;
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
currentLine = fileLines[i];
|
||||
if (currentCommand === undefined) {
|
||||
currentCommand = currentLine;
|
||||
} else {
|
||||
currentCommand += `\n${currentLine}`;
|
||||
}
|
||||
for (let c = 0; c < currentLine.length; c++) {
|
||||
if (wrapChar) {
|
||||
if (currentLine[c] === wrapChar) {
|
||||
wrapChar = undefined;
|
||||
}
|
||||
} else {
|
||||
if (currentLine[c].match(/['"]/)) {
|
||||
wrapChar = currentLine[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrapChar === undefined) {
|
||||
if (currentCommand.length > 0) {
|
||||
result.add(currentCommand.trim());
|
||||
}
|
||||
currentCommand = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export async function fetchZshHistory(accessor: ServicesAccessor) {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) {
|
||||
return undefined;
|
||||
}
|
||||
const content = await fetchFileContents(env['HOME'], '.zsh_history', false, fileService, remoteAgentService);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const fileLines = content.split(/\:\s\d+\:\d+;/);
|
||||
const result: Set<string> = new Set();
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
const sanitized = fileLines[i].replace(/\\\n/g, '\n').trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
}
|
||||
return result.values();
|
||||
}
|
||||
|
||||
|
||||
export async function fetchPythonHistory(accessor: ServicesAccessor): Promise<IterableIterator<string> | undefined> {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
|
||||
const content = await fetchFileContents(env['HOME'], '.python_history', false, fileService, remoteAgentService);
|
||||
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Python history file is a simple text file with one command per line
|
||||
const fileLines = content.split('\n');
|
||||
const result: Set<string> = new Set();
|
||||
|
||||
fileLines.forEach(line => {
|
||||
if (line.trim().length > 0) {
|
||||
result.add(line.trim());
|
||||
}
|
||||
});
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export async function fetchPwshHistory(accessor: ServicesAccessor) {
|
||||
const fileService: Pick<IFileService, 'readFile'> = accessor.get(IFileService);
|
||||
const remoteAgentService: Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'> = accessor.get(IRemoteAgentService);
|
||||
let folderPrefix: string | undefined;
|
||||
let filePath: string;
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
const isFileWindows = remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows;
|
||||
if (isFileWindows) {
|
||||
folderPrefix = env['APPDATA'];
|
||||
filePath = 'Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt';
|
||||
} else {
|
||||
folderPrefix = env['HOME'];
|
||||
filePath = '.local/share/powershell/PSReadline/ConsoleHost_history.txt';
|
||||
}
|
||||
const content = await fetchFileContents(folderPrefix, filePath, isFileWindows, fileService, remoteAgentService);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const fileLines = content.split('\n');
|
||||
const result: Set<string> = new Set();
|
||||
let currentLine: string;
|
||||
let currentCommand: string | undefined = undefined;
|
||||
let wrapChar: string | undefined = undefined;
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
currentLine = fileLines[i];
|
||||
if (currentCommand === undefined) {
|
||||
currentCommand = currentLine;
|
||||
} else {
|
||||
currentCommand += `\n${currentLine}`;
|
||||
}
|
||||
if (!currentLine.endsWith('`')) {
|
||||
const sanitized = currentCommand.trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
currentCommand = undefined;
|
||||
continue;
|
||||
}
|
||||
// If the line ends with `, the line may be wrapped. Need to also test the case where ` is
|
||||
// the last character in the line
|
||||
for (let c = 0; c < currentLine.length; c++) {
|
||||
if (wrapChar) {
|
||||
if (currentLine[c] === wrapChar) {
|
||||
wrapChar = undefined;
|
||||
}
|
||||
} else {
|
||||
if (currentLine[c].match(/`/)) {
|
||||
wrapChar = currentLine[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Having an even number of backticks means the line is terminated
|
||||
// TODO: This doesn't cover more complicated cases where ` is within quotes
|
||||
if (!wrapChar) {
|
||||
const sanitized = currentCommand.trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
currentCommand = undefined;
|
||||
} else {
|
||||
// Remove trailing backtick
|
||||
currentCommand = currentCommand.replace(/`$/, '');
|
||||
wrapChar = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export async function fetchFishHistory(accessor: ServicesAccessor) {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* From `fish` docs:
|
||||
* > The command history is stored in the file ~/.local/share/fish/fish_history
|
||||
* (or $XDG_DATA_HOME/fish/fish_history if that variable is set) by default.
|
||||
*
|
||||
* (https://fishshell.com/docs/current/interactive.html#history-search)
|
||||
*/
|
||||
const overridenDataHome = env['XDG_DATA_HOME'];
|
||||
|
||||
// TODO: Unchecked fish behavior:
|
||||
// What if XDG_DATA_HOME was defined but somehow $XDG_DATA_HOME/fish/fish_history
|
||||
// was not exist. Does fish fall back to ~/.local/share/fish/fish_history?
|
||||
|
||||
const content = await (overridenDataHome
|
||||
? fetchFileContents(env['XDG_DATA_HOME'], 'fish/fish_history', false, fileService, remoteAgentService)
|
||||
: fetchFileContents(env['HOME'], '.local/share/fish/fish_history', false, fileService, remoteAgentService));
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* These apply to `fish` v3.5.1:
|
||||
* - It looks like YAML but it's not. It's, quoting, *"a broken psuedo-YAML"*.
|
||||
* See these discussions for more details:
|
||||
* - https://github.com/fish-shell/fish-shell/pull/6493
|
||||
* - https://github.com/fish-shell/fish-shell/issues/3341
|
||||
* - Every record should exactly start with `- cmd:` (the whitespace between `-` and `cmd` cannot be replaced with tab)
|
||||
* - Both `- cmd: echo 1` and `- cmd:echo 1` are valid entries.
|
||||
* - Backslashes are esacped as `\\`.
|
||||
* - Multiline commands are joined with a `\n` sequence, hence they're read as single line commands.
|
||||
* - Property `when` is optional.
|
||||
* - History navigation respects the records order and ignore the actual `when` property values (chronological order).
|
||||
* - If `cmd` value is multiline , it just takes the first line. Also YAML operators like `>-` or `|-` are not supported.
|
||||
*/
|
||||
const result: Set<string> = new Set();
|
||||
const cmds = content.split('\n')
|
||||
.filter(x => x.startsWith('- cmd:'))
|
||||
.map(x => x.substring(6).trimStart());
|
||||
for (let i = 0; i < cmds.length; i++) {
|
||||
const sanitized = sanitizeFishHistoryCmd(cmds[i]).trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
}
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export function sanitizeFishHistoryCmd(cmd: string): string {
|
||||
/**
|
||||
* NOTE
|
||||
* This repeatedReplace() call can be eliminated by using look-ahead
|
||||
* caluses in the original RegExp pattern:
|
||||
*
|
||||
* >>> ```ts
|
||||
* >>> cmds[i].replace(/(?<=^|[^\\])((?:\\\\)*)(\\n)/g, '$1\n')
|
||||
* >>> ```
|
||||
*
|
||||
* But since not all browsers support look aheads we opted to a simple
|
||||
* pattern and repeatedly calling replace method.
|
||||
*/
|
||||
return repeatedReplace(/(^|[^\\])((?:\\\\)*)(\\n)/g, cmd, '$1$2\n');
|
||||
}
|
||||
|
||||
function repeatedReplace(pattern: RegExp, value: string, replaceValue: string): string {
|
||||
let last;
|
||||
let current = value;
|
||||
while (true) {
|
||||
last = current;
|
||||
current = current.replace(pattern, replaceValue);
|
||||
if (current === last) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFileContents(
|
||||
folderPrefix: string | undefined,
|
||||
filePath: string,
|
||||
isFileWindows: boolean,
|
||||
fileService: Pick<IFileService, 'readFile'>,
|
||||
remoteAgentService: Pick<IRemoteAgentService, 'getConnection'>,
|
||||
): Promise<string | undefined> {
|
||||
if (!folderPrefix) {
|
||||
return undefined;
|
||||
}
|
||||
const connection = remoteAgentService.getConnection();
|
||||
const isRemote = !!connection?.remoteAuthority;
|
||||
const historyFileUri = URI.from({
|
||||
scheme: isRemote ? Schemas.vscodeRemote : Schemas.file,
|
||||
authority: isRemote ? connection.remoteAuthority : undefined,
|
||||
path: URI.file(join(folderPrefix, filePath)).path
|
||||
});
|
||||
let content: IFileContent;
|
||||
try {
|
||||
content = await fileService.readFile(historyFileUri);
|
||||
} catch (e: unknown) {
|
||||
// Handle file not found only
|
||||
if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
|
||||
return undefined;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return content.value.toString();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../terminalContrib/environmentChanges/browser/terminal.environmentChange
|
||||
import '../terminalContrib/find/browser/terminal.find.contribution.js';
|
||||
import '../terminalContrib/chat/browser/terminal.chat.contribution.js';
|
||||
import '../terminalContrib/commandGuide/browser/terminal.commandGuide.contribution.js';
|
||||
import '../terminalContrib/history/browser/terminal.history.contribution.js';
|
||||
import '../terminalContrib/links/browser/terminal.links.contribution.js';
|
||||
import '../terminalContrib/zoom/browser/terminal.zoom.contribution.js';
|
||||
import '../terminalContrib/stickyScroll/browser/terminal.stickyScroll.contribution.js';
|
||||
|
||||
@@ -58,12 +58,6 @@ const terminalShellTypeContextKey = {
|
||||
get: () => undefined
|
||||
};
|
||||
|
||||
const terminalInRunCommandPicker = {
|
||||
set: () => { },
|
||||
reset: () => { },
|
||||
get: () => undefined
|
||||
};
|
||||
|
||||
class TestTerminalChildProcess extends Disposable implements ITerminalChildProcess {
|
||||
id: number = 0;
|
||||
get capabilities() { return []; }
|
||||
@@ -150,7 +144,7 @@ suite('Workbench - TerminalInstance', () => {
|
||||
instantiationService.stub(IViewDescriptorService, new TestViewDescriptorService());
|
||||
instantiationService.stub(IEnvironmentVariableService, store.add(instantiationService.createInstance(EnvironmentVariableService)));
|
||||
instantiationService.stub(ITerminalInstanceService, store.add(new TestTerminalInstanceService()));
|
||||
terminalInstance = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, terminalInRunCommandPicker, {}));
|
||||
terminalInstance = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, {}));
|
||||
// //Wait for the teminalInstance._xtermReadyPromise to resolve
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
deepStrictEqual(terminalInstance.shellLaunchConfig.env, { TEST: 'TEST' });
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { localize2 } from '../../../../../nls.js';
|
||||
import { AccessibleViewProviderId } from '../../../../../platform/accessibility/browser/accessibleView.js';
|
||||
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js';
|
||||
import { ContextKeyExpr, IContextKeyService, type IContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js';
|
||||
import { accessibleViewCurrentProviderId, accessibleViewIsShown } from '../../../accessibility/browser/accessibilityConfiguration.js';
|
||||
import type { ITerminalContribution, ITerminalInstance } from '../../../terminal/browser/terminal.js';
|
||||
import { registerActiveInstanceAction, registerTerminalAction } from '../../../terminal/browser/terminalActions.js';
|
||||
import { registerTerminalContribution } from '../../../terminal/browser/terminalExtensions.js';
|
||||
import type { TerminalWidgetManager } from '../../../terminal/browser/widgets/widgetManager.js';
|
||||
import { TerminalCommandId, type ITerminalProcessManager } from '../../../terminal/common/terminal.js';
|
||||
import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js';
|
||||
import { clearShellFileHistory, getCommandHistory } from '../common/history.js';
|
||||
import { showRunRecentQuickPick } from './terminalRunRecentQuickPick.js';
|
||||
|
||||
// #region Terminal Contributions
|
||||
|
||||
class TerminalHistoryContribution extends Disposable implements ITerminalContribution {
|
||||
static readonly ID = 'terminal.history';
|
||||
|
||||
static get(instance: ITerminalInstance): TerminalHistoryContribution | null {
|
||||
return instance.getContribution<TerminalHistoryContribution>(TerminalHistoryContribution.ID);
|
||||
}
|
||||
|
||||
private _terminalInRunCommandPicker: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
private readonly _instance: ITerminalInstance,
|
||||
processManager: ITerminalProcessManager,
|
||||
widgetManager: TerminalWidgetManager,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._terminalInRunCommandPicker = TerminalContextKeys.inTerminalRunCommandPicker.bindTo(this._contextKeyService);
|
||||
|
||||
this._register(this._instance.capabilities.onDidAddCapabilityType(e => {
|
||||
if (e === TerminalCapability.CommandDetection) {
|
||||
const commandCapability = this._instance.capabilities.get(TerminalCapability.CommandDetection);
|
||||
commandCapability?.onCommandFinished(e => {
|
||||
if (e.command.trim().length > 0) {
|
||||
this._instantiationService.invokeFunction(getCommandHistory)?.add(e.command, { shellType: this._instance.shellType });
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a quick pick that displays recent commands or cwds. Selecting one will
|
||||
* rerun it in the active terminal.
|
||||
*/
|
||||
async runRecent(type: 'command' | 'cwd', filterMode?: 'fuzzy' | 'contiguous', value?: string): Promise<void> {
|
||||
return this._instantiationService.invokeFunction(showRunRecentQuickPick,
|
||||
this._instance,
|
||||
this._terminalInRunCommandPicker,
|
||||
type,
|
||||
filterMode,
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerTerminalContribution(TerminalHistoryContribution.ID, TerminalHistoryContribution);
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Actions
|
||||
|
||||
const precondition = ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated);
|
||||
|
||||
registerActiveInstanceAction({
|
||||
id: TerminalCommandId.RunRecentCommand,
|
||||
title: localize2('workbench.action.terminal.runRecentCommand', 'Run Recent Command...'),
|
||||
precondition,
|
||||
keybinding: [
|
||||
{
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyR,
|
||||
when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ContextKeyExpr.or(TerminalContextKeys.focus, ContextKeyExpr.and(accessibleViewIsShown, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Terminal)))),
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
{
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyR,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KeyR },
|
||||
when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()),
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
],
|
||||
run: async (activeInstance, c) => {
|
||||
const history = TerminalHistoryContribution.get(activeInstance);
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
await history.runRecent('command');
|
||||
if (activeInstance?.target === TerminalLocation.Editor) {
|
||||
await c.editorService.revealActiveEditor();
|
||||
} else {
|
||||
await c.groupService.showPanel(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: move command IDs into this file
|
||||
registerActiveInstanceAction({
|
||||
id: TerminalCommandId.GoToRecentDirectory,
|
||||
title: localize2('workbench.action.terminal.goToRecentDirectory', 'Go to Recent Directory...'),
|
||||
metadata: {
|
||||
description: localize2('goToRecentDirectory.metadata', 'Goes to a recent folder'),
|
||||
},
|
||||
precondition,
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyG,
|
||||
when: TerminalContextKeys.focus,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
run: async (activeInstance, c) => {
|
||||
const history = TerminalHistoryContribution.get(activeInstance);
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
await history.runRecent('cwd');
|
||||
if (activeInstance?.target === TerminalLocation.Editor) {
|
||||
await c.editorService.revealActiveEditor();
|
||||
} else {
|
||||
await c.groupService.showPanel(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerTerminalAction({
|
||||
id: TerminalCommandId.ClearPreviousSessionHistory,
|
||||
title: localize2('workbench.action.terminal.clearPreviousSessionHistory', 'Clear Previous Session History'),
|
||||
precondition,
|
||||
run: async (c, accessor) => {
|
||||
getCommandHistory(accessor).clear();
|
||||
clearShellFileHistory();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// #endregion
|
||||
@@ -3,31 +3,32 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js';
|
||||
import { isMacintosh, OperatingSystem } from '../../../../base/common/platform.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
|
||||
import { ITerminalCommand, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { collapseTildePath } from '../../../../platform/terminal/common/terminalEnvironment.js';
|
||||
import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from '../../../../platform/theme/common/colorRegistry.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { ITerminalInstance } from './terminal.js';
|
||||
import { commandHistoryFuzzySearchIcon, commandHistoryOutputIcon, commandHistoryRemoveIcon } from './terminalIcons.js';
|
||||
import { getCommandHistory, getDirectoryHistory, getShellFileHistory } from '../common/history.js';
|
||||
import { TerminalStorageKeys } from '../common/terminalStorageKeys.js';
|
||||
import { terminalStrings } from '../common/terminalStrings.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { fromNow } from '../../../../base/common/date.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { showWithPinnedItems } from '../../../../platform/quickinput/browser/quickPickPin.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
import { IContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { AccessibleViewProviderId, IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js';
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { Toggle } from '../../../../../base/browser/ui/toggle/toggle.js';
|
||||
import { isMacintosh, OperatingSystem } from '../../../../../base/common/platform.js';
|
||||
import { ITextModel } from '../../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { ITextModelContentProvider, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { collapseTildePath } from '../../../../../platform/terminal/common/terminalEnvironment.js';
|
||||
import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from '../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { ITerminalInstance } from '../../../terminal/browser/terminal.js';
|
||||
import { commandHistoryFuzzySearchIcon, commandHistoryOutputIcon, commandHistoryRemoveIcon } from '../../../terminal/browser/terminalIcons.js';
|
||||
import { getDirectoryHistory } from '../../../terminal/common/history.js';
|
||||
import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js';
|
||||
import { terminalStrings } from '../../../terminal/common/terminalStrings.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { fromNow } from '../../../../../base/common/date.js';
|
||||
import { IEditorService } from '../../../../services/editor/common/editorService.js';
|
||||
import { showWithPinnedItems } from '../../../../../platform/quickinput/browser/quickPickPin.js';
|
||||
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { IContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { AccessibleViewProviderId, IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js';
|
||||
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { getCommandHistory, getShellFileHistory } from '../common/history.js';
|
||||
|
||||
export async function showRunRecentQuickPick(
|
||||
accessor: ServicesAccessor,
|
||||
@@ -0,0 +1,343 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Schemas } from '../../../../../base/common/network.js';
|
||||
import { join } from '../../../../../base/common/path.js';
|
||||
import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js';
|
||||
import { env } from '../../../../../base/common/process.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { FileOperationError, FileOperationResult, IFileContent, IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { GeneralShellType, PosixShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
|
||||
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
|
||||
import { TerminalPersistedHistory, type ITerminalPersistedHistory } from '../../../terminal/common/history.js';
|
||||
|
||||
|
||||
let commandHistory: ITerminalPersistedHistory<{ shellType: TerminalShellType }> | undefined = undefined;
|
||||
export function getCommandHistory(accessor: ServicesAccessor): ITerminalPersistedHistory<{ shellType: TerminalShellType | undefined }> {
|
||||
if (!commandHistory) {
|
||||
commandHistory = accessor.get(IInstantiationService).createInstance(TerminalPersistedHistory, 'commands') as TerminalPersistedHistory<{ shellType: TerminalShellType }>;
|
||||
}
|
||||
return commandHistory;
|
||||
}
|
||||
|
||||
// Shell file history loads once per shell per window
|
||||
const shellFileHistory: Map<TerminalShellType | undefined, string[] | null> = new Map();
|
||||
export async function getShellFileHistory(accessor: ServicesAccessor, shellType: TerminalShellType | undefined): Promise<string[]> {
|
||||
const cached = shellFileHistory.get(shellType);
|
||||
if (cached === null) {
|
||||
return [];
|
||||
}
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
let result: IterableIterator<string> | undefined;
|
||||
switch (shellType) {
|
||||
case PosixShellType.Bash:
|
||||
result = await fetchBashHistory(accessor);
|
||||
break;
|
||||
case GeneralShellType.PowerShell:
|
||||
result = await fetchPwshHistory(accessor);
|
||||
break;
|
||||
case PosixShellType.Zsh:
|
||||
result = await fetchZshHistory(accessor);
|
||||
break;
|
||||
case PosixShellType.Fish:
|
||||
result = await fetchFishHistory(accessor);
|
||||
break;
|
||||
case GeneralShellType.Python:
|
||||
result = await fetchPythonHistory(accessor);
|
||||
break;
|
||||
default: return [];
|
||||
}
|
||||
if (result === undefined) {
|
||||
shellFileHistory.set(shellType, null);
|
||||
return [];
|
||||
}
|
||||
const array = Array.from(result);
|
||||
shellFileHistory.set(shellType, array);
|
||||
return array;
|
||||
}
|
||||
export function clearShellFileHistory() {
|
||||
shellFileHistory.clear();
|
||||
}
|
||||
|
||||
export async function fetchBashHistory(accessor: ServicesAccessor): Promise<IterableIterator<string> | undefined> {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) {
|
||||
return undefined;
|
||||
}
|
||||
const content = await fetchFileContents(env['HOME'], '.bash_history', false, fileService, remoteAgentService);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// .bash_history does not differentiate wrapped commands from multiple commands. Parse
|
||||
// the output to get the
|
||||
const fileLines = content.split('\n');
|
||||
const result: Set<string> = new Set();
|
||||
let currentLine: string;
|
||||
let currentCommand: string | undefined = undefined;
|
||||
let wrapChar: string | undefined = undefined;
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
currentLine = fileLines[i];
|
||||
if (currentCommand === undefined) {
|
||||
currentCommand = currentLine;
|
||||
} else {
|
||||
currentCommand += `\n${currentLine}`;
|
||||
}
|
||||
for (let c = 0; c < currentLine.length; c++) {
|
||||
if (wrapChar) {
|
||||
if (currentLine[c] === wrapChar) {
|
||||
wrapChar = undefined;
|
||||
}
|
||||
} else {
|
||||
if (currentLine[c].match(/['"]/)) {
|
||||
wrapChar = currentLine[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrapChar === undefined) {
|
||||
if (currentCommand.length > 0) {
|
||||
result.add(currentCommand.trim());
|
||||
}
|
||||
currentCommand = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export async function fetchZshHistory(accessor: ServicesAccessor) {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) {
|
||||
return undefined;
|
||||
}
|
||||
const content = await fetchFileContents(env['HOME'], '.zsh_history', false, fileService, remoteAgentService);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const fileLines = content.split(/\:\s\d+\:\d+;/);
|
||||
const result: Set<string> = new Set();
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
const sanitized = fileLines[i].replace(/\\\n/g, '\n').trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
}
|
||||
return result.values();
|
||||
}
|
||||
|
||||
|
||||
export async function fetchPythonHistory(accessor: ServicesAccessor): Promise<IterableIterator<string> | undefined> {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
|
||||
const content = await fetchFileContents(env['HOME'], '.python_history', false, fileService, remoteAgentService);
|
||||
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Python history file is a simple text file with one command per line
|
||||
const fileLines = content.split('\n');
|
||||
const result: Set<string> = new Set();
|
||||
|
||||
fileLines.forEach(line => {
|
||||
if (line.trim().length > 0) {
|
||||
result.add(line.trim());
|
||||
}
|
||||
});
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export async function fetchPwshHistory(accessor: ServicesAccessor) {
|
||||
const fileService: Pick<IFileService, 'readFile'> = accessor.get(IFileService);
|
||||
const remoteAgentService: Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'> = accessor.get(IRemoteAgentService);
|
||||
let folderPrefix: string | undefined;
|
||||
let filePath: string;
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
const isFileWindows = remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows;
|
||||
if (isFileWindows) {
|
||||
folderPrefix = env['APPDATA'];
|
||||
filePath = 'Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt';
|
||||
} else {
|
||||
folderPrefix = env['HOME'];
|
||||
filePath = '.local/share/powershell/PSReadline/ConsoleHost_history.txt';
|
||||
}
|
||||
const content = await fetchFileContents(folderPrefix, filePath, isFileWindows, fileService, remoteAgentService);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const fileLines = content.split('\n');
|
||||
const result: Set<string> = new Set();
|
||||
let currentLine: string;
|
||||
let currentCommand: string | undefined = undefined;
|
||||
let wrapChar: string | undefined = undefined;
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
currentLine = fileLines[i];
|
||||
if (currentCommand === undefined) {
|
||||
currentCommand = currentLine;
|
||||
} else {
|
||||
currentCommand += `\n${currentLine}`;
|
||||
}
|
||||
if (!currentLine.endsWith('`')) {
|
||||
const sanitized = currentCommand.trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
currentCommand = undefined;
|
||||
continue;
|
||||
}
|
||||
// If the line ends with `, the line may be wrapped. Need to also test the case where ` is
|
||||
// the last character in the line
|
||||
for (let c = 0; c < currentLine.length; c++) {
|
||||
if (wrapChar) {
|
||||
if (currentLine[c] === wrapChar) {
|
||||
wrapChar = undefined;
|
||||
}
|
||||
} else {
|
||||
if (currentLine[c].match(/`/)) {
|
||||
wrapChar = currentLine[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Having an even number of backticks means the line is terminated
|
||||
// TODO: This doesn't cover more complicated cases where ` is within quotes
|
||||
if (!wrapChar) {
|
||||
const sanitized = currentCommand.trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
currentCommand = undefined;
|
||||
} else {
|
||||
// Remove trailing backtick
|
||||
currentCommand = currentCommand.replace(/`$/, '');
|
||||
wrapChar = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export async function fetchFishHistory(accessor: ServicesAccessor) {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const remoteAgentService = accessor.get(IRemoteAgentService);
|
||||
const remoteEnvironment = await remoteAgentService.getEnvironment();
|
||||
if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* From `fish` docs:
|
||||
* > The command history is stored in the file ~/.local/share/fish/fish_history
|
||||
* (or $XDG_DATA_HOME/fish/fish_history if that variable is set) by default.
|
||||
*
|
||||
* (https://fishshell.com/docs/current/interactive.html#history-search)
|
||||
*/
|
||||
const overridenDataHome = env['XDG_DATA_HOME'];
|
||||
|
||||
// TODO: Unchecked fish behavior:
|
||||
// What if XDG_DATA_HOME was defined but somehow $XDG_DATA_HOME/fish/fish_history
|
||||
// was not exist. Does fish fall back to ~/.local/share/fish/fish_history?
|
||||
|
||||
const content = await (overridenDataHome
|
||||
? fetchFileContents(env['XDG_DATA_HOME'], 'fish/fish_history', false, fileService, remoteAgentService)
|
||||
: fetchFileContents(env['HOME'], '.local/share/fish/fish_history', false, fileService, remoteAgentService));
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* These apply to `fish` v3.5.1:
|
||||
* - It looks like YAML but it's not. It's, quoting, *"a broken psuedo-YAML"*.
|
||||
* See these discussions for more details:
|
||||
* - https://github.com/fish-shell/fish-shell/pull/6493
|
||||
* - https://github.com/fish-shell/fish-shell/issues/3341
|
||||
* - Every record should exactly start with `- cmd:` (the whitespace between `-` and `cmd` cannot be replaced with tab)
|
||||
* - Both `- cmd: echo 1` and `- cmd:echo 1` are valid entries.
|
||||
* - Backslashes are esacped as `\\`.
|
||||
* - Multiline commands are joined with a `\n` sequence, hence they're read as single line commands.
|
||||
* - Property `when` is optional.
|
||||
* - History navigation respects the records order and ignore the actual `when` property values (chronological order).
|
||||
* - If `cmd` value is multiline , it just takes the first line. Also YAML operators like `>-` or `|-` are not supported.
|
||||
*/
|
||||
const result: Set<string> = new Set();
|
||||
const cmds = content.split('\n')
|
||||
.filter(x => x.startsWith('- cmd:'))
|
||||
.map(x => x.substring(6).trimStart());
|
||||
for (let i = 0; i < cmds.length; i++) {
|
||||
const sanitized = sanitizeFishHistoryCmd(cmds[i]).trim();
|
||||
if (sanitized.length > 0) {
|
||||
result.add(sanitized);
|
||||
}
|
||||
}
|
||||
return result.values();
|
||||
}
|
||||
|
||||
export function sanitizeFishHistoryCmd(cmd: string): string {
|
||||
/**
|
||||
* NOTE
|
||||
* This repeatedReplace() call can be eliminated by using look-ahead
|
||||
* caluses in the original RegExp pattern:
|
||||
*
|
||||
* >>> ```ts
|
||||
* >>> cmds[i].replace(/(?<=^|[^\\])((?:\\\\)*)(\\n)/g, '$1\n')
|
||||
* >>> ```
|
||||
*
|
||||
* But since not all browsers support look aheads we opted to a simple
|
||||
* pattern and repeatedly calling replace method.
|
||||
*/
|
||||
return repeatedReplace(/(^|[^\\])((?:\\\\)*)(\\n)/g, cmd, '$1$2\n');
|
||||
}
|
||||
|
||||
function repeatedReplace(pattern: RegExp, value: string, replaceValue: string): string {
|
||||
let last;
|
||||
let current = value;
|
||||
while (true) {
|
||||
last = current;
|
||||
current = current.replace(pattern, replaceValue);
|
||||
if (current === last) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFileContents(
|
||||
folderPrefix: string | undefined,
|
||||
filePath: string,
|
||||
isFileWindows: boolean,
|
||||
fileService: Pick<IFileService, 'readFile'>,
|
||||
remoteAgentService: Pick<IRemoteAgentService, 'getConnection'>,
|
||||
): Promise<string | undefined> {
|
||||
if (!folderPrefix) {
|
||||
return undefined;
|
||||
}
|
||||
const connection = remoteAgentService.getConnection();
|
||||
const isRemote = !!connection?.remoteAuthority;
|
||||
const historyFileUri = URI.from({
|
||||
scheme: isRemote ? Schemas.vscodeRemote : Schemas.file,
|
||||
authority: isRemote ? connection.remoteAuthority : undefined,
|
||||
path: URI.file(join(folderPrefix, filePath)).path
|
||||
});
|
||||
let content: IFileContent;
|
||||
try {
|
||||
content = await fileService.readFile(historyFileUri);
|
||||
} catch (e: unknown) {
|
||||
// Handle file not found only
|
||||
if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
|
||||
return undefined;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return content.value.toString();
|
||||
}
|
||||
@@ -4,22 +4,23 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual, strictEqual, ok } from 'assert';
|
||||
import { VSBuffer } from '../../../../../base/common/buffer.js';
|
||||
import { Schemas } from '../../../../../base/common/network.js';
|
||||
import { join } from '../../../../../base/common/path.js';
|
||||
import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js';
|
||||
import { env } from '../../../../../base/common/process.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { IRemoteAgentEnvironment } from '../../../../../platform/remote/common/remoteAgentEnvironment.js';
|
||||
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { fetchBashHistory, fetchFishHistory, fetchPwshHistory, fetchZshHistory, ITerminalPersistedHistory, sanitizeFishHistoryCmd, TerminalPersistedHistory } from '../../common/history.js';
|
||||
import { IRemoteAgentConnection, IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
|
||||
import { TestStorageService } from '../../../../test/common/workbenchTestServices.js';
|
||||
import { VSBuffer } from '../../../../../../base/common/buffer.js';
|
||||
import { Schemas } from '../../../../../../base/common/network.js';
|
||||
import { join } from '../../../../../../base/common/path.js';
|
||||
import { isWindows, OperatingSystem } from '../../../../../../base/common/platform.js';
|
||||
import { env } from '../../../../../../base/common/process.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IFileService } from '../../../../../../platform/files/common/files.js';
|
||||
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { IRemoteAgentEnvironment } from '../../../../../../platform/remote/common/remoteAgentEnvironment.js';
|
||||
import { IStorageService } from '../../../../../../platform/storage/common/storage.js';
|
||||
import { ITerminalPersistedHistory, TerminalPersistedHistory } from '../../../../terminal/common/history.js';
|
||||
import { IRemoteAgentConnection, IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
|
||||
import { TestStorageService } from '../../../../../test/common/workbenchTestServices.js';
|
||||
import { fetchBashHistory, fetchFishHistory, fetchPwshHistory, fetchZshHistory, sanitizeFishHistoryCmd } from '../../common/history.js';
|
||||
|
||||
function getConfig(limit: number) {
|
||||
return {
|
||||
Reference in New Issue
Block a user