Pull history from shell history files into recent commands (#152936)

This commit is contained in:
Daniel Imms 2022-06-23 15:39:16 -07:00 committed by GitHub
parent bcfdcf57e3
commit 4bb6ebfac6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 602 additions and 70 deletions

View file

@ -51,7 +51,7 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura
import { ITerminalQuickPickItem } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { getIconId, getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon';
import { getCommandHistory } from 'vs/workbench/contrib/terminal/common/history';
import { clearShellFileHistory, getCommandHistory } from 'vs/workbench/contrib/terminal/common/history';
import { CATEGORIES } from 'vs/workbench/common/actions';
export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500';
@ -2149,6 +2149,7 @@ export function registerTerminalActions() {
}
run(accessor: ServicesAccessor) {
getCommandHistory(accessor).clear();
clearShellFileHistory();
}
});
registerAction2(class extends Action2 {

View file

@ -71,7 +71,7 @@ import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/xterm
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { IEnvironmentVariableCollection, IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { deserializeEnvironmentVariableCollections } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import { getCommandHistory, getDirectoryHistory } from 'vs/workbench/contrib/terminal/common/history';
import { getCommandHistory, getDirectoryHistory, getShellFileHistory } from 'vs/workbench/contrib/terminal/common/history';
import { DEFAULT_COMMANDS_TO_SKIP_SHELL, INavigationMode, ITerminalBackend, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings';
@ -892,14 +892,31 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
label,
buttons: [removeFromCommandHistoryButton]
});
commandMap.add(label);
}
}
if (previousSessionItems.length > 0) {
items.push(
{ type: 'separator', label: terminalStrings.previousSessionCategory },
...previousSessionItems
);
}
// Gather shell file history
const shellFileHistory = await this._instantiationService.invokeFunction(getShellFileHistory, this._shellType);
const dedupedShellFileItems: IQuickPickItem[] = [];
for (const label of shellFileHistory) {
if (!commandMap.has(label)) {
dedupedShellFileItems.unshift({ label });
}
}
if (dedupedShellFileItems.length > 0) {
items.push(
{ type: 'separator', label: nls.localize('shellFileHistoryCategory', '{0} history', this._shellType) },
...dedupedShellFileItems
);
}
} else {
placeholder = isMacintosh
? nls.localize('selectRecentDirectoryMac', 'Select a directory to go to (hold Option-key to edit the command)')
@ -938,6 +955,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
const outputProvider = this._instantiationService.createInstance(TerminalOutputProvider);
const quickPick = this._quickInputService.createQuickPick();
quickPick.items = items;
quickPick.sortByLabel = false;
quickPick.placeholder = placeholder;
return new Promise<void>(r => {
quickPick.onDidTriggerItemButton(async e => {

View file

@ -3,12 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { env } from 'vs/base/common/process';
import { Disposable } from 'vs/base/common/lifecycle';
import { LRUCache } from 'vs/base/common/map';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { TerminalSettingId, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { PosixShellType, TerminalSettingId, TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { URI } from 'vs/base/common/uri';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Schemas } from 'vs/base/common/network';
import { isWindows, OperatingSystem } from 'vs/base/common/platform';
import { posix, win32 } from 'vs/base/common/path';
/**
* Tracks a list of generic entries.
@ -61,6 +68,42 @@ export function getDirectoryHistory(accessor: ServicesAccessor): ITerminalPersis
return directoryHistory;
}
// Shell file history loads once per shell per window
const shellFileHistory: Map<TerminalShellType, string[] | null> = new Map();
export async function getShellFileHistory(accessor: ServicesAccessor, shellType: TerminalShellType): 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 PosixShellType.PowerShell:
case WindowsShellType.PowerShell:
result = await fetchPwshHistory(accessor);
break;
case PosixShellType.Zsh:
result = await fetchZshHistory(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>;
private _timestamp: number = 0;
@ -180,3 +223,172 @@ 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 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();
}
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 isRemote = !!remoteAgentService.getConnection()?.remoteAuthority;
const historyFileUri = URI.from({
scheme: isRemote ? Schemas.vscodeRemote : Schemas.file,
path: (isFileWindows ? win32.join : posix.join)(folderPrefix, filePath)
});
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

@ -3,12 +3,21 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual, strictEqual } from 'assert';
import { deepStrictEqual, fail, strictEqual } from 'assert';
import { VSBuffer } from 'vs/base/common/buffer';
import { Schemas } from 'vs/base/common/network';
import { join } from 'vs/base/common/path';
import { isWindows, OperatingSystem } from 'vs/base/common/platform';
import { env } from 'vs/base/common/process';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IFileService } from 'vs/platform/files/common/files';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITerminalPersistedHistory, TerminalPersistedHistory } from 'vs/workbench/contrib/terminal/common/history';
import { fetchBashHistory, fetchPwshHistory, fetchZshHistory, ITerminalPersistedHistory, TerminalPersistedHistory } from 'vs/workbench/contrib/terminal/common/history';
import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
function getConfig(limit: number) {
@ -23,79 +32,371 @@ function getConfig(limit: number) {
};
}
suite('TerminalPersistedHistory', () => {
let history: ITerminalPersistedHistory<number>;
let instantiationService: TestInstantiationService;
let storageService: TestStorageService;
let configurationService: TestConfigurationService;
const expectedCommands = [
'single line command',
'git commit -m "A wrapped line in pwsh history\n\nSome commit description\n\nFixes #xyz"',
'git status',
'two "\nline"'
];
setup(() => {
configurationService = new TestConfigurationService(getConfig(5));
storageService = new TestStorageService();
instantiationService = new TestInstantiationService();
instantiationService.set(IConfigurationService, configurationService);
instantiationService.set(IStorageService, storageService);
suite('Terminal history', () => {
suite('TerminalPersistedHistory', () => {
let history: ITerminalPersistedHistory<number>;
let instantiationService: TestInstantiationService;
let storageService: TestStorageService;
let configurationService: TestConfigurationService;
history = instantiationService.createInstance(TerminalPersistedHistory, 'test');
setup(() => {
configurationService = new TestConfigurationService(getConfig(5));
storageService = new TestStorageService();
instantiationService = new TestInstantiationService();
instantiationService.set(IConfigurationService, configurationService);
instantiationService.set(IStorageService, storageService);
history = instantiationService.createInstance(TerminalPersistedHistory, 'test');
});
test('should support adding items to the cache and respect LRU', () => {
history.add('foo', 1);
deepStrictEqual(Array.from(history.entries), [
['foo', 1]
]);
history.add('bar', 2);
deepStrictEqual(Array.from(history.entries), [
['foo', 1],
['bar', 2]
]);
history.add('foo', 1);
deepStrictEqual(Array.from(history.entries), [
['bar', 2],
['foo', 1]
]);
});
test('should support removing specific items', () => {
history.add('1', 1);
history.add('2', 2);
history.add('3', 3);
history.add('4', 4);
history.add('5', 5);
strictEqual(Array.from(history.entries).length, 5);
history.add('6', 6);
strictEqual(Array.from(history.entries).length, 5);
});
test('should limit the number of entries based on config', () => {
history.add('1', 1);
history.add('2', 2);
history.add('3', 3);
history.add('4', 4);
history.add('5', 5);
strictEqual(Array.from(history.entries).length, 5);
history.add('6', 6);
strictEqual(Array.from(history.entries).length, 5);
configurationService.setUserConfiguration('terminal', getConfig(2).terminal);
configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);
strictEqual(Array.from(history.entries).length, 2);
history.add('7', 7);
strictEqual(Array.from(history.entries).length, 2);
configurationService.setUserConfiguration('terminal', getConfig(3).terminal);
configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);
strictEqual(Array.from(history.entries).length, 2);
history.add('8', 8);
strictEqual(Array.from(history.entries).length, 3);
history.add('9', 9);
strictEqual(Array.from(history.entries).length, 3);
});
test('should reload from storage service after recreation', () => {
history.add('1', 1);
history.add('2', 2);
history.add('3', 3);
strictEqual(Array.from(history.entries).length, 3);
const history2 = instantiationService.createInstance(TerminalPersistedHistory, 'test');
strictEqual(Array.from(history2.entries).length, 3);
});
});
suite('fetchBashHistory', () => {
let fileScheme: string;
let filePath: string;
const fileContent: string = [
'single line command',
'git commit -m "A wrapped line in pwsh history',
'',
'Some commit description',
'',
'Fixes #xyz"',
'git status',
'two "',
'line"'
].join('\n');
test('should support adding items to the cache and respect LRU', () => {
history.add('foo', 1);
deepStrictEqual(Array.from(history.entries), [
['foo', 1]
]);
history.add('bar', 2);
deepStrictEqual(Array.from(history.entries), [
['foo', 1],
['bar', 2]
]);
history.add('foo', 1);
deepStrictEqual(Array.from(history.entries), [
['bar', 2],
['foo', 1]
]);
let instantiationService: TestInstantiationService;
let remoteConnection: Pick<IRemoteAgentConnection, 'remoteAuthority'> | null = null;
let remoteEnvironment: Pick<IRemoteAgentEnvironment, 'os'> | null = null;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IFileService, {
async readFile(resource: URI) {
const expected = URI.from({ scheme: fileScheme, path: filePath });
strictEqual(resource.scheme, expected.scheme);
strictEqual(resource.path, expected.path);
return { value: VSBuffer.fromString(fileContent) };
}
} as Pick<IFileService, 'readFile'>);
instantiationService.stub(IRemoteAgentService, {
async getEnvironment() { return remoteEnvironment; },
getConnection() { return remoteConnection; }
} as Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'>);
});
if (!isWindows) {
suite('local', async () => {
let originalEnvValues: { HOME: string | undefined };
setup(() => {
originalEnvValues = { HOME: env['HOME'] };
env['HOME'] = '/home/user';
remoteConnection = { remoteAuthority: 'some-remote' };
fileScheme = Schemas.vscodeRemote;
filePath = '/home/user/.bash_history';
});
teardown(() => {
if (originalEnvValues['HOME'] === undefined) {
delete env['HOME'];
} else {
env['HOME'] = originalEnvValues['HOME'];
}
});
test('current OS', async () => {
filePath = '/home/user/.bash_history';
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchBashHistory))!), expectedCommands);
});
});
}
suite('remote', () => {
let originalEnvValues: { HOME: string | undefined };
setup(() => {
originalEnvValues = { HOME: env['HOME'] };
env['HOME'] = '/home/user';
remoteConnection = { remoteAuthority: 'some-remote' };
fileScheme = Schemas.vscodeRemote;
filePath = '/home/user/.bash_history';
});
teardown(() => {
if (originalEnvValues['HOME'] === undefined) {
delete env['HOME'];
} else {
env['HOME'] = originalEnvValues['HOME'];
}
});
test('Windows', async () => {
remoteEnvironment = { os: OperatingSystem.Windows };
strictEqual(await instantiationService.invokeFunction(fetchBashHistory), undefined);
});
test('macOS', async () => {
remoteEnvironment = { os: OperatingSystem.Macintosh };
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchBashHistory))!), expectedCommands);
});
test('Linux', async () => {
remoteEnvironment = { os: OperatingSystem.Linux };
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchBashHistory))!), expectedCommands);
});
});
});
suite('fetchZshHistory', () => {
let fileScheme: string;
let filePath: string;
const fileContent: string = [
': 1655252330:0;single line command',
': 1655252330:0;git commit -m "A wrapped line in pwsh history\\',
'\\',
'Some commit description\\',
'\\',
'Fixes #xyz"',
': 1655252330:0;git status',
': 1655252330:0;two "\\',
'line"'
].join('\n');
test('should support removing specific items', () => {
history.add('1', 1);
history.add('2', 2);
history.add('3', 3);
history.add('4', 4);
history.add('5', 5);
strictEqual(Array.from(history.entries).length, 5);
history.add('6', 6);
strictEqual(Array.from(history.entries).length, 5);
let instantiationService: TestInstantiationService;
let remoteConnection: Pick<IRemoteAgentConnection, 'remoteAuthority'> | null = null;
let remoteEnvironment: Pick<IRemoteAgentEnvironment, 'os'> | null = null;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IFileService, {
async readFile(resource: URI) {
const expected = URI.from({ scheme: fileScheme, path: filePath });
strictEqual(resource.scheme, expected.scheme);
strictEqual(resource.path, expected.path);
return { value: VSBuffer.fromString(fileContent) };
}
} as Pick<IFileService, 'readFile'>);
instantiationService.stub(IRemoteAgentService, {
async getEnvironment() { return remoteEnvironment; },
getConnection() { return remoteConnection; }
} as Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'>);
});
if (!isWindows) {
suite('local', () => {
let originalEnvValues: { HOME: string | undefined };
setup(() => {
originalEnvValues = { HOME: env['HOME'] };
env['HOME'] = '/home/user';
remoteConnection = { remoteAuthority: 'some-remote' };
fileScheme = Schemas.vscodeRemote;
filePath = '/home/user/.bash_history';
});
teardown(() => {
if (originalEnvValues['HOME'] === undefined) {
delete env['HOME'];
} else {
env['HOME'] = originalEnvValues['HOME'];
}
});
test('current OS', async () => {
filePath = '/home/user/.zsh_history';
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchZshHistory))!), expectedCommands);
});
});
}
suite('remote', () => {
let originalEnvValues: { HOME: string | undefined };
setup(() => {
originalEnvValues = { HOME: env['HOME'] };
env['HOME'] = '/home/user';
remoteConnection = { remoteAuthority: 'some-remote' };
fileScheme = Schemas.vscodeRemote;
filePath = '/home/user/.zsh_history';
});
teardown(() => {
if (originalEnvValues['HOME'] === undefined) {
delete env['HOME'];
} else {
env['HOME'] = originalEnvValues['HOME'];
}
});
test('Windows', async () => {
remoteEnvironment = { os: OperatingSystem.Windows };
strictEqual(await instantiationService.invokeFunction(fetchZshHistory), undefined);
});
test('macOS', async () => {
remoteEnvironment = { os: OperatingSystem.Macintosh };
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchZshHistory))!), expectedCommands);
});
test('Linux', async () => {
remoteEnvironment = { os: OperatingSystem.Linux };
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchZshHistory))!), expectedCommands);
});
});
});
suite('fetchPwshHistory', () => {
let fileScheme: string;
let filePath: string;
const fileContent: string = [
'single line command',
'git commit -m "A wrapped line in pwsh history`',
'`',
'Some commit description`',
'`',
'Fixes #xyz"',
'git status',
'two "`',
'line"'
].join('\n');
test('should limit the number of entries based on config', () => {
history.add('1', 1);
history.add('2', 2);
history.add('3', 3);
history.add('4', 4);
history.add('5', 5);
strictEqual(Array.from(history.entries).length, 5);
history.add('6', 6);
strictEqual(Array.from(history.entries).length, 5);
configurationService.setUserConfiguration('terminal', getConfig(2).terminal);
configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);
strictEqual(Array.from(history.entries).length, 2);
history.add('7', 7);
strictEqual(Array.from(history.entries).length, 2);
configurationService.setUserConfiguration('terminal', getConfig(3).terminal);
configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);
strictEqual(Array.from(history.entries).length, 2);
history.add('8', 8);
strictEqual(Array.from(history.entries).length, 3);
history.add('9', 9);
strictEqual(Array.from(history.entries).length, 3);
});
let instantiationService: TestInstantiationService;
let remoteConnection: Pick<IRemoteAgentConnection, 'remoteAuthority'> | null = null;
let remoteEnvironment: Pick<IRemoteAgentEnvironment, 'os'> | null = null;
test('should reload from storage service after recreation', () => {
history.add('1', 1);
history.add('2', 2);
history.add('3', 3);
strictEqual(Array.from(history.entries).length, 3);
const history2 = instantiationService.createInstance(TerminalPersistedHistory, 'test');
strictEqual(Array.from(history2.entries).length, 3);
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IFileService, {
async readFile(resource: URI) {
const expected = URI.from({ scheme: fileScheme, path: filePath });
if (resource.scheme !== expected.scheme || resource.fsPath !== expected.fsPath) {
fail(`Unexpected file scheme/path ${resource.scheme} ${resource.fsPath}`);
}
return { value: VSBuffer.fromString(fileContent) };
}
} as Pick<IFileService, 'readFile'>);
instantiationService.stub(IRemoteAgentService, {
async getEnvironment() { return remoteEnvironment; },
getConnection() { return remoteConnection; }
} as Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'>);
});
suite('local', async () => {
let originalEnvValues: { HOME: string | undefined; APPDATA: string | undefined };
setup(() => {
originalEnvValues = { HOME: env['HOME'], APPDATA: env['APPDATA'] };
env['HOME'] = '/home/user';
env['APPDATA'] = 'C:\\AppData';
remoteConnection = { remoteAuthority: 'some-remote' };
fileScheme = Schemas.vscodeRemote;
filePath = '/home/user/.zsh_history';
originalEnvValues = { HOME: env['HOME'], APPDATA: env['APPDATA'] };
});
teardown(() => {
if (originalEnvValues['HOME'] === undefined) {
delete env['HOME'];
} else {
env['HOME'] = originalEnvValues['HOME'];
}
if (originalEnvValues['APPDATA'] === undefined) {
delete env['APPDATA'];
} else {
env['APPDATA'] = originalEnvValues['APPDATA'];
}
});
test('current OS', async () => {
if (isWindows) {
filePath = join(env['APPDATA']!, 'Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt');
} else {
filePath = join(env['HOME']!, '.local/share/powershell/PSReadline/ConsoleHost_history.txt');
}
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands);
});
});
suite('remote', () => {
let originalEnvValues: { HOME: string | undefined; APPDATA: string | undefined };
setup(() => {
remoteConnection = { remoteAuthority: 'some-remote' };
fileScheme = Schemas.vscodeRemote;
originalEnvValues = { HOME: env['HOME'], APPDATA: env['APPDATA'] };
});
teardown(() => {
if (originalEnvValues['HOME'] === undefined) {
delete env['HOME'];
} else {
env['HOME'] = originalEnvValues['HOME'];
}
if (originalEnvValues['APPDATA'] === undefined) {
delete env['APPDATA'];
} else {
env['APPDATA'] = originalEnvValues['APPDATA'];
}
});
test('Windows', async () => {
remoteEnvironment = { os: OperatingSystem.Windows };
env['APPDATA'] = 'C:\\AppData';
filePath = 'C:\\AppData\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt';
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands);
});
test('macOS', async () => {
remoteEnvironment = { os: OperatingSystem.Macintosh };
env['HOME'] = '/home/user';
filePath = '/home/user/.local/share/powershell/PSReadline/ConsoleHost_history.txt';
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands);
});
test('Linux', async () => {
remoteEnvironment = { os: OperatingSystem.Linux };
env['HOME'] = '/home/user';
filePath = '/home/user/.local/share/powershell/PSReadline/ConsoleHost_history.txt';
deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands);
});
});
});
});