Skip to content

Commit

Permalink
support links w relative cwd (#140676)
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge authored Jan 15, 2022
1 parent 7853127 commit 00b535c
Show file tree
Hide file tree
Showing 20 changed files with 402 additions and 144 deletions.
7 changes: 0 additions & 7 deletions src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,13 +371,6 @@ export interface IHeartbeatService {
readonly onBeat: Event<void>;
}

export interface TerminalCommand {
command: string;
timestamp: number;
getOutput: () => string | undefined;
cwd?: string;
exitCode?: number;
}

export interface IShellLaunchConfig {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { TerminalHover, ILinkHoverTargetOptions } from 'vs/workbench/contrib/ter
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
import { TerminalExternalLinkProviderAdapter } from 'vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { TerminalCapabilityStoreMultiplexer } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore';

export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => Promise<void>;
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
Expand All @@ -46,10 +48,11 @@ export class TerminalLinkManager extends DisposableStore {
private _processCwd: string | undefined;
private _standardLinkProviders: Map<string, ILinkProvider> = new Map();
private _linkProvidersDisposables: IDisposable[] = [];

private readonly _xterm: Terminal;
constructor(
private _xterm: Terminal,
private _xtermTerminal: XtermTerminal,
private readonly _processManager: ITerminalProcessManager,
private readonly _capabilities: TerminalCapabilityStoreMultiplexer,
@IOpenerService private readonly _openerService: IOpenerService,
@IEditorService private readonly _editorService: IEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
Expand All @@ -58,7 +61,7 @@ export class TerminalLinkManager extends DisposableStore {
@ITunnelService private readonly _tunnelService: ITunnelService
) {
super();

this._xterm = _xtermTerminal.raw;
// Protocol links
const wrappedActivateCallback = this._wrapLinkHandler((_, link) => this._handleProtocolLink(link));
const protocolProvider = this._instantiationService.createInstance(TerminalProtocolLinkProvider,
Expand Down Expand Up @@ -91,7 +94,7 @@ export class TerminalLinkManager extends DisposableStore {
}

// Word links
const wordProvider = this._instantiationService.createInstance(TerminalWordLinkProvider, this._xterm, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this));
const wordProvider = this._instantiationService.createInstance(TerminalWordLinkProvider, this._xtermTerminal, this._capabilities, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this));
this._standardLinkProviders.set(TerminalWordLinkProvider.id, wordProvider);

this._registerStandardLinkProviders();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { Terminal, IViewportRange, IBufferLine, IBufferRange } from 'xterm';
import type { IViewportRange, IBufferLine, IBufferRange } 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';
Expand All @@ -23,14 +23,19 @@ import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { TerminalCapability } from 'vs/platform/terminal/common/terminal';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';

const MAX_LENGTH = 2000;

export class TerminalWordLinkProvider extends TerminalBaseLinkProvider {
private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder);
static id: string = 'TerminalWordLinkProvider';

constructor(
private readonly _xterm: Terminal,
private _xterm: XtermTerminal,
private _capabilities: ITerminalCapabilityStore,
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,
Expand All @@ -40,7 +45,7 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider {
@ISearchService private readonly _searchService: ISearchService,
@IEditorService private readonly _editorService: IEditorService,
@IFileService private readonly _fileService: IFileService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
) {
super();
}
Expand All @@ -49,26 +54,26 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider {
// Dispose of all old links if new links are provides, links are only cached for the current line
const links: TerminalLink[] = [];
const wordSeparators = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).wordSeparators;
const activateCallback = this._wrapLinkHandler((_, link) => this._activate(link));
const activateCallback = this._wrapLinkHandler((_, link) => this._activate(link, y));

let startLine = y - 1;
let endLine = startLine;

const lines: IBufferLine[] = [
this._xterm.buffer.active.getLine(startLine)!
this._xterm.raw.buffer.active.getLine(startLine)!
];

while (startLine >= 0 && this._xterm.buffer.active.getLine(startLine)?.isWrapped) {
lines.unshift(this._xterm.buffer.active.getLine(startLine - 1)!);
while (startLine >= 0 && this._xterm.raw.buffer.active.getLine(startLine)?.isWrapped) {
lines.unshift(this._xterm.raw.buffer.active.getLine(startLine - 1)!);
startLine--;
}

while (endLine < this._xterm.buffer.active.length && this._xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
lines.push(this._xterm.buffer.active.getLine(endLine + 1)!);
while (endLine < this._xterm.raw.buffer.active.length && this._xterm.raw.buffer.active.getLine(endLine + 1)?.isWrapped) {
lines.push(this._xterm.raw.buffer.active.getLine(endLine + 1)!);
endLine++;
}

const text = getXtermLineContent(this._xterm.buffer.active, startLine, endLine, this._xterm.cols);
const text = getXtermLineContent(this._xterm.raw.buffer.active, startLine, endLine, this._xterm.raw.cols);
if (text === '' || text.length > MAX_LENGTH) {
return [];
}
Expand All @@ -82,7 +87,7 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider {
const bufferRange = convertLinkRangeToBuffer
(
lines,
this._xterm.cols,
this._xterm.raw.cols,
{
startColumn: word.startIndex + 1,
startLineNumber: 1,
Expand Down Expand Up @@ -123,58 +128,90 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider {
bufferRange.end.x--;
}
return this._instantiationService.createInstance(TerminalLink,
this._xterm,
this._xterm.raw,
bufferRange,
text,
this._xterm.buffer.active.viewportY,
this._xterm.raw.buffer.active.viewportY,
activateCallback,
this._tooltipCallback,
false,
localize('searchWorkspace', 'Search workspace')
);
}

private async _activate(link: string) {
private async _activate(link: string, y: number) {
const pathSeparator = (isWindows ? '\\' : '/');
// Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format
link = link.replace(/^file:\/\/\/?/, '');
link = normalize(link).replace(/^(\.+[\\/])+/, '');

// Remove `:in` from the end which is how Ruby outputs stack traces
link = link.replace(/:in$/, '');

// If any of the names of the folders in the workspace matches
// a prefix of the link, remove that prefix and continue
this._workspaceContextService.getWorkspace().folders.forEach((folder) => {
if (link.substr(0, folder.name.length + 1) === folder.name + (isWindows ? '\\' : '/')) {
if (link.substr(0, folder.name.length + 1) === folder.name + pathSeparator) {
link = link.substring(folder.name.length + 1);
return;
}
});

const sanitizedLink = link.replace(/:\d+(:\d+)?$/, '');
let exactMatch = await this._getExactMatch(sanitizedLink, link);
if (exactMatch) {
// If there was exactly one match, open it
const match = link.match(/:(\d+)?(:(\d+))?$/);
const startLineNumber = match?.[1];
const startColumn = match?.[3];
await this._editorService.openEditor({
resource: exactMatch,
options: {
pinned: true,
revealIfOpened: true,
selection: startLineNumber ? {
startLineNumber: parseInt(startLineNumber),
startColumn: startColumn ? parseInt(startColumn) : 0
} : undefined
}
});
return;
let matchLink = link;
if (this._capabilities.has(TerminalCapability.CwdDetection)) {
matchLink = this._updateLinkWithRelativeCwd(y, link, pathSeparator);
}
const sanitizedLink = matchLink.replace(/:\d+(:\d+)?$/, '');
try {
const exactMatch = await this._getExactMatch(sanitizedLink, matchLink);
if (exactMatch) {
// If there was exactly one match, open it
const match = matchLink.match(/:(\d+)?(:(\d+))?$/);
const startLineNumber = match?.[1];
const startColumn = match?.[3];
await this._editorService.openEditor({
resource: exactMatch,
options: {
pinned: true,
revealIfOpened: true,
selection: startLineNumber ? {
startLineNumber: parseInt(startLineNumber),
startColumn: startColumn ? parseInt(startColumn) : 0
} : undefined
}
});
return;
}
} catch {
// Fallback to searching quick access
return this._quickInputService.quickAccess.show(link);
}
// Fallback to searching quick access
return this._quickInputService.quickAccess.show(link);
}

/*
* For shells with the CwdDetection capability, the cwd relative to the line
* of the particular link is used to narrow down the result for an exact file match, if possible.
*/
private _updateLinkWithRelativeCwd(y: number, link: string, pathSeparator: string): string {
const cwd = this._xterm.commandTracker.getCwdForLine(y);
if (cwd && !link.includes(pathSeparator)) {
link = cwd + pathSeparator + link;
} else {
let commonDirs = 0;
let i = 0;
const cwdPath = cwd.split(pathSeparator).reverse();
const linkPath = link.split(pathSeparator);
while (i < cwdPath.length) {
if (cwdPath[i] === linkPath[i]) {
commonDirs++;
}
i++;
}
link = cwd + pathSeparator + linkPath.slice(commonDirs).join(pathSeparator);
}
return link;
}

private async _getExactMatch(sanitizedLink: string, link: string): Promise<URI | undefined> {
let exactResource: URI | undefined;
if (isAbsolute(sanitizedLink)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ command_complete() {
update_cwd
}

set_shell_integration_enabled() {
printf "\033]133;E\007"
}

update_prompt() {
PRIOR_PROMPT="$PS1"
IN_COMMAND_EXECUTION=""
Expand All @@ -50,8 +46,8 @@ preexec() {
IN_COMMAND_EXECUTION="1"
command_output_start
}

update_prompt
PROMPT_COMMAND=${PROMPT_COMMAND:+"$PROMPT_COMMAND; "}'precmd'
trap 'preexec' DEBUG
update_cwd
set_shell_integration_enabled
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ command_complete() {
update_cwd
}

set_shell_integration_enabled() {
printf "\033]133;E\007"
}

update_prompt() {
PRIOR_PROMPT="$PS1"
IN_COMMAND_EXECUTION=""
Expand Down Expand Up @@ -54,4 +50,3 @@ preexec() {
precmd_functions+=($precmd_functions precmd)
preexec_functions+=($preexec_functions preexec)
update_cwd
set_shell_integration_enabled
25 changes: 24 additions & 1 deletion src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal';
import { ICommandTracker, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy, IRegisterContributedProfileArgs, IShellIntegration } from 'vs/workbench/contrib/terminal/common/terminal';
import { INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy, IRegisterContributedProfileArgs, IShellIntegration } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList';
import { Orientation } from 'vs/base/browser/ui/splitview/splitview';
import { IEditableData } from 'vs/workbench/common/views';
import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { IMarker } from 'xterm';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';

export const ITerminalService = createDecorator<ITerminalService>('terminalService');
Expand Down Expand Up @@ -80,6 +81,28 @@ export interface IQuickPickTerminalObject {
keyMods: IKeyMods | undefined
}

export interface TerminalCommand {
command: string;
timestamp: number;
cwd?: string;
exitCode?: number;
marker?: IMarker;
getOutput(): string | undefined;
}

export interface ICommandTracker {
readonly commands: TerminalCommand[];
readonly cwds: string[];
scrollToPreviousCommand(): void;
scrollToNextCommand(): void;
selectToPreviousCommand(): void;
selectToNextCommand(): void;
selectToPreviousLine(): void;
selectToNextLine(): void;
getCwdForLine(line: number): string;
clearMarker(): void;
}

export interface ITerminalGroup {
activeInstance: ITerminalInstance | undefined;
terminalInstances: ITerminalInstance[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { getIconId, getColorClass, getUriClasses } from 'vs/workbench/contrib/te
export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500';
export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs");

async function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise<string | URI | undefined> {
export async function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise<string | URI | undefined> {
switch (configHelper.config.splitCwd) {
case 'workspaceRoot':
if (folders !== undefined && commandService !== undefined) {
Expand Down
Loading

0 comments on commit 00b535c

Please sign in to comment.