Move most of history into terminalContrib

Part of #230129
This commit is contained in:
Daniel Imms
2024-10-01 07:12:23 -07:00
parent 620fd6cb9a
commit 7ee03be5d1
12 changed files with 553 additions and 475 deletions

View File

@@ -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

View File

@@ -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({

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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';

View File

@@ -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' });

View File

@@ -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

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -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 {