Merge branch 'main' into aiday/differentLightBulbDependingOnCodeAction

This commit is contained in:
Aiday Marlen Kyzy
2023-11-21 10:47:18 +01:00
169 changed files with 2902 additions and 2159 deletions
+10
View File
@@ -31,6 +31,16 @@ const extensions = [
workspaceFolder: path.join(os.tmpdir(), `nbout-${Math.floor(Math.random() * 100000)}`),
mocha: { timeout: 60_000 }
},
{
label: 'vscode-colorize-tests',
workspaceFolder: `extensions/vscode-colorize-tests/test`,
mocha: { timeout: 60_000 }
},
{
label: 'configuration-editing',
workspaceFolder: path.join(os.tmpdir(), `confeditout-${Math.floor(Math.random() * 100000)}`),
mocha: { timeout: 60_000 }
},
{
label: 'github-authentication',
workspaceFolder: path.join(os.tmpdir(), `msft-auth-${Math.floor(Math.random() * 100000)}`),
+1 -4
View File
@@ -166,8 +166,5 @@
"src/vs/workbench/api/common/extHostTypes.ts"
],
// Temporarily enabled for self-hosting
"terminal.integrated.stickyScroll.enabled": true,
// Temporarily enabled for self-hosting
"scm.showIncomingChanges": "always",
"scm.showOutgoingChanges": "always",
"terminal.integrated.stickyScroll.enabled": true
}
File diff suppressed because one or more lines are too long
+87 -167
View File
@@ -11,12 +11,13 @@ import { pipeline } from 'node:stream/promises';
import * as yauzl from 'yauzl';
import * as crypto from 'crypto';
import { retry } from './retry';
import { BlobServiceClient, BlockBlobParallelUploadOptions, StoragePipelineOptions, StorageRetryPolicyType } from '@azure/storage-blob';
import { BlobServiceClient, BlockBlobParallelUploadOptions, StorageRetryPolicyType } from '@azure/storage-blob';
import * as mime from 'mime';
import { CosmosClient } from '@azure/cosmos';
import { ClientSecretCredential } from '@azure/identity';
import * as cp from 'child_process';
import * as os from 'os';
import { Worker, isMainThread, workerData } from 'node:worker_threads';
function e(name: string): string {
const result = process.env[name];
@@ -48,49 +49,6 @@ class Temp {
}
}
export class Limiter {
private _size = 0;
private runningPromises: number;
private readonly maxDegreeOfParalellism: number;
private readonly outstandingPromises: { factory: () => Promise<any>; c: (v: any) => void; e: (err: Error) => void }[];
constructor(maxDegreeOfParalellism: number) {
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
this.outstandingPromises = [];
this.runningPromises = 0;
}
queue<T>(factory: () => Promise<T>): Promise<T> {
this._size++;
return new Promise<T>((c, e) => {
this.outstandingPromises.push({ factory, c, e });
this.consume();
});
}
private consume(): void {
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
const iLimitedTask = this.outstandingPromises.shift()!;
this.runningPromises++;
const promise = iLimitedTask.factory();
promise.then(iLimitedTask.c, iLimitedTask.e);
promise.then(() => this.consumed(), () => this.consumed());
}
}
private consumed(): void {
this._size--;
this.runningPromises--;
if (this.outstandingPromises.length > 0) {
this.consume();
}
}
}
interface RequestOptions {
readonly body?: string;
}
@@ -196,8 +154,6 @@ interface ReleaseDetailsResult {
class ESRPClient {
private static Limiter = new Limiter(1);
private readonly authPath: string;
constructor(
@@ -232,10 +188,8 @@ class ESRPClient {
version: string,
filePath: string
): Promise<Release> {
const submitReleaseResult = await ESRPClient.Limiter.queue(async () => {
this.log(`Submitting release for ${version}: ${filePath}`);
return await this.SubmitRelease(version, filePath);
});
this.log(`Submitting release for ${version}: ${filePath}`);
const submitReleaseResult = await this.SubmitRelease(version, filePath);
if (submitReleaseResult.submissionResponse.statusCode !== 'pass') {
throw new Error(`Unexpected status code: ${submitReleaseResult.submissionResponse.statusCode}`);
@@ -399,7 +353,6 @@ async function releaseAndProvision(
const credential = new ClientSecretCredential(provisionTenantId, provisionAADUsername, provisionAADPassword);
const accessToken = await credential.getToken(['https://microsoft.onmicrosoft.com/DS.Provisioning.WebApi/.default']);
const service = new ProvisionService(log, accessToken.token);
await service.provision(release.releaseId, release.fileId, fileName);
return result;
@@ -674,17 +627,12 @@ function getRealType(type: string) {
}
}
const azureLimiter = new Limiter(1);
const mooncakeLimiter = new Limiter(1);
async function uploadAssetLegacy(log: (...args: any[]) => void, quality: string, commit: string, filePath: string): Promise<{ assetUrl: string; mooncakeUrl: string }> {
async function uploadAssetLegacy(log: (...args: any[]) => void, quality: string, commit: string, filePath: string): Promise<string> {
const fileName = path.basename(filePath);
const blobName = commit + '/' + fileName;
const storagePipelineOptions: StoragePipelineOptions = { retryOptions: { retryPolicyType: StorageRetryPolicyType.EXPONENTIAL, maxTries: 6, tryTimeoutInMs: 10 * 60 * 1000 } };
const credential = new ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET'));
const blobServiceClient = new BlobServiceClient(`https://vscode.blob.core.windows.net`, credential, storagePipelineOptions);
const blobServiceClient = new BlobServiceClient(`https://vscode.blob.core.windows.net`, credential, { retryOptions: { retryPolicyType: StorageRetryPolicyType.FIXED, tryTimeoutInMs: 2 * 60 * 1000 } });
const containerClient = blobServiceClient.getContainerClient(quality);
const blobClient = containerClient.getBlockBlobClient(blobName);
@@ -696,121 +644,39 @@ async function uploadAssetLegacy(log: (...args: any[]) => void, quality: string,
}
};
const uploadPromises: Promise<void>[] = [];
log(`Checking for blob in Azure...`);
uploadPromises.push((async (): Promise<void> => {
log(`Checking for blob in Azure...`);
if (await retry(() => blobClient.exists())) {
throw new Error(`Blob ${quality}, ${blobName} already exists, not publishing again.`);
} else {
await retry(attempt => azureLimiter.queue(async () => {
log(`Uploading blobs to Azure storage (attempt ${attempt})...`);
await blobClient.uploadFile(filePath, blobOptions);
log('Blob successfully uploaded to Azure storage.');
}));
}
})());
const shouldUploadToMooncake = /true/i.test(e('VSCODE_PUBLISH_TO_MOONCAKE'));
if (shouldUploadToMooncake) {
const mooncakeCredential = new ClientSecretCredential(e('AZURE_MOONCAKE_TENANT_ID'), e('AZURE_MOONCAKE_CLIENT_ID'), e('AZURE_MOONCAKE_CLIENT_SECRET'));
const mooncakeBlobServiceClient = new BlobServiceClient(`https://vscode.blob.core.chinacloudapi.cn`, mooncakeCredential, storagePipelineOptions);
const mooncakeContainerClient = mooncakeBlobServiceClient.getContainerClient(quality);
const mooncakeBlobClient = mooncakeContainerClient.getBlockBlobClient(blobName);
uploadPromises.push((async (): Promise<void> => {
log(`Checking for blob in Mooncake Azure...`);
if (await retry(() => mooncakeBlobClient.exists())) {
throw new Error(`Mooncake Blob ${quality}, ${blobName} already exists, not publishing again.`);
} else {
await retry(attempt => mooncakeLimiter.queue(async () => {
log(`Uploading blobs to Mooncake Azure storage (attempt ${attempt})...`);
await mooncakeBlobClient.uploadFile(filePath, blobOptions);
log('Blob successfully uploaded to Mooncake Azure storage.');
}));
}
})());
}
const promiseResults = await Promise.allSettled(uploadPromises);
const rejectedPromiseResults = promiseResults.filter(result => result.status === 'rejected') as PromiseRejectedResult[];
if (rejectedPromiseResults.length === 0) {
log('All blobs successfully uploaded.');
} else if (rejectedPromiseResults[0]?.reason?.message?.includes('already exists')) {
log(rejectedPromiseResults[0].reason.message);
log('Some blobs successfully uploaded.');
if (await blobClient.exists()) {
log(`Blob ${quality}, ${blobName} already exists, not publishing again.`);
} else {
// eslint-disable-next-line no-throw-literal
throw rejectedPromiseResults[0]?.reason;
log(`Uploading blobs to Azure storage...`);
await blobClient.uploadFile(filePath, blobOptions);
log('Blob successfully uploaded to Azure storage.');
}
const assetUrl = `${e('AZURE_CDN_URL')}/${quality}/${blobName}`;
const blobPath = new URL(assetUrl).pathname;
const mooncakeUrl = `${e('MOONCAKE_CDN_URL')}${blobPath}`;
return { assetUrl, mooncakeUrl };
return `${e('AZURE_CDN_URL')}/${quality}/${blobName}`;
}
const downloadLimiter = new Limiter(5);
const cosmosLimiter = new Limiter(1);
async function processArtifact(artifact: Artifact): Promise<void> {
async function processArtifact(artifact: Artifact, artifactFilePath: string): Promise<void> {
const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args);
const match = /^vscode_(?<product>[^_]+)_(?<os>[^_]+)_(?<arch>[^_]+)_(?<unprocessedType>[^_]+)$/.exec(artifact.name);
if (!match) {
throw new Error(`Invalid artifact name: ${artifact.name}`);
}
const { product, os, arch, unprocessedType } = match.groups!;
const log = (...args: any[]) => console.log(`[${product} ${os} ${arch} ${unprocessedType}]`, ...args);
const start = Date.now();
const filePath = await retry(async attempt => {
const artifactZipPath = path.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`);
const start = Date.now();
log(`Downloading ${artifact.resource.downloadUrl} (attempt ${attempt})...`);
try {
await downloadLimiter.queue(() => downloadArtifact(artifact, artifactZipPath));
} catch (err) {
log(`Download failed: ${err.message}`);
throw err;
}
const archiveSize = fs.statSync(artifactZipPath).size;
const downloadDurationS = (Date.now() - start) / 1000;
const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS);
log(`Successfully downloaded ${artifact.resource.downloadUrl} after ${Math.floor(downloadDurationS)} seconds (${downloadSpeedKBS} KB/s).`);
const filePath = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY'));
const artifactSize = fs.statSync(filePath).size;
if (artifactSize !== Number(artifact.resource.properties.artifactsize)) {
log(`Artifact size mismatch. Expected ${artifact.resource.properties.artifactsize}. Actual ${artifactSize}`);
throw new Error(`Artifact size mismatch.`);
}
return filePath;
});
log(`Successfully downloaded and extracted after ${(Date.now() - start) / 1000} seconds.`);
// getPlatform needs the unprocessedType
const quality = e('VSCODE_QUALITY');
const commit = e('BUILD_SOURCEVERSION');
const { product, os, arch, unprocessedType } = match.groups!;
const platform = getPlatform(product, os, arch, unprocessedType);
const type = getRealType(unprocessedType);
const size = fs.statSync(filePath).size;
const stream = fs.createReadStream(filePath);
const size = fs.statSync(artifactFilePath).size;
const stream = fs.createReadStream(artifactFilePath);
const [sha1hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]);
const [{ assetUrl, mooncakeUrl }, prssUrl] = await Promise.all([
uploadAssetLegacy(log, quality, commit, filePath),
const [cdnSettledResult, prssSettledResult] = await Promise.allSettled([
uploadAssetLegacy(log, quality, commit, artifactFilePath),
releaseAndProvision(
log,
e('RELEASE_TENANT_ID'),
@@ -822,27 +688,46 @@ async function processArtifact(artifact: Artifact): Promise<void> {
e('PROVISION_AAD_PASSWORD'),
commit,
quality,
filePath
artifactFilePath
)
]);
const asset: Asset = { platform, type, url: assetUrl, hash: sha1hash, mooncakeUrl, prssUrl, sha256hash, size, supportsFastUpdate: true };
if (cdnSettledResult.status === 'rejected') {
throw cdnSettledResult.reason;
} else if (prssSettledResult.status === 'rejected') { // TODO@joaomoreno, let's temporarily ignore these errors
console.error(prssSettledResult.reason);
}
const assetUrl = cdnSettledResult.value;
const prssUrl = prssSettledResult.status === 'fulfilled' ? prssSettledResult.value : undefined;
const asset: Asset = { platform, type, url: assetUrl, hash: sha1hash, mooncakeUrl: prssUrl, prssUrl, sha256hash, size, supportsFastUpdate: true };
log('Creating asset...', JSON.stringify(asset));
await retry(async (attempt) => {
await cosmosLimiter.queue(async () => {
log(`Creating asset in Cosmos DB (attempt ${attempt})...`);
const aadCredentials = new ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET'));
const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), aadCredentials });
const scripts = client.database('builds').container(quality).scripts;
await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]);
});
log(`Creating asset in Cosmos DB (attempt ${attempt})...`);
const aadCredentials = new ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET'));
const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), aadCredentials });
const scripts = client.database('builds').container(quality).scripts;
await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]);
});
log('Asset successfully created');
}
// It is VERY important that we don't download artifacts too much too fast from AZDO.
// AZDO throttles us SEVERELY if we do. Not just that, but they also close open
// sockets, so the whole things turns to a grinding halt. So, downloading and extracting
// happens serially in the main thread, making the downloads are spaced out
// properly. For each extracted artifact, we spawn a worker thread to upload it to
// the CDN and finally update the build in Cosmos DB.
async function main() {
if (!isMainThread) {
const { artifact, artifactFilePath } = workerData;
await processArtifact(artifact, artifactFilePath);
return;
}
const done = new State();
const processing = new Set<string>();
@@ -857,6 +742,7 @@ async function main() {
if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') { stages.add('macOS'); }
if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); }
let resultPromise = Promise.resolve<PromiseSettledResult<void>[]>([]);
const operations: { name: string; operation: Promise<void> }[] = [];
while (true) {
@@ -880,15 +766,49 @@ async function main() {
continue;
}
console.log(`Found new artifact: ${artifact.name}`);
console.log(`[${artifact.name}] Found new artifact`);
const artifactZipPath = path.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`);
await retry(async (attempt) => {
const start = Date.now();
console.log(`[${artifact.name}] Downloading (attempt ${attempt})...`);
await downloadArtifact(artifact, artifactZipPath);
const archiveSize = fs.statSync(artifactZipPath).size;
const downloadDurationS = (Date.now() - start) / 1000;
const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS);
console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`);
});
const artifactFilePath = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY'));
const artifactSize = fs.statSync(artifactFilePath).size;
if (artifactSize !== Number(artifact.resource.properties.artifactsize)) {
console.log(`[${artifact.name}] Artifact size mismatch.Expected ${artifact.resource.properties.artifactsize}. Actual ${artifactSize} `);
throw new Error(`Artifact size mismatch.`);
}
processing.add(artifact.name);
const operation = processArtifact(artifact).then(() => {
const promise = new Promise<void>((resolve, reject) => {
const worker = new Worker(__filename, { workerData: { artifact, artifactFilePath } });
worker.on('error', reject);
worker.on('exit', code => {
if (code === 0) {
resolve();
} else {
reject(new Error(`[${artifact.name}] Worker stopped with exit code ${code}`));
}
});
});
const operation = promise.then(() => {
processing.delete(artifact.name);
done.add(artifact.name);
console.log(`\u2705 ${artifact.name}`);
console.log(`\u2705 ${artifact.name} `);
});
operations.push({ name: artifact.name, operation });
resultPromise = Promise.allSettled(operations.map(o => o.operation));
}
await new Promise(c => setTimeout(c, 10_000));
@@ -902,7 +822,7 @@ async function main() {
console.log('Artifacts in progress:', artifactsInProgress.map(a => a.name).join(', '));
}
const results = await Promise.allSettled(operations.map(o => o.operation));
const results = await resultPromise;
for (let i = 0; i < operations.length; i++) {
const result = results[i];
-8
View File
@@ -76,10 +76,6 @@ parameters:
displayName: "Publish to builds.code.visualstudio.com"
type: boolean
default: true
- name: VSCODE_PUBLISH_TO_MOONCAKE
displayName: "Publish to Azure China"
type: boolean
default: true
- name: VSCODE_RELEASE
displayName: "Release build if successful"
type: boolean
@@ -116,8 +112,6 @@ variables:
value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }}
- name: VSCODE_PUBLISH
value: ${{ and(eq(parameters.VSCODE_PUBLISH, true), eq(variables.VSCODE_CIBUILD, false), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}
- name: VSCODE_PUBLISH_TO_MOONCAKE
value: ${{ eq(parameters.VSCODE_PUBLISH_TO_MOONCAKE, true) }}
- name: VSCODE_SCHEDULEDBUILD
value: ${{ eq(variables['Build.Reason'], 'Schedule') }}
- name: VSCODE_7PM_BUILD
@@ -138,8 +132,6 @@ variables:
value: https://az764295.vo.msecnd.net
- name: AZURE_DOCUMENTDB_ENDPOINT
value: https://vscode.documents.azure.com:443/
- name: MOONCAKE_CDN_URL
value: https://vscode.cdn.azure.cn
- name: VSCODE_MIXIN_REPO
value: microsoft/vscode-distro
- name: skipComponentGovernanceDetection
-15
View File
@@ -49,18 +49,6 @@ steps:
Write-Host "##vso[task.setvariable variable=AZURE_CLIENT_ID]$env:servicePrincipalId"
Write-Host "##vso[task.setvariable variable=AZURE_CLIENT_SECRET;issecret=true]$env:servicePrincipalKey"
- task: AzureCLI@2
displayName: Fetch Mooncake secrets
inputs:
azureSubscription: "vscode-builds-mooncake-subscription"
scriptType: pscore
scriptLocation: inlineScript
addSpnToEnvironment: true
inlineScript: |
Write-Host "##vso[task.setvariable variable=AZURE_MOONCAKE_TENANT_ID]$env:tenantId"
Write-Host "##vso[task.setvariable variable=AZURE_MOONCAKE_CLIENT_ID]$env:servicePrincipalId"
Write-Host "##vso[task.setvariable variable=AZURE_MOONCAKE_CLIENT_SECRET;issecret=true]$env:servicePrincipalKey"
- pwsh: |
. build/azure-pipelines/win32/exec.ps1
@@ -102,9 +90,6 @@ steps:
AZURE_TENANT_ID: "$(AZURE_TENANT_ID)"
AZURE_CLIENT_ID: "$(AZURE_CLIENT_ID)"
AZURE_CLIENT_SECRET: "$(AZURE_CLIENT_SECRET)"
AZURE_MOONCAKE_TENANT_ID: "$(AZURE_MOONCAKE_TENANT_ID)"
AZURE_MOONCAKE_CLIENT_ID: "$(AZURE_MOONCAKE_CLIENT_ID)"
AZURE_MOONCAKE_CLIENT_SECRET: "$(AZURE_MOONCAKE_CLIENT_SECRET)"
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
RELEASE_TENANT_ID: "$(PRSS_RELEASE_TENANT_ID)"
RELEASE_CLIENT_ID: "$(PRSS_RELEASE_CLIENT_ID)"
+13 -15
View File
@@ -5,7 +5,7 @@
"publisher": "vscode",
"version": "1.0.0",
"license": "MIT",
"icon": "media/icon.png",
"icon": "media/icon.png",
"engines": {
"vscode": "^1.57.0"
},
@@ -58,10 +58,10 @@
"command": "ipynb.cleanInvalidImageAttachment",
"title": "%cleanInvalidImageAttachment.title%"
},
{
"command": "notebook.cellOutput.copy",
"title": "%copyCellOutput.title%"
}
{
"command": "notebook.cellOutput.copy",
"title": "%copyCellOutput.title%"
}
],
"notebooks": [
{
@@ -105,12 +105,12 @@
"when": "false"
}
],
"webview/context": [
{
"command": "notebook.cellOutput.copy",
"when": "webviewId == 'notebook.output' && webviewSection == 'image'"
}
]
"webview/context": [
{
"command": "notebook.cellOutput.copy",
"when": "webviewId == 'notebook.output' && webviewSection == 'image'"
}
]
}
},
"scripts": {
@@ -120,13 +120,11 @@
},
"dependencies": {
"@enonic/fnv-plus": "^1.3.0",
"detect-indent": "^6.0.0",
"uuid": "^8.3.2"
"detect-indent": "^6.0.0"
},
"devDependencies": {
"@jupyterlab/nbformat": "^3.2.9",
"@types/markdown-it": "12.2.3",
"@types/uuid": "^8.3.1"
"@types/markdown-it": "12.2.3"
},
"repository": {
"type": "git",
+54 -2
View File
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, NotebookDocument, NotebookDocumentChangeEvent, NotebookEdit, workspace, WorkspaceEdit } from 'vscode';
import { v4 as uuid } from 'uuid';
import { getCellMetadata } from './serializers';
import { CellMetadata } from './common';
import { getNotebookMetadata } from './notebookSerializer';
@@ -57,7 +56,7 @@ function generateCellId(notebook: NotebookDocument) {
while (true) {
// Details of the id can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#adding-an-id-field,
// & here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#updating-older-formats
const id = uuid().replace(/-/g, '').substring(0, 8);
const id = generateUuid().replace(/-/g, '').substring(0, 8);
let duplicate = false;
for (let index = 0; index < notebook.cellCount; index++) {
const cell = notebook.cellAt(index);
@@ -75,3 +74,56 @@ function generateCellId(notebook: NotebookDocument) {
}
}
}
/**
* Copied from src/vs/base/common/uuid.ts
*/
function generateUuid() {
// use `randomValues` if possible
function getRandomValues(bucket: Uint8Array): Uint8Array {
for (let i = 0; i < bucket.length; i++) {
bucket[i] = Math.floor(Math.random() * 256);
}
return bucket;
}
// prep-work
const _data = new Uint8Array(16);
const _hex: string[] = [];
for (let i = 0; i < 256; i++) {
_hex.push(i.toString(16).padStart(2, '0'));
}
// get data
getRandomValues(_data);
// set version bits
_data[6] = (_data[6] & 0x0f) | 0x40;
_data[8] = (_data[8] & 0x3f) | 0x80;
// print as string
let i = 0;
let result = '';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
return result;
}
-10
View File
@@ -37,17 +37,7 @@
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/uuid@^8.3.1":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
detect-indent@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
@@ -282,6 +282,15 @@ export const activate: ActivationFunction<void> = (ctx) => {
line-height: 1.357em;
white-space: pre-wrap;
}
li p {
margin-bottom: 0.7em;
}
ul,
ol {
margin-bottom: 0.7em;
}
`;
const template = document.createElement('template');
template.classList.add('markdown-style');
@@ -507,7 +507,7 @@
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.editor.pasteUrlAsFormattedLink.enabled%",
"default":"never",
"default": "never",
"enum": [
"always",
"smart",
@@ -734,6 +734,7 @@
"morphdom": "^2.6.1",
"picomatch": "^2.3.1",
"vscode-languageclient": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.11",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
@@ -3,18 +3,28 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as vscode from 'vscode';
import { ITextDocument } from '../types/textDocument';
export class InMemoryDocument implements ITextDocument {
constructor(
public readonly uri: vscode.Uri,
private readonly _contents: string,
public readonly version = 0,
) { }
private readonly _doc: TextDocument;
getText(): string {
return this._contents;
public readonly uri: vscode.Uri;
public readonly version: number;
constructor(
uri: vscode.Uri,
contents: string,
version: number = 0,
) {
this.uri = uri;
this.version = version;
this._doc = TextDocument.create(this.uri.toString(), 'markdown', 0, contents);
}
getText(range?: vscode.Range): string {
return this._doc.getText(range);
}
}
@@ -79,7 +79,7 @@ async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean, title = '', placeholderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false) {
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
const selectionText = activeEditor.document.getText(selection);
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, [], title, placeholderValue, pasteAsMarkdownLink, isExternalLink, {
const snippet = createUriListSnippet(activeEditor.document, selectedFiles.map(uri => ({ uri })), title, placeholderValue, pasteAsMarkdownLink, isExternalLink, {
insertAsMedia,
placeholderText: selectionText,
placeholderStartIndex: (i + 1) * selectedFiles.length,
@@ -44,12 +44,12 @@ class ResourceDropProvider implements vscode.DocumentDropEditProvider {
private async _getUriListEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const urlList = await dataTransfer.get(Mime.textUriList)?.asString();
if (!urlList || token.isCancellationRequested) {
return undefined;
return;
}
const snippet = await tryGetUriListSnippet(document, urlList, token);
const snippet = tryGetUriListSnippet(document, urlList);
if (!snippet) {
return undefined;
return;
}
const edit = new vscode.DocumentDropEdit(snippet.snippet);
@@ -53,7 +53,7 @@ class PasteResourceEditProvider implements vscode.DocumentPasteEditProvider {
}
const pasteUrlSetting = getPasteUrlAsFormattedLinkSetting(document);
const pasteEdit = await createEditAddingLinksForUriList(document, ranges, uriList, false, pasteUrlSetting === PasteUrlAsFormattedLink.Smart, token);
const pasteEdit = createEditAddingLinksForUriList(document, ranges, uriList, false, pasteUrlSetting === PasteUrlAsFormattedLink.Smart);
if (!pasteEdit) {
return;
}
@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import { Mime } from '../../util/mimes';
import { createEditAddingLinksForUriList, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink, validateLink } from './shared';
import { createEditAddingLinksForUriList, findValidUriInText, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink } from './shared';
class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
@@ -28,11 +28,16 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
const item = dataTransfer.get(Mime.textPlain);
const urlList = await item?.asString();
if (token.isCancellationRequested || !urlList || !validateLink(urlList).isValid) {
if (token.isCancellationRequested || !urlList) {
return;
}
const pasteEdit = await createEditAddingLinksForUriList(document, ranges, validateLink(urlList).cleanedUrlList, true, pasteUrlSetting === PasteUrlAsFormattedLink.Smart, token);
const uriText = findValidUriInText(urlList);
if (!uriText) {
return;
}
const pasteEdit = createEditAddingLinksForUriList(document, ranges, uriText, true, pasteUrlSetting === PasteUrlAsFormattedLink.Smart);
if (!pasteEdit) {
return;
}
@@ -6,6 +6,7 @@
import * as path from 'path';
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
import { ITextDocument } from '../../types/textDocument';
import { coalesce } from '../../util/arrays';
import { getDocumentDir } from '../../util/document';
import { mediaMimes } from '../../util/mimes';
@@ -18,7 +19,7 @@ enum MediaKind {
Audio,
}
export const externalUriSchemes = [
const externalUriSchemes = [
'http',
'https',
'mailto',
@@ -50,21 +51,6 @@ export const mediaFileExtensions = new Map<string, MediaKind>([
['wav', MediaKind.Audio],
]);
const smartPasteRegexes = [
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
{ regex: /^```[\s\S]*?```$/gm }, // In a backtick fenced code block
{ regex: /^~~~[\s\S]*?~~~$/gm }, // In a tildefenced code block
{ regex: /^\$\$[\s\S]*?\$\$$/gm }, // In a fenced math block
{ regex: /`[^`]*`/g }, // In inline code
{ regex: /\$[^$]*\$/g }, // In inline math
];
export interface SkinnyTextDocument {
offsetAt(position: vscode.Position): number;
getText(range?: vscode.Range): string;
readonly uri: vscode.Uri;
}
export enum PasteUrlAsFormattedLink {
Always = 'always',
Smart = 'smart',
@@ -75,18 +61,17 @@ export function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument)
return vscode.workspace.getConfiguration('markdown', document).get<PasteUrlAsFormattedLink>('editor.pasteUrlAsFormattedLink.enabled', PasteUrlAsFormattedLink.Smart);
}
export async function createEditAddingLinksForUriList(
document: SkinnyTextDocument,
export function createEditAddingLinksForUriList(
document: ITextDocument,
ranges: readonly vscode.Range[],
urlList: string,
isExternalLink: boolean,
useSmartPaste: boolean,
token: vscode.CancellationToken,
): Promise<{ additionalEdits: vscode.WorkspaceEdit; label: string; markdownLink: boolean } | undefined> {
if (ranges.length === 0) {
): { additionalEdits: vscode.WorkspaceEdit; label: string; markdownLink: boolean } | undefined {
if (!ranges.length) {
return;
}
const edits: vscode.SnippetTextEdit[] = [];
let placeHolderValue: number = ranges.length;
let label: string = '';
@@ -94,17 +79,12 @@ export async function createEditAddingLinksForUriList(
let markdownLink: boolean = true;
for (const range of ranges) {
const selectedRange: vscode.Range = new vscode.Range(
new vscode.Position(range.start.line, document.offsetAt(range.start)),
new vscode.Position(range.end.line, document.offsetAt(range.end))
);
if (useSmartPaste) {
pasteAsMarkdownLink = checkSmartPaste(document, selectedRange, range);
pasteAsMarkdownLink = shouldSmartPaste(document, range);
markdownLink = pasteAsMarkdownLink; // FIX: this will only match the last range
}
const snippet = await tryGetUriListSnippet(document, urlList, token, document.getText(range), placeHolderValue, pasteAsMarkdownLink, isExternalLink);
const snippet = tryGetUriListSnippet(document, urlList, document.getText(range), placeHolderValue, pasteAsMarkdownLink, isExternalLink);
if (!snippet) {
return;
}
@@ -121,13 +101,47 @@ export async function createEditAddingLinksForUriList(
return { additionalEdits, label, markdownLink };
}
export function checkSmartPaste(document: SkinnyTextDocument, selectedRange: vscode.Range, range: vscode.Range): boolean {
if (selectedRange.isEmpty || /^[\s\n]*$/.test(document.getText(range)) || validateLink(document.getText(range)).isValid) {
export function findValidUriInText(text: string): string | undefined {
const trimmedUrlList = text.trim();
// Uri must consist of a single sequence of characters without spaces
if (!/^\S+$/.test(trimmedUrlList)) {
return;
}
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(trimmedUrlList);
} catch {
// Could not parse
return;
}
if (!externalUriSchemes.includes(uri.scheme.toLowerCase()) || uri.authority.length <= 1) {
return;
}
return trimmedUrlList;
}
const smartPasteRegexes = [
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
{ regex: /^```[\s\S]*?```$/gm }, // In a backtick fenced code block
{ regex: /^~~~[\s\S]*?~~~$/gm }, // In a tildefenced code block
{ regex: /^\$\$[\s\S]*?\$\$$/gm }, // In a fenced math block
{ regex: /`[^`]*`/g }, // In inline code
{ regex: /\$[^$]*\$/g }, // In inline math
];
export function shouldSmartPaste(document: ITextDocument, selectedRange: vscode.Range): boolean {
if (selectedRange.isEmpty || /^[\s\n]*$/.test(document.getText(selectedRange)) || findValidUriInText(document.getText(selectedRange))) {
return false;
}
if (/\[.*\]\(.*\)/.test(document.getText(range)) || /!\[.*\]\(.*\)/.test(document.getText(range))) {
if (/\[.*\]\(.*\)/.test(document.getText(selectedRange)) || /!\[.*\]\(.*\)/.test(document.getText(selectedRange))) {
return false;
}
for (const regex of smartPasteRegexes) {
const matches = [...document.getText().matchAll(regex.regex)];
for (const match of matches) {
@@ -139,40 +153,20 @@ export function checkSmartPaste(document: SkinnyTextDocument, selectedRange: vsc
}
}
}
return true;
}
export function validateLink(urlList: string): { isValid: boolean; cleanedUrlList: string } {
let isValid = false;
let uri = undefined;
const trimmedUrlList = urlList?.trim(); //remove leading and trailing whitespace and new lines
try {
uri = vscode.Uri.parse(trimmedUrlList);
} catch (error) {
return { isValid: false, cleanedUrlList: urlList };
}
const splitUrlList = trimmedUrlList.split(' ').filter(item => item !== ''); //split on spaces and remove empty strings
if (uri) {
isValid = splitUrlList.length === 1 && !splitUrlList[0].includes('\n') && externalUriSchemes.includes(vscode.Uri.parse(splitUrlList[0]).scheme) && !!vscode.Uri.parse(splitUrlList[0]).authority;
}
return { isValid, cleanedUrlList: splitUrlList[0] };
}
export async function tryGetUriListSnippet(document: SkinnyTextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
if (token.isCancellationRequested) {
return undefined;
}
const uriStrings: string[] = [];
const uris: vscode.Uri[] = [];
for (const resource of urlList.split(/\r?\n/g)) {
export function tryGetUriListSnippet(document: ITextDocument, urlList: String, title = '', placeHolderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false): { snippet: vscode.SnippetString; label: string } | undefined {
const entries = coalesce(urlList.split(/\r?\n/g).map(line => {
try {
uris.push(vscode.Uri.parse(resource));
uriStrings.push(resource);
return { uri: vscode.Uri.parse(line), str: line };
} catch {
// noop
// Uri parse failure
return undefined;
}
}
return createUriListSnippet(document, uris, uriStrings, title, placeHolderValue, pasteAsMarkdownLink, isExternalLink);
}));
return createUriListSnippet(document, entries, title, placeHolderValue, pasteAsMarkdownLink, isExternalLink);
}
interface UriListSnippetOptions {
@@ -193,20 +187,21 @@ interface UriListSnippetOptions {
export function appendToLinkSnippet(
snippet: vscode.SnippetString,
title: string,
uriString: string,
link: string,
placeholderValue: number,
isExternalLink: boolean,
): vscode.SnippetString {
): void {
snippet.appendText('[');
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
snippet.appendText(`](${escapeMarkdownLinkPath(uriString, isExternalLink)})`);
return snippet;
snippet.appendText(`](${escapeMarkdownLinkPath(link, isExternalLink)})`);
}
export function createUriListSnippet(
document: SkinnyTextDocument,
uris: readonly vscode.Uri[],
uriStrings?: readonly string[],
document: ITextDocument,
uris: ReadonlyArray<{
readonly uri: vscode.Uri;
readonly str?: string;
}>,
title = '',
placeholderValue = 0,
pasteAsMarkdownLink = true,
@@ -219,15 +214,15 @@ export function createUriListSnippet(
const documentDir = getDocumentDir(document.uri);
let snippet = new vscode.SnippetString();
const snippet = new vscode.SnippetString();
let insertedLinkCount = 0;
let insertedImageCount = 0;
let insertedAudioVideoCount = 0;
uris.forEach((uri, i) => {
const mdPath = getMdPath(documentDir, uri);
const mdPath = getRelativeMdPath(documentDir, uri.uri) ?? uri.str ?? uri.uri.toString();
const ext = URI.Utils.extname(uri).toLowerCase().replace('.', '');
const ext = URI.Utils.extname(uri.uri).toLowerCase().replace('.', '');
const insertAsMedia = typeof options?.insertAsMedia === 'undefined' ? mediaFileExtensions.has(ext) : !!options.insertAsMedia;
const insertAsVideo = mediaFileExtensions.get(ext) === MediaKind.Video;
const insertAsAudio = mediaFileExtensions.get(ext) === MediaKind.Audio;
@@ -257,11 +252,7 @@ export function createUriListSnippet(
}
} else {
insertedLinkCount++;
if (uriStrings && isExternalLink) {
snippet = appendToLinkSnippet(snippet, title, uriStrings[i], placeholderValue, isExternalLink);
} else {
snippet.appendText(escapeMarkdownLinkPath(mdPath, isExternalLink));
}
appendToLinkSnippet(snippet, title, mdPath, placeholderValue, isExternalLink);
}
if (i < uris.length - 1 && uris.length > 1) {
@@ -349,7 +340,7 @@ export async function createEditForMediaFiles(
}
}
const snippet = createUriListSnippet(document, fileEntries.map(entry => entry.uri));
const snippet = createUriListSnippet(document, fileEntries);
if (!snippet) {
return;
}
@@ -361,7 +352,7 @@ export async function createEditForMediaFiles(
};
}
function getMdPath(dir: vscode.Uri | undefined, file: vscode.Uri) {
function getRelativeMdPath(dir: vscode.Uri | undefined, file: vscode.Uri): string | undefined {
if (dir && dir.scheme === file.scheme && dir.authority === file.authority) {
if (file.scheme === Schemes.file) {
// On windows, we must use the native `path.relative` to generate the relative path
@@ -373,8 +364,7 @@ function getMdPath(dir: vscode.Uri | undefined, file: vscode.Uri) {
return path.posix.relative(dir.path, file.path);
}
return file.toString(false);
return undefined;
}
function escapeHtmlAttribute(attr: string): string {
@@ -422,4 +412,3 @@ function needsBracketLink(mdPath: string) {
return nestingCount > 0;
}
@@ -2,92 +2,101 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as assert from 'assert';
import 'mocha';
import { SkinnyTextDocument, checkSmartPaste, createEditAddingLinksForUriList, appendToLinkSnippet, validateLink } from '../languageFeatures/copyFiles/shared';
import * as vscode from 'vscode';
import { InMemoryDocument } from '../client/inMemoryDocument';
import { appendToLinkSnippet, createEditAddingLinksForUriList, findValidUriInText, shouldSmartPaste } from '../languageFeatures/copyFiles/shared';
suite('createEditAddingLinksForUriList', () => {
test('Markdown Link Pasting should occur for a valid link (end to end)', async () => {
// createEditAddingLinksForUriList -> checkSmartPaste -> tryGetUriListSnippet -> createUriListSnippet -> createLinkSnippet
const skinnyDocument: SkinnyTextDocument = {
uri: vscode.Uri.parse('file:///path/to/your/file'),
offsetAt: function () { return 0; },
getText: function () { return 'hello world!'; },
};
const result = await createEditAddingLinksForUriList(skinnyDocument, [new vscode.Range(0, 0, 0, 12)], 'https://www.microsoft.com/', true, true, new vscode.CancellationTokenSource().token);
const result = createEditAddingLinksForUriList(
new InMemoryDocument(vscode.Uri.file('test.md'), 'hello world!'), [new vscode.Range(0, 0, 0, 12)], 'https://www.microsoft.com/', true, true);
// need to check the actual result -> snippet value
assert.strictEqual(result?.label, 'Insert Markdown Link');
});
suite('validateLink', () => {
test('Markdown pasting should occur for a valid link.', () => {
const isLink = validateLink('https://www.microsoft.com/').isValid;
assert.strictEqual(isLink, true);
test('Markdown pasting should occur for a valid link', () => {
assert.strictEqual(
findValidUriInText('https://www.microsoft.com/'),
'https://www.microsoft.com/');
});
test('Markdown pasting should occur for a valid link preceded by a new line.', () => {
const isLink = validateLink('\r\nhttps://www.microsoft.com/').isValid;
assert.strictEqual(isLink, true);
test('Markdown pasting should occur for a valid link preceded by a new line', () => {
assert.strictEqual(
findValidUriInText('\r\nhttps://www.microsoft.com/'),
'https://www.microsoft.com/');
});
test('Markdown pasting should occur for a valid link followed by a new line.', () => {
const isLink = validateLink('https://www.microsoft.com/\r\n').isValid;
assert.strictEqual(isLink, true);
test('Markdown pasting should occur for a valid link followed by a new line', () => {
assert.strictEqual(
findValidUriInText('https://www.microsoft.com/\r\n'),
'https://www.microsoft.com/');
});
test('Markdown pasting should not occur for a valid hostname and invalid protool.', () => {
const isLink = validateLink('invalid:www.microsoft.com').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for a valid hostname and invalid protool', () => {
assert.strictEqual(
findValidUriInText('invalid:www.microsoft.com'),
undefined);
});
test('Markdown pasting should not occur for plain text.', () => {
const isLink = validateLink('hello world!').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for plain text', () => {
assert.strictEqual(
findValidUriInText('hello world!'),
undefined);
});
test('Markdown pasting should not occur for plain text including a colon.', () => {
const isLink = validateLink('hello: world!').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for plain text including a colon', () => {
assert.strictEqual(
findValidUriInText('hello: world!'),
undefined);
});
test('Markdown pasting should not occur for plain text including a slashes.', () => {
const isLink = validateLink('helloworld!').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for plain text including a slashes', () => {
assert.strictEqual(
findValidUriInText('helloworld!'),
undefined);
});
test('Markdown pasting should not occur for a link followed by text.', () => {
const isLink = validateLink('https://www.microsoft.com/ hello world!').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for a link followed by text', () => {
assert.strictEqual(
findValidUriInText('https://www.microsoft.com/ hello world!'),
undefined);
});
test('Markdown pasting should occur for a link preceded or followed by spaces.', () => {
const isLink = validateLink(' https://www.microsoft.com/ ').isValid;
assert.strictEqual(isLink, true);
test('Markdown pasting should occur for a link preceded or followed by spaces', () => {
assert.strictEqual(
findValidUriInText(' https://www.microsoft.com/ '),
'https://www.microsoft.com/');
});
test('Markdown pasting should not occur for a link with an invalid scheme.', () => {
const isLink = validateLink('hello:www.microsoft.com').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for a link with an invalid scheme', () => {
assert.strictEqual(
findValidUriInText('hello:www.microsoft.com'),
undefined);
});
test('Markdown pasting should not occur for multiple links being pasted.', () => {
const isLink = validateLink('https://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for multiple links being pasted', () => {
assert.strictEqual(
findValidUriInText('https://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/'),
undefined);
});
test('Markdown pasting should not occur for multiple links with spaces being pasted.', () => {
const isLink = validateLink('https://www.microsoft.com/ \r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\n hello \r\nhttps://www.microsoft.com/').isValid;
assert.strictEqual(isLink, false);
test('Markdown pasting should not occur for multiple links with spaces being pasted', () => {
assert.strictEqual(
findValidUriInText('https://www.microsoft.com/ \r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\n hello \r\nhttps://www.microsoft.com/'),
undefined);
});
test('Markdown pasting should not occur for just a valid uri scheme', () => {
const isLink = validateLink('https://').isValid;
assert.strictEqual(isLink, false);
assert.strictEqual(
findValidUriInText('https://'),
undefined);
});
});
@@ -95,31 +104,36 @@ suite('createEditAddingLinksForUriList', () => {
test('Should create snippet with < > when pasted link has an mismatched parentheses', () => {
const uriString = 'https://www.mic(rosoft.com';
const snippet = appendToLinkSnippet(new vscode.SnippetString(''), 'abc', uriString, 0, true);
const snippet = new vscode.SnippetString('');
appendToLinkSnippet(snippet, 'abc', uriString, 0, true);
assert.strictEqual(snippet?.value, '[${0:abc}](<https://www.mic(rosoft.com>)');
});
test('Should create Markdown link snippet when pasteAsMarkdownLink is true', () => {
const uriString = 'https://www.microsoft.com';
const snippet = appendToLinkSnippet(new vscode.SnippetString(''), '', uriString, 0, true);
const snippet = new vscode.SnippetString('');
appendToLinkSnippet(snippet, '', uriString, 0, true);
assert.strictEqual(snippet?.value, '[${0:Title}](https://www.microsoft.com)');
});
test('Should use an unencoded URI string in Markdown link when passing in an external browser link', () => {
const uriString = 'https://www.microsoft.com';
const snippet = appendToLinkSnippet(new vscode.SnippetString(''), '', uriString, 0, true);
const snippet = new vscode.SnippetString('');
appendToLinkSnippet(snippet, '', uriString, 0, true);
assert.strictEqual(snippet?.value, '[${0:Title}](https://www.microsoft.com)');
});
test('Should not decode an encoded URI string when passing in an external browser link', () => {
const uriString = 'https://www.microsoft.com/%20';
const snippet = appendToLinkSnippet(new vscode.SnippetString(''), '', uriString, 0, true);
const snippet = new vscode.SnippetString('');
appendToLinkSnippet(snippet, '', uriString, 0, true);
assert.strictEqual(snippet?.value, '[${0:Title}](https://www.microsoft.com/%20)');
});
test('Should not encode an unencoded URI string when passing in an external browser link', () => {
const uriString = 'https://www.example.com/path?query=value&another=value#fragment';
const snippet = appendToLinkSnippet(new vscode.SnippetString(''), '', uriString, 0, true);
const snippet = new vscode.SnippetString('');
appendToLinkSnippet(snippet, '', uriString, 0, true);
assert.strictEqual(snippet?.value, '[${0:Title}](https://www.example.com/path?query=value&another=value#fragment)');
});
});
@@ -127,107 +141,86 @@ suite('createEditAddingLinksForUriList', () => {
suite('checkSmartPaste', () => {
const skinnyDocument: SkinnyTextDocument = {
uri: vscode.Uri.file('/path/to/your/file'),
offsetAt: function () { return 0; },
getText: function () { return 'hello world!'; },
};
test('Should evaluate pasteAsMarkdownLink as true for selected plain text', () => {
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 0, 0, 12), new vscode.Range(0, 0, 0, 12));
assert.strictEqual(pasteAsMarkdownLink, true);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('hello world'), new vscode.Range(0, 0, 0, 12)),
true);
});
test('Should evaluate pasteAsMarkdownLink as false for a valid selected link', () => {
skinnyDocument.getText = function () { return 'https://www.microsoft.com'; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 0, 0, 25), new vscode.Range(0, 0, 0, 25));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('https://www.microsoft.com'), new vscode.Range(0, 0, 0, 25)),
false);
});
test('Should evaluate pasteAsMarkdownLink as false for a valid selected link with trailing whitespace', () => {
skinnyDocument.getText = function () { return ' https://www.microsoft.com '; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 0, 0, 30), new vscode.Range(0, 0, 0, 30));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc(' https://www.microsoft.com '), new vscode.Range(0, 0, 0, 30)),
false);
});
test('Should evaluate pasteAsMarkdownLink as true for a link pasted in square brackets', () => {
skinnyDocument.getText = function () { return '[abc]'; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 1, 0, 4), new vscode.Range(0, 1, 0, 4));
assert.strictEqual(pasteAsMarkdownLink, true);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('[abc]'), new vscode.Range(0, 1, 0, 4)),
true);
});
test('Should evaluate pasteAsMarkdownLink as false for no selection', () => {
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 0, 0, 0), new vscode.Range(0, 0, 0, 0));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('xyz'), new vscode.Range(0, 0, 0, 0)),
false);
});
test('Should evaluate pasteAsMarkdownLink as false for selected whitespace and new lines', () => {
skinnyDocument.getText = function () { return ' \r\n\r\n'; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 0, 0, 7), new vscode.Range(0, 0, 0, 7));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc(' \r\n\r\n'), new vscode.Range(0, 0, 0, 7)),
false);
});
test('Should evaluate pasteAsMarkdownLink as false for pasting within a backtick code block', () => {
skinnyDocument.getText = function () { return '```\r\n\r\n```'; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 5, 0, 5), new vscode.Range(0, 5, 0, 5));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('```\r\n\r\n```'), new vscode.Range(0, 5, 0, 5)),
false);
});
test('Should evaluate pasteAsMarkdownLink as false for pasting within a tilde code block', () => {
skinnyDocument.getText = function () { return '~~~\r\n\r\n~~~'; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 5, 0, 5), new vscode.Range(0, 5, 0, 5));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('~~~\r\n\r\n~~~'), new vscode.Range(0, 5, 0, 5)),
false);
});
test('Should evaluate pasteAsMarkdownLink as false for pasting within a math block', () => {
skinnyDocument.getText = function () { return '$$$\r\n\r\n$$$'; };
const pasteAsMarkdownLink = checkSmartPaste(skinnyDocument, new vscode.Range(0, 5, 0, 5), new vscode.Range(0, 5, 0, 5));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('$$$\r\n\r\n$$$'), new vscode.Range(0, 5, 0, 5)),
false);
});
const linkSkinnyDoc: SkinnyTextDocument = {
uri: vscode.Uri.file('/path/to/your/file'),
offsetAt: function () { return 0; },
getText: function () { return '[a](bcdef)'; },
};
test('Should evaluate pasteAsMarkdownLink as false for pasting within a Markdown link', () => {
const pasteAsMarkdownLink = checkSmartPaste(linkSkinnyDoc, new vscode.Range(0, 4, 0, 6), new vscode.Range(0, 4, 0, 6));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('[a](bcdef)'), new vscode.Range(0, 4, 0, 6)),
false);
});
const imageLinkSkinnyDoc: SkinnyTextDocument = {
uri: vscode.Uri.file('/path/to/your/file'),
offsetAt: function () { return 0; },
getText: function () { return '![a](bcdef)'; },
};
test('Should evaluate pasteAsMarkdownLink as false for pasting within a Markdown image link', () => {
const pasteAsMarkdownLink = checkSmartPaste(imageLinkSkinnyDoc, new vscode.Range(0, 5, 0, 10), new vscode.Range(0, 5, 0, 10));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('![a](bcdef)'), new vscode.Range(0, 5, 0, 10)),
false);
});
const inlineCodeSkinnyCode: SkinnyTextDocument = {
uri: vscode.Uri.file('/path/to/your/file'),
offsetAt: function () { return 0; },
getText: function () { return '``'; },
};
test('Should evaluate pasteAsMarkdownLink as false for pasting within inline code', () => {
const pasteAsMarkdownLink = checkSmartPaste(inlineCodeSkinnyCode, new vscode.Range(0, 1, 0, 1), new vscode.Range(0, 1, 0, 1));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('``'), new vscode.Range(0, 1, 0, 1)),
false);
});
const inlineMathSkinnyDoc: SkinnyTextDocument = {
uri: vscode.Uri.file('/path/to/your/file'),
offsetAt: function () { return 0; },
getText: function () { return '$$'; },
};
test('Should evaluate pasteAsMarkdownLink as false for pasting within inline math', () => {
const pasteAsMarkdownLink = checkSmartPaste(inlineMathSkinnyDoc, new vscode.Range(0, 1, 0, 1), new vscode.Range(0, 1, 0, 1));
assert.strictEqual(pasteAsMarkdownLink, false);
assert.strictEqual(
shouldSmartPaste(makeTestDoc('$$'), new vscode.Range(0, 1, 0, 1)),
false);
});
});
});
function makeTestDoc(contents: string) {
return new InMemoryDocument(vscode.Uri.file('test.md'), contents);
}
@@ -12,6 +12,6 @@ export interface ITextDocument {
readonly uri: vscode.Uri;
readonly version: number;
getText(): string;
getText(range?: vscode.Range): string;
}
@@ -295,6 +295,11 @@ vscode-languageserver-protocol@3.17.2:
vscode-jsonrpc "8.0.2"
vscode-languageserver-types "3.17.2"
vscode-languageserver-textdocument@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
vscode-languageserver-textdocument@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz#838769940ece626176ec5d5a2aa2d0aa69f5095c"
+1 -1
View File
@@ -4,7 +4,7 @@
"license": "MIT",
"description": "Dependencies shared by all extensions",
"dependencies": {
"typescript": "^5.3.1-rc"
"typescript": "5.3.2"
},
"scripts": {
"postinstall": "node ./postinstall.mjs"
@@ -62,7 +62,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
new TypeScriptVersion(
TypeScriptVersionSource.Bundled,
vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(),
API.fromSimpleString('5.1.3')));
API.fromSimpleString('5.3.2')));
let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
const packageInfo = getPackageInfo(context);
@@ -77,7 +77,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens
switch (item.kind) {
case PConst.Kind.function: {
const showOnAllFunctions = vscode.workspace.getConfiguration(this.language.id).get<boolean>('referencesCodeLens.showOnAllFunctions');
if (showOnAllFunctions) {
if (showOnAllFunctions && item.nameSpan) {
return getSymbolRange(document, item);
}
}
@@ -223,7 +223,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
commandManager.register(new CompositeCommand());
commandManager.register(new ApplyCodeActionCommand(client, diagnosticsManager, telemetryReporter));
commandManager.register(new ApplyFixAllCodeAction(client, telemetryReporter));
commandManager.register(new EditorChatFollowUp(client));
commandManager.register(new EditorChatFollowUp(client, telemetryReporter));
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
}
@@ -349,7 +349,8 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
arguments: [<EditorChatFollowUp_Args>{
message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.',
expand: { kind: 'navtree-function', pos: diagnostic.range.start },
document
document,
action: { type: 'quickfix', quickfix: action }
}],
title: ''
};
@@ -506,7 +506,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
commandManager.register(new CompositeCommand());
commandManager.register(new SelectRefactorCommand(this.client));
commandManager.register(new MoveToFileRefactorCommand(this.client, didApplyRefactoringCommand));
commandManager.register(new EditorChatFollowUp(this.client));
commandManager.register(new EditorChatFollowUp(this.client, telemetryReporter));
}
public static readonly metadata: vscode.CodeActionProviderMetadata = {
@@ -634,7 +634,8 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
if (Extract_Constant.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractConstant')
|| Extract_Function.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractFunction')
|| Extract_Type.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractType')
|| Extract_Interface.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractInterface')) {
|| Extract_Interface.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractInterface')
) {
const newName = Extract_Constant.matches(action) ? 'newLocal'
: Extract_Function.matches(action) ? 'newFunction'
: Extract_Type.matches(action) ? 'NewType'
@@ -652,6 +653,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
kind: 'refactor-info',
refactor: info,
},
action: { type: 'refactor', refactor: action },
document,
}]
});
@@ -9,6 +9,7 @@ import { nulToken } from '../../utils/cancellation';
import type * as Proto from '../../tsServer/protocol/protocol';
import * as typeConverters from '../../typeConverters';
import { ITypeScriptServiceClient } from '../../typescriptService';
import { TelemetryReporter } from '../../logging/telemetry';
export class EditorChatFollowUp implements Command {
public static readonly ID = '_typescript.quickFix.editorChatReplacement2';
@@ -16,9 +17,38 @@ export class EditorChatFollowUp implements Command {
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
async execute({ message, document, expand }: EditorChatFollowUp_Args) {
async execute({ message, document, expand, action }: EditorChatFollowUp_Args) {
if (action.type === 'quickfix') {
/* __GDPR__
"aiQuickfix.execute" : {
"owner": "mjbvz",
"action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('aiQuickfix.execute', {
action: action.quickfix.fixName,
});
} else {
/* __GDPR__
"aiRefactor.execute" : {
"owner": "mjbvz",
"action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('aiRefactor.execute', {
action: action.refactor.name,
});
}
const initialRange =
expand.kind === 'navtree-function'
? await findScopeEndLineFromNavTree(
@@ -50,6 +80,13 @@ export interface EditorChatFollowUp_Args {
readonly message: string;
readonly document: vscode.TextDocument;
readonly expand: Expand;
readonly action: {
readonly type: 'refactor';
readonly refactor: Proto.RefactorActionInfo;
} | {
readonly type: 'quickfix';
readonly quickfix: Proto.CodeFixAction;
};
}
export class CompositeCommand implements Command {
+4 -4
View File
@@ -228,10 +228,10 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
typescript@^5.3.1-rc:
version "5.3.1-rc"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.1-rc.tgz#c307d4b69ea0c1c2cd17d4dd7700d30f7197f829"
integrity sha512-NVq/AufFc6KVjmVPcuVwdCkhTQlTcMEyRYJPvaGhPvj+X80MYUF+90qf0//uvINPb2ULg9m91/gbdIOhN2cZqA==
typescript@5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43"
integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==
vscode-grammar-updater@^1.1.0:
version "1.1.0"
+2 -2
View File
@@ -99,7 +99,6 @@
"native-watchdog": "^1.4.1",
"node-pty": "1.1.0-beta5",
"tas-client-umd": "0.1.8",
"util": "^0.12.4",
"v8-inspect-profiler": "^0.1.0",
"vscode-oniguruma": "1.7.0",
"vscode-regexpp": "^3.1.0",
@@ -211,6 +210,7 @@
"tsec": "0.2.7",
"typescript": "^5.4.0-dev.20231116",
"typescript-formatter": "7.1.0",
"util": "^0.12.4",
"vscode-nls-dev": "^3.3.1",
"webpack": "^5.77.0",
"webpack-cli": "^5.0.1",
@@ -228,4 +228,4 @@
"optionalDependencies": {
"windows-foreground-love": "0.5.0"
}
}
}
+2 -2
View File
@@ -49,7 +49,7 @@ if %errorlevel% neq 0 exit /b %errorlevel%
echo.
echo ### Colorize tests
call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out %API_TESTS_EXTRA_ARGS%
call yarn test-extension -l vscode-colorize-tests
if %errorlevel% neq 0 exit /b %errorlevel%
echo.
@@ -89,7 +89,7 @@ echo.
echo ### Configuration editing tests
set CFWORKSPACE=%TEMPDIR%\cf-%RANDOM%
mkdir %CFWORKSPACE%
call "%INTEGRATION_TEST_ELECTRON_PATH%" %CFWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\configuration-editing --extensionTestsPath=%~dp0\..\extensions\configuration-editing\out\test %API_TESTS_EXTRA_ARGS%
call yarn test-extension -l configuration-editing
if %errorlevel% neq 0 exit /b %errorlevel%
echo.
+2 -2
View File
@@ -67,7 +67,7 @@ kill_app
echo
echo "### Colorize tests"
echo
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out $API_TESTS_EXTRA_ARGS
yarn test-extension -l vscode-colorize-tests
kill_app
echo
@@ -109,7 +109,7 @@ kill_app
echo
echo "### Configuration editing tests"
echo
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/configuration-editing --extensionTestsPath=$ROOT/extensions/configuration-editing/out/test $API_TESTS_EXTRA_ARGS
yarn test-extension -l configuration-editing
kill_app
echo
Binary file not shown.
+1
View File
@@ -580,6 +580,7 @@ export const Codicon = {
micFilled: register('mic-filled', 0xec1c),
gitFetch: register('git-fetch', 0xec1d),
copilot: register('copilot', 0xec1e),
lightbulbSparkle: register('lightbulb-sparkle', 0xec1f),
// derived icons, that could become separate icons
+13
View File
@@ -5,6 +5,7 @@
import * as errors from 'vs/base/common/errors';
import * as platform from 'vs/base/common/platform';
import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
export namespace Schemas {
@@ -109,6 +110,18 @@ export namespace Schemas {
export const vscodeSourceControl = 'vscode-scm';
}
export function matchesScheme(target: URI | string, scheme: string): boolean {
if (URI.isUri(target)) {
return equalsIgnoreCase(target.scheme, scheme);
} else {
return startsWithIgnoreCase(target, scheme + ':');
}
}
export function matchesSomeScheme(target: URI | string, ...schemes: string[]): boolean {
return schemes.some(scheme => matchesScheme(target, scheme));
}
export const connectionTokenCookieName = 'vscode-tkn';
export const connectionTokenQueryName = 'tkn';
+9 -6
View File
@@ -11,8 +11,7 @@ import * as path from 'vs/base/common/path';
import { assertIsDefined } from 'vs/base/common/types';
import { Promises } from 'vs/base/node/pfs';
import * as nls from 'vs/nls';
import { Entry, open as _openZip, ZipFile } from 'yauzl';
import * as yazl from 'yazl';
import type { Entry, ZipFile } from 'yauzl';
export const CorruptZipMessage: string = 'end of central directory record signature not found';
const CORRUPT_ZIP_PATTERN = new RegExp(CorruptZipMessage);
@@ -161,9 +160,11 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok
}).finally(() => listener.dispose());
}
function openZip(zipFile: string, lazy: boolean = false): Promise<ZipFile> {
async function openZip(zipFile: string, lazy: boolean = false): Promise<ZipFile> {
const { open } = await import('yauzl');
return new Promise<ZipFile>((resolve, reject) => {
_openZip(zipFile, lazy ? { lazyEntries: true } : undefined!, (error?: Error, zipfile?: ZipFile) => {
open(zipFile, lazy ? { lazyEntries: true } : undefined!, (error?: Error, zipfile?: ZipFile) => {
if (error) {
reject(toExtractError(error));
} else {
@@ -191,9 +192,11 @@ export interface IFile {
localPath?: string;
}
export function zip(zipPath: string, files: IFile[]): Promise<string> {
export async function zip(zipPath: string, files: IFile[]): Promise<string> {
const { ZipFile } = await import('yazl');
return new Promise<string>((c, e) => {
const zip = new yazl.ZipFile();
const zip = new ZipFile();
files.forEach(f => {
if (f.contents) {
zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path);
@@ -42,6 +42,7 @@
'self'
vscode-remote-resource:
vscode-managed-remote-resource:
https://*.vscode-unpkg.net
;
require-trusted-types-for
'script'
@@ -42,6 +42,7 @@
'self'
vscode-remote-resource:
vscode-managed-remote-resource:
https://*.vscode-unpkg.net
;
require-trusted-types-for
'script'
@@ -10,13 +10,13 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { ResourceMap } from 'vs/base/common/map';
import { parse } from 'vs/base/common/marshalling';
import { Schemas } from 'vs/base/common/network';
import { matchesScheme, matchesSomeScheme, Schemas } from 'vs/base/common/network';
import { normalizePath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { EditorOpenSource } from 'vs/platform/editor/common/editor';
import { extractSelection, IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, matchesScheme, matchesSomeScheme, OpenOptions, ResolveExternalUriOptions } from 'vs/platform/opener/common/opener';
import { extractSelection, IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, OpenOptions, ResolveExternalUriOptions } from 'vs/platform/opener/common/opener';
class CommandOpener implements IOpener {
@@ -275,13 +275,14 @@ export class GlyphMarginWidgets extends ViewPart {
for (const widget of Object.values(this._widgets)) {
const range = widget.preference.range;
if (range.endLineNumber < visibleStartLineNumber || range.startLineNumber > visibleEndLineNumber) {
const { startLineNumber, endLineNumber } = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(Range.lift(range));
if (!startLineNumber || !endLineNumber || endLineNumber < visibleStartLineNumber || startLineNumber > visibleEndLineNumber) {
// The widget is not in the viewport
continue;
}
// The widget is in the viewport, find a good line for it
const widgetLineNumber = Math.max(range.startLineNumber, visibleStartLineNumber);
const widgetLineNumber = Math.max(startLineNumber, visibleStartLineNumber);
const lane = Math.min(widget.preference.lane, this._glyphMarginDecorationLaneCount);
requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, lane, widget.preference.zIndex, widget));
}
@@ -62,3 +62,7 @@
height: 16px;
margin: 2px 0;
}
.monaco-diff-editor .revertButton {
cursor: pointer;
}
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { Codicon } from 'vs/base/common/codicons';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { ThemeIcon } from 'vs/base/common/themables';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { localize } from 'vs/nls';
@@ -76,12 +75,3 @@ export const diffDeleteDecorationEmpty = ModelDecorationOptions.register({
className: 'char-delete diff-range-empty',
description: 'char-delete diff-range-empty',
});
export const arrowRevertChange = ModelDecorationOptions.register({
description: 'diff-editor-arrow-revert-change',
glyphMarginHoverMessage: new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true })
.appendMarkdown(localize('revertChangeHoverMessage', 'Click to revert change')),
glyphMarginClassName: 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight),
zIndex: 10001,
});
@@ -3,26 +3,36 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, derived } from 'vs/base/common/observable';
import { arrowRevertChange, diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackground, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackground, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/decorations';
import { h } from 'vs/base/browser/dom';
import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Codicon } from 'vs/base/common/codicons';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorunWithStore, derived } from 'vs/base/common/observable';
import { IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackground, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackground, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/decorations';
import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/diffEditorEditors';
import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions';
import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditor/movedBlocksLines';
import { applyObservableDecorations } from 'vs/editor/browser/widget/diffEditor/utils';
import { Position } from 'vs/editor/common/core/position';
import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange';
import { Range } from 'vs/editor/common/core/range';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { RangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { GlyphMarginLane, IModelDeltaDecoration } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
export class DiffEditorDecorations extends Disposable {
constructor(
private readonly _editors: DiffEditorEditors,
private readonly _diffModel: IObservable<DiffEditorViewModel | undefined>,
private readonly _options: DiffEditorOptions,
widget: DiffEditorWidget,
) {
super();
this._register(new RevertButtonsFeature(_editors, _diffModel, _options, widget));
this._register(applyObservableDecorations(this._editors.original, this._decorations.map(d => d?.originalDecorations || [])));
this._register(applyObservableDecorations(this._editors.modified, this._decorations.map(d => d?.modifiedDecorations || [])));
}
@@ -66,10 +76,6 @@ export class DiffEditorDecorations extends Disposable {
}
}
}
if (!m.lineRangeMapping.modified.isEmpty && this._options.shouldRenderRevertArrows.read(reader) && !movedTextToCompare) {
modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modified.startLineNumber, 1)), options: arrowRevertChange });
}
}
}
@@ -113,3 +119,139 @@ export class DiffEditorDecorations extends Disposable {
return { originalDecorations, modifiedDecorations };
});
}
class RevertButtonsFeature extends Disposable {
constructor(
private readonly _editors: DiffEditorEditors,
private readonly _diffModel: IObservable<DiffEditorViewModel | undefined>,
private readonly _options: DiffEditorOptions,
private readonly _widget: DiffEditorWidget,
) {
super();
const emptyArr: never[] = [];
const selectedDiffs = derived(this, (reader) => {
/** @description selectedDiffs */
const model = this._diffModel.read(reader);
const diff = model?.diff.read(reader);
if (!diff) { return emptyArr; }
const selections = this._editors.modifiedSelections.read(reader);
if (selections.every(s => s.isEmpty())) {
return emptyArr;
}
const lineRanges = new LineRangeSet(selections.map(s => LineRange.fromRangeInclusive(s)));
const mappings = diff.mappings.filter(m => m.lineRangeMapping.innerChanges && lineRanges.intersects(m.lineRangeMapping.modified));
const result = mappings.map(mapping => ({
mapping,
rangeMappings: mapping.lineRangeMapping.innerChanges!.filter(c => selections.some(s => Range.areIntersecting(c.modifiedRange, s)))
}));
if (result.length === 0 || result.every(r => r.rangeMappings.length === 0)) { return emptyArr; }
return result;
});
this._register(autorunWithStore((reader, store) => {
const model = this._diffModel.read(reader);
const diff = model?.diff.read(reader);
if (!model || !diff) { return; }
const movedTextToCompare = this._diffModel.read(reader)!.movedTextToCompare.read(reader);
if (movedTextToCompare) { return; }
if (!this._options.shouldRenderRevertArrows.read(reader)) { return; }
const glyphWidgetsModified: IGlyphMarginWidget[] = [];
const selectedDiffs_ = selectedDiffs.read(reader);
const diffsSet = new Set(selectedDiffs_.map(d => d.mapping));
if (selectedDiffs_.length > 0) {
const selections = this._editors.modifiedSelections.read(reader);
const btn = new RevertButton(selections[selections.length - 1].positionLineNumber, this._widget, selectedDiffs_.flatMap(d => d.rangeMappings), true);
this._editors.modified.addGlyphMarginWidget(btn);
glyphWidgetsModified.push(btn);
}
for (const m of diff.mappings) {
if (diffsSet.has(m)) {
continue;
}
if (!m.lineRangeMapping.modified.isEmpty && m.lineRangeMapping.innerChanges) {
const btn = new RevertButton(m.lineRangeMapping.modified.startLineNumber, this._widget, m.lineRangeMapping.innerChanges, false);
this._editors.modified.addGlyphMarginWidget(btn);
glyphWidgetsModified.push(btn);
}
}
store.add(toDisposable(() => {
for (const w of glyphWidgetsModified) {
this._editors.modified.removeGlyphMarginWidget(w);
}
}));
}));
}
}
class RevertButton implements IGlyphMarginWidget {
public static counter = 0;
private readonly _id: string = `revertButton${RevertButton.counter++}`;
getId(): string { return this._id; }
private readonly _domNode = h('div.revertButton', {
title: this._selection
? localize('revertSelectedChanges', 'Revert Selected Changes')
: localize('revertChange', 'Revert Change')
},
[renderIcon(Codicon.arrowRight)]
).root;
constructor(
private readonly _lineNumber: number,
private readonly _widget: DiffEditorWidget,
private readonly _diffs: RangeMapping[],
private readonly _selection: boolean,
) {
this._domNode.onmousedown = e => {
e.stopPropagation();
e.preventDefault();
};
this._domNode.onmouseup = e => {
e.stopPropagation();
e.preventDefault();
};
this._domNode.onclick = (e) => {
this._widget.revertRangeMappings(this._diffs);
e.stopPropagation();
e.preventDefault();
};
}
/**
* Get the dom node of the glyph widget.
*/
getDomNode(): HTMLElement {
return this._domNode;
}
/**
* Get the placement of the glyph widget.
*/
getPosition(): IGlyphMarginWidgetPosition {
return {
lane: GlyphMarginLane.Right,
range: {
startColumn: 1,
startLineNumber: this._lineNumber,
endColumn: 1,
endLineNumber: this._lineNumber,
},
zIndex: 0,
};
}
}
@@ -17,6 +17,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DiffEditorOptions } from './diffEditorOptions';
import { ITextModel } from 'vs/editor/common/model';
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
import { Selection } from 'vs/editor/common/core/selection';
import { Position } from 'vs/editor/common/core/position';
export class DiffEditorEditors extends Disposable {
public readonly modified: CodeEditorWidget;
@@ -30,6 +32,9 @@ export class DiffEditorEditors extends Disposable {
public readonly modifiedModel: IObservable<ITextModel | null>;
public readonly modifiedSelections: IObservable<Selection[]>;
public readonly modifiedCursor: IObservable<Position>;
constructor(
private readonly originalEditorElement: HTMLElement,
private readonly modifiedEditorElement: HTMLElement,
@@ -49,6 +54,9 @@ export class DiffEditorEditors extends Disposable {
this.modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop());
this.modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight());
this.modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []);
this.modifiedCursor = observableFromEvent(this.modified.onDidChangeCursorPosition, () => this.modified.getPosition() ?? new Position(1, 1));
this._register(autorunHandleChanges({
createEmptyChangeSummary: () => ({} as IDiffEditorConstructionOptions),
handleChange: (ctx, changeSummary) => {
@@ -104,12 +104,10 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
const lastUnchangedRegions = this._unchangedRegions.get();
const lastUnchangedRegionsOrigRanges = lastUnchangedRegions.originalDecorationIds
.map(id => model.original.getDecorationRange(id))
.filter(r => !!r)
.map(r => LineRange.fromRange(r!));
.map(r => r ? LineRange.fromRange(r) : undefined);
const lastUnchangedRegionsModRanges = lastUnchangedRegions.modifiedDecorationIds
.map(id => model.modified.getDecorationRange(id))
.filter(r => !!r)
.map(r => LineRange.fromRange(r!));
.map(r => r ? LineRange.fromRange(r) : undefined);
const originalDecorationIds = model.original.deltaDecorations(
lastUnchangedRegions.originalDecorationIds,
@@ -123,8 +121,8 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
for (const r of newUnchangedRegions) {
for (let i = 0; i < lastUnchangedRegions.regions.length; i++) {
if (r.originalUnchangedRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i])
&& r.modifiedUnchangedRange.intersectsStrict(lastUnchangedRegionsModRanges[i])) {
if (lastUnchangedRegionsOrigRanges[i] && r.originalUnchangedRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i]!)
&& lastUnchangedRegionsModRanges[i] && r.modifiedUnchangedRange.intersectsStrict(lastUnchangedRegionsModRanges[i]!)) {
r.setHiddenModifiedRange(lastUnchangedRegions.regions[i].getHiddenModifiedRange(undefined), tx);
break;
}
@@ -156,6 +154,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
}
}
this._isDiffUpToDate.set(false, undefined);
debouncer.schedule();
}));
this._register(model.original.onDidChangeContent((e) => {
@@ -174,6 +173,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
}
}
this._isDiffUpToDate.set(false, undefined);
debouncer.schedule();
}));
@@ -12,7 +12,7 @@ import { IObservable, ITransaction, autorunWithStore, derived, observableFromEve
import { derivedDisposable } from 'vs/base/common/observableInternal/derived';
import 'vs/css!./style';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IMouseTargetViewZone } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
@@ -31,7 +31,7 @@ import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
@@ -171,7 +171,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
derivedDisposable(this, reader => /** @description DiffEditorDecorations */
this._instantiationService.createInstance(
readHotReloadableExport(DiffEditorDecorations, reader),
this._editors, this._diffModel, this._options,
this._editors, this._diffModel, this._options, this,
)
).recomputeInitiallyAndOnChange(this._store);
@@ -260,27 +260,6 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
),
}));
// Revert change when an arrow is clicked.
this._register(this._editors.modified.onMouseDown(event => {
if (!event.event.rightButton && event.target.position && event.target.element?.className.includes('arrow-revert-change')) {
const lineNumber = event.target.position.lineNumber;
const viewZone = event.target as IMouseTargetViewZone | undefined;
const model = this._diffModel.get();
if (!model) { return; }
const diffs = model.diff.get()?.mappings;
if (!diffs) { return; }
const diff = diffs.find(d =>
viewZone?.detail.afterLineNumber === d.lineRangeMapping.modified.startLineNumber - 1 ||
d.lineRangeMapping.modified.startLineNumber === lineNumber
);
if (!diff) { return; }
this.revert(diff.lineRangeMapping);
event.event.stopPropagation();
}
}));
this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, (e) => {
if (e?.reason === CursorChangeReason.Explicit) {
const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber));
@@ -486,20 +465,29 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
}
revert(diff: DetailedLineRangeMapping): void {
if (diff.innerChanges) {
this.revertRangeMappings(diff.innerChanges);
}
const model = this._diffModel.get()?.model;
if (!model) { return; }
const changes: IIdentifiedSingleEditOperation[] = diff.innerChanges
? diff.innerChanges.map<IIdentifiedSingleEditOperation>(c => ({
range: c.modifiedRange,
text: model.original.getValueInRange(c.originalRange)
}))
: [
{
range: diff.modified.toExclusiveRange(),
text: model.original.getValueInRange(diff.original.toExclusiveRange())
}
];
this._editors.modified.executeEdits('diffEditor', [
{
range: diff.modified.toExclusiveRange(),
text: model.original.getValueInRange(diff.original.toExclusiveRange())
}
]);
}
revertRangeMappings(diffs: RangeMapping[]): void {
const model = this._diffModel.get();
if (!model || !model.isDiffUpToDate.get()) { return; }
const changes: IIdentifiedSingleEditOperation[] = diffs.map<IIdentifiedSingleEditOperation>(c => ({
range: c.modifiedRange,
text: model.model.original.getValueInRange(c.originalRange)
}));
this._editors.modified.executeEdits('diffEditor', changes);
}
@@ -6,30 +6,33 @@ import { h } from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { Codicon } from 'vs/base/common/codicons';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { autorun, derived } from 'vs/base/common/observable';
import { globalTransaction, observableValue } from 'vs/base/common/observableInternal/base';
import { autorun, derived, observableFromEvent } from 'vs/base/common/observable';
import { IObservable, globalTransaction, observableValue } from 'vs/base/common/observableInternal/base';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
import { IDocumentDiffItem } from 'vs/editor/browser/widget/multiDiffEditorWidget/model';
import { DocumentDiffItemViewModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel';
import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory';
import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IObjectData, IPooledObject } from './objectPool';
import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions';
export class TemplateData implements IObjectData {
constructor(
public readonly viewModel: IDiffEditorViewModel,
public readonly entry: IDocumentDiffItem,
public readonly viewModel: DocumentDiffItemViewModel,
) { }
getId(): unknown {
return this.entry;
return this.viewModel;
}
}
export class DiffEditorItemTemplate extends Disposable implements IPooledObject<TemplateData> {
private readonly _viewModel = observableValue<DocumentDiffItemViewModel | undefined>(this, undefined);
private readonly _collapsed = derived(this, reader => this._viewModel.read(reader)?.collapsed.read(reader));
private readonly _contentHeight = observableValue<number>(this, 500);
public readonly height = derived(this, reader => {
const h = this._collapsed.read(reader) ? 0 : this._contentHeight.read(reader);
@@ -51,29 +54,21 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
}
});
private readonly _collapsed = observableValue<boolean>(this, false);
private readonly _elements = h('div', {
style: {
display: 'flex',
flexDirection: 'column',
}
}, [
h('div', {
private readonly _elements = h('div.multiDiffEntry', [
h('div.content', {
style: {
display: 'flex',
flexDirection: 'column',
flex: '1',
overflow: 'hidden',
}
}, [
h('div@header', { style: { display: 'flex', alignItems: 'center', padding: '8px 5px', color: 'var(--vscode-foreground)', background: 'var(--vscode-editor-background)', zIndex: '10000' } }, [
h('div.expand-button@collapseButton', { style: { margin: '0 5px' } }),
h('div.show-file-icons@title', { style: { fontSize: '14px', lineHeight: '22px' } }, [] as any),
h('div.header@header', [
h('div.collapse-button@collapseButton'),
h('div.title.show-file-icons@title', [] as any),
]),
h('div', {
h('div.editorParent', {
style: {
flex: '1',
display: 'flex',
@@ -85,10 +80,14 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
])
]);
private readonly _editor = this._register(this._instantiationService.createInstance(DiffEditorWidget, this._elements.editor, {
public readonly editor = this._register(this._instantiationService.createInstance(DiffEditorWidget, this._elements.editor, {
overflowWidgetsDomNode: this._overflowWidgetsDomNode,
}, {}));
private readonly isModifedFocused = isFocused(this.editor.getModifiedEditor());
private readonly isOriginalFocused = isFocused(this.editor.getOriginalEditor());
public readonly isFocused = derived(this, reader => this.isModifedFocused.read(reader) || this.isOriginalFocused.read(reader));
private readonly _resourceLabel = this._workbenchUIElementFactory.createResourceLabel
? this._register(this._workbenchUIElementFactory.createResourceLabel(this._elements.title))
: undefined;
@@ -110,25 +109,28 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
btn.icon = this._collapsed.read(reader) ? Codicon.chevronRight : Codicon.chevronDown;
}));
this._register(btn.onDidClick(() => {
this._collapsed.set(!this._collapsed.get(), undefined);
this._elements.editor.style.display = this._collapsed.get() ? 'none' : 'block';
this._viewModel.get()?.collapsed.set(!this._collapsed.get(), undefined);
}));
this._editor.getModifiedEditor().onDidLayoutChange(e => {
const width = this._editor.getModifiedEditor().getLayoutInfo().contentWidth;
this._register(autorun(reader => {
this._elements.editor.style.display = this._collapsed.read(reader) ? 'none' : 'block';
}));
this.editor.getModifiedEditor().onDidLayoutChange(e => {
const width = this.editor.getModifiedEditor().getLayoutInfo().contentWidth;
this._modifiedWidth.set(width, undefined);
});
this._editor.getOriginalEditor().onDidLayoutChange(e => {
const width = this._editor.getOriginalEditor().getLayoutInfo().contentWidth;
this.editor.getOriginalEditor().onDidLayoutChange(e => {
const width = this.editor.getOriginalEditor().getLayoutInfo().contentWidth;
this._originalWidth.set(width, undefined);
});
this._register(this._editor.onDidContentSizeChange(e => {
this._register(this.editor.onDidContentSizeChange(e => {
globalTransaction(tx => {
this._contentHeight.set(e.contentHeight, tx);
this._modifiedContentWidth.set(this._editor.getModifiedEditor().getContentWidth(), tx);
this._originalContentWidth.set(this._editor.getOriginalEditor().getContentWidth(), tx);
this._modifiedContentWidth.set(this.editor.getModifiedEditor().getContentWidth(), tx);
this._originalContentWidth.set(this.editor.getOriginalEditor().getContentWidth(), tx);
});
}));
@@ -138,18 +140,15 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
public setScrollLeft(left: number): void {
if (this._modifiedContentWidth.get() - this._modifiedWidth.get() > this._originalContentWidth.get() - this._originalWidth.get()) {
this._editor.getModifiedEditor().setScrollLeft(left);
this.editor.getModifiedEditor().setScrollLeft(left);
} else {
this._editor.getOriginalEditor().setScrollLeft(left);
this.editor.getOriginalEditor().setScrollLeft(left);
}
}
private readonly _dataStore = new DisposableStore();
public setData(data: TemplateData): void {
this._resourceLabel?.setUri(data.viewModel.model.modified.uri);
this._dataStore.clear();
function updateOptions(options: IDiffEditorOptions): IDiffEditorOptions {
return {
...options,
@@ -161,20 +160,26 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
vertical: 'hidden',
horizontal: 'hidden',
handleMouseWheel: false,
useShadows: false,
},
renderOverviewRuler: false,
fixedOverflowWidgets: true,
};
}
if (data.entry.onOptionsDidChange) {
this._dataStore.add(data.entry.onOptionsDidChange(() => {
this._editor.updateOptions(updateOptions(data.entry.options ?? {}));
const value = data.viewModel.entry.value!; // TODO
if (value.onOptionsDidChange) {
this._dataStore.add(value.onOptionsDidChange(() => {
this.editor.updateOptions(updateOptions(value.options ?? {}));
}));
}
globalTransaction(tx => {
this._editor.setModel(data.viewModel, tx);
this._editor.updateOptions(updateOptions(data.entry.options ?? {}));
this._resourceLabel?.setUri(data.viewModel.diffEditorViewModel.model.modified.uri);
this._dataStore.clear();
this._viewModel.set(data.viewModel, tx);
this.editor.setModel(data.viewModel.diffEditorViewModel, tx);
this.editor.updateOptions(updateOptions(value.options ?? {}));
});
}
@@ -186,15 +191,18 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
this._elements.root.style.position = 'absolute';
// For sticky scroll
this._elements.header.style.transform = `translateY(${Math.max(0, Math.min(verticalRange.length - this._elements.header.clientHeight, viewPort.start - verticalRange.start))}px)`;
const delta = Math.max(0, Math.min(verticalRange.length - this._elements.header.clientHeight, viewPort.start - verticalRange.start));
this._elements.header.style.transform = `translateY(${delta}px)`;
globalTransaction(tx => {
this._editor.layout({
this.editor.layout({
width: width,
height: verticalRange.length - this._outerEditorHeight,
});
});
this._editor.getOriginalEditor().setScrollTop(editorScroll);
this.editor.getOriginalEditor().setScrollTop(editorScroll);
this._elements.header.classList.toggle('shadow', delta > 0 || editorScroll > 0);
}
public hide(): void {
@@ -202,3 +210,15 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
this._elements.root.style.visibility = 'hidden'; // Some editor parts are still visible
}
}
function isFocused(editor: ICodeEditor): IObservable<boolean> {
return observableFromEvent(
h => {
const store = new DisposableStore();
store.add(editor.onDidFocusEditorWidget(() => h(true)));
store.add(editor.onDidBlurEditorWidget(() => h(false)));
return store;
},
() => editor.hasWidgetFocus()
);
}
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { derivedWithStore, observableFromEvent, observableValue } from 'vs/base/common/observable';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
import { IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditorWidget/model';
import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon';
export class MultiDiffEditorViewModel extends Disposable {
private readonly _documents = observableFromEvent(this._model.onDidChange, /** @description MultiDiffEditorViewModel.documents */() => this._model.documents);
public readonly items = derivedWithStore<readonly DocumentDiffItemViewModel[]>(this,
(reader, store) => this._documents.read(reader).map(d => store.add(new DocumentDiffItemViewModel(d, this._diffEditorViewModelFactory)))
).recomputeInitiallyAndOnChange(this._store);
public readonly activeDiffItem = observableValue<DocumentDiffItemViewModel | undefined>(this, undefined);
constructor(
private readonly _model: IMultiDiffEditorModel,
private readonly _diffEditorViewModelFactory: DiffEditorWidget,
) {
super();
}
}
export class DocumentDiffItemViewModel extends Disposable {
public readonly diffEditorViewModel: IDiffEditorViewModel;
public readonly collapsed = observableValue<boolean>(this, false);
constructor(
public readonly entry: LazyPromise<IDocumentDiffItem>,
diffEditorViewModelFactory: DiffEditorWidget
) {
super();
this.diffEditorViewModel = this._register(diffEditorViewModelFactory.createViewModel({
original: entry.value!.original!,
modified: entry.value!.modified!,
}));
}
}
@@ -5,14 +5,16 @@
import { Dimension } from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
import { derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable';
import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable';
import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils';
import { IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/model';
import { MultiDiffEditorViewModel, MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl';
import { MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl';
import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import './colors';
import { DiffEditorItemTemplate } from 'vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate';
import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory';
import { Event } from 'vs/base/common/event';
export class MultiDiffEditorWidget extends Disposable {
private readonly _dimension = observableValue<Dimension | undefined>(this, undefined);
@@ -50,4 +52,12 @@ export class MultiDiffEditorWidget extends Disposable {
public layout(dimension: Dimension): void {
this._dimension.set(dimension, undefined);
}
private readonly _activeControl = derived(this, (reader) => this._widgetImpl.read(reader).activeControl.read(reader));
public getActiveControl(): any | undefined {
return this._activeControl.get();
}
public readonly onDidChangeActiveControl = Event.fromObservableLight(this._activeControl);
}
@@ -6,20 +6,20 @@
import { Dimension, getWindow, h, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { Disposable, IReference, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, autorun, derived, derivedWithStore, observableFromEvent, observableValue } from 'vs/base/common/observable';
import { IObservable, IReader, autorun, derived, derivedObservableWithCache, derivedWithStore, observableFromEvent, observableValue } from 'vs/base/common/observable';
import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
import 'vs/css!./style';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
import { ObservableElementSizeObserver } from 'vs/editor/browser/widget/diffEditor/utils';
import { IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditorWidget/model';
import { IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/model';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate';
import { ObjectPool } from './objectPool';
import { disposableObservableValue, globalTransaction, transaction } from 'vs/base/common/observableInternal/base';
import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory';
import { findFirstMaxBy } from 'vs/base/common/arraysFind';
import { MultiDiffEditorViewModel, DocumentDiffItemViewModel } from './multiDiffEditorViewModel';
export class MultiDiffEditorWidgetImpl extends Disposable {
private readonly _elements = h('div', {
@@ -85,6 +85,9 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
);
private readonly _totalHeight = this._viewItems.map(this, (items, reader) => items.reduce((r, i) => r + i.contentHeight.read(reader), 0));
public readonly activeDiffItem = derived(this, reader => this._viewItems.read(reader).find(i => i.template.read(reader)?.isFocused.read(reader)));
public readonly lastActiveDiffItem = derivedObservableWithCache<VirtualizedViewItem | undefined>((reader, lastValue) => this.activeDiffItem.read(reader) ?? lastValue);
public readonly activeControl = derived(this, reader => this.lastActiveDiffItem.read(reader)?.template.read(reader)?.editor);
constructor(
private readonly _element: HTMLElement,
@@ -95,6 +98,13 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
) {
super();
this._register(autorun((reader) => {
const lastActiveDiffItem = this.lastActiveDiffItem.read(reader);
transaction(tx => {
this._viewModel.read(reader)?.activeDiffItem.set(lastActiveDiffItem?.viewModel, tx);
});
}));
this._register(autorun((reader) => {
/** @description Update widget dimension */
const dimension = this._dimension.read(reader);
@@ -179,37 +189,6 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
}
}
export class MultiDiffEditorViewModel extends Disposable {
private readonly _documents = observableFromEvent(this._model.onDidChange, /** @description MultiDiffEditorViewModel.documents */() => this._model.documents);
public readonly items = derivedWithStore<readonly DocumentDiffItemViewModel[]>(this,
(reader, store) => this._documents.read(reader).map(d => store.add(new DocumentDiffItemViewModel(d, this._diffEditorViewModelFactory)))
).recomputeInitiallyAndOnChange(this._store);
constructor(
private readonly _model: IMultiDiffEditorModel,
private readonly _diffEditorViewModelFactory: DiffEditorWidget,
) {
super();
}
}
class DocumentDiffItemViewModel extends Disposable {
public readonly diffEditorViewModel: IDiffEditorViewModel;
constructor(
public readonly entry: LazyPromise<IDocumentDiffItem>,
diffEditorViewModelFactory: DiffEditorWidget,
) {
super();
this.diffEditorViewModel = this._register(diffEditorViewModelFactory.createViewModel({
original: entry.value!.original!,
modified: entry.value!.modified!,
}));
}
}
class VirtualizedViewItem extends Disposable {
// TODO this should be in the view model
private readonly _lastTemplateData = observableValue<{ contentHeight: number; maxScroll: { maxScroll: number; width: number } }>(
@@ -224,8 +203,10 @@ class VirtualizedViewItem extends Disposable {
public readonly maxScroll = derived(this, reader => this._templateRef.read(reader)?.object.maxScroll.read(reader) ?? this._lastTemplateData.read(reader).maxScroll);
public readonly template = derived(this, reader => this._templateRef.read(reader)?.object);
constructor(
private readonly _viewModel: DocumentDiffItemViewModel,
public readonly viewModel: DocumentDiffItemViewModel,
private readonly _objectPool: ObjectPool<TemplateData, DiffEditorItemTemplate>,
private readonly _scrollLeft: IObservable<number>,
) {
@@ -243,7 +224,7 @@ class VirtualizedViewItem extends Disposable {
}
public override toString(): string {
return `VirtualViewItem(${this._viewModel.entry.value!.title})`;
return `VirtualViewItem(${this.viewModel.entry.value!.title})`;
}
public hide(): void {
@@ -263,7 +244,7 @@ class VirtualizedViewItem extends Disposable {
public render(verticalSpace: OffsetRange, offset: number, width: number, viewPort: OffsetRange): void {
let ref = this._templateRef.get();
if (!ref) {
ref = this._objectPool.getUnusedObj(new TemplateData(this._viewModel.diffEditorViewModel, this._viewModel.entry.value!));
ref = this._objectPool.getUnusedObj(new TemplateData(this.viewModel));
this._templateRef.set(ref, undefined);
}
ref.object.render(verticalSpace, width, offset, viewPort);
@@ -3,6 +3,33 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-component .expand-button a {
.monaco-component .multiDiffEntry {
display: flex;
flex-direction: column;
}
.monaco-component .multiDiffEntry .collapse-button {
margin: 0 5px;
}
.monaco-component .multiDiffEntry .collapse-button a {
display: block;
}
.monaco-component .multiDiffEntry .header {
display: flex;
align-items: center;
padding: 8px 5px;
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
z-index: 1000;
}
.monaco-component .multiDiffEntry .header.shadow {
box-shadow: var(--vscode-scrollbar-shadow) 0px 6px 6px -6px;
}
.monaco-component .multiDiffEntry .header .title {
font-size: 14px;
line-height: 22px;
}
+4
View File
@@ -16,6 +16,10 @@ export class LineRange {
return new LineRange(range.startLineNumber, range.endLineNumber);
}
public static fromRangeInclusive(range: Range): LineRange {
return new LineRange(range.startLineNumber, range.endLineNumber + 1);
}
public static subtract(a: LineRange, b: LineRange | undefined): LineRange[] {
if (!b) {
return [a];
@@ -12,7 +12,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { matchesScheme } from 'vs/platform/opener/common/opener';
import { matchesScheme } from 'vs/base/common/network';
export const ILanguageFeatureDebounceService = createDecorator<ILanguageFeatureDebounceService>('ILanguageFeatureDebounceService');
@@ -1308,6 +1308,19 @@ class IdentityCoordinatesConverter implements ICoordinatesConverter {
return true;
}
public modelRangeIsVisible(modelRange: Range): boolean {
const lineCount = this._lines.model.getLineCount();
if (modelRange.startLineNumber < 1 || modelRange.startLineNumber > lineCount) {
// invalid arguments
return false;
}
if (modelRange.endLineNumber < 1 || modelRange.endLineNumber > lineCount) {
// invalid arguments
return false;
}
return true;
}
public getModelLineViewLineCount(modelLineNumber: number): number {
return 1;
}
@@ -759,6 +759,7 @@ class StandaloneTelemetryService implements ITelemetryService {
readonly telemetryLevel = TelemetryLevel.NONE;
readonly sessionId = 'someValue.sessionId';
readonly machineId = 'someValue.machineId';
readonly sqmId = 'someValue.sqmId';
readonly firstSessionDate = 'someValue.firstSessionDate';
readonly sendErrorTelemetry = false;
setEnabled(): void { }
@@ -11,7 +11,7 @@ import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { NullCommandService } from 'vs/platform/commands/test/common/nullCommandService';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { matchesScheme, matchesSomeScheme } from 'vs/platform/opener/common/opener';
import { matchesScheme, matchesSomeScheme } from 'vs/base/common/network';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
suite('OpenerService', function () {
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nativeKeymap from 'native-keymap';
import type * as nativeKeymap from 'native-keymap';
import * as platform from 'vs/base/common/platform';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as spdlog from '@vscode/spdlog';
import type * as spdlog from '@vscode/spdlog';
import { ByteSize } from 'vs/platform/files/common/files';
import { AbstractMessageLogger, ILogger, LogLevel } from 'vs/platform/log/common/log';
@@ -42,7 +42,6 @@ import { WindowProfiler } from 'vs/platform/profiling/electron-main/windowProfil
import { IV8Profile } from 'vs/platform/profiling/common/profiling';
import { IAuxiliaryWindowsMainService, isAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows';
import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow';
import { loadSystemCertificates } from '@vscode/proxy-agent';
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
@@ -766,7 +765,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
async loadCertificates(_windowId: number | undefined): Promise<string[]> {
return loadSystemCertificates({ log: this.logService });
const proxyAgent = await import('@vscode/proxy-agent');
return proxyAgent.loadSystemCertificates({ log: this.logService });
}
findFreePort(windowId: number | undefined, startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
-13
View File
@@ -5,7 +5,6 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IEditorOptions, ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -117,18 +116,6 @@ export interface IOpenerService {
resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri>;
}
export function matchesScheme(target: URI | string, scheme: string): boolean {
if (URI.isUri(target)) {
return equalsIgnoreCase(target.scheme, scheme);
} else {
return startsWithIgnoreCase(target, scheme + ':');
}
}
export function matchesSomeScheme(target: URI | string, ...schemes: string[]): boolean {
return schemes.some(scheme => matchesScheme(target, scheme));
}
/**
* Encodes selection into the `URI`.
*
@@ -6,7 +6,7 @@
import { AbstractPolicyService, IPolicyService, PolicyDefinition } from 'vs/platform/policy/common/policy';
import { IStringDictionary } from 'vs/base/common/collections';
import { Throttler } from 'vs/base/common/async';
import { createWatcher, PolicyUpdate, Watcher } from '@vscode/policy-watcher';
import type { PolicyUpdate, Watcher } from '@vscode/policy-watcher';
import { MutableDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
@@ -25,6 +25,8 @@ export class NativePolicyService extends AbstractPolicyService implements IPolic
protected async _updatePolicyDefinitions(policyDefinitions: IStringDictionary<PolicyDefinition>): Promise<void> {
this.logService.trace(`NativePolicyService#_updatePolicyDefinitions - Found ${Object.keys(policyDefinitions).length} policy definitions`);
const { createWatcher } = await import('@vscode/policy-watcher');
await this.throttler.queue(() => new Promise<void>((c, e) => {
try {
this.watcher.value = createWatcher(this.productName, policyDefinitions, update => {
@@ -20,7 +20,6 @@ import { ILogService, ILoggerService } from 'vs/platform/log/common/log';
import { AbstractRequestService, IRequestService } from 'vs/platform/request/common/request';
import { Agent, getProxyAgent } from 'vs/platform/request/node/proxy';
import { createGunzip } from 'zlib';
import { loadSystemCertificates } from '@vscode/proxy-agent';
interface IHTTPConfiguration {
proxy?: string;
@@ -112,7 +111,8 @@ export class RequestService extends AbstractRequestService implements IRequestSe
}
async loadCertificates(): Promise<string[]> {
return loadSystemCertificates({ log: this.logService });
const proxyAgent = await import('@vscode/proxy-agent');
return proxyAgent.loadSystemCertificates({ log: this.logService });
}
}
@@ -22,6 +22,7 @@ export interface ITelemetryService {
readonly sessionId: string;
readonly machineId: string;
readonly sqmId: string;
readonly firstSessionDate: string;
readonly msftInternal?: boolean;
@@ -33,6 +33,7 @@ export class TelemetryService implements ITelemetryService {
readonly sessionId: string;
readonly machineId: string;
readonly sqmId: string;
readonly firstSessionDate: string;
readonly msftInternal: boolean | undefined;
@@ -56,6 +57,7 @@ export class TelemetryService implements ITelemetryService {
this.sessionId = this._commonProperties['sessionID'] as string;
this.machineId = this._commonProperties['common.machineId'] as string;
this.sqmId = this._commonProperties['common.sqmId'] as string;
this.firstSessionDate = this._commonProperties['common.firstSessionDate'] as string;
this.msftInternal = this._commonProperties['common.msftInternal'] as boolean | undefined;
@@ -31,6 +31,7 @@ export class NullTelemetryServiceShape implements ITelemetryService {
readonly telemetryLevel = TelemetryLevel.NONE;
readonly sessionId = 'someValue.sessionId';
readonly machineId = 'someValue.machineId';
readonly sqmId = 'someValue.sqmId';
readonly firstSessionDate = 'someValue.firstSessionDate';
readonly sendErrorTelemetry = false;
publicLog() { }
@@ -32,7 +32,6 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
private __isCommandStorageDisabled: boolean = false;
private _handleCommandStartOptions?: IHandleCommandOptions;
private _commitCommandFinished?: RunOnceScheduler;
private _ptyHeuristicsHooks: ICommandDetectionHeuristicsHooks;
@@ -99,6 +98,10 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
get commandMarkers() { return that._commandMarkers; }
set commandMarkers(value) { that._commandMarkers = value; }
get clearCommandsInViewport() { return that._clearCommandsInViewport.bind(that); }
commitCommandFinished() {
that._commitCommandFinished?.flush();
that._commitCommandFinished = undefined;
}
};
this._ptyHeuristics = this._register(new MandatoryMutableDisposable(new UnixPtyHeuristics(this._terminal, this, this._ptyHeuristicsHooks, this._logService)));
@@ -172,6 +175,10 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
get commandMarkers() { return that._commandMarkers; }
set commandMarkers(value) { that._commandMarkers = value; }
get clearCommandsInViewport() { return that._clearCommandsInViewport.bind(that); }
commitCommandFinished() {
that._commitCommandFinished?.flush();
that._commitCommandFinished = undefined;
}
},
this._logService
);
@@ -235,8 +242,6 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
this._logService.debug('CommandDetectionCapability#handlePromptStart adjusted commandFinished', `${lastCommand.endMarker.line} -> ${lastCommand.executedMarker.line + 1}`);
lastCommand.endMarker = cloneMarker(this._terminal, lastCommand.executedMarker, 1);
}
this._commitCommandFinished?.flush();
this._commitCommandFinished = undefined;
this._currentCommand.promptStartMarker = options?.marker || (lastCommand?.endMarker ? cloneMarker(this._terminal, lastCommand.endMarker) : this._terminal.registerMarker(0));
@@ -408,6 +413,7 @@ interface ICommandDetectionHeuristicsHooks {
commandMarkers: IMarker[];
clearCommandsInViewport(): void;
commitCommandFinished(): void;
}
type IPtyHeuristics = (
@@ -439,6 +445,8 @@ class UnixPtyHeuristics extends Disposable {
}
async handleCommandStart(options?: IHandleCommandOptions) {
this._hooks.commitCommandFinished();
const currentCommand = this._capability.currentCommand;
currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
currentCommand.commandStartMarker = options?.marker || this._terminal.registerMarker(0);
@@ -507,6 +515,10 @@ class WindowsPtyHeuristics extends Disposable {
private _recentlyPerformedCsiJ = false;
private _tryAdjustCommandStartMarkerScheduler?: RunOnceScheduler;
private _tryAdjustCommandStartMarkerScannedLineCount: number = 0;
private _tryAdjustCommandStartMarkerPollCount: number = 0;
constructor(
private readonly _terminal: Terminal,
private readonly _capability: CommandDetectionCapability,
@@ -592,10 +604,6 @@ class WindowsPtyHeuristics extends Disposable {
}
}
private _tryAdjustCommandStartMarkerScheduler?: RunOnceScheduler;
private _tryAdjustCommandStartMarkerScannedLineCount: number = 0;
private _tryAdjustCommandStartMarkerPollCount: number = 0;
async handleCommandStart() {
this._capability.currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
@@ -661,6 +669,13 @@ class WindowsPtyHeuristics extends Disposable {
this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker adjusted promptStart', `${this._capability.currentCommand.promptStartMarker?.line} -> ${this._capability.currentCommand.commandStartMarker.line}`);
this._capability.currentCommand.promptStartMarker?.dispose();
this._capability.currentCommand.promptStartMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker);
// Adjust the last command if it's not in the same position as the following
// prompt start marker
const lastCommand = this._capability.commands.at(-1);
if (lastCommand && this._capability.currentCommand.commandStartMarker.line !== lastCommand.endMarker?.line) {
lastCommand.endMarker?.dispose();
lastCommand.endMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker);
}
}
// use the regex to set the position as it's possible input has occurred
this._capability.currentCommand.commandStartX = adjustedPrompt.length;
@@ -692,6 +707,8 @@ class WindowsPtyHeuristics extends Disposable {
this._tryAdjustCommandStartMarkerScheduler = undefined;
}
this._hooks.commitCommandFinished();
if (!this._capability.currentCommand.commandExecutedMarker) {
this._onCursorMoveListener.value = this._terminal.onCursorMove(() => {
if (this._hooks.commandMarkers.length === 0 || this._hooks.commandMarkers[this._hooks.commandMarkers.length - 1].line !== this._terminal.buffer.active.cursorY) {
@@ -16,10 +16,11 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService';
import { INotebookCellExecution, INotebookExecution, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
import { IKernelSourceActionProvider, INotebookKernel, INotebookKernelChangeEvent, INotebookKernelDetectionTask, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { IKernelSourceActionProvider, INotebookKernel, INotebookKernelChangeEvent, INotebookKernelDetectionTask, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async';
abstract class MainThreadKernel implements INotebookKernel {
private readonly _onDidChange = new Emitter<INotebookKernelChangeEvent>();
@@ -36,6 +37,7 @@ abstract class MainThreadKernel implements INotebookKernel {
detail?: string;
supportedLanguages: string[];
implementsExecutionOrder: boolean;
hasVariableProvider: boolean;
localResourceRoot: URI;
public get preloadUris() {
@@ -57,6 +59,7 @@ abstract class MainThreadKernel implements INotebookKernel {
this.detail = data.detail;
this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : _languageService.getRegisteredLanguageIds();
this.implementsExecutionOrder = data.supportsExecutionOrder ?? false;
this.hasVariableProvider = data.hasVariableProvider ?? false;
this.localResourceRoot = URI.revive(data.extensionLocation);
this.preloads = data.preloads?.map(u => ({ uri: URI.revive(u.uri), provides: u.provides })) ?? [];
}
@@ -89,11 +92,16 @@ abstract class MainThreadKernel implements INotebookKernel {
this.implementsInterrupt = data.supportsInterrupt;
event.hasInterruptHandler = true;
}
if (data.hasVariableProvider !== undefined) {
this.hasVariableProvider = data.hasVariableProvider;
event.hasVariableProvider = true;
}
this._onDidChange.fire(event);
}
abstract executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise<void>;
abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>;
abstract provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult>;
}
class MainThreadKernelDetectionTask implements INotebookKernelDetectionTask {
@@ -214,6 +222,15 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape
return didSend;
}
private variableRequestIndex = 0;
private variableRequestMap = new Map<string, AsyncIterableSource<VariablesResult>>();
$receiveVariable(requestId: string, variable: VariablesResult) {
const source = this.variableRequestMap.get(requestId);
if (source) {
source.emitOne(variable);
}
}
// --- kernel adding/updating/removal
async $addKernel(handle: number, data: INotebookKernelDto2): Promise<void> {
@@ -225,6 +242,24 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape
async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise<void> {
await that._proxy.$cancelCells(handle, uri, handles);
}
provideVariables(notebookUri: URI, parentName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {
const requestId = `${handle}variables${that.variableRequestIndex++}`;
if (that.variableRequestMap.has(requestId)) {
return that.variableRequestMap.get(requestId)!.asyncIterable;
}
const source = new AsyncIterableSource<VariablesResult>();
that.variableRequestMap.set(requestId, source);
that._proxy.$provideVariables(handle, requestId, notebookUri, parentName, kind, start, token).then(() => {
source.resolve();
that.variableRequestMap.delete(requestId);
}).catch((err) => {
source.reject(err);
that.variableRequestMap.delete(requestId);
});
return source.asyncIterable;
}
}(data, this._languageService);
const listener = this._notebookKernelService.onDidChangeSelectedNotebooks(e => {
@@ -405,4 +440,8 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape
emitter.fire(undefined);
}
}
$variablesUpdated(notebookUri: UriComponents): void {
this._notebookKernelService.notifyVariablesChange(URI.revive(notebookUri));
}
}
+7 -19
View File
@@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, DisposableStore, combinedDisposable, dispose, DisposableMap } from 'vs/base/common/lifecycle';
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor, ISCMInputValueProvider, ISCMInputValueProviderContext } from 'vs/workbench/contrib/scm/common/scm';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto, SCMInputActionButtonDto } from '../common/extHost.protocol';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMActionButtonDto, SCMHistoryItemGroupDto, SCMInputActionButtonDto } from '../common/extHost.protocol';
import { Command } from 'vs/editor/common/languages';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -18,6 +18,7 @@ import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/c
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemGroupDetails, ISCMHistoryItemGroupEntry, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history';
import { ResourceTree } from 'vs/base/common/resourceTree';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { Codicon } from 'vs/base/common/codicons';
function getSCMInputBoxActionButtonIcon(actionButton: SCMInputActionButtonDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
if (!actionButton.icon) {
@@ -32,19 +33,6 @@ function getSCMInputBoxActionButtonIcon(actionButton: SCMInputActionButtonDto):
}
}
function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
if (!historyItem.icon) {
return undefined;
} else if (URI.isUri(historyItem.icon)) {
return URI.revive(historyItem.icon);
} else if (ThemeIcon.isThemeIcon(historyItem.icon)) {
return historyItem.icon;
} else {
const icon = historyItem.icon as { light: UriComponents; dark: UriComponents };
return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) };
}
}
function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
if (iconDto === undefined) {
return undefined;
@@ -184,8 +172,8 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
if (historyItemGroupBase) {
incoming = {
id: historyItemGroupBase.id,
label: `$(cloud-download) ${historyItemGroupBase.label}`,
// description: localize('incoming', "Incoming Changes"),
label: historyItemGroupBase.label,
icon: Codicon.cloudDownload,
ancestor: ancestor?.id,
count: ancestor?.behind ?? 0,
};
@@ -194,8 +182,8 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
// Outgoing
const outgoing: ISCMHistoryItemGroupEntry = {
id: historyItemGroup.id,
label: `$(cloud-upload) ${historyItemGroup.label}`,
// description: localize('outgoing', "Outgoing Changes"),
label: historyItemGroup.label,
icon: Codicon.cloudUpload,
ancestor: ancestor?.id,
count: ancestor?.ahead ?? 0,
};
@@ -213,7 +201,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined> {
const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None);
return historyItems?.map(historyItem => ({ ...historyItem, icon: getSCMHistoryItemIcon(historyItem), }));
return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) }));
}
async provideHistoryItemChanges(historyItemId: string): Promise<ISCMHistoryItemChange[] | undefined> {
@@ -7,7 +7,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation';
import * as errors from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { combinedDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { Schemas, matchesScheme } from 'vs/base/common/network';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
@@ -18,7 +18,6 @@ import { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/exten
import * as files from 'vs/platform/files/common/files';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILogService, ILoggerService, LogLevel } from 'vs/platform/log/common/log';
import { matchesScheme } from 'vs/platform/opener/common/opener';
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUtils';
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
@@ -1574,6 +1573,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
NotebookControllerAffinity2: extHostTypes.NotebookControllerAffinity2,
NotebookEdit: extHostTypes.NotebookEdit,
NotebookKernelSourceAction: extHostTypes.NotebookKernelSourceAction,
NotebookVariablesRequestKind: extHostTypes.NotebookVariablesRequestKind,
PortAttributes: extHostTypes.PortAttributes,
LinkedEditingRanges: extHostTypes.LinkedEditingRanges,
TestResultState: extHostTypes.TestResultState,
@@ -80,6 +80,7 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel';
import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
import * as search from 'vs/workbench/services/search/common/search';
import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { VariablesResult } from 'vscode';
export interface IWorkspaceData extends IStaticWorkspaceData {
folders: { uri: UriComponents; name: string; index: number }[];
@@ -1075,6 +1076,7 @@ export interface INotebookKernelDto2 {
supportsInterrupt?: boolean;
supportsExecutionOrder?: boolean;
preloads?: { uri: UriComponents; provides: readonly string[] }[];
hasVariableProvider?: boolean;
}
export interface INotebookProxyKernelDto {
@@ -1131,6 +1133,8 @@ export interface MainThreadNotebookKernelsShape extends IDisposable {
$addKernelSourceActionProvider(handle: number, eventHandle: number, notebookType: string): Promise<void>;
$removeKernelSourceActionProvider(handle: number, eventHandle: number): void;
$emitNotebookKernelSourceActionsChangeEvent(eventHandle: number): void;
$receiveVariable(requestId: string, variable: VariablesResult): void;
$variablesUpdated(notebookUri: UriComponents): void;
}
export interface MainThreadNotebookRenderersShape extends IDisposable {
@@ -2538,6 +2542,7 @@ export interface ExtHostNotebookKernelsShape {
$acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void;
$cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void;
$provideKernelSourceActions(handle: number, token: CancellationToken): Promise<notebookCommon.INotebookKernelSourceAction[]>;
$provideVariables(handle: number, requestId: string, notebookUri: UriComponents, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise<void>;
}
export interface ExtHostInteractiveShape {
@@ -5,7 +5,7 @@
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { VSBuffer } from 'vs/base/common/buffer';
import { Schemas } from 'vs/base/common/network';
import { Schemas, matchesSomeScheme } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IPosition } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
@@ -13,7 +13,6 @@ import * as languages from 'vs/editor/common/languages';
import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto';
import { validateWhenClauses } from 'vs/platform/contextkey/common/contextkey';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { matchesSomeScheme } from 'vs/platform/opener/common/opener';
import { ICallHierarchyItemDto, IIncomingCallDto, IInlineValueContextDto, IOutgoingCallDto, IRawColorInfo, ITypeHierarchyItemDto, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol';
import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures';
@@ -96,6 +96,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
}
const convertedProgress = typeConvert.ChatResponseProgress.from(agent.extension, progress);
if (!convertedProgress) {
this._logService.error('Unknown progress type: ' + JSON.stringify(progress));
return;
}
if ('placeholder' in progress && 'resolvedContent' in progress) {
const resolvedContent = Promise.all([this._proxy.$handleProgressChunk(requestId, convertedProgress), progress.resolvedContent]);
raceCancellation(resolvedContent, token).then(res => {
@@ -104,6 +109,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
}
const [progressHandle, progressContent] = res;
const convertedContent = typeConvert.ChatResponseProgress.from(agent.extension, progressContent);
if (!convertedContent) {
this._logService.error('Unknown progress type: ' + JSON.stringify(progressContent));
return;
}
this._proxy.$handleProgressChunk(requestId, convertedContent, progressHandle ?? undefined);
});
} else {
@@ -18,7 +18,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
import { ExtHostCell, ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument';
import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { NotebookCellExecutionState as ExtHostNotebookCellExecutionState, NotebookCellOutput, NotebookControllerAffinity2 } from 'vs/workbench/api/common/extHostTypes';
import { NotebookCellExecutionState as ExtHostNotebookCellExecutionState, NotebookCellOutput, NotebookControllerAffinity2, NotebookVariablesRequestKind } from 'vs/workbench/api/common/extHostTypes';
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webview';
import { INotebookKernelSourceAction, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService';
@@ -125,6 +125,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
//
let _executeHandler = handler ?? _defaultExecutHandler;
let _interruptHandler: ((this: vscode.NotebookController, notebook: vscode.NotebookDocument) => void | Thenable<void>) | undefined;
let _variableProvider: vscode.NotebookVariableProvider | undefined;
this._proxy.$addKernel(handle, data).catch(err => {
// this can happen when a kernel with that ID is already registered
@@ -207,6 +208,16 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
data.supportsInterrupt = Boolean(value);
_update();
},
set variableProvider(value) {
checkProposedApiEnabled(extension, 'notebookVariableProvider');
_variableProvider = value;
data.hasVariableProvider = !!value;
value?.onDidChangeVariables(e => that._proxy.$variablesUpdated(e.uri));
_update();
},
get variableProvider() {
return _variableProvider;
},
createNotebookCellExecution(cell) {
if (isDisposed) {
throw new Error('notebook controller is DISPOSED');
@@ -404,6 +415,29 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
}
}
async $provideVariables(handle: number, requestId: string, notebookUri: UriComponents, parentName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise<void> {
const obj = this._kernelData.get(handle);
if (!obj) {
return;
}
const document = this._extHostNotebook.getNotebookDocument(URI.revive(notebookUri));
const variableProvider = obj.controller.variableProvider;
if (!variableProvider) {
return;
}
const parent = parentName ? { name: parentName, value: '' } : undefined;
const requestKind = kind === 'named' ? NotebookVariablesRequestKind.Named : NotebookVariablesRequestKind.Indexed;
const variables = variableProvider.provideVariables(document.apiNotebook, parent, requestKind, start, token);
for await (const variable of variables) {
if (token.isCancellationRequested) {
return;
}
this._proxy.$receiveVariable(requestId, variable);
}
}
$acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void {
const obj = this._kernelData.get(handle);
if (!obj) {
@@ -103,6 +103,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap
commonProperties['common.extversion'] = extension.version;
commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId;
commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId;
commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId;
commonProperties['common.vscodeversion'] = this.initData.version;
commonProperties['common.isnewappinstall'] = isNewAppInstall(this.initData.telemetryInfo.firstSessionDate);
commonProperties['common.product'] = this.initData.environment.appHost;
@@ -2295,13 +2295,18 @@ export namespace InteractiveEditorResponseFeedbackKind {
}
export namespace ChatResponseProgress {
export function from(extension: IExtensionDescription, progress: vscode.ChatAgentExtendedProgress): extHostProtocol.IChatProgressDto {
export function from(extension: IExtensionDescription, progress: vscode.ChatAgentExtendedProgress): extHostProtocol.IChatProgressDto | undefined {
if ('placeholder' in progress && 'resolvedContent' in progress) {
return { content: progress.placeholder, kind: 'asyncContent' } satisfies extHostProtocol.IChatAsyncContentDto;
} else if ('markdownContent' in progress) {
checkProposedApiEnabled(extension, 'chatAgents2Additions');
return { content: MarkdownString.from(progress.markdownContent), kind: 'markdownContent' };
} else if ('content' in progress) {
if ('vulnerability' in progress && progress.vulnerability) {
checkProposedApiEnabled(extension, 'chatAgents2Additions');
return { content: progress.content, title: progress.vulnerability.title, description: progress.vulnerability!.description, kind: 'vulnerability' };
}
if (typeof progress.content === 'string') {
return { content: progress.content, kind: 'content' };
}
@@ -2344,7 +2349,7 @@ export namespace ChatResponseProgress {
} else if ('message' in progress) {
return { content: progress.message, kind: 'progressMessage' };
} else {
throw new Error('Invalid progress type: ' + JSON.stringify(progress));
return undefined;
}
}
}
@@ -3798,6 +3798,11 @@ export class NotebookKernelSourceAction {
) { }
}
export enum NotebookVariablesRequestKind {
Named = 1,
Indexed = 2
}
//#endregion
//#region Timeline
@@ -42,6 +42,7 @@ suite('ExtHostTelemetry', function () {
firstSessionDate: '2020-01-01T00:00:00.000Z',
sessionId: 'test',
machineId: 'test',
sqmId: 'test'
};
const mockRemote = {
@@ -7,16 +7,15 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isEqual, dirname } from 'vs/base/common/resources';
import { Schemas, matchesSomeScheme } from 'vs/base/common/network';
import { dirname, isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Schemas } from 'vs/base/common/network';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { FileKind } from 'vs/platform/files/common/files';
import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { IEditorPane } from 'vs/workbench/common/editor';
import { matchesSomeScheme } from 'vs/platform/opener/common/opener';
import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline';
export class FileElement {
constructor(
@@ -41,7 +41,7 @@ import {
ReOpenInTextEditorAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction, ToggleEditorTypeAction, SplitEditorToAboveGroupAction, SplitEditorToBelowGroupAction,
SplitEditorToFirstGroupAction, SplitEditorToLastGroupAction, SplitEditorToLeftGroupAction, SplitEditorToNextGroupAction, SplitEditorToPreviousGroupAction, SplitEditorToRightGroupAction, NavigateForwardInEditsAction,
NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction,
MaximizeGroupHideSidebarAction, ExperimentalMoveEditorIntoNewWindowAction, ToggleMaximizeEditorGroupAction, MinimizeOtherGroupsHideSidebarAction
MaximizeGroupHideSidebarAction, MoveEditorToNewDetachedWindowAction, CopyEditorToNewDetachedWindowAction, RestoreEditorsToMainWindowAction, ToggleMaximizeEditorGroupAction, MinimizeOtherGroupsHideSidebarAction, CopyEditorGroupToNewDetachedWindowAction, MoveEditorGroupToNewDetachedWindowAction
} from 'vs/workbench/browser/parts/editor/editorActions';
import {
CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID,
@@ -292,7 +292,12 @@ registerAction2(QuickAccessLeastRecentlyUsedEditorAction);
registerAction2(QuickAccessPreviousRecentlyUsedEditorInGroupAction);
registerAction2(QuickAccessLeastRecentlyUsedEditorInGroupAction);
registerAction2(QuickAccessPreviousEditorFromHistoryAction);
registerAction2(ExperimentalMoveEditorIntoNewWindowAction);
registerAction2(MoveEditorToNewDetachedWindowAction);
registerAction2(CopyEditorToNewDetachedWindowAction);
registerAction2(MoveEditorGroupToNewDetachedWindowAction);
registerAction2(CopyEditorGroupToNewDetachedWindowAction);
registerAction2(RestoreEditorsToMainWindowAction);
const quickAccessNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker';
KeybindingsRegistry.registerCommandAndKeybindingRule({
@@ -14,7 +14,7 @@ import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP } from 'vs/workbench/browser/parts/editor/editorCommands';
import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
@@ -33,9 +33,10 @@ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ILogService } from 'vs/platform/log/common/log';
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext, AuxiliaryBarVisibleContext, EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext, MultipleEditorGroupsContext, SideBarVisibleContext } from 'vs/workbench/common/contextkeys';
import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext, AuxiliaryBarVisibleContext, EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, MultipleEditorGroupsContext, SideBarVisibleContext } from 'vs/workbench/common/contextkeys';
import { URI } from 'vs/base/common/uri';
import { getActiveDocument } from 'vs/base/browser/dom';
import { ICommandActionTitle } from 'vs/platform/action/common/action';
class ExecuteCommandAction extends Action2 {
@@ -225,7 +226,7 @@ export class JoinAllGroupsAction extends Action2 {
override async run(accessor: ServicesAccessor): Promise<void> {
const editorGroupService = accessor.get(IEditorGroupsService);
editorGroupService.mergeAllGroups();
editorGroupService.mergeAllGroups(editorGroupService.activeGroup);
}
}
@@ -2485,16 +2486,17 @@ export class ReOpenInTextEditorAction extends Action2 {
}
}
export class ExperimentalMoveEditorIntoNewWindowAction extends Action2 {
constructor() {
abstract class BaseMoveCopyEditorToNewDetachedWindowAction extends Action2 {
constructor(
id: string,
title: ICommandActionTitle,
private readonly move: boolean
) {
super({
id: 'workbench.action.experimentalMoveEditorIntoNewWindowAction',
title: {
value: localize('popEditorOut', "Move Active Editor into a New Window (Experimental)"),
mnemonicTitle: localize({ key: 'miPopEditorOut', comment: ['&& denotes a mnemonic'] }, "&&Move Active Editor into a New Window (Experimental)"),
original: 'Move Active Editor into a New Window (Experimental)'
},
id,
title,
category: Categories.View,
precondition: ActiveEditorContext,
f1: true
@@ -2512,6 +2514,124 @@ export class ExperimentalMoveEditorIntoNewWindowAction extends Action2 {
const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart();
activeEditorPane.group.moveEditor(activeEditorPane.input, auxiliaryEditorPart.activeGroup);
if (this.move) {
activeEditorPane.group.moveEditor(activeEditorPane.input, auxiliaryEditorPart.activeGroup);
} else {
activeEditorPane.group.copyEditor(activeEditorPane.input, auxiliaryEditorPart.activeGroup);
}
auxiliaryEditorPart.activeGroup.focus();
}
}
export class MoveEditorToNewDetachedWindowAction extends BaseMoveCopyEditorToNewDetachedWindowAction {
constructor() {
super(
'workbench.action.moveEditorToNewDetachedWindow',
{
value: localize('moveEditorToNewDetachedWindow', "Move Editor into a New Detached Window"),
mnemonicTitle: localize({ key: 'miMoveEditorToNewDetachedWindow', comment: ['&& denotes a mnemonic'] }, "&&Move Editor into a New Detached Window"),
original: 'Move Editor into a New Detached Window'
},
true
);
}
}
export class CopyEditorToNewDetachedWindowAction extends BaseMoveCopyEditorToNewDetachedWindowAction {
constructor() {
super(
'workbench.action.copyEditorToNewDetachedWindow',
{
value: localize('copyEditorToNewDetachedWindow', "Copy Editor into a New Detached Window"),
mnemonicTitle: localize({ key: 'miCopyEditorToNewDetachedWindow', comment: ['&& denotes a mnemonic'] }, "&&Copy Editor into a New Detached Window"),
original: 'Copy Editor into a New Detached Window'
},
false
);
}
}
abstract class BaseMoveCopyEditorGroupToNewDetachedWindowAction extends Action2 {
constructor(
id: string,
title: ICommandActionTitle,
private readonly move: boolean
) {
super({
id,
title,
category: Categories.View,
f1: true
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const editorGroupService = accessor.get(IEditorGroupsService);
const activeGroup = editorGroupService.activeGroup;
const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart();
editorGroupService.mergeGroup(activeGroup, auxiliaryEditorPart.activeGroup, {
mode: this.move ? MergeGroupMode.MOVE_EDITORS : MergeGroupMode.COPY_EDITORS
});
auxiliaryEditorPart.activeGroup.focus();
}
}
export class MoveEditorGroupToNewDetachedWindowAction extends BaseMoveCopyEditorGroupToNewDetachedWindowAction {
constructor() {
super(
'workbench.action.moveEditorGroupToNewDetachedWindow',
{
value: localize('moveEditorGroupToNewDetachedWindow', "Move Editor Group into a New Detached Window"),
mnemonicTitle: localize({ key: 'miMoveEditorGroupToNewDetachedWindow', comment: ['&& denotes a mnemonic'] }, "&&Move Editor Group into a New Detached Window"),
original: 'Move Editor Group into a New Detached Window'
},
true
);
}
}
export class CopyEditorGroupToNewDetachedWindowAction extends BaseMoveCopyEditorGroupToNewDetachedWindowAction {
constructor() {
super(
'workbench.action.copyEditorGroupToNewDetachedWindow',
{
value: localize('copyEditorGroupToNewDetachedWindow', "Copy Editor Group into a New Detached Window"),
mnemonicTitle: localize({ key: 'miCopyEditorGroupToNewDetachedWindow', comment: ['&& denotes a mnemonic'] }, "&&Copy Editor Group into a New Detached Window"),
original: 'Copy Editor Group into a New Detached Window'
},
false
);
}
}
export class RestoreEditorsToMainWindowAction extends Action2 {
constructor() {
super({
id: 'workbench.action.restoreDetachedEditorsToMainWindow',
title: {
value: localize('restoreDetachedEditorsToMainWindow', "Restore Detached Editors into Main Window"),
mnemonicTitle: localize({ key: 'miRestoreDetachedEditorsToMainWindow', comment: ['&& denotes a mnemonic'] }, "&&Restore Detached Editors into Main Window"),
original: 'Restore Detached Editors into Main Window'
},
f1: true,
precondition: IsAuxiliaryWindowFocusedContext,
category: Categories.View
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const editorGroupService = accessor.get(IEditorGroupsService);
editorGroupService.mergeAllGroups(editorGroupService.mainPart.activeGroup);
}
}
@@ -3,44 +3,44 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { isObject, isString, isUndefined, isNumber } from 'vs/base/common/types';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IEditorIdentifier, IEditorCommandsContext, CloseDirection, IVisibleEditorPane, EditorsOrder, EditorInputCapabilities, isEditorIdentifier, isEditorInputWithOptionsAndGroup, IUntitledTextResourceEditorInput, IResourceDiffEditorInput } from 'vs/workbench/common/editor';
import { TextCompareEditorVisibleContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, ActiveEditorStickyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, TextCompareEditorActiveContext, SideBySideEditorActiveContext } from 'vs/workbench/common/contextkeys';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { EditorGroupColumn, columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor';
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IListService, IOpenEvent } from 'vs/platform/list/browser/listService';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { distinct, coalesce } from 'vs/base/common/arrays';
import { IEditorGroupsService, IEditorGroup, GroupDirection, GroupLocation, GroupsOrder, preferredSideBySideGroupDirection, EditorGroupLayout, isEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands';
import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
import { EditorResolution, IEditorOptions, IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { Schemas } from 'vs/base/common/network';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { extname } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { getActiveElement } from 'vs/base/browser/dom';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { coalesce, distinct } from 'vs/base/common/arrays';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Schemas, matchesScheme } from 'vs/base/common/network';
import { extname } from 'vs/base/common/resources';
import { isNumber, isObject, isString, isUndefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { localize } from 'vs/nls';
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { EditorResolution, IEditorOptions, IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IListService, IOpenEvent } from 'vs/platform/list/browser/listService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess';
import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor';
import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor';
import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext, TextCompareEditorVisibleContext } from 'vs/workbench/common/contextkeys';
import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { EditorGroupColumn, columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, isEditorGroup, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors';
export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup';
@@ -132,7 +132,7 @@ export class EditorPanes extends Disposable {
// Assert the `EditorInputCapabilities.AuxWindowUnsupported` condition
if (getWindow(this.editorPanesParent) !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) {
return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in floating windows yet."), [
return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in detached windows yet."), [
toAction({
id: 'workbench.editor.action.closeEditor', label: localize('openFolder', "Close Editor"), run: async () => {
return this.groupView.closeEditor(editor);
@@ -910,16 +910,18 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
return targetView;
}
mergeAllGroups(target = this.activeGroup): IEditorGroupView {
mergeAllGroups(target: IEditorGroupView | GroupIdentifier): IEditorGroupView {
const targetView = this.assertGroupView(target);
for (const group of this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
if (group === target) {
if (group === targetView) {
continue; // keep target
}
this.mergeGroup(group, target);
this.mergeGroup(group, targetView);
}
return target;
return targetView;
}
protected assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { Event, Emitter } from 'vs/base/common/event';
import { getActiveDocument } from 'vs/base/browser/dom';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -261,6 +261,21 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd
return this.mainPart.getGroup(identifier);
}
private assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
let groupView: IEditorGroupView | undefined;
if (typeof group === 'number') {
groupView = this.getGroup(group);
} else {
groupView = group;
}
if (!groupView) {
throw new Error('Invalid editor group provided!');
}
return groupView;
}
activateGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
return this.getPart(group).activateGroup(group);
}
@@ -305,12 +320,46 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd
this.activePart.setGroupOrientation(orientation);
}
findGroup(scope: IFindGroupScope, source?: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView | undefined {
if (source) {
return this.getPart(source).findGroup(scope, source, wrap);
findGroup(scope: IFindGroupScope, source: IEditorGroupView | GroupIdentifier = this.activeGroup, wrap?: boolean): IEditorGroupView | undefined {
const sourcePart = this.getPart(source);
if (this._parts.size > 1) {
const groups = this.getGroups(GroupsOrder.GRID_APPEARANCE);
// Ensure that FIRST/LAST dispatches globally over all parts
if (scope.location === GroupLocation.FIRST || scope.location === GroupLocation.LAST) {
return scope.location === GroupLocation.FIRST ? groups[0] : groups[groups.length - 1];
}
// Try to find in target part first without wrapping
const group = sourcePart.findGroup(scope, source, false);
if (group) {
return group;
}
// Ensure that NEXT/PREVIOUS dispatches globally over all parts
if (scope.location === GroupLocation.NEXT || scope.location === GroupLocation.PREVIOUS) {
const sourceGroup = this.assertGroupView(source);
const index = groups.indexOf(sourceGroup);
if (scope.location === GroupLocation.NEXT) {
let nextGroup: IEditorGroupView | undefined = groups[index + 1];
if (!nextGroup && wrap) {
nextGroup = groups[0];
}
return nextGroup;
} else {
let previousGroup: IEditorGroupView | undefined = groups[index - 1];
if (!previousGroup && wrap) {
previousGroup = groups[groups.length - 1];
}
return previousGroup;
}
}
}
return this.activePart.findGroup(scope, source, wrap);
return sourcePart.findGroup(scope, source, wrap);
}
addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
@@ -329,8 +378,8 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd
return this.getPart(group).mergeGroup(group, target, options);
}
mergeAllGroups(): IEditorGroupView {
return this.activePart.mergeAllGroups();
mergeAllGroups(target: IEditorGroupView | GroupIdentifier): IEditorGroupView {
return this.activePart.mergeAllGroups(target);
}
copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
@@ -140,7 +140,7 @@ class ViewPaneDropOverlay extends Themable {
this.overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : '';
this.overlay.style.borderColor = activeContrastBorderColor || '';
this.overlay.style.borderStyle = 'solid' || '';
this.overlay.style.borderStyle = 'solid';
this.overlay.style.borderWidth = '0px';
}
+2 -2
View File
@@ -10,7 +10,7 @@ import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, S
import { timeout } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { matchesScheme, Schemas } from 'vs/base/common/network';
import { isIOS, isMacintosh } from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
@@ -19,7 +19,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
@@ -247,7 +247,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'workbench.editor.dragToOpenWindow': {
'type': 'boolean',
'default': product.quality !== 'stable',
'markdownDescription': localize('dragToOpenWindow', "Controls if editors can be dragged out of the window to open them in a new floating window. Press and hold `Alt`-key while dragging to toggle this dynamically.")
'markdownDescription': localize('dragToOpenWindow', "Controls if editors can be dragged out of the window to open them in a new detached window. Press and hold `Alt`-key while dragging to toggle this dynamically.")
},
'workbench.editor.focusRecentEditorAfterClose': {
'type': 'boolean',
@@ -9,6 +9,7 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
export function registerChatCopyActions() {
@@ -24,6 +25,7 @@ export function registerChatCopyActions() {
category: CHAT_CATEGORY,
menu: {
id: MenuId.ChatContext,
when: CONTEXT_RESPONSE_FILTERED.toNegated(),
group: 'copy',
}
});
@@ -36,8 +38,8 @@ export function registerChatCopyActions() {
if (widget) {
const viewModel = widget.viewModel;
const sessionAsText = viewModel?.getItems()
.filter((item): item is (IChatRequestViewModel | IChatResponseViewModel) => isRequestVM(item) || isResponseVM(item))
.map(stringifyItem)
.filter((item): item is (IChatRequestViewModel | IChatResponseViewModel) => isRequestVM(item) || (isResponseVM(item) && !item.errorDetails?.responseIsFiltered))
.map(item => stringifyItem(item))
.join('\n\n');
if (sessionAsText) {
clipboardService.writeText(sessionAsText);
@@ -58,6 +60,7 @@ export function registerChatCopyActions() {
category: CHAT_CATEGORY,
menu: {
id: MenuId.ChatContext,
when: CONTEXT_RESPONSE_FILTERED.toNegated(),
group: 'copy',
}
});
@@ -70,13 +73,16 @@ export function registerChatCopyActions() {
}
const clipboardService = accessor.get(IClipboardService);
const text = stringifyItem(item);
const text = stringifyItem(item, false);
clipboardService.writeText(text);
}
});
}
function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel): string {
return isRequestVM(item) ?
`${item.username}: ${item.messageText}` : `${item.username}: ${item.response.asString()}`;
function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string {
if (isRequestVM(item)) {
return (includeName ? `${item.username}: ` : '') + item.messageText;
} else {
return (includeName ? `${item.username}: ` : '') + item.response.asString();
}
}
@@ -50,14 +50,14 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
import { convertParsedRequestToMarkdown, reduceInlineContentReferences, walkTreeAndAnnotateReferenceLinks } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
import { annotateSpecialMarkdownContent, convertParsedRequestToMarkdown, extractVulnerabilitiesFromText, walkTreeAndAnnotateReferenceLinks } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
import { CodeBlockPart, ICodeBlockData, ICodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatContentInlineReference, IChatContentReference, IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatContentReference, IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseMarkdownRenderData, IChatResponseRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView';
@@ -68,7 +68,7 @@ const $ = dom.$;
interface IChatListItemTemplate {
readonly rowContainer: HTMLElement;
readonly titleToolbar: MenuWorkbenchToolBar;
readonly titleToolbar?: MenuWorkbenchToolBar;
readonly avatarContainer: HTMLElement;
readonly agentAvatarContainer: HTMLElement;
readonly username: HTMLElement;
@@ -94,6 +94,8 @@ export interface IChatRendererDelegate {
export interface IChatListItemRendererOptions {
readonly renderStyle?: 'default' | 'compact';
readonly noHeader?: boolean;
readonly noPadding?: boolean;
}
export class ChatListItemRenderer extends Disposable implements ITreeRenderer<ChatTreeItem, FuzzyScore, IChatListItemTemplate> {
@@ -211,7 +213,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}
layout(width: number): void {
this._currentLayoutWidth = width - 40; // TODO Padding
this._currentLayoutWidth = width - (this.rendererOptions.noPadding ? 0 : 40); // padding
this._editorPool.inUse.forEach(editor => {
editor.layout(this._currentLayoutWidth);
});
@@ -223,6 +225,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
if (this.rendererOptions.renderStyle === 'compact') {
rowContainer.classList.add('interactive-item-compact');
}
if (this.rendererOptions.noPadding) {
rowContainer.classList.add('no-padding');
}
const header = dom.append(rowContainer, $('.header'));
const user = dom.append(header, $('.user'));
const avatarContainer = dom.append(user, $('.avatar-container'));
@@ -238,26 +243,32 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
const contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(rowContainer));
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]));
const titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, header, MenuId.ChatMessageTitle, {
menuOptions: {
shouldForwardArgs: true
},
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.chat.voteDown' || action.item.id === 'workbench.action.chat.voteUp')) {
return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions);
let titleToolbar: MenuWorkbenchToolBar | undefined;
if (this.rendererOptions.noHeader) {
header.classList.add('hidden');
} else {
titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, header, MenuId.ChatMessageTitle, {
menuOptions: {
shouldForwardArgs: true
},
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.chat.voteDown' || action.item.id === 'workbench.action.chat.voteUp')) {
return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions);
}
return undefined;
}
return undefined;
}
}));
}));
}
const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, progressSteps, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService };
return template;
}
renderElement(node: ITreeNode<ChatTreeItem, FuzzyScore>, index: number, templateData: IChatListItemTemplate): void {
const { element } = node;
this.renderChatTreeItem(node.element, index, templateData);
}
renderChatTreeItem(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate): void {
const kind = isRequestVM(element) ? 'request' :
isResponseVM(element) ? 'response' :
'welcome';
@@ -272,7 +283,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set('');
}
templateData.titleToolbar.context = element;
if (templateData.titleToolbar) {
templateData.titleToolbar.context = element;
}
const isFiltered = !!(isResponseVM(element) && element.errorDetails?.responseIsFiltered);
CONTEXT_RESPONSE_FILTERED.bindTo(templateData.contextKeyService).set(isFiltered);
@@ -283,7 +296,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
templateData.rowContainer.classList.toggle('filtered-response', isFiltered);
templateData.rowContainer.classList.toggle('show-progress', isResponseVM(element) && !element.isComplete);
templateData.username.textContent = element.username;
this.renderAvatar(element, templateData);
if (!this.rendererOptions.noHeader) {
this.renderAvatar(element, templateData);
}
dom.clearNode(templateData.detail);
if (isResponseVM(element)) {
@@ -316,7 +331,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
timer.cancelAndSet(runProgressiveRender, 50, dom.getWindow(templateData.rowContainer));
runProgressiveRender(true);
} else if (isResponseVM(element)) {
const renderableResponse = reduceInlineContentReferences(element.response.value);
const renderableResponse = annotateSpecialMarkdownContent(element.response.value);
this.basicRenderElement(renderableResponse, element, index, templateData);
} else if (isRequestVM(element)) {
const markdown = 'kind' in element.message ?
@@ -417,7 +432,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}
}
private basicRenderElement(value: ReadonlyArray<Exclude<IChatProgressResponseContent, IChatContentInlineReference>>, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) {
private basicRenderElement(value: ReadonlyArray<IChatProgressRenderableResponseContent>, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) {
const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete);
dom.clearNode(templateData.value);
@@ -520,7 +535,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
disposables.clear();
const renderableResponse = reduceInlineContentReferences(element.response.value);
const annotatedResult = annotateSpecialMarkdownContent(element.response.value);
const renderableResponse = annotatedResult;
let isFullyRendered = false;
if (element.isCanceled) {
this.traceLayout('runProgressiveRender', `canceled, index=${index}`);
@@ -801,8 +817,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
const result = this.renderer.render(markdown, {
fillInIncompleteTokens,
codeBlockRendererSync: (languageId, text) => {
const vulns = extractVulnerabilitiesFromText(text);
const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered;
const data = { languageId, text, codeBlockIndex: codeBlockIndex++, element, hideToolbar, parentContextKeyService: templateData.contextKeyService };
const data = { languageId, text: vulns.newText, codeBlockIndex: codeBlockIndex++, element, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns: vulns.vulnerabilities };
const ref = this.renderCodeBlock(data, disposables);
// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)
@@ -8,10 +8,11 @@ import { MarkdownString } from 'vs/base/common/htmlContent';
import { revive } from 'vs/base/common/marshalling';
import { basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { Location } from 'vs/editor/common/languages';
import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatProgressRenderableResponseContent, IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatContentInlineReference } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatAgentMarkdownContentWithVulnerability, IChatContentInlineReference } from 'vs/workbench/contrib/chat/common/chatService';
const variableRefUrl = 'http://_vscodedecoration_';
@@ -58,10 +59,39 @@ function renderFileWidget(href: string, a: HTMLAnchorElement): void {
a.setAttribute('data-href', location.uri.with({ fragment }).toString());
}
export interface IMarkdownVulnerability {
title: string;
description: string;
range: IRange;
}
export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } {
const vulnerabilities: IMarkdownVulnerability[] = [];
let newText = text;
let match: RegExpExecArray | null;
while ((match = /<vscode_annotation title="(.*?)" description="(.*?)">(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) {
const [full, title, description, content] = match;
const start = match.index;
const textBefore = newText.substring(0, start);
const linesBefore = textBefore.split('\n').length - 1;
const linesInside = content.split('\n').length - 1;
const previousNewlineIdx = textBefore.lastIndexOf('\n');
const startColumn = start - (previousNewlineIdx + 1) + 1;
const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n');
const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1;
vulnerabilities.push({ title: decodeURIComponent(title), description: decodeURIComponent(description), range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn } });
newText = newText.substring(0, start) + content + newText.substring(start + full.length);
}
return { newText, vulnerabilities };
}
const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI
export function reduceInlineContentReferences(response: ReadonlyArray<IChatProgressResponseContent>): ReadonlyArray<Exclude<IChatProgressResponseContent, IChatContentInlineReference>> {
const result: Exclude<IChatProgressResponseContent, IChatContentInlineReference>[] = [];
export function annotateSpecialMarkdownContent(response: ReadonlyArray<IChatProgressResponseContent>): ReadonlyArray<IChatProgressRenderableResponseContent> {
const result: Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability>[] = [];
for (const item of response) {
const previousItem = result[result.length - 1];
if (item.kind === 'inlineReference') {
@@ -75,6 +105,13 @@ export function reduceInlineContentReferences(response: ReadonlyArray<IChatProgr
}
} else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent') {
result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };
} else if (item.kind === 'markdownVuln') {
const markdownText = `<vscode_annotation title="${encodeURIComponent(item.title)}" description="${encodeURIComponent(item.description)}">${item.content.value}</vscode_annotation>`;
if (previousItem?.kind === 'markdownContent') {
result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };
} else {
result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });
}
} else {
result.push(item);
}
@@ -27,7 +27,7 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { ChatAccessibilityProvider, ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer';
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatReplyFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
@@ -354,12 +354,16 @@ export class ChatWidget extends Disposable implements IChatWidget {
e.browserEvent.preventDefault();
e.browserEvent.stopPropagation();
const selected = e.element;
const scopedContextKeyService = this.contextKeyService.createOverlay([
[CONTEXT_RESPONSE_FILTERED.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered]
]);
this.contextMenuService.showContextMenu({
menuId: MenuId.ChatContext,
menuActionOptions: { shouldForwardArgs: true },
contextKeyService: this.contextKeyService,
contextKeyService: scopedContextKeyService,
getAnchor: () => e.anchor,
getActionsContext: () => e.element,
getActionsContext: () => selected,
});
}
@@ -44,11 +44,12 @@
margin: 16px 0;
}
.interactive-result-code-block .interactive-result-editor .monaco-editor {
.interactive-result-code-block {
border: 1px solid var(--vscode-input-border, transparent);
background-color: var(--vscode-interactive-result-editor-background-color);
}
.interactive-result-code-block .interactive-result-editor .monaco-editor.focused {
.interactive-result-code-block:has(.monaco-editor.focused) {
border-color: var(--vscode-focusBorder, transparent);
}
@@ -57,3 +58,56 @@
.interactive-result-code-block .monaco-editor .overflow-guard {
border-radius: 4px;
}
.interactive-result-code-block .interactive-result-vulns {
font-size: 0.9em;
padding: 0px 8px 2px 8px;
}
.interactive-result-code-block .interactive-result-vulns-header {
display: flex;
height: 22px;
}
.interactive-result-code-block .interactive-result-vulns-header,
.interactive-result-code-block .interactive-result-vulns-list {
opacity: 0.8;
}
.interactive-result-code-block .interactive-result-vulns-list {
margin: 0px;
padding-bottom: 3px;
padding-left: 16px !important; /* Override markdown styles */
}
.interactive-result-code-block.chat-vulnerabilities-collapsed .interactive-result-vulns-list {
display: none;
}
.interactive-result-code-block .interactive-result-vulns-list .chat-vuln-title {
font-weight: bold;
}
.interactive-result-code-block.no-vulns .interactive-result-vulns {
display: none;
}
.interactive-result-code-block .interactive-result-vulns-header .monaco-button {
/* unset Button styles */
display: inline-flex;
width: 100%;
border: none;
padding: 0;
text-align: initial;
justify-content: initial;
color: var(--vscode-foreground) !important; /* This is inside .rendered-markdown */
user-select: none;
}
.interactive-result-code-block .interactive-result-vulns-header .monaco-text-button:focus {
outline: none;
}
.interactive-result-code-block .interactive-result-vulns-header .monaco-text-button:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
}
@@ -9,6 +9,8 @@ import * as dom from 'vs/base/browser/dom';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Button } from 'vs/base/browser/ui/button/button';
import { Codicon } from 'vs/base/common/codicons';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
@@ -31,14 +33,15 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IMarkdownVulnerability } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
const $ = dom.$;
export interface ICodeBlockData {
text: string;
languageId: string;
@@ -46,6 +49,7 @@ export interface ICodeBlockData {
element: unknown;
parentContextKeyService?: IContextKeyService;
hideToolbar?: boolean;
vulns?: IMarkdownVulnerability[];
}
export interface ICodeBlockActionContext {
@@ -57,7 +61,7 @@ export interface ICodeBlockActionContext {
export interface ICodeBlockPart {
readonly onDidChangeContentHeight: Event<number>;
readonly onDidChangeContentHeight: Event<void>;
readonly element: HTMLElement;
readonly textModel: ITextModel;
layout(width: number): void;
@@ -69,16 +73,20 @@ export interface ICodeBlockPart {
const defaultCodeblockPadding = 10;
export class CodeBlockPart extends Disposable implements ICodeBlockPart {
private readonly _onDidChangeContentHeight = this._register(new Emitter<number>());
private readonly _onDidChangeContentHeight = this._register(new Emitter<void>());
public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event;
private readonly editor: CodeEditorWidget;
private readonly toolbar: MenuWorkbenchToolBar;
private readonly contextKeyService: IContextKeyService;
private readonly vulnsButton: Button;
private readonly vulnsListElement: HTMLElement;
public readonly textModel: ITextModel;
public readonly element: HTMLElement;
private currentCodeBlockData: ICodeBlockData | undefined;
private currentScrollWidth = 0;
constructor(
@@ -134,6 +142,32 @@ export class CodeBlockPart extends Disposable implements ICodeBlockPart {
shouldForwardArgs: true
}
}));
const vulnsContainer = dom.append(this.element, $('.interactive-result-vulns'));
const vulnsHeaderElement = dom.append(vulnsContainer, $('.interactive-result-vulns-header', undefined));
this.vulnsButton = new Button(vulnsHeaderElement, {
buttonBackground: undefined,
buttonBorder: undefined,
buttonForeground: undefined,
buttonHoverBackground: undefined,
buttonSecondaryBackground: undefined,
buttonSecondaryForeground: undefined,
buttonSecondaryHoverBackground: undefined,
buttonSeparator: undefined,
supportIcons: true
});
this.vulnsListElement = dom.append(vulnsContainer, $('ul.interactive-result-vulns-list'));
this.vulnsButton.onDidClick(() => {
const element = this.currentCodeBlockData!.element as IChatResponseViewModel;
element.vulnerabilitiesListExpanded = !element.vulnerabilitiesListExpanded;
this.vulnsButton.label = this.getVulnerabilitiesLabel();
this.element.classList.toggle('chat-vulnerabilities-collapsed', !element.vulnerabilitiesListExpanded);
this._onDidChangeContentHeight.fire();
// this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded);
});
this._register(this.toolbar.onDidChangeDropdownVisibility(e => {
toolbarElement.classList.toggle('force-visibility', e);
}));
@@ -155,7 +189,7 @@ export class CodeBlockPart extends Disposable implements ICodeBlockPart {
}));
this._register(this.editor.onDidContentSizeChange(e => {
if (e.contentHeightChanged) {
this._onDidChangeContentHeight.fire(e.contentHeight);
this._onDidChangeContentHeight.fire();
}
}));
this._register(this.editor.onDidBlurEditorWidget(() => {
@@ -220,6 +254,7 @@ export class CodeBlockPart extends Disposable implements ICodeBlockPart {
render(data: ICodeBlockData, width: number): void {
this.currentCodeBlockData = data;
if (data.parentContextKeyService) {
this.contextKeyService.updateParent(data.parentContextKeyService);
}
@@ -250,6 +285,28 @@ export class CodeBlockPart extends Disposable implements ICodeBlockPart {
} else {
dom.show(this.toolbar.getElement());
}
if (data.vulns?.length && isResponseVM(data.element)) {
dom.clearNode(this.vulnsListElement);
this.element.classList.remove('no-vulns');
this.element.classList.toggle('chat-vulnerabilities-collapsed', !data.element.vulnerabilitiesListExpanded);
dom.append(this.vulnsListElement, ...data.vulns.map(v => $('li', undefined, $('span.chat-vuln-title', undefined, v.title), ' ' + v.description)));
this.vulnsButton.label = this.getVulnerabilitiesLabel();
} else {
this.element.classList.add('no-vulns');
}
}
private getVulnerabilitiesLabel(): string {
if (!this.currentCodeBlockData || !this.currentCodeBlockData.vulns) {
return '';
}
const referencesLabel = this.currentCodeBlockData.vulns.length > 1 ?
localize('vulnerabilitiesPlural', "{0} vulnerabilities", this.currentCodeBlockData.vulns.length) :
localize('vulnerabilitiesSingular', "{0} vulnerability", 1);
const icon = (element: IChatResponseViewModel) => element.vulnerabilitiesListExpanded ? Codicon.chevronDown : Codicon.chevronRight;
return `${referencesLabel} $(${icon(this.currentCodeBlockData.element as IChatResponseViewModel).id})`;
}
private fixCodeText(text: string, languageId: string): string {
@@ -31,6 +31,10 @@
justify-content: space-between;
}
.interactive-item-container .header.hidden {
display: none;
}
.interactive-item-container .header .user {
display: flex;
align-items: center;
@@ -270,6 +274,10 @@
padding: 8px 20px;
}
.interactive-item-container.interactive-item-compact.no-padding {
padding: unset;
}
.interactive-item-container.interactive-item-compact .header {
height: 16px;
}
@@ -460,8 +468,9 @@
}
.interactive-item-container.filtered-response .value > .rendered-markdown {
-webkit-mask-image: linear-gradient(rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.05));
mask-image: linear-gradient(rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.05));
pointer-events: none;
-webkit-mask-image: linear-gradient(rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.05) 60%, rgba(0, 0, 0, 0.00) 80%);
mask-image: linear-gradient(rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.05) 60%, rgba(0, 0, 0, 0.00) 80%);
}
/* #region Quick Chat */
@@ -602,5 +611,6 @@
}
.interactive-item-container .progress-steps .progress-step .codicon-check {
font-size: 14px;
color: var(--vscode-debugIcon-startForeground);
}
@@ -16,7 +16,7 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { ILogService } from 'vs/platform/log/common/log';
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChat, IChatAsyncContent, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';
import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatAsyncContent, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';
export interface IChatRequestModel {
readonly id: string;
@@ -29,10 +29,13 @@ export interface IChatRequestModel {
export type IChatProgressResponseContent =
| IChatMarkdownContent
| IChatAgentMarkdownContentWithVulnerability
| IChatTreeData
| IChatAsyncContent
| IChatContentInlineReference;
export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability>;
export interface IResponse {
readonly value: ReadonlyArray<IChatProgressResponseContent>;
asString(): string;
@@ -100,7 +103,7 @@ export class Response implements IResponse {
return this._responseParts;
}
constructor(value: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference>) {
constructor(value: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability>) {
this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ?
{ content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent :
'kind' in v ? v : { kind: 'treeData', treeData: v }));
@@ -112,6 +115,11 @@ export class Response implements IResponse {
return this._responseRepr;
}
clear(): void {
this._responseParts = [];
this._updateRepr(true);
}
updateContent(progress: IChatProgressResponseContent | IChatContent, quiet?: boolean): void {
if (progress.kind === 'content' || progress.kind === 'markdownContent') {
const responsePartLength = this._responseParts.length - 1;
@@ -155,7 +163,7 @@ export class Response implements IResponse {
}
this._updateRepr(quiet);
});
} else if (progress.kind === 'treeData' || progress.kind === 'inlineReference') {
} else if (progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln') {
this._responseParts.push(progress);
this._updateRepr(quiet);
}
@@ -256,7 +264,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
}
constructor(
_response: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference>,
_response: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability>,
public readonly session: ChatModel,
agent: IChatAgentData | undefined,
public readonly requestId: string,
@@ -307,7 +315,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
this._onDidChange.fire();
}
complete(): void {
complete(errorDetails?: IChatResponseErrorDetails): void {
if (errorDetails?.responseIsRedacted) {
this._response.clear();
}
this._isComplete = true;
this._onDidChange.fire();
}
@@ -352,7 +364,7 @@ export type ISerializableChatAgentData = UriDto<IChatAgentData>;
export interface ISerializableChatRequestData {
message: string | IParsedChatRequest;
response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference> | undefined;
response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability> | undefined;
agent?: ISerializableChatAgentData;
slashCommand?: IChatAgentCommand;
responseErrorDetails: IChatResponseErrorDetails | undefined;
@@ -640,7 +652,10 @@ export class ChatModel extends Disposable implements IChatModel {
throw new Error('acceptResponseProgress: Adding progress to a completed response');
}
if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'asyncContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference') {
if (progress.kind === 'vulnerability') {
// TODO@roblourens ChatModel should just work with strings
request.response.updateContent({ kind: 'markdownVuln', content: { value: progress.content }, title: progress.title, description: progress.description }, quiet);
} else if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'asyncContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln') {
request.response.updateContent(progress, quiet);
} else if (progress.kind === 'usedContext' || progress.kind === 'reference' || progress.kind === 'progressMessage') {
request.response.applyProgress(progress);
@@ -683,12 +698,12 @@ export class ChatModel extends Disposable implements IChatModel {
request.response.setErrorDetails(rawResponse.errorDetails);
}
completeResponse(request: ChatRequestModel): void {
completeResponse(request: ChatRequestModel, errorDetails: IChatResponseErrorDetails | undefined): void {
if (!request.response) {
throw new Error('Call setResponse before completeResponse');
}
request.response.complete();
request.response.complete(errorDetails);
}
setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {
@@ -36,6 +36,7 @@ export interface IChatResponseErrorDetails {
message: string;
responseIsIncomplete?: boolean;
responseIsFiltered?: boolean;
responseIsRedacted?: boolean;
}
export interface IChatResponse {
@@ -130,9 +131,26 @@ export interface IChatProgressMessage {
kind: 'progressMessage';
}
export interface IChatAgentContentWithVulnerability {
content: string;
title: string;
description: string;
kind: 'vulnerability';
}
// TODO@roblourens Temp until I get MarkdownString out of ChatModel
export interface IChatAgentMarkdownContentWithVulnerability {
content: IMarkdownString;
title: string;
description: string;
kind: 'markdownVuln';
}
export type IChatProgress =
| IChatContent
| IChatMarkdownContent
| IChatAgentContentWithVulnerability
| IChatAgentMarkdownContentWithVulnerability
| IChatTreeData
| IChatAsyncContent
| IChatUsedContext
@@ -558,7 +558,7 @@ export class ChatService extends Disposable implements IChatService {
rawResponse = { session: model.session! };
} else {
throw new Error(`Can't handle request`);
throw new Error(`Cannot handle request`);
}
if (token.isCancellationRequested) {
@@ -590,10 +590,10 @@ export class ChatService extends Disposable implements IChatService {
if (agentOrCommandFollowups) {
agentOrCommandFollowups.then(followups => {
model.setFollowups(request, followups);
model.completeResponse(request);
model.completeResponse(request, rawResponse?.errorDetails);
});
} else {
model.completeResponse(request);
model.completeResponse(request, rawResponse?.errorDetails);
}
}
} finally {
@@ -656,7 +656,7 @@ export class ChatService extends Disposable implements IChatService {
if (response.followups !== undefined) {
model.setFollowups(request, response.followups);
}
model.completeResponse(request);
model.completeResponse(request, response.errorDetails);
}
cancelCurrentRequestForSession(sessionId: string): void {
@@ -79,6 +79,7 @@ export interface IChatLiveUpdateData {
loadingStartTime: number;
lastUpdateTime: number;
impliedWordLoadRate: number;
lastWordCount: number;
}
export interface IChatResponseViewModel {
@@ -109,6 +110,7 @@ export interface IChatResponseViewModel {
currentRenderedHeight: number | undefined;
setVote(vote: InteractiveSessionVoteDirection): void;
usedReferencesExpanded?: boolean;
vulnerabilitiesListExpanded: boolean;
}
export class ChatViewModel extends Disposable implements IChatViewModel {
@@ -335,7 +337,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
currentRenderedHeight: number | undefined;
private _usedReferencesExpanded: boolean | undefined;
get usedReferencesExpanded(): boolean | undefined {
if (typeof this._usedReferencesExpanded === 'boolean') {
return this._usedReferencesExpanded;
@@ -348,6 +349,15 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
this._usedReferencesExpanded = v;
}
private _vulnerabilitiesListExpanded: boolean = false;
get vulnerabilitiesListExpanded(): boolean {
return this._vulnerabilitiesListExpanded;
}
set vulnerabilitiesListExpanded(v: boolean) {
this._vulnerabilitiesListExpanded = v;
}
private _contentUpdateTimings: IChatLiveUpdateData | undefined = undefined;
get contentUpdateTimings(): IChatLiveUpdateData | undefined {
return this._contentUpdateTimings;
@@ -363,7 +373,8 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
this._contentUpdateTimings = {
loadingStartTime: Date.now(),
lastUpdateTime: Date.now(),
impliedWordLoadRate: 0
impliedWordLoadRate: 0,
lastWordCount: 0
};
}
@@ -373,18 +384,14 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
const now = Date.now();
const wordCount = countWords(_model.response.asString());
const timeDiff = now - this._contentUpdateTimings!.loadingStartTime;
const impliedWordLoadRate = wordCount / (timeDiff / 1000);
const renderedWordCount = this.renderData?.renderedParts.reduce((acc, part) => acc += ('label' in part ? 0 : part.renderedWordCount), 0);
if (!this.isComplete) {
this.trace('onDidChange', `Update- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${renderedWordCount} words are rendered.`);
this._contentUpdateTimings = {
loadingStartTime: this._contentUpdateTimings!.loadingStartTime,
lastUpdateTime: now,
impliedWordLoadRate
};
} else {
this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${renderedWordCount} words are rendered.`);
}
const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (timeDiff / 1000);
this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`);
this._contentUpdateTimings = {
loadingStartTime: this._contentUpdateTimings!.loadingStartTime,
lastUpdateTime: now,
impliedWordLoadRate,
lastWordCount: wordCount
};
} else {
this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized');
}
@@ -0,0 +1,11 @@
[
{
content: {
value: "some code\nover\nmultiple lines <vscode_annotation title=\"title\" description=\"vuln\">content with vuln\nand\nnewlines</vscode_annotation>more code\nwith newline",
isTrusted: false,
supportThemeIcons: false,
supportHtml: false
},
kind: "markdownContent"
}
]
@@ -0,0 +1,15 @@
{
newText: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline",
vulnerabilities: [
{
title: "title",
description: "vuln",
range: {
startLineNumber: 3,
startColumn: 16,
endLineNumber: 5,
endColumn: 9
}
}
]
}
@@ -0,0 +1,11 @@
[
{
content: {
value: "some code\nover\nmultiple lines <vscode_annotation title=\"title\" description=\"vuln\">content with vuln\nand\nnewlines</vscode_annotation>more code\nwith newline<vscode_annotation title=\"title\" description=\"vuln\">content with vuln\nand\nnewlines</vscode_annotation>",
isTrusted: false,
supportThemeIcons: false,
supportHtml: false
},
kind: "markdownContent"
}
]
@@ -0,0 +1,25 @@
{
newText: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines",
vulnerabilities: [
{
title: "title",
description: "vuln",
range: {
startLineNumber: 3,
startColumn: 16,
endLineNumber: 5,
endColumn: 9
}
},
{
title: "title",
description: "vuln",
range: {
startLineNumber: 6,
startColumn: 13,
endLineNumber: 8,
endColumn: 9
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More