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