diff --git a/.vscode-test.js b/.vscode-test.js index 6846ca522f6..f2db8d695fe 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -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)}`), diff --git a/.vscode/settings.json b/.vscode/settings.json index 46ec2e83d68..95888a8f542 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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 } diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index f34a446c916..4f46a5ee563 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -4,7 +4,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.Limiter = void 0; const fs = require("fs"); const path = require("path"); const stream_1 = require("stream"); @@ -18,6 +17,7 @@ const cosmos_1 = require("@azure/cosmos"); const identity_1 = require("@azure/identity"); const cp = require("child_process"); const os = require("os"); +const node_worker_threads_1 = require("node:worker_threads"); function e(name) { const result = process.env[name]; if (typeof result !== 'string') { @@ -43,41 +43,6 @@ class Temp { } } } -class Limiter { - _size = 0; - runningPromises; - maxDegreeOfParalellism; - outstandingPromises; - constructor(maxDegreeOfParalellism) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - } - queue(factory) { - this._size++; - return new Promise((c, e) => { - this.outstandingPromises.push({ factory, c, e }); - this.consume(); - }); - } - consume() { - 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()); - } - } - consumed() { - this._size--; - this.runningPromises--; - if (this.outstandingPromises.length > 0) { - this.consume(); - } - } -} -exports.Limiter = Limiter; class ProvisionService { log; accessToken; @@ -133,7 +98,6 @@ function hashStream(hashName, stream) { class ESRPClient { log; tmp; - static Limiter = new Limiter(1); authPath; constructor(log, tmp, tenantId, clientId, authCertSubjectName, requestSigningCertSubjectName) { this.log = log; @@ -158,10 +122,8 @@ class ESRPClient { })); } async release(version, filePath) { - 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}`); } @@ -505,14 +467,11 @@ function getRealType(type) { return type; } } -const azureLimiter = new Limiter(1); -const mooncakeLimiter = new Limiter(1); async function uploadAssetLegacy(log, quality, commit, filePath) { const fileName = path.basename(filePath); const blobName = commit + '/' + fileName; - const storagePipelineOptions = { retryOptions: { retryPolicyType: storage_blob_1.StorageRetryPolicyType.EXPONENTIAL, maxTries: 6, tryTimeoutInMs: 10 * 60 * 1000 } }; const credential = new identity_1.ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET')); - const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://vscode.blob.core.windows.net`, credential, storagePipelineOptions); + const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://vscode.blob.core.windows.net`, credential, { retryOptions: { retryPolicyType: storage_blob_1.StorageRetryPolicyType.FIXED, tryTimeoutInMs: 2 * 60 * 1000 } }); const containerClient = blobServiceClient.getContainerClient(quality); const blobClient = containerClient.getBlockBlobClient(blobName); const blobOptions = { @@ -522,118 +481,67 @@ async function uploadAssetLegacy(log, quality, commit, filePath) { blobCacheControl: 'max-age=31536000, public' } }; - const uploadPromises = []; - uploadPromises.push((async () => { - log(`Checking for blob in Azure...`); - if (await (0, retry_1.retry)(() => blobClient.exists())) { - throw new Error(`Blob ${quality}, ${blobName} already exists, not publishing again.`); - } - else { - await (0, retry_1.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 identity_1.ClientSecretCredential(e('AZURE_MOONCAKE_TENANT_ID'), e('AZURE_MOONCAKE_CLIENT_ID'), e('AZURE_MOONCAKE_CLIENT_SECRET')); - const mooncakeBlobServiceClient = new storage_blob_1.BlobServiceClient(`https://vscode.blob.core.chinacloudapi.cn`, mooncakeCredential, storagePipelineOptions); - const mooncakeContainerClient = mooncakeBlobServiceClient.getContainerClient(quality); - const mooncakeBlobClient = mooncakeContainerClient.getBlockBlobClient(blobName); - uploadPromises.push((async () => { - log(`Checking for blob in Mooncake Azure...`); - if (await (0, retry_1.retry)(() => mooncakeBlobClient.exists())) { - throw new Error(`Mooncake Blob ${quality}, ${blobName} already exists, not publishing again.`); - } - else { - await (0, retry_1.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'); - 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.'); + log(`Checking for blob in Azure...`); + 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) { +async function processArtifact(artifact, artifactFilePath) { + const log = (...args) => console.log(`[${artifact.name}]`, ...args); const match = /^vscode_(?[^_]+)_(?[^_]+)_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { throw new Error(`Invalid artifact name: ${artifact.name}`); } - const { product, os, arch, unprocessedType } = match.groups; - const log = (...args) => console.log(`[${product} ${os} ${arch} ${unprocessedType}]`, ...args); - const start = Date.now(); - const filePath = await (0, retry_1.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), - releaseAndProvision(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT_SUBJECT_NAME'), e('RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME'), e('PROVISION_TENANT_ID'), e('PROVISION_AAD_USERNAME'), e('PROVISION_AAD_PASSWORD'), commit, quality, filePath) + const [cdnSettledResult, prssSettledResult] = await Promise.allSettled([ + uploadAssetLegacy(log, quality, commit, artifactFilePath), + releaseAndProvision(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT_SUBJECT_NAME'), e('RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME'), e('PROVISION_TENANT_ID'), e('PROVISION_AAD_USERNAME'), e('PROVISION_AAD_PASSWORD'), commit, quality, artifactFilePath) ]); - const 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 = { platform, type, url: assetUrl, hash: sha1hash, mooncakeUrl: prssUrl, prssUrl, sha256hash, size, supportsFastUpdate: true }; log('Creating asset...', JSON.stringify(asset)); await (0, retry_1.retry)(async (attempt) => { - await cosmosLimiter.queue(async () => { - log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const aadCredentials = new identity_1.ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET')); - const client = new cosmos_1.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 identity_1.ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET')); + const client = new cosmos_1.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 (!node_worker_threads_1.isMainThread) { + const { artifact, artifactFilePath } = node_worker_threads_1.workerData; + await processArtifact(artifact, artifactFilePath); + return; + } const done = new State(); const processing = new Set(); for (const name of done) { @@ -655,6 +563,7 @@ async function main() { if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); } + let resultPromise = Promise.resolve([]); const operations = []; while (true) { const [timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]); @@ -677,14 +586,43 @@ async function main() { if (done.has(artifact.name) || processing.has(artifact.name)) { 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 (0, retry_1.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((resolve, reject) => { + const worker = new node_worker_threads_1.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, 10000)); } @@ -693,7 +631,7 @@ async function main() { if (artifactsInProgress.length > 0) { 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]; if (result.status === 'rejected') { @@ -713,4 +651,4 @@ if (require.main === module) { process.exit(1); }); } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGlzaC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInB1Ymxpc2gudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOzs7QUFFaEcseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3QixtQ0FBa0M7QUFFbEMsbURBQWdEO0FBQ2hELCtCQUErQjtBQUMvQixpQ0FBaUM7QUFDakMsbUNBQWdDO0FBQ2hDLHNEQUF3STtBQUN4SSw2QkFBNkI7QUFDN0IsMENBQTZDO0FBQzdDLDhDQUF5RDtBQUN6RCxvQ0FBb0M7QUFDcEMseUJBQXlCO0FBRXpCLFNBQVMsQ0FBQyxDQUFDLElBQVk7SUFDdEIsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUVqQyxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsRUFBRSxDQUFDO1FBQ2hDLE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLElBQUksRUFBRSxDQUFDLENBQUM7SUFDekMsQ0FBQztJQUVELE9BQU8sTUFBTSxDQUFDO0FBQ2YsQ0FBQztBQUVELE1BQU0sSUFBSTtJQUNELE1BQU0sR0FBYSxFQUFFLENBQUM7SUFFOUIsV0FBVztRQUNWLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLE1BQU0sQ0FBQyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDNUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDdkIsT0FBTyxJQUFJLENBQUM7SUFDYixDQUFDO0lBRUQsT0FBTztRQUNOLEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO1lBQ2hDLElBQUksQ0FBQztnQkFDSixFQUFFLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ3JCLENBQUM7WUFBQyxPQUFP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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGlzaC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInB1Ymxpc2gudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOztBQUVoRyx5QkFBeUI7QUFDekIsNkJBQTZCO0FBQzdCLG1DQUFrQztBQUVsQyxtREFBZ0Q7QUFDaEQsK0JBQStCO0FBQy9CLGlDQUFpQztBQUNqQyxtQ0FBZ0M7QUFDaEMsc0RBQWdIO0FBQ2hILDZCQUE2QjtBQUM3QiwwQ0FBNkM7QUFDN0MsOENBQXlEO0FBQ3pELG9DQUFvQztBQUNwQyx5QkFBeUI7QUFDekIsNkRBQXVFO0FBRXZFLFNBQVMsQ0FBQyxDQUFDLElBQVk7SUFDdEIsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUVqQyxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsRUFBRSxDQUFDO1FBQ2hDLE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLElBQUksRUFBRSxDQUFDLENBQUM7SUFDekMsQ0FBQztJQUVELE9BQU8sTUFBTSxDQUFDO0FBQ2YsQ0FBQztBQUVELE1BQU0sSUFBSTtJQUNELE1BQU0sR0FBYSxFQUFFLENBQUM7SUFFOUIsV0FBVztRQUNWLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLE1BQU0sQ0FBQyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDNUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDdkIsT0FBTyxJQUFJLENBQUM7SUFDYixDQUFDO0lBRUQsT0FBTztRQUNOLEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO1lBQ2hDLElBQUksQ0FBQztnQkFDSixFQUFFLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ3JCLENBQUM7WUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO2dCQUNkLE9BQU87WUFDUixDQUFDO1FBQ0YsQ0FBQztJQUNGLENBQUM7Q0FDRDtBQXdCRCxNQUFNLGdCQUFnQjtJQUdIO0lBQ0E7SUFGbEIsWUFDa0IsR0FBNkIsRUFDN0IsV0FBbUI7UUFEbkIsUUFBRyxHQUFILEdBQUcsQ0FBMEI7UUFDN0IsZ0JBQVcsR0FBWCxXQUFXLENBQVE7SUFDakMsQ0FBQztJQUVMLEtBQUssQ0FBQyxTQUFTLENBQUMsU0FBaUIsRUFBRSxNQUFjLEVBQUUsUUFBZ0I7UUFDbEUsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQztZQUMzQixTQUFTLEVBQUUsU0FBUztZQUNwQixVQUFVLEVBQUUsUUFBUTtZQUNwQixhQUFhLEVBQUUsUUFBUTtZQUN2QiwwQkFBMEIsRUFBRSxDQUFDO29CQUM1QixZQUFZLEVBQUUsTUFBTTtvQkFDcEIsd0JBQXdCLEVBQUUsSUFBSTtvQkFDOUIsZ0JBQWdCLEVBQUUsUUFBUTtvQkFDMUIsTUFBTSxFQUFFLE1BQU07b0JBQ2QsV0FBVyxFQUFFLENBQUMsS0FBSyxDQUFDO2lCQUNwQixDQUFDO1NBQ0YsQ0FBQyxDQUFDO1FBRUgsSUFBSSxDQUFDLEdBQUcsQ0FBQyxnQkFBZ0IsUUFBUSxnQkFBZ0IsU0FBUyxhQUFhLE1BQU0sTUFBTSxDQUFDLENBQUM7UUFDckYsTUFBTSxHQUFHLEdBQUcsTUFBTSxJQUFBLGFBQUssRUFBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFpQyxNQUFNLEVBQUUsaURBQWlELEVBQUUsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUM7UUFFakosSUFBSSxDQUFDLEdBQUcsQ0FBQyxTQUFTLEVBQUUsQ0FBQztZQUNwQixNQUFNLElBQUksS0FBSyxDQUFDLDBDQUEwQyxJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDL0YsQ0FBQztRQUVELElBQUksQ0FBQyxHQUFHLENBQUMsNEJBQTRCLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDbEQsQ0FBQztJQUVPLEtBQUssQ0FBQyxPQUFPLENBQUksTUFBYyxFQUFFLEdBQVcsRUFBRSxPQUF3QjtRQUM3RSxNQUFNLElBQUksR0FBZ0I7WUFDekIsTUFBTTtZQUNOLElBQUksRUFBRSxPQUFPLEVBQUUsSUFBSTtZQUNuQixPQUFPLEVBQUU7Z0JBQ1IsYUFBYSxFQUFFLFVBQVUsSUFBSSxDQUFDLFdBQVcsRUFBRTtnQkFDM0MsY0FBYyxFQUFFLGtCQUFrQjthQUNsQztTQUNELENBQUM7UUFFRixNQUFNLEdBQUcsR0FBRyxNQUFNLEtBQUssQ0FBQyx1Q0FBdUMsR0FBRyxFQUFFLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFFNUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLElBQUksR0FBRyxDQUFDLE1BQU0sR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLE1BQU0sSUFBSSxHQUFHLEVBQUUsQ0FBQztZQUN0RCxNQUFNLElBQUksS0FBSyxDQUFDLDJCQUEyQixHQUFHLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUMxRCxDQUFDO1FBRUQsT0FBTyxNQUFNLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUN6QixDQUFDO0NBQ0Q7QUFFRCxTQUFTLFVBQVUsQ0FBQyxRQUFnQixFQUFFLE1BQWdCO0lBQ3JELE9BQU8sSUFBSSxPQUFPLENBQVMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUU7UUFDbkMsTUFBTSxNQUFNLEdBQUcsTUFBTSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUUzQyxNQUFNO2FBQ0osRUFBRSxDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQzthQUN0QyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQzthQUNkLEVBQUUsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQzlDLENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQXFCRCxNQUFNLFVBQVU7SUFLRztJQUNBO0lBSkQsUUFBUSxDQUFTO0lBRWxDLFlBQ2tCLEdBQTZCLEVBQzdCLEdBQVMsRUFDMUIsUUFBZ0IsRUFDaEIsUUFBZ0IsRUFDaEIsbUJBQTJCLEVBQzNCLDZCQUFxQztRQUxwQixRQUFHLEdBQUgsR0FBRyxDQUEwQjtRQUM3QixRQUFHLEdBQUgsR0FBRyxDQUFNO1FBTTFCLElBQUksQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUN2QyxFQUFFLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQztZQUM5QyxPQUFPLEVBQUUsT0FBTztZQUNoQixrQkFBa0IsRUFBRSxVQUFVO1lBQzlCLFFBQVEsRUFBRSxRQUFRO1lBQ2xCLFFBQVEsRUFBRSxRQUFRO1lBQ2xCLFFBQVEsRUFBRTtnQkFDVCxXQUFXLEVBQUUsbUJBQW1CO2dCQUNoQyxhQUFhLEVBQUUsY0FBYztnQkFDN0IsU0FBUyxFQUFFLElBQUk7Z0JBQ2YsT0FBTyxFQUFFLE1BQU07YUFDZjtZQUNELGtCQUFrQixFQUFFO2dCQUNuQixXQUFXLEVBQUUsNkJBQTZCO2dCQUMxQyxhQUFhLEVBQUUsY0FBYztnQkFDN0IsU0FBUyxFQUFFLElBQUk7YUFDZjtTQUNELENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVELEtBQUssQ0FBQyxPQUFPLENBQ1osT0FBZSxFQUNmLFFBQWdCO1FBRWhCLElBQUksQ0FBQyxHQUFHLENBQUMsMEJBQTBCLE9BQU8sS0FBSyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1FBQzNELE1BQU0sbUJBQW1CLEdBQUcsTUFBTSxJQUFJLENBQUMsYUFBYSxDQUFDLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQztRQUV4RSxJQUFJLG1CQUFtQixDQUFDLGtCQUFrQixDQUFDLFVBQVUsS0FBSyxNQUFNLEVBQUUsQ0FBQztZQUNsRSxNQUFNLElBQUksS0FBSyxDQUFDLDJCQUEyQixtQkFBbUIsQ0FBQyxrQkFBa0IsQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUFDO1FBQ2pHLENBQUM7UUFFRCxNQUFNLFNBQVMsR0FBRyxtQkFBbUIsQ0FBQyxrQkFBa0IsQ0FBQyxXQUFXLENBQUM7UUFDckUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxrQ0FBa0MsU0FBUyw2QkFBNkIsQ0FBQyxDQUFDO1FBRW5GLElBQUksT0FBOEIsQ0FBQztRQUVuQyxzRUFBc0U7UUFDdEUsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLEdBQUcsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDO1lBQzlCLE9BQU8sR0FBRyxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsU0FBUyxDQUFDLENBQUM7WUFFL0MsSUFBSSxPQUFPLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxDQUFDLFVBQVUsS0FBSyxNQUFNLEVBQUUsQ0FBQztnQkFDckQsTUFBTTtZQUNQLENBQUM7aUJBQU0sSUFBSSxPQUFPLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxDQUFDLFVBQVUsS0FBSyxZQUFZLEVBQUUsQ0FBQztnQkFDbEUsTUFBTSxJQUFJLEtBQUssQ0FBQyw2QkFBNkIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLENBQUM7WUFDekUsQ0FBQztZQUVELE1BQU0sSUFBSSxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxVQUFVLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDN0MsQ0FBQztRQUVELElBQUksT0FBTyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxVQUFVLEtBQUssTUFBTSxFQUFFLENBQUM7WUFDckQsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQ0FBaUMsU0FBUyxLQUFLLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQzNGLENBQUM7UUFFRCxNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxZQUFZLENBQUM7UUFDckUsSUFBSSxDQUFDLEdBQUcsQ0FBQyw4Q0FBOEMsRUFBRSxNQUFNLENBQUMsQ0FBQztRQUVqRSxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxDQUFDO0lBQzlCLENBQUM7SUFFTyxLQUFLLENBQUMsYUFBYSxDQUMxQixPQUFlLEVBQ2YsUUFBZ0I7UUFFaEIsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUMxQyxFQUFFLENBQUMsYUFBYSxDQUFDLFVBQVUsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDO1lBQzNDLE9BQU8sRUFBRSxPQUFPO1lBQ2hCLFFBQVEsRUFBRSxpQkFBaUI7WUFDM0IsTUFBTSxFQUFFLGNBQWM7WUFDdEIsV0FBVyxFQUFFLGdCQUFnQjtTQUM3QixDQUFDLENBQUMsQ0FBQztRQUVKLE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDekMsTUFBTSxJQUFJLEdBQUcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDeEMsTUFBTSxPQUFPLEdBQUcsRUFBRSxDQUFDLGdCQUFnQixDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQzlDLE1BQU0sTUFBTSxHQUFHLE1BQU0sVUFBVSxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBQztRQUNuRCxFQUFFLENBQUMsYUFBYSxDQUFDLFNBQVMsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDO1lBQzFDLE9BQU8sRUFBRSxPQUFPO1lBQ2hCLFdBQVcsRUFBRTtnQkFDWixlQUFlLEVBQUU7b0JBQ2hCLEtBQUssRUFBRSxTQUFTO29CQUNoQixVQUFVLEVBQUU7d0JBQ1gsa0JBQWtCLEVBQUUsZ0JBQWdCO3FCQUNwQztvQkFDRCx3QkFBd0IsRUFBRSxDQUFDO2lCQUMzQjtnQkFDRCxXQUFXLEVBQUU7b0JBQ1osSUFBSSxFQUFFLFNBQVM7b0JBQ2YsT0FBTyxFQUFFLE9BQU87b0JBQ2hCLFdBQVcsRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDO2lCQUM1RDtnQkFDRCxNQUFNLEVBQUU7b0JBQ1A7d0JBQ0MsS0FBSyxFQUFFOzRCQUNOLGlCQUFpQixFQUFFLG9CQUFvQjt5QkFDdkM7cUJBQ0Q7aUJBQ0Q7Z0JBQ0QsU0FBUyxFQUFFO29CQUNWO3dCQUNDLFFBQVEsRUFBRTs0QkFDVCxpQkFBaUIsRUFBRSxvQkFBb0I7eUJBQ3ZDO3dCQUNELGNBQWMsRUFBRSxJQUFJO3dCQUNwQixXQUFXLEVBQUUsS0FBSztxQkFDbEI7aUJBQ0Q7Z0JBQ0QsaUJBQWlCLEVBQUU7b0JBQ2xCLGFBQWEsRUFBRSxRQUFRO29CQUN2Qiw0QkFBNEIsRUFBRTt3QkFDN0IsUUFBUSxFQUFFLENBQUMsUUFBUSxDQUFDO3FCQUNwQjtpQkFDRDtnQkFDRCxTQUFTLEVBQUU7b0JBQ1YsaUJBQWlCLEVBQUUsb0JBQW9CO2lCQUN2QzthQUNEO1lBQ0QsY0FBYyxFQUFFO2dCQUNmO29CQUNDLG1CQUFtQixFQUFFO3dCQUNwQjs0QkFDQyxXQUFXLEVBQUUsSUFBSTs0QkFDakIsVUFBVSxFQUFFLE1BQU07NEJBQ2xCLFFBQVEsRUFBRSxRQUFROzRCQUNsQixjQUFjLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUM7eUJBQ3ZDO3FCQUNEO29CQUNELGtCQUFrQixFQUFFLEtBQUs7b0JBQ3pCLG1CQUFtQixFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO29CQUMzQyx1QkFBdUIsRUFBRSxXQUFXO2lCQUNwQzthQUNEO1NBQ0QsQ0FBQyxDQUFDLENBQUM7UUFFSixNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQzFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsK0JBQStCLElBQUksQ0FBQyxRQUFRLE9BQU8sVUFBVSxPQUFPLFNBQVMsT0FBTyxVQUFVLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDO1FBRXBJLE1BQU0sTUFBTSxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsVUFBVSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQ25ELE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQXdCLENBQUM7SUFDbEQsQ0FBQztJQUVPLEtBQUssQ0FBQyxjQUFjLENBQzNCLFNBQWlCO1FBRWpCLE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDekMsRUFBRSxDQUFDLGFBQWEsQ0FBQyxTQUFTLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQztZQUMxQyxPQUFPLEVBQUUsT0FBTztZQUNoQixZQUFZLEVBQUUsQ0FBQyxTQUFTLENBQUM7U0FDekIsQ0FBQyxDQUFDLENBQUM7UUFFSixNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQzFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsZ0NBQWdDLElBQUksQ0FBQyxRQUFRLE9BQU8sU0FBUyxPQUFPLFVBQVUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUM7UUFFcEgsTUFBTSxNQUFNLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxVQUFVLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFDbkQsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBeUIsQ0FBQztJQUNuRCxDQUFDO0NBQ0Q7QUFFRCxLQUFLLFVBQVUsbUJBQW1CLENBQ2pDLEdBQTZCLEVBQzdCLGVBQXVCLEVBQ3ZCLGVBQXVCLEVBQ3ZCLDBCQUFrQyxFQUNsQyxvQ0FBNEMsRUFDNUMsaUJBQXlCLEVBQ3pCLG9CQUE0QixFQUM1QixvQkFBNEIsRUFDNUIsT0FBZSxFQUNmLE9BQWUsRUFDZixRQUFnQjtJQUVoQixNQUFNLFFBQVEsR0FBRyxHQUFHLE9BQU8sSUFBSSxPQUFPLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO0lBQ3BFLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxDQUFDLGNBQWMsQ0FBQyxJQUFJLFFBQVEsRUFBRSxDQUFDO0lBRWxELE1BQU0sR0FBRyxHQUFHLE1BQU0sSUFBQSxhQUFLLEVBQUMsR0FBRyxFQUFFLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFFN0MsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDO1FBQ3hCLEdBQUcsQ0FBQyxxQ0FBcUMsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUNuRCxPQUFPLE1BQU0sQ0FBQztJQUNmLENBQUM7SUFFRCxNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDO0lBQ3ZCLE9BQU8sQ0FBQyxFQUFFLENBQUMsTUFBTSxFQUFFLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO0lBRXhDLE1BQU0sVUFBVSxHQUFHLElBQUksVUFBVSxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsZUFBZSxFQUFFLGVBQWUsRUFBRSwwQkFBMEIsRUFBRSxvQ0FBb0MsQ0FBQyxDQUFDO0lBQ2hKLE1BQU0sT0FBTyxHQUFHLE1BQU0sVUFBVSxDQUFDLE9BQU8sQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFFNUQsTUFBTSxVQUFVLEdBQUcsSUFBSSxpQ0FBc0IsQ0FBQyxpQkFBaUIsRUFBRSxvQkFBb0IsRUFBRSxvQkFBb0IsQ0FBQyxDQUFDO0lBQzdHLE1BQU0sV0FBVyxHQUFHLE1BQU0sVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDLG1FQUFtRSxDQUFDLENBQUMsQ0FBQztJQUNySCxNQUFNLE9BQU8sR0FBRyxJQUFJLGdCQUFnQixDQUFDLEdBQUcsRUFBRSxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDN0QsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLE1BQU0sRUFBRSxRQUFRLENBQUMsQ0FBQztJQUVyRSxPQUFPLE1BQU0sQ0FBQztBQUNmLENBQUM7QUFFRCxNQUFNLEtBQUs7SUFFRixTQUFTLENBQVM7SUFDbEIsR0FBRyxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFFaEM7UUFDQyxNQUFNLHFCQUFxQixHQUFHLENBQUMsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO1FBQ3RELE1BQU0sYUFBYSxHQUFHLEVBQUUsQ0FBQyxXQUFXLENBQUMscUJBQXFCLENBQUM7YUFDekQsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsNkJBQTZCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO2FBQ3JELE1BQU0sQ0FBQyxDQUFDLEtBQUssRUFBNEIsRUFBRSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUM7YUFDcEQsR0FBRyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLElBQUksRUFBRSxLQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsT0FBTyxFQUFFLE1BQU0sQ0FBQyxLQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUM7YUFDL0QsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLE9BQU8sR0FBRyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFM0MsSUFBSSxhQUFhLEVBQUUsQ0FBQztZQUNuQixNQUFNLGlCQUFpQixHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMscUJBQXFCLEVBQUUsYUFBYSxDQUFDLElBQUksRUFBRSxhQUFhLENBQUMsSUFBSSxHQUFHLE1BQU0sQ0FBQyxDQUFDO1lBQzVHLEVBQUUsQ0FBQyxZQUFZLENBQUMsaUJBQWlCLEVBQUUsTUFBTSxDQUFDLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQ25ILENBQUM7UUFFRCxNQUFNLFlBQVksR0FBRyxDQUFDLENBQUMscUJBQXFCLENBQUMsQ0FBQztRQUM5QyxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMscUJBQXFCLEVBQUUsdUJBQXVCLFlBQVksRUFBRSxFQUFFLHVCQUF1QixZQUFZLE1BQU0sQ0FBQyxDQUFDO1FBQ3BJLEVBQUUsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUNoRSxFQUFFLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsQ0FBQyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxHQUFHLElBQUksSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDNUYsQ0FBQztJQUVELElBQUksSUFBSTtRQUNQLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUM7SUFDdEIsQ0FBQztJQUVELEdBQUcsQ0FBQyxJQUFZO1FBQ2YsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMzQixDQUFDO0lBRUQsR0FBRyxDQUFDLElBQVk7UUFDZixJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNuQixFQUFFLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsR0FBRyxJQUFJLElBQUksQ0FBQyxDQUFDO0lBQ2hELENBQUM7SUFFRCxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUM7UUFDaEIsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO0lBQ3BDLENBQUM7Q0FDRDtBQUVELE1BQU0sZ0JBQWdCLEdBQUc7SUFDeEIsT0FBTyxFQUFFO1FBQ1IsNERBQTREO1FBQzVELFlBQVksRUFBRSxxSUFBcUk7UUFDbkosUUFBUSxFQUFFLDhIQUE4SDtRQUN4SSxpQkFBaUIsRUFBRSxtQkFBbUI7UUFDdEMsaUJBQWlCLEVBQUUsZ0JBQWdCO1FBQ25DLFNBQVMsRUFBRSx1QkFBdUI7UUFDbEMsYUFBYSxFQUFFLFVBQVUsQ0FBQyxDQUFDLG9CQUFvQixDQUFDLEVBQUU7S0FDbEQ7Q0FDRCxDQUFDO0FBRUYsS0FBSyxVQUFVLGNBQWMsQ0FBSSxJQUFZO0lBQzVDLE1BQU0sZUFBZSxHQUFHLElBQUksZUFBZSxFQUFFLENBQUM7SUFDOUMsTUFBTSxPQUFPLEdBQUcsVUFBVSxDQUFDLEdBQUcsRUFBRSxDQUFDLGVBQWUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBRXpFLElBQUksQ0FBQztRQUNKLE1BQU0sR0FBRyxHQUFHLE1BQU0sS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLGdCQUFnQixDQUFDLEdBQUcsSUFBSSxrQkFBa0IsRUFBRSxFQUFFLEdBQUcsZ0JBQWdCLEVBQUUsTUFBTSxFQUFFLGVBQWUsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBRWxJLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxFQUFFLENBQUM7WUFDYixNQUFNLElBQUksS0FBSyxDQUFDLDJCQUEyQixHQUFHLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUMxRCxDQUFDO1FBRUQsT0FBTyxNQUFNLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUN6QixDQUFDO1lBQVMsQ0FBQztRQUNWLFlBQVksQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN2QixDQUFDO0FBQ0YsQ0FBQztBQVlELEtBQUssVUFBVSxvQkFBb0I7SUFDbEMsTUFBTSxNQUFNLEdBQUcsTUFBTSxjQUFjLENBQWlDLFdBQVcsQ0FBQyxDQUFDO0lBQ2pGLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7QUFDbkYsQ0FBQztBQVVELEtBQUssVUFBVSxtQkFBbUI7SUFDakMsT0FBTyxNQUFNLGNBQWMsQ0FBVyxVQUFVLENBQUMsQ0FBQztBQUNuRCxDQUFDO0FBRUQsS0FBSyxVQUFVLGdCQUFnQixDQUFDLFFBQWtCLEVBQUUsWUFBb0I7SUFDdkUsTUFBTSxlQUFlLEdBQUcsSUFBSSxlQUFlLEVBQUUsQ0FBQztJQUM5QyxNQUFNLE9BQU8sR0FBRyxVQUFVLENBQUMsR0FBRyxFQUFFLENBQUMsZUFBZSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUM7SUFFekUsSUFBSSxDQUFDO1FBQ0osTUFBTSxHQUFHLEdBQUcsTUFBTSxLQUFLLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxXQUFXLEVBQUUsRUFBRSxHQUFHLGdCQUFnQixFQUFFLE1BQU0sRUFBRSxlQUFlLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUVoSCxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsRUFBRSxDQUFDO1lBQ2IsTUFBTSxJQUFJLEtBQUssQ0FBQywyQkFBMkIsR0FBRyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7UUFDMUQsQ0FBQztRQUVELE1BQU0sSUFBQSxtQkFBUSxFQUFDLGlCQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFzQixDQUFDLEVBQUUsRUFBRSxDQUFDLGlCQUFpQixDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFDbEcsQ0FBQztZQUFTLENBQUM7UUFDVixZQUFZLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdkIsQ0FBQztBQUNGLENBQUM7QUFFRCxLQUFLLFVBQVUsS0FBSyxDQUFDLFdBQW1CLEVBQUUsVUFBa0I7SUFDM0QsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUN0QyxLQUFLLENBQUMsSUFBSSxDQUFDLFdBQVcsRUFBRSxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsRUFBRSxDQUFDLEdBQUcsRUFBRSxPQUFPLEVBQUUsRUFBRTtZQUMvRCxJQUFJLEdBQUcsRUFBRSxDQUFDO2dCQUNULE9BQU8sTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3BCLENBQUM7WUFFRCxPQUFRLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsRUFBRTtnQkFDNUIsSUFBSSxLQUFLLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO29CQUNoQyxPQUFRLENBQUMsU0FBUyxFQUFFLENBQUM7Z0JBQ3RCLENBQUM7cUJBQU0sQ0FBQztvQkFDUCxPQUFRLENBQUMsY0FBYyxDQUFDLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxPQUFPLEVBQUUsRUFBRTt3QkFDL0MsSUFBSSxHQUFHLEVBQUUsQ0FBQzs0QkFDVCxPQUFPLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQzt3QkFDcEIsQ0FBQzt3QkFFRCxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxLQUFLLENBQUMsUUFBUSxDQUFDLENBQUM7d0JBQ3ZELEVBQUUsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO3dCQUUxRCxNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUMsaUJBQWlCLENBQUMsUUFBUSxDQUFDLENBQUM7d0JBQy9DLE9BQU8sQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRTs0QkFDekIsT0FBUSxDQUFDLEtBQUssRUFBRSxDQUFDOzRCQUNqQixPQUFPLENBQUMsUUFBUSxDQUFDLENBQUM7d0JBQ25CLENBQUMsQ0FBQyxDQUFDO3dCQUNILE9BQU8sRUFBRSxFQUFFLENBQUMsT0FBTyxFQUFFLEdBQUcsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7d0JBQ3pDLE9BQVEsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUM7b0JBQ3hCLENBQUMsQ0FBQyxDQUFDO2dCQUNKLENBQUM7WUFDRixDQUFDLENBQUMsQ0FBQztZQUVILE9BQVEsQ0FBQyxTQUFTLEVBQUUsQ0FBQztRQUN0QixDQUFDLENBQUMsQ0FBQztJQUNKLENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQWNELHdGQUF3RjtBQUN4RixTQUFTLFdBQVcsQ0FBQyxPQUFlLEVBQUUsRUFBVSxFQUFFLElBQVksRUFBRSxJQUFZO0lBQzNFLFFBQVEsRUFBRSxFQUFFLENBQUM7UUFDWixLQUFLLE9BQU87WUFDWCxRQUFRLE9BQU8sRUFBRSxDQUFDO2dCQUNqQixLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUM7b0JBQ2YsUUFBUSxJQUFJLEVBQUUsQ0FBQzt3QkFDZCxLQUFLLFNBQVM7NEJBQ2IsT0FBTyxTQUFTLElBQUksVUFBVSxDQUFDO3dCQUNoQyxLQUFLLE9BQU87NEJBQ1gsT0FBTyxTQUFTLElBQUksRUFBRSxDQUFDO3dCQUN4QixLQUFLLFlBQVk7NEJBQ2hCLE9BQU8sU0FBUyxJQUFJLE9BQU8sQ0FBQzt3QkFDN0I7NEJBQ0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztvQkFDcEUsQ0FBQztnQkFDRixDQUFDO2dCQUNELEtBQUssUUFBUTtvQkFDWixJQUFJLElBQUksS0FBSyxPQUFPLEVBQUUsQ0FBQzt3QkFDdEIsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztvQkFDbkUsQ0FBQztvQkFDRCxPQUFPLGdCQUFnQixJQUFJLEVBQUUsQ0FBQztnQkFDL0IsS0FBSyxLQUFLO29CQUNULElBQUksSUFBSSxLQUFLLE9BQU8sRUFBRSxDQUFDO3dCQUN0QixNQUFNLElBQUksS0FBSyxDQUFDLGlCQUFpQixPQUFPLElBQUksRUFBRSxJQUFJLElBQUksSUFBSSxJQUFJLEVBQUUsQ0FBQyxDQUFDO29CQUNuRSxDQUFDO29CQUNELE9BQU8sZ0JBQWdCLElBQUksTUFBTSxDQUFDO2dCQUNuQyxLQUFLLEtBQUs7b0JBQ1QsT0FBTyxhQUFhLElBQUksRUFBRSxDQUFDO2dCQUM1QjtvQkFDQyxNQUFNLElBQUksS0FBSyxDQUFDLGlCQUFpQixPQUFPLElBQUksRUFBRSxJQUFJLElBQUksSUFBSSxJQUFJLEVBQUUsQ0FBQyxDQUFDO1lBQ3BFLENBQUM7UUFDRixLQUFLLFFBQVE7WUFDWixRQUFRLE9BQU8sRUFBRSxDQUFDO2dCQUNqQixLQUFLLFFBQVE7b0JBQ1osT0FBTyxpQkFBaUIsSUFBSSxFQUFFLENBQUM7Z0JBQ2hDLEtBQUssS0FBSztvQkFDVCxPQUFPLGlCQUFpQixJQUFJLE1BQU0sQ0FBQztnQkFDcEMsS0FBSyxLQUFLO29CQUNULE9BQU8sY0FBYyxJQUFJLEVBQUUsQ0FBQztnQkFDN0I7b0JBQ0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztZQUNwRSxDQUFDO1FBQ0YsS0FBSyxPQUFPO1lBQ1gsUUFBUSxJQUFJLEVBQUUsQ0FBQztnQkFDZCxLQUFLLE1BQU07b0JBQ1YsT0FBTyxjQUFjLElBQUksRUFBRSxDQUFDO2dCQUM3QixLQUFLLGtCQUFrQjtvQkFDdEIsUUFBUSxPQUFPLEVBQUUsQ0FBQzt3QkFDakIsS0FBSyxRQUFROzRCQUNaLE9BQU8sU0FBUyxJQUFJLEVBQUUsQ0FBQzt3QkFDeEIsS0FBSyxRQUFROzRCQUNaLE9BQU8sZ0JBQWdCLElBQUksRUFBRSxDQUFDO3dCQUMvQixLQUFLLEtBQUs7NEJBQ1QsT0FBTyxJQUFJLEtBQUssWUFBWSxDQUFDLENBQUMsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMsZ0JBQWdCLElBQUksTUFBTSxDQUFDO3dCQUM5RTs0QkFDQyxNQUFNLElBQUksS0FBSyxDQUFDLGlCQUFpQixPQUFPLElBQUksRUFBRSxJQUFJLElBQUksSUFBSSxJQUFJLEVBQUUsQ0FBQyxDQUFDO29CQUNwRSxDQUFDO2dCQUNGLEtBQUssYUFBYTtvQkFDakIsT0FBTyxhQUFhLElBQUksRUFBRSxDQUFDO2dCQUM1QixLQUFLLGFBQWE7b0JBQ2pCLE9BQU8sYUFBYSxJQUFJLEVBQUUsQ0FBQztnQkFDNUIsS0FBSyxLQUFLO29CQUNULE9BQU8sYUFBYSxJQUFJLEVBQUUsQ0FBQztnQkFDNUI7b0JBQ0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztZQUNwRSxDQUFDO1FBQ0YsS0FBSyxRQUFRO1lBQ1osUUFBUSxPQUFPLEVBQUUsQ0FBQztnQkFDakIsS0FBSyxRQUFRO29CQUNaLElBQUksSUFBSSxLQUFLLEtBQUssRUFBRSxDQUFDO3dCQUNwQixPQUFPLFFBQVEsQ0FBQztvQkFDakIsQ0FBQztvQkFDRCxPQUFPLFVBQVUsSUFBSSxFQUFFLENBQUM7Z0JBQ3pCLEtBQUssUUFBUTtvQkFDWixJQUFJLElBQUksS0FBSyxLQUFLLEVBQUUsQ0FBQzt3QkFDcEIsT0FBTyxlQUFlLENBQUM7b0JBQ3hCLENBQUM7b0JBQ0QsT0FBTyxpQkFBaUIsSUFBSSxFQUFFLENBQUM7Z0JBQ2hDLEtBQUssS0FBSztvQkFDVCxJQUFJLElBQUksS0FBSyxLQUFLLEVBQUUsQ0FBQzt3QkFDcEIsT0FBTyxtQkFBbUIsQ0FBQztvQkFDNUIsQ0FBQztvQkFDRCxPQUFPLGlCQUFpQixJQUFJLE1BQU0sQ0FBQztnQkFDcEMsS0FBSyxLQUFLO29CQUNULE9BQU8sY0FBYyxJQUFJLEVBQUUsQ0FBQztnQkFDN0I7b0JBQ0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztZQUNwRSxDQUFDO1FBQ0Y7WUFDQyxNQUFNLElBQUksS0FBSyxDQUFDLGlCQUFpQixPQUFPLElBQUksRUFBRSxJQUFJLElBQUksSUFBSSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3BFLENBQUM7QUFDRixDQUFDO0FBRUQsOEVBQThFO0FBQzlFLFNBQVMsV0FBVyxDQUFDLElBQVk7SUFDaEMsUUFBUSxJQUFJLEVBQUUsQ0FBQztRQUNkLEtBQUssWUFBWTtZQUNoQixPQUFPLE9BQU8sQ0FBQztRQUNoQixLQUFLLGFBQWEsQ0FBQztRQUNuQixLQUFLLGFBQWE7WUFDakIsT0FBTyxTQUFTLENBQUM7UUFDbEI7WUFDQyxPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7QUFDRixDQUFDO0FBRUQsS0FBSyxVQUFVLGlCQUFpQixDQUFDLEdBQTZCLEVBQUUsT0FBZSxFQUFFLE1BQWMsRUFBRSxRQUFnQjtJQUNoSCxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3pDLE1BQU0sUUFBUSxHQUFHLE1BQU0sR0FBRyxHQUFHLEdBQUcsUUFBUSxDQUFDO0lBRXpDLE1BQU0sVUFBVSxHQUFHLElBQUksaUNBQXNCLENBQUMsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQyxDQUFDLHFCQUFxQixDQUFDLENBQUMsQ0FBQztJQUNwSCxNQUFNLGlCQUFpQixHQUFHLElBQUksZ0NBQWlCLENBQUMsc0NBQXNDLEVBQUUsVUFBVSxFQUFFLEVBQUUsWUFBWSxFQUFFLEVBQUUsZUFBZSxFQUFFLHFDQUFzQixDQUFDLEtBQUssRUFBRSxjQUFjLEVBQUUsQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDeE0sTUFBTSxlQUFlLEdBQUcsaUJBQWlCLENBQUMsa0JBQWtCLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdEUsTUFBTSxVQUFVLEdBQUcsZUFBZSxDQUFDLGtCQUFrQixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBRWhFLE1BQU0sV0FBVyxHQUFtQztRQUNuRCxlQUFlLEVBQUU7WUFDaEIsZUFBZSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDO1lBQ3RDLHNCQUFzQixFQUFFLHlCQUF5QixRQUFRLEdBQUc7WUFDNUQsZ0JBQWdCLEVBQUUsMEJBQTBCO1NBQzVDO0tBQ0QsQ0FBQztJQUVGLEdBQUcsQ0FBQywrQkFBK0IsQ0FBQyxDQUFDO0lBRXJDLElBQUksTUFBTSxVQUFVLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQztRQUMvQixHQUFHLENBQUMsUUFBUSxPQUFPLEtBQUssUUFBUSx3Q0FBd0MsQ0FBQyxDQUFDO0lBQzNFLENBQUM7U0FBTSxDQUFDO1FBQ1AsR0FBRyxDQUFDLHFDQUFxQyxDQUFDLENBQUM7UUFDM0MsTUFBTSxVQUFVLENBQUMsVUFBVSxDQUFDLFFBQVEsRUFBRSxXQUFXLENBQUMsQ0FBQztRQUNuRCxHQUFHLENBQUMsOENBQThDLENBQUMsQ0FBQztJQUNyRCxDQUFDO0lBRUQsT0FBTyxHQUFHLENBQUMsQ0FBQyxlQUFlLENBQUMsSUFBSSxPQUFPLElBQUksUUFBUSxFQUFFLENBQUM7QUFDdkQsQ0FBQztBQUVELEtBQUssVUFBVSxlQUFlLENBQUMsUUFBa0IsRUFBRSxnQkFBd0I7SUFDMUUsTUFBTSxHQUFHLEdBQUcsQ0FBQyxHQUFHLElBQVcsRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLFFBQVEsQ0FBQyxJQUFJLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQzNFLE1BQU0sS0FBSyxHQUFHLGtGQUFrRixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7SUFFckgsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1osTUFBTSxJQUFJLEtBQUssQ0FBQywwQkFBMEIsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7SUFDNUQsQ0FBQztJQUVELHdDQUF3QztJQUN4QyxNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztJQUNwQyxNQUFNLE1BQU0sR0FBRyxDQUFDLENBQUMscUJBQXFCLENBQUMsQ0FBQztJQUN4QyxNQUFNLEVBQUUsT0FBTyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsZUFBZSxFQUFFLEdBQUcsS0FBSyxDQUFDLE1BQU8sQ0FBQztJQUM3RCxNQUFNLFFBQVEsR0FBRyxXQUFXLENBQUMsT0FBTyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsZUFBZSxDQUFDLENBQUM7SUFDakUsTUFBTSxJQUFJLEdBQUcsV0FBVyxDQUFDLGVBQWUsQ0FBQyxDQUFDO0lBQzFDLE1BQU0sSUFBSSxHQUFHLEVBQUUsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFDaEQsTUFBTSxNQUFNLEdBQUcsRUFBRSxDQUFDLGdCQUFnQixDQUFDLGdCQUFnQixDQUFDLENBQUM7SUFDckQsTUFBTSxDQUFDLFFBQVEsRUFBRSxVQUFVLENBQUMsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxVQUFVLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxFQUFFLFVBQVUsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBRTdHLE1BQU0sQ0FBQyxnQkFBZ0IsRUFBRSxpQkFBaUIsQ0FBQyxHQUFHLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQztRQUN0RSxpQkFBaUIsQ0FBQyxHQUFHLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztRQUN6RCxtQkFBbUIsQ0FDbEIsR0FBRyxFQUNILENBQUMsQ0FBQyxtQkFBbUIsQ0FBQyxFQUN0QixDQUFDLENBQUMsbUJBQW1CLENBQUMsRUFDdEIsQ0FBQyxDQUFDLGdDQUFnQyxDQUFDLEVBQ25DLENBQUMsQ0FBQywyQ0FBMkMsQ0FBQyxFQUM5QyxDQUFDLENBQUMscUJBQXFCLENBQUMsRUFDeEIsQ0FBQyxDQUFDLHdCQUF3QixDQUFDLEVBQzNCLENBQUMsQ0FBQyx3QkFBd0IsQ0FBQyxFQUMzQixNQUFNLEVBQ04sT0FBTyxFQUNQLGdCQUFnQixDQUNoQjtLQUNELENBQUMsQ0FBQztJQUVILElBQUksZ0JBQWdCLENBQUMsTUFBTSxLQUFLLFVBQVUsRUFBRSxDQUFDO1FBQzVDLE1BQU0sZ0JBQWdCLENBQUMsTUFBTSxDQUFDO0lBQy9CLENBQUM7U0FBTSxJQUFJLGlCQUFpQixDQUFDLE1BQU0sS0FBSyxVQUFVLEVBQUUsQ0FBQyxDQUFDLHlEQUF5RDtRQUM5RyxPQUFPLENBQUMsS0FBSyxDQUFDLGlCQUFpQixDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3pDLENBQUM7SUFFRCxNQUFNLFFBQVEsR0FBRyxnQkFBZ0IsQ0FBQyxLQUFLLENBQUM7SUFDeEMsTUFBTSxPQUFPLEdBQUcsaUJBQWlCLENBQUMsTUFBTSxLQUFLLFdBQVcsQ0FBQyxDQUFDLENBQUMsaUJBQWlCLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7SUFFL0YsTUFBTSxLQUFLLEdBQVUsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxXQUFXLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLGtCQUFrQixFQUFFLElBQUksRUFBRSxDQUFDO0lBQ2xKLEdBQUcsQ0FBQyxtQkFBbUIsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7SUFFaEQsTUFBTSxJQUFBLGFBQUssRUFBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7UUFDN0IsR0FBRyxDQUFDLHdDQUF3QyxPQUFPLE1BQU0sQ0FBQyxDQUFDO1FBQzNELE1BQU0sY0FBYyxHQUFHLElBQUksaUNBQXNCLENBQUMsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQyxDQUFDLHFCQUFxQixDQUFDLENBQUMsQ0FBQztRQUN4SCxNQUFNLE1BQU0sR0FBRyxJQUFJLHFCQUFZLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDLDJCQUEyQixDQUFDLEVBQUUsY0FBYyxFQUFFLENBQUMsQ0FBQztRQUM5RixNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDckUsTUFBTSxPQUFPLENBQUMsZUFBZSxDQUFDLGFBQWEsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDakYsQ0FBQyxDQUFDLENBQUM7SUFFSCxHQUFHLENBQUMsNEJBQTRCLENBQUMsQ0FBQztBQUNuQyxDQUFDO0FBRUQscUZBQXFGO0FBQ3JGLCtFQUErRTtBQUMvRSx3RkFBd0Y7QUFDeEYsMkVBQTJFO0FBQzNFLGtGQUFrRjtBQUNsRixxREFBcUQ7QUFDckQsS0FBSyxVQUFVLElBQUk7SUFDbEIsSUFBSSxDQUFDLGtDQUFZLEVBQUUsQ0FBQztRQUNuQixNQUFNLEVBQUUsUUFBUSxFQUFFLGdCQUFnQixFQUFFLEdBQUcsZ0NBQVUsQ0FBQztRQUNsRCxNQUFNLGVBQWUsQ0FBQyxRQUFRLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQztRQUNsRCxPQUFPO0lBQ1IsQ0FBQztJQUVELE1BQU0sSUFBSSxHQUFHLElBQUksS0FBSyxFQUFFLENBQUM7SUFDekIsTUFBTSxVQUFVLEdBQUcsSUFBSSxHQUFHLEVBQVUsQ0FBQztJQUVyQyxLQUFLLE1BQU0sSUFBSSxJQUFJLElBQUksRUFBRSxDQUFDO1FBQ3pCLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQy9CLENBQUM7SUFFRCxNQUFNLE1BQU0sR0FBRyxJQUFJLEdBQUcsQ0FBUyxDQUFDLFNBQVMsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDO0lBQzFELElBQUksQ0FBQyxDQUFDLDRCQUE0QixDQUFDLEtBQUssTUFBTSxFQUFFLENBQUM7UUFBQyxNQUFNLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQUMsQ0FBQztJQUMxRSxJQUFJLENBQUMsQ0FBQywwQkFBMEIsQ0FBQyxLQUFLLE1BQU0sRUFBRSxDQUFDO1FBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUFDLENBQUM7SUFDdEUsSUFBSSxDQUFDLENBQUMsMkJBQTJCLENBQUMsS0FBSyxNQUFNLEVBQUUsQ0FBQztRQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFBQyxDQUFDO0lBQ3hFLElBQUksQ0FBQyxDQUFDLDBCQUEwQixDQUFDLEtBQUssTUFBTSxFQUFFLENBQUM7UUFBQyxNQUFNLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQUMsQ0FBQztJQUN0RSxJQUFJLENBQUMsQ0FBQyx3QkFBd0IsQ0FBQyxLQUFLLE1BQU0sRUFBRSxDQUFDO1FBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUFDLENBQUM7SUFFbEUsSUFBSSxhQUFhLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBK0IsRUFBRSxDQUFDLENBQUM7SUFDdEUsTUFBTSxVQUFVLEdBQWlELEVBQUUsQ0FBQztJQUVwRSxPQUFPLElBQUksRUFBRSxDQUFDO1FBQ2IsTUFBTSxDQUFDLFFBQVEsRUFBRSxTQUFTLENBQUMsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFBLGFBQUssRUFBQyxHQUFHLEVBQUUsQ0FBQyxtQkFBbUIsRUFBRSxDQUFDLEVBQUUsSUFBQSxhQUFLLEVBQUMsR0FBRyxFQUFFLENBQUMsb0JBQW9CLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUMzSCxNQUFNLGVBQWUsR0FBRyxJQUFJLEdBQUcsQ0FBUyxRQUFRLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEtBQUssT0FBTyxJQUFJLENBQUMsQ0FBQyxLQUFLLEtBQUssV0FBVyxJQUFJLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDNUosTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLEdBQUcsTUFBTSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxlQUFlLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDMUUsTUFBTSxtQkFBbUIsR0FBRyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUUxRSxJQUFJLGdCQUFnQixDQUFDLE1BQU0sS0FBSyxDQUFDLElBQUksU0FBUyxDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUMsSUFBSSxHQUFHLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUN2RixNQUFNO1FBQ1AsQ0FBQzthQUFNLElBQUksZ0JBQWdCLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3hDLE9BQU8sQ0FBQyxHQUFHLENBQUMscUJBQXFCLEVBQUUsZ0JBQWdCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDakUsQ0FBQzthQUFNLElBQUksbUJBQW1CLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQzNDLE9BQU8sQ0FBQyxHQUFHLENBQUMsd0JBQXdCLEVBQUUsbUJBQW1CLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQ3hGLENBQUM7YUFBTSxDQUFDO1lBQ1AsT0FBTyxDQUFDLEdBQUcsQ0FBQywwQkFBMEIsU0FBUyxDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUMsSUFBSSxVQUFVLFVBQVUsQ0FBQyxJQUFJLGlCQUFpQixDQUFDLENBQUM7UUFDakgsQ0FBQztRQUVELEtBQUssTUFBTSxRQUFRLElBQUksU0FBUyxFQUFFLENBQUM7WUFDbEMsSUFBSSxJQUFJLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxVQUFVLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO2dCQUM5RCxTQUFTO1lBQ1YsQ0FBQztZQUVELE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxRQUFRLENBQUMsSUFBSSxzQkFBc0IsQ0FBQyxDQUFDO1lBRXJELE1BQU0sZUFBZSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLHFCQUFxQixDQUFDLEVBQUUsR0FBRyxRQUFRLENBQUMsSUFBSSxNQUFNLENBQUMsQ0FBQztZQUVwRixNQUFNLElBQUEsYUFBSyxFQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtnQkFDN0IsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO2dCQUN6QixPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksUUFBUSxDQUFDLElBQUksMEJBQTBCLE9BQU8sTUFBTSxDQUFDLENBQUM7Z0JBQ3RFLE1BQU0sZ0JBQWdCLENBQUMsUUFBUSxFQUFFLGVBQWUsQ0FBQyxDQUFDO2dCQUNsRCxNQUFNLFdBQVcsR0FBRyxFQUFFLENBQUMsUUFBUSxDQUFDLGVBQWUsQ0FBQyxDQUFDLElBQUksQ0FBQztnQkFDdEQsTUFBTSxpQkFBaUIsR0FBRyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxLQUFLLENBQUMsR0FBRyxJQUFJLENBQUM7Z0JBQ3RELE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLFdBQVcsR0FBRyxJQUFJLENBQUMsR0FBRyxpQkFBaUIsQ0FBQyxDQUFDO2dCQUM5RSxPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksUUFBUSxDQUFDLElBQUksbUNBQW1DLElBQUksQ0FBQyxLQUFLLENBQUMsaUJBQWlCLENBQUMsWUFBWSxnQkFBZ0IsU0FBUyxDQUFDLENBQUM7WUFDckksQ0FBQyxDQUFDLENBQUM7WUFFSCxNQUFNLGdCQUFnQixHQUFHLE1BQU0sS0FBSyxDQUFDLGVBQWUsRUFBRSxDQUFDLENBQUMscUJBQXFCLENBQUMsQ0FBQyxDQUFDO1lBQ2hGLE1BQU0sWUFBWSxHQUFHLEVBQUUsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxJQUFJLENBQUM7WUFFeEQsSUFBSSxZQUFZLEtBQUssTUFBTSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUM7Z0JBQ3hFLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxRQUFRLENBQUMsSUFBSSxxQ0FBcUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsWUFBWSxZQUFZLFlBQVksR0FBRyxDQUFDLENBQUM7Z0JBQ3hJLE1BQU0sSUFBSSxLQUFLLENBQUMseUJBQXlCLENBQUMsQ0FBQztZQUM1QyxDQUFDO1lBRUQsVUFBVSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDOUIsTUFBTSxPQUFPLEdBQUcsSUFBSSxPQUFPLENBQU8sQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7Z0JBQ3JELE1BQU0sTUFBTSxHQUFHLElBQUksNEJBQU0sQ0FBQyxVQUFVLEVBQUUsRUFBRSxVQUFVLEVBQUUsRUFBRSxRQUFRLEVBQUUsZ0JBQWdCLEVBQUUsRUFBRSxDQUFDLENBQUM7Z0JBQ3RGLE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO2dCQUMzQixNQUFNLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsRUFBRTtvQkFDeEIsSUFBSSxJQUFJLEtBQUssQ0FBQyxFQUFFLENBQUM7d0JBQ2hCLE9BQU8sRUFBRSxDQUFDO29CQUNYLENBQUM7eUJBQU0sQ0FBQzt3QkFDUCxNQUFNLENBQUMsSUFBSSxLQUFLLENBQUMsSUFBSSxRQUFRLENBQUMsSUFBSSxtQ0FBbUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDO29CQUMvRSxDQUFDO2dCQUNGLENBQUMsQ0FBQyxDQUFDO1lBQ0osQ0FBQyxDQUFDLENBQUM7WUFFSCxNQUFNLFNBQVMsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRTtnQkFDbkMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ2pDLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO2dCQUN4QixPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVUsUUFBUSxDQUFDLElBQUksR0FBRyxDQUFDLENBQUM7WUFDekMsQ0FBQyxDQUFDLENBQUM7WUFFSCxVQUFVLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLFFBQVEsQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQztZQUNwRCxhQUFhLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUM7UUFDdEUsQ0FBQztRQUVELE1BQU0sSUFBSSxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxVQUFVLENBQUMsQ0FBQyxFQUFFLEtBQU0sQ0FBQyxDQUFDLENBQUM7SUFDL0MsQ0FBQztJQUVELE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxJQUFJLENBQUMsSUFBSSxHQUFHLFVBQVUsQ0FBQyxJQUFJLDJCQUEyQixVQUFVLENBQUMsSUFBSSxvQ0FBb0MsQ0FBQyxDQUFDO0lBRXBJLE1BQU0sbUJBQW1CLEdBQUcsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFFM0UsSUFBSSxtQkFBbUIsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDcEMsT0FBTyxDQUFDLEdBQUcsQ0FBQyx3QkFBd0IsRUFBRSxtQkFBbUIsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDeEYsQ0FBQztJQUVELE1BQU0sT0FBTyxHQUFHLE1BQU0sYUFBYSxDQUFDO0lBRXBDLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxVQUFVLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDNUMsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBRTFCLElBQUksTUFBTSxDQUFDLE1BQU0sS0FBSyxVQUFVLEVBQUUsQ0FBQztZQUNsQyxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksR0FBRyxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUN6RCxDQUFDO0lBQ0YsQ0FBQztJQUVELElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxNQUFNLEtBQUssVUFBVSxDQUFDLEVBQUUsQ0FBQztRQUNoRCxNQUFNLElBQUksS0FBSyxDQUFDLGtDQUFrQyxDQUFDLENBQUM7SUFDckQsQ0FBQztJQUVELE9BQU8sQ0FBQyxHQUFHLENBQUMsT0FBTyxJQUFJLENBQUMsSUFBSSx1QkFBdUIsQ0FBQyxDQUFDO0FBQ3RELENBQUM7QUFFRCxJQUFJLE9BQU8sQ0FBQyxJQUFJLEtBQUssTUFBTSxFQUFFLENBQUM7SUFDN0IsSUFBSSxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRTtRQUNoQixPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ2pCLENBQUMsRUFBRSxHQUFHLENBQUMsRUFBRTtRQUNSLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDbkIsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNqQixDQUFDLENBQUMsQ0FBQztBQUNKLENBQUMifQ== \ No newline at end of file diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index a338b2e4384..a67c29e98b4 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -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; c: (v: any) => void; e: (err: Error) => void }[]; - - constructor(maxDegreeOfParalellism: number) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - } - - queue(factory: () => Promise): Promise { - this._size++; - - return new Promise((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 { - 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 { 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[] = []; + log(`Checking for blob in Azure...`); - uploadPromises.push((async (): Promise => { - 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 => { - 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 { +async function processArtifact(artifact: Artifact, artifactFilePath: string): Promise { + const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); const match = /^vscode_(?[^_]+)_(?[^_]+)_(?[^_]+)_(?[^_]+)$/.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 { 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(); @@ -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[]>([]); const operations: { name: string; operation: Promise }[] = []; 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((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]; diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 972ddb3411f..a46a61be86e 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -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 diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 16730948182..1cf0209aa63 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -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)" diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index f7afc12a448..aee7e85a30a 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -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", diff --git a/extensions/ipynb/src/cellIdService.ts b/extensions/ipynb/src/cellIdService.ts index 2eccb586ef4..43c769bca51 100644 --- a/extensions/ipynb/src/cellIdService.ts +++ b/extensions/ipynb/src/cellIdService.ts @@ -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; +} diff --git a/extensions/ipynb/yarn.lock b/extensions/ipynb/yarn.lock index 7b5488e7109..fa950f66b64 100644 --- a/extensions/ipynb/yarn.lock +++ b/extensions/ipynb/yarn.lock @@ -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== diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index d52d6b6b12a..22fe3119ad1 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -282,6 +282,15 @@ export const activate: ActivationFunction = (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'); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 749673f151d..dbbf937e6c6 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -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": { diff --git a/extensions/markdown-language-features/src/client/inMemoryDocument.ts b/extensions/markdown-language-features/src/client/inMemoryDocument.ts index 0848a79093f..adf7a143c1d 100644 --- a/extensions/markdown-language-features/src/client/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/client/inMemoryDocument.ts @@ -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); } } diff --git a/extensions/markdown-language-features/src/commands/insertResource.ts b/extensions/markdown-language-features/src/commands/insertResource.ts index 87de74270ca..598afb9bcdf 100644 --- a/extensions/markdown-language-features/src/commands/insertResource.ts +++ b/extensions/markdown-language-features/src/commands/insertResource.ts @@ -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, diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropResourceProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropResourceProvider.ts index 65bd9bf9d7c..6a8bdf88609 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropResourceProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropResourceProvider.ts @@ -44,12 +44,12 @@ class ResourceDropProvider implements vscode.DocumentDropEditProvider { private async _getUriListEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { 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); diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteResourceProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteResourceProvider.ts index 23292cbb97d..46b47e5198e 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteResourceProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteResourceProvider.ts @@ -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; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index b68b9cdaa2b..814f5e5d9f3 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -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; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index dc8c15075f4..5c591457c41 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -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([ ['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('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; } - diff --git a/extensions/markdown-language-features/src/test/markdownLink.test.ts b/extensions/markdown-language-features/src/test/markdownLink.test.ts index f0133ad85cc..4d0fffd0585 100644 --- a/extensions/markdown-language-features/src/test/markdownLink.test.ts +++ b/extensions/markdown-language-features/src/test/markdownLink.test.ts @@ -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}]()'); }); 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); +} diff --git a/extensions/markdown-language-features/src/types/textDocument.ts b/extensions/markdown-language-features/src/types/textDocument.ts index a960e54d220..e57299671f6 100644 --- a/extensions/markdown-language-features/src/types/textDocument.ts +++ b/extensions/markdown-language-features/src/types/textDocument.ts @@ -12,6 +12,6 @@ export interface ITextDocument { readonly uri: vscode.Uri; readonly version: number; - getText(): string; + getText(range?: vscode.Range): string; } diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index bb33b5fe336..5e3d1826af5 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -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" diff --git a/extensions/package.json b/extensions/package.json index 680720b8a54..8bd2f6d07ce 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -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" diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 65e9d57fc38..66532bc81fc 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -62,7 +62,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { 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); diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts index 73e6335dbad..de7d1f6d900 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts @@ -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('referencesCodeLens.showOnAllFunctions'); - if (showOnAllFunctions) { + if (showOnAllFunctions && item.nameSpan) { return getSymbolRange(document, item); } } diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index e0fadc0cc43..b37b1cdf7da 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -223,7 +223,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider{ 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: '' }; diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 1c05c6b8a0c..0b75dab4036 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -506,7 +506,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider/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 diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 0694339a3f8..38e4c223bea 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index c314f5169b4..16c6df2478c 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -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 diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index ad4808472c6..00ad27c3d55 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -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'; diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index cc4c65a625f..3cb7bc9795b 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -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 { +async function openZip(zipFile: string, lazy: boolean = false): Promise { + const { open } = await import('yauzl'); + return new Promise((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 { +export async function zip(zipPath: string, files: IFile[]): Promise { + const { ZipFile } = await import('yazl'); + return new Promise((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); diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html index 02f40f0b97e..a92bea242a4 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -42,6 +42,7 @@ 'self' vscode-remote-resource: vscode-managed-remote-resource: + https://*.vscode-unpkg.net ; require-trusted-types-for 'script' diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index c787d56e30b..eb525bd59f6 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -42,6 +42,7 @@ 'self' vscode-remote-resource: vscode-managed-remote-resource: + https://*.vscode-unpkg.net ; require-trusted-types-for 'script' diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 5e63ede1f3f..23f8d13ce46 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -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 { diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 74e92ed7867..73236152541 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -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)); } diff --git a/src/vs/editor/browser/widget/diffEditor/accessibleDiffViewer.css b/src/vs/editor/browser/widget/diffEditor/accessibleDiffViewer.css index cca9d486aa6..cd20cfb12f3 100644 --- a/src/vs/editor/browser/widget/diffEditor/accessibleDiffViewer.css +++ b/src/vs/editor/browser/widget/diffEditor/accessibleDiffViewer.css @@ -62,3 +62,7 @@ height: 16px; margin: 2px 0; } + +.monaco-diff-editor .revertButton { + cursor: pointer; +} diff --git a/src/vs/editor/browser/widget/diffEditor/decorations.ts b/src/vs/editor/browser/widget/diffEditor/decorations.ts index 350bbd58120..d8be47dce6a 100644 --- a/src/vs/editor/browser/widget/diffEditor/decorations.ts +++ b/src/vs/editor/browser/widget/diffEditor/decorations.ts @@ -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, -}); diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorDecorations.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorDecorations.ts index 2e8fc3f0cf3..6791b2f9df2 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorDecorations.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorDecorations.ts @@ -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, 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, + 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, + }; + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts index 56f4f7b2686..aea7fc9b20b 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts @@ -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; + public readonly modifiedSelections: IObservable; + public readonly modifiedCursor: IObservable; + 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) => { diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index 3b54ff415f4..265c885643b 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -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(); })); diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 1231ec322f4..0747ff3fde2 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -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(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(c => ({ + range: c.modifiedRange, + text: model.model.original.getValueInRange(c.originalRange) + })); this._editors.modified.executeEdits('diffEditor', changes); } diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts index 7021e1a0147..ea2d446132d 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts @@ -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 { + private readonly _viewModel = observableValue(this, undefined); + + private readonly _collapsed = derived(this, reader => this._viewModel.read(reader)?.collapsed.read(reader)); + private readonly _contentHeight = observableValue(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(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 { + return observableFromEvent( + h => { + const store = new DisposableStore(); + store.add(editor.onDidFocusEditorWidget(() => h(true))); + store.add(editor.onDidBlurEditorWidget(() => h(false))); + return store; + }, + () => editor.hasWidgetFocus() + ); +} diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel.ts new file mode 100644 index 00000000000..35d669db764 --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel.ts @@ -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(this, + (reader, store) => this._documents.read(reader).map(d => store.add(new DocumentDiffItemViewModel(d, this._diffEditorViewModelFactory))) + ).recomputeInitiallyAndOnChange(this._store); + + public readonly activeDiffItem = observableValue(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(this, false); + + constructor( + public readonly entry: LazyPromise, + diffEditorViewModelFactory: DiffEditorWidget + ) { + super(); + + this.diffEditorViewModel = this._register(diffEditorViewModelFactory.createViewModel({ + original: entry.value!.original!, + modified: entry.value!.modified!, + })); + } +} diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts index 69b47dfb5f5..97376e36d19 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts @@ -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(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); } diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts index 2261883a556..e35b1101265 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts @@ -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((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(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, - 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, private readonly _scrollLeft: IObservable, ) { @@ -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); diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css b/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css index 7206dda8cf6..75c8db7a956 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css @@ -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; +} diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index 6a99f241df0..da150f47952 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -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]; diff --git a/src/vs/editor/common/services/languageFeatureDebounce.ts b/src/vs/editor/common/services/languageFeatureDebounce.ts index e0d2cb832f3..e537b05bc82 100644 --- a/src/vs/editor/common/services/languageFeatureDebounce.ts +++ b/src/vs/editor/common/services/languageFeatureDebounce.ts @@ -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'); diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index c9fb7f3f662..bcabeffed2b 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -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; } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index b64534c7865..19eee1dcc54 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -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 { } diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 4f1b579d5e7..26dea91e681 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -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 () { diff --git a/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts b/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts index 1acbb89651c..1d17e4c709e 100644 --- a/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts +++ b/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts @@ -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'; diff --git a/src/vs/platform/log/node/spdlogLog.ts b/src/vs/platform/log/node/spdlogLog.ts index 4f5f1bf8a8c..11f078f941e 100644 --- a/src/vs/platform/log/node/spdlogLog.ts +++ b/src/vs/platform/log/node/spdlogLog.ts @@ -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'; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 88ec38a2787..646d67a9c3e 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -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 /* 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 { - 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 { diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 1996a2c8b49..1f4d1d96b13 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -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; } -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`. * diff --git a/src/vs/platform/policy/node/nativePolicyService.ts b/src/vs/platform/policy/node/nativePolicyService.ts index eae3bf8dfac..d6e3f60cdd1 100644 --- a/src/vs/platform/policy/node/nativePolicyService.ts +++ b/src/vs/platform/policy/node/nativePolicyService.ts @@ -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): Promise { 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((c, e) => { try { this.watcher.value = createWatcher(this.productName, policyDefinitions, update => { diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 07318efce27..23f8f0d44c8 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -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 { - return loadSystemCertificates({ log: this.logService }); + const proxyAgent = await import('@vscode/proxy-agent'); + return proxyAgent.loadSystemCertificates({ log: this.logService }); } } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 3c48ecfaf0d..73db745352a 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -22,6 +22,7 @@ export interface ITelemetryService { readonly sessionId: string; readonly machineId: string; + readonly sqmId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index e10d846b083..a9a9cc48109 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -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; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 85c0fbee4dd..7971bdaadbf 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -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() { } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index d4d2892df16..da16580b4b4 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -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) { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index f85d1d111b8..e1190a65660 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -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(); @@ -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; abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; + abstract provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject; } class MainThreadKernelDetectionTask implements INotebookKernelDetectionTask { @@ -214,6 +222,15 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape return didSend; } + private variableRequestIndex = 0; + private variableRequestMap = new Map>(); + $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 { @@ -225,6 +242,24 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise { await that._proxy.$cancelCells(handle, uri, handles); } + provideVariables(notebookUri: URI, parentName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject { + const requestId = `${handle}variables${that.variableRequestIndex++}`; + if (that.variableRequestMap.has(requestId)) { + return that.variableRequestMap.get(requestId)!.asyncIterable; + } + + const source = new AsyncIterableSource(); + 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)); + } } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 1e29e53e8bb..b5d02c40e37 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -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 { 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 { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 6ea907a0f07..ee5ec60e54b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ba6939ca741..a5675e79f1e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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; $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; + $provideVariables(handle: number, requestId: string, notebookUri: UriComponents, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise; } export interface ExtHostInteractiveShape { diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 7412cb9168a..62f9ebbf690 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -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'; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 144dc007e93..c43db4a08b6 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -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 { diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index a3ac51d0446..164369d1212 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -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) | 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 { + 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) { diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index ea9d652f143..549c604ad0b 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -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; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 3079d146530..f4bc4f410ea 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -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; } } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index df40c53d3be..a9d705653b1 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3798,6 +3798,11 @@ export class NotebookKernelSourceAction { ) { } } +export enum NotebookVariablesRequestKind { + Named = 1, + Indexed = 2 +} + //#endregion //#region Timeline diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 262ba48a3ef..40e8224f45f 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -42,6 +42,7 @@ suite('ExtHostTelemetry', function () { firstSessionDate: '2020-01-01T00:00:00.000Z', sessionId: 'test', machineId: 'test', + sqmId: 'test' }; const mockRemote = { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index c056eb29fb3..22f155f85c7 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -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( diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 0f325909c06..8ba56b807fb 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -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({ diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index d199084e616..3b1850cc88d 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -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 { 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 { + 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 { + const editorGroupService = accessor.get(IEditorGroupsService); + + editorGroupService.mergeAllGroups(editorGroupService.mainPart.activeGroup); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index c7cee50d8c3..761e48edc6b 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -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'; diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index f57b9e54e1c..2f72fc9740e 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -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); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 932001f18ca..dee3a78e751 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -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 { diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 475889c9d15..2ce17e894ca 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -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 { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index a185956cd94..a36b7a4de29 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -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'; } diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 8c983240ded..c1ea5ce7b4a 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -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'; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index a0c22da313c..a435aa0ccdf 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -247,7 +247,7 @@ const registry = Registry.as(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', diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 972b483c675..b679221edce 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -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(); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 57fb9595ac1..dac8c2a0423 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -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 { @@ -211,7 +213,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { editor.layout(this._currentLayoutWidth); }); @@ -223,6 +225,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - 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, 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>, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { + private basicRenderElement(value: ReadonlyArray, 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 { + 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) diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 0015e1cf339..b630cba32d2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -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>/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): ReadonlyArray> { - const result: Exclude[] = []; +export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { + const result: Exclude[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { @@ -75,6 +105,13 @@ export function reduceInlineContentReferences(response: ReadonlyArray${item.content.value}`; + 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); } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 89e4e1bd940..0bf13a5db1a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -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, }); } diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css index cfda6c03a44..5c778eb2601 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -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); +} diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index fd5350bf367..7f55edcb93b 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -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; + readonly onDidChangeContentHeight: Event; 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()); + private readonly _onDidChangeContentHeight = this._register(new Emitter()); 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 { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 7ba3e573bd9..d994f964e0c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -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); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d4617660efe..2b8e3e3292a 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -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; + export interface IResponse { readonly value: ReadonlyArray; asString(): string; @@ -100,7 +103,7 @@ export class Response implements IResponse { return this._responseParts; } - constructor(value: IMarkdownString | ReadonlyArray) { + constructor(value: IMarkdownString | ReadonlyArray) { 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, + _response: IMarkdownString | ReadonlyArray, 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; export interface ISerializableChatRequestData { message: string | IParsedChatRequest; - response: ReadonlyArray | undefined; + response: ReadonlyArray | 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 { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index fcd5a652619..a5376b97e0e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -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 diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 9d447d5bbfc..a6775318b6c 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -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 { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 6fba224d47f..2ed73840d90 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -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'); } diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.0.snap new file mode 100644 index 00000000000..6c449047db6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.1.snap new file mode 100644 index 00000000000..644f8a772ae --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.1.snap @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.0.snap new file mode 100644 index 00000000000..f6b34c1a018 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.1.snap new file mode 100644 index 00000000000..5107938a33a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.1.snap @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.0.snap new file mode 100644 index 00000000000..5fd2e9f33b6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "some code content with vuln after", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.1.snap new file mode 100644 index 00000000000..d61a6b5926e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.1.snap @@ -0,0 +1,15 @@ +{ + newText: "some code content with vuln after", + vulnerabilities: [ + { + title: "title", + description: "vuln", + range: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 28 + } + } + ] +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownDecorationsRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownDecorationsRenderer.test.ts new file mode 100644 index 00000000000..3d8901b789f --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownDecorationsRenderer.test.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { assertSnapshot } from 'vs/base/test/common/snapshot'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; + +function content(str: string): IChatMarkdownContent { + return { kind: 'markdownContent', content: new MarkdownString(str) }; +} + +suite('ChatMarkdownDecorationsRenderer', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('extractVulnerabilitiesFromText', () => { + test('single line', async () => { + const before = 'some code '; + const vulnContent = 'content with vuln'; + const after = ' after'; + const annotatedResult = annotateSpecialMarkdownContent([content(before), { kind: 'markdownVuln', content: new MarkdownString(vulnContent), title: 'title', description: 'vuln' }, content(after)]); + await assertSnapshot(annotatedResult); + + const markdown = annotatedResult[0] as IChatMarkdownContent; + const result = extractVulnerabilitiesFromText(markdown.content.value); + await assertSnapshot(result); + }); + + test('multiline', async () => { + const before = 'some code\nover\nmultiple lines '; + const vulnContent = 'content with vuln\nand\nnewlines'; + const after = 'more code\nwith newline'; + const annotatedResult = annotateSpecialMarkdownContent([content(before), { kind: 'markdownVuln', content: new MarkdownString(vulnContent), title: 'title', description: 'vuln' }, content(after)]); + await assertSnapshot(annotatedResult); + + const markdown = annotatedResult[0] as IChatMarkdownContent; + const result = extractVulnerabilitiesFromText(markdown.content.value); + await assertSnapshot(result); + }); + + test('multiple vulns', async () => { + const before = 'some code\nover\nmultiple lines '; + const vulnContent = 'content with vuln\nand\nnewlines'; + const after = 'more code\nwith newline'; + const annotatedResult = annotateSpecialMarkdownContent([ + content(before), + { kind: 'markdownVuln', content: new MarkdownString(vulnContent), title: 'title', description: 'vuln' }, + content(after), + { kind: 'markdownVuln', content: new MarkdownString(vulnContent), title: 'title', description: 'vuln' }, + ]); + await assertSnapshot(annotatedResult); + + const markdown = annotatedResult[0] as IChatMarkdownContent; + const result = extractVulnerabilitiesFromText(markdown.content.value); + await assertSnapshot(result); + }); + }); +}); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 8293c365d25..5b5b09c6966 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -3,74 +3,94 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/extensionEditor'; -import { localize } from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; -import { OS, language } from 'vs/base/common/platform'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Cache, CacheResult } from 'vs/base/common/cache'; -import { Action, IAction } from 'vs/base/common/actions'; -import { getErrorMessage, isCancellationError, onUnexpectedError } from 'vs/base/common/errors'; -import { dispose, toDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { append, $, join, addDisposableListener, setParentFlowTo, reset, Dimension } from 'vs/base/browser/dom'; -import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; -import { ExtensionIdentifier, IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platform/extensions/common/extensions'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtensionContainer } from 'vs/workbench/contrib/extensions/common/extensions'; -import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget, ExtensionWidget, ExtensionStatusWidget, ExtensionRecommendationWidget, SponsorWidget, onClick, VerifiedPublisherWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { IEditorOpenContext } from 'vs/workbench/common/editor'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { - UpdateAction, ReloadAction, EnableDropDownAction, DisableDropDownAction, ExtensionStatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, - RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction, - ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, - InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, SkipUpdateAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { ContextKeyExpr, IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Color } from 'vs/base/common/color'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ExtensionsTree, ExtensionData, ExtensionsGridView, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer'; -import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; -import { isUndefined } from 'vs/base/common/types'; -import { IWebviewService, IWebview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; -import { generateUuid } from 'vs/base/common/uuid'; -import { platform } from 'vs/base/common/process'; -import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { TokenizationRegistry } from 'vs/editor/common/languages'; -import { generateTokensCSSForColorMap } from 'vs/editor/common/languages/supports/tokenization'; -import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; +import { $, Dimension, addDisposableListener, append, join, reset, setParentFlowTo } from 'vs/base/browser/dom'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { errorIcon, infoIcon, preReleaseIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; -import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; -import { IExtensionGalleryService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Action, IAction } from 'vs/base/common/actions'; +import * as arrays from 'vs/base/common/arrays'; +import { Cache, CacheResult } from 'vs/base/common/cache'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; +import { getErrorMessage, isCancellationError, onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { Disposable, DisposableStore, MutableDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas, matchesScheme } from 'vs/base/common/network'; +import { OS, language } from 'vs/base/common/platform'; +import { platform } from 'vs/base/common/process'; import * as semver from 'vs/base/common/semver/semver'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { isUndefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import 'vs/css!./media/extensionEditor'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { TokenizationRegistry } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { generateTokensCSSForColorMap } from 'vs/editor/common/languages/supports/tokenization'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr, IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IExtensionGalleryService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionIdentifier, IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { + ActionWithDropDownAction, + ClearLanguageAction, + DisableDropDownAction, + EnableDropDownAction, + ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, + ExtensionEditorManageExtensionAction, + ExtensionStatusAction, + ExtensionStatusLabelAction, + InstallAnotherVersionAction, + InstallDropdownAction, InstallingLabelAction, + LocalInstallAction, + MigrateDeprecatedExtensionAction, + ReloadAction, + RemoteInstallAction, + SetColorThemeAction, + SetFileIconThemeAction, + SetLanguageAction, + SetProductIconThemeAction, + SkipUpdateAction, + SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, + ToggleSyncExtensionAction, + UninstallAction, + UpdateAction, + WebInstallAction +} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { errorIcon, infoIcon, preReleaseIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; +import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer'; +import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, VerifiedPublisherWidget, onClick } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; +import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; +import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; +import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; +import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; class NavBar extends Disposable { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 97516b77272..d48e61ff247 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -151,7 +151,6 @@ color: var(--vscode-chat-slashCommandForeground); } - .monaco-editor .inline-chat .markdownMessage { padding: 10px 5px; } @@ -194,6 +193,32 @@ -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); } +.monaco-editor .inline-chat .followUps { + padding: 5px 5px; +} + +.monaco-editor .inline-chat .followUps .interactive-session-followups .monaco-button { + display: block; + color: var(--vscode-textLink-foreground); + font-size: 12px; +} + +.monaco-editor .inline-chat .followUps.hidden { + display: none; +} + +.monaco-editor .inline-chat .chatMessage { + padding: 8px 3px; +} + +.monaco-editor .inline-chat .chatMessage .chatMessageContent { + padding: 2px 2px; +} + +.monaco-editor .inline-chat .chatMessage.hidden { + display: none; +} + .monaco-editor .inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 4d5563f66f2..f981c60c757 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -10,7 +10,7 @@ import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, InlineChatResponseType, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChateResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, InlineChatResponseType, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChateResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_TOOLBAR_ICON_ENABLED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize } from 'vs/nls'; import { IAction2Options, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -52,7 +52,7 @@ export class StartSessionAction extends EditorAction2 { icon: START_INLINE_CHAT, menu: [{ id: MenuId.EditorTitle, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_VISIBLE.toNegated(), EditorContextKeys.focus), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_TOOLBAR_ICON_ENABLED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_VISIBLE.toNegated(), EditorContextKeys.focus), group: 'navigation', order: -1000000, // at the very front }], diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index ed2a46603fe..f9d0c371bcd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Barrier, Queue, raceCancellationError } from 'vs/base/common/async'; +import { Barrier, Queue, raceCancellation, raceCancellationError } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; @@ -26,7 +26,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ILogService } from 'vs/platform/log/common/log'; import { ReplyResponse, EmptyResponse, ErrorResponse, ExpansionState, IInlineChatSessionService, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, EditMode, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, InlineChatResponseType, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, InlineChateResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_USER_DID_EDIT, IInlineChatProgressItem, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -45,6 +45,7 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -105,8 +106,10 @@ export class InlineChatController implements IEditorContribution { className: 'inline-chat-block-selection', }); + private static _storageKey = 'inline-chat-history'; private static _promptHistory: string[] = []; private _historyOffset: number = -1; + private _historyUpdate: (prompt: string) => void; private readonly _store = new DisposableStore(); private readonly _zone: Lazy; @@ -146,6 +149,7 @@ export class InlineChatController implements IEditorContribution { @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, + @IStorageService private readonly _storageService: IStorageService, ) { this._ctxHasActiveRequest = CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); this._ctxDidEdit = CTX_INLINE_CHAT_DID_EDIT.bindTo(contextKeyService); @@ -171,6 +175,17 @@ export class InlineChatController implements IEditorContribution { this._log('session done or paused'); })); this._log('NEW controller'); + + InlineChatController._promptHistory = JSON.parse(_storageService.get(InlineChatController._storageKey, StorageScope.PROFILE, '[]')); + this._historyUpdate = (prompt: string) => { + const idx = InlineChatController._promptHistory.indexOf(prompt); + if (idx >= 0) { + InlineChatController._promptHistory.splice(idx, 1); + } + InlineChatController._promptHistory.unshift(prompt); + this._historyOffset = -1; + this._storageService.store(InlineChatController._storageKey, JSON.stringify(InlineChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); + }; } dispose(): void { @@ -542,9 +557,8 @@ export class InlineChatController implements IEditorContribution { const input = this.getInput(); - if (!InlineChatController._promptHistory.includes(input)) { - InlineChatController._promptHistory.unshift(input); - } + + this._historyUpdate(input); const refer = this._activeSession.session.slashCommands?.some(value => value.refer && input!.startsWith(`/${value.command}`)); if (refer) { @@ -606,13 +620,14 @@ export class InlineChatController implements IEditorContribution { const modelAltVersionIdNow = this._activeSession.textModelN.getAlternativeVersionId(); const progressEdits: TextEdit[][] = []; - const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); const progressiveEditsAvgDuration = new MovingAverage(); const progressiveEditsCts = new CancellationTokenSource(requestCts.token); const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); + let progressiveChatResponse: IInlineChatMessageAppender | undefined; + const progress = new Progress(data => { this._log('received chunk', data, request); @@ -658,8 +673,16 @@ export class InlineChatController implements IEditorContribution { }); } if (data.markdownFragment) { - markdownContents.appendMarkdown(data.markdownFragment); - this._zone.value.widget.updateMarkdownMessage(markdownContents); + if (!progressiveChatResponse) { + const message = { + message: new MarkdownString(data.markdownFragment, { supportThemeIcons: true, supportHtml: true, isTrusted: false }), + providerId: this._activeSession!.provider.debugName, + requestId: request.requestId, + }; + progressiveChatResponse = this._zone.value.widget.updateChatMessage(message, true); + } else { + progressiveChatResponse.appendContent(data.markdownFragment); + } } }); @@ -673,6 +696,9 @@ export class InlineChatController implements IEditorContribution { let response: ReplyResponse | ErrorResponse | EmptyResponse; let reply: IInlineChatResponse | null | undefined; try { + this._zone.value.widget.updateChatMessage(undefined); + this._zone.value.widget.updateMarkdownMessage(undefined); + this._zone.value.widget.updateFollowUps(undefined); this._zone.value.widget.updateProgress(true); this._zone.value.widget.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : ''); this._ctxHasActiveRequest.set(true); @@ -682,15 +708,16 @@ export class InlineChatController implements IEditorContribution { // we must wait for all edits that came in via progress to complete await Event.toPromise(progressiveEditsQueue.onDrained); } + if (progressiveChatResponse) { + progressiveChatResponse.cancel(); + } if (!reply) { response = new EmptyResponse(); a11yResponse = localize('empty', "No results, please refine your input and try again"); } else { - if (reply.message) { - markdownContents.appendMarkdown(reply.message.value); - } - const replyResponse = response = this._instaService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, modelAltVersionIdNow, progressEdits); + const markdownContents = reply.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); + const replyResponse = response = this._instaService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId); for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { await this._makeChanges(replyResponse.allLocalEdits[i], undefined); @@ -738,7 +765,7 @@ export class InlineChatController implements IEditorContribution { } } - private async [State.APPLY_RESPONSE](): Promise { + private async[State.APPLY_RESPONSE](): Promise { assertType(this._activeSession); assertType(this._strategy); @@ -820,11 +847,35 @@ export class InlineChatController implements IEditorContribution { } else if (response instanceof ReplyResponse) { // real response -> complex... this._zone.value.widget.updateStatus(''); - this._zone.value.widget.updateMarkdownMessage(response.mdContent); + const message = { message: response.mdContent, providerId: this._activeSession.provider.debugName, requestId: response.requestId }; + this._zone.value.widget.updateChatMessage(message); + + //this._zone.value.widget.updateMarkdownMessage(response.mdContent); this._activeSession.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); await this._strategy.renderChanges(response); + + if (this._activeSession.provider.provideFollowups) { + const followupCts = new CancellationTokenSource(); + const msgListener = Event.once(this._messages.event)(() => { + followupCts.cancel(); + }); + const followupTask = this._activeSession.provider.provideFollowups(this._activeSession.session, response.raw, followupCts.token); + this._log('followup request started', this._activeSession.provider.debugName, this._activeSession.session, response.raw); + raceCancellation(Promise.resolve(followupTask), followupCts.token).then(followupReply => { + if (followupReply && this._activeSession) { + this._log('followup request received', this._activeSession.provider.debugName, this._activeSession.session, followupReply); + this._zone.value.widget.updateFollowUps(followupReply, followup => { + this.updateInput(followup.message); + this.acceptInput(); + }); + } + }).finally(() => { + msgListener.dispose(); + followupCts.dispose(); + }); + } } this._showWidget(false); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDecorations.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDecorations.ts index b69a5c233bd..36a03fb663f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDecorations.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDecorations.ts @@ -16,7 +16,7 @@ import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/co import { DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { GutterActionsRegistry } from 'vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu'; import { Action } from 'vs/base/common/actions'; -import { IInlineChatService, ShowGutterIcon } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_TOOLBAR_ICON_ENABLED, IInlineChatService, ShowGutterIcon } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Iterable } from 'vs/base/common/iterator'; import { Range } from 'vs/editor/common/core/range'; @@ -24,29 +24,35 @@ import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/brows import { MarkdownString } from 'vs/base/common/htmlContent'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { LOCALIZED_START_INLINE_CHAT_STRING } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { IBreakpoint, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IBreakpoint, IDebugService, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { URI } from 'vs/base/common/uri'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const GUTTER_INLINE_CHAT_OPAQUE_ICON = registerIcon('inline-chat-opaque', Codicon.sparkle, localize('startInlineChatOpaqueIcon', 'Icon which spawns the inline chat from the gutter. It is half opaque by default and becomes completely opaque on hover.')); const GUTTER_INLINE_CHAT_TRANSPARENT_ICON = registerIcon('inline-chat-transparent', Codicon.sparkle, localize('startInlineChatTransparentIcon', 'Icon which spawns the inline chat from the gutter. It is transparent by default and becomes opaque on hover.')); export class InlineChatDecorationsContribution extends Disposable implements IEditorContribution { + private _ctxToolbarIconEnabled: IContextKey; private _currentBreakpoints: readonly IBreakpoint[] = []; private _gutterDecorationID: string | undefined; private _inlineChatKeybinding: string | undefined; private _hasInlineChatSession: boolean = false; + private _hasActiveDebugSession: boolean = false; + private _debugSessions: Set = new Set(); private readonly _localToDispose = new DisposableStore(); private readonly _gutterDecorationOpaque: IModelDecorationOptions; private readonly _gutterDecorationTransparent: IModelDecorationOptions; + public static readonly TOOLBAR_SETTING_ID = 'inlineChat.showToolbarIcon'; public static readonly GUTTER_SETTING_ID = 'inlineChat.showGutterIcon'; private static readonly GUTTER_ICON_OPAQUE_CLASSNAME = 'codicon-inline-chat-opaque'; private static readonly GUTTER_ICON_TRANSPARENT_CLASSNAME = 'codicon-inline-chat-transparent'; constructor( private readonly _editor: ICodeEditor, + @IContextKeyService _contextKeyService: IContextKeyService, @IInlineChatService private readonly _inlineChatService: IInlineChatService, @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -56,7 +62,13 @@ export class InlineChatDecorationsContribution extends Disposable implements IEd super(); this._gutterDecorationTransparent = this._registerGutterDecoration(true); this._gutterDecorationOpaque = this._registerGutterDecoration(false); + this._ctxToolbarIconEnabled = CTX_INLINE_CHAT_TOOLBAR_ICON_ENABLED.bindTo(_contextKeyService); + this._setToolbarIconEnablementToSetting(); this._register(this._configurationService.onDidChangeConfiguration((e: IConfigurationChangeEvent) => { + if (e.affectsConfiguration(InlineChatDecorationsContribution.TOOLBAR_SETTING_ID)) { + this._setToolbarIconEnablementToSetting(); + return; + } if (!e.affectsConfiguration(InlineChatDecorationsContribution.GUTTER_SETTING_ID)) { return; } @@ -74,6 +86,20 @@ export class InlineChatDecorationsContribution extends Disposable implements IEd this._onEnablementOrModelChanged(); } })); + this._register(this._debugService.onWillNewSession((session) => { + this._debugSessions.add(session); + if (!this._hasActiveDebugSession) { + this._hasActiveDebugSession = true; + this._onEnablementOrModelChanged(); + } + })); + this._register(this._debugService.onDidEndSession((session) => { + this._debugSessions.delete(session); + if (this._debugSessions.size === 0) { + this._hasActiveDebugSession = false; + this._onEnablementOrModelChanged(); + } + })); this._register(this._inlineChatService.onDidChangeProviders(() => this._onEnablementOrModelChanged())); this._register(this._editor.onDidChangeModel(() => this._onEnablementOrModelChanged())); this._register(this._keybindingService.onDidUpdateKeybindings(() => { @@ -84,6 +110,10 @@ export class InlineChatDecorationsContribution extends Disposable implements IEd this._onEnablementOrModelChanged(); } + private _setToolbarIconEnablementToSetting(): void { + this._ctxToolbarIconEnabled.set(this._configurationService.getValue(InlineChatDecorationsContribution.TOOLBAR_SETTING_ID)); + } + private _registerGutterDecoration(isTransparent: boolean): ModelDecorationOptions { return ModelDecorationOptions.register({ description: 'inline-chat-decoration', @@ -111,7 +141,7 @@ export class InlineChatDecorationsContribution extends Disposable implements IEd private _onEnablementOrModelChanged(): void { // cancels the scheduler, removes editor listeners / removes decoration this._localToDispose.clear(); - if (!this._editor.hasModel() || this._hasInlineChatSession || this._showGutterIconMode() === ShowGutterIcon.Never || !this._hasProvider()) { + if (!this._editor.hasModel() || this._hasActiveDebugSession || this._hasInlineChatSession || this._showGutterIconMode() === ShowGutterIcon.Never || !this._hasProvider()) { return; } const editor = this._editor; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 388856b8007..8dd5e4ffca7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -298,6 +298,7 @@ export class ReplyResponse { localUri: URI, readonly modelAltVersionId: number, progressEdits: TextEdit[][], + readonly requestId: string, @ITextFileService private readonly _textFileService: ITextFileService, @ILanguageService private readonly _languageService: ILanguageService, ) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index a87cfef068b..986178c283a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -54,7 +54,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { assertType } from 'vs/base/common/types'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { MenuId } from 'vs/platform/actions/common/actions'; @@ -62,8 +62,14 @@ import { editorForeground, inputBackground, editorBackground } from 'vs/platform import { CodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { Lazy } from 'vs/base/common/lazy'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IChatReplyFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -137,6 +143,18 @@ export interface IInlineChatWidgetConstructionOptions { feedbackMenuId: MenuId; } +export interface IInlineChatMessage { + message: IMarkdownString; + requestId: string; + providerId: string; +} + +export interface IInlineChatMessageAppender { + appendContent(fragment: string): void; + cancel(): void; + complete(): void; +} + export class InlineChatWidget { private static _modelPool: number = 1; @@ -157,10 +175,14 @@ export class InlineChatWidget { h('div.previewDiff.hidden@previewDiff'), h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), h('div.previewCreate.hidden@previewCreate'), + h('div.chatMessage.hidden@chatMessage', [ + h('div.chatMessageContent@chatMessageContent') + ]), h('div.markdownMessage.hidden@markdownMessage', [ h('div.message@message'), h('div.messageActions@messageActions') ]), + h('div.followUps.hidden@followUps'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), h('div.actions.hidden@statusToolbar'), @@ -212,6 +234,8 @@ export class InlineChatWidget { private readonly _markdownRenderer: MarkdownRenderer; private readonly _editorOptions: ChatEditorOptions; private _codeBlockDisposables = this._store.add(new DisposableStore()); + private _chatMessageDisposables = this._store.add(new DisposableStore()); + private _followUpDisposables = this._store.add(new DisposableStore()); constructor( private readonly parentEditor: ICodeEditor, @@ -226,7 +250,9 @@ export class InlineChatWidget { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @ILogService private readonly _logService: ILogService, @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { // input editor logic @@ -395,8 +421,14 @@ export class InlineChatWidget { this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); + this._elements.chatMessageContent.tabIndex = 0; + this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.message.tabIndex = 0; this._elements.message.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._elements.followUps.tabIndex = 0; + this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + + this._elements.statusLabel.tabIndex = 0; const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions); this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); @@ -481,10 +513,12 @@ export class InlineChatWidget { const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; const markdownMessageHeight = getTotalHeight(this._elements.markdownMessage); + const followUpsHeight = getTotalHeight(this._elements.followUps); + const chatResponseHeight = getTotalHeight(this._elements.chatMessage); const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; - return base + editorHeight + markdownMessageHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + 18 /* padding */ + 8 /*shadow*/; + return base + editorHeight + markdownMessageHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + 18 /* padding */ + 8 /*shadow*/; } updateProgress(show: boolean) { @@ -550,6 +584,52 @@ export class InlineChatWidget { return this._elements.markdownMessage.textContent ?? undefined; } + updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; + updateChatMessage(message: IInlineChatMessage | undefined): void; + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { + this._chatMessageDisposables.clear(); + this._elements.chatMessage.classList.toggle('hidden', !message); + reset(this._elements.chatMessageContent); + if (message) { + const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService)); + const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, message.requestId, !isIncomplete, false, undefined)); + const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); + const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; + const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; + const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate)); + renderer.layout(this._elements.chatMessageContent.clientWidth - 4); // 2 for the padding used for the tab index border + this._chatMessageDisposables.add(this._onDidChangeLayout.event(() => { + renderer.layout(this._elements.chatMessageContent.clientWidth - 4); + })); + const template = renderer.renderTemplate(this._elements.chatMessageContent); + this._chatMessageDisposables.add(template.elementDisposables); + this._chatMessageDisposables.add(template.templateDisposables); + renderer.renderChatTreeItem(viewModel, 0, template); + this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire())); + this._onDidChangeHeight.fire(); + return { + cancel: () => responseModel.cancel(), + complete: () => responseModel.complete(), + appendContent: (fragment: string) => responseModel.updateContent({ kind: 'markdownContent', content: new MarkdownString(fragment) }), + }; + } else { + this._onDidChangeHeight.fire(); + return undefined; + } + } + + updateFollowUps(items: IChatReplyFollowup[], onFollowup: (followup: IChatReplyFollowup) => void): void; + updateFollowUps(items: undefined): void; + updateFollowUps(items: IChatReplyFollowup[] | undefined, onFollowup?: ((followup: IChatReplyFollowup) => void)) { + this._followUpDisposables.clear(); + this._elements.followUps.classList.toggle('hidden', !items || items.length === 0); + reset(this._elements.followUps); + if (items && items.length > 0 && onFollowup) { + this._followUpDisposables.add(new ChatFollowups(this._elements.followUps, items, undefined, onFollowup, this._contextKeyService)); + } + this._onDidChangeHeight.fire(); + } + updateMarkdownMessage(message: IMarkdownString | undefined) { this._codeBlockDisposables.clear(); const hasMessage = message?.value; @@ -656,6 +736,8 @@ export class InlineChatWidget { this.value = ''; this.updateMarkdownMessage(undefined); + this.updateChatMessage(undefined); + this.updateFollowUps(undefined); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 72a8ba05785..65fd54cec1f 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -153,6 +153,7 @@ export const CTX_INLINE_CHAT_LAST_FEEDBACK = new RawContextKey<'unhelpful' | 'he export const CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING = new RawContextKey('inlineChatSupportIssueReporting', false, localize('inlineChatSupportIssueReporting', "Whether the interactive editor supports issue reporting")); export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.editMode', EditMode.Live); +export const CTX_INLINE_CHAT_TOOLBAR_ICON_ENABLED = new RawContextKey('inlineChatToolbarIconEnabled', false, localize('inlineChatToolbarIconEnabled', "Whether the toolbar icon spawning inline chat is enabled.")); // --- (select) action identifier @@ -235,6 +236,11 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('showGutterIcon.mouseover', "Show the gutter icon when the mouse is over the icon."), localize('showGutterIcon.never', "Never show the gutter icon."), ] + }, + 'inlineChat.showToolbarIcon': { + description: localize('showToolbarIcon', "Controls whether the toolbar icon spawning the inline chat is enabled."), + default: true, + type: 'boolean' } } }); diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index 4c74c8e7484..4fddc9ac6b0 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -15,6 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { ICompositeControl } from 'vs/workbench/common/composite'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; @@ -38,6 +39,10 @@ export class MultiDiffEditor extends EditorPane { parent, this.instantiationService.createInstance(WorkbenchUIElementFactory), )); + + this._register(this._multiDiffEditorWidget.onDidChangeActiveControl(() => { + this._onDidChangeControl.fire(); + })); } override async setInput(input: MultiDiffEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -51,11 +56,15 @@ export class MultiDiffEditor extends EditorPane { override async clearInput(): Promise { await super.clearInput(); - this._multiDiffEditorWidget?.setViewModel(undefined); + this._multiDiffEditorWidget!.setViewModel(undefined); } layout(dimension: DOM.Dimension): void { - this._multiDiffEditorWidget?.layout(dimension); + this._multiDiffEditorWidget!.layout(dimension); + } + + override getControl(): ICompositeControl | undefined { + return this._multiDiffEditorWidget!.getActiveControl(); } } diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index 29e3eaebf53..d97b3424f0c 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -8,7 +8,7 @@ import { deepClone } from 'vs/base/common/objects'; import { isObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { ConstLazyPromise, IDocumentDiffItem, IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/model'; -import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl'; +import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; @@ -16,8 +16,9 @@ import { localize } from 'vs/nls'; import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { ILanguageSupport } from 'vs/workbench/services/textfile/common/textfiles'; -export class MultiDiffEditorInput extends EditorInput { +export class MultiDiffEditorInput extends EditorInput implements ILanguageSupport { static readonly ID: string = 'workbench.input.multiDiffEditor'; get resource(): URI | undefined { @@ -54,6 +55,15 @@ export class MultiDiffEditorInput extends EditorInput { super(); } + setLanguageId(languageId: string, source?: string | undefined): void { + const activeDiffItem = this.viewModel?.activeDiffItem.get(); + const value = activeDiffItem?.entry?.value; + if (!value) { return; } + const target = value.modified ?? value.original; + if (!target) { return; } + target.setLanguage(languageId, source); + } + // TODO this should return the view model async getModel(): Promise { if (!this._model) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables.ts new file mode 100644 index 00000000000..be6c2da14e1 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IViewContainersRegistry, IViewsRegistry } from 'vs/workbench/common/views'; +import { VIEWLET_ID as debugContainerId } from 'vs/workbench/contrib/debug/common/debug'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { NotebookVariablesView } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView'; +import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { variablesViewIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + + +export class NotebookVariables extends Disposable implements IWorkbenchContribution { + private listener: IDisposable | undefined; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + this.listener = this.editorService.onDidEditorsChange(() => { + if (configurationService.getValue('notebook.experimental.notebookVariablesView') + && this.editorService.activeEditorPane?.getId() === 'workbench.editor.notebook') { + if (this.initializeView()) { + this.listener?.dispose(); + } + } + }); + } + + private initializeView() { + const debugViewContainer = Registry.as('workbench.registry.view.containers').get(debugContainerId); + + if (debugViewContainer) { + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + const viewDescriptor = { + id: 'NOTEBOOK_VARIABLES', name: nls.localize2('notebookVariables', "Notebook Variables"), containerIcon: variablesViewIcon, ctorDescriptor: new SyncDescriptor(NotebookVariablesView), + order: 50, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.notEquals(NOTEBOOK_KERNEL.key, ''), + }; + + viewsRegistry.registerViews([viewDescriptor], debugViewContainer); + return true; + } + + return false; + } + +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts new file mode 100644 index 00000000000..1fbda38b993 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { localize } from 'vs/nls'; +import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; + +export interface INotebookVariableElement { + readonly id: string; + readonly label: string; + readonly value: string; +} + +export class NotebookVariablesTree extends WorkbenchObjectTree { } + +export class NotebookVariablesDelegate implements IListVirtualDelegate { + + getHeight(element: INotebookVariableElement): number { + return 22; + } + + getTemplateId(element: INotebookVariableElement): string { + return NotebookVariableRenderer.ID; + } +} + +export class NotebookVariableRenderer implements ITreeRenderer { + + static readonly ID = 'variableElement'; + + get templateId(): string { + return NotebookVariableRenderer.ID; + } + + renderTemplate(container: HTMLElement) { + const wrapper = dom.append(container, dom.$('.variable')); + return { wrapper }; + } + + renderElement(element: ITreeNode, index: number, templateData: { wrapper: HTMLElement }, height: number | undefined): void { + templateData.wrapper.innerText = `${element.element.label} - ${element.element.value}`; + } + + disposeTemplate(): void { + // noop + } +} + +export class NotebookVariableAccessibilityProvider implements IListAccessibilityProvider { + + getWidgetAriaLabel(): string { + return localize('debugConsole', "Notebook Variables"); + } + + getAriaLabel(element: INotebookVariableElement): string { + return localize('notebookVariableAriaLabel', "Variable {0}, value {1}", element.label, element.value); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts new file mode 100644 index 00000000000..eba684c4c9f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IObjectTreeElement } from 'vs/base/browser/ui/tree/tree'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { WorkbenchObjectTree } 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 { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { NotebookVariableAccessibilityProvider, NotebookVariableRenderer, INotebookVariableElement, NotebookVariablesDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class NotebookVariablesView extends ViewPane { + + static readonly ID = 'notebookVariablesView'; + static readonly TITLE: ILocalizedString = nls.localize2('notebook.notebookVariables', "Notebook Variables"); + + private tree: WorkbenchObjectTree | undefined; + private activeNotebook: NotebookTextModel | undefined; + + constructor( + options: IViewPaneOptions, + @IEditorService private readonly editorService: IEditorService, + @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService openerService: IOpenerService, + @IQuickInputService protected quickInputService: IQuickInputService, + @ICommandService protected commandService: ICommandService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + + this._register(this.editorService.onDidActiveEditorChange(this.handleActiveEditorChange.bind(this))); + this._register(this.notebookExecutionStateService.onDidChangeExecution(this.handleExecutionStateChange.bind(this))); + this._register(this.notebookKernelService.onDidNotebookVariablesUpdate(this.handleVariablesChanged.bind(this))); + } + + protected override renderBody(container: HTMLElement): void { + super.renderBody(container); + + this.tree = >this.instantiationService.createInstance( + WorkbenchObjectTree, + 'notebookVariablesTree', + container, + new NotebookVariablesDelegate(), + [new NotebookVariableRenderer()], + { + identityProvider: { getId: (e: INotebookVariableElement) => e.id }, + horizontalScrolling: false, + supportDynamicHeights: true, + hideTwistiesOfChildlessElements: true, + accessibilityProvider: new NotebookVariableAccessibilityProvider(), + setRowLineHeight: false, + }); + + this.tree.layout(); + this.tree?.setChildren(null, []); + } + + protected override layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this.tree?.layout(height, width); + } + + private handleActiveEditorChange() { + const activeEditorPane = this.editorService.activeEditorPane; + if (activeEditorPane && activeEditorPane.getId() === 'workbench.editor.notebook') { + const notebookDocument = getNotebookEditorFromEditorPane(activeEditorPane)?.getViewModel()?.notebookDocument; + if (notebookDocument && notebookDocument !== this.activeNotebook) { + this.activeNotebook = notebookDocument; + this.updateVariables(this.activeNotebook); + } + } + } + + private handleExecutionStateChange(event: ICellExecutionStateChangedEvent | IExecutionStateChangedEvent) { + if (this.activeNotebook) { + // changed === undefined -> excecution ended + if (event.changed === undefined && event.affectsNotebook(this.activeNotebook?.uri)) { + this.updateVariables(this.activeNotebook); + } + } + } + + private handleVariablesChanged(notebookUri: URI) { + if (this.activeNotebook && notebookUri.toString() === this.activeNotebook.uri.toString()) { + this.updateVariables(this.activeNotebook); + } + } + + private async updateVariables(notebook: NotebookTextModel) { + const selectedKernel = this.notebookKernelService.getMatchingKernel(notebook).selected; + if (selectedKernel && selectedKernel.hasVariableProvider) { + + const variables = selectedKernel.provideVariables(notebook.uri, undefined, 'named', 0, CancellationToken.None); + const treeData = await variables + .map(variable => { return this.createTreeItem(variable); }) + .toPromise(); + + this.tree?.setChildren(null, treeData); + } + } + + private index = 0; + + private createTreeItem(variable: VariablesResult): IObjectTreeElement { + let collapsed: boolean | undefined = undefined; + let children: IObjectTreeElement[] | undefined = undefined; + if (variable.namedChildrenCount > 0 || variable.indexedChildrenCount > 0) { + collapsed = true; + children = [ + { + element: { + id: `${this.index + 1}-placeholder`, + label: ' ', + value: 'loading...', + } + } + ]; + } + + const element = { + element: { + id: `${this.index++}`, + label: variable.variable.name, + value: variable.variable.value, + }, + children, + collapsed + }; + + + + return element; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index e891cdce5ab..5c0081e507d 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -6,7 +6,7 @@ .monaco-workbench .notebookOverlay .cell-chat-part { display: none; color: inherit; - padding: 6px; + padding: 0 6px; border-radius: 6px; border: 1px solid var(--vscode-inlineChat-border); background: var(--vscode-inlineChat-background); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 9689de4040b..701abbd1384 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -119,6 +119,8 @@ import { runAccessibilityHelpAction, showAccessibleOutput } from 'vs/workbench/c import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { NotebookVariables } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables'; + /*--------------------------------------------------------------------------------------------- */ Registry.as(EditorExtensions.EditorPane).registerEditorPane( @@ -718,6 +720,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(NotebookLanguageSel workbenchContributionsRegistry.registerWorkbenchContribution(SimpleNotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibilityHelpContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibleViewContribution, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookVariables, LifecyclePhase.Eventually); registerSingleton(INotebookService, NotebookService, InstantiationType.Delayed); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl, InstantiationType.Delayed); @@ -1047,6 +1050,11 @@ configurationRegistry.registerConfiguration({ nls.localize('notebook.scrolling.anchorToFocusedCell.off.description', "The focused cell may shift around as cells resize.") ], default: 'auto' + }, + [NotebookSetting.cellChat]: { + markdownDescription: nls.localize('notebook.cellChat', "Enable experimental cell chat for notebooks."), + type: 'boolean', + default: false } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 9ff69e99572..60d7b1a9902 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -863,7 +863,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD // chat styleSheets.push(` .monaco-workbench .notebookOverlay .cell-chat-part { - margin: 0 ${cellRightMargin}px 8px 6px; + margin: 0 ${cellRightMargin}px 6px 6px; } `); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 69c932935eb..20ffe3867e8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -36,3 +36,4 @@ export const copyIcon = registerIcon('notebook-copy', Codicon.copy, localize('co export const previousChangeIcon = registerIcon('notebook-diff-editor-previous-change', Codicon.arrowUp, localize('previousChangeIcon', 'Icon for the previous change action in the diff editor.')); export const nextChangeIcon = registerIcon('notebook-diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); +export const variablesViewIcon = registerIcon('variables-view-icon', Codicon.debugAlt, localize('variablesViewIcon', 'View icon of the variables view.')); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts index adc3249aab0..d86b1b7f928 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts @@ -106,6 +106,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel private readonly _onDidRemoveKernel = this._register(new Emitter()); private readonly _onDidChangeNotebookAffinity = this._register(new Emitter()); private readonly _onDidChangeSourceActions = this._register(new Emitter()); + private readonly _onDidNotebookVariablesChange = this._register(new Emitter()); private readonly _kernelSources = new Map(); private readonly _kernelSourceActionsUpdates = new Map(); private readonly _kernelDetectionTasks = new Map(); @@ -118,6 +119,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel readonly onDidChangeNotebookAffinity: Event = this._onDidChangeNotebookAffinity.event; readonly onDidChangeSourceActions: Event = this._onDidChangeSourceActions.event; readonly onDidChangeKernelDetectionTasks: Event = this._onDidChangeKernelDetectionTasks.event; + readonly onDidNotebookVariablesUpdate: Event = this._onDidNotebookVariablesChange.event; private static _storageNotebookBinding = 'notebook.controller2NotebookBindings'; @@ -201,6 +203,10 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } } + notifyVariablesChange(notebookUri: URI): void { + this._onDidNotebookVariablesChange.fire(notebookUri); + } + registerKernel(kernel: INotebookKernel): IDisposable { if (this._kernels.has(kernel.id)) { throw new Error(`NOTEBOOK CONTROLLER with id '${kernel.id}' already exists`); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index 385d2cbaa39..e62b28ee4dc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -78,7 +78,7 @@ export class CellExecutionPart extends CellContentPart { DOM.hide(this._executionOrderLabel); } else { DOM.show(this._executionOrderLabel); - const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight + element.layoutInfo.chatHeight; + const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight; this._executionOrderLabel.style.top = `${top}px`; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts index 95333b95f4c..7549fdeaab8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -80,7 +80,6 @@ export class CellFocusIndicator extends CellContentPart { this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop}px)`; this.left.setHeight(indicatorPostion.verticalIndicatorHeight); this.right.setHeight(indicatorPostion.verticalIndicatorHeight); - this.codeFocusIndicator.setTop(element.layoutInfo.chatHeight); this.codeFocusIndicator.setHeight(indicatorPostion.verticalIndicatorHeight - this.getIndicatorTopMargin() * 2 - element.layoutInfo.chatHeight); } else { const cell = element as CodeCellViewModel; @@ -89,9 +88,7 @@ export class CellFocusIndicator extends CellContentPart { const indicatorHeight = cell.layoutInfo.codeIndicatorHeight + cell.layoutInfo.outputIndicatorHeight + cell.layoutInfo.commentHeight; this.left.setHeight(indicatorHeight); this.right.setHeight(indicatorHeight); - this.codeFocusIndicator.setTop(cell.layoutInfo.chatHeight); this.codeFocusIndicator.setHeight(cell.layoutInfo.codeIndicatorHeight); - this.outputFocusIndicator.setTop(cell.layoutInfo.chatHeight); this.outputFocusIndicator.setHeight(Math.max(cell.layoutInfo.outputIndicatorHeight - cell.viewContext.notebookOptions.getLayoutConfiguration().focusIndicatorGap, 0)); this.bottom.domNode.style.transform = `translateY(${cell.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap - layoutInfo.cellBottomMargin}px)`; } @@ -100,8 +97,9 @@ export class CellFocusIndicator extends CellContentPart { } private updateFocusIndicatorsForTitleMenu(): void { - this.left.domNode.style.transform = `translateY(${this.getIndicatorTopMargin()}px)`; - this.right.domNode.style.transform = `translateY(${this.getIndicatorTopMargin()}px)`; + const y = (this.currentCell?.layoutInfo.chatHeight ?? 0) + this.getIndicatorTopMargin(); + this.left.domNode.style.transform = `translateY(${y}px)`; + this.right.domNode.style.transform = `translateY(${y}px)`; } private getIndicatorTopMargin() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts index 683141c5275..b53f0279e13 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts @@ -15,7 +15,7 @@ import { INotebookCellActionContext, NotebookAction, NotebookCellAction } from ' import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS, MENU_CELL_CHAT_WIDGET_TOOLBAR, NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; registerAction2(class extends NotebookCellAction { @@ -148,9 +148,14 @@ registerAction2(class extends NotebookCellAction { constructor() { super( { - id: 'OFF.inlineChat.discard', + id: 'notebook.cell.chat.discard', title: localize('discard', 'Discard'), icon: Codicon.discard, + keybinding: { + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED), + weight: KeybindingWeight.EditorContrib, + primary: KeyCode.Escape + }, menu: { id: MENU_CELL_CHAT_WIDGET_STATUS, group: 'main', diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index e0426c4c663..20a55a63bcf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -8,7 +8,7 @@ import { CancelablePromise, Queue, createCancelablePromise, raceCancellationErro import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; @@ -34,6 +34,7 @@ import { ProgressingEditsOptions, asProgressiveEdit, performAsyncTextEdit } from import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { CTX_INLINE_CHAT_VISIBLE, EditMode, IInlineChatProgressItem, IInlineChatRequest } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused")); export const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request")); @@ -65,6 +66,7 @@ export class NotebookCellChatController extends Disposable { private _toolbar: MenuWorkbenchToolBar | undefined; private readonly _ctxVisible: IContextKey; private readonly _ctxCellWidgetFocused: IContextKey; + private _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); constructor( private readonly _notebookEditor: INotebookEditorDelegate, private readonly _chatPart: ICellChatPart, @@ -74,6 +76,7 @@ export class NotebookCellChatController extends Disposable { @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { super(); @@ -90,20 +93,8 @@ export class NotebookCellChatController extends Disposable { return; } - if (!this._widget) { - this._widget = this._instantiationService.createInstance(InlineChatWidget, editor, { - menuId: MENU_CELL_CHAT_WIDGET, - statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK - }); - - this._partContainer.appendChild(this._widget.domNode); - this._partContainer.appendChild(this._toolbarDOM.editorToolbar); - - this._toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarDOM.editorToolbar, MENU_CELL_CHAT_WIDGET_TOOLBAR, { - telemetrySource: 'interactiveEditorWidget-toolbar', - toolbarOptions: { primaryGroup: 'main' } - })); + if (!this._widget && this._isVisible) { + this._initialize(editor); } const inlineChatController = InlineChatController.get(editor); @@ -115,6 +106,41 @@ export class NotebookCellChatController extends Disposable { })); } + private _initialize(editor: IActiveCodeEditor) { + this._widget = this._instantiationService.createInstance(InlineChatWidget, editor, { + menuId: MENU_CELL_CHAT_WIDGET, + statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + }); + + this._widgetDisposableStore.add(this._widget.onDidChangeHeight(() => { + this._updateHeight(); + })); + + this._widgetDisposableStore.add(this._notebookExecutionStateService.onDidChangeExecution(e => { + if (e.notebook.toString() !== this._notebookEditor.textModel?.uri.toString()) { + return; + } + + if (e.type === NotebookExecutionType.cell && e.affectsCell(this._cell.uri) && e.changed === undefined /** complete */) { + // check if execution is successfull + const { lastRunSuccess } = this._cell.internalMetadata; + if (lastRunSuccess) { + this._strategy?.createSnapshot(); + } + } + })); + + + this._partContainer.appendChild(this._widget.domNode); + this._partContainer.appendChild(this._toolbarDOM.editorToolbar); + + this._toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarDOM.editorToolbar, MENU_CELL_CHAT_WIDGET_TOOLBAR, { + telemetrySource: 'interactiveEditorWidget-toolbar', + toolbarOptions: { primaryGroup: 'main' } + })); + } + public override dispose(): void { if (this._isVisible) { // detach the chat widget @@ -123,12 +149,17 @@ export class NotebookCellChatController extends Disposable { this._sessionCtor = undefined; } - if (this._widget) { - this._partContainer.removeChild(this._widget.domNode); + try { + if (this._widget) { + this._partContainer.removeChild(this._widget.domNode); + this._partContainer.removeChild(this._toolbarDOM.editorToolbar); + } + + } catch (_ex) { + // might not be attached } - this._partContainer.removeChild(this._toolbarDOM.editorToolbar); - + this._widget?.dispose(); this._inlineChatListener?.dispose(); this._toolbar?.dispose(); this._inlineChatListener = undefined; @@ -141,20 +172,40 @@ export class NotebookCellChatController extends Disposable { layout() { if (this._isVisible && this._widget) { const innerEditorWidth = this._cell.layoutInfo.editorWidth; - const height = 82 + 8 * 2 /* vertical margin*/; - + const height = this._widget.getHeight(); this._widget.layout(new Dimension(innerEditorWidth, height)); } } + private _updateHeight() { + const margin = 6; + const heightWithMargin = this._isVisible && this._widget + ? (this._widget.getHeight() - 8 /** shadow */ - 18 /** padding */ + margin /** bottom margin for the cell part */) + : 0; + + if (this._cell.chatHeight === heightWithMargin) { + return; + } + + this._cell.chatHeight = heightWithMargin; + this._partContainer.style.height = `${heightWithMargin - margin}px`; + } + async show() { this._isVisible = true; + if (!this._widget) { + const editor = this._getCellEditor(); + if (editor) { + this._initialize(editor); + } + } + this._partContainer.style.display = 'flex'; this._widget?.focus(); this._widget?.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); this._ctxVisible.set(true); this._ctxCellWidgetFocused.set(true); - this._cell.chatHeight = 82 + 8 * 2 /* vertical margin*/; + this._updateHeight(); this._sessionCtor = createCancelablePromise(async token => { if (this._cell.editorAttached) { @@ -292,7 +343,7 @@ export class NotebookCellChatController extends Disposable { } const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const replyResponse = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits); + const replyResponse = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId); for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { await this._makeChanges(editor, replyResponse.allLocalEdits[i], undefined); } @@ -336,7 +387,7 @@ export class NotebookCellChatController extends Disposable { this._ctxCellWidgetFocused.set(false); this._ctxVisible.set(false); this._widget?.reset(); - this._cell.chatHeight = 0; + this._updateHeight(); } private async _makeChanges(editor: IActiveCodeEditor, edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { @@ -434,4 +485,10 @@ class EditStrategy { modelN.undo(); } } + + createSnapshot(): void { + if (this._session && !this._session.textModel0.equalsTextBuffer(this._session.textModelN.getTextBuffer())) { + this._session.createSnapshot(); + } + } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts index 7bd5d43e88a..461dc85c664 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts @@ -9,6 +9,8 @@ import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cell import { NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; import 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class CellChatPart extends CellContentPart { private _controller: NotebookCellChatController | undefined; @@ -21,13 +23,17 @@ export class CellChatPart extends CellContentPart { private readonly _notebookEditor: INotebookEditorDelegate, private readonly _partContainer: HTMLElement, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); } override didRenderCell(element: ICellViewModel): void { this._controller?.dispose(); - this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element, this._partContainer); + const enabled = this._configurationService.getValue(NotebookSetting.cellChat); + if (enabled) { + this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element, this._partContainer); + } super.didRenderCell(element); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index c2151962a4d..4c559a6d359 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as osPath from 'vs/base/common/path'; +import { getWindow } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { coalesce } from 'vs/base/common/arrays'; import { DeferredPromise, runWhenGlobalIdle } from 'vs/base/common/async'; import { decodeBase64 } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { getExtensionForMimeType } from 'vs/base/common/mime'; -import { FileAccess, Schemas } from 'vs/base/common/network'; +import { FileAccess, Schemas, matchesScheme, matchesSomeScheme } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; +import * as osPath from 'vs/base/common/path'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { dirname, extname, isEqual, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -26,13 +29,16 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; -import { IOpenerService, matchesScheme, matchesSomeScheme } from 'vs/platform/opener/common/opener'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorFindMatch, editorFindMatchHighlight } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CellEditState, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookEditorCreationOptions, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; @@ -40,6 +46,7 @@ import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellUri, ICellOutput, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement, IWebviewService, WebviewContentPurpose, WebviewOriginStore } from 'vs/workbench/contrib/webview/browser/webview'; @@ -47,15 +54,8 @@ import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/w import { asWebviewUri, webviewGenericCspSource } from 'vs/workbench/contrib/webview/common/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, RendererMetadata, StaticPreloadMetadata, ToWebviewMessage } from './webviewMessages'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; -import { getWindow } from 'vs/base/browser/dom'; +import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, RendererMetadata, StaticPreloadMetadata, ToWebviewMessage } from './webviewMessages'; const LINE_COLUMN_REGEX = /:([\d]+)(?::([\d]+))?$/; const LineQueryRegex = /line=(\d+)$/; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index dc9d1dd3bb0..93ae763c6e9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -290,6 +290,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } else { const codeIndicatorHeight = notebookLayoutConfiguration.collapsedIndicatorHeight; const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight; + const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight; const outputContainerOffset = notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight; const totalHeight = @@ -297,6 +298,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + notebookLayoutConfiguration.collapsedIndicatorHeight + notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN + bottomToolbarDimensions.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + + chatHeight + commentHeight + outputTotalHeight + outputShowMoreContainerHeight; const outputShowMoreContainerOffset = totalHeight @@ -312,7 +314,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null, editorHeight: this._layoutInfo.editorHeight, editorWidth, - chatHeight: 0, + chatHeight: chatHeight, statusBarHeight: 0, commentHeight, outputContainerOffset, diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index f2dacb01c0e..6b1e5ca8c9b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IAction } from 'vs/base/common/actions'; +import { AsyncIterableObject } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -33,6 +35,18 @@ export interface INotebookKernelChangeEvent { supportedLanguages?: true; hasExecutionOrder?: true; hasInterruptHandler?: true; + hasVariableProvider?: true; +} + +export interface Variable { + name: string; + value: string; +} + +export interface VariablesResult { + variable: Variable; + namedChildrenCount: number; + indexedChildrenCount: number; } export interface INotebookKernel { @@ -51,9 +65,12 @@ export interface INotebookKernel { supportedLanguages: string[]; implementsInterrupt?: boolean; implementsExecutionOrder?: boolean; + hasVariableProvider?: boolean; executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise; cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; + + provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject; } export const enum ProxyKernelState { @@ -100,6 +117,7 @@ export interface INotebookKernelService { readonly onDidRemoveKernel: Event; readonly onDidChangeSelectedNotebooks: Event; readonly onDidChangeNotebookAffinity: Event; + readonly onDidNotebookVariablesUpdate: Event; registerKernel(kernel: INotebookKernel): IDisposable; getMatchingKernel(notebook: INotebookTextModelLike): INotebookKernelMatchResult; @@ -138,6 +156,8 @@ export interface INotebookKernelService { registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable; getKernelSourceActions2(notebook: INotebookTextModelLike): Promise; //#endregion + + notifyVariablesChange(notebookUri: URI): void; } export const INotebookKernelHistoryService = createDecorator('INotebookKernelHistoryService'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index 76853078119..a471c3c1ed7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -5,6 +5,8 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; +import { AsyncIterableObject } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -23,7 +25,7 @@ import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelService, INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelService, INotebookTextModelLike, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -172,6 +174,9 @@ class TestNotebookKernel implements INotebookKernel { preloadUris: URI[] = []; preloadProvides: string[] = []; supportedLanguages: string[] = []; + provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject { + return AsyncIterableObject.EMPTY; + } executeNotebookCellsRequest(): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 8874152bdb9..6e608d7e03a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DeferredPromise } from 'vs/base/common/async'; +import { AsyncIterableObject, DeferredPromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -22,7 +23,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CellEditType, CellKind, CellUri, IOutputDto, NotebookCellMetadata, NotebookExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType, INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -361,6 +362,9 @@ class TestNotebookKernel implements INotebookKernel { supportedLanguages: string[] = []; async executeNotebookCellsRequest(): Promise { } async cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise { } + provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject { + return AsyncIterableObject.EMPTY; + } constructor(opts?: { languages?: string[]; id?: string }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts index 24d459f9eef..084f7e5f0b0 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { mock } from 'vs/base/test/common/mock'; @@ -21,6 +21,8 @@ import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/brow import { IApplicationStorageValueChangeEvent, IProfileStorageValueChangeEvent, IStorageService, IStorageValueChangeEvent, IWillSaveStateEvent, IWorkspaceStorageValueChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { AsyncIterableObject } from 'vs/base/common/async'; suite('NotebookKernelHistoryService', () => { @@ -184,6 +186,9 @@ class TestNotebookKernel implements INotebookKernel { cancelNotebookCellExecution(): Promise { throw new Error('Method not implemented.'); } + provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject { + return AsyncIterableObject.EMPTY; + } constructor(opts?: { languages?: string[]; label?: string; viewType?: string }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index 61664a2bfa1..f82e97ae7fa 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { mock } from 'vs/base/test/common/mock'; @@ -19,6 +19,8 @@ import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry' import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { AsyncIterableObject } from 'vs/base/common/async'; suite('NotebookKernelService', () => { @@ -197,6 +199,9 @@ class TestNotebookKernel implements INotebookKernel { cancelNotebookCellExecution(): Promise { throw new Error('Method not implemented.'); } + provideVariables(notebookUri: URI, variableName: string | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject { + return AsyncIterableObject.EMPTY; + } constructor(opts?: { languages?: string[]; label?: string; viewType?: string }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index c2be5d08e38..e30c3bb549a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -866,7 +866,7 @@ export class SearchResultModel extends SettingsTreeModel { viewState: ISettingsEditorViewState, settingsOrderByTocIndex: Map | null, isWorkspaceTrusted: boolean, - @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, + @IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ILanguageService languageService: ILanguageService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @@ -884,18 +884,13 @@ export class SearchResultModel extends SettingsTreeModel { } } - const tocHiddenDuringSearch = this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide'; - if (!tocHiddenDuringSearch) { - // Sort the settings according to internal order if indexed. - if (this.settingsOrderByTocIndex) { - filterMatches.sort((a, b) => compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder)); - } - return filterMatches; + // The search only has filters, so we can sort by the order in the TOC. + if (!this._viewState.query) { + return filterMatches.sort((a, b) => compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder)); } - // The table of contents is hidden during the search. - // The settings could appear in a more haphazard order. - // Sort the settings according to their score. + // Sort the settings according to their relevancy. + // https://github.com/microsoft/vscode/issues/197773 filterMatches.sort((a, b) => { if (a.matchType !== b.matchType) { // Sort by match type if the match types are not the same. diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 22ff960b224..ad60f18f80f 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -152,8 +152,8 @@ registry.registerConfiguration({ 'type': 'string', 'enum': ['hide', 'filter'], 'enumDescriptions': [ - nls.localize('settingsSearchTocBehavior.hide', "Hide the Table of Contents while searching. The search results will not be grouped by category, and instead will be sorted by similarity to the query, with exact keyword matches coming first."), - nls.localize('settingsSearchTocBehavior.filter', "Filter the Table of Contents to just categories that have matching settings. Clicking a category will filter the results to that category. The search results will be grouped by category."), + nls.localize('settingsSearchTocBehavior.hide', "Hide the Table of Contents while searching."), + nls.localize('settingsSearchTocBehavior.filter', "Filter the Table of Contents to just categories that have matching settings. Clicking on a category will filter the results to that category."), ], 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the Settings editor Table of Contents while searching. If this setting is being changed in the Settings editor, the setting will take effect after the search query is modified."), 'default': 'filter', diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 38ff045e4d1..38f4e87b259 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -118,25 +118,12 @@ align-items: center; } -.scm-view .monaco-list-row .history-item-group .monaco-highlighted-label { - display: flex; - align-items: center; -} - .scm-view .monaco-list-row .history-item-group .monaco-icon-label, .scm-view .monaco-list-row .history-item .monaco-icon-label { flex-grow: 1; align-items: center; } -.scm-view .monaco-list-row .history-item-group .monaco-icon-label > .monaco-icon-label-container { - display: flex; -} -.scm-view .monaco-list-row .history-item-group .monaco-icon-label > .monaco-icon-label-container .monaco-icon-description-container { - overflow: hidden; - text-overflow: ellipsis; -} - .scm-sync-view .monaco-list-row .monaco-icon-label .icon-container .scm-sync-view .monaco-list-row .monaco-icon-label .icon-container { display: flex; @@ -185,6 +172,7 @@ color: var(--vscode-scm-historyItemDeletionsForeground); } +.scm-view .monaco-list-row .history-item-group .monaco-icon-label .icon-container, .scm-view .monaco-list-row .history-item .monaco-icon-label .icon-container { display: flex; font-size: 14px; diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index e53baa92f8f..ca8d16a7276 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,7 +7,7 @@ import { localize, localize2 } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; @@ -33,7 +33,6 @@ import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/con import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff'; import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService'; import { getActiveElement } from 'vs/base/browser/dom'; -import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -111,17 +110,6 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon }], viewContainer); -viewsRegistry.registerViews([{ - id: SYNC_VIEW_PANE_ID, - name: localize2('source control sync', "Source Control Sync"), - ctorDescriptor: new SyncDescriptor(SCMSyncViewPane), - canToggleVisibility: true, - canMoveView: true, - weight: 20, - order: -998, - when: ContextKeyExpr.equals('config.scm.experimental.showSyncView', true), -}], viewContainer); - Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); @@ -307,7 +295,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis localize('scm.showIncomingChanges.auto', "Only show incoming changes in the Source Control view when any exist."), ], description: localize('scm.showIncomingChanges', "Controls whether incoming changes are shown in the Source Control view."), - default: 'never' + default: 'auto' }, 'scm.showOutgoingChanges': { type: 'string', @@ -318,12 +306,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis localize('scm.showOutgoingChanges.auto', "Only show outgoing changes in the Source Control view when any exist."), ], description: localize('scm.showOutgoingChanges', "Controls whether outgoing changes are shown in the Source Control view."), - default: 'never' - }, - 'scm.experimental.showSyncView': { - type: 'boolean', - description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."), - default: false + default: 'auto' } } }); diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts deleted file mode 100644 index 4805711e193..00000000000 --- a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts +++ /dev/null @@ -1,885 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as path from 'vs/base/common/path'; -import { append, $, prepend } from 'vs/base/browser/dom'; -import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; -import { DisposableStore, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { URI } from 'vs/base/common/uri'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; -import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPane'; -import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util'; -import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; -import { comparePaths } from 'vs/base/common/comparers'; -import { localize } from 'vs/nls'; -import { Iterable } from 'vs/base/common/iterator'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { basename, dirname } from 'vs/base/common/resources'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { stripIcons } from 'vs/base/common/iconLabels'; -import { FileKind } from 'vs/platform/files/common/files'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history'; - -type SCMHistoryItemChangeResourceTreeNode = IResourceNode; -type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement | SCMHistoryItemChangeResourceTreeNode; - -function isSCMHistoryItemGroupTreeElement(obj: any): obj is SCMHistoryItemGroupTreeElement { - return (obj as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup'; -} - -function isSCMHistoryItemTreeElement(obj: any): obj is SCMHistoryItemTreeElement { - return (obj as SCMHistoryItemTreeElement).type === 'historyItem'; -} - -function isSCMHistoryItemChangeTreeElement(obj: any): obj is SCMHistoryItemChangeTreeElement { - return (obj as SCMHistoryItemChangeTreeElement).type === 'historyItemChange'; -} - -function toDiffEditorArguments(uri: URI, originalUri: URI, modifiedUri: URI): unknown[] { - const basename = path.basename(uri.fsPath); - const originalQuery = JSON.parse(originalUri.query) as { path: string; ref: string }; - const modifiedQuery = JSON.parse(modifiedUri.query) as { path: string; ref: string }; - - const originalShortRef = originalQuery.ref.substring(0, 8).concat(originalQuery.ref.endsWith('^') ? '^' : ''); - const modifiedShortRef = modifiedQuery.ref.substring(0, 8).concat(modifiedQuery.ref.endsWith('^') ? '^' : ''); - - return [originalUri, modifiedUri, `${basename} (${originalShortRef}) ↔ ${basename} (${modifiedShortRef})`, null]; -} - -function getSCMResourceId(element: TreeElement): string { - if (isSCMRepository(element)) { - const provider = element.provider; - return `repo:${provider.id}`; - } else if (isSCMActionButton(element)) { - const provider = element.repository.provider; - return `actionButton:${provider.id}`; - } else if (isSCMHistoryItemGroupTreeElement(element)) { - const provider = element.repository.provider; - return `historyItemGroup:${provider.id}/${element.id}`; - } else if (isSCMHistoryItemTreeElement(element)) { - const historyItemGroup = element.historyItemGroup; - const provider = historyItemGroup.repository.provider; - return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}`; - } else if (isSCMHistoryItemChangeTreeElement(element)) { - const historyItem = element.historyItem; - const historyItemGroup = historyItem.historyItemGroup; - const provider = historyItemGroup.repository.provider; - return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; - } else if (ResourceTree.isResourceNode(element)) { - const historyItem = element.context; - const historyItemGroup = historyItem.historyItemGroup; - const provider = historyItemGroup.repository.provider; - return element.childrenCount === 0 && element.element ? - `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.element.uri.toString()}` : - `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; - } else { - throw new Error('Invalid tree element'); - } -} - -const enum ViewMode { - List = 'list', - Tree = 'tree' -} - -const ContextKeys = { - ViewMode: new RawContextKey('scmSyncViewMode', ViewMode.List), -}; - -class ListDelegate implements IListVirtualDelegate { - - getHeight(element: any): number { - if (isSCMActionButton(element)) { - return ActionButtonRenderer.DEFAULT_HEIGHT + 10; - } else { - return 22; - } - } - - getTemplateId(element: any): string { - if (isSCMRepository(element)) { - return RepositoryRenderer.TEMPLATE_ID; - } else if (isSCMActionButton(element)) { - return ActionButtonRenderer.TEMPLATE_ID; - } else if (isSCMHistoryItemGroupTreeElement(element)) { - return HistoryItemGroupRenderer.TEMPLATE_ID; - } else if (isSCMHistoryItemTreeElement(element)) { - return HistoryItemRenderer.TEMPLATE_ID; - } else if (isSCMHistoryItemChangeTreeElement(element)) { - return HistoryItemChangeRenderer.TEMPLATE_ID; - } else if (ResourceTree.isResourceNode(element)) { - return HistoryItemChangeRenderer.TEMPLATE_ID; - } else { - throw new Error('Invalid tree element'); - } - } -} - -class CompressionDelegate implements ITreeCompressionDelegate { - - isIncompressible(element: TreeElement): boolean { - if (ResourceTree.isResourceNode(element)) { - return element.childrenCount === 0 || !element.parent || !element.parent.parent; - } - - return true; - } - -} - -interface HistoryItemGroupTemplate { - readonly label: IconLabel; - readonly count: CountBadge; - readonly disposables: IDisposable; -} - -class HistoryItemGroupRenderer implements ITreeRenderer { - - static readonly TEMPLATE_ID = 'history-item-group'; - get templateId(): string { return HistoryItemGroupRenderer.TEMPLATE_ID; } - - renderTemplate(container: HTMLElement) { - // hack - (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie'); - - const element = append(container, $('.history-item-group')); - const label = new IconLabel(element, { supportIcons: true }); - const countContainer = append(element, $('.count')); - const count = new CountBadge(countContainer, {}, defaultCountBadgeStyles); - - return { label, count, disposables: new DisposableStore() }; - } - - renderElement(node: ITreeNode, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void { - const historyItemGroup = node.element; - templateData.label.setLabel(historyItemGroup.label, historyItemGroup.description); - templateData.count.setCount(historyItemGroup.count ?? 0); - } - - disposeTemplate(templateData: HistoryItemGroupTemplate): void { - templateData.disposables.dispose(); - } -} - -interface HistoryItemTemplate { - readonly iconContainer: HTMLElement; - // readonly avatarImg: HTMLImageElement; - readonly iconLabel: IconLabel; - // readonly timestampContainer: HTMLElement; - // readonly timestamp: HTMLSpanElement; - readonly disposables: IDisposable; -} - -class HistoryItemRenderer implements ITreeRenderer { - - static readonly TEMPLATE_ID = 'history-item'; - get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; } - - renderTemplate(container: HTMLElement): HistoryItemTemplate { - // hack - (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie'); - - const element = append(container, $('.history-item')); - const iconLabel = new IconLabel(element, { supportIcons: true }); - - const iconContainer = prepend(iconLabel.element, $('.icon-container')); - // const avatarImg = append(iconContainer, $('img.avatar')) as HTMLImageElement; - - // const timestampContainer = append(iconLabel.element, $('.timestamp-container')); - // const timestamp = append(timestampContainer, $('span.timestamp')); - - return { iconContainer, iconLabel, disposables: new DisposableStore() }; - } - - renderElement(node: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { - const historyItem = node.element; - - templateData.iconContainer.className = 'icon-container'; - if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { - templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); - } - - // if (commit.authorAvatar) { - // templateData.avatarImg.src = commit.authorAvatar; - // templateData.avatarImg.style.display = 'block'; - // templateData.iconContainer.classList.remove(...ThemeIcon.asClassNameArray(Codicon.account)); - // } else { - // templateData.avatarImg.style.display = 'none'; - // templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(Codicon.account)); - // } - - templateData.iconLabel.setLabel(historyItem.label, historyItem.description); - - // templateData.timestampContainer.classList.toggle('timestamp-duplicate', commit.hideTimestamp === true); - // templateData.timestamp.textContent = fromNow(commit.timestamp); - } - - disposeTemplate(templateData: HistoryItemTemplate): void { - templateData.disposables.dispose(); - } -} - -interface HistoryItemChangeTemplate { - readonly element: HTMLElement; - readonly name: HTMLElement; - readonly fileLabel: IResourceLabel; - readonly decorationIcon: HTMLElement; - readonly disposables: IDisposable; -} - -class HistoryItemChangeRenderer implements ICompressibleTreeRenderer { - - static readonly TEMPLATE_ID = 'historyItemChange'; - get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; } - - constructor( - private readonly viewMode: () => ViewMode, - private readonly labels: ResourceLabels, - @ILabelService private labelService: ILabelService) { } - - renderTemplate(container: HTMLElement): HistoryItemChangeTemplate { - const element = append(container, $('.change')); - const name = append(element, $('.name')); - const fileLabel = this.labels.create(name, { supportDescriptionHighlights: true, supportHighlights: true }); - const decorationIcon = append(element, $('.decoration-icon')); - - return { element, name, fileLabel, decorationIcon, disposables: new DisposableStore() }; - } - - renderElement(node: ITreeNode, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { - const historyItemChangeOrFolder = node.element; - const uri = ResourceTree.isResourceNode(historyItemChangeOrFolder) ? historyItemChangeOrFolder.element?.uri ?? historyItemChangeOrFolder.uri : historyItemChangeOrFolder.uri; - const fileKind = ResourceTree.isResourceNode(historyItemChangeOrFolder) && historyItemChangeOrFolder.childrenCount > 0 ? FileKind.FOLDER : FileKind.FILE; - - templateData.fileLabel.setFile(uri, { - fileDecorations: { colors: false, badges: true }, - fileKind, - hidePath: this.viewMode() === ViewMode.Tree, - }); - } - - renderCompressedElements(node: ITreeNode, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { - const compressed = node.element as ICompressedTreeNode; - - const folder = compressed.elements[compressed.elements.length - 1]; - const label = compressed.elements.map(e => e.name); - - templateData.fileLabel.setResource({ resource: folder.uri, name: label }, { - fileDecorations: { colors: false, badges: true }, - fileKind: FileKind.FOLDER, - separator: this.labelService.getSeparator(folder.uri.scheme) - }); - } - - disposeTemplate(templateData: HistoryItemChangeTemplate): void { - templateData.disposables.dispose(); - } -} - -class SCMSyncViewPaneAccessibilityProvider implements IListAccessibilityProvider { - - constructor( - @ILabelService private readonly labelService: ILabelService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService - ) { } - - getAriaLabel(element: TreeElement): string { - if (isSCMRepository(element)) { - let folderName = ''; - if (element.provider.rootUri) { - const folder = this.workspaceContextService.getWorkspaceFolder(element.provider.rootUri); - - if (folder?.uri.toString() === element.provider.rootUri.toString()) { - folderName = folder.name; - } else { - folderName = basename(element.provider.rootUri); - } - } - return `${folderName} ${element.provider.label}`; - } else if (isSCMHistoryItemGroupTreeElement(element)) { - return `${stripIcons(element.label).trim()}${element.description ? `, ${element.description}` : ''}`; - } else if (isSCMActionButton(element)) { - return element.button?.command.title ?? ''; - } else if (isSCMHistoryItemTreeElement(element)) { - return `${stripIcons(element.label).trim()}${element.description ? `, ${element.description}` : ''}`; - } else if (isSCMHistoryItemChangeTreeElement(element)) { - const result = [basename(element.uri)]; - const path = this.labelService.getUriLabel(dirname(element.uri), { relative: true, noPrefix: true }); - - if (path) { - result.push(path); - } - - return result.join(', '); - } - - return ''; - } - getWidgetAriaLabel(): string { - return localize('scmSync', 'Source Control Sync'); - } - -} - -class SCMSyncViewPaneTreeIdentityProvider implements IIdentityProvider { - - getId(element: TreeElement): string { - return getSCMResourceId(element); - } - -} - -class SCMSyncViewPaneTreeSorter implements ITreeSorter { - - compare(element: TreeElement, otherElement: TreeElement): number { - // Repository - if (isSCMRepository(element)) { - if (!isSCMRepository(otherElement)) { - throw new Error('Invalid comparison'); - } - - return 0; - } - - // Action button - if (isSCMActionButton(element)) { - return -1; - } else if (isSCMActionButton(otherElement)) { - return 1; - } - - // History item group - if (isSCMHistoryItemGroupTreeElement(element)) { - if (!isSCMHistoryItemGroupTreeElement(otherElement)) { - throw new Error('Invalid comparison'); - } - - return 0; - } - - // History item - if (isSCMHistoryItemTreeElement(element)) { - if (!isSCMHistoryItemTreeElement(otherElement)) { - throw new Error('Invalid comparison'); - } - - return 0; - } - - // History item change - const elementPath = (element as SCMHistoryItemChangeTreeElement).uri.fsPath; - const otherElementPath = (otherElement as SCMHistoryItemChangeTreeElement).uri.fsPath; - - return comparePaths(elementPath, otherElementPath); - } -} - -export class SCMSyncViewPane extends ViewPane { - - private listLabels!: ResourceLabels; - private treeContainer!: HTMLElement; - private _tree!: WorkbenchCompressibleAsyncDataTree; - - private _viewModel!: SCMSyncPaneViewModel; - get viewModel(): SCMSyncPaneViewModel { return this._viewModel; } - - private readonly disposables = new DisposableStore(); - - constructor( - options: IViewPaneOptions, - @ICommandService private commandService: ICommandService, - @IKeybindingService keybindingService: IKeybindingService, - @IContextMenuService contextMenuService: IContextMenuService, - @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService, - @IOpenerService openerService: IOpenerService, - @IThemeService themeService: IThemeService, - @ITelemetryService telemetryService: ITelemetryService - ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - } - - protected override renderBody(container: HTMLElement): void { - super.renderBody(container); - - this.treeContainer = append(container, $('.scm-view.scm-sync-view')); - - this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); - this._register(this.listLabels); - - const updateProviderCountVisibility = () => { - const value = this.configurationService.getValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge'); - this.treeContainer.classList.toggle('hide-provider-counts', value === 'hidden'); - this.treeContainer.classList.toggle('auto-provider-counts', value === 'auto'); - }; - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); - updateProviderCountVisibility(); - - this._tree = this.instantiationService.createInstance( - WorkbenchCompressibleAsyncDataTree, - 'SCM Sync View', - this.treeContainer, - new ListDelegate(), - new CompressionDelegate(), - [ - this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), - this.instantiationService.createInstance(ActionButtonRenderer), - this.instantiationService.createInstance(HistoryItemGroupRenderer), - this.instantiationService.createInstance(HistoryItemRenderer), - this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewModel.mode, this.listLabels), - ], - this.instantiationService.createInstance(SCMSyncDataSource, () => this.viewModel.mode), - { - compressionEnabled: true, - horizontalScrolling: false, - autoExpandSingleChildren: true, - multipleSelectionSupport: false, - collapseByDefault: (e) => !ResourceTree.isResourceNode(e), - accessibilityProvider: this.instantiationService.createInstance(SCMSyncViewPaneAccessibilityProvider), - identityProvider: this.instantiationService.createInstance(SCMSyncViewPaneTreeIdentityProvider), - sorter: this.instantiationService.createInstance(SCMSyncViewPaneTreeSorter), - }) as WorkbenchCompressibleAsyncDataTree; - - this._register(this._tree); - this._register(this._tree.onDidOpen(this.onDidOpen, this)); - - this._viewModel = this.instantiationService.createInstance(SCMSyncPaneViewModel, this._tree); - - this.treeContainer.classList.add('file-icon-themable-tree'); - this.treeContainer.classList.add('show-file-icons'); - - this.updateIndentStyles(this.themeService.getFileIconTheme()); - this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); - this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - } - - protected override layoutBody(height: number, width: number): void { - super.layoutBody(height, width); - this._tree.layout(height, width); - } - - private onDidChangeMode(): void { - this.updateIndentStyles(this.themeService.getFileIconTheme()); - } - - private async onDidOpen(e: IOpenEvent): Promise { - if (!e.element) { - return; - } - - if (isSCMHistoryItemChangeTreeElement(e.element)) { - if (e.element.originalUri && e.element.modifiedUri) { - await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); - } - } else if (ResourceTree.isResourceNode(e.element) && e.element.childrenCount === 0) { - if (e.element.element?.originalUri && e.element.element?.modifiedUri) { - await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.element.uri, e.element.element.originalUri, e.element.element.modifiedUri), e); - } - } - } - - private updateIndentStyles(theme: any): void { - this.treeContainer.classList.toggle('list-view-mode', this._viewModel.mode === ViewMode.List); - this.treeContainer.classList.toggle('tree-view-mode', this._viewModel.mode === ViewMode.Tree); - this.treeContainer.classList.toggle('align-icons-and-twisties', (this._viewModel.mode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); - } - - override dispose(): void { - this.disposables.dispose(); - super.dispose(); - } -} - -class SCMSyncPaneViewModel { - - private readonly _onDidChangeMode = new Emitter(); - readonly onDidChangeMode = this._onDidChangeMode.event; - - private _mode: ViewMode; - get mode(): ViewMode { return this._mode; } - set mode(mode: ViewMode) { - if (this._mode === mode) { - return; - } - - this._mode = mode; - - this.refresh(); - this.modeContextKey.set(mode); - this._onDidChangeMode.fire(mode); - - this.storageService.store(`scm.syncViewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); - } - - private modeContextKey: IContextKey; - private repositories = new Map(); - private historyProviders = new Map(); - - private alwaysShowRepositories = false; - - private readonly disposables = new DisposableStore(); - - constructor( - private readonly tree: WorkbenchCompressibleAsyncDataTree, - @IContextKeyService contextKeyService: IContextKeyService, - @ISCMViewService scmViewService: ISCMViewService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IStorageService private readonly storageService: IStorageService, - ) { - configurationService.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this.disposables); - this._onDidChangeConfiguration(); - - scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.disposables); - this._onDidChangeVisibleRepositories({ added: scmViewService.visibleRepositories, removed: [] }); - - this._mode = this.getViewMode(); - - this.modeContextKey = ContextKeys.ViewMode.bindTo(contextKeyService); - this.modeContextKey.set(this._mode); - } - - private _onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { - if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) { - this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); - this.refresh(); - } - } - - private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - for (const repository of added) { - const repositoryDisposable = repository.provider.onDidChangeHistoryProvider(() => this._onDidChangeHistoryProvider(repository)); - this._onDidChangeHistoryProvider(repository); - - this.repositories.set(repository, repositoryDisposable); - } - - for (const repository of removed) { - this.historyProviders.get(repository)?.dispose(); - this.historyProviders.delete(repository); - - this.repositories.get(repository)?.dispose(); - this.repositories.delete(repository); - } - - this.refresh(); - } - - private _onDidChangeHistoryProvider(repository: ISCMRepository): void { - if (repository.provider.historyProvider) { - const historyProviderDisposable = combinedDisposable( - repository.provider.historyProvider.onDidChangeActionButton(() => this.refresh(repository)), - repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => this.refresh(repository))); - - this.historyProviders.set(repository, historyProviderDisposable); - } else { - this.historyProviders.get(repository)?.dispose(); - this.historyProviders.delete(repository); - } - } - - private getViewMode(): ViewMode { - let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewMode.List : ViewMode.Tree; - const storageMode = this.storageService.get(`scm.syncViewMode`, StorageScope.WORKSPACE) as ViewMode; - if (typeof storageMode === 'string') { - mode = storageMode; - } - - return mode; - } - - async refresh(repository?: ISCMRepository): Promise { - if (this.repositories.size === 0) { - return; - } - - if (repository) { - // Particular repository - await this.tree.updateChildren(repository); - } else if (this.repositories.size === 1 && !this.alwaysShowRepositories) { - // Single repository and not always show repositories - await this.tree.setInput(Iterable.first(this.repositories.keys())!); - } else { - // Expand repository nodes - const expanded = Array.from(this.repositories.keys()) - .map(repository => `repo:${repository.provider.id}`); - - // Multiple repositories or always show repositories - await this.tree.setInput([...this.repositories.keys()], { expanded }); - } - } -} - -class SCMSyncDataSource implements IAsyncDataSource { - - constructor( - private readonly viewMode: () => ViewMode, - @IUriIdentityService private uriIdentityService: IUriIdentityService) { } - - hasChildren(element: TreeElement): boolean { - if (isSCMRepositoryArray(element)) { - return true; - } else if (isSCMRepository(element)) { - return true; - } else if (isSCMActionButton(element)) { - return false; - } else if (isSCMHistoryItemGroupTreeElement(element)) { - return true; - } else if (isSCMHistoryItemTreeElement(element)) { - return true; - } else if (isSCMHistoryItemChangeTreeElement(element)) { - return false; - } else if (ResourceTree.isResourceNode(element)) { - return element.childrenCount > 0; - } else { - throw new Error('hasChildren not implemented.'); - } - } - - async getChildren(element: TreeElement): Promise { - const children: TreeElement[] = []; - - if (isSCMRepositoryArray(element)) { - children.push(...element); - } else if (isSCMRepository(element)) { - const scmProvider = element.provider; - const historyProvider = scmProvider.historyProvider; - const historyItemGroup = historyProvider?.currentHistoryItemGroup; - - if (!historyProvider || !historyItemGroup) { - return []; - } - - // Action Button - const actionButton = historyProvider.actionButton; - if (actionButton) { - children.push({ - type: 'actionButton', - repository: element, - button: actionButton - } as ISCMActionButton); - } - - // History item groups - children.push(...await this.getHistoryItemGroups(element)); - } else if (isSCMHistoryItemGroupTreeElement(element)) { - children.push(...await this.getHistoryItems(element)); - } else if (isSCMHistoryItemTreeElement(element)) { - children.push(...await this.getHistoryItemChanges(element)); - } else if (ResourceTree.isResourceNode(element)) { - children.push(...element.children); - } else { - throw new Error('getChildren Method not implemented.'); - } - - return children; - } - - private async getHistoryItemGroups(element: ISCMRepository): Promise { - const scmProvider = element.provider; - const historyProvider = scmProvider.historyProvider; - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - - if (!historyProvider || !currentHistoryItemGroup) { - return []; - } - - // History item group base - const historyItemGroupBase = await historyProvider.resolveHistoryItemGroupBase(currentHistoryItemGroup.id); - if (!historyItemGroupBase) { - return []; - } - - // Common ancestor, ahead, behind - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, historyItemGroupBase.id); - - const children: SCMHistoryItemGroupTreeElement[] = []; - // Incoming - if (historyItemGroupBase) { - children.push({ - id: historyItemGroupBase.id, - label: `$(cloud-download) ${historyItemGroupBase.label}`, - description: localize('incoming', "Incoming Changes"), - ancestor: ancestor?.id, - count: ancestor?.behind ?? 0, - repository: element, - type: 'historyItemGroup' - } as SCMHistoryItemGroupTreeElement); - } - - // Outgoing - children.push({ - id: currentHistoryItemGroup.id, - label: `$(cloud-upload) ${currentHistoryItemGroup.label}`, - description: localize('outgoing', "Outgoing Changes"), - ancestor: ancestor?.id, - count: ancestor?.ahead ?? 0, - repository: element, - type: 'historyItemGroup' - } as SCMHistoryItemGroupTreeElement); - - return children; - } - - private async getHistoryItems(element: SCMHistoryItemGroupTreeElement): Promise { - const scmProvider = element.repository.provider; - const historyProvider = scmProvider.historyProvider; - - if (!historyProvider) { - return []; - } - - const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? []; - return historyItems.map(historyItem => ({ - id: historyItem.id, - label: historyItem.label, - description: historyItem.description, - icon: historyItem.icon, - historyItemGroup: element, - type: 'historyItem' - } as SCMHistoryItemTreeElement)); - } - - private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | SCMHistoryItemChangeResourceTreeNode)[]> { - const repository = element.historyItemGroup.repository; - const historyProvider = repository.provider.historyProvider; - - if (!historyProvider) { - return []; - } - - // History Item Changes - const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? []; - - if (this.viewMode() === ViewMode.List) { - // List - return changes.map(change => ({ - uri: change.uri, - originalUri: change.originalUri, - modifiedUri: change.modifiedUri, - renameUri: change.renameUri, - historyItem: element, - type: 'historyItemChange' - } as SCMHistoryItemChangeTreeElement)); - } - - // Tree - const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); - for (const change of changes) { - tree.add(change.uri, { - uri: change.uri, - originalUri: change.originalUri, - modifiedUri: change.modifiedUri, - renameUri: change.renameUri, - historyItem: element, - type: 'historyItemChange' - } as SCMHistoryItemChangeTreeElement); - } - - return [...tree.root.children]; - } - -} - -class RefreshAction extends ViewAction { - constructor() { - super({ - id: 'workbench.scm.sync.action.refresh', - title: localize('refresh', "Refresh"), - viewId: SYNC_VIEW_PANE_ID, - f1: false, - icon: Codicon.refresh, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', SYNC_VIEW_PANE_ID), - group: 'navigation' - } - }); - } - - async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { - view.viewModel.refresh(); - } -} - -class SetListViewModeAction extends ViewAction { - - constructor() { - super({ - id: 'workbench.scm.sync.action.setListViewMode', - title: localize('setListViewMode', "View as List"), - viewId: SYNC_VIEW_PANE_ID, - f1: false, - icon: Codicon.listTree, - toggled: ContextKeys.ViewMode.isEqualTo(ViewMode.List), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', SYNC_VIEW_PANE_ID), - group: '1_viewmode' - } - }); - } - - async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { - view.viewModel.mode = ViewMode.List; - } -} - -class SetTreeViewModeAction extends ViewAction { - - constructor() { - super({ - id: 'workbench.scm.sync.action.setTreeViewMode', - title: localize('setTreeViewMode', "View as Tree"), - viewId: SYNC_VIEW_PANE_ID, - f1: false, - icon: Codicon.listFlat, - toggled: ContextKeys.ViewMode.isEqualTo(ViewMode.Tree), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', SYNC_VIEW_PANE_ID), - group: '1_viewmode' - } - }); - } - - async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { - view.viewModel.mode = ViewMode.Tree; - } -} - -registerAction2(RefreshAction); -registerAction2(SetListViewModeAction); -registerAction2(SetTreeViewModeAction); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 95706a2e8dd..5db197f9ab6 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -726,6 +726,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void { const historyItemGroup = node.element; + + templateData.iconContainer.className = 'icon-container'; + if (historyItemGroup.icon && ThemeIcon.isThemeIcon(historyItemGroup.icon)) { + templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItemGroup.icon)); + } + templateData.label.setLabel(historyItemGroup.label, historyItemGroup.description); templateData.count.setCount(historyItemGroup.count ?? 0); } @@ -2881,7 +2891,7 @@ export class SCMViewPane extends ViewPane { this.asyncOperationSequencer.queue(async () => { const focusedInput = this.inputRenderer.getFocusedInput(); - if (element && (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length > 1)) { + if (element && this.tree.hasNode(element)) { // Refresh specific repository await this.tree.updateChildren(element); } else { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 9e05de0285c..eb350b43286 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -61,15 +61,13 @@ export interface ISCMHistoryItemGroupDetails { export interface ISCMHistoryItemGroupEntry { readonly id: string; readonly label: string; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; readonly description?: string; readonly ancestor?: string; readonly count?: number; } -export interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { - readonly description?: string; - readonly ancestor?: string; - readonly count?: number; +export interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroupEntry { readonly repository: ISCMRepository; readonly type: 'historyItemGroup'; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 3378d796300..3c222ca9393 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -18,7 +18,6 @@ import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/cont export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; export const REPOSITORIES_VIEW_PANE_ID = 'workbench.scm.repositories'; -export const SYNC_VIEW_PANE_ID = 'workbench.scm.sync'; export interface IBaselineResourceProvider { getBaselineResource(resource: URI): Promise; diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 3d462bd7524..a934dae26c7 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -479,6 +479,23 @@ pointer-events: none; } +.terminal-command-guide { + left: 0; + border: 1.5px solid #ffffff; + width: 0px !important; + border-color: var(--vscode-notebook-inactiveFocusedCellBorder); + box-sizing: border-box; + transform: translateX(3px); +} +.terminal-command-guide.top { + border-top-left-radius: 1px; + border-top-right-radius: 1px; +} +.terminal-command-guide.bottom { + border-bottom-left-radius: 1px; + border-bottom-right-radius: 1px; +} + .terminal-scroll-highlight-outline { border-left: 1px solid #ffffff; border-right: 1px solid #ffffff; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index aaa3ee98c5d..185370a084f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -116,7 +116,8 @@ export interface IMarkTracker { scrollToLine(line: number, position: ScrollPosition): void; revealCommand(command: ITerminalCommand, position?: ScrollPosition): void; - registerTemporaryDecoration(marker: IMarker, endMarker?: IMarker): void; + registerTemporaryDecoration(marker: IMarker, endMarker: IMarker | undefined, showOutline: boolean): void; + showCommandGuide(command: ITerminalCommand | undefined): void; } export interface ITerminalGroup { @@ -653,6 +654,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { onDidInputData: Event; onDidChangeSelection: Event; onDidRunText: Event; + onDidChangeTarget: Event; /** * An event that fires when a terminal is dropped on this instance via drag and drop. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 73a905dcd57..51229ea570d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -201,7 +201,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } get target(): TerminalLocation | undefined { return this._target; } - set target(value: TerminalLocation | undefined) { this._target = value; } + set target(value: TerminalLocation | undefined) { + this._target = value; + this._onDidChangeTarget.fire(value); + } get instanceId(): number { return this._instanceId; } get resource(): URI { return this._resource; } @@ -322,6 +325,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; private readonly _onDidRunText = this._register(new Emitter()); readonly onDidRunText = this._onDidRunText.event; + private readonly _onDidChangeTarget = this._register(new Emitter()); + readonly onDidChangeTarget = this._onDidChangeTarget.event; constructor( private readonly _terminalShellTypeContextKey: IContextKey, diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts index b7ac4dd0902..7cedc2249dc 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; -import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { IMarkTracker } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import type { Terminal, IMarker, ITerminalAddon, IDecoration } from '@xterm/xterm'; @@ -31,6 +31,9 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe protected _terminal: Terminal | undefined; private _navigationDecorations: IDecoration[] | undefined; + private _activeCommandGuide?: ITerminalCommand; + private _commandGuideDecorations = this._register(new MutableDisposable()); + activate(terminal: Terminal): void { this._terminal = terminal; this._register(this._terminal.onData(() => { @@ -225,7 +228,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe this._terminal.scrollToLine(line); } if (!hideDecoration) { - this.registerTemporaryDecoration(start, end); + this.registerTemporaryDecoration(start, end, true); } } @@ -257,7 +260,56 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe ); } - registerTemporaryDecoration(marker: IMarker | number, endMarker?: IMarker | number): void { + showCommandGuide(command: ITerminalCommand | undefined): void { + if (!this._terminal) { + return; + } + if (!command) { + this._commandGuideDecorations.clear(); + this._activeCommandGuide = undefined; + return; + } + if (this._activeCommandGuide === command) { + return; + } + if (command.marker) { + this._activeCommandGuide = command; + + // Highlight output + const store = this._commandGuideDecorations.value = new DisposableStore(); + if (!command.executedMarker || !command.endMarker) { + return; + } + const startLine = command.marker.line - (command.getPromptRowCount() - 1); + const decorationCount = toLineIndex(command.endMarker) - startLine; + for (let i = 0; i < decorationCount; i++) { + const decoration = this._terminal.registerDecoration({ + marker: this._createMarkerForOffset(command.executedMarker, i) + }); + if (decoration) { + store.add(decoration); + let renderedElement: HTMLElement | undefined; + store.add(decoration.onRender(element => { + if (!renderedElement) { + renderedElement = element; + element.classList.add('terminal-command-guide'); + if (i === 0) { + element.classList.add('top'); + } + if (i === decorationCount - 1) { + element.classList.add('bottom'); + } + } + if (this._terminal?.element) { + element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`; + } + })); + } + } + } + } + + registerTemporaryDecoration(marker: IMarker | number, endMarker: IMarker | number | undefined, showOutline: boolean): void { if (!this._terminal) { return; } @@ -265,7 +317,6 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe const color = this._themeService.getColorTheme().getColor(TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR); const startLine = toLineIndex(marker); const decorationCount = endMarker ? toLineIndex(endMarker) - startLine + 1 : 1; - for (let i = 0; i < decorationCount; i++) { const decoration = this._terminal.registerDecoration({ marker: this._createMarkerForOffset(marker, i), @@ -281,7 +332,10 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe decoration.onRender(element => { if (!renderedElement) { renderedElement = element; - element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline'); + element.classList.add('terminal-scroll-highlight'); + if (showOutline) { + element.classList.add('terminal-scroll-highlight-outline'); + } if (i === 0) { element.classList.add('top'); } @@ -295,13 +349,16 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`; } }); + // TODO: This is not efficient for a large decorationCount decoration.onDispose(() => { this._navigationDecorations = this._navigationDecorations?.filter(d => d !== decoration); }); // Number picked to align with symbol highlight in the editor - timeout(350).then(() => { - if (renderedElement) { - renderedElement.classList.remove('terminal-scroll-highlight-outline'); - } - }); + if (showOutline) { + timeout(350).then(() => { + if (renderedElement) { + renderedElement.classList.remove('terminal-scroll-highlight-outline'); + } + }); + } } } } diff --git a/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index 563bf065317..e8ecb40a47c 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -17,6 +17,7 @@ import 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.acce import 'vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution'; import 'vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution'; import 'vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution'; +import 'vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution'; import 'vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution'; import 'vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminal.stickyScroll.contribution'; import 'vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution'; diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index aa656c1a431..a9765ea9bd3 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -63,6 +63,7 @@ suite('CommandDetectionCapability', () => { async function printCommandStart(prompt: string) { capability.handlePromptStart(); await writeP(xterm, `\r${prompt}`); + capability.handleCommandStart(); } diff --git a/src/vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution.ts b/src/vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution.ts new file mode 100644 index 00000000000..a0708b038a0 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { addDisposableListener } from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ITerminalProcessInfo, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; + + +class TerminalHighlightContribution extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.highlight'; + + static get(instance: ITerminalInstance | IDetachedTerminalInstance): TerminalHighlightContribution | null { + return instance.getContribution(TerminalHighlightContribution.ID); + } + + constructor( + private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, + processManager: ITerminalProcessManager | ITerminalProcessInfo, + widgetManager: TerminalWidgetManager, + ) { + super(); + } + + xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + const screenElement = xterm.raw.element!.querySelector('.xterm-screen')!; + this._register(addDisposableListener(screenElement, 'mousemove', (e: MouseEvent) => { + if ((e.target as any).tagName !== 'CANVAS') { + return; + } + const rect = xterm.raw.element?.getBoundingClientRect(); + if (!rect) { + return; + } + const mouseCursorY = Math.floor(e.offsetY / (rect.height / xterm.raw.rows)); + const command = this._instance.capabilities.get(TerminalCapability.CommandDetection)?.getCommandForLine(xterm.raw.buffer.active.viewportY + mouseCursorY); + if (command && 'getOutput' in command) { + xterm.markTracker.showCommandGuide(command); + } else { + xterm.markTracker.showCommandGuide(undefined); + } + })); + this._register(addDisposableListener(screenElement, 'mouseout', () => xterm.markTracker.showCommandGuide(undefined))); + this._register(xterm.raw.onData(() => xterm.markTracker.showCommandGuide(undefined))); + } +} + +registerTerminalContribution(TerminalHighlightContribution.ID, TerminalHighlightContribution, false); diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 3285042999a..1675d0fac73 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { matchesScheme } from 'vs/platform/opener/common/opener'; +import { matchesScheme } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalSimpleLink, ITerminalLinkDetector, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/links/browser/links'; diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css index 8c65bf940a6..182471ad813 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css @@ -12,6 +12,9 @@ background: var(--vscode-terminalStickyScroll-background, var(--vscode-terminal-background, var(--vscode-panel-background))); box-shadow: var(--vscode-scrollbar-shadow) 0 3px 2px -2px; } +.editor-instance .terminal-sticky-scroll { + background: var(--vscode-terminalStickyScroll-background, var(--vscode-terminal-background, var(--vscode-editor-background))); +} .terminal-sticky-scroll.visible { display:block; @@ -39,23 +42,3 @@ .terminal-sticky-scroll .hover-overlay:hover { background-color: var(--vscode-terminalStickyScrollHover-background, transparent); } - -.terminal-sticky-scroll .hover-overlay .keybinding-hint { - display: none; - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - z-index: 100; - opacity: 0.5; - pointer-events: none; - display: flex; -} -.terminal-sticky-scroll .hover-overlay .keybinding-hint > div { - /* Preventing wrapping of keybinding and its localized string */ - display: inline-block; -} -.terminal-sticky-scroll .hover-overlay:hover .keybinding-hint { - display: flex; - align-items: center; -} diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index a54e52081e0..7a9fa1aafda 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -85,6 +85,9 @@ export class TerminalStickyScrollOverlay extends Disposable { } })); + // React to terminal location changes + this._register(this._instance.onDidChangeTarget(() => this._syncOptions())); + // Eagerly create the overlay xtermCtor.then(ctor => { this._stickyScrollOverlay = this._register(new ctor({ diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 88f98b07a07..28036f14af7 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -279,15 +279,6 @@ export class TimelinePane extends ViewPane { this.followActiveEditorContext = TimelineFollowActiveEditorContext.bindTo(this.contextKeyService); this.timelineExcludeSourcesContext = TimelineExcludeSources.bindTo(this.contextKeyService); - // TOOD @lramos15 remove after a few iterations of deprecated setting - const oldExcludedSourcesSetting: string[] = configurationService.getValue('timeline.excludeSources'); - if (oldExcludedSourcesSetting) { - configurationService.updateValue('timeline.excludeSources', undefined); - const oldSettingString = JSON.stringify(oldExcludedSourcesSetting); - this.timelineExcludeSourcesContext.set(oldSettingString); - // Update the storage service with the setting - storageService.store('timeline.excludeSources', oldSettingString, StorageScope.PROFILE, StorageTarget.USER); - } const excludedSourcesString = storageService.get('timeline.excludeSources', StorageScope.PROFILE, '[]'); this.timelineExcludeSourcesContext.set(excludedSourcesString); this.excludedSources = new Set(JSON.parse(excludedSourcesString)); diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index 20cb4e67a50..57fc3951b07 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -3,28 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Schemas } from 'vs/base/common/network'; +import { WindowIdleValue } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { Schemas, matchesScheme } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IOpenerService, matchesScheme, OpenOptions } from 'vs/platform/opener/common/opener'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService, OpenOptions } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { configureOpenerTrustedDomainsHandler, readAuthenticationTrustedDomains, readStaticTrustedDomains, readWorkspaceTrustedDomains } from 'vs/workbench/contrib/url/browser/trustedDomains'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { testUrlMatchesGlob } from 'vs/workbench/contrib/url/common/urlGlob'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { mainWindow } from 'vs/base/browser/window'; -import { WindowIdleValue } from 'vs/base/browser/dom'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class OpenerValidatorContributions implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 5fd3578b39f..04ddd988865 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -3,77 +3,77 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors'; -import 'vs/css!./media/gettingStarted'; -import { localize } from 'vs/nls'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorSerializer, IEditorOpenContext } from 'vs/workbench/common/editor'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { assertIsDefined } from 'vs/base/common/types'; -import { $, addDisposableListener, append, clearNode, Dimension, getWindow, reset } from 'vs/base/browser/dom'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { hiddenEntriesConfigurationKey, IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { firstSessionDateStorageKey, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { $, Dimension, addDisposableListener, append, clearNode, getWindow, reset } from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { gettingStartedCheckedCodicon, gettingStartedUncheckedCodicon } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons'; -import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; -import { URI } from 'vs/base/common/uri'; -import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { coalesce, equals, flatten } from 'vs/base/common/arrays'; +import { Delayer, Throttler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { splitRecentLabel } from 'vs/base/common/labels'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ILink, LinkedText } from 'vs/base/common/linkedText'; +import { parse } from 'vs/base/common/marshalling'; +import { Schemas, matchesScheme } from 'vs/base/common/network'; +import { isMacintosh } from 'vs/base/common/platform'; +import { IFeaturedExtension } from 'vs/base/common/product'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { assertIsDefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import 'vs/css!./media/gettingStarted'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; +import { localize } from 'vs/nls'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IRecentFolder, IRecentlyOpened, IRecentWorkspace, isRecentFolder, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/workspace/common/workspace'; -import { ILabelService, Verbosity } from 'vs/platform/label/common/label'; -import { IWindowOpenable } from 'vs/platform/window/common/window'; -import { splitRecentLabel } from 'vs/base/common/labels'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { isMacintosh } from 'vs/base/common/platform'; -import { Delayer, Throttler } from 'vs/base/common/async'; -import { GettingStartedInput } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput'; -import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ILink, LinkedText } from 'vs/base/common/linkedText'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { Link } from 'vs/platform/opener/browser/link'; -import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { IWebviewElement, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IFileService } from 'vs/platform/files/common/files'; -import { parse } from 'vs/base/common/marshalling'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Schemas } from 'vs/base/common/network'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { coalesce, equals, flatten } from 'vs/base/common/arrays'; -import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { startEntries } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; -import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { GettingStartedIndexList } from './gettingStartedList'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils'; -import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; -import { OpenFolderAction, OpenFileFolderAction, OpenFolderViaWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { OpenRecentAction } from 'vs/workbench/browser/actions/windowActions'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; -import { Codicon } from 'vs/base/common/codicons'; -import { restoreWalkthroughsConfigurationKey, RestoreWalkthroughsConfigurationValue } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; -import { GettingStartedDetailsRenderer } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { defaultButtonStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { IFeaturedExtensionsService } from 'vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService'; -import { IFeaturedExtension } from 'vs/base/common/product'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILabelService, Verbosity } from 'vs/platform/label/common/label'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Link } from 'vs/platform/opener/browser/link'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryService, TelemetryLevel, firstSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils'; +import { defaultButtonStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWindowOpenable } from 'vs/platform/window/common/window'; +import { IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/workspace/common/workspace'; +import { IRecentFolder, IRecentWorkspace, IRecentlyOpened, IWorkspacesService, isRecentFolder, isRecentWorkspace } from 'vs/platform/workspaces/common/workspaces'; +import { OpenRecentAction } from 'vs/workbench/browser/actions/windowActions'; +import { OpenFileFolderAction, OpenFolderAction, OpenFolderViaWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; +import { IEditorOpenContext, IEditorSerializer } from 'vs/workbench/common/editor'; +import { IWebviewElement, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { IFeaturedExtensionsService } from 'vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService'; +import 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors'; +import { GettingStartedDetailsRenderer } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer'; +import { gettingStartedCheckedCodicon, gettingStartedUncheckedCodicon } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons'; +import { GettingStartedInput } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput'; +import { IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService, hiddenEntriesConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; +import { RestoreWalkthroughsConfigurationValue, restoreWalkthroughsConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; +import { startEntries } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; +import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { GettingStartedIndexList } from './gettingStartedList'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 1874fbe5728..8f441972a6d 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -361,9 +361,9 @@ export interface IEditorGroupsContainer { mergeGroup(group: IEditorGroup | GroupIdentifier, target: IEditorGroup | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroup; /** - * Merge all editor groups into the active one. + * Merge all editor groups into the target one. */ - mergeAllGroups(): IEditorGroup; + mergeAllGroups(target: IEditorGroup | GroupIdentifier): IEditorGroup; /** * Copy a group to a new group in the container. diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 071e78a9f0e..b95fe353242 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -431,7 +431,7 @@ suite('EditorGroupsService', () => { assert.strictEqual(rootGroup.count, 1); - const result = part.mergeAllGroups(); + const result = part.mergeAllGroups(part.activeGroup); assert.strictEqual(result.id, rootGroup.id); assert.strictEqual(rootGroup.count, 3); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index ff8c53636fd..b6d4f0f3831 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -309,6 +309,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost telemetryInfo: { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, + sqmId: this._telemetryService.sqmId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index 87b031a1c56..b145bc80f6c 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -40,6 +40,7 @@ export interface IExtensionHostInitData { telemetryInfo: { readonly sessionId: string; readonly machineId: string; + readonly sqmId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; }; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 770411e3e93..e29607525e3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -69,6 +69,7 @@ export const allApiProposals = Object.freeze({ notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', + notebookVariableProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', quickDiffProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 9a60251454b..69184c22b2e 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -243,6 +243,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { telemetryInfo: { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, + sqmId: this._telemetryService.sqmId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index b4ae92453ae..2360b276ddc 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -496,6 +496,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { telemetryInfo: { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, + sqmId: this._telemetryService.sqmId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index ac45112d126..3b7937e3afc 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -28,6 +28,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } + get sqmId(): string { return this.impl.sqmId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index 84207e13089..f87e5a9f950 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -27,6 +27,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } + get sqmId(): string { return this.impl.sqmId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } diff --git a/src/vs/workbench/services/url/browser/urlService.ts b/src/vs/workbench/services/url/browser/urlService.ts index 602d6d32c6b..5c1b1872096 100644 --- a/src/vs/workbench/services/url/browser/urlService.ts +++ b/src/vs/workbench/services/url/browser/urlService.ts @@ -9,7 +9,8 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { AbstractURLService } from 'vs/platform/url/common/urlService'; import { Event } from 'vs/base/common/event'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; -import { IOpenerService, IOpener, OpenExternalOptions, OpenInternalOptions, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IOpenerService, IOpener, OpenExternalOptions, OpenInternalOptions } from 'vs/platform/opener/common/opener'; +import { matchesScheme } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; export interface IURLCallbackProvider { diff --git a/src/vs/workbench/services/url/electron-sandbox/urlService.ts b/src/vs/workbench/services/url/electron-sandbox/urlService.ts index 6e3662033e6..f8ec96c4998 100644 --- a/src/vs/workbench/services/url/electron-sandbox/urlService.ts +++ b/src/vs/workbench/services/url/electron-sandbox/urlService.ts @@ -7,7 +7,8 @@ import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/commo import { URI, UriComponents } from 'vs/base/common/uri'; import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { URLHandlerChannel } from 'vs/platform/url/common/urlIpc'; -import { IOpenerService, IOpener, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IOpenerService, IOpener } from 'vs/platform/opener/common/opener'; +import { matchesScheme } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 170d5d05d98..14d46d46f4c 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -870,7 +870,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { removeGroup(_group: number | IEditorGroup): void { } moveGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); } mergeGroup(_group: number | IEditorGroup, _target: number | IEditorGroup, _options?: IMergeGroupOptions): IEditorGroup { throw new Error('not implemented'); } - mergeAllGroups(): IEditorGroup { throw new Error('not implemented'); } + mergeAllGroups(_group: number | IEditorGroup): IEditorGroup { throw new Error('not implemented'); } copyGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); } centerLayout(active: boolean): void { } isLayoutCentered(): boolean { return false; } diff --git a/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts b/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts index 1005628fe0c..c0e889d41e0 100644 --- a/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts @@ -15,6 +15,13 @@ declare module 'vscode' { supportIssueReporting?: boolean; } + export interface ChatAgentErrorDetails { + /** + * If set to true, the message content is completely hidden. Only ChatAgentErrorDetails#message will be shown. + */ + responseIsRedacted?: boolean; + } + /** * This is temporary until inline references are fully supported and adopted */ @@ -27,6 +34,16 @@ declare module 'vscode' { command?: ChatAgentSlashCommand; } + export interface ChatAgentVulnerability { + title: string; + description: string; + // id: string; // Later we will need to be able to link these across multiple content chunks. + } + + export interface ChatAgentContent { + vulnerability?: ChatAgentVulnerability; + } + export type ChatAgentExtendedProgress = ChatAgentProgress | ChatAgentMarkdownContent | ChatAgentDetectedAgent; diff --git a/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts b/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts new file mode 100644 index 00000000000..0071af59ff4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'vscode' { + + export interface NotebookController { + /** Set this to attach a variable provider to this controller. */ + variableProvider?: NotebookVariableProvider; + } + + export enum NotebookVariablesRequestKind { + Named = 1, + Indexed = 2 + } + + interface VariablesResult { + variable: Variable; + namedChildrenCount: number; + indexedChildrenCount: number; + } + + interface NotebookVariableProvider { + onDidChangeVariables: Event; + + /** When parent is undefined, this is requesting global Variables. When a variable is passed, it's requesting child props of that Variable. */ + provideVariables(notebook: NotebookDocument, parent: Variable | undefined, kind: NotebookVariablesRequestKind, start: number, token: CancellationToken): AsyncIterable; + } + + interface Variable { + /** The variable's name. */ + name: string; + + /** The variable's value. + This can be a multi-line text, e.g. for a function the body of a function. + For structured variables (which do not have a simple value), it is recommended to provide a one-line representation of the structured object. + This helps to identify the structured object in the collapsed state when its children are not yet visible. + An empty string can be used if no value should be shown in the UI. + */ + value: string; + } + +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index ef9e04f39bf..cd964d010ca 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -193,7 +193,7 @@ if (!opts.web) { } if (!fs.existsSync(electronPath || '')) { - fail(`Can't find VSCode at ${electronPath}. Please run VSCode once first (scripts/code.sh, scripts\\code.bat) and try again.`); + fail(`Cannot find VSCode at ${electronPath}. Please run VSCode once first (scripts/code.sh, scripts\\code.bat) and try again.`); } quality = parseQuality(); @@ -213,7 +213,7 @@ else { if (typeof testCodeServerPath === 'string') { if (!fs.existsSync(testCodeServerPath)) { - fail(`Can't find Code server at ${testCodeServerPath}.`); + fail(`Cannot find Code server at ${testCodeServerPath}.`); } else { logger.log(`Running web smoke tests against ${testCodeServerPath}`); } @@ -326,7 +326,7 @@ async function ensureStableCode(): Promise { } if (!fs.existsSync(stableCodePath)) { - throw new Error(`Can't find Stable VSCode at ${stableCodePath}.`); + throw new Error(`Cannot find Stable VSCode at ${stableCodePath}.`); } logger.log(`Using stable build ${stableCodePath} for migration tests`);