Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/context/node/resolvers/promptWorkspaceLabels.ts
13405 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
7
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
8
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
9
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
10
import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
11
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
12
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
13
import { createServiceIdentifier } from '../../../../util/common/services';
14
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
15
import { Uri } from '../../../../vscodeTypes';
16
17
export const IPromptWorkspaceLabels = createServiceIdentifier<IPromptWorkspaceLabels>('IPromptWorkspaceLabels');
18
export interface IPromptWorkspaceLabels {
19
readonly _serviceBrand: undefined;
20
/**
21
* Will be unique and sorted.
22
*/
23
readonly labels: string[];
24
collectContext(): Promise<void>;
25
}
26
27
const enum PromptWorkspaceLabelsStrategy {
28
Basic,
29
Expanded
30
}
31
32
export class PromptWorkspaceLabels implements IPromptWorkspaceLabels {
33
declare _serviceBrand: undefined;
34
35
private readonly basicWorkspaceLabels: IPromptWorkspaceLabelsStrategy;
36
private readonly expandedWorkspaceLabels: IPromptWorkspaceLabelsStrategy;
37
private strategy = PromptWorkspaceLabelsStrategy.Basic;
38
39
private get workspaceLabels(): IPromptWorkspaceLabelsStrategy {
40
return this.strategy === PromptWorkspaceLabelsStrategy.Basic ? this.basicWorkspaceLabels : this.expandedWorkspaceLabels;
41
}
42
43
constructor(
44
@IExperimentationService private readonly _experimentationService: IExperimentationService,
45
@IConfigurationService private readonly _configurationService: IConfigurationService,
46
@ITelemetryService private readonly _telemetryService: ITelemetryService,
47
@IInstantiationService private readonly _instantiationService: IInstantiationService,
48
) {
49
this.basicWorkspaceLabels = this._instantiationService.createInstance(BasicPromptWorkspaceLabels);
50
this.expandedWorkspaceLabels = this._instantiationService.createInstance(ExpandedPromptWorkspaceLabels);
51
}
52
53
public get labels(): string[] {
54
const uniqueLabels = [...new Set(this.workspaceLabels.labels)].sort();
55
return uniqueLabels;
56
}
57
58
public async collectContext(): Promise<void> {
59
const expandedLabels = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.ProjectLabelsExpanded, this._experimentationService);
60
this.strategy = expandedLabels ? PromptWorkspaceLabelsStrategy.Expanded : PromptWorkspaceLabelsStrategy.Basic;
61
await this.workspaceLabels.collectContext();
62
63
const uniqueLabels = [...new Set(this.labels)].sort();
64
65
/* __GDPR__
66
"projectLabels" : {
67
"owner": "digitarald",
68
"comment": "Reports quality of labels detected in a workspace",
69
"labels": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique workspace label count." },
70
"count": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Unique workspace labels in context." }
71
}
72
*/
73
this._telemetryService.sendMSFTTelemetryEvent('projectLabels', {
74
labels: uniqueLabels.join(',').replaceAll('@', ' ')
75
}, {
76
count: uniqueLabels.length,
77
});
78
}
79
}
80
81
interface IPromptWorkspaceLabelsStrategy {
82
readonly labels: string[];
83
collectContext(): Promise<void>;
84
}
85
86
class BasicPromptWorkspaceLabels implements IPromptWorkspaceLabelsStrategy {
87
88
indicators: Map<string, string[]> = new Map<string, string[]>();
89
contentIndicators: Map<string, (contents: string) => string[]> = new Map<string, (contents: string) => string[]>();
90
private readonly _labels: string[] = [];
91
92
constructor(
93
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
94
@IFileSystemService private readonly _fileSystemService: IFileSystemService,
95
@IIgnoreService private readonly _ignoreService: IIgnoreService,
96
) {
97
this.initIndicators();
98
}
99
100
public get labels(): string[] {
101
// Check if labels have both javascript and typescript and remove javascript
102
// This can confuse the LLM and typescript should take precedent so types are returned.
103
if (this._labels.includes('javascript') && this._labels.includes('typescript')) {
104
const index = this._labels.indexOf('javascript');
105
this._labels.splice(index, 1);
106
}
107
return this._labels;
108
}
109
110
public async collectContext() {
111
const folders = this._workspaceService.getWorkspaceFolders();
112
if (folders) {
113
for (let i = 0; i < folders.length; i++) {
114
await this.addContextForFolders(folders[i]);
115
}
116
}
117
}
118
119
private async addContextForFolders(f: Uri) {
120
for (const [filename, labels] of this.indicators.entries()) {
121
await this.addLabelIfApplicable(f, filename, labels);
122
}
123
}
124
125
private async addLabelIfApplicable(rootFolder: Uri, filename: string, labels: string[]) {
126
const uri = Uri.joinPath(rootFolder, filename);
127
128
if (await this._ignoreService.isCopilotIgnored(uri)) {
129
return;
130
}
131
132
try {
133
await this._fileSystemService.stat(uri);
134
labels.forEach(label => this._labels.push(label));
135
const parseCallback = this.contentIndicators.get(filename);
136
if (parseCallback) {
137
const b = await this._fileSystemService.readFile(uri);
138
try {
139
const contentLabels = parseCallback(new TextDecoder().decode(b));
140
contentLabels.forEach(label => this._labels.push(label));
141
} catch (e) {
142
// it's ok if we can't parse those files
143
}
144
}
145
} catch (e) {
146
// ignore non-existing files
147
}
148
}
149
150
private initIndicators() {
151
this.addIndicator('package.json', 'javascript', 'npm');
152
this.addIndicator('tsconfig.json', 'typescript');
153
this.addIndicator('pom.xml', 'java', 'maven');
154
this.addIndicator('build.gradle', 'java', 'gradle');
155
this.addIndicator('requirements.txt', 'python', 'pip');
156
this.addIndicator('Pipfile', 'python', 'pip');
157
this.addIndicator('Cargo.toml', 'rust', 'cargo');
158
this.addIndicator('go.mod', 'go', 'go.mod');
159
this.addIndicator('pubspec.yaml', 'dart', 'pub');
160
this.addIndicator('build.sbt', 'scala', 'sbt');
161
this.addIndicator('build.boot', 'clojure', 'boot');
162
this.addIndicator('project.clj', 'clojure', 'lein');
163
this.addIndicator('mix.exs', 'elixir', 'mix');
164
this.addIndicator('composer.json', 'php', 'composer');
165
this.addIndicator('Gemfile', 'ruby', 'bundler');
166
this.addIndicator('build.xml', 'java', 'ant');
167
this.addIndicator('build.gradle.kts', 'java', 'gradle');
168
this.addIndicator('yarn.lock', 'yarn');
169
this.addIndicator('CMakeLists.txt', 'c++', 'cmake');
170
this.addIndicator('vcpkg.json', 'c++');
171
this.addIndicator('Makefile', 'c++', 'makefile');
172
this.addContentIndicator('CMakeLists.txt', this.collectCMakeListsTxtIndicators);
173
this.addContentIndicator('package.json', this.collectPackageJsonIndicators);
174
}
175
176
private addIndicator(filename: string, ...labels: string[]) {
177
this.indicators.set(filename, labels);
178
}
179
180
protected addContentIndicator(filename: string, callback: (contents: string) => string[]) {
181
this.contentIndicators.set(filename, callback);
182
}
183
184
private collectCMakeListsTxtIndicators(contents: string): string[] {
185
function parseStandardVersion(contents: string, regex: RegExp, allowedList: number[]): number | undefined {
186
try {
187
const matchResult = Array.from(contents.matchAll(regex));
188
if (matchResult && matchResult[0] && matchResult[0][1]) {
189
const version = parseInt(matchResult[0][1]);
190
if (allowedList.includes(version)) {
191
return version;
192
}
193
}
194
} catch (e) {
195
// It's ok if the parsing of the standard version fails.
196
}
197
return undefined;
198
}
199
200
const tags: string[] = [];
201
const cppLangStdVer = parseStandardVersion(contents,
202
/set\s*\(\s*CMAKE_CXX_STANDARD\s*(\d+)/gmi, [98, 11, 14, 17, 20, 23, 26]);
203
if (cppLangStdVer) {
204
tags.push(`C++${cppLangStdVer}`);
205
}
206
207
const cLangStdVer = parseStandardVersion(contents,
208
/set\s*\(\s*CMAKE_C_STANDARD\s*(\d+)/gmi, [90, 99, 11, 17, 23]);
209
if (cLangStdVer) {
210
tags.push(`C${cLangStdVer}`);
211
}
212
return tags;
213
}
214
215
private collectPackageJsonIndicators(contents: string): string[] {
216
const tags = [];
217
const json = JSON.parse(contents);
218
const dependencies = json.dependencies;
219
const devDependencies = json.devDependencies;
220
if (dependencies) {
221
if (dependencies['@angular/core']) {
222
tags.push('angular');
223
}
224
if (dependencies['react']) {
225
tags.push('react');
226
}
227
if (dependencies['vue']) {
228
tags.push('vue');
229
}
230
}
231
if (devDependencies) {
232
if (devDependencies['typescript']) {
233
tags.push('typescript');
234
}
235
}
236
const engines = json.engines;
237
if (engines) {
238
if (engines['node']) {
239
tags.push('node');
240
}
241
if (engines['vscode']) {
242
tags.push('vscode extension');
243
}
244
}
245
return tags;
246
}
247
}
248
249
class ExpandedPromptWorkspaceLabels extends BasicPromptWorkspaceLabels {
250
251
constructor(
252
@IWorkspaceService workspaceService: IWorkspaceService,
253
@IFileSystemService fileSystemService: IFileSystemService,
254
@IIgnoreService ignoreService: IIgnoreService,
255
) {
256
super(workspaceService, fileSystemService, ignoreService);
257
this.addContentIndicator('package.json', this.collectPackageJsonIndicatorsExpanded);
258
this.addContentIndicator('requirements.txt', this.collectPythonRequirementsIndicators);
259
this.addContentIndicator('pyproject.toml', this.collectPythonTomlIndicators);
260
}
261
262
protected collectPackageJsonIndicatorsExpanded(contents: string): string[] {
263
const tags: string[] = [];
264
265
const extractMajorMinorVersion = (version: string): string => {
266
const [major, minor] = version.split('.');
267
return `${major.replace(/[^0-9]/g, '')}.${minor.replace(/[^0-9]/g, '')}`;
268
};
269
270
const checkDependencies = (dependencies: Record<string, string> | undefined, list: { dependency: string; prefix?: string }[]) => {
271
if (!dependencies) { return; }
272
list.forEach(({ dependency, prefix }) => {
273
if (dependencies[dependency]) {
274
const version = extractMajorMinorVersion(dependencies[dependency]);
275
tags.push(`${prefix || dependency}@${version}`);
276
}
277
});
278
};
279
280
let json: any;
281
try {
282
json = JSON.parse(contents);
283
} catch {
284
return tags;
285
}
286
287
const allDependenciesFields = [
288
json.dependencies,
289
json.devDependencies,
290
json.peerDependencies,
291
json.optionalDependencies
292
];
293
294
const dependenciesList = [
295
// Frontend Frameworks
296
{ dependency: 'react' },
297
{ dependency: 'vue' },
298
{ dependency: '@angular/core' },
299
{ dependency: 'svelte' },
300
{ dependency: 'solid-js' },
301
{ dependency: 'alpinejs' },
302
303
// State Management Libraries
304
{ dependency: 'redux' },
305
{ dependency: 'mobx' },
306
{ dependency: 'vuex' },
307
{ dependency: 'ngrx' },
308
309
// UI Libraries
310
{ dependency: 'antd' },
311
{ dependency: 'bootstrap' },
312
{ dependency: 'bulma' },
313
{ dependency: '@mui/material' },
314
{ dependency: 'semantic-ui-react' },
315
316
// Rendering Frameworks
317
{ dependency: 'next' },
318
{ dependency: 'gatsby' },
319
{ dependency: 'remix' },
320
{ dependency: 'astro' },
321
{ dependency: 'sveltekit' },
322
{ dependency: 'nuxt' },
323
324
// Testing Tools
325
{ dependency: 'jest' },
326
{ dependency: 'mocha' },
327
{ dependency: 'cypress' },
328
{ dependency: '@testing-library/react' },
329
{ dependency: '@playwright/test' },
330
{ dependency: 'vitest' },
331
{ dependency: '@storybook/react' },
332
333
// CSS Tools
334
{ dependency: 'tailwindcss' },
335
{ dependency: 'sass' },
336
{ dependency: 'styled-components' },
337
{ dependency: 'css-modules' },
338
{ dependency: 'postcss' },
339
{ dependency: '@emotion/react' },
340
341
// Build Tools
342
{ dependency: 'vite' },
343
{ dependency: 'webpack' },
344
{ dependency: 'parcel' },
345
{ dependency: 'rollup' },
346
{ dependency: 'snowpack' },
347
{ dependency: 'esbuild' },
348
{ dependency: '@swc/core' },
349
350
// Real-time Communication
351
{ dependency: 'socket.io' },
352
353
// API and Data Handling
354
{ dependency: 'd3' },
355
{ dependency: 'graphql' },
356
357
// Utility Libraries
358
{ dependency: 'lodash' },
359
{ dependency: 'moment' },
360
{ dependency: 'rxjs' },
361
{ dependency: 'underscore' },
362
363
// Task Runners
364
{ dependency: 'gulp' },
365
366
// Older Libraries
367
{ dependency: 'backbone' },
368
{ dependency: 'ember-source' },
369
{ dependency: 'handlebars' },
370
{ dependency: 'jquery' },
371
{ dependency: 'knockout' },
372
373
// Cloud SDKs
374
{ dependency: 'aws-sdk' },
375
{ dependency: 'cloudinary' },
376
{ dependency: 'firebase' },
377
{ dependency: '@azure/storage-blob' },
378
{ dependency: '@google-cloud/storage' },
379
380
// Cloud Functions
381
{ dependency: '@aws-lambda' },
382
{ dependency: '@azure/functions' },
383
{ dependency: '@google-cloud/functions' },
384
{ dependency: 'firebase-functions' },
385
386
// Cloud Databases
387
{ dependency: '@azure/cosmos' },
388
{ dependency: '@google-cloud/firestore' },
389
{ dependency: 'mongoose' },
390
391
// Containerization and Orchestration
392
{ dependency: 'dockerode' },
393
{ dependency: 'kubernetes-client' },
394
395
// Monitoring and Logging
396
{ dependency: '@elastic/elasticsearch' },
397
{ dependency: '@sentry/node' },
398
{ dependency: 'log4js' },
399
{ dependency: 'winston' },
400
401
// Security
402
{ dependency: 'bcrypt' },
403
{ dependency: 'helmet' },
404
{ dependency: 'jsonwebtoken' },
405
{ dependency: 'passport' },
406
407
// Azure Libraries
408
{ dependency: '@azure/identity' },
409
{ dependency: '@azure/keyvault-certificates' },
410
{ dependency: '@azure/keyvault-keys' },
411
{ dependency: '@azure/keyvault-secrets' },
412
{ dependency: '@azure/service-bus' },
413
{ dependency: '@azure/event-hubs' },
414
{ dependency: '@azure/data-tables' },
415
{ dependency: '@azure/monitor-query' },
416
{ dependency: '@azure/app-configuration' },
417
418
// Development Tools
419
{ dependency: 'babel' },
420
{ dependency: 'eslint' },
421
{ dependency: 'parcel' },
422
{ dependency: 'prettier' },
423
{ dependency: 'rollup' },
424
{ dependency: 'typescript' },
425
{ dependency: 'webpack' },
426
{ dependency: 'vite' },
427
];
428
429
const enginesList = [
430
// Engines
431
{ dependency: 'node' },
432
{ dependency: 'vscode', prefix: 'vscode extension' }
433
];
434
435
allDependenciesFields.forEach((deps) => checkDependencies(deps, dependenciesList));
436
checkDependencies(json.engines, enginesList);
437
438
return tags;
439
}
440
441
442
private popularPackages: string[] = [
443
// Data Science and Machine Learning
444
'numpy', 'pandas', 'scipy', 'scikit-learn', 'matplotlib', 'tensorflow', 'keras',
445
'torch', 'seaborn', 'plotly', 'dash', 'jupyter', 'notebook', 'ipython', 'openai', 'pyspark',
446
'airflow', 'nltk', 'sympy', 'spacy', 'langchain',
447
448
// Web Development
449
'Flask', 'Django', 'fastapi', 'pydantic', 'requests', 'beautifulsoup4',
450
'gunicorn', 'uvicorn', 'httpx', 'Jinja2', 'aiohttp',
451
452
// Testing
453
'pytest', 'tox', 'nox', 'selenium', 'playwright', 'coverage', 'hypothesis',
454
455
// Documentation
456
'Sphinx',
457
458
// Task Queue
459
'celery', 'asyncio',
460
461
// Cloud and DevOps
462
'boto3', 'google-cloud-storage', 'azure-storage-blob', 'docker', 'kubernetes', 'azure', 'google', 'ansible',
463
464
// Security
465
'cryptography', 'paramiko', 'PyJWT',
466
467
// Enterprise, Legacy & data storage
468
'xlrd', 'xlrd-2024', 'openpyxl', 'pywin32', 'pywin', 'psycopg2', 'mysqlclient', 'SQLite4', 'Werkzeug', 'pymongo', 'redis', 'PyMySQL',
469
470
// Utilities
471
'Pillow', 'SQLAlchemy', 'lxml', 'html5lib', 'Markdown', 'pytz', 'Click',
472
'attrs', 'PyYAML', 'configparser', 'loguru', 'structlog', 'pygame', 'discord'
473
];
474
475
476
private collectPythonRequirementsIndicators(contents: string): string[] {
477
const tags: string[] = [];
478
479
const lines = contents.split('\n');
480
lines.forEach(line => {
481
const [pkg, version] = line.split('==');
482
if (this.popularPackages.includes(pkg)) {
483
tags.push(`${pkg}-${version || 'latest'}`);
484
}
485
});
486
487
return tags;
488
}
489
490
private collectPythonTomlIndicators(contents: string): string[] {
491
const tags: string[] = [];
492
493
const lines = contents.split('\n');
494
let inDependenciesSection = false;
495
496
// TODO@digitarald: Should use npm `toml` package, but this is avoiding a dependency for now
497
lines.forEach(line => {
498
line = line.trim();
499
if (line === '[tool.poetry.dependencies]') {
500
inDependenciesSection = true;
501
} else if (line.startsWith('[') && line.endsWith(']')) {
502
inDependenciesSection = false;
503
} else if (inDependenciesSection && line) {
504
const [pkg, version] = line.split('=').map(s => s.trim().replace(/"|'/g, ''));
505
if (this.popularPackages.includes(pkg)) {
506
tags.push(`${pkg}-${version || 'latest'}`);
507
}
508
}
509
});
510
511
return tags;
512
}
513
}
514
515