Skip to content

Commit b898fe1

Browse files
committed
Initial working multi-line terminal links
Part of microsoft#181837
1 parent ec8c12d commit b898fe1

File tree

6 files changed

+189
-8
lines changed

6 files changed

+189
-8
lines changed

src/vs/workbench/contrib/terminalContrib/links/browser/links.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
1313
import { ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal';
1414
import { Event } from 'vs/base/common/event';
1515
import { ITerminalBackend } from 'vs/platform/terminal/common/terminal';
16+
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
1617

1718
export const ITerminalLinkProviderService = createDecorator<ITerminalLinkProviderService>('terminalLinkProviderService');
1819
export interface ITerminalLinkProviderService {
@@ -82,6 +83,12 @@ export interface ITerminalSimpleLink {
8283
*/
8384
uri?: URI;
8485

86+
// TODO: This is similar to parsedLink, combine into a single prop?
87+
/**
88+
* The location or selection range of the link.
89+
*/
90+
selection?: ITextEditorSelection;
91+
8592
/**
8693
* A hover label to override the default for the type.
8794
*/

src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkDetectorAdapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProv
9797
}));
9898
}
9999

100+
console.log('provided links', links);
100101
return links;
101102
}
102103

src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type { ILink, ILinkProvider, IViewportRange, Terminal } from 'xterm';
3131
import { convertBufferRangeToViewport } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers';
3232
import { RunOnceScheduler } from 'vs/base/common/async';
3333
import { ITerminalLogService } from 'vs/platform/terminal/common/terminal';
34+
import { TerminalMultiLineLinkDetector } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalMultiLineLinkDetector';
3435

3536
export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => Promise<void>;
3637

@@ -74,6 +75,7 @@ export class TerminalLinkManager extends DisposableStore {
7475
this._setupLinkDetector(TerminalUriLinkDetector.id, this._instantiationService.createInstance(TerminalUriLinkDetector, this._xterm, this._processManager, this._linkResolver));
7576
if (enableFileLinks) {
7677
this._setupLinkDetector(TerminalLocalLinkDetector.id, this._instantiationService.createInstance(TerminalLocalLinkDetector, this._xterm, capabilities, this._processManager, this._linkResolver));
78+
this._setupLinkDetector(TerminalMultiLineLinkDetector.id, this._instantiationService.createInstance(TerminalMultiLineLinkDetector, this._xterm, capabilities, this._processManager, this._linkResolver));
7779
}
7880
this._setupLinkDetector(TerminalWordLinkDetector.id, this._instantiationService.createInstance(TerminalWordLinkDetector, this._xterm));
7981

src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener {
3636
throw new Error('Tried to open file link without a resolved URI');
3737
}
3838
const linkSuffix = link.parsedLink ? link.parsedLink.suffix : getLinkSuffix(link.text);
39-
const selection: ITextEditorSelection | undefined = linkSuffix?.row === undefined ? undefined : {
40-
startLineNumber: linkSuffix?.row ?? 1,
41-
startColumn: linkSuffix?.col ?? 1
42-
};
39+
let selection: ITextEditorSelection | undefined = link.selection;
40+
if (!selection) {
41+
selection = linkSuffix?.row === undefined ? undefined : {
42+
startLineNumber: linkSuffix.row ?? 1,
43+
startColumn: linkSuffix.col ?? 1
44+
};
45+
}
4346
await this._editorService.openEditor({
4447
resource: link.uri,
4548
options: { pinned: true, selection, revealIfOpened: true }

src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector {
9090

9191
const os = this._processManager.os || OS;
9292
const parsedLinks = detectLinks(text, os);
93-
this._logService.trace('terminalLocaLinkDetector#detect text', text);
94-
this._logService.trace('terminalLocaLinkDetector#detect parsedLinks', parsedLinks);
93+
this._logService.trace('terminalLocalLinkDetector#detect text', text);
94+
this._logService.trace('terminalLocalLinkDetector#detect parsedLinks', parsedLinks);
9595
for (const parsedLink of parsedLinks) {
9696

9797
// Don't try resolve any links of excessive length
@@ -153,7 +153,7 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector {
153153
}
154154
}
155155
linkCandidates.push(...specialEndLinkCandidates);
156-
this._logService.trace('terminalLocaLinkDetector#detect linkCandidates', linkCandidates);
156+
this._logService.trace('terminalLocalLinkDetector#detect linkCandidates', linkCandidates);
157157

158158
// Validate the path and convert to the outgoing type
159159
const simpleLink = await this._validateAndGetLink(undefined, bufferRange, linkCandidates, trimRangeMap);
@@ -163,7 +163,7 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector {
163163
parsedLink.prefix?.index ?? parsedLink.path.index,
164164
parsedLink.suffix ? parsedLink.suffix.suffix.index + parsedLink.suffix.suffix.text.length : parsedLink.path.index + parsedLink.path.text.length
165165
);
166-
this._logService.trace('terminalLocaLinkDetector#detect verified link', simpleLink);
166+
this._logService.trace('terminalLocalLinkDetector#detect verified link', simpleLink);
167167
links.push(simpleLink);
168168
}
169169

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { URI } from 'vs/base/common/uri';
7+
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
8+
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
9+
import { ITerminalLinkDetector, ITerminalLinkResolver, ITerminalSimpleLink, ResolvedLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/links/browser/links';
10+
import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers';
11+
import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
12+
import { IBufferLine, Terminal } from 'xterm';
13+
import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
14+
import { ITerminalBackend, ITerminalLogService } from 'vs/platform/terminal/common/terminal';
15+
16+
const enum Constants {
17+
/**
18+
* The max line length to try extract word links from.
19+
*/
20+
MaxLineLength = 2000,
21+
22+
/**
23+
* The maximum number of links in a line to resolve against the file system. This limit is put
24+
* in place to avoid sending excessive data when remote connections are in place.
25+
*/
26+
MaxResolvedLinksInLine = 10,
27+
28+
/**
29+
* The maximum length of a link to resolve against the file system. This limit is put in place
30+
* to avoid sending excessive data when remote connections are in place.
31+
*/
32+
MaxResolvedLinkLength = 1024,
33+
}
34+
35+
const candidateMatchers = [
36+
// Ripgrep:
37+
// 16:console.log(...)
38+
// 16: console.log(...)
39+
// Eslint:
40+
// 16:5 error ...
41+
/\s*(?<link>(?<line>\d+):(?<col>\d+)?)/
42+
];
43+
44+
export class TerminalMultiLineLinkDetector implements ITerminalLinkDetector {
45+
static id = 'multiline';
46+
47+
// This was chosen as a reasonable maximum line length given the tradeoff between performance
48+
// and how likely it is to encounter such a large line length. Some useful reference points:
49+
// - Window old max length: 260 ($MAX_PATH)
50+
// - Linux max length: 4096 ($PATH_MAX)
51+
readonly maxLinkLength = 500;
52+
53+
constructor(
54+
readonly xterm: Terminal,
55+
private readonly _capabilities: ITerminalCapabilityStore,
56+
private readonly _processManager: Pick<ITerminalProcessManager, 'initialCwd' | 'os' | 'remoteAuthority' | 'userHome'> & { backend?: Pick<ITerminalBackend, 'getWslPath'> },
57+
private readonly _linkResolver: ITerminalLinkResolver,
58+
@ITerminalLogService private readonly _logService: ITerminalLogService,
59+
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
60+
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
61+
) {
62+
}
63+
64+
async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise<ITerminalSimpleLink[]> {
65+
const links: ITerminalSimpleLink[] = [];
66+
67+
// Get the text representation of the wrapped line
68+
const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols);
69+
if (text === '' || text.length > Constants.MaxLineLength) {
70+
return [];
71+
}
72+
73+
let stringIndex = -1;
74+
75+
this._logService.trace('terminalMultiLineLinkDetector#detect text', text);
76+
77+
// Match against the fallback matchers which are mainly designed to catch paths with spaces
78+
// that aren't possible using the regular mechanism.
79+
for (const matcher of candidateMatchers) {
80+
const match = text.match(matcher);
81+
const group = match?.groups;
82+
if (!group) {
83+
continue;
84+
}
85+
const link = group?.link;
86+
const line = group?.line;
87+
const col = group?.col;
88+
if (!link || line === undefined) {
89+
continue;
90+
}
91+
92+
// Don't try resolve any links of excessive length
93+
if (link.length > Constants.MaxResolvedLinkLength) {
94+
continue;
95+
}
96+
97+
// TODO: Log more info?
98+
this._logService.trace('terminalMultiLineLinkDetector#detect candidates', link);
99+
100+
// Scan up looking for the first line that could be a path
101+
let possiblePath: string | undefined;
102+
for (let index = startLine - 1; index > 0; index--) {
103+
// TODO: Skip wrapped lines
104+
const text = getXtermLineContent(this.xterm.buffer.active, index, index, this.xterm.cols);
105+
if (!text.match(/^\s*\d/)) {
106+
possiblePath = text;
107+
break;
108+
}
109+
}
110+
if (!possiblePath) {
111+
continue;
112+
}
113+
114+
// Check if the first non-matching line is an absolute or relative link
115+
const linkStat = await this._linkResolver.resolveLink(this._processManager, possiblePath);
116+
if (linkStat) {
117+
let type: TerminalBuiltinLinkType;
118+
if (linkStat.isDirectory) {
119+
if (this._isDirectoryInsideWorkspace(linkStat.uri)) {
120+
type = TerminalBuiltinLinkType.LocalFolderInWorkspace;
121+
} else {
122+
type = TerminalBuiltinLinkType.LocalFolderOutsideWorkspace;
123+
}
124+
} else {
125+
type = TerminalBuiltinLinkType.LocalFile;
126+
}
127+
128+
// TODO: Extend link to whole line
129+
// Convert the link text's string index into a wrapped buffer range
130+
stringIndex = text.indexOf(link);
131+
const bufferRange = convertLinkRangeToBuffer(lines, this.xterm.cols, {
132+
startColumn: stringIndex + 1,
133+
startLineNumber: 1,
134+
endColumn: stringIndex + link.length + 1,
135+
endLineNumber: 1
136+
}, startLine);
137+
138+
const simpleLink: ITerminalSimpleLink = {
139+
text: link,
140+
uri: linkStat.uri,
141+
selection: {
142+
startLineNumber: parseInt(line),
143+
startColumn: col ? parseInt(col) : 0
144+
},
145+
bufferRange: bufferRange,
146+
type
147+
};
148+
this._logService.trace('terminalMultiLineLinkDetector#detect verified link', simpleLink);
149+
links.push(simpleLink);
150+
151+
// Break on the first match
152+
break;
153+
}
154+
}
155+
156+
return links;
157+
}
158+
159+
private _isDirectoryInsideWorkspace(uri: URI) {
160+
const folders = this._workspaceContextService.getWorkspace().folders;
161+
for (let i = 0; i < folders.length; i++) {
162+
if (this._uriIdentityService.extUri.isEqualOrParent(uri, folders[i].uri)) {
163+
return true;
164+
}
165+
}
166+
return false;
167+
}
168+
}

0 commit comments

Comments
 (0)