Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/linkify/common/filePathLinkifier.ts
13399 views
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 { FileType } from '../../../platform/filesystem/common/fileTypes';
7
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
8
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
9
import { hasDriveLetter } from '../../../util/vs/base/common/extpath';
10
import { Schemas } from '../../../util/vs/base/common/network';
11
import * as path from '../../../util/vs/base/common/path';
12
import { isWindows } from '../../../util/vs/base/common/platform';
13
import * as resources from '../../../util/vs/base/common/resources';
14
import { isUriComponents } from '../../../util/vs/base/common/uri';
15
import { Uri } from '../../../vscodeTypes';
16
import { coalesceParts, LinkifiedPart, LinkifiedText, LinkifyLocationAnchor } from './linkifiedText';
17
import { IContributedLinkifier, LinkifierContext } from './linkifyService';
18
import { IStatCache } from './statCache';
19
20
// Create a single regex which runs different regexp parts in a big `|` expression.
21
const pathMatchRe = new RegExp(
22
[
23
// Inline code paths (exclude code-like characters $, {, }, that are common in code but rare in filenames)
24
/(?<!\[)`(?<inlineCodePath>[^`\s${}]+)`(?!\])/.source,
25
26
// File paths rendered as plain text (exclude code-like characters)
27
/(?<![\[`()<])(?<plainTextPath>[^\s`*${}()]+\.[^\s`*${}()]+)(?![\]`])/.source
28
].join('|'),
29
'gu');
30
31
/**
32
* Linkifies file paths in responses. This includes:
33
*
34
* ```
35
* `file.md`
36
* foo.ts
37
* ```
38
*/
39
export class FilePathLinkifier implements IContributedLinkifier {
40
41
constructor(
42
private readonly workspaceService: IWorkspaceService,
43
private readonly statCache: IStatCache,
44
) { }
45
46
async linkify(text: string, context: LinkifierContext, token: CancellationToken): Promise<LinkifiedText> {
47
const parts: Array<Promise<LinkifiedPart> | LinkifiedPart> = [];
48
49
let endLastMatch = 0;
50
for (const match of text.matchAll(pathMatchRe)) {
51
const prefix = text.slice(endLastMatch, match.index);
52
if (prefix) {
53
parts.push(prefix);
54
}
55
56
const matched = match[0];
57
58
const pathText = match.groups?.['inlineCodePath'] ?? match.groups?.['plainTextPath'] ?? '';
59
60
parts.push(this.resolvePathText(pathText, context)
61
.then(uri => {
62
if (uri) {
63
return new LinkifyLocationAnchor(uri);
64
}
65
return matched;
66
}));
67
68
endLastMatch = match.index + matched.length;
69
}
70
71
const suffix = text.slice(endLastMatch);
72
if (suffix) {
73
parts.push(suffix);
74
}
75
76
return { parts: coalesceParts(await Promise.all(parts)) };
77
}
78
79
private async resolvePathText(pathText: string, context: LinkifierContext): Promise<Uri | undefined> {
80
const includeDirectorySlash = pathText.endsWith('/');
81
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
82
83
// Don't linkify very short paths such as '/' or special paths such as '../'
84
if (pathText.length < 2 || ['../', '..\\', '/.', './', '\\.', '..'].includes(pathText)) {
85
return;
86
}
87
88
if (pathText.startsWith('/') || (isWindows && (pathText.startsWith('\\') || hasDriveLetter(pathText)))) {
89
try {
90
const uri = await this.statAndNormalizeUri(Uri.file(pathText.startsWith('/') ? path.posix.normalize(pathText) : path.normalize(pathText)), includeDirectorySlash);
91
if (uri) {
92
if (path.posix.normalize(uri.path) === '/') {
93
return undefined;
94
}
95
96
return uri;
97
}
98
} catch {
99
// noop
100
}
101
}
102
103
// Handle paths that look like uris
104
const scheme = pathText.match(/^([a-z]+):/i)?.[1];
105
if (scheme) {
106
try {
107
const uri = Uri.parse(pathText);
108
if (uri.scheme === Schemas.file || workspaceFolders.some(folder => folder.scheme === uri.scheme && folder.authority === uri.authority)) {
109
const statedUri = await this.statAndNormalizeUri(uri, includeDirectorySlash);
110
if (statedUri) {
111
return statedUri;
112
}
113
}
114
} catch {
115
// Noop, parsing error
116
}
117
return;
118
}
119
120
const result = await this.resolveInWorkspaceFolders(workspaceFolders, pathText, includeDirectorySlash);
121
if (result) {
122
return result;
123
}
124
125
// Then fallback to checking references based on filename.
126
// Only do this for simple filenames without directory components - if the user
127
// specified a path like `./node_modules/cli.js`, we shouldn't match a reference
128
// with a completely different path just because the basename matches.
129
// Also skip if text contains code-like characters that are rarely in real filenames.
130
if (!pathText.includes('/') && !pathText.includes('\\') && !/[${}()]/.test(pathText)) {
131
const name = path.basename(pathText);
132
const refUri = context.references
133
.map(ref => {
134
if ('variableName' in ref.anchor) {
135
return isUriComponents(ref.anchor.value) ? ref.anchor.value : ref.anchor.value?.uri;
136
}
137
return isUriComponents(ref.anchor) ? ref.anchor : ref.anchor.uri;
138
})
139
.filter((item): item is Uri => !!item)
140
.find(refUri => resources.basename(refUri) === name);
141
142
return refUri;
143
}
144
145
return undefined;
146
}
147
148
private async resolveInWorkspaceFolders(workspaceFolders: readonly Uri[], pathText: string, includeDirectorySlash: boolean): Promise<Uri | undefined> {
149
const candidates = workspaceFolders.map(folder => Uri.joinPath(folder, pathText));
150
const results = await Promise.all(candidates.map(uri => this.statAndNormalizeUri(uri, includeDirectorySlash)));
151
return results.find((r): r is Uri => r !== undefined);
152
}
153
154
private async statAndNormalizeUri(uri: Uri, includeDirectorySlash: boolean): Promise<Uri | undefined> {
155
try {
156
const stat = await this.statCache.stat(uri);
157
if (!stat) {
158
return undefined;
159
}
160
if (stat.type === FileType.Directory) {
161
if (includeDirectorySlash) {
162
return uri.path.endsWith('/') ? uri : uri.with({ path: `${uri.path}/` });
163
}
164
165
if (uri.path.endsWith('/') && uri.path !== '/') {
166
return uri.with({ path: uri.path.slice(0, -1) });
167
}
168
return uri;
169
}
170
171
return uri;
172
} catch {
173
return undefined;
174
}
175
}
176
}
177
178