mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-31 01:00:29 -04:00
Merge from vscode 2e5312cd61ff99c570299ecc122c52584265eda2
This commit is contained in:
committed by
Anthony Dresser
parent
3603f55d97
commit
7f1d8fc32f
@@ -50,7 +50,7 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon {
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
}
|
||||
if (this._terminal.buffer.cursorX >= MINIMUM_PROMPT_LENGTH) {
|
||||
if (this._terminal.buffer.active.cursorX >= MINIMUM_PROMPT_LENGTH) {
|
||||
this._terminal.registerMarker(0);
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,8 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon {
|
||||
}
|
||||
|
||||
let markerIndex;
|
||||
const currentLineY = Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.baseY);
|
||||
const viewportY = this._terminal.buffer.viewportY;
|
||||
const currentLineY = Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.active.baseY);
|
||||
const viewportY = this._terminal.buffer.active.viewportY;
|
||||
if (!retainSelection && currentLineY !== viewportY) {
|
||||
// The user has scrolled, find the line based on the current scroll position. This only
|
||||
// works when not retaining selection
|
||||
@@ -103,8 +103,8 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon {
|
||||
}
|
||||
|
||||
let markerIndex;
|
||||
const currentLineY = Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.baseY);
|
||||
const viewportY = this._terminal.buffer.viewportY;
|
||||
const currentLineY = Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.active.baseY);
|
||||
const viewportY = this._terminal.buffer.active.viewportY;
|
||||
if (!retainSelection && currentLineY !== viewportY) {
|
||||
// The user has scrolled, find the line based on the current scroll position. This only
|
||||
// works when not retaining selection
|
||||
@@ -212,7 +212,7 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon {
|
||||
private _getLine(xterm: Terminal, marker: IMarker | Boundary): number {
|
||||
// Use the _second last_ row as the last row is likely the prompt
|
||||
if (marker === Boundary.Bottom) {
|
||||
return xterm.buffer.baseY + xterm.rows - 1;
|
||||
return xterm.buffer.active.baseY + xterm.rows - 1;
|
||||
}
|
||||
|
||||
if (marker === Boundary.Top) {
|
||||
@@ -272,10 +272,10 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon {
|
||||
if (this._currentMarker === Boundary.Bottom) {
|
||||
return 0;
|
||||
} else if (this._currentMarker === Boundary.Top) {
|
||||
return 0 - (xterm.buffer.baseY + xterm.buffer.cursorY);
|
||||
return 0 - (xterm.buffer.active.baseY + xterm.buffer.active.cursorY);
|
||||
} else {
|
||||
let offset = this._getLine(xterm, this._currentMarker);
|
||||
offset -= xterm.buffer.baseY + xterm.buffer.cursorY;
|
||||
offset -= xterm.buffer.active.baseY + xterm.buffer.active.cursorY;
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon {
|
||||
}
|
||||
|
||||
// Target is row before the cursor
|
||||
const targetRow = Math.max(this._terminal.buffer.cursorY - 1, 0);
|
||||
const targetRow = Math.max(this._terminal.buffer.active.cursorY - 1, 0);
|
||||
|
||||
// Check bounds
|
||||
if (treeContainer.childElementCount < targetRow) {
|
||||
@@ -98,7 +98,7 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon {
|
||||
}
|
||||
|
||||
// Target is cursor row
|
||||
const targetRow = this._terminal.buffer.cursorY;
|
||||
const targetRow = this._terminal.buffer.active.cursorY;
|
||||
|
||||
// Check bounds
|
||||
if (treeContainer.childElementCount < targetRow) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IEnvironmentVariableInfo, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff, EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export class EnvironmentVariableInfoStale implements IEnvironmentVariableInfo {
|
||||
readonly requiresAction = true;
|
||||
|
||||
constructor(
|
||||
private readonly _diff: IMergedEnvironmentVariableCollectionDiff,
|
||||
private readonly _terminalId: number,
|
||||
@ITerminalService private readonly _terminalService: ITerminalService
|
||||
) {
|
||||
}
|
||||
|
||||
getInfo(): string {
|
||||
let info = localize('extensionEnvironmentContribution', "Extensions want to make the follow changes to the terminal's environment:");
|
||||
info += `\n\n${this._summarizeDiff()}`;
|
||||
return info;
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
getActions(): { label: string, iconClass?: string, run: () => void, commandId: string }[] {
|
||||
return [{
|
||||
label: localize('relaunchTerminalLabel', "Relaunch terminal"),
|
||||
run: () => this._terminalService.getInstanceFromId(this._terminalId)?.relaunch(),
|
||||
commandId: TERMINAL_COMMAND_ID.RELAUNCH
|
||||
}];
|
||||
}
|
||||
|
||||
private _summarizeDiff(): string {
|
||||
const summary: string[] = [];
|
||||
this._diff.added.forEach((mutators, variable) => {
|
||||
mutators.forEach(mutator => {
|
||||
summary.push(`- ${mutatorTypeLabel(mutator.type, mutator.value, variable)}`);
|
||||
});
|
||||
});
|
||||
this._diff.changed.forEach((mutators, variable) => {
|
||||
mutators.forEach(mutator => {
|
||||
summary.push(`- "${mutatorTypeLabel(mutator.type, mutator.value, variable)}"`);
|
||||
});
|
||||
});
|
||||
this._diff.removed.forEach((mutators, variable) => {
|
||||
mutators.forEach(mutator => {
|
||||
const removePrefixText = localize('removeEnvironmentVariableChange', "Remove the change {0}", mutatorTypeLabel(mutator.type, mutator.value, variable));
|
||||
summary.push(`- ${removePrefixText}`);
|
||||
});
|
||||
});
|
||||
return summary.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariableInfo {
|
||||
readonly requiresAction = false;
|
||||
|
||||
constructor(
|
||||
private _collection: IMergedEnvironmentVariableCollection
|
||||
) {
|
||||
}
|
||||
|
||||
getInfo(): string {
|
||||
const info: string[] = ['Extensions have made changes to this terminal\'s environment:', ''];
|
||||
this._collection.map.forEach((mutators, variable) => {
|
||||
mutators.forEach(mutator => {
|
||||
info.push(`- ${mutatorTypeLabel(mutator.type, mutator.value, variable)}`);
|
||||
});
|
||||
});
|
||||
return info.join('\n');
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
|
||||
function mutatorTypeLabel(type: EnvironmentVariableMutatorType, value: string, variable: string): string {
|
||||
switch (type) {
|
||||
case EnvironmentVariableMutatorType.Prepend: return localize('prependValueToEnvironmentVariableMarkdown', "Add `{0}` to the beginning of `{1}`", value, variable);
|
||||
case EnvironmentVariableMutatorType.Append: return localize('appendValueToEnvironmentVariableMarkdown', "Add `{0}` to the end of `{1}`", value, variable);
|
||||
default: return localize('replaceEnvironmentVariableWithValueMarkdown', "Replace `{1}`\'s value with `{0}`", value, variable);
|
||||
}
|
||||
}
|
||||
116
src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts
Normal file
116
src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IViewportRange, IBufferRange, ILink, ILinkDecorations } from 'xterm';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { convertBufferRangeToViewport } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export const OPEN_FILE_LABEL = localize('openFile', 'Open file in editor');
|
||||
export const FOLDER_IN_WORKSPACE_LABEL = localize('focusFolder', 'Focus folder in explorer');
|
||||
export const FOLDER_NOT_IN_WORKSPACE_LABEL = localize('openFolder', 'Open folder in new window');
|
||||
|
||||
export class TerminalLink extends DisposableStore implements ILink {
|
||||
decorations: ILinkDecorations;
|
||||
|
||||
constructor(
|
||||
public readonly range: IBufferRange,
|
||||
public readonly text: string,
|
||||
private readonly _viewportY: number,
|
||||
private readonly _activateCallback: (event: MouseEvent | undefined, uri: string) => void,
|
||||
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
|
||||
private readonly _isHighConfidenceLink: boolean,
|
||||
public readonly label: string | undefined,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this.decorations = {
|
||||
pointerCursor: false,
|
||||
underline: this._isHighConfidenceLink
|
||||
};
|
||||
}
|
||||
|
||||
activate(event: MouseEvent | undefined, text: string): void {
|
||||
this._activateCallback(event, text);
|
||||
}
|
||||
|
||||
hover(event: MouseEvent, text: string): void {
|
||||
// Listen for modifier before handing it off to the hover to handle so it gets disposed correctly
|
||||
this.add(dom.addDisposableListener(document, 'keydown', e => {
|
||||
if (this._isModifierDown(e)) {
|
||||
this._enableDecorations();
|
||||
}
|
||||
}));
|
||||
this.add(dom.addDisposableListener(document, 'keyup', e => {
|
||||
if (!this._isModifierDown(e)) {
|
||||
this._disableDecorations();
|
||||
}
|
||||
}));
|
||||
|
||||
const timeout = this._configurationService.getValue<number>('editor.hover.delay');
|
||||
const scheduler = new RunOnceScheduler(() => {
|
||||
this._tooltipCallback(
|
||||
this,
|
||||
convertBufferRangeToViewport(this.range, this._viewportY),
|
||||
this._isHighConfidenceLink ? () => this._enableDecorations() : undefined,
|
||||
this._isHighConfidenceLink ? () => this._disableDecorations() : undefined
|
||||
);
|
||||
this.dispose();
|
||||
}, timeout);
|
||||
this.add(scheduler);
|
||||
scheduler.schedule();
|
||||
|
||||
const origin = { x: event.pageX, y: event.pageY };
|
||||
this.add(dom.addDisposableListener(document, dom.EventType.MOUSE_MOVE, e => {
|
||||
// Update decorations
|
||||
if (this._isModifierDown(e)) {
|
||||
this._enableDecorations();
|
||||
} else {
|
||||
this._disableDecorations();
|
||||
}
|
||||
|
||||
// Reset the scheduler if the mouse moves too much
|
||||
if (Math.abs(e.pageX - origin.x) > window.devicePixelRatio * 2 || Math.abs(e.pageY - origin.y) > window.devicePixelRatio * 2) {
|
||||
origin.x = e.pageX;
|
||||
origin.y = e.pageY;
|
||||
scheduler.schedule();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
leave(): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private _enableDecorations(): void {
|
||||
if (!this.decorations.pointerCursor) {
|
||||
this.decorations.pointerCursor = true;
|
||||
}
|
||||
if (!this.decorations.underline) {
|
||||
this.decorations.underline = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _disableDecorations(): void {
|
||||
if (this.decorations.pointerCursor) {
|
||||
this.decorations.pointerCursor = false;
|
||||
}
|
||||
if (this.decorations.underline !== this._isHighConfidenceLink) {
|
||||
this.decorations.underline = this._isHighConfidenceLink;
|
||||
}
|
||||
}
|
||||
|
||||
private _isModifierDown(event: MouseEvent | KeyboardEvent): boolean {
|
||||
const multiCursorModifier = this._configurationService.getValue<'ctrlCmd' | 'alt'>('editor.multiCursorModifier');
|
||||
if (multiCursorModifier === 'ctrlCmd') {
|
||||
return !!event.altKey;
|
||||
}
|
||||
return isMacintosh ? event.metaKey : event.ctrlKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPosition } from 'xterm';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
|
||||
export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: number, range: IRange, startLine: number) {
|
||||
const bufferRange: IBufferRange = {
|
||||
start: {
|
||||
x: range.startColumn,
|
||||
y: range.startLineNumber + startLine
|
||||
},
|
||||
end: {
|
||||
x: range.endColumn - 1,
|
||||
y: range.endLineNumber + startLine
|
||||
}
|
||||
};
|
||||
|
||||
// Shift start range right for each wide character before the link
|
||||
let startOffset = 0;
|
||||
const startWrappedLineCount = Math.ceil(range.startColumn / bufferWidth);
|
||||
for (let y = 0; y < startWrappedLineCount; y++) {
|
||||
const lineLength = Math.min(bufferWidth, range.startColumn - y * bufferWidth);
|
||||
let lineOffset = 0;
|
||||
const line = lines[y];
|
||||
for (let x = 0; x < Math.min(bufferWidth, lineLength + lineOffset); x++) {
|
||||
const cell = line.getCell(x)!;
|
||||
const width = cell.getWidth();
|
||||
if (width === 2) {
|
||||
lineOffset++;
|
||||
}
|
||||
const char = cell.getChars();
|
||||
if (char.length > 1) {
|
||||
lineOffset -= char.length - 1;
|
||||
}
|
||||
}
|
||||
startOffset += lineOffset;
|
||||
}
|
||||
|
||||
// Shift end range right for each wide character inside the link
|
||||
let endOffset = 0;
|
||||
const endWrappedLineCount = Math.ceil(range.endColumn / bufferWidth);
|
||||
for (let y = Math.max(0, startWrappedLineCount - 1); y < endWrappedLineCount; y++) {
|
||||
const start = (y === startWrappedLineCount - 1 ? (range.startColumn + startOffset) % bufferWidth : 0);
|
||||
const lineLength = Math.min(bufferWidth, range.endColumn + startOffset - y * bufferWidth);
|
||||
const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0);
|
||||
let lineOffset = 0;
|
||||
const line = lines[y];
|
||||
for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) {
|
||||
const cell = line.getCell(x)!;
|
||||
const width = cell.getWidth();
|
||||
// Offset for 0 cells following wide characters
|
||||
if (width === 2) {
|
||||
lineOffset++;
|
||||
}
|
||||
// Offset for early wrapping when the last cell in row is a wide character
|
||||
if (x === bufferWidth - 1 && cell.getChars() === '') {
|
||||
lineOffset++;
|
||||
}
|
||||
}
|
||||
endOffset += lineOffset;
|
||||
}
|
||||
|
||||
// Apply the width character offsets to the result
|
||||
bufferRange.start.x += startOffset;
|
||||
bufferRange.end.x += startOffset + endOffset;
|
||||
|
||||
// Convert back to wrapped lines
|
||||
while (bufferRange.start.x > bufferWidth) {
|
||||
bufferRange.start.x -= bufferWidth;
|
||||
bufferRange.start.y++;
|
||||
}
|
||||
while (bufferRange.end.x > bufferWidth) {
|
||||
bufferRange.end.x -= bufferWidth;
|
||||
bufferRange.end.y++;
|
||||
}
|
||||
|
||||
return bufferRange;
|
||||
}
|
||||
|
||||
export function convertBufferRangeToViewport(bufferRange: IBufferRange, viewportY: number): IViewportRange {
|
||||
return {
|
||||
start: {
|
||||
x: bufferRange.start.x - 1,
|
||||
y: bufferRange.start.y - viewportY - 1
|
||||
},
|
||||
end: {
|
||||
x: bufferRange.end.x - 1,
|
||||
y: bufferRange.end.y - viewportY - 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number): string {
|
||||
let line = '';
|
||||
for (let i = lineStart; i <= lineEnd; i++) {
|
||||
line += buffer.getLine(i)?.translateToString(true);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
export function positionIsInRange(position: IBufferCellPosition, range: IBufferRange): boolean {
|
||||
if (position.y < range.start.y || position.y > range.end.y) {
|
||||
return false;
|
||||
}
|
||||
if (position.y === range.start.y && position.x < range.start.x) {
|
||||
return false;
|
||||
}
|
||||
if (position.y === range.end.y && position.x > range.end.x) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -5,22 +5,29 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { DisposableStore, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { TerminalWidgetManager, WidgetVerticalAlignment } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
|
||||
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITerminalProcessManager, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ITerminalProcessManager, ITerminalConfigHelper, ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Terminal, ILinkMatcherOptions, IViewportRange } from 'xterm';
|
||||
import { Terminal, ILinkMatcherOptions, IViewportRange, ITerminalAddon } from 'xterm';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { posix, win32 } from 'vs/base/common/path';
|
||||
import { ITerminalInstanceService, ITerminalBeforeHandleLinkEvent, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { OperatingSystem, isMacintosh } from 'vs/base/common/platform';
|
||||
import { OperatingSystem, isMacintosh, OS } from 'vs/base/common/platform';
|
||||
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider';
|
||||
import { TerminalValidatedLocalLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
|
||||
import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
|
||||
import { TerminalHover, ILinkHoverTargetOptions } from 'vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget';
|
||||
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
|
||||
const pathPrefix = '(\\.\\.?|\\~)';
|
||||
const pathSeparatorClause = '\\/';
|
||||
@@ -30,11 +37,11 @@ const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]+\'":;\\\\]';
|
||||
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */
|
||||
const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)';
|
||||
|
||||
const winDrivePrefix = '[a-zA-Z]:';
|
||||
const winDrivePrefix = '(?:\\\\\\\\\\?\\\\)?[a-zA-Z]:';
|
||||
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)';
|
||||
const winPathSeparatorClause = '(\\\\|\\/)';
|
||||
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]';
|
||||
/** A regex that matches paths in the form c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
|
||||
/** A regex that matches paths in the form \\?\c:\foo c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
|
||||
const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
|
||||
|
||||
/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160,
|
||||
@@ -60,7 +67,7 @@ const CUSTOM_LINK_PRIORITY = -1;
|
||||
/** Lowest */
|
||||
const LOCAL_LINK_PRIORITY = -2;
|
||||
|
||||
export type XtermLinkMatcherHandler = (event: MouseEvent, link: string) => Promise<void>;
|
||||
export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => Promise<void>;
|
||||
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
interface IPath {
|
||||
@@ -68,17 +75,21 @@ interface IPath {
|
||||
normalize(path: string): string;
|
||||
}
|
||||
|
||||
export class TerminalLinkHandler extends DisposableStore {
|
||||
/**
|
||||
* An object responsible for managing registration of link matchers and link providers.
|
||||
*/
|
||||
export class TerminalLinkManager extends DisposableStore {
|
||||
private _widgetManager: TerminalWidgetManager | undefined;
|
||||
private _processCwd: string | undefined;
|
||||
private _gitDiffPreImagePattern: RegExp;
|
||||
private _gitDiffPostImagePattern: RegExp;
|
||||
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void;
|
||||
private readonly _leaveCallback: () => void;
|
||||
private _linkMatchers: number[] = [];
|
||||
private _webLinksAddon: ITerminalAddon | undefined;
|
||||
private _linkProviders: IDisposable[] = [];
|
||||
private _hasBeforeHandleLinkListeners = false;
|
||||
|
||||
protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD;
|
||||
public static readonly LINK_INTERCEPT_THRESHOLD = TerminalLinkHandler._LINK_INTERCEPT_THRESHOLD;
|
||||
public static readonly LINK_INTERCEPT_THRESHOLD = TerminalLinkManager._LINK_INTERCEPT_THRESHOLD;
|
||||
|
||||
private readonly _onBeforeHandleLink = this.add(new Emitter<ITerminalBeforeHandleLinkEvent>({
|
||||
onFirstListenerAdd: () => this._hasBeforeHandleLinkListeners = true,
|
||||
@@ -93,14 +104,15 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
|
||||
constructor(
|
||||
private _xterm: Terminal,
|
||||
private readonly _processManager: ITerminalProcessManager | undefined,
|
||||
private readonly _processManager: ITerminalProcessManager,
|
||||
private readonly _configHelper: ITerminalConfigHelper,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -109,50 +121,85 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
// Matches '+++ b/src/file1', capturing 'src/file1' in group 1
|
||||
this._gitDiffPostImagePattern = /^\+\+\+ b\/(\S*)/;
|
||||
|
||||
this._tooltipCallback = (e: MouseEvent, uri: string, location: IViewportRange) => {
|
||||
if (!this._widgetManager) {
|
||||
return;
|
||||
}
|
||||
if (this._configHelper.config.experimentalLinkProvider) {
|
||||
this.registerLinkProvider();
|
||||
} else {
|
||||
this._registerLinkMatchers();
|
||||
}
|
||||
|
||||
// Get the row bottom up
|
||||
let offsetRow = this._xterm.rows - location.start.y;
|
||||
let verticalAlignment = WidgetVerticalAlignment.Bottom;
|
||||
|
||||
// Show the tooltip on the top of the next row to avoid obscuring the first row
|
||||
if (location.start.y <= 0) {
|
||||
offsetRow = this._xterm.rows - 1;
|
||||
verticalAlignment = WidgetVerticalAlignment.Top;
|
||||
// The start of the wrapped line is above the viewport, move to start of the line
|
||||
if (location.start.y < 0) {
|
||||
location.start.x = 0;
|
||||
this._configurationService?.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('terminal.integrated.experimentalLinkProvider')) {
|
||||
if (this._configHelper.config.experimentalLinkProvider) {
|
||||
this._deregisterLinkMatchers();
|
||||
this.registerLinkProvider();
|
||||
} else {
|
||||
dispose(this._linkProviders);
|
||||
this._linkProviders.length = 0;
|
||||
this._registerLinkMatchers();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this._configHelper.config.rendererType === 'dom') {
|
||||
const font = this._configHelper.getFont();
|
||||
const charWidth = font.charWidth;
|
||||
const charHeight = font.charHeight;
|
||||
private _tooltipCallback(linkText: string, viewportRange: IViewportRange, linkHandler: (url: string) => void) {
|
||||
if (!this._widgetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftPosition = location.start.x * (charWidth! + (font.letterSpacing / window.devicePixelRatio));
|
||||
const bottomPosition = offsetRow * (Math.ceil(charHeight! * window.devicePixelRatio) * font.lineHeight) / window.devicePixelRatio;
|
||||
|
||||
this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(uri), verticalAlignment);
|
||||
} else {
|
||||
const target = (e.target as HTMLElement);
|
||||
const colWidth = target.offsetWidth / this._xterm.cols;
|
||||
const rowHeight = target.offsetHeight / this._xterm.rows;
|
||||
|
||||
const leftPosition = location.start.x * colWidth;
|
||||
const bottomPosition = offsetRow * rowHeight;
|
||||
this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(uri), verticalAlignment);
|
||||
}
|
||||
const core = (this._xterm as any)._core as XTermCore;
|
||||
const cellDimensions = {
|
||||
width: core._renderService.dimensions.actualCellWidth,
|
||||
height: core._renderService.dimensions.actualCellHeight
|
||||
};
|
||||
this._leaveCallback = () => {
|
||||
if (this._widgetManager) {
|
||||
this._widgetManager.closeMessage();
|
||||
}
|
||||
const terminalDimensions = {
|
||||
width: this._xterm.cols,
|
||||
height: this._xterm.rows
|
||||
};
|
||||
|
||||
this._showHover({
|
||||
viewportRange,
|
||||
cellDimensions,
|
||||
terminalDimensions
|
||||
}, this._getLinkHoverString(linkText, undefined), linkHandler);
|
||||
}
|
||||
|
||||
private _tooltipCallback2(link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) {
|
||||
if (!this._widgetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const core = (this._xterm as any)._core as XTermCore;
|
||||
const cellDimensions = {
|
||||
width: core._renderService.dimensions.actualCellWidth,
|
||||
height: core._renderService.dimensions.actualCellHeight
|
||||
};
|
||||
const terminalDimensions = {
|
||||
width: this._xterm.cols,
|
||||
height: this._xterm.rows
|
||||
};
|
||||
|
||||
// Don't pass the mouse event as this avoids the modifier check
|
||||
this._showHover({
|
||||
viewportRange,
|
||||
cellDimensions,
|
||||
terminalDimensions,
|
||||
modifierDownCallback,
|
||||
modifierUpCallback
|
||||
}, this._getLinkHoverString(link.text, link.label), (text) => link.activate(undefined, text));
|
||||
}
|
||||
|
||||
private _showHover(
|
||||
targetOptions: ILinkHoverTargetOptions,
|
||||
text: IMarkdownString,
|
||||
linkHandler: (url: string) => void
|
||||
) {
|
||||
if (this._widgetManager) {
|
||||
const widget = this._instantiationService.createInstance(TerminalHover, targetOptions, text, linkHandler);
|
||||
this._widgetManager.attachWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
private _registerLinkMatchers() {
|
||||
this.registerWebLinkHandler();
|
||||
if (this._processManager) {
|
||||
if (this._configHelper.config.enableFileLinks) {
|
||||
@@ -162,6 +209,14 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
}
|
||||
}
|
||||
|
||||
private _deregisterLinkMatchers() {
|
||||
this._webLinksAddon?.dispose();
|
||||
|
||||
this._linkMatchers.forEach(matcherId => {
|
||||
this._xterm.deregisterLinkMatcher(matcherId);
|
||||
});
|
||||
}
|
||||
|
||||
public setWidgetManager(widgetManager: TerminalWidgetManager): void {
|
||||
this._widgetManager = widgetManager;
|
||||
}
|
||||
@@ -170,11 +225,13 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
this._processCwd = processCwd;
|
||||
}
|
||||
|
||||
public registerCustomLinkHandler(regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number {
|
||||
public registerCustomLinkHandler(regex: RegExp, handler: (event: MouseEvent | undefined, uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number {
|
||||
const tooltipCallback = (_: MouseEvent, linkText: string, location: IViewportRange) => {
|
||||
this._tooltipCallback(linkText, location, text => handler(undefined, text));
|
||||
};
|
||||
const options: ILinkMatcherOptions = {
|
||||
matchIndex,
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: this._leaveCallback,
|
||||
tooltipCallback,
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
|
||||
priority: CUSTOM_LINK_PRIORITY
|
||||
};
|
||||
@@ -189,84 +246,116 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
if (!this._xterm) {
|
||||
return;
|
||||
}
|
||||
const wrappedHandler = this._wrapLinkHandler(uri => {
|
||||
this._handleHypertextLink(uri);
|
||||
});
|
||||
this._xterm.loadAddon(new WebLinksAddon(wrappedHandler, {
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback),
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: this._leaveCallback,
|
||||
const wrappedHandler = this._wrapLinkHandler((_, link) => this._handleHypertextLink(link));
|
||||
const tooltipCallback = (_: MouseEvent, linkText: string, location: IViewportRange) => {
|
||||
this._tooltipCallback(linkText, location, this._handleHypertextLink.bind(this));
|
||||
};
|
||||
this._webLinksAddon = new WebLinksAddon(wrappedHandler, {
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(callback),
|
||||
tooltipCallback,
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
|
||||
}));
|
||||
});
|
||||
this._xterm.loadAddon(this._webLinksAddon);
|
||||
});
|
||||
}
|
||||
|
||||
public registerLocalLinkHandler(): void {
|
||||
const wrappedHandler = this._wrapLinkHandler(url => {
|
||||
this._handleLocalLink(url);
|
||||
});
|
||||
this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
|
||||
const wrappedHandler = this._wrapLinkHandler((_, url) => this._handleLocalLink(url));
|
||||
const tooltipCallback = (event: MouseEvent, linkText: string, location: IViewportRange) => {
|
||||
this._tooltipCallback(linkText, location, this._handleLocalLink.bind(this));
|
||||
};
|
||||
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: this._leaveCallback,
|
||||
tooltipCallback,
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
|
||||
priority: LOCAL_LINK_PRIORITY
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public registerGitDiffLinkHandlers(): void {
|
||||
const wrappedHandler = this._wrapLinkHandler(url => {
|
||||
const wrappedHandler = this._wrapLinkHandler((_, url) => {
|
||||
this._handleLocalLink(url);
|
||||
});
|
||||
const tooltipCallback = (event: MouseEvent, linkText: string, location: IViewportRange) => {
|
||||
this._tooltipCallback(linkText, location, this._handleLocalLink.bind(this));
|
||||
};
|
||||
const options = {
|
||||
matchIndex: 1,
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: this._leaveCallback,
|
||||
tooltipCallback,
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
|
||||
priority: LOCAL_LINK_PRIORITY
|
||||
};
|
||||
this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options);
|
||||
this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options);
|
||||
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options));
|
||||
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options));
|
||||
}
|
||||
|
||||
protected _wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
|
||||
return async (event: MouseEvent, link: string) => {
|
||||
public registerLinkProvider(): void {
|
||||
// Protocol links
|
||||
const wrappedActivateCallback = this._wrapLinkHandler((_, link) => this._handleProtocolLink(link));
|
||||
const protocolProvider = this._instantiationService.createInstance(TerminalProtocolLinkProvider, this._xterm, wrappedActivateCallback, this._tooltipCallback2.bind(this));
|
||||
this._linkProviders.push(this._xterm.registerLinkProvider(protocolProvider));
|
||||
|
||||
// Validated local links
|
||||
if (this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).enableFileLinks) {
|
||||
const wrappedTextLinkActivateCallback = this._wrapLinkHandler((_, link) => this._handleLocalLink(link));
|
||||
const validatedProvider = this._instantiationService.createInstance(TerminalValidatedLocalLinkProvider,
|
||||
this._xterm,
|
||||
this._processManager.os || OS,
|
||||
wrappedTextLinkActivateCallback,
|
||||
this._wrapLinkHandler.bind(this),
|
||||
this._tooltipCallback2.bind(this),
|
||||
async (link, cb) => cb(await this._resolvePath(link)));
|
||||
this._linkProviders.push(this._xterm.registerLinkProvider(validatedProvider));
|
||||
}
|
||||
|
||||
// Word links
|
||||
const wordProvider = this._instantiationService.createInstance(TerminalWordLinkProvider, this._xterm, this._wrapLinkHandler.bind(this), this._tooltipCallback2.bind(this));
|
||||
this._linkProviders.push(this._xterm.registerLinkProvider(wordProvider));
|
||||
}
|
||||
|
||||
protected _wrapLinkHandler(handler: (event: MouseEvent | undefined, link: string) => void): XtermLinkMatcherHandler {
|
||||
return async (event: MouseEvent | undefined, link: string) => {
|
||||
// Prevent default electron link handling so Alt+Click mode works normally
|
||||
event.preventDefault();
|
||||
event?.preventDefault();
|
||||
|
||||
// Require correct modifier on click
|
||||
if (!this._isLinkActivationModifierDown(event)) {
|
||||
if (event && !this._isLinkActivationModifierDown(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow the link to be intercepted if there are listeners
|
||||
if (this._hasBeforeHandleLinkListeners) {
|
||||
const wasHandled = await new Promise<boolean>(r => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
canceled = true;
|
||||
this._logService.error(`An extension intecepted a terminal link but it timed out after ${TerminalLinkHandler.LINK_INTERCEPT_THRESHOLD / 1000} seconds`);
|
||||
r(false);
|
||||
}, TerminalLinkHandler.LINK_INTERCEPT_THRESHOLD);
|
||||
let canceled = false;
|
||||
const resolve = (handled: boolean) => {
|
||||
if (!canceled) {
|
||||
clearTimeout(timeoutId);
|
||||
r(handled);
|
||||
}
|
||||
};
|
||||
this._onBeforeHandleLink.fire({ link, resolve });
|
||||
});
|
||||
const wasHandled = await this._triggerBeforeHandleLinkListeners(link);
|
||||
if (!wasHandled) {
|
||||
handler(link);
|
||||
handler(event, link);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Just call the handler if there is no before listener
|
||||
handler(link);
|
||||
handler(event, link);
|
||||
};
|
||||
}
|
||||
|
||||
private async _triggerBeforeHandleLinkListeners(link: string): Promise<boolean> {
|
||||
return new Promise<boolean>(r => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
canceled = true;
|
||||
this._logService.error(`An extension intecepted a terminal link but it timed out after ${TerminalLinkManager.LINK_INTERCEPT_THRESHOLD / 1000} seconds`);
|
||||
r(false);
|
||||
}, TerminalLinkManager.LINK_INTERCEPT_THRESHOLD);
|
||||
let canceled = false;
|
||||
const resolve = (handled: boolean) => {
|
||||
if (!canceled) {
|
||||
clearTimeout(timeoutId);
|
||||
r(handled);
|
||||
}
|
||||
};
|
||||
this._onBeforeHandleLink.fire({ link, resolve });
|
||||
});
|
||||
}
|
||||
|
||||
protected get _localLinkRegex(): RegExp {
|
||||
if (!this._processManager) {
|
||||
throw new Error('Process manager is required');
|
||||
@@ -285,6 +374,7 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
}
|
||||
|
||||
private async _handleLocalLink(link: string): Promise<void> {
|
||||
// TODO: This gets resolved again but doesn't need to as it's already validated
|
||||
const resolvedLink = await this._resolvePath(link);
|
||||
if (!resolvedLink) {
|
||||
return;
|
||||
@@ -294,14 +384,14 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
startLineNumber: lineColumnInfo.lineNumber,
|
||||
startColumn: lineColumnInfo.columnNumber
|
||||
};
|
||||
await this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } });
|
||||
await this._editorService.openEditor({ resource: resolvedLink.uri, options: { pinned: true, selection } });
|
||||
}
|
||||
|
||||
private _validateLocalLink(link: string, callback: (isValid: boolean) => void): void {
|
||||
this._resolvePath(link).then(resolvedLink => callback(!!resolvedLink));
|
||||
}
|
||||
|
||||
private _validateWebLink(link: string, callback: (isValid: boolean) => void): void {
|
||||
private _validateWebLink(callback: (isValid: boolean) => void): void {
|
||||
callback(true);
|
||||
}
|
||||
|
||||
@@ -309,6 +399,19 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) });
|
||||
}
|
||||
|
||||
private async _handleProtocolLink(link: string): Promise<void> {
|
||||
// Check if it's a file:/// link, hand off to local link handler so to open an editor and
|
||||
// respect line/col attachment
|
||||
const uri = URI.parse(link);
|
||||
if (uri.scheme === 'file') {
|
||||
this._handleLocalLink(uri.fsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open as a web link if it's not a file
|
||||
this._handleHypertextLink(link);
|
||||
}
|
||||
|
||||
protected _isLinkActivationModifierDown(event: MouseEvent): boolean {
|
||||
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
|
||||
if (editorConf.multiCursorModifier === 'ctrlCmd') {
|
||||
@@ -317,29 +420,25 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
return isMacintosh ? event.metaKey : event.ctrlKey;
|
||||
}
|
||||
|
||||
private _getLinkHoverString(uri: string): IMarkdownString {
|
||||
private _getLinkHoverString(uri: string, label: string | undefined): IMarkdownString {
|
||||
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
|
||||
|
||||
let label = '';
|
||||
let clickLabel = '';
|
||||
if (editorConf.multiCursorModifier === 'ctrlCmd') {
|
||||
if (isMacintosh) {
|
||||
label = nls.localize('terminalLinkHandler.followLinkAlt.mac', "Option + click");
|
||||
clickLabel = nls.localize('terminalLinkHandler.followLinkAlt.mac', "option + click");
|
||||
} else {
|
||||
label = nls.localize('terminalLinkHandler.followLinkAlt', "Alt + click");
|
||||
clickLabel = nls.localize('terminalLinkHandler.followLinkAlt', "alt + click");
|
||||
}
|
||||
} else {
|
||||
if (isMacintosh) {
|
||||
label = nls.localize('terminalLinkHandler.followLinkCmd', "Cmd + click");
|
||||
clickLabel = nls.localize('terminalLinkHandler.followLinkCmd', "cmd + click");
|
||||
} else {
|
||||
label = nls.localize('terminalLinkHandler.followLinkCtrl', "Ctrl + click");
|
||||
clickLabel = nls.localize('terminalLinkHandler.followLinkCtrl', "ctrl + click");
|
||||
}
|
||||
}
|
||||
|
||||
const message: IMarkdownString = new MarkdownString(`[Follow Link](${uri}) (${label})`, true);
|
||||
message.uris = {
|
||||
[uri]: URI.parse(uri).toJSON()
|
||||
};
|
||||
return message;
|
||||
return new MarkdownString(`[${label || nls.localize('followLink', "Follow Link")}](${uri}) (${clickLabel})`, true);
|
||||
}
|
||||
|
||||
private get osPath(): IPath {
|
||||
@@ -371,6 +470,10 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
return null;
|
||||
}
|
||||
link = this.osPath.join(this._processCwd, link);
|
||||
} else {
|
||||
// Remove \\?\ from paths so that they share the same underlying
|
||||
// uri and don't open multiple tabs for the same file
|
||||
link = link.replace(/^\\\\\?\\/, '');
|
||||
}
|
||||
} else {
|
||||
if (!this._processCwd) {
|
||||
@@ -385,7 +488,7 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
return link;
|
||||
}
|
||||
|
||||
private async _resolvePath(link: string): Promise<URI | undefined> {
|
||||
private async _resolvePath(link: string): Promise<{ uri: URI, isDirectory: boolean } | undefined> {
|
||||
if (!this._processManager) {
|
||||
throw new Error('Process manager is required');
|
||||
}
|
||||
@@ -414,10 +517,7 @@ export class TerminalLinkHandler extends DisposableStore {
|
||||
|
||||
try {
|
||||
const stat = await this._fileService.resolve(uri);
|
||||
if (stat.isDirectory) {
|
||||
return undefined;
|
||||
}
|
||||
return uri;
|
||||
return { uri, isDirectory: stat.isDirectory };
|
||||
}
|
||||
catch (e) {
|
||||
// Does not exist
|
||||
@@ -0,0 +1,80 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Terminal, IViewportRange, ILinkProvider, IBufferCellPosition, ILink, IBufferLine } from 'xterm';
|
||||
import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer';
|
||||
import { getXtermLineContent, convertLinkRangeToBuffer, positionIsInRange } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { TerminalLink, OPEN_FILE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export class TerminalProtocolLinkProvider implements ILinkProvider {
|
||||
private _linkComputerTarget: ILinkComputerTarget | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _xterm: Terminal,
|
||||
private readonly _activateCallback: (event: MouseEvent | undefined, uri: string) => void,
|
||||
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService
|
||||
) {
|
||||
}
|
||||
|
||||
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
|
||||
let startLine = position.y - 1;
|
||||
let endLine = startLine;
|
||||
|
||||
const lines: IBufferLine[] = [
|
||||
this._xterm.buffer.active.getLine(startLine)!
|
||||
];
|
||||
|
||||
while (this._xterm.buffer.active.getLine(startLine)?.isWrapped) {
|
||||
lines.unshift(this._xterm.buffer.active.getLine(startLine - 1)!);
|
||||
startLine--;
|
||||
}
|
||||
|
||||
while (this._xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
|
||||
lines.push(this._xterm.buffer.active.getLine(endLine + 1)!);
|
||||
endLine++;
|
||||
}
|
||||
|
||||
this._linkComputerTarget = new TerminalLinkAdapter(this._xterm, startLine, endLine);
|
||||
const links = LinkComputer.computeLinks(this._linkComputerTarget);
|
||||
|
||||
let found = false;
|
||||
links.forEach(link => {
|
||||
const range = convertLinkRangeToBuffer(lines, this._xterm.cols, link.range, startLine);
|
||||
|
||||
// Check if the link if within the mouse position
|
||||
if (positionIsInRange(position, range)) {
|
||||
found = true;
|
||||
const uri = link.url
|
||||
? (typeof link.url === 'string' ? URI.parse(link.url) : link.url)
|
||||
: undefined;
|
||||
const label = (uri?.scheme === 'file') ? OPEN_FILE_LABEL : undefined;
|
||||
callback(this._instantiationService.createInstance(TerminalLink, range, link.url?.toString() || '', this._xterm.buffer.active.viewportY, this._activateCallback, this._tooltipCallback, true, label));
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
callback(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalLinkAdapter implements ILinkComputerTarget {
|
||||
constructor(
|
||||
private _xterm: Terminal,
|
||||
private _lineStart: number,
|
||||
private _lineEnd: number
|
||||
) { }
|
||||
|
||||
getLineCount(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getLineContent(): string {
|
||||
return getXtermLineContent(this._xterm.buffer.active, this._lineStart, this._lineEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Terminal, ILinkProvider, IViewportRange, IBufferCellPosition, ILink, IBufferLine } from 'xterm';
|
||||
import { getXtermLineContent, convertLinkRangeToBuffer, positionIsInRange } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TerminalLink, OPEN_FILE_LABEL, FOLDER_IN_WORKSPACE_LABEL, FOLDER_NOT_IN_WORKSPACE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
|
||||
const pathPrefix = '(\\.\\.?|\\~)';
|
||||
const pathSeparatorClause = '\\/';
|
||||
// '":; are allowed in paths but they are often separators so ignore them
|
||||
// Also disallow \\ to prevent a catastropic backtracking case #24798
|
||||
const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]+\'":;\\\\]';
|
||||
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */
|
||||
const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)';
|
||||
|
||||
const winDrivePrefix = '(?:\\\\\\\\\\?\\\\)?[a-zA-Z]:';
|
||||
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)';
|
||||
const winPathSeparatorClause = '(\\\\|\\/)';
|
||||
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]';
|
||||
/** A regex that matches paths in the form \\?\c:\foo c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
|
||||
const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
|
||||
|
||||
/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160,
|
||||
replacing space with nonBreakningSpace or space ASCII code - 32. */
|
||||
const lineAndColumnClause = [
|
||||
'((\\S*)", line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468]
|
||||
'((\\S*)",((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205]
|
||||
'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13
|
||||
'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13
|
||||
'(([^\\s\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18), (file path)(45, 18), (file path) (45, 18), also with []
|
||||
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
|
||||
].join('|').replace(/ /g, `[${'\u00A0'} ]`);
|
||||
|
||||
export class TerminalValidatedLocalLinkProvider implements ILinkProvider {
|
||||
constructor(
|
||||
private readonly _xterm: Terminal,
|
||||
private readonly _processOperatingSystem: OperatingSystem,
|
||||
private readonly _activateFileCallback: (event: MouseEvent | undefined, link: string) => void,
|
||||
private readonly _wrapLinkHandler: (handler: (event: MouseEvent | undefined, link: string) => void) => XtermLinkMatcherHandler,
|
||||
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
|
||||
private readonly _validationCallback: (link: string, callback: (result: { uri: URI, isDirectory: boolean } | undefined) => void) => void,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IHostService private readonly _hostService: IHostService
|
||||
) {
|
||||
}
|
||||
|
||||
async provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void) {
|
||||
let startLine = position.y - 1;
|
||||
let endLine = startLine;
|
||||
|
||||
const lines: IBufferLine[] = [
|
||||
this._xterm.buffer.active.getLine(startLine)!
|
||||
];
|
||||
|
||||
while (this._xterm.buffer.active.getLine(startLine)?.isWrapped) {
|
||||
lines.unshift(this._xterm.buffer.active.getLine(startLine - 1)!);
|
||||
startLine--;
|
||||
}
|
||||
|
||||
while (this._xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
|
||||
lines.push(this._xterm.buffer.active.getLine(endLine + 1)!);
|
||||
endLine++;
|
||||
}
|
||||
|
||||
const text = getXtermLineContent(this._xterm.buffer.active, startLine, endLine);
|
||||
|
||||
// clone regex to do a global search on text
|
||||
const rex = new RegExp(this._localLinkRegex, 'g');
|
||||
let match;
|
||||
let stringIndex = -1;
|
||||
while ((match = rex.exec(text)) !== null) {
|
||||
// const link = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
let link = match[0];
|
||||
if (!link) {
|
||||
// something matched but does not comply with the given matchIndex
|
||||
// since this is most likely a bug the regex itself we simply do nothing here
|
||||
// this._logService.debug('match found without corresponding matchIndex', match, matcher);
|
||||
break;
|
||||
}
|
||||
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
// therefore we cannot use match.index directly, instead we search the position
|
||||
// of the match group in text again
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(link, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + link.length;
|
||||
if (stringIndex < 0) {
|
||||
// invalid stringIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
// Adjust the link range to exclude a/ and b/ if it looks like a git diff
|
||||
if (
|
||||
// --- a/foo/bar
|
||||
// +++ b/foo/bar
|
||||
((text.startsWith('--- a/') || text.startsWith('+++ b/')) && stringIndex === 4) ||
|
||||
// diff --git a/foo/bar b/foo/bar
|
||||
(text.startsWith('diff --git') && (link.startsWith('a/') || link.startsWith('b/')))
|
||||
) {
|
||||
link = link.substring(2);
|
||||
stringIndex += 2;
|
||||
}
|
||||
|
||||
// Convert the link text's string index into a wrapped buffer range
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, this._xterm.cols, {
|
||||
startColumn: stringIndex + 1,
|
||||
startLineNumber: 1,
|
||||
endColumn: stringIndex + link.length + 1,
|
||||
endLineNumber: 1
|
||||
}, startLine);
|
||||
|
||||
if (positionIsInRange(position, bufferRange)) {
|
||||
const validatedLink = await new Promise<ILink | undefined>(r => {
|
||||
this._validationCallback(link, (result) => {
|
||||
if (result) {
|
||||
const label = result.isDirectory
|
||||
? (this._isDirectoryInsideWorkspace(result.uri) ? FOLDER_IN_WORKSPACE_LABEL : FOLDER_NOT_IN_WORKSPACE_LABEL)
|
||||
: OPEN_FILE_LABEL;
|
||||
const activateCallback = this._wrapLinkHandler((event: MouseEvent | undefined, text: string) => {
|
||||
if (result.isDirectory) {
|
||||
this._handleLocalFolderLink(result.uri);
|
||||
} else {
|
||||
this._activateFileCallback(event, text);
|
||||
}
|
||||
});
|
||||
r(this._instantiationService.createInstance(TerminalLink, bufferRange, link, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, true, label));
|
||||
} else {
|
||||
r(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (validatedLink) {
|
||||
callback(validatedLink);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(undefined);
|
||||
}
|
||||
|
||||
protected get _localLinkRegex(): RegExp {
|
||||
const baseLocalLinkClause = this._processOperatingSystem === OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause;
|
||||
// Append line and column number regex
|
||||
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
|
||||
}
|
||||
|
||||
private async _handleLocalFolderLink(uri: URI): Promise<void> {
|
||||
// If the folder is within one of the window's workspaces, focus it in the explorer
|
||||
if (this._isDirectoryInsideWorkspace(uri)) {
|
||||
await this._commandService.executeCommand('revealInExplorer', uri);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open a new window for the folder
|
||||
this._hostService.openWindow([{ folderUri: uri }], { forceNewWindow: true });
|
||||
}
|
||||
|
||||
private async _isDirectoryInsideWorkspace(uri: URI) {
|
||||
const folders = this._workspaceContextService.getWorkspace().folders;
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (isEqualOrParent(uri, folders[0].uri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Terminal, ILinkProvider, IViewportRange, IBufferCellPosition, ILink } from 'xterm';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ISearchService } from 'vs/workbench/services/search/common/search';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
|
||||
export class TerminalWordLinkProvider implements ILinkProvider {
|
||||
private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
constructor(
|
||||
private readonly _xterm: Terminal,
|
||||
private readonly _wrapLinkHandler: (handler: (event: MouseEvent | undefined, link: string) => void) => XtermLinkMatcherHandler,
|
||||
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IQuickInputService private readonly _quickInputService: IQuickInputService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService private readonly _searchService: ISearchService,
|
||||
@IEditorService private readonly _editorService: IEditorService
|
||||
) {
|
||||
}
|
||||
|
||||
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
|
||||
const start: IBufferCellPosition = { x: position.x, y: position.y };
|
||||
const end: IBufferCellPosition = { x: position.x, y: position.y };
|
||||
|
||||
// TODO: Support wrapping
|
||||
|
||||
// Expand to the left until a word separator is hit
|
||||
const line = this._xterm.buffer.active.getLine(position.y - 1)!;
|
||||
let text = '';
|
||||
start.x++; // The hovered cell is considered first
|
||||
for (let x = position.x; x > 0; x--) {
|
||||
const char = line.getCell(x - 1)?.getChars();
|
||||
if (!char) {
|
||||
break;
|
||||
}
|
||||
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
|
||||
if (config.wordSeparators.indexOf(char) >= 0) {
|
||||
break;
|
||||
}
|
||||
start.x--;
|
||||
text = char + text;
|
||||
}
|
||||
|
||||
// No links were found (the hovered cell is whitespace)
|
||||
if (text.length === 0) {
|
||||
callback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand to the right until a word separator is hit
|
||||
// end.x++; // The hovered cell is considered first
|
||||
for (let x = position.x + 1; x <= line.length; x++) {
|
||||
const char = line.getCell(x - 1)?.getChars();
|
||||
if (!char) {
|
||||
break;
|
||||
}
|
||||
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
|
||||
if (config.wordSeparators.indexOf(char) >= 0) {
|
||||
break;
|
||||
}
|
||||
end.x++;
|
||||
text += char;
|
||||
}
|
||||
|
||||
const activateCallback = this._wrapLinkHandler((_, link) => this._activate(link));
|
||||
callback(new TerminalLink({ start, end }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService));
|
||||
}
|
||||
|
||||
private async _activate(link: string) {
|
||||
const results = await this._searchService.fileSearch(
|
||||
this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, {
|
||||
filePattern: link,
|
||||
maxResults: 2
|
||||
})
|
||||
);
|
||||
|
||||
// If there was exactly one match, open it
|
||||
if (results.results.length === 1) {
|
||||
const match = results.results[0];
|
||||
await this._editorService.openEditor({ resource: match.resource, options: { pinned: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to searching quick access
|
||||
this._quickInputService.quickAccess.show(link);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport {
|
||||
box-sizing: border-box;
|
||||
margin-right: -10px;
|
||||
}
|
||||
.monaco-workbench .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:last-child .xterm-viewport {
|
||||
@@ -156,11 +157,6 @@
|
||||
cursor: pointer!important;
|
||||
}
|
||||
|
||||
/* Rotate icon when terminal is in the sidebar */
|
||||
.monaco-workbench .part.sidebar .title-actions .terminal-action.codicon-split-horizontal {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .title > .title-actions .switch-terminal,
|
||||
.monaco-pane-view .pane > .pane-header .monaco-action-bar .switch-terminal {
|
||||
border-width: 1px;
|
||||
|
||||
@@ -3,27 +3,61 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .terminal-widget-overlay {
|
||||
.monaco-workbench .terminal-widget-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-message-widget {
|
||||
font-size: 12px;
|
||||
.monaco-workbench .terminal-hover-widget {
|
||||
position: fixed;
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
padding: 4px 8px;
|
||||
animation: fadein 100ms linear;
|
||||
white-space: nowrap;
|
||||
/* Must be drawn on the top of the terminal's canvases */
|
||||
z-index: 20;
|
||||
/* Must be higher than sash's z-index and terminal canvases */
|
||||
z-index: 40;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-message-widget p {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-message-widget a {
|
||||
.monaco-workbench .terminal-hover-widget a {
|
||||
color: #3794ff;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-overlay-widget {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
color: #3794ff;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-hover-target {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-env-var-info {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
z-index: 25;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-workbench .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:last-child .terminal-env-var-info {
|
||||
/* Adjust for reduced margin in splits */
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-env-var-info:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-env-var-info.codicon {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'vs/css!./media/terminal';
|
||||
import 'vs/css!./media/widgets';
|
||||
import 'vs/css!./media/xterm';
|
||||
import * as nls from 'vs/nls';
|
||||
import { SyncActionDescriptor, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingWeight, KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
@@ -19,35 +19,30 @@ import * as panel from 'vs/workbench/browser/panel';
|
||||
import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views';
|
||||
import { ClearSelectionTerminalAction, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewInActiveWorkspaceTerminalAction, CreateNewTerminalAction, FindNext, FindPrevious, FocusActiveTerminalAction, FocusNextPaneTerminalAction, FocusNextTerminalAction, FocusPreviousPaneTerminalAction, FocusPreviousTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, KillTerminalAction, QuickAccessTerminalAction, RenameTerminalAction, ResizePaneDownTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, RunActiveFileInTerminalAction, RunSelectedTextInTerminalAction, ScrollDownPageTerminalAction, ScrollDownTerminalAction, ScrollToBottomTerminalAction, ScrollToNextCommandAction, ScrollToPreviousCommandAction, ScrollToTopTerminalAction, ScrollUpPageTerminalAction, ScrollUpTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SelectToNextCommandAction, SelectToNextLineAction, SelectToPreviousCommandAction, SelectToPreviousLineAction, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, ToggleCaseSensitiveCommand, ToggleEscapeSequenceLoggingAction, ToggleRegexCommand, ToggleTerminalAction, ToggleWholeWordCommand, NavigationModeFocusPreviousTerminalAction, NavigationModeFocusNextTerminalAction, NavigationModeExitTerminalAction, ManageWorkspaceShellPermissionsTerminalCommand, CreateNewWithCwdTerminalAction, RenameWithArgTerminalAction, SendSequenceTerminalAction, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions';
|
||||
import { registerTerminalActions, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, KillTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, ToggleTerminalAction, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions';
|
||||
import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView';
|
||||
import { KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_VIEW_ID, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_VIEW_ID, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
|
||||
import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands';
|
||||
import { setupTerminalMenu } from 'vs/workbench/contrib/terminal/common/terminalMenu';
|
||||
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
|
||||
import { DEFAULT_COMMANDS_TO_SKIP_SHELL } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
|
||||
import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { registerShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalShellConfig';
|
||||
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { ITerminalService, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
|
||||
import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess';
|
||||
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalsQuickAccess';
|
||||
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess';
|
||||
import { terminalConfiguration, getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration';
|
||||
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
|
||||
|
||||
// Register services
|
||||
registerSingleton(ITerminalService, TerminalService, true);
|
||||
|
||||
if (platform.isWeb) {
|
||||
registerShellConfiguration();
|
||||
}
|
||||
|
||||
const inTerminalsPicker = 'inTerminalPicker';
|
||||
// Register quick accesses
|
||||
const quickAccessRegistry = (Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess));
|
||||
|
||||
const inTerminalsPicker = 'inTerminalPicker';
|
||||
quickAccessRegistry.registerQuickAccessProvider({
|
||||
ctor: TerminalQuickAccessProvider,
|
||||
prefix: TerminalQuickAccessProvider.PREFIX,
|
||||
@@ -55,325 +50,31 @@ quickAccessRegistry.registerQuickAccessProvider({
|
||||
placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a terminal to open."),
|
||||
helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Show All Opened Terminals"), needsEditor: false }]
|
||||
});
|
||||
|
||||
const quickAccessNavigateNextInTerminalPickerId = 'workbench.action.quickOpenNavigateNextInTerminalPicker';
|
||||
CommandsRegistry.registerCommand(
|
||||
{ id: quickAccessNavigateNextInTerminalPickerId, handler: getQuickNavigateHandler(quickAccessNavigateNextInTerminalPickerId, true) });
|
||||
|
||||
CommandsRegistry.registerCommand({ id: quickAccessNavigateNextInTerminalPickerId, handler: getQuickNavigateHandler(quickAccessNavigateNextInTerminalPickerId, true) });
|
||||
const quickAccessNavigatePreviousInTerminalPickerId = 'workbench.action.quickOpenNavigatePreviousInTerminalPicker';
|
||||
CommandsRegistry.registerCommand(
|
||||
{ id: quickAccessNavigatePreviousInTerminalPickerId, handler: getQuickNavigateHandler(quickAccessNavigatePreviousInTerminalPickerId, false) });
|
||||
|
||||
CommandsRegistry.registerCommand({ id: quickAccessNavigatePreviousInTerminalPickerId, handler: getQuickNavigateHandler(quickAccessNavigatePreviousInTerminalPickerId, false) });
|
||||
|
||||
// Register configurations
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'terminal',
|
||||
order: 100,
|
||||
title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'terminal.integrated.automationShell.linux': {
|
||||
markdownDescription: nls.localize('terminal.integrated.automationShell.linux', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`'),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.automationShell.osx': {
|
||||
markdownDescription: nls.localize('terminal.integrated.automationShell.osx', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`'),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.automationShell.windows': {
|
||||
markdownDescription: nls.localize('terminal.integrated.automationShell.windows', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`'),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.shellArgs.linux': {
|
||||
markdownDescription: nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
default: []
|
||||
},
|
||||
'terminal.integrated.shellArgs.osx': {
|
||||
markdownDescription: nls.localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
// Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This
|
||||
// is the reason terminals on macOS typically run login shells by default which set up
|
||||
// the environment. See http://unix.stackexchange.com/a/119675/115410
|
||||
default: ['-l']
|
||||
},
|
||||
'terminal.integrated.shellArgs.windows': {
|
||||
markdownDescription: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
'anyOf': [
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")
|
||||
}
|
||||
],
|
||||
default: []
|
||||
},
|
||||
'terminal.integrated.macOptionIsMeta': {
|
||||
description: nls.localize('terminal.integrated.macOptionIsMeta', "Controls whether to treat the option key as the meta key in the terminal on macOS."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.macOptionClickForcesSelection': {
|
||||
description: nls.localize('terminal.integrated.macOptionClickForcesSelection', "Controls whether to force selection when using Option+click on macOS. This will force a regular (line) selection and disallow the use of column selection mode. This enables copying and pasting using the regular terminal selection, for example, when mouse mode is enabled in tmux."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.copyOnSelection': {
|
||||
description: nls.localize('terminal.integrated.copyOnSelection', "Controls whether text selected in the terminal will be copied to the clipboard."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.drawBoldTextInBrightColors': {
|
||||
description: nls.localize('terminal.integrated.drawBoldTextInBrightColors', "Controls whether bold text in the terminal will always use the \"bright\" ANSI color variant."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.fontFamily': {
|
||||
markdownDescription: nls.localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."),
|
||||
type: 'string'
|
||||
},
|
||||
// TODO: Support font ligatures
|
||||
// 'terminal.integrated.fontLigatures': {
|
||||
// 'description': nls.localize('terminal.integrated.fontLigatures', "Controls whether font ligatures are enabled in the terminal."),
|
||||
// 'type': 'boolean',
|
||||
// 'default': false
|
||||
// },
|
||||
'terminal.integrated.fontSize': {
|
||||
description: nls.localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."),
|
||||
type: 'number',
|
||||
default: EDITOR_FONT_DEFAULTS.fontSize
|
||||
},
|
||||
'terminal.integrated.letterSpacing': {
|
||||
description: nls.localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."),
|
||||
type: 'number',
|
||||
default: DEFAULT_LETTER_SPACING
|
||||
},
|
||||
'terminal.integrated.lineHeight': {
|
||||
description: nls.localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."),
|
||||
type: 'number',
|
||||
default: DEFAULT_LINE_HEIGHT
|
||||
},
|
||||
'terminal.integrated.minimumContrastRatio': {
|
||||
markdownDescription: nls.localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."),
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
'terminal.integrated.fastScrollSensitivity': {
|
||||
markdownDescription: nls.localize('terminal.integrated.fastScrollSensitivity', "Scrolling speed multiplier when pressing `Alt`."),
|
||||
type: 'number',
|
||||
default: 5
|
||||
},
|
||||
'terminal.integrated.mouseWheelScrollSensitivity': {
|
||||
markdownDescription: nls.localize('terminal.integrated.mouseWheelScrollSensitivity', "A multiplier to be used on the `deltaY` of mouse wheel scroll events."),
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
'terminal.integrated.fontWeight': {
|
||||
type: 'string',
|
||||
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
description: nls.localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."),
|
||||
default: 'normal'
|
||||
},
|
||||
'terminal.integrated.fontWeightBold': {
|
||||
type: 'string',
|
||||
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
description: nls.localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."),
|
||||
default: 'bold'
|
||||
},
|
||||
'terminal.integrated.cursorBlinking': {
|
||||
description: nls.localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.cursorStyle': {
|
||||
description: nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."),
|
||||
enum: [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE],
|
||||
default: TerminalCursorStyle.BLOCK
|
||||
},
|
||||
'terminal.integrated.cursorWidth': {
|
||||
markdownDescription: nls.localize('terminal.integrated.cursorWidth', "Controls the width of the cursor when `#terminal.integrated.cursorStyle#` is set to `line`."),
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
'terminal.integrated.scrollback': {
|
||||
description: nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."),
|
||||
type: 'number',
|
||||
default: 1000
|
||||
},
|
||||
'terminal.integrated.detectLocale': {
|
||||
markdownDescription: nls.localize('terminal.integrated.detectLocale', "Controls whether to detect and set the `$LANG` environment variable to a UTF-8 compliant option since Azure Data Studio's terminal only supports UTF-8 encoded data coming from the shell."), // {{SQL CARBON EDIT}} Change product name to ADS
|
||||
type: 'string',
|
||||
enum: ['auto', 'off', 'on'],
|
||||
markdownEnumDescriptions: [
|
||||
nls.localize('terminal.integrated.detectLocale.auto', "Set the `$LANG` environment variable if the existing variable does not exist or it does not end in `'.UTF-8'`."),
|
||||
nls.localize('terminal.integrated.detectLocale.off', "Do not set the `$LANG` environment variable."),
|
||||
nls.localize('terminal.integrated.detectLocale.on', "Always set the `$LANG` environment variable.")
|
||||
],
|
||||
default: 'auto'
|
||||
},
|
||||
'terminal.integrated.rendererType': {
|
||||
type: 'string',
|
||||
enum: ['auto', 'canvas', 'dom', 'experimentalWebgl'],
|
||||
markdownEnumDescriptions: [
|
||||
nls.localize('terminal.integrated.rendererType.auto', "Let Azure Data Studio guess which renderer to use."), // {{SQL CARBON EDIT}} Change product name to ADS
|
||||
nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer."),
|
||||
nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer."),
|
||||
nls.localize('terminal.integrated.rendererType.experimentalWebgl', "Use the experimental webgl-based renderer. Note that this has some [known issues](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) and this will only be enabled for new terminals (not hot swappable like the other renderers).")
|
||||
],
|
||||
default: 'auto',
|
||||
description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered.")
|
||||
},
|
||||
'terminal.integrated.rightClickBehavior': {
|
||||
type: 'string',
|
||||
enum: ['default', 'copyPaste', 'paste', 'selectWord'],
|
||||
enumDescriptions: [
|
||||
nls.localize('terminal.integrated.rightClickBehavior.default', "Show the context menu."),
|
||||
nls.localize('terminal.integrated.rightClickBehavior.copyPaste', "Copy when there is a selection, otherwise paste."),
|
||||
nls.localize('terminal.integrated.rightClickBehavior.paste', "Paste on right click."),
|
||||
nls.localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu.")
|
||||
],
|
||||
default: platform.isMacintosh ? 'selectWord' : platform.isWindows ? 'copyPaste' : 'default',
|
||||
description: nls.localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click.")
|
||||
},
|
||||
'terminal.integrated.cwd': {
|
||||
description: nls.localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."),
|
||||
type: 'string',
|
||||
default: undefined
|
||||
},
|
||||
'terminal.integrated.confirmOnExit': {
|
||||
description: nls.localize('terminal.integrated.confirmOnExit', "Controls whether to confirm on exit if there are active terminal sessions."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.enableBell': {
|
||||
description: nls.localize('terminal.integrated.enableBell', "Controls whether the terminal bell is enabled."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.commandsToSkipShell': {
|
||||
markdownDescription: nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
default: []
|
||||
},
|
||||
'terminal.integrated.allowChords': {
|
||||
markdownDescription: nls.localize('terminal.integrated.allowChords', "Whether or not to allow chord keybindings in the terminal. Note that when this is true and the keystroke results in a chord it will bypass `#terminal.integrated.commandsToSkipShell#`, setting this to false is particularly useful when you want ctrl+k to go to your shell (not VS Code)."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.allowMnemonics': {
|
||||
markdownDescription: nls.localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true. This does nothing on macOS."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.inheritEnv': {
|
||||
markdownDescription: nls.localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from Azure Data Studio. This is not supported on Windows."), // {{SQL CARBON EDIT}} Change product name to ADS
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.env.osx': {
|
||||
markdownDescription: nls.localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the Azure Data Studio process to be used by the terminal on macOS. Set to `null` to delete the environment variable."), // {{SQL CARBON EDIT}} Change product name to ADS
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
'terminal.integrated.env.linux': {
|
||||
markdownDescription: nls.localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the Azure Data Studio process to be used by the terminal on Linux. Set to `null` to delete the environment variable."), // {{SQL CARBON EDIT}} Change product name to ADS
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
'terminal.integrated.env.windows': {
|
||||
markdownDescription: nls.localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the Azure Data Studio process to be used by the terminal on Windows. Set to `null` to delete the environment variable."), // {{SQL CARBON EDIT}} Change product name to ADS
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
'terminal.integrated.showExitAlert': {
|
||||
description: nls.localize('terminal.integrated.showExitAlert', "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.splitCwd': {
|
||||
description: nls.localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."),
|
||||
type: 'string',
|
||||
enum: ['workspaceRoot', 'initial', 'inherited'],
|
||||
enumDescriptions: [
|
||||
nls.localize('terminal.integrated.splitCwd.workspaceRoot', "A new split terminal will use the workspace root as the working directory. In a multi-root workspace a choice for which root folder to use is offered."),
|
||||
nls.localize('terminal.integrated.splitCwd.initial', "A new split terminal will use the working directory that the parent terminal started with."),
|
||||
nls.localize('terminal.integrated.splitCwd.inherited', "On macOS and Linux, a new split terminal will use the working directory of the parent terminal. On Windows, this behaves the same as initial."),
|
||||
],
|
||||
default: 'inherited'
|
||||
},
|
||||
'terminal.integrated.windowsEnableConpty': {
|
||||
description: nls.localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.wordSeparators': {
|
||||
description: nls.localize('terminal.integrated.wordSeparators', "A string containing all characters to be considered word separators by the double click to select word feature."),
|
||||
type: 'string',
|
||||
default: ' ()[]{}\',"`'
|
||||
},
|
||||
'terminal.integrated.experimentalUseTitleEvent': {
|
||||
description: nls.localize('terminal.integrated.experimentalUseTitleEvent', "An experimental setting that will use the terminal title event for the dropdown title. This setting will only apply to new terminals."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.enableFileLinks': {
|
||||
description: nls.localize('terminal.integrated.enableFileLinks', "Whether to enable file links in the terminal. Links can be slow when working on a network drive in particular because each file link is verified against the file system."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.unicodeVersion': {
|
||||
type: 'string',
|
||||
enum: ['6', '11'],
|
||||
enumDescriptions: [
|
||||
nls.localize('terminal.integrated.unicodeVersion.six', "Version 6 of unicode, this is an older version which should work better on older systems."),
|
||||
nls.localize('terminal.integrated.unicodeVersion.eleven', "Version 11 of unicode, this version provides better support on modern systems that use modern versions of unicode.")
|
||||
],
|
||||
default: '11',
|
||||
description: nls.localize('terminal.integrated.unicodeVersion', "Controls what version of unicode to use when evaluating the width of characters in the terminal. If you experience emoji or other wide characters not taking up the right amount of space or backspace either deleting too much or too little then you may want to try tweaking this setting.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(QuickAccessTerminalAction, QuickAccessTerminalAction.ID, QuickAccessTerminalAction.LABEL), 'Terminal: Switch Active Terminal', nls.localize('terminal', "Terminal"));
|
||||
configurationRegistry.registerConfiguration(terminalConfiguration);
|
||||
if (platform.isWeb) {
|
||||
// Desktop shell configuration are registered in electron-browser as their default values rely
|
||||
// on process.env
|
||||
configurationRegistry.registerConfiguration(getTerminalShellConfiguration());
|
||||
}
|
||||
|
||||
// Register views
|
||||
const VIEW_CONTAINER = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({
|
||||
id: TERMINAL_VIEW_ID,
|
||||
name: nls.localize('terminal', "Terminal"),
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
|
||||
storageId: TERMINAL_VIEW_ID,
|
||||
focusCommand: { id: TERMINAL_COMMAND_ID.FOCUS },
|
||||
hideIfEmpty: true,
|
||||
order: 3
|
||||
}, ViewContainerLocation.Panel);
|
||||
Registry.as<panel.PanelRegistry>(panel.Extensions.Panels).setDefaultPanelId(TERMINAL_VIEW_ID);
|
||||
|
||||
Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews([{
|
||||
id: TERMINAL_VIEW_ID,
|
||||
name: nls.localize('terminal', "Terminal"),
|
||||
@@ -383,23 +84,16 @@ Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews
|
||||
ctorDescriptor: new SyncDescriptor(TerminalViewPane)
|
||||
}], VIEW_CONTAINER);
|
||||
|
||||
// On mac cmd+` is reserved to cycle between windows, that's why the keybindings use WinCtrl
|
||||
const category = TERMINAL_ACTION_CATEGORY;
|
||||
// Register actions
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.LABEL), 'Terminal: Kill the Active Terminal Instance', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.LABEL, {
|
||||
registerTerminalActions();
|
||||
const category = TERMINAL_ACTION_CATEGORY;
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(KillTerminalAction), 'Terminal: Kill the Active Terminal Instance', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(CreateNewTerminalAction, {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK }
|
||||
}), 'Terminal: Create New Integrated Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ClearSelectionTerminalAction, ClearSelectionTerminalAction.ID, ClearSelectionTerminalAction.LABEL, {
|
||||
primary: KeyCode.Escape,
|
||||
linux: { primary: KeyCode.Escape }
|
||||
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE)), 'Terminal: Clear Selection', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(CreateNewInActiveWorkspaceTerminalAction, CreateNewInActiveWorkspaceTerminalAction.ID, CreateNewInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Create New Integrated Terminal (In Active Workspace)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusActiveTerminalAction, FocusActiveTerminalAction.ID, FocusActiveTerminalAction.LABEL), 'Terminal: Focus Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusNextTerminalAction, FocusNextTerminalAction.ID, FocusNextTerminalAction.LABEL), 'Terminal: Focus Next Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPreviousTerminalAction, FocusPreviousTerminalAction.ID, FocusPreviousTerminalAction.LABEL), 'Terminal: Focus Previous Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectAllTerminalAction, SelectAllTerminalAction.ID, SelectAllTerminalAction.LABEL, {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAllTerminalAction, {
|
||||
// Don't use ctrl+a by default as that would override the common go to start
|
||||
// of prompt shell binding
|
||||
primary: 0,
|
||||
@@ -408,154 +102,29 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectAllTerm
|
||||
// makes it easier for users to see how it works though.
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_A }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select All', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(RunSelectedTextInTerminalAction, RunSelectedTextInTerminalAction.ID, RunSelectedTextInTerminalAction.LABEL), 'Terminal: Run Selected Text In Active Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(RunActiveFileInTerminalAction, RunActiveFileInTerminalAction.ID, RunActiveFileInTerminalAction.LABEL), 'Terminal: Run Active File In Active Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleTerminalAction, ToggleTerminalAction.ID, ToggleTerminalAction.LABEL, {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTerminalAction, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyCode.US_BACKTICK }
|
||||
}), 'View: Toggle Integrated Terminal', nls.localize('viewCategory', "View"));
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollDownTerminalAction, ScrollDownTerminalAction.ID, ScrollDownTerminalAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Down (Line)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollDownPageTerminalAction, ScrollDownPageTerminalAction.ID, ScrollDownPageTerminalAction.LABEL, {
|
||||
primary: KeyMod.Shift | KeyCode.PageDown,
|
||||
mac: { primary: KeyCode.PageDown }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Down (Page)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollToBottomTerminalAction, ScrollToBottomTerminalAction.ID, ScrollToBottomTerminalAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.End,
|
||||
linux: { primary: KeyMod.Shift | KeyCode.End }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll to Bottom', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollUpTerminalAction, ScrollUpTerminalAction.ID, ScrollUpTerminalAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow },
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Up (Line)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollUpPageTerminalAction, ScrollUpPageTerminalAction.ID, ScrollUpPageTerminalAction.LABEL, {
|
||||
primary: KeyMod.Shift | KeyCode.PageUp,
|
||||
mac: { primary: KeyCode.PageUp }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Up (Page)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollToTopTerminalAction, ScrollToTopTerminalAction.ID, ScrollToTopTerminalAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Home,
|
||||
linux: { primary: KeyMod.Shift | KeyCode.Home }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll to Top', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL, {
|
||||
// Weight is higher than work workbench contributions so the keybinding remains
|
||||
// highest priority when chords are registered afterwards
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClearTerminalAction, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectDefaultShellWindowsTerminalAction, SelectDefaultShellWindowsTerminalAction.ID, SelectDefaultShellWindowsTerminalAction.LABEL), 'Terminal: Select Default Shell', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ManageWorkspaceShellPermissionsTerminalCommand, ManageWorkspaceShellPermissionsTerminalCommand.ID, ManageWorkspaceShellPermissionsTerminalCommand.LABEL), 'Terminal: Manage Workspace Shell Permissions', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(RenameTerminalAction, RenameTerminalAction.ID, RenameTerminalAction.LABEL), 'Terminal: Rename', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusTerminalFindWidgetAction, FocusTerminalFindWidgetAction.ID, FocusTerminalFindWidgetAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_F
|
||||
}, ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Focus Find Widget', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(HideTerminalFindWidgetAction, HideTerminalFindWidgetAction.ID, HideTerminalFindWidgetAction.LABEL, {
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape]
|
||||
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE)), 'Terminal: Hide Find Widget', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL, {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectDefaultShellWindowsTerminalAction), 'Terminal: Select Default Shell', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitTerminalAction, {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_5,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH,
|
||||
secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5]
|
||||
}
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split Terminal', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SplitInActiveWorkspaceTerminalAction, SplitInActiveWorkspaceTerminalAction.ID, SplitInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Split Terminal (In Active Workspace)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPreviousPaneTerminalAction, FocusPreviousPaneTerminalAction.ID, FocusPreviousPaneTerminalAction.LABEL, {
|
||||
primary: KeyMod.Alt | KeyCode.LeftArrow,
|
||||
secondary: [KeyMod.Alt | KeyCode.UpArrow],
|
||||
mac: {
|
||||
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.LeftArrow,
|
||||
secondary: [KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.UpArrow]
|
||||
}
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Previous Pane', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusNextPaneTerminalAction, FocusNextPaneTerminalAction.ID, FocusNextPaneTerminalAction.LABEL, {
|
||||
primary: KeyMod.Alt | KeyCode.RightArrow,
|
||||
secondary: [KeyMod.Alt | KeyCode.DownArrow],
|
||||
mac: {
|
||||
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.RightArrow,
|
||||
secondary: [KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.DownArrow]
|
||||
}
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Next Pane', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ResizePaneLeftTerminalAction, ResizePaneLeftTerminalAction.ID, ResizePaneLeftTerminalAction.LABEL, {
|
||||
primary: 0,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow },
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Left', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ResizePaneRightTerminalAction, ResizePaneRightTerminalAction.ID, ResizePaneRightTerminalAction.LABEL, {
|
||||
primary: 0,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.RightArrow },
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Right', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ResizePaneUpTerminalAction, ResizePaneUpTerminalAction.ID, ResizePaneUpTerminalAction.LABEL, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.UpArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Up', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ResizePaneDownTerminalAction, ResizePaneDownTerminalAction.ID, ResizePaneDownTerminalAction.LABEL, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Down', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollToPreviousCommandAction, ScrollToPreviousCommandAction.ID, ScrollToPreviousCommandAction.LABEL, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }
|
||||
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate())), 'Terminal: Scroll To Previous Command', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ScrollToNextCommandAction, ScrollToNextCommandAction.ID, ScrollToNextCommandAction.LABEL, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }
|
||||
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate())), 'Terminal: Scroll To Next Command', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectToPreviousCommandAction, SelectToPreviousCommandAction.ID, SelectToPreviousCommandAction.LABEL, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Previous Command', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectToNextCommandAction, SelectToNextCommandAction.ID, SelectToNextCommandAction.LABEL, {
|
||||
primary: 0,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Next Command', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(NavigationModeExitTerminalAction, NavigationModeExitTerminalAction.ID, NavigationModeExitTerminalAction.LABEL, {
|
||||
primary: KeyCode.Escape
|
||||
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED)), 'Terminal: Exit Navigation Mode', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(NavigationModeFocusPreviousTerminalAction, NavigationModeFocusPreviousTerminalAction.ID, NavigationModeFocusPreviousTerminalAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.UpArrow
|
||||
}, ContextKeyExpr.or(ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED), ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED))), 'Terminal: Focus Previous Line (Navigation Mode)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(NavigationModeFocusNextTerminalAction, NavigationModeFocusNextTerminalAction.ID, NavigationModeFocusNextTerminalAction.LABEL, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.DownArrow
|
||||
}, ContextKeyExpr.or(ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED), ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED))), 'Terminal: Focus Next Line (Navigation Mode)', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectToPreviousLineAction, SelectToPreviousLineAction.ID, SelectToPreviousLineAction.LABEL), 'Terminal: Select To Previous Line', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SelectToNextLineAction, SelectToNextLineAction.ID, SelectToNextLineAction.LABEL), 'Terminal: Select To Next Line', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleEscapeSequenceLoggingAction, ToggleEscapeSequenceLoggingAction.ID, ToggleEscapeSequenceLoggingAction.LABEL), 'Terminal: Toggle Escape Sequence Logging', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleRegexCommand, ToggleRegexCommand.ID, ToggleRegexCommand.LABEL, {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_R,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }
|
||||
}, ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED)), 'Terminal: Toggle find using regex', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleWholeWordCommand, ToggleWholeWordCommand.ID, ToggleWholeWordCommand.LABEL, {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_W,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W }
|
||||
}, ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED)), 'Terminal: Toggle find using whole word', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleCaseSensitiveCommand, ToggleCaseSensitiveCommand.ID, ToggleCaseSensitiveCommand.LABEL, {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_C,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }
|
||||
}, ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED)), 'Terminal: Toggle find using case sensitive', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FindNext, FindNext.ID, FindNext.LABEL, {
|
||||
primary: KeyCode.F3,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find next', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FindNext, FindNext.ID, FindNext.LABEL, {
|
||||
primary: KeyCode.F3,
|
||||
secondary: [KeyMod.Shift | KeyCode.Enter],
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3, KeyMod.Shift | KeyCode.Enter] }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find next', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FindPrevious, FindPrevious.ID, FindPrevious.LABEL, {
|
||||
primary: KeyMod.Shift | KeyCode.F3,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] },
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find previous', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FindPrevious, FindPrevious.ID, FindPrevious.LABEL, {
|
||||
primary: KeyMod.Shift | KeyCode.F3,
|
||||
secondary: [KeyCode.Enter],
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3, KeyCode.Enter] },
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find previous', category);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitInActiveWorkspaceTerminalAction), 'Terminal: Split Terminal (In Active Workspace)', category);
|
||||
|
||||
// Commands might be affected by Web restrictons
|
||||
if (BrowserFeatures.clipboard.writeText) {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.LABEL, {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(CopyTerminalSelectionAction, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C] },
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C }
|
||||
@@ -564,7 +133,7 @@ if (BrowserFeatures.clipboard.writeText) {
|
||||
|
||||
function registerSendSequenceKeybinding(text: string, rule: { when?: ContextKeyExpression } & IKeybindings): void {
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: SendSequenceTerminalAction.ID,
|
||||
id: TERMINAL_COMMAND_ID.SEND_SEQUENCE,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: rule.when || KEYBINDING_CONTEXT_TERMINAL_FOCUS,
|
||||
primary: rule.primary,
|
||||
@@ -577,21 +146,34 @@ function registerSendSequenceKeybinding(text: string, rule: { when?: ContextKeyE
|
||||
}
|
||||
|
||||
if (BrowserFeatures.clipboard.readText) {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.LABEL, {
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(TerminalPasteAction, {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V] },
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V }
|
||||
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category);
|
||||
// An extra Windows-only ctrl+v keybinding is used for pwsh that sends ctrl+v directly to the
|
||||
// shell, this gets handled by PSReadLine which properly handles multi-line pastes
|
||||
// shell, this gets handled by PSReadLine which properly handles multi-line pastes. This is
|
||||
// disabled in accessibility mode as PowerShell does not run PSReadLine when it detects a screen
|
||||
// reader.
|
||||
if (platform.isWindows) {
|
||||
registerSendSequenceKeybinding(String.fromCharCode('V'.charCodeAt(0) - 64), { // ctrl+v
|
||||
when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, ContextKeyExpr.equals(KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, WindowsShellType.PowerShell)),
|
||||
when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, ContextKeyExpr.equals(KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, WindowsShellType.PowerShell), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()),
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (platform.isWeb) {
|
||||
// Register standard external terminal keybinding as integrated terminal when in web as the
|
||||
// external terminal is not available
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
id: TERMINAL_COMMAND_ID.NEW,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: undefined,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C
|
||||
});
|
||||
}
|
||||
|
||||
// Delete word left: ctrl+w
|
||||
registerSendSequenceKeybinding(String.fromCharCode('W'.charCodeAt(0) - 64), {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
|
||||
@@ -615,77 +197,6 @@ registerSendSequenceKeybinding(String.fromCharCode('E'.charCodeAt(0) - 64), {
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.RightArrow }
|
||||
});
|
||||
|
||||
registerAction2(class extends SendSequenceTerminalAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: SendSequenceTerminalAction.ID,
|
||||
title: SendSequenceTerminalAction.LABEL,
|
||||
description: {
|
||||
description: SendSequenceTerminalAction.LABEL,
|
||||
args: [{
|
||||
name: 'args',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['text'],
|
||||
properties: {
|
||||
text: { type: 'string' }
|
||||
},
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
registerAction2(class extends CreateNewWithCwdTerminalAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: CreateNewWithCwdTerminalAction.ID,
|
||||
title: CreateNewWithCwdTerminalAction.LABEL,
|
||||
description: {
|
||||
description: CreateNewWithCwdTerminalAction.LABEL,
|
||||
args: [{
|
||||
name: 'args',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['cwd'],
|
||||
properties: {
|
||||
cwd: {
|
||||
description: CreateNewWithCwdTerminalAction.CWD_ARG_LABEL,
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
registerAction2(class extends RenameWithArgTerminalAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: RenameWithArgTerminalAction.ID,
|
||||
title: RenameWithArgTerminalAction.LABEL,
|
||||
description: {
|
||||
description: RenameWithArgTerminalAction.LABEL,
|
||||
args: [{
|
||||
name: 'args',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: {
|
||||
description: RenameWithArgTerminalAction.NAME_ARG_LABEL,
|
||||
type: 'string',
|
||||
minLength: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setupTerminalCommands();
|
||||
setupTerminalMenu();
|
||||
|
||||
|
||||
@@ -112,6 +112,13 @@ export interface ITerminalService {
|
||||
getActiveOrCreateInstance(): ITerminalInstance;
|
||||
splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): ITerminalInstance | null;
|
||||
|
||||
/**
|
||||
* Perform an action with the active terminal instance, if the terminal does
|
||||
* not exist the callback will not be called.
|
||||
* @param callback The callback that fires with the active terminal
|
||||
*/
|
||||
doWithActiveInstance<T>(callback: (terminal: ITerminalInstance) => T): T | void;
|
||||
|
||||
getActiveTab(): ITerminalTab | null;
|
||||
setActiveTabToNext(): void;
|
||||
setActiveTabToPrevious(): void;
|
||||
@@ -311,6 +318,11 @@ export interface ITerminalInstance {
|
||||
|
||||
readonly navigationMode: INavigationMode | undefined;
|
||||
|
||||
/**
|
||||
* Shows the environment information hover if the widget exists.
|
||||
*/
|
||||
showEnvironmentInfoHover(): void;
|
||||
|
||||
/**
|
||||
* Dispose the terminal instance, removing it from the panel/service and freeing up resources.
|
||||
*
|
||||
@@ -464,6 +476,12 @@ export interface ITerminalInstance {
|
||||
*/
|
||||
reuseTerminal(shell: IShellLaunchConfig): void;
|
||||
|
||||
/**
|
||||
* Relaunches the terminal, killing it and reusing the launch config used initially. Any
|
||||
* environment variable changes will be recalculated when this happens.
|
||||
*/
|
||||
relaunch(): void;
|
||||
|
||||
/**
|
||||
* Sets the title of the terminal instance.
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,9 +135,10 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
|
||||
if (this.config.rendererType === 'dom') {
|
||||
this._lastFontMeasurement.charWidth = rect.width;
|
||||
} else {
|
||||
const scaledCharWidth = rect.width * window.devicePixelRatio;
|
||||
const scaledCharWidth = Math.floor(rect.width * window.devicePixelRatio);
|
||||
const scaledCellWidth = scaledCharWidth + Math.round(letterSpacing);
|
||||
this._lastFontMeasurement.charWidth = Math.round(scaledCellWidth / window.devicePixelRatio);
|
||||
const actualCellWidth = scaledCellWidth / window.devicePixelRatio;
|
||||
this._lastFontMeasurement.charWidth = actualCellWidth - Math.round(letterSpacing) / window.devicePixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +189,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
|
||||
letterSpacing,
|
||||
lineHeight,
|
||||
charHeight: xtermCore._renderService.dimensions.actualCellHeight / lineHeight,
|
||||
charWidth: xtermCore._renderService.dimensions.actualCellWidth
|
||||
charWidth: xtermCore._renderService.dimensions.actualCellWidth - Math.round(letterSpacing) / window.devicePixelRatio
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
|
||||
import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
|
||||
import { IShellLaunchConfig, ITerminalDimensions, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, IWindowsShellHelper, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, TERMINAL_COMMAND_ID, LEGACY_CONSOLE_MODE_EXIT_CODE } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
|
||||
import { IShellLaunchConfig, ITerminalDimensions, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, IWindowsShellHelper, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, LEGACY_CONSOLE_MODE_EXIT_CODE, DEFAULT_COMMANDS_TO_SKIP_SHELL } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
|
||||
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
|
||||
import { TerminalLinkHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
|
||||
import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
|
||||
@@ -39,133 +39,15 @@ import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addon
|
||||
import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/addons/navigationModeAddon';
|
||||
import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
|
||||
import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget';
|
||||
import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
|
||||
// How long in milliseconds should an average frame take to render for a notification to appear
|
||||
// which suggests the fallback DOM-based renderer
|
||||
const SLOW_CANVAS_RENDER_THRESHOLD = 50;
|
||||
const NUMBER_OF_FRAMES_TO_MEASURE = 20;
|
||||
|
||||
export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [
|
||||
TERMINAL_COMMAND_ID.CLEAR_SELECTION,
|
||||
TERMINAL_COMMAND_ID.CLEAR,
|
||||
TERMINAL_COMMAND_ID.COPY_SELECTION,
|
||||
TERMINAL_COMMAND_ID.DELETE_TO_LINE_START,
|
||||
TERMINAL_COMMAND_ID.DELETE_WORD_LEFT,
|
||||
TERMINAL_COMMAND_ID.DELETE_WORD_RIGHT,
|
||||
TERMINAL_COMMAND_ID.FIND_WIDGET_FOCUS,
|
||||
TERMINAL_COMMAND_ID.FIND_WIDGET_HIDE,
|
||||
TERMINAL_COMMAND_ID.FIND_NEXT,
|
||||
TERMINAL_COMMAND_ID.FIND_PREVIOUS,
|
||||
TERMINAL_COMMAND_ID.TOGGLE_FIND_REGEX,
|
||||
TERMINAL_COMMAND_ID.TOGGLE_FIND_WHOLE_WORD,
|
||||
TERMINAL_COMMAND_ID.TOGGLE_FIND_CASE_SENSITIVE,
|
||||
TERMINAL_COMMAND_ID.FOCUS_NEXT_PANE,
|
||||
TERMINAL_COMMAND_ID.FOCUS_NEXT,
|
||||
TERMINAL_COMMAND_ID.FOCUS_PREVIOUS_PANE,
|
||||
TERMINAL_COMMAND_ID.FOCUS_PREVIOUS,
|
||||
TERMINAL_COMMAND_ID.FOCUS,
|
||||
TERMINAL_COMMAND_ID.KILL,
|
||||
TERMINAL_COMMAND_ID.MOVE_TO_LINE_END,
|
||||
TERMINAL_COMMAND_ID.MOVE_TO_LINE_START,
|
||||
TERMINAL_COMMAND_ID.NEW_IN_ACTIVE_WORKSPACE,
|
||||
TERMINAL_COMMAND_ID.NEW,
|
||||
TERMINAL_COMMAND_ID.PASTE,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_DOWN,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_LEFT,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_RIGHT,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_UP,
|
||||
TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE,
|
||||
TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT,
|
||||
TERMINAL_COMMAND_ID.SCROLL_DOWN_LINE,
|
||||
TERMINAL_COMMAND_ID.SCROLL_DOWN_PAGE,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_BOTTOM,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_TOP,
|
||||
TERMINAL_COMMAND_ID.SCROLL_UP_LINE,
|
||||
TERMINAL_COMMAND_ID.SCROLL_UP_PAGE,
|
||||
TERMINAL_COMMAND_ID.SEND_SEQUENCE,
|
||||
TERMINAL_COMMAND_ID.SELECT_ALL,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE,
|
||||
TERMINAL_COMMAND_ID.SPLIT_IN_ACTIVE_WORKSPACE,
|
||||
TERMINAL_COMMAND_ID.SPLIT,
|
||||
TERMINAL_COMMAND_ID.TOGGLE,
|
||||
TERMINAL_COMMAND_ID.NAVIGATION_MODE_EXIT,
|
||||
TERMINAL_COMMAND_ID.NAVIGATION_MODE_FOCUS_NEXT,
|
||||
TERMINAL_COMMAND_ID.NAVIGATION_MODE_FOCUS_PREVIOUS,
|
||||
'editor.action.toggleTabFocusMode',
|
||||
'workbench.action.quickOpen',
|
||||
'workbench.action.quickOpenPreviousEditor',
|
||||
'workbench.action.showCommands',
|
||||
'workbench.action.tasks.build',
|
||||
'workbench.action.tasks.restartTask',
|
||||
'workbench.action.tasks.runTask',
|
||||
'workbench.action.tasks.reRunTask',
|
||||
'workbench.action.tasks.showLog',
|
||||
'workbench.action.tasks.showTasks',
|
||||
'workbench.action.tasks.terminate',
|
||||
'workbench.action.tasks.test',
|
||||
'workbench.action.toggleFullScreen',
|
||||
'workbench.action.terminal.focusAtIndex1',
|
||||
'workbench.action.terminal.focusAtIndex2',
|
||||
'workbench.action.terminal.focusAtIndex3',
|
||||
'workbench.action.terminal.focusAtIndex4',
|
||||
'workbench.action.terminal.focusAtIndex5',
|
||||
'workbench.action.terminal.focusAtIndex6',
|
||||
'workbench.action.terminal.focusAtIndex7',
|
||||
'workbench.action.terminal.focusAtIndex8',
|
||||
'workbench.action.terminal.focusAtIndex9',
|
||||
'workbench.action.focusSecondEditorGroup',
|
||||
'workbench.action.focusThirdEditorGroup',
|
||||
'workbench.action.focusFourthEditorGroup',
|
||||
'workbench.action.focusFifthEditorGroup',
|
||||
'workbench.action.focusSixthEditorGroup',
|
||||
'workbench.action.focusSeventhEditorGroup',
|
||||
'workbench.action.focusEighthEditorGroup',
|
||||
'workbench.action.nextPanelView',
|
||||
'workbench.action.previousPanelView',
|
||||
'workbench.action.nextSideBarView',
|
||||
'workbench.action.previousSideBarView',
|
||||
'workbench.action.debug.start',
|
||||
'workbench.action.debug.stop',
|
||||
'workbench.action.debug.run',
|
||||
'workbench.action.debug.restart',
|
||||
'workbench.action.debug.continue',
|
||||
'workbench.action.debug.pause',
|
||||
'workbench.action.debug.stepInto',
|
||||
'workbench.action.debug.stepOut',
|
||||
'workbench.action.debug.stepOver',
|
||||
'workbench.action.nextEditor',
|
||||
'workbench.action.previousEditor',
|
||||
'workbench.action.nextEditorInGroup',
|
||||
'workbench.action.previousEditorInGroup',
|
||||
'workbench.action.openNextRecentlyUsedEditor',
|
||||
'workbench.action.openPreviousRecentlyUsedEditor',
|
||||
'workbench.action.openNextRecentlyUsedEditorInGroup',
|
||||
'workbench.action.openPreviousRecentlyUsedEditorInGroup',
|
||||
'workbench.action.quickOpenPreviousRecentlyUsedEditor',
|
||||
'workbench.action.quickOpenLeastRecentlyUsedEditor',
|
||||
'workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup',
|
||||
'workbench.action.quickOpenLeastRecentlyUsedEditorInGroup',
|
||||
'workbench.action.focusActiveEditorGroup',
|
||||
'workbench.action.focusFirstEditorGroup',
|
||||
'workbench.action.focusLastEditorGroup',
|
||||
'workbench.action.firstEditorInGroup',
|
||||
'workbench.action.lastEditorInGroup',
|
||||
'workbench.action.navigateUp',
|
||||
'workbench.action.navigateDown',
|
||||
'workbench.action.navigateRight',
|
||||
'workbench.action.navigateLeft',
|
||||
'workbench.action.togglePanel',
|
||||
'workbench.action.quickOpenView',
|
||||
'workbench.action.toggleMaximizedPanel'
|
||||
];
|
||||
|
||||
let xtermConstructor: Promise<typeof XTermTerminal> | undefined;
|
||||
|
||||
interface ICanvasDimensions {
|
||||
@@ -215,8 +97,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
|
||||
private _messageTitleDisposable: IDisposable | undefined;
|
||||
|
||||
private _widgetManager: TerminalWidgetManager | undefined;
|
||||
private _linkHandler: TerminalLinkHandler | undefined;
|
||||
private _widgetManager: TerminalWidgetManager = this._instantiationService.createInstance(TerminalWidgetManager);
|
||||
private _linkManager: TerminalLinkManager | undefined;
|
||||
private _environmentInfo: { widget: EnvironmentVariableInfoWidget, disposable: IDisposable } | undefined;
|
||||
private _commandTrackerAddon: CommandTrackerAddon | undefined;
|
||||
private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined;
|
||||
|
||||
@@ -293,8 +176,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
|
||||
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService
|
||||
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -509,8 +391,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._processManager.onProcessData(data => this._onProcessData(data));
|
||||
this._xterm.onData(data => this._processManager.write(data));
|
||||
this.processReady.then(async () => {
|
||||
if (this._linkHandler) {
|
||||
this._linkHandler.processCwd = await this._processManager.getInitialCwd();
|
||||
if (this._linkManager) {
|
||||
this._linkManager.processCwd = await this._processManager.getInitialCwd();
|
||||
}
|
||||
});
|
||||
// Init winpty compat and link handler after process creation as they rely on the
|
||||
@@ -526,8 +408,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, xterm, this._processManager, this._configHelper);
|
||||
this._linkHandler.onBeforeHandleLink(e => {
|
||||
this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!, this._configHelper);
|
||||
this._linkManager.onBeforeHandleLink(e => {
|
||||
e.terminal = this;
|
||||
this._onBeforeHandleLink.fire(e);
|
||||
});
|
||||
@@ -601,6 +483,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
throw new Error('xterm elements not set after open');
|
||||
}
|
||||
|
||||
|
||||
xterm.textarea.setAttribute('aria-label', nls.localize('terminalTextBoxAriaLabel', "Terminal {0}", this._id));
|
||||
xterm.textarea.addEventListener('focus', () => this._onFocus.fire(this));
|
||||
xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => {
|
||||
// Disable all input if the terminal is exiting
|
||||
@@ -703,9 +587,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._refreshSelectionContextKey();
|
||||
}));
|
||||
|
||||
const widgetManager = new TerminalWidgetManager(this._wrapperElement, this._openerService);
|
||||
this._widgetManager = widgetManager;
|
||||
this._processManager.onProcessReady(() => this._linkHandler?.setWidgetManager(widgetManager));
|
||||
this._widgetManager.attachToElement(xterm.element);
|
||||
this._processManager.onProcessReady(() => this._linkManager?.setWidgetManager(this._widgetManager));
|
||||
|
||||
const computedStyle = window.getComputedStyle(this._container);
|
||||
const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10);
|
||||
@@ -774,11 +657,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
}
|
||||
|
||||
public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void): number {
|
||||
return this._linkHandler!.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback);
|
||||
return this._linkManager!.registerCustomLinkHandler(regex, (_, url) => handler(url), matchIndex, validationCallback);
|
||||
}
|
||||
|
||||
public deregisterLinkMatcher(linkMatcherId: number): void {
|
||||
// TODO: Move this into TerminalLinkHandler to avoid the promise check
|
||||
this._xtermReadyPromise.then(xterm => xterm.deregisterLinkMatcher(linkMatcherId));
|
||||
}
|
||||
|
||||
@@ -836,12 +718,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
|
||||
dispose(this._windowsShellHelper);
|
||||
this._windowsShellHelper = undefined;
|
||||
dispose(this._linkHandler);
|
||||
this._linkHandler = undefined;
|
||||
dispose(this._linkManager);
|
||||
this._linkManager = undefined;
|
||||
dispose(this._commandTrackerAddon);
|
||||
this._commandTrackerAddon = undefined;
|
||||
dispose(this._widgetManager);
|
||||
this._widgetManager = undefined;
|
||||
|
||||
if (this._xterm && this._xterm.element) {
|
||||
this._hadFocusOnExit = dom.hasClass(this._xterm.element, 'focus');
|
||||
@@ -856,7 +737,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
}
|
||||
if (this._xterm) {
|
||||
const buffer = this._xterm.buffer;
|
||||
this._sendLineData(buffer, buffer.baseY + buffer.cursorY);
|
||||
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY);
|
||||
this._xterm.dispose();
|
||||
}
|
||||
|
||||
@@ -910,6 +791,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this.focus();
|
||||
this._xterm.paste(await this._clipboardService.readText());
|
||||
}
|
||||
|
||||
public async sendText(text: string, addNewLine: boolean): Promise<void> {
|
||||
// Normalize line endings to 'enter' press.
|
||||
text = text.replace(TerminalInstance.EOL_REGEX, '\r');
|
||||
@@ -934,7 +816,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
// change since the number of visible rows decreases.
|
||||
// This can likely be removed after https://github.com/xtermjs/xterm.js/issues/291 is
|
||||
// fixed upstream.
|
||||
this._xtermCore._onScroll.fire(this._xterm.buffer.viewportY);
|
||||
this._xtermCore._onScroll.fire(this._xterm.buffer.active.viewportY);
|
||||
if (this._container && this._container.parentElement) {
|
||||
// Force a layout when the instance becomes invisible. This is particularly important
|
||||
// for ensuring that terminals that are created in the background by an extension will
|
||||
@@ -992,6 +874,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._processManager.onProcessData(data => this._onData.fire(data));
|
||||
this._processManager.onProcessOverrideDimensions(e => this.setDimensions(e));
|
||||
this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e));
|
||||
this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e));
|
||||
|
||||
if (this._shellLaunchConfig.name) {
|
||||
this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api);
|
||||
@@ -1033,7 +916,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
}
|
||||
|
||||
private _onProcessData(data: string): void {
|
||||
this._widgetManager?.closeMessage();
|
||||
this._xterm?.write(data);
|
||||
}
|
||||
|
||||
@@ -1139,7 +1021,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
}
|
||||
}
|
||||
|
||||
public reuseTerminal(shell: IShellLaunchConfig): void {
|
||||
public reuseTerminal(shell: IShellLaunchConfig, reset: boolean = false): void {
|
||||
// Unsubscribe any key listener we may have.
|
||||
this._pressAnyKeyToCloseListener?.dispose();
|
||||
this._pressAnyKeyToCloseListener = undefined;
|
||||
@@ -1148,8 +1030,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._processManager.dispose();
|
||||
|
||||
if (this._xterm) {
|
||||
// Ensure new processes' output starts at start of new line
|
||||
this._xterm.write('\n\x1b[G');
|
||||
if (reset) {
|
||||
this._xterm.reset();
|
||||
} else {
|
||||
// Ensure new processes' output starts at start of new line
|
||||
this._xterm.write('\n\x1b[G');
|
||||
}
|
||||
|
||||
// Print initialText if specified
|
||||
if (shell.initialText) {
|
||||
@@ -1163,11 +1049,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Force initialText to be non-falsy for reused terminals such that the
|
||||
// conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to hang
|
||||
// in Windows 10 1903 so we only want to use it when something is definitely written to the
|
||||
// terminal.
|
||||
shell.initialText = ' ';
|
||||
if (!reset) {
|
||||
// HACK: Force initialText to be non-falsy for reused terminals such that the
|
||||
// conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to hang
|
||||
// in Windows 10 1903 so we only want to use it when something is definitely written to the
|
||||
// terminal.
|
||||
shell.initialText = ' ';
|
||||
}
|
||||
|
||||
// Set the new shell launch config
|
||||
this._shellLaunchConfig = shell; // Must be done before calling _createProcess()
|
||||
@@ -1184,17 +1072,21 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._processManager.onProcessData(data => this._onProcessData(data));
|
||||
}
|
||||
|
||||
public relaunch(): void {
|
||||
this.reuseTerminal(this._shellLaunchConfig, true);
|
||||
}
|
||||
|
||||
private _onLineFeed(): void {
|
||||
const buffer = this._xterm!.buffer;
|
||||
const newLine = buffer.getLine(buffer.baseY + buffer.cursorY);
|
||||
const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY);
|
||||
if (newLine && !newLine.isWrapped) {
|
||||
this._sendLineData(buffer, buffer.baseY + buffer.cursorY - 1);
|
||||
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _onCursorMove(): void {
|
||||
const buffer = this._xterm!.buffer;
|
||||
this._sendLineData(buffer, buffer.baseY + buffer.cursorY);
|
||||
this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY);
|
||||
}
|
||||
|
||||
private _onTitleChange(title: string): void {
|
||||
@@ -1239,8 +1131,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
private async _updateProcessCwd(): Promise<string> {
|
||||
// reset cwd if it has changed, so file based url paths can be resolved
|
||||
const cwd = await this.getCwd();
|
||||
if (cwd && this._linkHandler) {
|
||||
this._linkHandler.processCwd = cwd;
|
||||
if (cwd && this._linkManager) {
|
||||
this._linkManager.processCwd = cwd;
|
||||
}
|
||||
return cwd;
|
||||
}
|
||||
@@ -1264,6 +1156,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
// Never set webgl as it's an addon not a rendererType
|
||||
this._safeSetOption('rendererType', config.rendererType === 'auto' ? 'canvas' : config.rendererType);
|
||||
}
|
||||
this._refreshEnvironmentVariableInfoWidgetState(this._processManager.environmentVariableInfo);
|
||||
}
|
||||
|
||||
private async _updateUnicodeVersion(): Promise<void> {
|
||||
@@ -1466,12 +1359,43 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
||||
this._shellLaunchConfig.env = shellLaunchConfig.env;
|
||||
}
|
||||
|
||||
public showEnvironmentInfoHover(): void {
|
||||
if (this._environmentInfo) {
|
||||
this._environmentInfo.widget.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _onEnvironmentVariableInfoChanged(info: IEnvironmentVariableInfo): void {
|
||||
if (info.requiresAction) {
|
||||
this._xterm?.textarea?.setAttribute('aria-label', nls.localize('terminalStaleTextBoxAriaLabel', "Terminal {0} environment is stale, run the 'Show Environment Information' command for more information", this._id));
|
||||
}
|
||||
this._refreshEnvironmentVariableInfoWidgetState(info);
|
||||
}
|
||||
|
||||
private _refreshEnvironmentVariableInfoWidgetState(info?: IEnvironmentVariableInfo): void {
|
||||
this._environmentInfo?.disposable.dispose();
|
||||
|
||||
// Check if the widget should not exist
|
||||
if (!info ||
|
||||
this._configHelper.config.environmentChangesIndicator === 'off' ||
|
||||
this._configHelper.config.environmentChangesIndicator === 'warnonly' && !info.requiresAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// (Re-)create the widget
|
||||
const widget = this._instantiationService.createInstance(EnvironmentVariableInfoWidget, info);
|
||||
const disposable = this._widgetManager.attachWidget(widget);
|
||||
if (disposable) {
|
||||
this._environmentInfo = { widget, disposable };
|
||||
}
|
||||
}
|
||||
|
||||
private _getXtermTheme(theme?: IColorTheme): any {
|
||||
if (!theme) {
|
||||
theme = this._themeService.getColorTheme();
|
||||
}
|
||||
|
||||
const location = this._viewDescriptorService.getViewLocation(TERMINAL_VIEW_ID)!;
|
||||
const location = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!;
|
||||
const foregroundColor = theme.getColor(TERMINAL_FOREGROUND_COLOR);
|
||||
const backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || (location === ViewContainerLocation.Sidebar ? theme.getColor(SIDE_BAR_BACKGROUND) : theme.getColor(PANEL_BACKGROUND));
|
||||
const cursorColor = theme.getColor(TERMINAL_CURSOR_FOREGROUND_COLOR) || foregroundColor;
|
||||
|
||||
@@ -23,8 +23,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
|
||||
import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection, IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { EnvironmentVariableInfoStale, EnvironmentVariableInfoChangesActive } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
|
||||
/** The amount of time to consider terminal errors to be related to the launch */
|
||||
const LAUNCHING_DURATION = 500;
|
||||
@@ -62,6 +63,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
private _latencyLastMeasured: number = 0;
|
||||
private _initialCwd: string | undefined;
|
||||
private _extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined;
|
||||
private _environmentVariableInfo: IEnvironmentVariableInfo | undefined;
|
||||
|
||||
private readonly _onProcessReady = this._register(new Emitter<void>());
|
||||
public get onProcessReady(): Event<void> { return this._onProcessReady.event; }
|
||||
@@ -77,6 +79,10 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
public get onProcessOverrideDimensions(): Event<ITerminalDimensions | undefined> { return this._onProcessOverrideDimensions.event; }
|
||||
private readonly _onProcessOverrideShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
|
||||
public get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { return this._onProcessOverrideShellLaunchConfig.event; }
|
||||
private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter<IEnvironmentVariableInfo>());
|
||||
public get onEnvironmentVariableInfoChanged(): Event<IEnvironmentVariableInfo> { return this._onEnvironmentVariableInfoChange.event; }
|
||||
|
||||
public get environmentVariableInfo(): IEnvironmentVariableInfo | undefined { return this._environmentVariableInfo; }
|
||||
|
||||
constructor(
|
||||
private readonly _terminalId: number,
|
||||
@@ -91,7 +97,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
@IRemotePathService private readonly _remotePathService: IRemotePathService,
|
||||
@IPathService private readonly _pathService: IPathService,
|
||||
@IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService
|
||||
) {
|
||||
super();
|
||||
@@ -135,13 +141,17 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
const hasRemoteAuthority = !!this.remoteAuthority;
|
||||
let launchRemotely = hasRemoteAuthority || forceExtHostProcess;
|
||||
|
||||
const userHomeUri = await this._remotePathService.userHome;
|
||||
// resolvedUserHome is needed here as remote resolvers can launch local terminals before
|
||||
// they're connected to the remote.
|
||||
this.userHome = this._pathService.resolvedUserHome?.fsPath;
|
||||
this.os = platform.OS;
|
||||
if (launchRemotely) {
|
||||
const userHomeUri = await this._pathService.userHome;
|
||||
this.userHome = userHomeUri.path;
|
||||
if (hasRemoteAuthority) {
|
||||
const remoteEnv = await this._remoteAgentService.getEnvironment();
|
||||
if (remoteEnv) {
|
||||
this.userHome = remoteEnv.userHome.path;
|
||||
this.os = remoteEnv.os;
|
||||
}
|
||||
}
|
||||
@@ -149,7 +159,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot();
|
||||
this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper);
|
||||
} else {
|
||||
this.userHome = userHomeUri.fsPath;
|
||||
this._process = await this._launchProcess(shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled);
|
||||
}
|
||||
}
|
||||
@@ -195,7 +204,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
shellLaunchConfig: IShellLaunchConfig,
|
||||
cols: number,
|
||||
rows: number,
|
||||
userHome: string,
|
||||
userHome: string | undefined,
|
||||
isScreenReaderModeEnabled: boolean
|
||||
): Promise<ITerminalChildProcess> {
|
||||
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file);
|
||||
@@ -239,6 +248,10 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection;
|
||||
this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection)));
|
||||
this._extEnvironmentVariableCollection.applyToProcessEnvironment(env);
|
||||
if (this._extEnvironmentVariableCollection.map.size > 0) {
|
||||
this._environmentVariableInfo = new EnvironmentVariableInfoChangesActive(this._extEnvironmentVariableCollection);
|
||||
this._onEnvironmentVariableInfoChange.fire(this._environmentVariableInfo);
|
||||
}
|
||||
|
||||
const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled;
|
||||
return this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty);
|
||||
@@ -316,35 +329,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
|
||||
}
|
||||
|
||||
private _onEnvironmentVariableCollectionChange(newCollection: IMergedEnvironmentVariableCollection): void {
|
||||
// TODO: React to changes in environment variable collections
|
||||
// const newAdditions = this._extEnvironmentVariableCollection!.getNewAdditions(newCollection);
|
||||
// if (newAdditions === undefined) {
|
||||
// return;
|
||||
// }
|
||||
// const promptChoices: IPromptChoice[] = [
|
||||
// {
|
||||
// label: nls.localize('apply', "Apply"),
|
||||
// run: () => {
|
||||
// let text = '';
|
||||
// newAdditions.forEach((mutator, variable) => {
|
||||
// // TODO: Support other common shells
|
||||
// // TODO: Escape the new values properly
|
||||
// switch (mutator.type) {
|
||||
// case EnvironmentVariableMutatorType.Append:
|
||||
// text += `export ${variable}="$${variable}${mutator.value}"\n`;
|
||||
// break;
|
||||
// case EnvironmentVariableMutatorType.Prepend:
|
||||
// text += `export ${variable}="${mutator.value}$${variable}"\n`;
|
||||
// break;
|
||||
// case EnvironmentVariableMutatorType.Replace:
|
||||
// text += `export ${variable}="${mutator.value}"\n`;
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
// this.write(text);
|
||||
// }
|
||||
// } as IPromptChoice
|
||||
// ];
|
||||
// this._notificationService.prompt(Severity.Info, nls.localize('environmentchange', "An extension wants to change the terminal environment, do you want to send commands to set the variables in the terminal? Note if you have an application open in the terminal this may not work."), promptChoices);
|
||||
const diff = this._extEnvironmentVariableCollection!.diff(newCollection);
|
||||
if (diff === undefined) {
|
||||
return;
|
||||
}
|
||||
this._environmentVariableInfo = this._instantiationService.createInstance(EnvironmentVariableInfoStale, diff, this._terminalId);
|
||||
this._onEnvironmentVariableInfoChange.fire(this._environmentVariableInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,13 @@ export class TerminalService implements ITerminalService {
|
||||
return tab.activeInstance;
|
||||
}
|
||||
|
||||
public doWithActiveInstance<T>(callback: (terminal: ITerminalInstance) => T): T | void {
|
||||
const instance = this.getActiveInstance();
|
||||
if (instance) {
|
||||
return callback(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public getInstanceFromId(terminalId: number): ITerminalInstance | undefined {
|
||||
let bgIndex = -1;
|
||||
this._backgroundedTerminalInstances.forEach((terminalInstance, i) => {
|
||||
@@ -694,10 +701,10 @@ export class TerminalService implements ITerminalService {
|
||||
|
||||
public hidePanel(): void {
|
||||
// Hide the panel if the terminal is in the panel and it has no sibling views
|
||||
const location = this._viewDescriptorService.getViewLocation(TERMINAL_VIEW_ID);
|
||||
const location = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID);
|
||||
if (location === ViewContainerLocation.Panel) {
|
||||
const panel = this._viewDescriptorService.getViewContainer(TERMINAL_VIEW_ID);
|
||||
if (panel && this._viewDescriptorService.getViewDescriptors(panel).activeViewDescriptors.length === 1) {
|
||||
const panel = this._viewDescriptorService.getViewContainerByViewId(TERMINAL_VIEW_ID);
|
||||
if (panel && this._viewDescriptorService.getViewContainerModel(panel).activeViewDescriptors.length === 1) {
|
||||
this._layoutService.setPanelHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { IShellLaunchConfig, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
@@ -218,6 +217,7 @@ export class TerminalTab extends Disposable implements ITerminalTab {
|
||||
private _terminalLocation: ViewContainerLocation = ViewContainerLocation.Panel;
|
||||
|
||||
private _activeInstanceIndex: number;
|
||||
private _isVisible: boolean = false;
|
||||
|
||||
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
|
||||
|
||||
@@ -270,10 +270,7 @@ export class TerminalTab extends Disposable implements ITerminalTab {
|
||||
|
||||
private _initInstanceListeners(instance: ITerminalInstance): void {
|
||||
instance.addDisposable(instance.onDisposed(instance => this._onInstanceDisposed(instance)));
|
||||
instance.addDisposable(instance.onFocused(instance => {
|
||||
aria.alert(nls.localize('terminalFocus', "Terminal {0}", this._terminalService.activeTabIndex + 1));
|
||||
this._setActiveInstance(instance);
|
||||
}));
|
||||
instance.addDisposable(instance.onFocused(instance => this._setActiveInstance(instance)));
|
||||
}
|
||||
|
||||
private _onInstanceDisposed(instance: ITerminalInstance): void {
|
||||
@@ -350,12 +347,14 @@ export class TerminalTab extends Disposable implements ITerminalTab {
|
||||
this._container.appendChild(this._tabElement);
|
||||
if (!this._splitPaneContainer) {
|
||||
this._panelPosition = this._layoutService.getPanelPosition();
|
||||
this._terminalLocation = this._viewDescriptorService.getViewLocation(TERMINAL_VIEW_ID)!;
|
||||
this._terminalLocation = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!;
|
||||
const orientation = this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL;
|
||||
const newLocal = this._instantiationService.createInstance(SplitPaneContainer, this._tabElement, orientation);
|
||||
this._splitPaneContainer = newLocal;
|
||||
this.terminalInstances.forEach(instance => this._splitPaneContainer!.split(instance));
|
||||
}
|
||||
|
||||
this.setVisible(this._isVisible);
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
@@ -369,6 +368,7 @@ export class TerminalTab extends Disposable implements ITerminalTab {
|
||||
}
|
||||
|
||||
public setVisible(visible: boolean): void {
|
||||
this._isVisible = visible;
|
||||
if (this._tabElement) {
|
||||
this._tabElement.style.display = visible ? '' : 'none';
|
||||
}
|
||||
@@ -400,7 +400,7 @@ export class TerminalTab extends Disposable implements ITerminalTab {
|
||||
if (this._splitPaneContainer) {
|
||||
// Check if the panel position changed and rotate panes if so
|
||||
const newPanelPosition = this._layoutService.getPanelPosition();
|
||||
const newTerminalLocation = this._viewDescriptorService.getViewLocation(TERMINAL_VIEW_ID)!;
|
||||
const newTerminalLocation = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!;
|
||||
const terminalPositionChanged = newPanelPosition !== this._panelPosition || newTerminalLocation !== this._terminalLocation;
|
||||
|
||||
if (terminalPositionChanged) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService, IColorTheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
|
||||
import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget';
|
||||
import { editorHoverBackground, editorHoverBorder, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionViewItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction, CreateNewTerminalAction, SplitTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
@@ -30,6 +29,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
|
||||
const FIND_FOCUS_CLASS = 'find-focused';
|
||||
|
||||
@@ -42,6 +42,7 @@ export class TerminalViewPane extends ViewPane {
|
||||
private _parentDomElement: HTMLElement | undefined;
|
||||
private _terminalContainer: HTMLElement | undefined;
|
||||
private _findWidget: TerminalFindWidget | undefined;
|
||||
private _splitTerminalAction: IAction | undefined;
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@@ -62,6 +63,7 @@ export class TerminalViewPane extends ViewPane {
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
super.renderBody(container);
|
||||
this._parentDomElement = container;
|
||||
dom.addClass(this._parentDomElement, 'integrated-terminal');
|
||||
this._fontStyleElement = document.createElement('style');
|
||||
@@ -119,15 +121,21 @@ export class TerminalViewPane extends ViewPane {
|
||||
}
|
||||
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
super.layoutBody(height, width);
|
||||
this._terminalService.terminalTabs.forEach(t => t.layout(width, height));
|
||||
// Update orientation of split button icon
|
||||
if (this._splitTerminalAction) {
|
||||
this._splitTerminalAction.class = this.orientation === Orientation.HORIZONTAL ? SplitTerminalAction.HORIZONTAL_CLASS : SplitTerminalAction.VERTICAL_CLASS;
|
||||
}
|
||||
}
|
||||
|
||||
public getActions(): IAction[] {
|
||||
if (!this._actions) {
|
||||
this._splitTerminalAction = this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL);
|
||||
this._actions = [
|
||||
this._instantiationService.createInstance(SwitchTerminalAction, SwitchTerminalAction.ID, SwitchTerminalAction.LABEL),
|
||||
this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL),
|
||||
this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL),
|
||||
this._splitTerminalAction,
|
||||
this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL)
|
||||
];
|
||||
this._actions.forEach(a => {
|
||||
@@ -348,18 +356,4 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
|
||||
if (borderColor) {
|
||||
collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .split-view-view:not(:first-child) { border-color: ${borderColor.toString()}; }`);
|
||||
}
|
||||
|
||||
// Borrow the editor's hover background for now
|
||||
const hoverBackground = theme.getColor(editorHoverBackground);
|
||||
if (hoverBackground) {
|
||||
collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .terminal-message-widget { background-color: ${hoverBackground}; }`);
|
||||
}
|
||||
const hoverBorder = theme.getColor(editorHoverBorder);
|
||||
if (hoverBorder) {
|
||||
collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .terminal-message-widget { border: 1px solid ${hoverBorder}; }`);
|
||||
}
|
||||
const hoverForeground = theme.getColor(editorHoverForeground);
|
||||
if (hoverForeground) {
|
||||
collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .terminal-message-widget { color: ${hoverForeground}; }`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export enum WidgetVerticalAlignment {
|
||||
Bottom,
|
||||
Top
|
||||
}
|
||||
|
||||
const WIDGET_HEIGHT = 29;
|
||||
|
||||
export class TerminalWidgetManager implements IDisposable {
|
||||
private _container: HTMLElement | undefined;
|
||||
private _xtermViewport: HTMLElement | undefined;
|
||||
|
||||
private _messageWidget: MessageWidget | undefined;
|
||||
private readonly _messageListeners = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
terminalWrapper: HTMLElement,
|
||||
private readonly _openerService: IOpenerService
|
||||
) {
|
||||
this._container = document.createElement('div');
|
||||
this._container.classList.add('terminal-widget-overlay');
|
||||
terminalWrapper.appendChild(this._container);
|
||||
|
||||
this._initTerminalHeightWatcher(terminalWrapper);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._container && this._container.parentElement) {
|
||||
this._container.parentElement.removeChild(this._container);
|
||||
this._container = undefined;
|
||||
}
|
||||
this._xtermViewport = undefined;
|
||||
this._messageListeners.dispose();
|
||||
}
|
||||
|
||||
private _initTerminalHeightWatcher(terminalWrapper: HTMLElement) {
|
||||
// Watch the xterm.js viewport for style changes and do a layout if it changes
|
||||
this._xtermViewport = <HTMLElement>terminalWrapper.querySelector('.xterm-viewport');
|
||||
if (!this._xtermViewport) {
|
||||
return;
|
||||
}
|
||||
const mutationObserver = new MutationObserver(() => this._refreshHeight());
|
||||
mutationObserver.observe(this._xtermViewport, { attributes: true, attributeFilter: ['style'] });
|
||||
}
|
||||
|
||||
public showMessage(left: number, y: number, text: IMarkdownString, verticalAlignment: WidgetVerticalAlignment = WidgetVerticalAlignment.Bottom): void {
|
||||
if (!this._container) {
|
||||
return;
|
||||
}
|
||||
dispose(this._messageWidget);
|
||||
this._messageListeners.clear();
|
||||
this._messageWidget = new MessageWidget(this._container, left, y, text, verticalAlignment, this._openerService);
|
||||
}
|
||||
|
||||
public closeMessage(): void {
|
||||
this._messageListeners.clear();
|
||||
setTimeout(() => {
|
||||
if (this._messageWidget && !this._messageWidget.mouseOver) {
|
||||
this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget));
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private _refreshHeight(): void {
|
||||
if (!this._container || !this._xtermViewport) {
|
||||
return;
|
||||
}
|
||||
this._container.style.height = this._xtermViewport.style.height;
|
||||
}
|
||||
}
|
||||
|
||||
class MessageWidget {
|
||||
private _domNode: HTMLElement;
|
||||
private _mouseOver = false;
|
||||
private readonly _messageListeners = new DisposableStore();
|
||||
|
||||
public get left(): number { return this._left; }
|
||||
public get y(): number { return this._y; }
|
||||
public get text(): IMarkdownString { return this._text; }
|
||||
public get domNode(): HTMLElement { return this._domNode; }
|
||||
public get verticalAlignment(): WidgetVerticalAlignment { return this._verticalAlignment; }
|
||||
public get mouseOver(): boolean { return this._mouseOver; }
|
||||
|
||||
public static fadeOut(messageWidget: MessageWidget): IDisposable {
|
||||
let handle: any;
|
||||
const dispose = () => {
|
||||
messageWidget.dispose();
|
||||
clearTimeout(handle);
|
||||
messageWidget.domNode.removeEventListener('animationend', dispose);
|
||||
};
|
||||
handle = setTimeout(dispose, 110);
|
||||
messageWidget.domNode.addEventListener('animationend', dispose);
|
||||
messageWidget.domNode.classList.add('fadeOut');
|
||||
return { dispose };
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
private _left: number,
|
||||
private _y: number,
|
||||
private _text: IMarkdownString,
|
||||
private _verticalAlignment: WidgetVerticalAlignment,
|
||||
private readonly _openerService: IOpenerService
|
||||
) {
|
||||
this._domNode = renderMarkdown(this._text, {
|
||||
actionHandler: {
|
||||
callback: this._handleLinkClicked.bind(this),
|
||||
disposeables: this._messageListeners
|
||||
}
|
||||
});
|
||||
this._domNode.style.position = 'absolute';
|
||||
this._domNode.style.left = `${_left}px`;
|
||||
|
||||
if (this.verticalAlignment === WidgetVerticalAlignment.Top) {
|
||||
// Y position is to the top of the widget
|
||||
this._domNode.style.bottom = `${Math.max(_y, WIDGET_HEIGHT) - WIDGET_HEIGHT}px`;
|
||||
} else {
|
||||
// Y position is to the bottom of the widget
|
||||
this._domNode.style.bottom = `${Math.min(_y, _container.offsetHeight - WIDGET_HEIGHT)}px`;
|
||||
}
|
||||
|
||||
this._domNode.classList.add('terminal-message-widget', 'fadeIn');
|
||||
this._domNode.addEventListener('mouseenter', () => {
|
||||
this._mouseOver = true;
|
||||
});
|
||||
|
||||
this._domNode.addEventListener('mouseleave', () => {
|
||||
this._mouseOver = false;
|
||||
this._messageListeners.add(MessageWidget.fadeOut(this));
|
||||
});
|
||||
|
||||
this._container.appendChild(this._domNode);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.domNode.parentElement === this._container) {
|
||||
this._container.removeChild(this.domNode);
|
||||
}
|
||||
|
||||
this._messageListeners.dispose();
|
||||
}
|
||||
|
||||
private _handleLinkClicked(content: string) {
|
||||
this._openerService.open(URI.parse(content));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { getDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { ITerminalWidget, IHoverTarget, IHoverAnchor, HorizontalAnchorSide, VerticalAnchorSide } from 'vs/workbench/contrib/terminal/browser/widgets/widgets';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { HoverWidget } from 'vs/workbench/contrib/terminal/browser/widgets/hoverWidget';
|
||||
|
||||
export class EnvironmentVariableInfoWidget extends Widget implements ITerminalWidget {
|
||||
readonly id = 'env-var-info';
|
||||
|
||||
private _domNode: HTMLElement | undefined;
|
||||
private _container: HTMLElement | undefined;
|
||||
private _hoverWidget: HoverWidget | undefined;
|
||||
|
||||
get requiresAction() { return this._info.requiresAction; }
|
||||
|
||||
constructor(
|
||||
private _info: IEnvironmentVariableInfo,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement): void {
|
||||
this._container = container;
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.classList.add('terminal-env-var-info', 'codicon', `codicon-${this._info.getIcon()}`);
|
||||
container.appendChild(this._domNode);
|
||||
this.onmouseover(this._domNode, () => this._showHover());
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._domNode?.parentElement?.removeChild(this._domNode);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._showHover();
|
||||
this._hoverWidget?.focus();
|
||||
}
|
||||
|
||||
private _showHover() {
|
||||
if (!this._domNode || !this._container || this._hoverWidget) {
|
||||
return;
|
||||
}
|
||||
const target = new ElementHoverTarget(this._domNode);
|
||||
const actions = this._info.getActions ? this._info.getActions() : undefined;
|
||||
this._hoverWidget = this._instantiationService.createInstance(HoverWidget, this._container, target, new MarkdownString(this._info.getInfo()), () => { }, actions);
|
||||
this._register(this._hoverWidget);
|
||||
this._register(this._hoverWidget.onDispose(() => this._hoverWidget = undefined));
|
||||
}
|
||||
}
|
||||
|
||||
class ElementHoverTarget implements IHoverTarget {
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
|
||||
constructor(
|
||||
private _element: HTMLElement
|
||||
) {
|
||||
this.targetElements = [this._element];
|
||||
}
|
||||
|
||||
get anchor(): IHoverAnchor {
|
||||
const position = getDomNodePagePosition(this._element);
|
||||
return {
|
||||
x: position.left,
|
||||
horizontalAnchorSide: HorizontalAnchorSide.Left,
|
||||
y: document.documentElement.clientHeight - position.top - 1,
|
||||
verticalAnchorSide: VerticalAnchorSide.Bottom,
|
||||
fallbackY: position.top + position.height
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
}
|
||||
235
src/vs/workbench/contrib/terminal/browser/widgets/hoverWidget.ts
Normal file
235
src/vs/workbench/contrib/terminal/browser/widgets/hoverWidget.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorHoverHighlight, editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IHoverTarget, HorizontalAnchorSide, VerticalAnchorSide } from 'vs/workbench/contrib/terminal/browser/widgets/widgets';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class HoverWidget extends Widget {
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly _messageListeners = new DisposableStore();
|
||||
private readonly _mouseTracker: CompositeMouseTracker;
|
||||
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
get isDisposed(): boolean { return this._isDisposed; }
|
||||
get domNode(): HTMLElement { return this._domNode; }
|
||||
|
||||
private readonly _onDispose = new Emitter<void>();
|
||||
get onDispose(): Event<void> { return this._onDispose.event; }
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
private _target: IHoverTarget,
|
||||
private _text: IMarkdownString,
|
||||
private _linkHandler: (url: string) => void,
|
||||
private _actions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }[] | undefined,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService
|
||||
) {
|
||||
super();
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.classList.add('terminal-hover-widget', 'fadeIn', 'monaco-editor-hover', 'xterm-hover');
|
||||
this._domNode.tabIndex = 0;
|
||||
this._domNode.setAttribute('role', 'tooltip');
|
||||
|
||||
// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will
|
||||
// not be selected.
|
||||
this.onmousedown(this._domNode, e => e.stopPropagation());
|
||||
|
||||
// Hide hover on escape
|
||||
this.onkeydown(this._domNode, e => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
const rowElement = $('div.hover-row.markdown-hover');
|
||||
const contentsElement = $('div.hover-contents');
|
||||
const markdownElement = renderMarkdown(this._text, {
|
||||
actionHandler: {
|
||||
callback: (content) => this._linkHandler(content),
|
||||
disposeables: this._messageListeners
|
||||
}
|
||||
});
|
||||
contentsElement.appendChild(markdownElement);
|
||||
rowElement.appendChild(contentsElement);
|
||||
this._domNode.appendChild(rowElement);
|
||||
|
||||
if (this._actions && this._actions.length > 0) {
|
||||
const statusBarElement = $('div.hover-row.status-bar');
|
||||
const actionsElement = $('div.actions');
|
||||
this._actions.forEach(action => this._renderAction(actionsElement, action));
|
||||
statusBarElement.appendChild(actionsElement);
|
||||
this._domNode.appendChild(statusBarElement);
|
||||
}
|
||||
|
||||
this._mouseTracker = new CompositeMouseTracker([this._domNode, ..._target.targetElements]);
|
||||
this._register(this._mouseTracker.onMouseOut(() => this.dispose()));
|
||||
this._register(this._mouseTracker);
|
||||
|
||||
this._container.appendChild(this._domNode);
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private _renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable {
|
||||
const actionContainer = dom.append(parent, $('div.action-container'));
|
||||
const action = dom.append(actionContainer, $('a.action'));
|
||||
action.setAttribute('href', '#');
|
||||
action.setAttribute('role', 'button');
|
||||
if (actionOptions.iconClass) {
|
||||
dom.append(action, $(`span.icon.${actionOptions.iconClass}`));
|
||||
}
|
||||
const label = dom.append(action, $('span'));
|
||||
const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId);
|
||||
const keybindingLabel = keybinding ? keybinding.getLabel() : null;
|
||||
label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
|
||||
return dom.addDisposableListener(actionContainer, dom.EventType.CLICK, e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
actionOptions.run(actionContainer);
|
||||
});
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
const anchor = this._target.anchor;
|
||||
|
||||
if (anchor.horizontalAnchorSide === HorizontalAnchorSide.Left) {
|
||||
if (anchor.x + this._domNode.clientWidth * 0.75 > document.documentElement.clientWidth) {
|
||||
// Shift the hover to the left when > 25% would be cut off
|
||||
const width = Math.round(this._domNode.clientWidth * 0.75);
|
||||
this._domNode.style.width = `${width - 1}px`;
|
||||
this._domNode.style.maxWidth = '';
|
||||
this._domNode.style.left = `${document.documentElement.clientWidth - width - 1}px`;
|
||||
} else {
|
||||
this._domNode.style.width = '';
|
||||
this._domNode.style.maxWidth = `${document.documentElement.clientWidth - anchor.x - 1}px`;
|
||||
this._domNode.style.left = `${anchor.x}px`;
|
||||
}
|
||||
} else {
|
||||
this._domNode.style.right = `${anchor.x}px`;
|
||||
}
|
||||
// Use fallback y value if there is not enough vertical space
|
||||
if (anchor.verticalAnchorSide === VerticalAnchorSide.Bottom) {
|
||||
if (anchor.y + this._domNode.clientHeight > document.documentElement.clientHeight) {
|
||||
this._domNode.style.top = `${anchor.fallbackY}px`;
|
||||
} else {
|
||||
this._domNode.style.bottom = `${anchor.y}px`;
|
||||
}
|
||||
} else {
|
||||
if (anchor.y + this._domNode.clientHeight > document.documentElement.clientHeight) {
|
||||
this._domNode.style.bottom = `${anchor.fallbackY}px`;
|
||||
} else {
|
||||
this._domNode.style.top = `${anchor.y}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this._domNode.focus();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (!this._isDisposed) {
|
||||
this._onDispose.fire();
|
||||
this._domNode.parentElement?.removeChild(this.domNode);
|
||||
this._messageListeners.dispose();
|
||||
this._target.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class CompositeMouseTracker extends Widget {
|
||||
private _isMouseIn: boolean = false;
|
||||
private _mouseTimeout: number | undefined;
|
||||
|
||||
private readonly _onMouseOut = new Emitter<void>();
|
||||
get onMouseOut(): Event<void> { return this._onMouseOut.event; }
|
||||
|
||||
constructor(
|
||||
private _elements: HTMLElement[]
|
||||
) {
|
||||
super();
|
||||
this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver()));
|
||||
this._elements.forEach(n => this.onnonbubblingmouseout(n, () => this._onTargetMouseOut()));
|
||||
}
|
||||
|
||||
private _onTargetMouseOver(): void {
|
||||
this._isMouseIn = true;
|
||||
this._clearEvaluateMouseStateTimeout();
|
||||
}
|
||||
|
||||
private _onTargetMouseOut(): void {
|
||||
this._isMouseIn = false;
|
||||
this._evaluateMouseState();
|
||||
}
|
||||
|
||||
private _evaluateMouseState(): void {
|
||||
this._clearEvaluateMouseStateTimeout();
|
||||
// Evaluate whether the mouse is still outside asynchronously such that other mouse targets
|
||||
// have the opportunity to first their mouse in event.
|
||||
this._mouseTimeout = window.setTimeout(() => this._fireIfMouseOutside(), 0);
|
||||
}
|
||||
|
||||
private _clearEvaluateMouseStateTimeout(): void {
|
||||
if (this._mouseTimeout) {
|
||||
clearTimeout(this._mouseTimeout);
|
||||
this._mouseTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _fireIfMouseOutside(): void {
|
||||
if (!this._isMouseIn) {
|
||||
this._onMouseOut.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const editorHoverHighlightColor = theme.getColor(editorHoverHighlight);
|
||||
if (editorHoverHighlightColor) {
|
||||
collector.addRule(`.integrated-terminal .hoverHighlight { background-color: ${editorHoverHighlightColor}; }`);
|
||||
}
|
||||
const hoverBackground = theme.getColor(editorHoverBackground);
|
||||
if (hoverBackground) {
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover { background-color: ${hoverBackground}; }`);
|
||||
}
|
||||
const hoverBorder = theme.getColor(editorHoverBorder);
|
||||
if (hoverBorder) {
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover { border: 1px solid ${hoverBorder}; }`);
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
}
|
||||
const link = theme.getColor(textLinkForeground);
|
||||
if (link) {
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover a { color: ${link}; }`);
|
||||
}
|
||||
const hoverForeground = theme.getColor(editorHoverForeground);
|
||||
if (hoverForeground) {
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover { color: ${hoverForeground}; }`);
|
||||
}
|
||||
const actionsBackground = theme.getColor(editorHoverStatusBarBackground);
|
||||
if (actionsBackground) {
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover .hover-row .actions { background-color: ${actionsBackground}; }`);
|
||||
}
|
||||
const codeBackground = theme.getColor(textCodeBlockBackground);
|
||||
if (codeBackground) {
|
||||
collector.addRule(`.integrated-terminal .monaco-editor-hover code { background-color: ${codeBackground}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { ITerminalWidget, IHoverAnchor, IHoverTarget, HorizontalAnchorSide, VerticalAnchorSide } from 'vs/workbench/contrib/terminal/browser/widgets/widgets';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { HoverWidget } from 'vs/workbench/contrib/terminal/browser/widgets/hoverWidget';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IViewportRange } from 'xterm';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export interface ILinkHoverTargetOptions {
|
||||
readonly viewportRange: IViewportRange;
|
||||
readonly cellDimensions: { width: number, height: number };
|
||||
readonly terminalDimensions: { width: number, height: number };
|
||||
readonly modifierDownCallback?: () => void;
|
||||
readonly modifierUpCallback?: () => void;
|
||||
}
|
||||
|
||||
export class TerminalHover extends Disposable implements ITerminalWidget {
|
||||
readonly id = 'hover';
|
||||
|
||||
constructor(
|
||||
private readonly _targetOptions: ILinkHoverTargetOptions,
|
||||
private readonly _text: IMarkdownString,
|
||||
private readonly _linkHandler: (url: string) => void,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement): void {
|
||||
const target = new CellHoverTarget(container, this._targetOptions);
|
||||
this._register(this._instantiationService.createInstance(HoverWidget, container, target, this._text, this._linkHandler, []));
|
||||
}
|
||||
}
|
||||
|
||||
class CellHoverTarget extends Widget implements IHoverTarget {
|
||||
private _domNode: HTMLElement;
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
|
||||
constructor(
|
||||
private readonly _container: HTMLElement,
|
||||
o: ILinkHoverTargetOptions
|
||||
) {
|
||||
super();
|
||||
|
||||
this._domNode = $('div.terminal-hover-targets');
|
||||
const targets: HTMLElement[] = [];
|
||||
const rowCount = o.viewportRange.end.y - o.viewportRange.start.y + 1;
|
||||
|
||||
// Add top target row
|
||||
const width = (o.viewportRange.end.y > o.viewportRange.start.y ? o.terminalDimensions.width - o.viewportRange.start.x : o.viewportRange.end.x - o.viewportRange.start.x + 1) * o.cellDimensions.width;
|
||||
const topTarget = $('div.terminal-hover-target.hoverHighlight');
|
||||
topTarget.style.left = `${o.viewportRange.start.x * o.cellDimensions.width}px`;
|
||||
topTarget.style.bottom = `${(o.terminalDimensions.height - o.viewportRange.start.y - 1) * o.cellDimensions.height}px`;
|
||||
topTarget.style.width = `${width}px`;
|
||||
topTarget.style.height = `${o.cellDimensions.height}px`;
|
||||
targets.push(this._domNode.appendChild(topTarget));
|
||||
|
||||
// Add middle target rows
|
||||
if (rowCount > 2) {
|
||||
const middleTarget = $('div.terminal-hover-target.hoverHighlight');
|
||||
middleTarget.style.left = `0px`;
|
||||
middleTarget.style.bottom = `${(o.terminalDimensions.height - o.viewportRange.start.y - 1 - (rowCount - 2)) * o.cellDimensions.height}px`;
|
||||
middleTarget.style.width = `${o.terminalDimensions.width * o.cellDimensions.width}px`;
|
||||
middleTarget.style.height = `${(rowCount - 2) * o.cellDimensions.height}px`;
|
||||
targets.push(this._domNode.appendChild(middleTarget));
|
||||
}
|
||||
|
||||
// Add bottom target row
|
||||
if (rowCount > 1) {
|
||||
const bottomTarget = $('div.terminal-hover-target.hoverHighlight');
|
||||
bottomTarget.style.left = `0px`;
|
||||
bottomTarget.style.bottom = `${(o.terminalDimensions.height - o.viewportRange.end.y - 1) * o.cellDimensions.height}px`;
|
||||
bottomTarget.style.width = `${(o.viewportRange.end.x + 1) * o.cellDimensions.width}px`;
|
||||
bottomTarget.style.height = `${o.cellDimensions.height}px`;
|
||||
targets.push(this._domNode.appendChild(bottomTarget));
|
||||
}
|
||||
|
||||
this.targetElements = targets;
|
||||
|
||||
if (o.modifierDownCallback && o.modifierUpCallback) {
|
||||
let down = false;
|
||||
this._register(dom.addDisposableListener(document, 'keydown', e => {
|
||||
if (e.ctrlKey && !down) {
|
||||
down = true;
|
||||
o.modifierDownCallback!();
|
||||
}
|
||||
}));
|
||||
this._register(dom.addDisposableListener(document, 'keyup', e => {
|
||||
if (!e.ctrlKey) {
|
||||
down = false;
|
||||
o.modifierUpCallback!();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._container.appendChild(this._domNode);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (!this._isDisposed) {
|
||||
this._container.removeChild(this._domNode);
|
||||
}
|
||||
this._isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
get anchor(): IHoverAnchor {
|
||||
const firstPosition = dom.getDomNodePagePosition(this.targetElements[0]);
|
||||
return {
|
||||
x: firstPosition.left,
|
||||
horizontalAnchorSide: HorizontalAnchorSide.Left,
|
||||
y: document.documentElement.clientHeight - firstPosition.top - 1,
|
||||
verticalAnchorSide: VerticalAnchorSide.Bottom,
|
||||
fallbackY: firstPosition.top + firstPosition.height - 1
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets';
|
||||
|
||||
export class TerminalWidgetManager implements IDisposable {
|
||||
private _container: HTMLElement | undefined;
|
||||
private _attached: Map<string, ITerminalWidget> = new Map();
|
||||
|
||||
attachToElement(terminalWrapper: HTMLElement) {
|
||||
if (!this._container) {
|
||||
this._container = document.createElement('div');
|
||||
this._container.classList.add('terminal-widget-container');
|
||||
terminalWrapper.appendChild(this._container);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._container && this._container.parentElement) {
|
||||
this._container.parentElement.removeChild(this._container);
|
||||
this._container = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
attachWidget(widget: ITerminalWidget): IDisposable | undefined {
|
||||
if (!this._container) {
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-check
|
||||
}
|
||||
this._attached.get(widget.id)?.dispose();
|
||||
widget.attach(this._container);
|
||||
this._attached.set(widget.id, widget);
|
||||
return {
|
||||
dispose: () => {
|
||||
const current = this._attached.get(widget.id);
|
||||
if (current === widget) {
|
||||
this._attached.delete(widget.id);
|
||||
widget.dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
43
src/vs/workbench/contrib/terminal/browser/widgets/widgets.ts
Normal file
43
src/vs/workbench/contrib/terminal/browser/widgets/widgets.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface ITerminalWidget extends IDisposable {
|
||||
/**
|
||||
* Only one widget of each ID can be displayed at once.
|
||||
*/
|
||||
id: string;
|
||||
attach(container: HTMLElement): void;
|
||||
}
|
||||
|
||||
export enum HorizontalAnchorSide {
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
export enum VerticalAnchorSide {
|
||||
Top,
|
||||
Bottom
|
||||
}
|
||||
|
||||
export interface IHoverAnchor {
|
||||
x: number;
|
||||
y: number;
|
||||
horizontalAnchorSide: HorizontalAnchorSide;
|
||||
verticalAnchorSide: VerticalAnchorSide;
|
||||
/**
|
||||
* Fallback Y value to try with opposite VerticalAlignment if the hover does not fit vertically.
|
||||
*/
|
||||
fallbackY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A target for a hover which can know about domain-specific locations.
|
||||
*/
|
||||
export interface IHoverTarget extends IDisposable {
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
readonly anchor: IHoverAnchor;
|
||||
}
|
||||
@@ -51,9 +51,10 @@ export interface IMergedEnvironmentVariableCollection {
|
||||
applyToProcessEnvironment(env: IProcessEnvironment): void;
|
||||
|
||||
/**
|
||||
* Generates a diff of this connection against another.
|
||||
* Generates a diff of this connection against another. Returns undefined if the collections are
|
||||
* the same.
|
||||
*/
|
||||
diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff;
|
||||
diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,3 +98,10 @@ export interface IEnvironmentVariableService {
|
||||
* Third: Type
|
||||
*/
|
||||
export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][];
|
||||
|
||||
export interface IEnvironmentVariableInfo {
|
||||
readonly requiresAction: boolean;
|
||||
getInfo(): string;
|
||||
getIcon(): string;
|
||||
getActions?(): { label: string, iconClass?: string, run: () => void, commandId: string }[];
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa
|
||||
});
|
||||
}
|
||||
|
||||
diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff {
|
||||
diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined {
|
||||
const added: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
|
||||
const changed: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
|
||||
const removed: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
|
||||
@@ -97,6 +97,10 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa
|
||||
}
|
||||
});
|
||||
|
||||
if (added.size === 0 && changed.size === 0 && removed.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { added, changed, removed };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { IOpenFileRequest } from 'vs/platform/windows/common/windows';
|
||||
import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
|
||||
export const TERMINAL_VIEW_ID = 'workbench.panel.terminal';
|
||||
|
||||
@@ -124,6 +125,7 @@ export interface ITerminalConfiguration {
|
||||
osx: { [key: string]: string };
|
||||
windows: { [key: string]: string };
|
||||
};
|
||||
environmentChangesIndicator: 'off' | 'on' | 'warnonly';
|
||||
showExitAlert: boolean;
|
||||
splitCwd: 'workspaceRoot' | 'initial' | 'inherited';
|
||||
windowsEnableConpty: boolean;
|
||||
@@ -131,6 +133,7 @@ export interface ITerminalConfiguration {
|
||||
experimentalUseTitleEvent: boolean;
|
||||
enableFileLinks: boolean;
|
||||
unicodeVersion: '6' | '11';
|
||||
experimentalLinkProvider: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminalConfigHelper {
|
||||
@@ -293,6 +296,7 @@ export interface ITerminalProcessManager extends IDisposable {
|
||||
readonly remoteAuthority: string | undefined;
|
||||
readonly os: OperatingSystem | undefined;
|
||||
readonly userHome: string | undefined;
|
||||
readonly environmentVariableInfo: IEnvironmentVariableInfo | undefined;
|
||||
|
||||
readonly onProcessReady: Event<void>;
|
||||
readonly onBeforeProcessData: Event<IBeforeProcessDataEvent>;
|
||||
@@ -301,6 +305,7 @@ export interface ITerminalProcessManager extends IDisposable {
|
||||
readonly onProcessExit: Event<number | undefined>;
|
||||
readonly onProcessOverrideDimensions: Event<ITerminalDimensions | undefined>;
|
||||
readonly onProcessResolvedShellLaunchConfig: Event<IShellLaunchConfig>;
|
||||
readonly onEnvironmentVariableInfoChanged: Event<IEnvironmentVariableInfo>;
|
||||
|
||||
dispose(immediate?: boolean): void;
|
||||
createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, isScreenReaderModeEnabled: boolean): Promise<void>;
|
||||
@@ -441,6 +446,7 @@ export const enum TERMINAL_COMMAND_ID {
|
||||
NEW_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.newInActiveWorkspace',
|
||||
SPLIT = 'workbench.action.terminal.split',
|
||||
SPLIT_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.splitInActiveWorkspace',
|
||||
RELAUNCH = 'workbench.action.terminal.relaunch',
|
||||
FOCUS_PREVIOUS_PANE = 'workbench.action.terminal.focusPreviousPane',
|
||||
FOCUS_NEXT_PANE = 'workbench.action.terminal.focusNextPane',
|
||||
RESIZE_PANE_LEFT = 'workbench.action.terminal.resizePaneLeft',
|
||||
@@ -482,5 +488,127 @@ export const enum TERMINAL_COMMAND_ID {
|
||||
TOGGLE_FIND_CASE_SENSITIVE = 'workbench.action.terminal.toggleFindCaseSensitive',
|
||||
NAVIGATION_MODE_EXIT = 'workbench.action.terminal.navigationModeExit',
|
||||
NAVIGATION_MODE_FOCUS_NEXT = 'workbench.action.terminal.navigationModeFocusNext',
|
||||
NAVIGATION_MODE_FOCUS_PREVIOUS = 'workbench.action.terminal.navigationModeFocusPrevious'
|
||||
NAVIGATION_MODE_FOCUS_PREVIOUS = 'workbench.action.terminal.navigationModeFocusPrevious',
|
||||
SHOW_ENVIRONMENT_INFORMATION = 'workbench.action.terminal.showEnvironmentInformation'
|
||||
}
|
||||
|
||||
export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [
|
||||
TERMINAL_COMMAND_ID.CLEAR_SELECTION,
|
||||
TERMINAL_COMMAND_ID.CLEAR,
|
||||
TERMINAL_COMMAND_ID.COPY_SELECTION,
|
||||
TERMINAL_COMMAND_ID.DELETE_TO_LINE_START,
|
||||
TERMINAL_COMMAND_ID.DELETE_WORD_LEFT,
|
||||
TERMINAL_COMMAND_ID.DELETE_WORD_RIGHT,
|
||||
TERMINAL_COMMAND_ID.FIND_WIDGET_FOCUS,
|
||||
TERMINAL_COMMAND_ID.FIND_WIDGET_HIDE,
|
||||
TERMINAL_COMMAND_ID.FIND_NEXT,
|
||||
TERMINAL_COMMAND_ID.FIND_PREVIOUS,
|
||||
TERMINAL_COMMAND_ID.TOGGLE_FIND_REGEX,
|
||||
TERMINAL_COMMAND_ID.TOGGLE_FIND_WHOLE_WORD,
|
||||
TERMINAL_COMMAND_ID.TOGGLE_FIND_CASE_SENSITIVE,
|
||||
TERMINAL_COMMAND_ID.FOCUS_NEXT_PANE,
|
||||
TERMINAL_COMMAND_ID.FOCUS_NEXT,
|
||||
TERMINAL_COMMAND_ID.FOCUS_PREVIOUS_PANE,
|
||||
TERMINAL_COMMAND_ID.FOCUS_PREVIOUS,
|
||||
TERMINAL_COMMAND_ID.FOCUS,
|
||||
TERMINAL_COMMAND_ID.KILL,
|
||||
TERMINAL_COMMAND_ID.MOVE_TO_LINE_END,
|
||||
TERMINAL_COMMAND_ID.MOVE_TO_LINE_START,
|
||||
TERMINAL_COMMAND_ID.NEW_IN_ACTIVE_WORKSPACE,
|
||||
TERMINAL_COMMAND_ID.NEW,
|
||||
TERMINAL_COMMAND_ID.PASTE,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_DOWN,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_LEFT,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_RIGHT,
|
||||
TERMINAL_COMMAND_ID.RESIZE_PANE_UP,
|
||||
TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE,
|
||||
TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT,
|
||||
TERMINAL_COMMAND_ID.SCROLL_DOWN_LINE,
|
||||
TERMINAL_COMMAND_ID.SCROLL_DOWN_PAGE,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_BOTTOM,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SCROLL_TO_TOP,
|
||||
TERMINAL_COMMAND_ID.SCROLL_UP_LINE,
|
||||
TERMINAL_COMMAND_ID.SCROLL_UP_PAGE,
|
||||
TERMINAL_COMMAND_ID.SEND_SEQUENCE,
|
||||
TERMINAL_COMMAND_ID.SELECT_ALL,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND,
|
||||
TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE,
|
||||
TERMINAL_COMMAND_ID.SPLIT_IN_ACTIVE_WORKSPACE,
|
||||
TERMINAL_COMMAND_ID.SPLIT,
|
||||
TERMINAL_COMMAND_ID.TOGGLE,
|
||||
TERMINAL_COMMAND_ID.NAVIGATION_MODE_EXIT,
|
||||
TERMINAL_COMMAND_ID.NAVIGATION_MODE_FOCUS_NEXT,
|
||||
TERMINAL_COMMAND_ID.NAVIGATION_MODE_FOCUS_PREVIOUS,
|
||||
'editor.action.toggleTabFocusMode',
|
||||
'workbench.action.quickOpen',
|
||||
'workbench.action.quickOpenPreviousEditor',
|
||||
'workbench.action.showCommands',
|
||||
'workbench.action.tasks.build',
|
||||
'workbench.action.tasks.restartTask',
|
||||
'workbench.action.tasks.runTask',
|
||||
'workbench.action.tasks.reRunTask',
|
||||
'workbench.action.tasks.showLog',
|
||||
'workbench.action.tasks.showTasks',
|
||||
'workbench.action.tasks.terminate',
|
||||
'workbench.action.tasks.test',
|
||||
'workbench.action.toggleFullScreen',
|
||||
'workbench.action.terminal.focusAtIndex1',
|
||||
'workbench.action.terminal.focusAtIndex2',
|
||||
'workbench.action.terminal.focusAtIndex3',
|
||||
'workbench.action.terminal.focusAtIndex4',
|
||||
'workbench.action.terminal.focusAtIndex5',
|
||||
'workbench.action.terminal.focusAtIndex6',
|
||||
'workbench.action.terminal.focusAtIndex7',
|
||||
'workbench.action.terminal.focusAtIndex8',
|
||||
'workbench.action.terminal.focusAtIndex9',
|
||||
'workbench.action.focusSecondEditorGroup',
|
||||
'workbench.action.focusThirdEditorGroup',
|
||||
'workbench.action.focusFourthEditorGroup',
|
||||
'workbench.action.focusFifthEditorGroup',
|
||||
'workbench.action.focusSixthEditorGroup',
|
||||
'workbench.action.focusSeventhEditorGroup',
|
||||
'workbench.action.focusEighthEditorGroup',
|
||||
'workbench.action.focusNextPart',
|
||||
'workbench.action.focusPreviousPart',
|
||||
'workbench.action.nextPanelView',
|
||||
'workbench.action.previousPanelView',
|
||||
'workbench.action.nextSideBarView',
|
||||
'workbench.action.previousSideBarView',
|
||||
'workbench.action.debug.start',
|
||||
'workbench.action.debug.stop',
|
||||
'workbench.action.debug.run',
|
||||
'workbench.action.debug.restart',
|
||||
'workbench.action.debug.continue',
|
||||
'workbench.action.debug.pause',
|
||||
'workbench.action.debug.stepInto',
|
||||
'workbench.action.debug.stepOut',
|
||||
'workbench.action.debug.stepOver',
|
||||
'workbench.action.nextEditor',
|
||||
'workbench.action.previousEditor',
|
||||
'workbench.action.nextEditorInGroup',
|
||||
'workbench.action.previousEditorInGroup',
|
||||
'workbench.action.openNextRecentlyUsedEditor',
|
||||
'workbench.action.openPreviousRecentlyUsedEditor',
|
||||
'workbench.action.openNextRecentlyUsedEditorInGroup',
|
||||
'workbench.action.openPreviousRecentlyUsedEditorInGroup',
|
||||
'workbench.action.quickOpenPreviousRecentlyUsedEditor',
|
||||
'workbench.action.quickOpenLeastRecentlyUsedEditor',
|
||||
'workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup',
|
||||
'workbench.action.quickOpenLeastRecentlyUsedEditorInGroup',
|
||||
'workbench.action.focusActiveEditorGroup',
|
||||
'workbench.action.focusFirstEditorGroup',
|
||||
'workbench.action.focusLastEditorGroup',
|
||||
'workbench.action.firstEditorInGroup',
|
||||
'workbench.action.lastEditorInGroup',
|
||||
'workbench.action.navigateUp',
|
||||
'workbench.action.navigateDown',
|
||||
'workbench.action.navigateRight',
|
||||
'workbench.action.navigateLeft',
|
||||
'workbench.action.togglePanel',
|
||||
'workbench.action.quickOpenView',
|
||||
'workbench.action.toggleMaximizedPanel'
|
||||
];
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
|
||||
import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { isMacintosh, isWindows, Platform } from 'vs/base/common/platform';
|
||||
|
||||
export const terminalConfiguration: IConfigurationNode = {
|
||||
id: 'terminal',
|
||||
order: 100,
|
||||
title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'terminal.integrated.automationShell.linux': {
|
||||
markdownDescription: localize('terminal.integrated.automationShell.linux', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`'),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.automationShell.osx': {
|
||||
markdownDescription: localize('terminal.integrated.automationShell.osx', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`'),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.automationShell.windows': {
|
||||
markdownDescription: localize('terminal.integrated.automationShell.windows', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`'),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.shellArgs.linux': {
|
||||
markdownDescription: localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
default: []
|
||||
},
|
||||
'terminal.integrated.shellArgs.osx': {
|
||||
markdownDescription: localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
// Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This
|
||||
// is the reason terminals on macOS typically run login shells by default which set up
|
||||
// the environment. See http://unix.stackexchange.com/a/119675/115410
|
||||
default: ['-l']
|
||||
},
|
||||
'terminal.integrated.shellArgs.windows': {
|
||||
markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
'anyOf': [
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
markdownDescription: localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")
|
||||
}
|
||||
],
|
||||
default: []
|
||||
},
|
||||
'terminal.integrated.macOptionIsMeta': {
|
||||
description: localize('terminal.integrated.macOptionIsMeta', "Controls whether to treat the option key as the meta key in the terminal on macOS."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.macOptionClickForcesSelection': {
|
||||
description: localize('terminal.integrated.macOptionClickForcesSelection', "Controls whether to force selection when using Option+click on macOS. This will force a regular (line) selection and disallow the use of column selection mode. This enables copying and pasting using the regular terminal selection, for example, when mouse mode is enabled in tmux."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.copyOnSelection': {
|
||||
description: localize('terminal.integrated.copyOnSelection', "Controls whether text selected in the terminal will be copied to the clipboard."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.drawBoldTextInBrightColors': {
|
||||
description: localize('terminal.integrated.drawBoldTextInBrightColors', "Controls whether bold text in the terminal will always use the \"bright\" ANSI color variant."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.fontFamily': {
|
||||
markdownDescription: localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."),
|
||||
type: 'string'
|
||||
},
|
||||
// TODO: Support font ligatures
|
||||
// 'terminal.integrated.fontLigatures': {
|
||||
// 'description': localize('terminal.integrated.fontLigatures', "Controls whether font ligatures are enabled in the terminal."),
|
||||
// 'type': 'boolean',
|
||||
// 'default': false
|
||||
// },
|
||||
'terminal.integrated.fontSize': {
|
||||
description: localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."),
|
||||
type: 'number',
|
||||
default: EDITOR_FONT_DEFAULTS.fontSize
|
||||
},
|
||||
'terminal.integrated.letterSpacing': {
|
||||
description: localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."),
|
||||
type: 'number',
|
||||
default: DEFAULT_LETTER_SPACING
|
||||
},
|
||||
'terminal.integrated.lineHeight': {
|
||||
description: localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."),
|
||||
type: 'number',
|
||||
default: DEFAULT_LINE_HEIGHT
|
||||
},
|
||||
'terminal.integrated.minimumContrastRatio': {
|
||||
markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."),
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
'terminal.integrated.fastScrollSensitivity': {
|
||||
markdownDescription: localize('terminal.integrated.fastScrollSensitivity', "Scrolling speed multiplier when pressing `Alt`."),
|
||||
type: 'number',
|
||||
default: 5
|
||||
},
|
||||
'terminal.integrated.mouseWheelScrollSensitivity': {
|
||||
markdownDescription: localize('terminal.integrated.mouseWheelScrollSensitivity', "A multiplier to be used on the `deltaY` of mouse wheel scroll events."),
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
'terminal.integrated.fontWeight': {
|
||||
type: 'string',
|
||||
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
description: localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."),
|
||||
default: 'normal'
|
||||
},
|
||||
'terminal.integrated.fontWeightBold': {
|
||||
type: 'string',
|
||||
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
description: localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."),
|
||||
default: 'bold'
|
||||
},
|
||||
'terminal.integrated.cursorBlinking': {
|
||||
description: localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.cursorStyle': {
|
||||
description: localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."),
|
||||
enum: [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE],
|
||||
default: TerminalCursorStyle.BLOCK
|
||||
},
|
||||
'terminal.integrated.cursorWidth': {
|
||||
markdownDescription: localize('terminal.integrated.cursorWidth', "Controls the width of the cursor when `#terminal.integrated.cursorStyle#` is set to `line`."),
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
'terminal.integrated.scrollback': {
|
||||
description: localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."),
|
||||
type: 'number',
|
||||
default: 1000
|
||||
},
|
||||
'terminal.integrated.detectLocale': {
|
||||
markdownDescription: localize('terminal.integrated.detectLocale', "Controls whether to detect and set the `$LANG` environment variable to a UTF-8 compliant option since VS Code's terminal only supports UTF-8 encoded data coming from the shell."),
|
||||
type: 'string',
|
||||
enum: ['auto', 'off', 'on'],
|
||||
markdownEnumDescriptions: [
|
||||
localize('terminal.integrated.detectLocale.auto', "Set the `$LANG` environment variable if the existing variable does not exist or it does not end in `'.UTF-8'`."),
|
||||
localize('terminal.integrated.detectLocale.off', "Do not set the `$LANG` environment variable."),
|
||||
localize('terminal.integrated.detectLocale.on', "Always set the `$LANG` environment variable.")
|
||||
],
|
||||
default: 'auto'
|
||||
},
|
||||
'terminal.integrated.rendererType': {
|
||||
type: 'string',
|
||||
enum: ['auto', 'canvas', 'dom', 'experimentalWebgl'],
|
||||
markdownEnumDescriptions: [
|
||||
localize('terminal.integrated.rendererType.auto', "Let VS Code guess which renderer to use."),
|
||||
localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer."),
|
||||
localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer."),
|
||||
localize('terminal.integrated.rendererType.experimentalWebgl', "Use the experimental webgl-based renderer. Note that this has some [known issues](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) and this will only be enabled for new terminals (not hot swappable like the other renderers).")
|
||||
],
|
||||
default: 'auto',
|
||||
description: localize('terminal.integrated.rendererType', "Controls how the terminal is rendered.")
|
||||
},
|
||||
'terminal.integrated.rightClickBehavior': {
|
||||
type: 'string',
|
||||
enum: ['default', 'copyPaste', 'paste', 'selectWord'],
|
||||
enumDescriptions: [
|
||||
localize('terminal.integrated.rightClickBehavior.default', "Show the context menu."),
|
||||
localize('terminal.integrated.rightClickBehavior.copyPaste', "Copy when there is a selection, otherwise paste."),
|
||||
localize('terminal.integrated.rightClickBehavior.paste', "Paste on right click."),
|
||||
localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu.")
|
||||
],
|
||||
default: isMacintosh ? 'selectWord' : isWindows ? 'copyPaste' : 'default',
|
||||
description: localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click.")
|
||||
},
|
||||
'terminal.integrated.cwd': {
|
||||
description: localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."),
|
||||
type: 'string',
|
||||
default: undefined
|
||||
},
|
||||
'terminal.integrated.confirmOnExit': {
|
||||
description: localize('terminal.integrated.confirmOnExit', "Controls whether to confirm on exit if there are active terminal sessions."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.enableBell': {
|
||||
description: localize('terminal.integrated.enableBell', "Controls whether the terminal bell is enabled."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.commandsToSkipShell': {
|
||||
markdownDescription: localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
default: []
|
||||
},
|
||||
'terminal.integrated.allowChords': {
|
||||
markdownDescription: localize('terminal.integrated.allowChords', "Whether or not to allow chord keybindings in the terminal. Note that when this is true and the keystroke results in a chord it will bypass `#terminal.integrated.commandsToSkipShell#`, setting this to false is particularly useful when you want ctrl+k to go to your shell (not VS Code)."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.allowMnemonics': {
|
||||
markdownDescription: localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true. This does nothing on macOS."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.inheritEnv': {
|
||||
markdownDescription: localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from VS Code. This is not supported on Windows."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.env.osx': {
|
||||
markdownDescription: localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable."),
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
'terminal.integrated.env.linux': {
|
||||
markdownDescription: localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable."),
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
'terminal.integrated.env.windows': {
|
||||
markdownDescription: localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable."),
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
'terminal.integrated.environmentChangesIndicator': {
|
||||
markdownDescription: localize('terminal.integrated.environmentChangesIndicator', "Whether to display the environment changes indicator on each terminal which explains whether extensions have made, or want to make changes to the terminal's environment."),
|
||||
type: 'string',
|
||||
enum: ['off', 'on', 'warnonly'],
|
||||
enumDescriptions: [
|
||||
localize('terminal.integrated.environmentChangesIndicator.off', "Disable the indicator."),
|
||||
localize('terminal.integrated.environmentChangesIndicator.on', "Enable the indicator."),
|
||||
localize('terminal.integrated.environmentChangesIndicator.warnonly', "Only show the warning indicator when a terminal's environment is 'stale', not the information indicator that shows a terminal has had its environment modified by an extension."),
|
||||
],
|
||||
default: 'on'
|
||||
},
|
||||
'terminal.integrated.showExitAlert': {
|
||||
description: localize('terminal.integrated.showExitAlert', "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.splitCwd': {
|
||||
description: localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."),
|
||||
type: 'string',
|
||||
enum: ['workspaceRoot', 'initial', 'inherited'],
|
||||
enumDescriptions: [
|
||||
localize('terminal.integrated.splitCwd.workspaceRoot', "A new split terminal will use the workspace root as the working directory. In a multi-root workspace a choice for which root folder to use is offered."),
|
||||
localize('terminal.integrated.splitCwd.initial', "A new split terminal will use the working directory that the parent terminal started with."),
|
||||
localize('terminal.integrated.splitCwd.inherited', "On macOS and Linux, a new split terminal will use the working directory of the parent terminal. On Windows, this behaves the same as initial."),
|
||||
],
|
||||
default: 'inherited'
|
||||
},
|
||||
'terminal.integrated.windowsEnableConpty': {
|
||||
description: localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.wordSeparators': {
|
||||
description: localize('terminal.integrated.wordSeparators', "A string containing all characters to be considered word separators by the double click to select word feature."),
|
||||
type: 'string',
|
||||
default: ' ()[]{}\',"`─'
|
||||
},
|
||||
'terminal.integrated.experimentalUseTitleEvent': {
|
||||
description: localize('terminal.integrated.experimentalUseTitleEvent', "An experimental setting that will use the terminal title event for the dropdown title. This setting will only apply to new terminals."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'terminal.integrated.enableFileLinks': {
|
||||
description: localize('terminal.integrated.enableFileLinks', "Whether to enable file links in the terminal. Links can be slow when working on a network drive in particular because each file link is verified against the file system. Changing this will take effect only in new terminals."),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
'terminal.integrated.unicodeVersion': {
|
||||
type: 'string',
|
||||
enum: ['6', '11'],
|
||||
enumDescriptions: [
|
||||
localize('terminal.integrated.unicodeVersion.six', "Version 6 of unicode, this is an older version which should work better on older systems."),
|
||||
localize('terminal.integrated.unicodeVersion.eleven', "Version 11 of unicode, this version provides better support on modern systems that use modern versions of unicode.")
|
||||
],
|
||||
default: '11',
|
||||
description: localize('terminal.integrated.unicodeVersion', "Controls what version of unicode to use when evaluating the width of characters in the terminal. If you experience emoji or other wide characters not taking up the right amount of space or backspace either deleting too much or too little then you may want to try tweaking this setting.")
|
||||
},
|
||||
'terminal.integrated.experimentalLinkProvider': {
|
||||
description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function getTerminalShellConfiguration(getSystemShell?: (p: Platform) => string): IConfigurationNode {
|
||||
return {
|
||||
id: 'terminal',
|
||||
order: 100,
|
||||
title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'terminal.integrated.shell.linux': {
|
||||
markdownDescription:
|
||||
getSystemShell
|
||||
? localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Linux))
|
||||
: localize('terminal.integrated.shell.linux.noDefault', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.shell.osx': {
|
||||
markdownDescription:
|
||||
getSystemShell
|
||||
? localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Mac))
|
||||
: localize('terminal.integrated.shell.osx.noDefault', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.shell.windows': {
|
||||
markdownDescription:
|
||||
getSystemShell
|
||||
? localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Windows))
|
||||
: localize('terminal.integrated.shell.windows.noDefault', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -176,7 +176,7 @@ export function getLangEnvVariable(locale?: string): string {
|
||||
|
||||
export function getCwd(
|
||||
shell: IShellLaunchConfig,
|
||||
userHome: string,
|
||||
userHome: string | undefined,
|
||||
lastActiveWorkspace: IWorkspaceFolder | undefined,
|
||||
configurationResolverService: IConfigurationResolverService | undefined,
|
||||
root: Uri | undefined,
|
||||
@@ -206,7 +206,7 @@ export function getCwd(
|
||||
|
||||
// If there was no custom cwd or it was relative with no workspace
|
||||
if (!cwd) {
|
||||
cwd = root ? root.fsPath : userHome;
|
||||
cwd = root ? root.fsPath : userHome || '';
|
||||
}
|
||||
|
||||
return _sanitizeCwd(cwd);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Platform } from 'vs/base/common/platform';
|
||||
|
||||
export function registerShellConfiguration(getSystemShell?: (p: Platform) => string): void {
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'terminal',
|
||||
order: 100,
|
||||
title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'terminal.integrated.shell.linux': {
|
||||
markdownDescription:
|
||||
getSystemShell
|
||||
? nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Linux))
|
||||
: nls.localize('terminal.integrated.shell.linux.noDefault', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.shell.osx': {
|
||||
markdownDescription:
|
||||
getSystemShell
|
||||
? nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Mac))
|
||||
: nls.localize('terminal.integrated.shell.osx.noDefault', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
},
|
||||
'terminal.integrated.shell.windows': {
|
||||
markdownDescription:
|
||||
getSystemShell
|
||||
? nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Windows))
|
||||
: nls.localize('terminal.integrated.shell.windows.noDefault', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
|
||||
type: ['string', 'null'],
|
||||
default: null
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7,10 +7,18 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/electron-browser/terminalInstanceService';
|
||||
import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal';
|
||||
import { registerShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalShellConfig';
|
||||
import { TerminalNativeService } from 'vs/workbench/contrib/terminal/electron-browser/terminalNativeService';
|
||||
import { ITerminalNativeService } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration';
|
||||
|
||||
registerShellConfiguration(getSystemShell);
|
||||
// This file contains additional desktop-only contributions on top of those in browser/
|
||||
|
||||
// Register services
|
||||
registerSingleton(ITerminalNativeService, TerminalNativeService, true);
|
||||
registerSingleton(ITerminalInstanceService, TerminalInstanceService, true);
|
||||
|
||||
// Register configurations
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
configurationRegistry.registerConfiguration(getTerminalShellConfiguration(getSystemShell));
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal
|
||||
|
||||
export function registerRemoteContributions() {
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(CreateNewLocalTerminalAction, CreateNewLocalTerminalAction.ID, CreateNewLocalTerminalAction.LABEL), 'Terminal: Create New Integrated Terminal (Local)', TERMINAL_ACTION_CATEGORY);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(CreateNewLocalTerminalAction), 'Terminal: Create New Integrated Terminal (Local)', TERMINAL_ACTION_CATEGORY);
|
||||
}
|
||||
|
||||
export class CreateNewLocalTerminalAction extends Action {
|
||||
|
||||
@@ -125,7 +125,7 @@ async function detectAvailableWindowsShells(): Promise<IShellDefinition[]> {
|
||||
// `${process.env['HOMEDRIVE']}\\cygwin\\bin\\bash.exe`
|
||||
// ]
|
||||
};
|
||||
const promises: PromiseLike<IShellDefinition | undefined>[] = [];
|
||||
const promises: Promise<IShellDefinition | undefined>[] = [];
|
||||
Object.keys(expectedLocations).forEach(key => promises.push(validateShellPaths(key, expectedLocations[key])));
|
||||
const shells = await Promise.all(promises);
|
||||
return coalesce(shells);
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IBufferLine, IBufferCell } from 'xterm';
|
||||
import { convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
|
||||
suite('Workbench - Terminal Link Helpers', () => {
|
||||
suite('convertLinkRangeToBuffer', () => {
|
||||
test('should convert ranges for ascii characters', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'AA http://t', width: 11 },
|
||||
{ text: '.com/f/', width: 8 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4, y: 1 },
|
||||
end: { x: 7, y: 2 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for wide characters before the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'A文 http://', width: 11 },
|
||||
{ text: 't.com/f/', width: 9 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4 + 1, y: 1 },
|
||||
end: { x: 7 + 1, y: 2 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for combining characters before the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'A🙂 http://', width: 11 },
|
||||
{ text: 't.com/f/', width: 9 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4 + 1, startLineNumber: 1, endColumn: 19 + 1, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4, y: 1 },
|
||||
end: { x: 7, y: 2 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for wide characters inside the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'AA http://t', width: 11 },
|
||||
{ text: '.com/文/', width: 8 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4, y: 1 },
|
||||
end: { x: 7 + 1, y: 2 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for wide characters before and inside the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'A文 http://', width: 11 },
|
||||
{ text: 't.com/文/', width: 9 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4 + 1, y: 1 },
|
||||
end: { x: 7 + 2, y: 2 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for emoji before before and wide inside the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'A🙂 http://', width: 11 },
|
||||
{ text: 't.com/文/', width: 9 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4 + 1, startLineNumber: 1, endColumn: 19 + 1, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4, y: 1 },
|
||||
end: { x: 7 + 1, y: 2 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for ascii characters (link starts on wrapped)', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'AAAAAAAAAAA', width: 11 },
|
||||
{ text: 'AA http://t', width: 11 },
|
||||
{ text: '.com/f/', width: 8 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4, y: 2 },
|
||||
end: { x: 7, y: 3 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for wide characters before the link (link starts on wrapped)', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'AAAAAAAAAAA', width: 11 },
|
||||
{ text: 'A文 http://', width: 11 },
|
||||
{ text: 't.com/f/', width: 9 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4 + 1, y: 2 },
|
||||
end: { x: 7 + 1, y: 3 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for wide characters inside the link (link starts on wrapped)', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'AAAAAAAAAAA', width: 11 },
|
||||
{ text: 'AA http://t', width: 11 },
|
||||
{ text: '.com/文/', width: 8 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4, y: 2 },
|
||||
end: { x: 7 + 1, y: 3 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for wide characters before and inside the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'AAAAAAAAAAA', width: 11 },
|
||||
{ text: 'A文 http://', width: 11 },
|
||||
{ text: 't.com/文/', width: 9 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4 + 1, y: 2 },
|
||||
end: { x: 7 + 2, y: 3 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for several wide characters before the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'A文文AAAAAA', width: 11 },
|
||||
{ text: 'AA文文 http', width: 11 },
|
||||
{ text: '://t.com/f/', width: 11 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
|
||||
// This test ensures that the start offset is applies to the end before it's counted
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4 + 4, y: 2 },
|
||||
end: { x: 7 + 4, y: 3 }
|
||||
});
|
||||
});
|
||||
test('should convert ranges for several wide characters before and inside the link', () => {
|
||||
const lines = createBufferLineArray([
|
||||
{ text: 'A文文AAAAAA', width: 11 },
|
||||
{ text: 'AA文文 http', width: 11 },
|
||||
{ text: '://t.com/文', width: 11 },
|
||||
{ text: '文/', width: 3 }
|
||||
]);
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 31, endLineNumber: 1 }, 0);
|
||||
// This test ensures that the start offset is applies to the end before it's counted
|
||||
assert.deepEqual(bufferRange, {
|
||||
start: { x: 4 + 4, y: 2 },
|
||||
end: { x: 2, y: 4 }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const TEST_WIDE_CHAR = '文';
|
||||
const TEST_NULL_CHAR = 'C';
|
||||
|
||||
function createBufferLineArray(lines: { text: string, width: number }[]): IBufferLine[] {
|
||||
let result: IBufferLine[] = [];
|
||||
lines.forEach((l, i) => {
|
||||
result.push(new TestBufferLine(
|
||||
l.text,
|
||||
l.width,
|
||||
i + 1 !== lines.length
|
||||
));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
class TestBufferLine implements IBufferLine {
|
||||
constructor(
|
||||
private _text: string,
|
||||
public length: number,
|
||||
public isWrapped: boolean
|
||||
) {
|
||||
|
||||
}
|
||||
getCell(x: number): IBufferCell | undefined {
|
||||
// Create a fake line of cells and use that to resolve the width
|
||||
let cells: string[] = [];
|
||||
let wideNullCellOffset = 0; // There is no null 0 width char after a wide char
|
||||
let emojiOffset = 0; // Skip chars as emoji are multiple characters
|
||||
for (let i = 0; i <= x - wideNullCellOffset + emojiOffset; i++) {
|
||||
let char = this._text.charAt(i);
|
||||
if (char === '\ud83d') {
|
||||
// Make "🙂"
|
||||
char += '\ude42';
|
||||
}
|
||||
cells.push(char);
|
||||
if (this._text.charAt(i) === TEST_WIDE_CHAR) {
|
||||
// Skip the next character as it's width is 0
|
||||
cells.push(TEST_NULL_CHAR);
|
||||
wideNullCellOffset++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
getChars: () => {
|
||||
return x >= cells.length ? '' : cells[x];
|
||||
},
|
||||
getWidth: () => {
|
||||
switch (cells[x]) {
|
||||
case TEST_WIDE_CHAR: return 2;
|
||||
case TEST_NULL_CHAR: return 0;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
}
|
||||
translateToString(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,19 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { TerminalLinkHandler, LineColumnInfo, XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
|
||||
import { TerminalLinkManager, LineColumnInfo, XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { TestPathService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
class TestTerminalLinkHandler extends TerminalLinkHandler {
|
||||
class TestTerminalLinkManager extends TerminalLinkManager {
|
||||
public get localLinkRegex(): RegExp {
|
||||
return this._localLinkRegex;
|
||||
}
|
||||
@@ -29,8 +34,8 @@ class TestTerminalLinkHandler extends TerminalLinkHandler {
|
||||
return true;
|
||||
}
|
||||
public wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
|
||||
TerminalLinkHandler._LINK_INTERCEPT_THRESHOLD = 0;
|
||||
return this._wrapLinkHandler(handler);
|
||||
TerminalLinkManager._LINK_INTERCEPT_THRESHOLD = 0;
|
||||
return this._wrapLinkHandler((_, link) => handler(link));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +89,22 @@ const testConfigHelper: ITerminalConfigHelper = <any>{
|
||||
};
|
||||
|
||||
suite('Workbench - TerminalLinkHandler', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IEnvironmentService, TestEnvironmentService);
|
||||
instantiationService.stub(IPathService, new TestPathService());
|
||||
instantiationService.stub(ITerminalInstanceService, new MockTerminalInstanceService());
|
||||
instantiationService.stub(IConfigurationService, new TestConfigurationService());
|
||||
});
|
||||
|
||||
suite('localLinkRegex', () => {
|
||||
test('Windows', () => {
|
||||
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const terminalLinkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm(), {
|
||||
os: OperatingSystem.Windows,
|
||||
userHome: ''
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
|
||||
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
|
||||
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
|
||||
@@ -109,6 +124,7 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
function generateAndTestLinks() {
|
||||
const linkUrls = [
|
||||
'c:\\foo',
|
||||
'\\\\?\\c:\\foo',
|
||||
'c:/foo',
|
||||
'.\\foo',
|
||||
'./foo',
|
||||
@@ -162,10 +178,10 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
});
|
||||
|
||||
test('Linux', () => {
|
||||
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const terminalLinkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm(), {
|
||||
os: OperatingSystem.Linux,
|
||||
userHome: ''
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
|
||||
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
|
||||
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
|
||||
@@ -230,10 +246,10 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
|
||||
suite('preprocessPath', () => {
|
||||
test('Windows', () => {
|
||||
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm() as any, {
|
||||
os: OperatingSystem.Windows,
|
||||
userHome: 'C:\\Users\\Me'
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
linkHandler.processCwd = 'C:\\base';
|
||||
|
||||
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\src\\file1');
|
||||
@@ -241,12 +257,13 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\Me\\src\\file3');
|
||||
assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\Me\\src\\file4');
|
||||
assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file5'), 'C:\\absolute\\path\\file5');
|
||||
assert.equal(linkHandler.preprocessPath('\\\\?\\C:\\absolute\\path\\extended\\file6'), 'C:\\absolute\\path\\extended\\file6');
|
||||
});
|
||||
test('Windows - spaces', () => {
|
||||
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm() as any, {
|
||||
os: OperatingSystem.Windows,
|
||||
userHome: 'C:\\Users\\M e'
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
linkHandler.processCwd = 'C:\\base dir';
|
||||
|
||||
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\src\\file1');
|
||||
@@ -257,10 +274,10 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
});
|
||||
|
||||
test('Linux', () => {
|
||||
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm() as any, {
|
||||
os: OperatingSystem.Linux,
|
||||
userHome: '/home/me'
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
linkHandler.processCwd = '/base';
|
||||
|
||||
assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/src/file1');
|
||||
@@ -270,10 +287,10 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
});
|
||||
|
||||
test('No Workspace', () => {
|
||||
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm() as any, {
|
||||
os: OperatingSystem.Linux,
|
||||
userHome: '/home/me'
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
|
||||
assert.equal(linkHandler.preprocessPath('./src/file1'), null);
|
||||
assert.equal(linkHandler.preprocessPath('src/file2'), null);
|
||||
@@ -284,10 +301,10 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
|
||||
test('gitDiffLinkRegex', () => {
|
||||
// The platform is irrelevant because the links generated by Git are the same format regardless of platform
|
||||
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm() as any, {
|
||||
os: OperatingSystem.Linux,
|
||||
userHome: ''
|
||||
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
|
||||
function assertAreGoodMatches(matches: RegExpMatchArray | null) {
|
||||
if (matches) {
|
||||
@@ -315,10 +332,10 @@ suite('Workbench - TerminalLinkHandler', () => {
|
||||
const nullMouseEvent: any = Object.freeze({ preventDefault: () => { } });
|
||||
|
||||
test('should allow intercepting of links with onBeforeHandleLink', async () => {
|
||||
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
|
||||
const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new TestXterm() as any, {
|
||||
os: OperatingSystem.Linux,
|
||||
userHome: ''
|
||||
} as any, testConfigHelper, null!, null!, new TestConfigurationService(), new MockTerminalInstanceService(), null!, null!);
|
||||
} as any, testConfigHelper);
|
||||
linkHandler.onBeforeHandleLink(e => {
|
||||
if (e.link === 'https://www.microsoft.com') {
|
||||
intercepted = true;
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider';
|
||||
import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
suite('Workbench - TerminalWebLinkProvider', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IConfigurationService, TestConfigurationService);
|
||||
});
|
||||
|
||||
async function assertLink(text: string, expected: { text: string, range: [number, number][] }) {
|
||||
const xterm = new Terminal();
|
||||
const provider = instantiationService.createInstance(TerminalProtocolLinkProvider, xterm, () => { }, () => { });
|
||||
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => xterm.write(text, r));
|
||||
|
||||
// Calculate positions just outside of link boundaries
|
||||
const noLinkPositions: IBufferCellPosition[] = [
|
||||
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
|
||||
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
|
||||
];
|
||||
|
||||
// Ensure outside positions do not detect the link
|
||||
for (let i = 0; i < noLinkPositions.length; i++) {
|
||||
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
|
||||
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`);
|
||||
}
|
||||
|
||||
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
|
||||
const linkRange: IBufferRange = {
|
||||
start: { x: expected.range[0][0], y: expected.range[0][1] },
|
||||
end: { x: expected.range[1][0], y: expected.range[1][1] },
|
||||
};
|
||||
|
||||
// Calculate positions inside the link boundaries
|
||||
const linkPositions: IBufferCellPosition[] = [
|
||||
linkRange.start,
|
||||
linkRange.end
|
||||
];
|
||||
|
||||
// Ensure inside positions do detect the link
|
||||
for (let i = 0; i < linkPositions.length; i++) {
|
||||
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
|
||||
assert.deepEqual(link?.text, expected.text);
|
||||
assert.deepEqual(link?.range, linkRange);
|
||||
}
|
||||
}
|
||||
|
||||
// These tests are based on LinkComputer.test.ts
|
||||
test('LinkComputer cases', async () => {
|
||||
await assertLink('x = "http://foo.bar";', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('x = (http://foo.bar);', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('x = \'http://foo.bar\';', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('x = http://foo.bar ;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('x = <http://foo.bar>;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('x = {http://foo.bar};', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('(see http://foo.bar)', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('[see http://foo.bar]', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('{see http://foo.bar}', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('<see http://foo.bar>', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('<url>http://foo.bar</url>', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
|
||||
await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', { range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' });
|
||||
await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', { range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' });
|
||||
await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', { range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' });
|
||||
await assertLink('<!-- !!! Do not remove !!! WebContentRef(link:https://go.microsoft.com/fwlink/?LinkId=166007, area:Admin, updated:2015, nextUpdate:2016, tags:SqlServer) !!! Do not remove !!! -->', { range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' });
|
||||
await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.</value>', { range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' });
|
||||
await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.</value>', { range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' });
|
||||
await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', { range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' });
|
||||
await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', { range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' });
|
||||
await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', { range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' });
|
||||
await assertLink('x = "file:///foo.bar";', { range: [[6, 1], [20, 1]], text: 'file:///foo.bar' });
|
||||
await assertLink('x = "file://c:/foo.bar";', { range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' });
|
||||
await assertLink('x = "file://shares/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' });
|
||||
await assertLink('x = "file://shäres/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' });
|
||||
await assertLink('Some text, then http://www.bing.com.', { range: [[17, 1], [35, 1]], text: 'http://www.bing.com' });
|
||||
await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', { range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' });
|
||||
await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', { range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' });
|
||||
await assertLink('let x = "http://[::1]:5000/connect/token"', { range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' });
|
||||
await assertLink('2. Navigate to **https://portal.azure.com**', { range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' });
|
||||
await assertLink('POST|https://portal.azure.com|2019-12-05|', { range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' });
|
||||
await assertLink('aa https://foo.bar/[this is foo site] aa', { range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TerminalValidatedLocalLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
|
||||
import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { format } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
|
||||
const unixLinks = [
|
||||
'/foo',
|
||||
'~/foo',
|
||||
'./foo',
|
||||
'../foo',
|
||||
'/foo/bar',
|
||||
'foo/bar'
|
||||
];
|
||||
|
||||
const windowsLinks = [
|
||||
'c:\\foo',
|
||||
'\\\\?\\c:\\foo',
|
||||
'c:/foo',
|
||||
'.\\foo',
|
||||
'./foo',
|
||||
'..\\foo',
|
||||
'~\\foo',
|
||||
'~/foo',
|
||||
'c:/foo/bar',
|
||||
'c:\\foo\\bar',
|
||||
'c:\\foo/bar\\baz',
|
||||
'foo/bar',
|
||||
'foo/bar',
|
||||
'foo\\bar'
|
||||
];
|
||||
|
||||
interface LinkFormatInfo {
|
||||
urlFormat: string;
|
||||
line?: string;
|
||||
column?: string;
|
||||
}
|
||||
|
||||
const supportedLinkFormats: LinkFormatInfo[] = [
|
||||
{ urlFormat: '{0}' },
|
||||
{ urlFormat: '{0} on line {1}', line: '5' },
|
||||
{ urlFormat: '{0} on line {1}, column {2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}:line {1}', line: '5' },
|
||||
{ urlFormat: '{0}:line {1}, column {2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}({1})', line: '5' },
|
||||
{ urlFormat: '{0} ({1})', line: '5' },
|
||||
{ urlFormat: '{0}({1},{2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} ({1},{2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}({1}, {2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} ({1}, {2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}:{1}', line: '5' },
|
||||
{ urlFormat: '{0}:{1}:{2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}[{1}]', line: '5' },
|
||||
{ urlFormat: '{0} [{1}]', line: '5' },
|
||||
{ urlFormat: '{0}[{1},{2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} [{1},{2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}[{1}, {2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} [{1}, {2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}",{1}', line: '5' }
|
||||
];
|
||||
|
||||
suite('Workbench - TerminalValidatedLocalLinkProvider', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IConfigurationService, TestConfigurationService);
|
||||
});
|
||||
|
||||
async function assertLink(text: string, os: OperatingSystem, expected: { text: string, range: [number, number][] }) {
|
||||
const xterm = new Terminal();
|
||||
const provider = instantiationService.createInstance(TerminalValidatedLocalLinkProvider, xterm, os, () => { }, () => { }, () => { }, (_: string, cb: (result: { uri: URI, isDirectory: boolean } | undefined) => void) => { cb({ uri: URI.file('/'), isDirectory: false }); });
|
||||
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => xterm.write(text, r));
|
||||
|
||||
// Calculate positions just outside of link boundaries
|
||||
const noLinkPositions: IBufferCellPosition[] = [
|
||||
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
|
||||
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
|
||||
];
|
||||
|
||||
// Ensure outside positions do not detect the link
|
||||
for (let i = 0; i < noLinkPositions.length; i++) {
|
||||
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
|
||||
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`);
|
||||
}
|
||||
|
||||
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
|
||||
const linkRange: IBufferRange = {
|
||||
start: { x: expected.range[0][0], y: expected.range[0][1] },
|
||||
end: { x: expected.range[1][0], y: expected.range[1][1] },
|
||||
};
|
||||
|
||||
// Calculate positions inside the link boundaries
|
||||
const linkPositions: IBufferCellPosition[] = [
|
||||
linkRange.start,
|
||||
linkRange.end
|
||||
];
|
||||
|
||||
// Ensure inside positions do detect the link
|
||||
for (let i = 0; i < linkPositions.length; i++) {
|
||||
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
|
||||
assert.deepEqual(link?.text, expected.text);
|
||||
assert.deepEqual(link?.range, linkRange);
|
||||
}
|
||||
}
|
||||
|
||||
suite('Linux/macOS', () => {
|
||||
unixLinks.forEach(baseLink => {
|
||||
suite(`Link: ${baseLink}`, () => {
|
||||
for (let i = 0; i < supportedLinkFormats.length; i++) {
|
||||
const linkFormat = supportedLinkFormats[i];
|
||||
test(`Format: ${linkFormat.urlFormat}`, async () => {
|
||||
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
|
||||
await assertLink(formattedLink, OperatingSystem.Linux, { text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] });
|
||||
await assertLink(` ${formattedLink} `, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
|
||||
await assertLink(`(${formattedLink})`, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
|
||||
await assertLink(`[${formattedLink}]`, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
test('Git diff links', async () => {
|
||||
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[14, 1], [20, 1]] });
|
||||
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[24, 1], [30, 1]] });
|
||||
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
|
||||
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
|
||||
});
|
||||
});
|
||||
|
||||
suite('Windows', () => {
|
||||
windowsLinks.forEach(baseLink => {
|
||||
suite(`Link "${baseLink}"`, () => {
|
||||
for (let i = 0; i < supportedLinkFormats.length; i++) {
|
||||
const linkFormat = supportedLinkFormats[i];
|
||||
test(`Format: ${linkFormat.urlFormat}`, async () => {
|
||||
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
|
||||
await assertLink(formattedLink, OperatingSystem.Windows, { text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] });
|
||||
await assertLink(` ${formattedLink} `, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
|
||||
await assertLink(`(${formattedLink})`, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
|
||||
await assertLink(`[${formattedLink}]`, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
test('Git diff links', async () => {
|
||||
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[14, 1], [20, 1]] });
|
||||
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[24, 1], [30, 1]] });
|
||||
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
|
||||
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
|
||||
import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
suite('Workbench - TerminalWordLinkProvider', () => {
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
let configurationService: TestConfigurationService;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
configurationService = new TestConfigurationService();
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
});
|
||||
|
||||
async function assertLink(text: string, expected: { text: string, range: [number, number][] }) {
|
||||
const xterm = new Terminal();
|
||||
const provider = instantiationService.createInstance(TerminalWordLinkProvider, xterm, () => { }, () => { }, () => { });
|
||||
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => xterm.write(text, r));
|
||||
|
||||
// Calculate positions just outside of link boundaries
|
||||
const noLinkPositions: IBufferCellPosition[] = [
|
||||
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
|
||||
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
|
||||
];
|
||||
|
||||
// Ensure outside positions do not detect the link
|
||||
for (let i = 0; i < noLinkPositions.length; i++) {
|
||||
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
|
||||
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`);
|
||||
}
|
||||
|
||||
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
|
||||
const linkRange: IBufferRange = {
|
||||
start: { x: expected.range[0][0], y: expected.range[0][1] },
|
||||
end: { x: expected.range[1][0], y: expected.range[1][1] },
|
||||
};
|
||||
|
||||
// Calculate positions inside the link boundaries
|
||||
const linkPositions: IBufferCellPosition[] = [
|
||||
linkRange.start,
|
||||
linkRange.end
|
||||
];
|
||||
|
||||
// Ensure inside positions do detect the link
|
||||
for (let i = 0; i < linkPositions.length; i++) {
|
||||
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
|
||||
assert.deepEqual(link?.text, expected.text);
|
||||
assert.deepEqual(link?.range, linkRange);
|
||||
}
|
||||
}
|
||||
|
||||
test('should link words as defined by wordSeparators', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ()[]' } });
|
||||
await assertLink('foo', { range: [[1, 1], [3, 1]], text: 'foo' });
|
||||
await assertLink(' foo ', { range: [[2, 1], [4, 1]], text: 'foo' });
|
||||
await assertLink('(foo)', { range: [[2, 1], [4, 1]], text: 'foo' });
|
||||
await assertLink('[foo]', { range: [[2, 1], [4, 1]], text: 'foo' });
|
||||
await assertLink('{foo}', { range: [[1, 1], [5, 1]], text: '{foo}' });
|
||||
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo', { range: [[1, 1], [3, 1]], text: 'foo' });
|
||||
await assertLink(' foo ', { range: [[2, 1], [4, 1]], text: 'foo' });
|
||||
await assertLink('(foo)', { range: [[1, 1], [5, 1]], text: '(foo)' });
|
||||
await assertLink('[foo]', { range: [[1, 1], [5, 1]], text: '[foo]' });
|
||||
await assertLink('{foo}', { range: [[1, 1], [5, 1]], text: '{foo}' });
|
||||
});
|
||||
});
|
||||
@@ -61,24 +61,24 @@ suite.skip('Workbench - TerminalCommandTracker', () => { // {{SQL CARBON EDIT}}
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await writePromise(xterm, `\r\n`);
|
||||
}
|
||||
assert.equal(xterm.buffer.baseY, 20);
|
||||
assert.equal(xterm.buffer.viewportY, 20);
|
||||
assert.equal(xterm.buffer.active.baseY, 20);
|
||||
assert.equal(xterm.buffer.active.viewportY, 20);
|
||||
|
||||
// Scroll to marker
|
||||
commandTracker.scrollToPreviousCommand();
|
||||
assert.equal(xterm.buffer.viewportY, 9);
|
||||
assert.equal(xterm.buffer.active.viewportY, 9);
|
||||
|
||||
// Scroll to top boundary
|
||||
commandTracker.scrollToPreviousCommand();
|
||||
assert.equal(xterm.buffer.viewportY, 0);
|
||||
assert.equal(xterm.buffer.active.viewportY, 0);
|
||||
|
||||
// Scroll to marker
|
||||
commandTracker.scrollToNextCommand();
|
||||
assert.equal(xterm.buffer.viewportY, 9);
|
||||
assert.equal(xterm.buffer.active.viewportY, 9);
|
||||
|
||||
// Scroll to bottom boundary
|
||||
commandTracker.scrollToNextCommand();
|
||||
assert.equal(xterm.buffer.viewportY, 20);
|
||||
assert.equal(xterm.buffer.active.viewportY, 20);
|
||||
});
|
||||
test('should select to the next and previous commands', async () => {
|
||||
(<any>window).matchMedia = () => {
|
||||
@@ -99,8 +99,8 @@ suite.skip('Workbench - TerminalCommandTracker', () => { // {{SQL CARBON EDIT}}
|
||||
assert.equal(xterm.markers[1].line, 11);
|
||||
await writePromise(xterm, '\n\r3');
|
||||
|
||||
assert.equal(xterm.buffer.baseY, 3);
|
||||
assert.equal(xterm.buffer.viewportY, 3);
|
||||
assert.equal(xterm.buffer.active.baseY, 3);
|
||||
assert.equal(xterm.buffer.active.viewportY, 3);
|
||||
|
||||
assert.equal(xterm.getSelection(), '');
|
||||
commandTracker.selectToPreviousCommand();
|
||||
@@ -133,8 +133,8 @@ suite.skip('Workbench - TerminalCommandTracker', () => { // {{SQL CARBON EDIT}}
|
||||
assert.equal(xterm.markers[1].line, 11);
|
||||
await writePromise(xterm, '\n\r3');
|
||||
|
||||
assert.equal(xterm.buffer.baseY, 3);
|
||||
assert.equal(xterm.buffer.viewportY, 3);
|
||||
assert.equal(xterm.buffer.active.baseY, 3);
|
||||
assert.equal(xterm.buffer.active.viewportY, 3);
|
||||
|
||||
assert.equal(xterm.getSelection(), '');
|
||||
commandTracker.selectToPreviousLine();
|
||||
|
||||
@@ -156,6 +156,24 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
});
|
||||
|
||||
suite('diff', () => {
|
||||
test('should return undefined when collectinos are the same', () => {
|
||||
const merged1 = new MergedEnvironmentVariableCollection(new Map([
|
||||
['ext1', {
|
||||
map: deserializeEnvironmentVariableCollection([
|
||||
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }]
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const merged2 = new MergedEnvironmentVariableCollection(new Map([
|
||||
['ext1', {
|
||||
map: deserializeEnvironmentVariableCollection([
|
||||
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }]
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
strictEqual(diff, undefined);
|
||||
});
|
||||
test('should generate added diffs from when the first entry is added', () => {
|
||||
const merged1 = new MergedEnvironmentVariableCollection(new Map([]));
|
||||
const merged2 = new MergedEnvironmentVariableCollection(new Map([
|
||||
@@ -165,7 +183,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
const diff = merged1.diff(merged2)!;
|
||||
strictEqual(diff.changed.size, 0);
|
||||
strictEqual(diff.removed.size, 0);
|
||||
const entries = [...diff.added.entries()];
|
||||
@@ -190,7 +208,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
const diff = merged1.diff(merged2)!;
|
||||
strictEqual(diff.changed.size, 0);
|
||||
strictEqual(diff.removed.size, 0);
|
||||
const entries = [...diff.added.entries()];
|
||||
@@ -220,7 +238,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
const diff = merged1.diff(merged2)!;
|
||||
strictEqual(diff.changed.size, 0);
|
||||
strictEqual(diff.removed.size, 0);
|
||||
deepStrictEqual([...diff.added.entries()], [
|
||||
@@ -240,13 +258,13 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff2 = merged1.diff(merged3);
|
||||
const diff2 = merged1.diff(merged3)!;
|
||||
strictEqual(diff2.changed.size, 0);
|
||||
strictEqual(diff2.removed.size, 0);
|
||||
deepStrictEqual([...diff.added.entries()], [...diff2.added.entries()], 'Swapping the order of the entries in the other collection should yield the same result');
|
||||
});
|
||||
|
||||
test('should remove entries in the diff that come after a Replce', () => {
|
||||
test('should remove entries in the diff that come after a Replace', () => {
|
||||
const merged1 = new MergedEnvironmentVariableCollection(new Map([
|
||||
['ext1', {
|
||||
map: deserializeEnvironmentVariableCollection([
|
||||
@@ -268,9 +286,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged4);
|
||||
strictEqual(diff.changed.size, 0);
|
||||
strictEqual(diff.removed.size, 0);
|
||||
deepStrictEqual([...diff.added.entries()], [], 'Replace should ignore any entries after it');
|
||||
strictEqual(diff, undefined, 'Replace should ignore any entries after it');
|
||||
});
|
||||
|
||||
test('should generate removed diffs', () => {
|
||||
@@ -289,7 +305,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
const diff = merged1.diff(merged2)!;
|
||||
strictEqual(diff.changed.size, 0);
|
||||
strictEqual(diff.added.size, 0);
|
||||
deepStrictEqual([...diff.removed.entries()], [
|
||||
@@ -314,7 +330,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
const diff = merged1.diff(merged2)!;
|
||||
strictEqual(diff.added.size, 0);
|
||||
strictEqual(diff.removed.size, 0);
|
||||
deepStrictEqual([...diff.changed.entries()], [
|
||||
@@ -340,7 +356,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
|
||||
])
|
||||
}]
|
||||
]));
|
||||
const diff = merged1.diff(merged2);
|
||||
const diff = merged1.diff(merged2)!;
|
||||
deepStrictEqual([...diff.added.entries()], [
|
||||
['C', [{ extensionIdentifier: 'ext1', value: 'c', type: EnvironmentVariableMutatorType.Append }]],
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user