|
| 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