mirror of
https://github.com/Microsoft/vscode
synced 2024-10-06 03:17:00 +00:00
Pull history from shell history files into recent commands (#152936)
This commit is contained in:
parent
bcfdcf57e3
commit
4bb6ebfac6
|
@ -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 {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue