diff --git a/.vscode/settings.json b/.vscode/settings.json index 6925e3ed8c6..0069fee3d12 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -151,4 +151,12 @@ "application.experimental.rendererProfiling": true, "editor.experimental.asyncTokenization": true, "editor.experimental.asyncTokenizationVerification": true, + "typescript.preferences.autoImportFileExcludePatterns": [ + "xterm", + "xterm-headless", + "node-pty", + "vscode-notebook-renderer", + "src/vs/workbench/workbench.web.main.ts", + "src/vs/workbench/api/common/extHostTypes.ts" + ] } diff --git a/build/azure-pipelines/common/createAsset.js b/build/azure-pipelines/common/createAsset.js index e4ca63b6573..89f675ebcec 100644 --- a/build/azure-pipelines/common/createAsset.js +++ b/build/azure-pipelines/common/createAsset.js @@ -166,32 +166,38 @@ async function main() { } }; const uploadPromises = []; - if (await blobClient.exists()) { - uploadPromises.push(Promise.reject(new Error(`Blob ${quality}, ${blobName} already exists, not publishing again.`))); - } - else { - uploadPromises.push((0, retry_1.retry)(async (attempt) => { - console.log(`Uploading blobs to Azure storage (attempt ${attempt})...`); - await blobClient.uploadFile(filePath, blobOptions); - console.log('Blob successfully uploaded to Azure storage.'); - })); - } + uploadPromises.push((async () => { + console.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)(async (attempt) => { + console.log(`Uploading blobs to Azure storage (attempt ${attempt})...`); + await blobClient.uploadFile(filePath, blobOptions); + console.log('Blob successfully uploaded to Azure storage.'); + }); + } + })()); const shouldUploadToMooncake = /true/i.test(process.env['VSCODE_PUBLISH_TO_MOONCAKE'] ?? 'true'); if (shouldUploadToMooncake) { const mooncakeCredential = new identity_1.ClientSecretCredential(process.env['AZURE_MOONCAKE_TENANT_ID'], process.env['AZURE_MOONCAKE_CLIENT_ID'], process.env['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); - if (await mooncakeBlobClient.exists()) { - uploadPromises.push(Promise.reject(new Error(`Mooncake Blob ${quality}, ${blobName} already exists, not publishing again.`))); - } - else { - uploadPromises.push((0, retry_1.retry)(async (attempt) => { - console.log(`Uploading blobs to Mooncake Azure storage (attempt ${attempt})...`); - await mooncakeBlobClient.uploadFile(filePath, blobOptions); - console.log('Blob successfully uploaded to Mooncake Azure storage.'); - })); - } + uploadPromises.push((async () => { + console.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)(async (attempt) => { + console.log(`Uploading blobs to Mooncake Azure storage (attempt ${attempt})...`); + await mooncakeBlobClient.uploadFile(filePath, blobOptions); + console.log('Blob successfully uploaded to Mooncake Azure storage.'); + }); + } + })()); } const promiseResults = await Promise.allSettled(uploadPromises); const rejectedPromiseResults = promiseResults.filter(result => result.status === 'rejected'); @@ -235,4 +241,4 @@ main().then(() => { console.error(err); process.exit(1); }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlQXNzZXQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjcmVhdGVBc3NldC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7O0FBRWhHLHlCQUF5QjtBQUV6QixpQ0FBaUM7QUFDakMsc0RBQXdJO0FBQ3hJLDZCQUE2QjtBQUM3QiwwQ0FBNkM7QUFDN0MsOENBQXlEO0FBQ3pELG1DQUFnQztBQWFoQyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRTtJQUM5QixPQUFPLENBQUMsS0FBSyxDQUFDLDJEQUEyRCxDQUFDLENBQUM7SUFDM0UsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0NBQ2pCO0FBRUQsd0ZBQXdGO0FBQ3hGLFNBQVMsV0FBVyxDQUFDLE9BQWUsRUFBRSxFQUFVLEVBQUUsSUFBWSxFQUFFLElBQVk7SUFDM0UsUUFBUSxFQUFFLEVBQUU7UUFDWCxLQUFLLE9BQU87WUFDWCxRQUFRLE9BQU8sRUFBRTtnQkFDaEIsS0FBSyxRQUFRLENBQUMsQ0FBQztvQkFDZCxNQUFNLEtBQUssR0FBRyxJQUFJLEtBQUssTUFBTSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsSUFBSSxFQUFFLENBQUM7b0JBQzFELFFBQVEsSUFBSSxFQUFFO3dCQUNiLEtBQUssU0FBUzs0QkFDYixPQUFPLEdBQUcsS0FBSyxVQUFVLENBQUM7d0JBQzNCLEtBQUssT0FBTzs0QkFDWCxPQUFPLEtBQUssQ0FBQzt3QkFDZCxLQUFLLFlBQVk7NEJBQ2hCLE9BQU8sR0FBRyxLQUFLLE9BQU8sQ0FBQzt3QkFDeEI7NEJBQ0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztxQkFDbkU7aUJBQ0Q7Z0JBQ0QsS0FBSyxRQUFRO29CQUNaLElBQUksSUFBSSxLQUFLLE9BQU8s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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlQXNzZXQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjcmVhdGVBc3NldC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7O0FBRWhHLHlCQUF5QjtBQUV6QixpQ0FBaUM7QUFDakMsc0RBQXdJO0FBQ3hJLDZCQUE2QjtBQUM3QiwwQ0FBNkM7QUFDN0MsOENBQXlEO0FBQ3pELG1DQUFnQztBQWFoQyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRTtJQUM5QixPQUFPLENBQUMsS0FBSyxDQUFDLDJEQUEyRCxDQUFDLENBQUM7SUFDM0UsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0NBQ2pCO0FBRUQsd0ZBQXdGO0FBQ3hGLFNBQVMsV0FBVyxDQUFDLE9BQWUsRUFBRSxFQUFVLEVBQUUsSUFBWSxFQUFFLElBQVk7SUFDM0UsUUFBUSxFQUFFLEVBQUU7UUFDWCxLQUFLLE9BQU87WUFDWCxRQUFRLE9BQU8sRUFBRTtnQkFDaEIsS0FBSyxRQUFRLENBQUMsQ0FBQztvQkFDZCxNQUFNLEtBQUssR0FBRyxJQUFJLEtBQUssTUFBTSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsSUFBSSxFQUFFLENBQUM7b0JBQzFELFFBQVEsSUFBSSxFQUFFO3dCQUNiLEtBQUssU0FBUzs0QkFDYixPQUFPLEdBQUcsS0FBSyxVQUFVLENBQUM7d0JBQzNCLEtBQUssT0FBTzs0QkFDWCxPQUFPLEtBQUssQ0FBQzt3QkFDZCxLQUFLLFlBQVk7NEJBQ2hCLE9BQU8sR0FBRyxLQUFLLE9BQU8sQ0FBQzt3QkFDeEI7NEJBQ0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsT0FBTyxJQUFJLEVBQUUsSUFBSSxJQUFJLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQztxQkFDbkU7aUJBQ0Q7Z0JBQ0QsS0FBSyxRQUFRO29CQUNaLElBQUksSUFBSSxLQUFLLE9BQU8s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 \ No newline at end of file diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index 74c76407a78..97c90ab40e2 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -197,15 +197,19 @@ async function main(): Promise { const uploadPromises: Promise[] = []; - if (await blobClient.exists()) { - uploadPromises.push(Promise.reject(new Error(`Blob ${quality}, ${blobName} already exists, not publishing again.`))); - } else { - uploadPromises.push(retry(async (attempt) => { - console.log(`Uploading blobs to Azure storage (attempt ${attempt})...`); - await blobClient.uploadFile(filePath, blobOptions); - console.log('Blob successfully uploaded to Azure storage.'); - })); - } + uploadPromises.push((async () => { + console.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(async (attempt) => { + console.log(`Uploading blobs to Azure storage (attempt ${attempt})...`); + await blobClient.uploadFile(filePath, blobOptions); + console.log('Blob successfully uploaded to Azure storage.'); + }); + } + })()); const shouldUploadToMooncake = /true/i.test(process.env['VSCODE_PUBLISH_TO_MOONCAKE'] ?? 'true'); @@ -215,15 +219,19 @@ async function main(): Promise { const mooncakeContainerClient = mooncakeBlobServiceClient.getContainerClient(quality); const mooncakeBlobClient = mooncakeContainerClient.getBlockBlobClient(blobName); - if (await mooncakeBlobClient.exists()) { - uploadPromises.push(Promise.reject(new Error(`Mooncake Blob ${quality}, ${blobName} already exists, not publishing again.`))); - } else { - uploadPromises.push(retry(async (attempt) => { - console.log(`Uploading blobs to Mooncake Azure storage (attempt ${attempt})...`); - await mooncakeBlobClient.uploadFile(filePath, blobOptions); - console.log('Blob successfully uploaded to Mooncake Azure storage.'); - })); - } + uploadPromises.push((async () => { + console.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(async (attempt) => { + console.log(`Uploading blobs to Mooncake Azure storage (attempt ${attempt})...`); + await mooncakeBlobClient.uploadFile(filePath, blobOptions); + console.log('Blob successfully uploaded to Mooncake Azure storage.'); + }); + } + })()); } const promiseResults = await Promise.allSettled(uploadPromises); diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index a01c222c35e..08380169335 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -32,8 +32,8 @@ async function main(buildDir) { 'Credits.rtf', 'CodeResources', 'fsevents.node', - 'Info.plist', - 'MainMenu.nib', + 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds + 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 '.npmrc' ], outAppPath, @@ -57,4 +57,4 @@ if (require.main === module) { process.exit(1); }); } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLXVuaXZlcnNhbC1hcHAuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjcmVhdGUtdW5pdmVyc2FsLWFwcC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7O0FBRWhHLDZCQUE2QjtBQUM3Qix5QkFBeUI7QUFDekIsdUVBQTREO0FBQzVELHFFQUFvRDtBQUVwRCxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztBQUVuRCxLQUFLLFVBQVUsSUFBSSxDQUFDLFFBQWlCO0lBQ3BDLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7SUFFeEMsSUFBSSxDQUFDLFFBQVEsRUFBRTtRQUNkLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztLQUMxQztJQUVELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxjQUFjLENBQUMsRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDO0lBQ3JGLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxRQUFRLEdBQUcsTUFBTSxDQUFDO0lBQzFDLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLG1CQUFtQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3JFLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLHFCQUFxQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLG1CQUFtQixDQUFDLENBQUM7SUFDL0YsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxLQUFLLEVBQUUsbUJBQW1CLENBQUMsQ0FBQztJQUNuRyxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxpQkFBaUIsSUFBSSxFQUFFLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDekUsTUFBTSxlQUFlLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxLQUFLLEVBQUUsY0FBYyxDQUFDLENBQUM7SUFFakcsTUFBTSxJQUFBLDJDQUFnQixFQUFDO1FBQ3RCLFVBQVU7UUFDVixZQUFZO1FBQ1osV0FBVztRQUNYLGFBQWE7UUFDYixXQUFXLEVBQUU7WUFDWixjQUFjO1lBQ2QsYUFBYTtZQUNiLGVBQWU7WUFDZixlQUFlO1lBQ2YsWUFBWTtZQUNaLGNBQWM7WUFDZCxRQUFRO1NBQ1I7UUFDRCxVQUFVO1FBQ1YsS0FBSyxFQUFFLElBQUk7S0FDWCxDQUFDLENBQUM7SUFFSCxNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxZQUFZLENBQUMsZUFBZSxFQUFFLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFDekUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxXQUFXLEVBQUU7UUFDMUIsc0JBQXNCLEVBQUUsa0JBQWtCO0tBQzFDLENBQUMsQ0FBQztJQUNILEVBQUUsQ0FBQyxhQUFhLENBQUMsZUFBZSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsV0FBVyxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBRTNFLGtEQUFrRDtJQUNsRCxNQUFNLFVBQVUsR0FBRyxNQUFNLElBQUEsMkJBQUssRUFBQyxNQUFNLEVBQUUsQ0FBQyxVQUFVLEVBQUUsT0FBTyxFQUFFLGVBQWUsQ0FBQyxDQUFDLENBQUM7SUFDL0UsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFBLDJCQUFLLEVBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxFQUFFLFVBQVUsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNsRixJQUFJLFVBQVUsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLGNBQWMsRUFBRTtRQUNyRCxNQUFNLElBQUksS0FBSyxDQUFDLHVCQUF1QixVQUFVLEVBQUUsQ0FBQyxDQUFDO0tBQ3JEO0FBQ0YsQ0FBQztBQUVELElBQUksT0FBTyxDQUFDLElBQUksS0FBSyxNQUFNLEVBQUU7SUFDNUIsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUU7UUFDakMsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUNuQixPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ2pCLENBQUMsQ0FBQyxDQUFDO0NBQ0gifQ== \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLXVuaXZlcnNhbC1hcHAuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjcmVhdGUtdW5pdmVyc2FsLWFwcC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7O0FBRWhHLDZCQUE2QjtBQUM3Qix5QkFBeUI7QUFDekIsdUVBQTREO0FBQzVELHFFQUFvRDtBQUVwRCxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztBQUVuRCxLQUFLLFVBQVUsSUFBSSxDQUFDLFFBQWlCO0lBQ3BDLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7SUFFeEMsSUFBSSxDQUFDLFFBQVEsRUFBRTtRQUNkLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztLQUMxQztJQUVELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxjQUFjLENBQUMsRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDO0lBQ3JGLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxRQUFRLEdBQUcsTUFBTSxDQUFDO0lBQzFDLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLG1CQUFtQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3JFLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLHFCQUFxQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLG1CQUFtQixDQUFDLENBQUM7SUFDL0YsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxLQUFLLEVBQUUsbUJBQW1CLENBQUMsQ0FBQztJQUNuRyxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxpQkFBaUIsSUFBSSxFQUFFLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDekUsTUFBTSxlQUFlLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxLQUFLLEVBQUUsY0FBYyxDQUFDLENBQUM7SUFFakcsTUFBTSxJQUFBLDJDQUFnQixFQUFDO1FBQ3RCLFVBQVU7UUFDVixZQUFZO1FBQ1osV0FBVztRQUNYLGFBQWE7UUFDYixXQUFXLEVBQUU7WUFDWixjQUFjO1lBQ2QsYUFBYTtZQUNiLGVBQWU7WUFDZixlQUFlO1lBQ2YsWUFBWSxFQUFFLHlEQUF5RDtZQUN2RSxjQUFjLEVBQUUsd0RBQXdEO1lBQ3hFLFFBQVE7U0FDUjtRQUNELFVBQVU7UUFDVixLQUFLLEVBQUUsSUFBSTtLQUNYLENBQUMsQ0FBQztJQUVILE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxlQUFlLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQztJQUN6RSxNQUFNLENBQUMsTUFBTSxDQUFDLFdBQVcsRUFBRTtRQUMxQixzQkFBc0IsRUFBRSxrQkFBa0I7S0FDMUMsQ0FBQyxDQUFDO0lBQ0gsRUFBRSxDQUFDLGFBQWEsQ0FBQyxlQUFlLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxXQUFXLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxDQUFDLENBQUM7SUFFM0Usa0RBQWtEO0lBQ2xELE1BQU0sVUFBVSxHQUFHLE1BQU0sSUFBQSwyQkFBSyxFQUFDLE1BQU0sRUFBRSxDQUFDLFVBQVUsRUFBRSxPQUFPLEVBQUUsZUFBZSxDQUFDLENBQUMsQ0FBQztJQUMvRSxNQUFNLFVBQVUsR0FBRyxNQUFNLElBQUEsMkJBQUssRUFBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLEVBQUUsVUFBVSxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ2xGLElBQUksVUFBVSxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLEtBQUssY0FBYyxFQUFFO1FBQ3JELE1BQU0sSUFBSSxLQUFLLENBQUMsdUJBQXVCLFVBQVUsRUFBRSxDQUFDLENBQUM7S0FDckQ7QUFDRixDQUFDO0FBRUQsSUFBSSxPQUFPLENBQUMsSUFBSSxLQUFLLE1BQU0sRUFBRTtJQUM1QixJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsRUFBRTtRQUNqQyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ25CLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDakIsQ0FBQyxDQUFDLENBQUM7Q0FDSCJ9 \ No newline at end of file diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 618c736b30a..9cae94895e9 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -23,7 +23,7 @@ const minimatch_1 = require("minimatch"); // Types we assume are present in all implementations of JS VMs (node.js, browsers) // Feel free to add more core types as you see needed if present in node.js and browsers const CORE_TYPES = [ - 'require', + 'require', // from our AMD loader 'setTimeout', 'clearTimeout', 'setInterval', @@ -91,7 +91,7 @@ const RULES = [ ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', + 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, @@ -101,7 +101,7 @@ const RULES = [ allowedTypes: CORE_TYPES, disallowedTypes: [ /* Ignore native types that are defined from here */], disallowedDefinitions: [ - 'lib.dom.d.ts', + 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, @@ -111,7 +111,7 @@ const RULES = [ allowedTypes: CORE_TYPES, disallowedTypes: [ /* Ignore native types that are defined from here */], disallowedDefinitions: [ - 'lib.dom.d.ts', + 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, @@ -121,7 +121,7 @@ const RULES = [ allowedTypes: CORE_TYPES, disallowedTypes: [ /* Ignore native types that are defined from here */], disallowedDefinitions: [ - 'lib.dom.d.ts', + 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, @@ -135,7 +135,7 @@ const RULES = [ ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', + 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, @@ -145,7 +145,7 @@ const RULES = [ allowedTypes: CORE_TYPES, disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', + 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, @@ -288,4 +288,4 @@ for (const sourceFile of program.getSourceFiles()) { if (hasErrors) { process.exit(1); } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGF5ZXJzQ2hlY2tlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImxheWVyc0NoZWNrZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOztBQUVoRyxpQ0FBaUM7QUFDakMsMkJBQThDO0FBQzlDLCtCQUE4QztBQUM5Qyx5Q0FBa0M7QUFFbEMsRUFBRTtBQUNGLGdHQUFnRztBQUNoRyxFQUFFO0FBQ0YsK0ZBQStGO0FBQy9GLG1EQUFtRDtBQUNuRCw0RUFBNEU7QUFDNUUsaUVBQWlFO0FBQ2pFLEVBQUU7QUFDRixnR0FBZ0c7QUFDaEcsRUFBRTtBQUNGLGdHQUFnRztBQUNoRyxFQUFFO0FBRUYsbUZBQW1GO0FBQ25GLHdGQUF3RjtBQUN4RixNQUFNLFVBQVUsR0FBRztJQUNsQixTQUFTO0lBQ1QsWUFBWTtJQUNaLGNBQWM7SUFDZCxhQUFhO0lBQ2IsZUFBZTtJQUNmLFNBQVM7SUFDVCxTQUFTO0lBQ1QsT0FBTztJQUNQLGtCQUFrQjtJQUNsQixRQUFRO0lBQ1IsYUFBYTtJQUNiLGFBQWE7SUFDYixNQUFNO0lBQ04sZ0JBQWdCO0lBQ2hCLE9BQU87SUFDUCxZQUFZO0lBQ1osYUFBYTtJQUNiLGFBQWE7SUFDYixXQUFXO0lBQ1gsWUFBWTtJQUNaLFlBQVk7SUFDWixjQUFjO0lBQ2QsY0FBYztJQUNkLG1CQUFtQjtJQUNuQixnQkFBZ0I7SUFDaEIsZUFBZTtJQUNmLE1BQU07SUFDTixNQUFNO0lBQ04saUJBQWlCO0lBQ2pCLGFBQWE7SUFDYixnQkFBZ0I7SUFDaEIsYUFBYTtJQUNiLEtBQUs7SUFDTCxpQkFBaUI7SUFDakIsZUFBZTtJQUNmLE9BQU87SUFDUCxhQUFhO0lBQ2Isa0JBQWtCO0lBQ2xCLGFBQWE7SUFDYixNQUFNO0NBQ04sQ0FBQztBQUVGLG9FQUFvRTtBQUNwRSxvRUFBb0U7QUFDcEUsTUFBTSxZQUFZLEdBQUc7SUFDcEIsa0JBQWtCO0lBQ2xCLDJCQUEyQjtJQUMzQixrQ0FBa0M7SUFDbEMsNEJBQTRCO0lB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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGF5ZXJzQ2hlY2tlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImxheWVyc0NoZWNrZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOztBQUVoRyxpQ0FBaUM7QUFDakMsMkJBQThDO0FBQzlDLCtCQUE4QztBQUM5Qyx5Q0FBa0M7QUFFbEMsRUFBRTtBQUNGLGdHQUFnRztBQUNoRyxFQUFFO0FBQ0YsK0ZBQStGO0FBQy9GLG1EQUFtRDtBQUNuRCw0RUFBNEU7QUFDNUUsaUVBQWlFO0FBQ2pFLEVBQUU7QUFDRixnR0FBZ0c7QUFDaEcsRUFBRTtBQUNGLGdHQUFnRztBQUNoRyxFQUFFO0FBRUYsbUZBQW1GO0FBQ25GLHdGQUF3RjtBQUN4RixNQUFNLFVBQVUsR0FBRztJQUNsQixTQUFTLEVBQUUsc0JBQXNCO0lBQ2pDLFlBQVk7SUFDWixjQUFjO0lBQ2QsYUFBYTtJQUNiLGVBQWU7SUFDZixTQUFTO0lBQ1QsU0FBUztJQUNULE9BQU87SUFDUCxrQkFBa0I7SUFDbEIsUUFBUTtJQUNSLGFBQWE7SUFDYixhQUFhO0lBQ2IsTUFBTTtJQUNOLGdCQUFnQjtJQUNoQixPQUFPO0lBQ1AsWUFBWTtJQUNaLGFBQWE7SUFDYixhQUFhO0lBQ2IsV0FBVztJQUNYLFlBQVk7SUFDWixZQUFZO0lBQ1osY0FBYztJQUNkLGNBQWM7SUFDZCxtQkFBbUI7SUFDbkIsZ0JBQWdCO0lBQ2hCLGVBQWU7SUFDZixNQUFNO0lBQ04sTUFBTTtJQUNOLGlCQUFpQjtJQUNqQixhQUFhO0lBQ2IsZ0JBQWdCO0lBQ2hCLGFBQWE7SUFDYixLQUFLO0lBQ0wsaUJBQWlCO0lBQ2pCLGVBQWU7SUFDZixPQUFPO0lBQ1AsYUFBYTtJQUNiLGtCQUFrQjtJQUNsQixhQUFhO0lBQ2IsTUFBTTtDQUNOLENBQUM7QUFFRixvRUFBb0U7QUFDcEUsb0VBQW9FO0FBQ3BFLE1BQU0sWUFBWSxHQUFHO0lBQ3BCLGtCQUFrQjtJQUNsQiwyQkFBMkI7SUFDM0Isa0NBQWtDO0lB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 \ No newline at end of file diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index e60b340b45e..16e27eedf01 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -8,11 +8,11 @@ exports.referenceGeneratedDepsByArch = exports.recommendedDeps = exports.additio // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/additional_deps // Additional dependencies not in the dpkg-shlibdeps output. exports.additionalDeps = [ - 'ca-certificates', + 'ca-certificates', // Make sure users have SSL certificates. 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', 'libnss3 (>= 3.26)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'xdg-utils (>= 1.0.2)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', // For Breakpad crash reports. + 'xdg-utils (>= 1.0.2)', // OS integration 'libgssapi-krb5-2', 'libkrb5-3', ]; @@ -140,4 +140,4 @@ exports.referenceGeneratedDepsByArch = { 'xdg-utils (>= 1.0.2)' ] }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLGtIQUFrSDtBQUNsSCw0REFBNEQ7QUFDL0MsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCO0lBQ2pCLHFDQUFxQztJQUNyQyxtQkFBbUI7SUFDbkIsc0RBQXNEO0lBQ3RELHNCQUFzQjtJQUN0QixrQkFBa0I7SUFDbEIsV0FBVztDQUNYLENBQUM7QUFFRixvSEFBb0g7QUFDcEgsMENBQTBDO0FBQzFDLDhEQUE4RDtBQUNqRCxRQUFBLGVBQWUsR0FBRztJQUM5QixZQUFZLENBQUMseUVBQXlFO0NBQ3RGLENBQUM7QUFFVyxRQUFBLDRCQUE0QixHQUFHO0lBQzNDLE9BQU8sRUFBRTtRQUNSLGlCQUFpQjtRQUNqQix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLHdCQUF3QjtRQUN4QiwyQkFBMkI7UUFDM0IsaUJBQWlCO1FBQ2pCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsa0JBQWtCO1FBQ2xCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQseUJBQXlCO1FBQ3pCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQixrQkFBa0I7UUFDbEIsd0JBQXdCO1FBQ3hCLHFDQUFxQztRQUNyQyxXQUFXO1FBQ1gsd0JBQXdCO1FBQ3hCLHFCQUFxQjtRQUNyQixtQkFBbUI7UUFDbkIsNEJBQTRCO1FBQzVCLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7SUFDRCxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsaUJBQWlCO1FBQ2pCLGdCQUFnQjtRQUNoQixnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQseUJBQXlCO1FBQ3pCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQixrQkFBa0I7UUFDbEIsd0JBQXdCO1FBQ3hCLHFDQUFxQztRQUNyQyxXQUFXO1FBQ1gsd0JBQXdCO1FBQ3hCLHFCQUFxQjtRQUNyQixtQkFBbUI7UUFDbkIsNEJBQTRCO1FBQzVCLG1CQUFtQjtRQUNuQixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7SUFDRCxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixzQkFBc0I7UUFDdEIsc0RBQXNEO1FBQ3RELHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsc0JBQXNCO1FBQ3RCLHlCQUF5QjtRQUN6QiwwQkFBMEI7UUFDMUIsa0JBQWtCO1FBQ2xCLHdCQUF3QjtRQUN4QixxQ0FBcUM7UUFDckMsV0FBVztRQUNYLHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLDRCQUE0QjtRQUM1QixtQkFBbUI7UUFDbkIscUJBQXFCO1FBQ3JCLG1CQUFtQjtRQUNuQixVQUFVO1FBQ1YsMEJBQTBCO1FBQzFCLG9CQUFvQjtRQUNwQiwrQkFBK0I7UUFDL0Isd0JBQXdCO1FBQ3hCLFVBQVU7UUFDVixZQUFZO1FBQ1osMEJBQTBCO1FBQzFCLGFBQWE7UUFDYixZQUFZO1FBQ1osc0JBQXNCO0tBQ3RCO0NBQ0QsQ0FBQyJ9 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLGtIQUFrSDtBQUNsSCw0REFBNEQ7QUFDL0MsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCLEVBQUUseUNBQXlDO0lBQzVELHFDQUFxQztJQUNyQyxtQkFBbUI7SUFDbkIsc0RBQXNELEVBQUUsOEJBQThCO0lBQ3RGLHNCQUFzQixFQUFFLGlCQUFpQjtJQUN6QyxrQkFBa0I7SUFDbEIsV0FBVztDQUNYLENBQUM7QUFFRixvSEFBb0g7QUFDcEgsMENBQTBDO0FBQzFDLDhEQUE4RDtBQUNqRCxRQUFBLGVBQWUsR0FBRztJQUM5QixZQUFZLENBQUMseUVBQXlFO0NBQ3RGLENBQUM7QUFFVyxRQUFBLDRCQUE0QixHQUFHO0lBQzNDLE9BQU8sRUFBRTtRQUNSLGlCQUFpQjtRQUNqQix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLHdCQUF3QjtRQUN4QiwyQkFBMkI7UUFDM0IsaUJBQWlCO1FBQ2pCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsa0JBQWtCO1FBQ2xCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQseUJBQXlCO1FBQ3pCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQixrQkFBa0I7UUFDbEIsd0JBQXdCO1FBQ3hCLHFDQUFxQztRQUNyQyxXQUFXO1FBQ1gsd0JBQXdCO1FBQ3hCLHFCQUFxQjtRQUNyQixtQkFBbUI7UUFDbkIsNEJBQTRCO1FBQzVCLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7SUFDRCxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsaUJBQWlCO1FBQ2pCLGdCQUFnQjtRQUNoQixnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQseUJBQXlCO1FBQ3pCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQixrQkFBa0I7UUFDbEIsd0JBQXdCO1FBQ3hCLHFDQUFxQztRQUNyQyxXQUFXO1FBQ1gsd0JBQXdCO1FBQ3hCLHFCQUFxQjtRQUNyQixtQkFBbUI7UUFDbkIsNEJBQTRCO1FBQzVCLG1CQUFtQjtRQUNuQixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7SUFDRCxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixzQkFBc0I7UUFDdEIsc0RBQXNEO1FBQ3RELHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsc0JBQXNCO1FBQ3RCLHlCQUF5QjtRQUN6QiwwQkFBMEI7UUFDMUIsa0JBQWtCO1FBQ2xCLHdCQUF3QjtRQUN4QixxQ0FBcUM7UUFDckMsV0FBVztRQUNYLHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLDRCQUE0QjtRQUM1QixtQkFBbUI7UUFDbkIscUJBQXFCO1FBQ3JCLG1CQUFtQjtRQUNuQixVQUFVO1FBQ1YsMEJBQTBCO1FBQzFCLG9CQUFvQjtRQUNwQiwrQkFBK0I7UUFDL0Isd0JBQXdCO1FBQ3hCLFVBQVU7UUFDVixZQUFZO1FBQ1osMEJBQTBCO1FBQzFCLGFBQWE7UUFDYixZQUFZO1FBQ1osc0JBQXNCO0tBQ3RCO0NBQ0QsQ0FBQyJ9 \ No newline at end of file diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index 667d207ca9d..c97574a083b 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -8,7 +8,7 @@ exports.referenceGeneratedDepsByArch = exports.additionalDeps = void 0; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/additional_deps // Additional dependencies not in the rpm find-requires output. exports.additionalDeps = [ - 'ca-certificates', + 'ca-certificates', // Make sure users have SSL certificates. 'libgtk-3.so.0()(64bit)', 'libnss3.so(NSS_3.22)(64bit)', 'libssl3.so(NSS_3.28)(64bit)', @@ -302,4 +302,4 @@ exports.referenceGeneratedDepsByArch = { 'xdg-utils' ] }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLCtHQUErRztBQUMvRywrREFBK0Q7QUFDbEQsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCO0lBQ2pCLHdCQUF3QjtJQUN4Qiw2QkFBNkI7SUFDN0IsNkJBQTZCO0lBQzdCLGdDQUFnQztJQUNoQyx5QkFBeUI7SUFDekIsdUJBQXVCO0lBQ3ZCLFdBQVcsQ0FBQyxpQkFBaUI7Q0FDN0IsQ0FBQztBQUVXLFFBQUEsNEJBQTRCLEdBQUc7SUFDM0MsUUFBUSxFQUFFO1FBQ1QsaUJBQWlCO1FBQ2pCLCtCQUErQjtRQUMvQiwwQ0FBMEM7UUFDMUMsd0NBQXdDO1FBQ3hDLHNCQUFzQjtRQUN0Qiw2QkFBNkI7UUFDN0IsMEJBQTBCO1FBQzFCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIseUJBQXlCO1FBQ3pCLHlCQUF5QjtRQUN6QixpQ0FBaUM7UUFDakMsc0NBQXNDO1FBQ3RDLDBCQUEwQjtRQUMxQixpQ0FBaUM7UUFDakMsd0JBQXdCO1FBQ3hCLG9CQUFvQjtRQUNwQiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QiwrQkFBK0I7UUFDL0IsNkJBQTZCO1FBQzdCLCtCQUErQjtRQUMvQiwrQkFBK0I7UUFDL0IsK0JBQStCO1FBQy9CLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0IsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0Isd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQyxzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLDBCQUEwQjtR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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLCtHQUErRztBQUMvRywrREFBK0Q7QUFDbEQsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCLEVBQUUseUNBQXlDO0lBQzVELHdCQUF3QjtJQUN4Qiw2QkFBNkI7SUFDN0IsNkJBQTZCO0lBQzdCLGdDQUFnQztJQUNoQyx5QkFBeUI7SUFDekIsdUJBQXVCO0lBQ3ZCLFdBQVcsQ0FBQyxpQkFBaUI7Q0FDN0IsQ0FBQztBQUVXLFFBQUEsNEJBQTRCLEdBQUc7SUFDM0MsUUFBUSxFQUFFO1FBQ1QsaUJBQWlCO1FBQ2pCLCtCQUErQjtRQUMvQiwwQ0FBMEM7UUFDMUMsd0NBQXdDO1FBQ3hDLHNCQUFzQjtRQUN0Qiw2QkFBNkI7UUFDN0IsMEJBQTBCO1FBQzFCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIseUJBQXlCO1FBQ3pCLHlCQUF5QjtRQUN6QixpQ0FBaUM7UUFDakMsc0NBQXNDO1FBQ3RDLDBCQUEwQjtRQUMxQixpQ0FBaUM7UUFDakMsd0JBQXdCO1FBQ3hCLG9CQUFvQjtRQUNwQiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QiwrQkFBK0I7UUFDL0IsNkJBQTZCO1FBQzdCLCtCQUErQjtRQUMvQiwrQkFBK0I7UUFDL0IsK0JBQStCO1FBQy9CLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0IsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0Isd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQyxzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLDBCQUEwQjtRQUMxQiwyQkFBMkI7UUFDM0IsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QiwrQ0FBK0M7UUFDL0Msd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2QixpQ0FBaUM7UUFDakMsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQixzQkFBc0I7UUFDdEIscUJBQXFCO1FBQ3JCLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0IsK0JBQStCO1FBQy9CLDZCQUE2QjtRQUM3Qiw0QkFBNEI7UUFDNUIsNkJBQTZCO1FBQzdCLDRCQUE0QjtRQUM1Qiw2QkFBNkI7UUFDN0IsNEJBQTRCO1FBQzVCLDRCQUE0QjtRQUM1Qiw4QkFBOEI7UUFDOUIseUJBQXlCO1FBQ3pCLHVDQUF1QztRQUN2Qyw0QkFBNEI7UUFDNUIsMEJBQTBCO1FBQzFCLG9DQUFvQztRQUNwQyxxQ0FBcUM7UUFDckMscUNBQXFDO1FBQ3JDLHFDQUFxQztRQUNyQyxxQ0FBcUM7UUFDckMscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQyx1QkFBdUI7UUFDdkIsK0JBQStCO1FBQy9CLDhCQUE4QjtRQUM5Qiw2QkFBNkI7UUFDN0IsdUJBQXVCO1FBQ3ZCLGtDQUFrQztRQUNsQyxzQkFBc0I7UUFDdEIsNEJBQTRCO1FBQzVCLDBCQUEwQjtRQUMxQixnQ0FBZ0M7UUFDaEMsZ0JBQWdCO1FBQ2hCLFdBQVc7S0FDWDtJQUNELFNBQVMsRUFBRTtRQUNWLGlCQUFpQjtRQUNqQixxQkFBcUI7UUFDckIsZ0NBQWdDO1FBQ2hDLGFBQWE7UUFDYixvQkFBb0I7UUFDcEIsaUJBQWlCO1FBQ2pCLGNBQWM7UUFDZCxnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLGdCQUFnQjtRQUNoQiwwQkFBMEI7UUFDMUIsK0JBQStCO1FBQy9CLGlCQUFpQjtRQUNqQix3QkFBd0I7UUFDeEIsZUFBZTtRQUNmLFdBQVc7UUFDWCx1QkFBdUI7UUFDdkIsdUJBQXVCO1FBQ3ZCLHVCQUF1QjtRQUN2Qix1QkFBdUI7UUFDdkIsdUJBQXVCO1FBQ3ZCLHNCQUFzQjtRQUN0QixzQkFBc0I7UUFDdEIsc0JBQXNCO1FBQ3RCLHNCQUFzQjtRQUN0QixzQkFBc0I7UUFDdEIsZUFBZTtRQUNmLHVCQUF1QjtRQUN2QixnQkFBZ0I7UUFDaEIsWUFBWTtRQUNaLHVCQUF1QjtRQUN2QixhQUFhO1FBQ2IsZUFBZTtRQUNmLGFBQWE7UUFDYixlQUFlO1FBQ2Ysd0JBQXdCO1FBQ3hCLHdCQUF3QjtRQUN4QixpQkFBaUI7UUFDakIsa0JBQWtCO1FBQ2xCLHFCQUFxQjtRQUNyQixxQkFBcUI7UUFDckIsd0NBQXdDO1FBQ3hDLGVBQWU7UUFDZix3QkFBd0I7UUFDeEIsY0FBYztRQUNkLDBCQUEwQjtRQUMxQixXQUFXO1FBQ1gsc0JBQXNCO1FBQ3RCLGFBQWE7UUFDYixZQUFZO1FBQ1osc0JBQXNCO1FBQ3RCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsc0JBQXNCO1FBQ3RCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIsNkJBQTZCO1FBQzdCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIscUJBQXFCO1FBQ3JCLHFCQUFxQjtRQUNyQix1QkFBdUI7UUFDdkIsZ0JBQWdCO1FBQ2hCLGdDQUFnQztRQUNoQyxtQkFBbUI7UUFDbkIsaUJBQWlCO1FBQ2pCLDZCQUE2QjtRQUM3Qiw0QkFBNEI7UUFDNUIsWUFBWTtRQUNaLHVCQUF1QjtRQUN2QixjQUFjO1FBQ2Qsd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qiw2QkFBNkI7UUFDN0IsZ0JBQWdCO1FBQ2hCLDRCQUE0QjtRQUM1Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QixrQ0FBa0M7UUFDbEMsNkJBQTZCO1FBQzdCLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsK0JBQStCO1FBQy9CLCtCQUErQjtRQUMvQixjQUFjO1FBQ2QseUJBQXlCO1FBQ3pCLGFBQWE7UUFDYixtQkFBbUI7UUFDbkIsaUJBQWlCO1FBQ2pCLGdDQUFnQztRQUNoQyxnQkFBZ0I7UUFDaEIsV0FBVztLQUNYO0lBQ0QsU0FBUyxFQUFFO1FBQ1YsaUJBQWlCO1FBQ2pCLGdDQUFnQztRQUNoQywwQ0FBMEM7UUFDMUMsc0JBQXNCO1FBQ3RCLDZCQUE2QjtRQUM3QiwwQkFBMEI7UUFDMUIsdUJBQXVCO1FBQ3ZCLHlCQUF5QjtRQUN6Qix5QkFBeUI7UUFDekIseUJBQXlCO1FBQ3pCLGlDQUFpQztRQUNqQyxzQ0FBc0M7UUFDdEMsMEJBQTBCO1FBQzFCLGlDQUFpQztRQUNqQyx3QkFBd0I7UUFDeEIsb0JBQW9CO1FBQ3BCLDhCQUE4QjtRQUM5Qix3QkFBd0I7UUFDeEIsdUJBQXVCO1FBQ3ZCLHlCQUF5QjtRQUN6QixvQ0FBb0M7UUFDcEMscUJBQXFCO1FBQ3JCLCtCQUErQjtRQUMvQixzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLGlDQUFpQztRQUNqQyxpQ0FBaUM7UUFDakMsMEJBQTBCO1FBQzFCLDJCQUEyQjtRQUMzQiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLCtDQUErQztRQUMvQyx3QkFBd0I7UUFDeEIsdUJBQXVCO1FBQ3ZCLGlDQUFpQztRQUNqQyxvQkFBb0I7UUFDcEIsOEJBQThCO1FBQzlCLHNCQUFzQjtRQUN0QixxQkFBcUI7UUFDckIsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3QiwrQkFBK0I7UUFDL0IsNkJBQTZCO1FBQzdCLDRCQUE0QjtRQUM1Qiw2QkFBNkI7UUFDN0IsNEJBQTRCO1FBQzVCLDZCQUE2QjtRQUM3Qiw0QkFBNEI7UUFDNUIsNEJBQTRCO1FBQzVCLDhCQUE4QjtRQUM5Qix5QkFBeUI7UUFDekIsdUNBQXVDO1FBQ3ZDLDRCQUE0QjtRQUM1QiwwQkFBMEI7UUFDMUIsb0NBQW9DO1FBQ3BDLHFCQUFxQjtRQUNyQiwrQkFBK0I7UUFDL0IsdUJBQXVCO1FBQ3ZCLCtCQUErQjtRQUMvQiw4QkFBOEI7UUFDOUIsNkJBQTZCO1FBQzdCLHlCQUF5QjtRQUN6QixtQ0FBbUM7UUFDbkMscUNBQXFDO1FBQ3JDLHFDQUFxQztRQUNyQyxxQ0FBcUM7UUFDckMsb0NBQW9DO1FBQ3BDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsc0NBQXNDO1FBQ3RDLHNDQUFzQztRQUN0Qyx1QkFBdUI7UUFDdkIsaUNBQWlDO1FBQ2pDLHNCQUFzQjtRQUN0Qiw0QkFBNEI7UUFDNUIsbUNBQW1DO1FBQ25DLDBCQUEwQjtRQUMxQixnQ0FBZ0M7UUFDaEMsZ0JBQWdCO1FBQ2hCLFdBQVc7S0FDWDtDQUNELENBQUMifQ== \ No newline at end of file diff --git a/extensions/css/cgmanifest.json b/extensions/css/cgmanifest.json index fe46b5c4acd..0705aca66c4 100644 --- a/extensions/css/cgmanifest.json +++ b/extensions/css/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "microsoft/vscode-css", "repositoryUrl": "https://github.com/microsoft/vscode-css", - "commitHash": "3bd00206f6b0d16eb2eba53fb886462eb8c58baa" + "commitHash": "c216f777497265700ff336f739328e5197e012cd" } }, "licenseDetail": [ diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index c44db9bd68f..2096e16e920 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-css/commit/3bd00206f6b0d16eb2eba53fb886462eb8c58baa", + "version": "https://github.com/microsoft/vscode-css/commit/c216f777497265700ff336f739328e5197e012cd", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -1401,7 +1401,7 @@ "property-keywords": { "patterns": [ { - "match": "(?xi) (?(); +abstract class AbstractActionButton { + protected _onDidChange = new EventEmitter(); get onDidChange(): Event { return this._onDidChange.event; } private _state: ActionButtonState; - private get state() { return this._state; } - private set state(state: ActionButtonState) { + protected get state() { return this._state; } + protected set state(state: ActionButtonState) { if (JSON.stringify(this._state) !== JSON.stringify(state)) { this._state = state; this._onDidChange.fire(); } } - private disposables: Disposable[] = []; + abstract get button(): SourceControlActionButton | undefined; - constructor( - readonly repository: Repository, - readonly postCommitCommandCenter: CommitCommandsCenter) { + protected disposables: Disposable[] = []; + + constructor(readonly repository: Repository) { this._state = { HEAD: undefined, isCheckoutInProgress: false, @@ -50,6 +50,126 @@ export class ActionButtonCommand { repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); + } + + protected getPublishBranchActionButton(): SourceControlActionButton | undefined { + const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)'; + + return { + command: { + command: 'git.publish', + title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }), + tooltip: this.state.isSyncInProgress ? + (this.state.HEAD?.name ? + l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) : + l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) : + (this.repository.HEAD?.name ? + l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) : + l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })), + arguments: [this.repository.sourceControl], + }, + enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress && !this.state.isCommitInProgress && !this.state.isMergeInProgress && !this.state.isRebaseInProgress + }; + } + + protected getSyncChangesActionButton(): SourceControlActionButton | undefined { + const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0; + + const ahead = this.state.HEAD?.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : ''; + const behind = this.state.HEAD?.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : ''; + const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)'; + + return { + command: { + command: 'git.sync', + title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead), + tooltip: this.state.isSyncInProgress ? + l10n.t('Synchronizing Changes...') + : this.repository.syncTooltip, + arguments: [this.repository.sourceControl], + }, + description: `${icon}${behind}${ahead}`, + enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress && !this.state.isCommitInProgress && !this.state.isMergeInProgress && !this.state.isRebaseInProgress && branchIsAheadOrBehind + }; + } + + private onDidChangeOperations(): void { + const isCheckoutInProgress + = this.repository.operations.isRunning(OperationKind.Checkout) || + this.repository.operations.isRunning(OperationKind.CheckoutTracking); + + const isCommitInProgress = + this.repository.operations.isRunning(OperationKind.Commit) || + this.repository.operations.isRunning(OperationKind.PostCommitCommand) || + this.repository.operations.isRunning(OperationKind.RebaseContinue); + + const isSyncInProgress = + this.repository.operations.isRunning(OperationKind.Sync) || + this.repository.operations.isRunning(OperationKind.Push) || + this.repository.operations.isRunning(OperationKind.Pull); + + this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress }; + } + + private onDidRunGitStatus(): void { + this.state = { + ...this.state, + HEAD: this.repository.HEAD, + isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0, + isRebaseInProgress: !!this.repository.rebaseCommit, + repositoryHasChangesToCommit: this.repositoryHasChangesToCommit() + }; + } + + protected repositoryHasChangesToCommit(): boolean { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const enableSmartCommit = config.get('enableSmartCommit') === true; + const suggestSmartCommit = config.get('suggestSmartCommit') === true; + const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all'); + + const resources = [...this.repository.indexGroup.resourceStates]; + + if ( + // Smart commit enabled (all) + (enableSmartCommit && smartCommitChanges === 'all') || + // Smart commit disabled, smart suggestion enabled + (!enableSmartCommit && suggestSmartCommit) + ) { + resources.push(...this.repository.workingTreeGroup.resourceStates); + } + + // Smart commit enabled (tracked only) + if (enableSmartCommit && smartCommitChanges === 'tracked') { + resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED)); + } + + return resources.length !== 0; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +export class CommitActionButton extends AbstractActionButton { + override get button(): SourceControlActionButton | undefined { + if (!this.state.HEAD) { return undefined; } + + let actionButton: SourceControlActionButton | undefined; + + if (this.state.repositoryHasChangesToCommit) { + // Commit Changes (enabled) + actionButton = this.getCommitActionButton(); + } + + // Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled) + return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton(); + } + + constructor( + repository: Repository, + readonly postCommitCommandCenter: CommitCommandsCenter) { + super(repository); this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire())); this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire())); @@ -62,7 +182,8 @@ export class ActionButtonCommand { this.onDidChangeSmartCommitSettings(); } - if (e.affectsConfiguration('git.branchProtectionPrompt', root) || + if (e.affectsConfiguration('scm.experimental.showSyncView') || + e.affectsConfiguration('git.branchProtectionPrompt', root) || e.affectsConfiguration('git.postCommitCommand', root) || e.affectsConfiguration('git.rememberPostCommitCommand', root) || e.affectsConfiguration('git.showActionButton', root)) { @@ -71,20 +192,6 @@ export class ActionButtonCommand { })); } - get button(): SourceControlActionButton | undefined { - if (!this.state.HEAD) { return undefined; } - - let actionButton: SourceControlActionButton | undefined; - - if (this.state.repositoryHasChangesToCommit) { - // Commit Changes (enabled) - actionButton = this.getCommitActionButton(); - } - - // Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled) - return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton(); - } - private getCommitActionButton(): SourceControlActionButton | undefined { const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true }); @@ -133,34 +240,27 @@ export class ActionButtonCommand { return commandGroups; } - private getPublishBranchActionButton(): SourceControlActionButton | undefined { + protected override getPublishBranchActionButton(): SourceControlActionButton | undefined { + const scmConfig = workspace.getConfiguration('scm'); + if (scmConfig.get('experimental.showSyncView', false)) { + return undefined; + } + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true }); // Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name || this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; } - // Button icon - const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)'; - - return { - command: { - command: 'git.publish', - title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }), - tooltip: this.state.isSyncInProgress ? - (this.state.HEAD?.name ? - l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) : - l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) : - (this.repository.HEAD?.name ? - l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) : - l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })), - arguments: [this.repository.sourceControl], - }, - enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress - }; + return super.getPublishBranchActionButton(); } - private getSyncChangesActionButton(): SourceControlActionButton | undefined { + protected override getSyncChangesActionButton(): SourceControlActionButton | undefined { + const scmConfig = workspace.getConfiguration('scm'); + if (scmConfig.get('experimental.showSyncView', false)) { + return undefined; + } + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true }); const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0; @@ -168,40 +268,7 @@ export class ActionButtonCommand { // Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; } - const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : ''; - const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : ''; - const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)'; - - return { - command: { - command: 'git.sync', - title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead), - tooltip: this.state.isSyncInProgress ? - l10n.t('Synchronizing Changes...') - : this.repository.syncTooltip, - arguments: [this.repository.sourceControl], - }, - description: `${icon}${behind}${ahead}`, - enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress - }; - } - - private onDidChangeOperations(): void { - const isCheckoutInProgress - = this.repository.operations.isRunning(OperationKind.Checkout) || - this.repository.operations.isRunning(OperationKind.CheckoutTracking); - - const isCommitInProgress = - this.repository.operations.isRunning(OperationKind.Commit) || - this.repository.operations.isRunning(OperationKind.PostCommitCommand) || - this.repository.operations.isRunning(OperationKind.RebaseContinue); - - const isSyncInProgress = - this.repository.operations.isRunning(OperationKind.Sync) || - this.repository.operations.isRunning(OperationKind.Push) || - this.repository.operations.isRunning(OperationKind.Pull); - - this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress }; + return super.getSyncChangesActionButton(); } private onDidChangeSmartCommitSettings(): void { @@ -210,43 +277,30 @@ export class ActionButtonCommand { repositoryHasChangesToCommit: this.repositoryHasChangesToCommit() }; } +} - private onDidRunGitStatus(): void { - this.state = { - ...this.state, - HEAD: this.repository.HEAD, - isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0, - isRebaseInProgress: !!this.repository.rebaseCommit, - repositoryHasChangesToCommit: this.repositoryHasChangesToCommit() - }; +export class SyncActionButton extends AbstractActionButton { + override get button(): SourceControlActionButton | undefined { + if (!this.state.HEAD) { return undefined; } + + // Publish Branch -> Sync Changes + return this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton(); } - private repositoryHasChangesToCommit(): boolean { - const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); - const enableSmartCommit = config.get('enableSmartCommit') === true; - const suggestSmartCommit = config.get('suggestSmartCommit') === true; - const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all'); + constructor(repository: Repository) { + super(repository); - const resources = [...this.repository.indexGroup.resourceStates]; - - if ( - // Smart commit enabled (all) - (enableSmartCommit && smartCommitChanges === 'all') || - // Smart commit disabled, smart suggestion enabled - (!enableSmartCommit && suggestSmartCommit) - ) { - resources.push(...this.repository.workingTreeGroup.resourceStates); - } - - // Smart commit enabled (tracked only) - if (enableSmartCommit && smartCommitChanges === 'tracked') { - resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED)); - } - - return resources.length !== 0; + this.disposables.push(workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('scm.experimental.showSyncView')) { + this._onDidChange.fire(); + } + })); } - dispose(): void { - this.disposables = dispose(this.disposables); + protected override getPublishBranchActionButton(): SourceControlActionButton | undefined { + // Not a branch (tag, detached), branch does have an upstream + if (this.state.HEAD?.type === RefType.Tag || this.state.HEAD?.upstream) { return undefined; } + + return super.getPublishBranchActionButton(); } } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 05af77899f0..d972ce59f7e 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -131,6 +131,8 @@ export interface LogOptions { readonly path?: string; /** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */ readonly range?: string; + readonly reverse?: boolean; + readonly sortByAuthorDate?: boolean; } export interface CommitOptions { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 502e8de947b..f8e080e76fd 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1023,17 +1023,24 @@ export class Repository { } async log(options?: LogOptions): Promise { - const maxEntries = options?.maxEntries ?? 32; - const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z']; + const args = ['log', `--format=${COMMIT_FORMAT}`, '-z']; + + if (options?.reverse) { + args.push('--reverse', '--ancestry-path'); + } + + if (options?.sortByAuthorDate) { + args.push('--author-date-order'); + } if (options?.range) { args.push(options.range); + } else { + args.push(`-n${options?.maxEntries ?? 32}`); } - args.push('--'); - if (options?.path) { - args.push(options.path); + args.push('--', options.path); } const result = await this.exec(args); @@ -1258,7 +1265,7 @@ export class Repository { diffIndexWithHEAD(): Promise; diffIndexWithHEAD(path: string): Promise; - diffIndexWithHEAD(path?: string | undefined): Promise; + diffIndexWithHEAD(path?: string | undefined): Promise; async diffIndexWithHEAD(path?: string): Promise { if (!path) { return await this.diffFiles(true); @@ -1303,6 +1310,17 @@ export class Repository { return result.stdout.trim(); } + async diffBetweenShortStat(ref1: string, ref2: string): Promise { + const args = ['diff', '--shortstat', `${ref1}...${ref2}`]; + + const result = await this.exec(args); + if (result.exitCode) { + return ''; + } + + return result.stdout.trim(); + } + private async diffFiles(cached: boolean, ref?: string): Promise { const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR']; if (cached) { @@ -2450,6 +2468,15 @@ export class Repository { return Promise.reject(new Error('No such branch')); } + async getDefaultBranch(): Promise { + const result = await this.exec(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']); + if (!result.stdout) { + throw new Error('No default branch'); + } + + return this.getBranch(result.stdout.trim()); + } + // TODO: Support core.commentChar stripCommitMessageComments(message: string): string { return message.replace(/^\s*#.*$\n?/gm, '').trim(); @@ -2510,6 +2537,13 @@ export class Repository { return commits[0]; } + async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { + const result = await this.exec(['rev-list', '--count', '--left-right', range]); + const [ahead, behind] = result.stdout.trim().split('\t'); + + return { ahead: Number(ahead) || 0, behind: Number(behind) || 0 }; + } + async updateSubmodules(paths: string[]): Promise { const args = ['submodule', 'update']; diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts new file mode 100644 index 00000000000..f5a5b27b93c --- /dev/null +++ b/extensions/git/src/historyProvider.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlActionButton, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, l10n } from 'vscode'; +import { Repository, Resource } from './repository'; +import { IDisposable } from './util'; +import { toGitUri } from './uri'; +import { SyncActionButton } from './actionButton'; +import { Status } from './api/git'; + +export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { + + private readonly _onDidChangeActionButton = new EventEmitter(); + readonly onDidChangeActionButton: Event = this._onDidChangeActionButton.event; + + private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter(); + readonly onDidChangeCurrentHistoryItemGroup: Event = this._onDidChangeCurrentHistoryItemGroup.event; + + private readonly _onDidChangeDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; + + private _actionButton: SourceControlActionButton | undefined; + get actionButton(): SourceControlActionButton | undefined { return this._actionButton; } + set actionButton(button: SourceControlActionButton | undefined) { + this._actionButton = button; + this._onDidChangeActionButton.fire(); + } + + private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined; + + get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } + set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) { + this._currentHistoryItemGroup = value; + this._onDidChangeCurrentHistoryItemGroup.fire(); + } + + private historyItemDecorations = new Map(); + + private disposables: Disposable[] = []; + + constructor(protected readonly repository: Repository) { + const actionButton = new SyncActionButton(repository); + this.actionButton = actionButton.button; + this.disposables.push(actionButton); + + this.disposables.push(repository.onDidRunGitStatus(this.onDidRunGitStatus, this)); + this.disposables.push(actionButton.onDidChange(() => this.actionButton = actionButton.button)); + + this.disposables.push(window.registerFileDecorationProvider(this)); + } + + private async onDidRunGitStatus(): Promise { + if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit) { return; } + + this.currentHistoryItemGroup = { + id: `refs/heads/${this.repository.HEAD.name}`, + label: this.repository.HEAD.name, + upstream: this.repository.HEAD.upstream ? + { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + label: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + } : undefined + }; + } + + async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise { + //TODO@lszomoru - support limit and cursor + if (typeof options.limit === 'number') { + throw new Error('Unsupported options.'); + } + if (typeof options.limit?.id !== 'string') { + throw new Error('Unsupported options.'); + } + + const optionsRef = options.limit.id; + const [commits, summary] = await Promise.all([ + this.repository.log({ range: `${optionsRef}..${historyItemGroupId}`, sortByAuthorDate: true }), + this.getSummaryHistoryItem(optionsRef, historyItemGroupId) + ]); + + const historyItems = commits.length === 0 ? [] : [summary]; + historyItems.push(...commits.map(commit => { + const newLineIndex = commit.message.indexOf('\n'); + const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + + return { + id: commit.hash, + parentIds: commit.parents, + label: subject, + description: commit.authorName, + icon: new ThemeIcon('account'), + timestamp: commit.authorDate?.getTime() + }; + })); + + return historyItems; + } + + async provideHistoryItemChanges(historyItemId: string): Promise { + const [ref1, ref2] = historyItemId.includes('..') + ? historyItemId.split('..') + : [`${historyItemId}^`, historyItemId]; + + const historyItemChangesUri: Uri[] = []; + const historyItemChanges: SourceControlHistoryItemChange[] = []; + const changes = await this.repository.diffBetween(ref1, ref2); + + for (const change of changes) { + const historyItemUri = change.uri.with({ + query: `ref=${historyItemId}` + }); + + // History item change + historyItemChanges.push({ + uri: historyItemUri, + originalUri: toGitUri(change.originalUri, ref1), + modifiedUri: toGitUri(change.originalUri, ref2), + renameUri: change.renameUri, + }); + + // History item change decoration + const fileDecoration = this.historyItemChangeFileDecoration(change.status); + this.historyItemDecorations.set(historyItemUri.toString(), fileDecoration); + + historyItemChangesUri.push(historyItemUri); + } + + this._onDidChangeDecorations.fire(historyItemChangesUri); + return historyItemChanges; + } + + async resolveHistoryItemGroupBase(historyItemGroupId: string): Promise { + // TODO - support for all history item groups + if (historyItemGroupId !== this.currentHistoryItemGroup?.id) { + return undefined; + } + + if (this.currentHistoryItemGroup?.upstream) { + return this.currentHistoryItemGroup.upstream; + } + + // Default branch + const defaultBranch = await this.repository.getDefaultBranch(); + return defaultBranch.name ? { id: `refs/heads/${defaultBranch.name}`, label: defaultBranch.name } : undefined; + } + + async resolveHistoryItemGroupCommonAncestor(refId1: string, refId2: string): Promise<{ id: string; ahead: number; behind: number } | undefined> { + const ancestor = await this.repository.getMergeBase(refId1, refId2); + if (ancestor === '') { + return undefined; + } + + const commitCount = await this.repository.getCommitCount(`${refId1}...${refId2}`); + return { id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind }; + } + + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.historyItemDecorations.get(uri.toString()); + } + + private historyItemChangeFileDecoration(status: Status): FileDecoration { + const letter = Resource.getStatusLetter(status); + const tooltip = Resource.getStatusText(status); + const color = Resource.getStatusColor(status); + + const fileDecoration = new FileDecoration(letter, tooltip, color); + fileDecoration.propagate = status !== Status.DELETED && status !== Status.INDEX_DELETED; + + return fileDecoration; + } + + private async getSummaryHistoryItem(ref1: string, ref2: string): Promise { + const diffShortStat = await this.repository.diffBetweenShortStat(ref1, ref2); + return { id: `${ref1}..${ref2}`, parentIds: [], icon: new ThemeIcon('files'), label: l10n.t('All Changes'), description: diffShortStat }; + } + + dispose(): void { + this.disposables.forEach(d => d.dispose()); + } +} diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index b385acb11c5..29eff86a59b 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -53,6 +53,7 @@ export const enum OperationKind { RebaseContinue = 'RebaseContinue', RevertFiles = 'RevertFiles', RevertFilesNoProgress = 'RevertFilesNoProgress', + RevList = 'RevList', SetBranchUpstream = 'SetBranchUpstream', Show = 'Show', Stage = 'Stage', @@ -69,8 +70,9 @@ export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchO GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation | - ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation | - ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation; + ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | RevListOperation | + SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | + TagOperation; type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean }; export type AddOperation = BaseOperation & { kind: OperationKind.Add }; @@ -116,6 +118,7 @@ export type RebaseOperation = BaseOperation & { kind: OperationKind.Rebase }; export type RebaseAbortOperation = BaseOperation & { kind: OperationKind.RebaseAbort }; export type RebaseContinueOperation = BaseOperation & { kind: OperationKind.RebaseContinue }; export type RevertFilesOperation = BaseOperation & { kind: OperationKind.RevertFiles }; +export type RevListOperation = BaseOperation & { kind: OperationKind.RevList }; export type SetBranchUpstreamOperation = BaseOperation & { kind: OperationKind.SetBranchUpstream }; export type ShowOperation = BaseOperation & { kind: OperationKind.Show }; export type StageOperation = BaseOperation & { kind: OperationKind.Stage }; @@ -169,6 +172,7 @@ export const Operation = { RebaseAbort: { kind: OperationKind.RebaseAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseAbortOperation, RebaseContinue: { kind: OperationKind.RebaseContinue, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseContinueOperation, RevertFiles: (showProgress: boolean) => ({ kind: OperationKind.RevertFiles, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as RevertFilesOperation), + RevList: { kind: OperationKind.RevList, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as RevListOperation, SetBranchUpstream: { kind: OperationKind.SetBranchUpstream, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SetBranchUpstreamOperation, Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation, Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ea4d6cbe56f..1065762a3fb 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -19,10 +19,11 @@ import { IFileWatcher, watch } from './watch'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; -import { ActionButtonCommand } from './actionButton'; +import { CommitActionButton } from './actionButton'; import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands'; import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; +import { GitHistoryProvider } from './historyProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -46,6 +47,43 @@ export const enum ResourceGroupType { export class Resource implements SourceControlResourceState { + static getStatusLetter(type: Status): string { + switch (type) { + case Status.INDEX_MODIFIED: + case Status.MODIFIED: + return 'M'; + case Status.INDEX_ADDED: + case Status.INTENT_TO_ADD: + return 'A'; + case Status.INDEX_DELETED: + case Status.DELETED: + return 'D'; + case Status.INDEX_RENAMED: + case Status.INTENT_TO_RENAME: + return 'R'; + case Status.TYPE_CHANGED: + return 'T'; + case Status.UNTRACKED: + return 'U'; + case Status.IGNORED: + return 'I'; + case Status.DELETED_BY_THEM: + return 'D'; + case Status.DELETED_BY_US: + return 'D'; + case Status.INDEX_COPIED: + return 'C'; + case Status.BOTH_DELETED: + case Status.ADDED_BY_US: + case Status.ADDED_BY_THEM: + case Status.BOTH_ADDED: + case Status.BOTH_MODIFIED: + return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows + default: + throw new Error('Unknown git status: ' + type); + } + } + static getStatusText(type: Status) { switch (type) { case Status.INDEX_MODIFIED: return l10n.t('Index Modified'); @@ -71,6 +109,41 @@ export class Resource implements SourceControlResourceState { } } + static getStatusColor(type: Status): ThemeColor { + switch (type) { + case Status.INDEX_MODIFIED: + return new ThemeColor('gitDecoration.stageModifiedResourceForeground'); + case Status.MODIFIED: + case Status.TYPE_CHANGED: + return new ThemeColor('gitDecoration.modifiedResourceForeground'); + case Status.INDEX_DELETED: + return new ThemeColor('gitDecoration.stageDeletedResourceForeground'); + case Status.DELETED: + return new ThemeColor('gitDecoration.deletedResourceForeground'); + case Status.INDEX_ADDED: + case Status.INTENT_TO_ADD: + return new ThemeColor('gitDecoration.addedResourceForeground'); + case Status.INDEX_COPIED: + case Status.INDEX_RENAMED: + case Status.INTENT_TO_RENAME: + return new ThemeColor('gitDecoration.renamedResourceForeground'); + case Status.UNTRACKED: + return new ThemeColor('gitDecoration.untrackedResourceForeground'); + case Status.IGNORED: + return new ThemeColor('gitDecoration.ignoredResourceForeground'); + case Status.BOTH_DELETED: + case Status.ADDED_BY_US: + case Status.DELETED_BY_THEM: + case Status.ADDED_BY_THEM: + case Status.DELETED_BY_US: + case Status.BOTH_ADDED: + case Status.BOTH_MODIFIED: + return new ThemeColor('gitDecoration.conflictingResourceForeground'); + default: + throw new Error('Unknown git status: ' + type); + } + } + @memoize get resourceUri(): Uri { if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED || this._type === Status.INTENT_TO_RENAME)) { @@ -188,75 +261,11 @@ export class Resource implements SourceControlResourceState { } get letter(): string { - switch (this.type) { - case Status.INDEX_MODIFIED: - case Status.MODIFIED: - return 'M'; - case Status.INDEX_ADDED: - case Status.INTENT_TO_ADD: - return 'A'; - case Status.INDEX_DELETED: - case Status.DELETED: - return 'D'; - case Status.INDEX_RENAMED: - case Status.INTENT_TO_RENAME: - return 'R'; - case Status.TYPE_CHANGED: - return 'T'; - case Status.UNTRACKED: - return 'U'; - case Status.IGNORED: - return 'I'; - case Status.DELETED_BY_THEM: - return 'D'; - case Status.DELETED_BY_US: - return 'D'; - case Status.INDEX_COPIED: - return 'C'; - case Status.BOTH_DELETED: - case Status.ADDED_BY_US: - case Status.ADDED_BY_THEM: - case Status.BOTH_ADDED: - case Status.BOTH_MODIFIED: - return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows - default: - throw new Error('Unknown git status: ' + this.type); - } + return Resource.getStatusLetter(this.type); } get color(): ThemeColor { - switch (this.type) { - case Status.INDEX_MODIFIED: - return new ThemeColor('gitDecoration.stageModifiedResourceForeground'); - case Status.MODIFIED: - case Status.TYPE_CHANGED: - return new ThemeColor('gitDecoration.modifiedResourceForeground'); - case Status.INDEX_DELETED: - return new ThemeColor('gitDecoration.stageDeletedResourceForeground'); - case Status.DELETED: - return new ThemeColor('gitDecoration.deletedResourceForeground'); - case Status.INDEX_ADDED: - case Status.INTENT_TO_ADD: - return new ThemeColor('gitDecoration.addedResourceForeground'); - case Status.INDEX_COPIED: - case Status.INDEX_RENAMED: - case Status.INTENT_TO_RENAME: - return new ThemeColor('gitDecoration.renamedResourceForeground'); - case Status.UNTRACKED: - return new ThemeColor('gitDecoration.untrackedResourceForeground'); - case Status.IGNORED: - return new ThemeColor('gitDecoration.ignoredResourceForeground'); - case Status.BOTH_DELETED: - case Status.ADDED_BY_US: - case Status.DELETED_BY_THEM: - case Status.ADDED_BY_THEM: - case Status.DELETED_BY_US: - case Status.BOTH_ADDED: - case Status.BOTH_MODIFIED: - return new ThemeColor('gitDecoration.conflictingResourceForeground'); - default: - throw new Error('Unknown git status: ' + this.type); - } + return Resource.getStatusColor(this.type); } get priority(): number { @@ -833,8 +842,13 @@ export class Repository implements Disposable { const root = Uri.file(repository.root); this._sourceControl = scm.createSourceControl('git', 'Git', root); - this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] }; this._sourceControl.quickDiffProvider = this; + + const historyProvider = new GitHistoryProvider(this); + this._sourceControl.historyProvider = historyProvider; + this.disposables.push(historyProvider); + + this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] }; this._sourceControl.inputBox.validateInput = this.validateInput.bind(this); this.disposables.push(this._sourceControl); @@ -921,10 +935,10 @@ export class Repository implements Disposable { this.commitCommandCenter = new CommitCommandsCenter(globalState, this, postCommitCommandsProviderRegistry); this.disposables.push(this.commitCommandCenter); - const actionButton = new ActionButtonCommand(this, this.commitCommandCenter); - this.disposables.push(actionButton); - actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button); - this._sourceControl.actionButton = actionButton.button; + const commitActionButton = new CommitActionButton(this, this.commitCommandCenter); + this.disposables.push(commitActionButton); + commitActionButton.onDidChange(() => this._sourceControl.actionButton = commitActionButton.button); + this._sourceControl.actionButton = commitActionButton.button; const progressManager = new ProgressManager(this); this.disposables.push(progressManager); @@ -1115,6 +1129,10 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path)); } + diffBetweenShortStat(ref1: string, ref2: string): Promise { + return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2)); + } + getMergeBase(ref1: string, ref2: string): Promise { return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2)); } @@ -1421,6 +1439,10 @@ export class Repository implements Disposable { await this.run(Operation.Move, () => this.repository.move(from, to)); } + async getDefaultBranch(): Promise { + return await this.run(Operation.GetBranch, () => this.repository.getDefaultBranch()); + } + async getBranch(name: string): Promise { return await this.run(Operation.GetBranch, () => this.repository.getBranch(name)); } @@ -1506,6 +1528,10 @@ export class Repository implements Disposable { return await this.repository.getCommit(ref); } + async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { + return await this.run(Operation.RevList, () => this.repository.getCommitCount(range)); + } + async reset(treeish: string, hard?: boolean): Promise { await this.run(Operation.Reset, () => this.repository.reset(treeish, hard)); } diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index c62c25401f2..d5fdbd539da 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -12,6 +12,7 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", + "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", "../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts", "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 0c00aec71c6..6869f9ce506 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -4,6 +4,7 @@ "description": "Extension to add Grunt capabilities to VS Code.", "displayName": "Grunt support for VS Code", "version": "1.0.0", + "private": true, "icon": "images/grunt.png", "license": "MIT", "engines": { diff --git a/extensions/ini/package.json b/extensions/ini/package.json index 0c7dda3c880..ac1a59e8af5 100644 --- a/extensions/ini/package.json +++ b/extensions/ini/package.json @@ -3,6 +3,7 @@ "displayName": "%displayName%", "description": "%description%", "version": "1.0.0", + "private": true, "publisher": "vscode", "license": "MIT", "engines": { diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 2bacd70b76d..624e2c91a13 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -4,6 +4,7 @@ "displayName": "%displayName%", "description": "%description%", "version": "1.0.1", + "private": true, "license": "MIT", "engines": { "vscode": "0.10.x" diff --git a/extensions/shellscript/cgmanifest.json b/extensions/shellscript/cgmanifest.json index f246d45fe21..dbb4301b62c 100644 --- a/extensions/shellscript/cgmanifest.json +++ b/extensions/shellscript/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jeff-hykin/better-shell-syntax", "repositoryUrl": "https://github.com/jeff-hykin/better-shell-syntax", - "commitHash": "ce62ea59e8e522f8a07d8d8a2d1f992c6c600b91" + "commitHash": "a3de7b32f1537194a83ee848838402fbf4b67424" } }, "license": "MIT", diff --git a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json index 68055eb7b29..9950c577c48 100644 --- a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json +++ b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/ce62ea59e8e522f8a07d8d8a2d1f992c6c600b91", + "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/a3de7b32f1537194a83ee848838402fbf4b67424", "name": "Shell Script", "scopeName": "source.shell", "patterns": [ @@ -268,7 +268,7 @@ "patterns": [ { "begin": "(?=\\S)", - "end": ";;", + "end": ";;&?|;&", "endCaptures": { "0": { "name": "punctuation.terminator.case-clause.shell" @@ -311,7 +311,7 @@ }, { "begin": "(?<=\\))", - "end": "(?=;;)", + "end": "(?=;;&?|;&)", "name": "meta.scope.case-clause-body.shell", "patterns": [ { diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 1f6c21a5d88..450ffcb886c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -206,7 +206,7 @@ class DiagnosticsTelemetryManager extends Disposable { this._diagnosticCodesMap.clear(); /* __GDPR__ "typescript.diagnostics" : { - "owner": "@aiday-mar", + "owner": "aiday-mar", "diagnosticCodes" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" @@ -214,7 +214,7 @@ class DiagnosticsTelemetryManager extends Disposable { } */ this._telemetryReporter.logTelemetry('typescript.diagnostics', { - diagnoticCodes: diagnosticCodes + diagnosticCodes: diagnosticCodes }); } }, 5 * 60 * 1000); // 5 minutes diff --git a/package.json b/package.json index 8e9dd05a1b2..1940684eb1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.83.0", - "distro": "c13fa037c20b672a48c9e7990df10998974196f9", + "distro": "5a794f00ac87e37b6abea46ccf030aeb4b323aeb", "author": { "name": "Microsoft Corporation" }, @@ -70,7 +70,7 @@ "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.17.2", + "@vscode/proxy-agent": "^0.17.4", "@vscode/ripgrep": "^1.15.5", "@vscode/spdlog": "^0.13.11", "@vscode/sqlite3": "5.1.6-vscode", @@ -210,7 +210,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.3.0-dev.20230911", + "typescript": "^5.3.0-dev.20230919", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/remote/package.json b/remote/package.json index 50c98b47a48..5da071b0724 100644 --- a/remote/package.json +++ b/remote/package.json @@ -7,7 +7,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.17.2", + "@vscode/proxy-agent": "^0.17.4", "@vscode/ripgrep": "^1.15.5", "@vscode/spdlog": "^0.13.11", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/remote/yarn.lock b/remote/yarn.lock index 1771063a82c..21b77b54043 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -58,10 +58,10 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.17.2": - version "0.17.2" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.17.2.tgz#0e0dac24478e2d71a4fd1b2bb5f84dc61add79e2" - integrity sha512-aKRo1YfUCsgEjHvr2HXfM6dwHhieyO6G+WHly7jewyyTJ1nANWEocS3JRnRbC4KjlajKhSUEOx838cdnY/vRtA== +"@vscode/proxy-agent@^0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.17.4.tgz#e3ffb63357353a428436f15a69de3453a5061f0c" + integrity sha512-tX8eidofoJlZFRWzdiiW3wyu26hgIRk8HvM/RoP1wVSu3U/As36EgGIZYG6pPnqiythRqTcsddniVNA5M39g4w== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index a3430a44354..143a15e1c6c 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -17,6 +17,29 @@ import { FileAccess, RemoteAuthorities, Schemas } from 'vs/base/common/network'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +export const { registerWindow, getWindows, onDidCreateWindow } = (function () { + const windows: Window[] = []; + const onDidCreateWindow = new event.Emitter<{ window: Window; disposableStore: DisposableStore }>(); + return { + onDidCreateWindow: onDidCreateWindow.event, + registerWindow(window: Window): IDisposable { + windows.push(window); + const disposableStore = new DisposableStore(); + disposableStore.add(toDisposable(() => { + const index = windows.indexOf(window); + if (index !== -1) { + windows.splice(index, 1); + } + })); + onDidCreateWindow.fire({ window, disposableStore }); + return disposableStore; + }, + getWindows(): Window[] { + return windows; + } + }; +})(); + export function clearNode(node: HTMLElement): void { while (node.firstChild) { node.firstChild.remove(); @@ -282,34 +305,37 @@ export function addDisposableThrottledListener(node: } export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration { - return document.defaultView!.getComputedStyle(el, null); + return el.ownerDocument.defaultView!.getComputedStyle(el, null); } export function getClientArea(element: HTMLElement): Dimension { + const elDocument = element.ownerDocument; + const elWindow = elDocument.defaultView?.window; + // Try with DOM clientWidth / clientHeight - if (element !== document.body) { + if (element !== elDocument.body) { return new Dimension(element.clientWidth, element.clientHeight); } // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight - if (platform.isIOS && window.visualViewport) { - return new Dimension(window.visualViewport.width, window.visualViewport.height); + if (platform.isIOS && elWindow?.visualViewport) { + return new Dimension(elWindow.visualViewport.width, elWindow.visualViewport.height); } // Try innerWidth / innerHeight - if (window.innerWidth && window.innerHeight) { - return new Dimension(window.innerWidth, window.innerHeight); + if (elWindow?.innerWidth && elWindow.innerHeight) { + return new Dimension(elWindow.innerWidth, elWindow.innerHeight); } // Try with document.body.clientWidth / document.body.clientHeight - if (document.body && document.body.clientWidth && document.body.clientHeight) { - return new Dimension(document.body.clientWidth, document.body.clientHeight); + if (elDocument.body && elDocument.body.clientWidth && elDocument.body.clientHeight) { + return new Dimension(elDocument.body.clientWidth, elDocument.body.clientHeight); } // Try with document.documentElement.clientWidth / document.documentElement.clientHeight - if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientHeight) { - return new Dimension(document.documentElement.clientWidth, document.documentElement.clientHeight); + if (elDocument.documentElement && elDocument.documentElement.clientWidth && elDocument.documentElement.clientHeight) { + return new Dimension(elDocument.documentElement.clientWidth, elDocument.documentElement.clientHeight); } throw new Error('Unable to figure out browser width and height'); @@ -431,8 +457,8 @@ export function getTopLeftOffset(element: HTMLElement): IDomPosition { while ( (element = element.parentNode) !== null - && element !== document.body - && element !== document.documentElement + && element !== element.ownerDocument.body + && element !== element.ownerDocument.documentElement ) { top -= element.scrollTop; const c = isShadowRoot(element) ? null : getComputedStyle(element); @@ -498,8 +524,8 @@ export function position(element: HTMLElement, top: number, right?: number, bott export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition { const bb = domNode.getBoundingClientRect(); return { - left: bb.left + window.scrollX, - top: bb.top + window.scrollY, + left: bb.left + (domNode.ownerDocument.defaultView?.scrollX ?? 0), + top: bb.top + (domNode.ownerDocument.defaultView?.scrollY ?? 0), width: bb.width, height: bb.height }; @@ -518,7 +544,7 @@ export function getDomNodeZoomLevel(domNode: HTMLElement): number { } testElement = testElement.parentElement; - } while (testElement !== null && testElement !== document.documentElement); + } while (testElement !== null && testElement !== testElement.ownerDocument.documentElement); return zoom; } @@ -602,7 +628,7 @@ export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement: function getParentFlowToElement(node: HTMLElement): HTMLElement | null { const flowToParentId = node.dataset[parentFlowToDataKey]; if (typeof flowToParentId === 'string') { - return document.getElementById(flowToParentId); + return node.ownerDocument.getElementById(flowToParentId); } return null; } @@ -671,7 +697,7 @@ export function isInShadowDOM(domNode: Node): boolean { export function getShadowRoot(domNode: Node): ShadowRoot | null { while (domNode.parentNode) { - if (domNode === document.body) { + if (domNode === domNode.ownerDocument?.body) { // reached the body return null; } @@ -680,8 +706,12 @@ export function getShadowRoot(domNode: Node): ShadowRoot | null { return isShadowRoot(domNode) ? domNode : null; } +/** + * Returns the active element across all child windows. + * Use this instead of `document.activeElement` to handle multiple windows. + */ export function getActiveElement(): Element | null { - let result = document.activeElement; + let result = getActiveDocument().activeElement; while (result?.shadowRoot) { result = result.shadowRoot.activeElement; @@ -690,6 +720,15 @@ export function getActiveElement(): Element | null { return result; } +/** + * Returns the active document across all child windows. + * Use this instead of `document` when reacting to dom events to handle multiple windows. + */ +export function getActiveDocument(): Document { + const documents = [document, ...getWindows().map(w => w.document)]; + return documents.find(doc => doc.hasFocus()) ?? document; +} + export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0], beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement { const style = document.createElement('style'); style.type = 'text/css'; @@ -875,15 +914,19 @@ class FocusTracker extends Disposable implements IFocusTracker { private _refreshStateHandler: () => void; - private static hasFocusWithin(element: HTMLElement): boolean { - const shadowRoot = getShadowRoot(element); - const activeElement = (shadowRoot ? shadowRoot.activeElement : document.activeElement); - return isAncestor(activeElement, element); + private static hasFocusWithin(element: HTMLElement | Window): boolean { + if (isHTMLElement(element)) { + const shadowRoot = getShadowRoot(element); + const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement); + return isAncestor(activeElement, element); + } else { + return isAncestor(window.document.activeElement, window.document); + } } constructor(element: HTMLElement | Window) { super(); - let hasFocus = FocusTracker.hasFocusWithin(element); + let hasFocus = FocusTracker.hasFocusWithin(element); let loosingFocus = false; const onFocus = () => { @@ -1092,7 +1135,7 @@ export function removeTabIndexAndUpdateFocus(node: HTMLElement): void { // standard DOM behavior is to move focus to the element. We // typically never want that, rather put focus to the closest element // in the hierarchy of the parent DOM nodes. - if (document.activeElement === node) { + if (node.ownerDocument.activeElement === node) { const parentFocusable = findParentWithAttribute(node.parentElement, 'tabIndex'); parentFocusable?.focus(); } diff --git a/src/vs/base/browser/mouseEvent.ts b/src/vs/base/browser/mouseEvent.ts index db8ba364a86..bdfbf2ed661 100644 --- a/src/vs/base/browser/mouseEvent.ts +++ b/src/vs/base/browser/mouseEvent.ts @@ -69,8 +69,8 @@ export class StandardMouseEvent implements IMouseEvent { this.posy = e.pageY; } else { // Probably hit by MSGestureEvent - this.posx = e.clientX + document.body.scrollLeft + document.documentElement!.scrollLeft; - this.posy = e.clientY + document.body.scrollTop + document.documentElement!.scrollTop; + this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft; + this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop; } // Find the position of the iframe this code is executing in relative to the iframe where the event was captured. diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index bdc630dbcf7..f73e7a8ce7a 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -778,7 +778,7 @@ class LeafNode implements ISplitView, IDisposable { private _onDidViewChange: Event; readonly onDidChange: Event; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); constructor( readonly view: IView, diff --git a/src/vs/base/browser/ui/icons/iconSelectBox.css b/src/vs/base/browser/ui/icons/iconSelectBox.css new file mode 100644 index 00000000000..4e2278d04bc --- /dev/null +++ b/src/vs/base/browser/ui/icons/iconSelectBox.css @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.icon-select-box > .icon-select-box-container { + height: 100%; +} + +.icon-select-box .icon-select-icons-container { + height: 100%; + outline: 0 !important; +} + +.icon-select-box .icon-select-icons-container > .icon-container { + display: inline-flex; + cursor: pointer; + font-size: 20px; + align-items: center; + justify-content: center; + border-radius: 5px; +} + +.icon-select-box .icon-select-icons-container > .icon-container.focused { + outline: 1px dashed var(--vscode-toolbar-hoverOutline); + outline-offset: -1px; + background-color: var(--vscode-toolbar-hoverBackground); +} + +.icon-select-box .icon-select-id-container .icon-select-id-label { + height: 24px; + padding: 10px; + opacity: .8; +} + +.icon-select-box .icon-select-id-container .icon-select-id-label .highlight { + color: var(--vscode-list-highlightForeground); +} diff --git a/src/vs/base/browser/ui/icons/iconSelectBox.ts b/src/vs/base/browser/ui/icons/iconSelectBox.ts new file mode 100644 index 00000000000..c592c37d3f7 --- /dev/null +++ b/src/vs/base/browser/ui/icons/iconSelectBox.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./iconSelectBox'; +import * as dom from 'vs/base/browser/dom'; +import { alert } from 'vs/base/browser/ui/aria/aria'; +import { IInputBoxStyles, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IMatch } from 'vs/base/common/filters'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; + +export interface IIconSelectBoxOptions { + readonly icons: ThemeIcon[]; + readonly inputBoxStyles: IInputBoxStyles; +} + +interface IRenderedIconItem { + readonly icon: ThemeIcon; + readonly element: HTMLElement; + readonly highlightMatches?: IMatch[]; +} + +export class IconSelectBox extends Disposable { + + private static InstanceCount = 0; + readonly domId = `icon_select_box_id_${++IconSelectBox.InstanceCount}`; + + readonly domNode: HTMLElement; + + private _onDidSelect = this._register(new Emitter()); + readonly onDidSelect = this._onDidSelect.event; + + private renderedIcons: IRenderedIconItem[] = []; + + private focusedItemIndex: number = 0; + private numberOfElementsPerRow: number = 1; + + protected inputBox: InputBox | undefined; + private scrollableElement: DomScrollableElement | undefined; + private iconIdElement: HighlightedLabel | undefined; + private readonly iconContainerWidth = 36; + private readonly iconContainerHeight = 32; + + constructor( + private readonly options: IIconSelectBoxOptions, + ) { + super(); + this.domNode = dom.$('.icon-select-box'); + this._register(this.create()); + } + + private create(): IDisposable { + const disposables = new DisposableStore(); + + const iconSelectBoxContainer = dom.append(this.domNode, dom.$('.icon-select-box-container')); + iconSelectBoxContainer.style.margin = '10px 15px'; + + const iconSelectInputContainer = dom.append(iconSelectBoxContainer, dom.$('.icon-select-input-container')); + iconSelectInputContainer.style.paddingBottom = '10px'; + this.inputBox = disposables.add(new InputBox(iconSelectInputContainer, undefined, { + placeholder: localize('iconSelect.placeholder', "Search icons"), + inputBoxStyles: this.options.inputBoxStyles, + })); + + const iconsContainer = dom.$('.icon-select-icons-container', { id: `${this.domId}_icons` }); + iconsContainer.style.paddingRight = '10px'; + iconsContainer.role = 'listbox'; + iconsContainer.tabIndex = 0; + this.scrollableElement = disposables.add(new DomScrollableElement(iconsContainer, { + useShadows: false, + horizontal: ScrollbarVisibility.Hidden, + })); + dom.append(iconSelectBoxContainer, this.scrollableElement.getDomNode()); + this.iconIdElement = new HighlightedLabel(dom.append(dom.append(iconSelectBoxContainer, dom.$('.icon-select-id-container')), dom.$('.icon-select-id-label'))); + + const iconsDisposables = disposables.add(new MutableDisposable()); + iconsDisposables.value = this.renderIcons(this.options.icons, [], iconsContainer); + this.scrollableElement.scanDomNode(); + + disposables.add(this.inputBox.onDidChange(value => { + const icons = [], matches = []; + for (const icon of this.options.icons) { + const match = this.matchesContiguous(value, icon.id); + if (match) { + icons.push(icon); + matches.push(match); + } + } + iconsDisposables.value = this.renderIcons(icons, matches, iconsContainer); + this.scrollableElement?.scanDomNode(); + })); + + this.inputBox.inputElement.role = 'combobox'; + this.inputBox.inputElement.ariaHasPopup = 'menu'; + this.inputBox.inputElement.ariaAutoComplete = 'list'; + this.inputBox.inputElement.ariaExpanded = 'true'; + this.inputBox.inputElement.setAttribute('aria-controls', iconsContainer.id); + + return disposables; + } + + private renderIcons(icons: ThemeIcon[], matches: IMatch[][], container: HTMLElement): IDisposable { + const disposables = new DisposableStore(); + dom.clearNode(container); + const focusedIcon = this.renderedIcons[this.focusedItemIndex]?.icon; + let focusedIconIndex = 0; + const renderedIcons: IRenderedIconItem[] = []; + if (icons.length) { + for (let index = 0; index < icons.length; index++) { + const icon = icons[index]; + const iconContainer = dom.append(container, dom.$('.icon-container', { id: `${this.domId}_icons_${index}` })); + iconContainer.style.width = `${this.iconContainerWidth}px`; + iconContainer.style.height = `${this.iconContainerHeight}px`; + iconContainer.title = icon.id; + iconContainer.role = 'button'; + iconContainer.setAttribute('aria-setsize', `${icons.length}`); + iconContainer.setAttribute('aria-posinset', `${index + 1}`); + dom.append(iconContainer, dom.$(ThemeIcon.asCSSSelector(icon))); + renderedIcons.push({ icon, element: iconContainer, highlightMatches: matches[index] }); + + disposables.add(dom.addDisposableListener(iconContainer, dom.EventType.CLICK, (e: MouseEvent) => { + e.stopPropagation(); + this.setSelection(index); + })); + + disposables.add(dom.addDisposableListener(iconContainer, dom.EventType.MOUSE_OVER, (e: MouseEvent) => { + this.focusIcon(index); + })); + + if (icon === focusedIcon) { + focusedIconIndex = index; + } + } + } else { + const noResults = localize('iconSelect.noResults', "No results"); + dom.append(container, dom.$('.icon-no-results', undefined, noResults)); + alert(noResults); + } + + this.renderedIcons.splice(0, this.renderedIcons.length, ...renderedIcons); + this.focusIcon(focusedIconIndex); + + return disposables; + } + + private focusIcon(index: number): void { + const existing = this.renderedIcons[this.focusedItemIndex]; + if (existing) { + existing.element.classList.remove('focused'); + } + + this.focusedItemIndex = index; + const renderedItem = this.renderedIcons[index]; + + if (renderedItem) { + renderedItem.element.classList.add('focused'); + } + + if (this.inputBox) { + if (renderedItem) { + this.inputBox.inputElement.setAttribute('aria-activedescendant', renderedItem.element.id); + } else { + this.inputBox.inputElement.removeAttribute('aria-activedescendant'); + } + } + + if (this.iconIdElement) { + if (renderedItem) { + this.iconIdElement.set(renderedItem.icon.id, renderedItem.highlightMatches); + } else { + this.iconIdElement.set(''); + } + } + + this.reveal(index); + } + + private reveal(index: number): void { + if (!this.scrollableElement) { + return; + } + if (index < 0 || index >= this.renderedIcons.length) { + return; + } + const element = this.renderedIcons[index].element; + if (!element) { + return; + } + const { height } = this.scrollableElement.getScrollDimensions(); + const { scrollTop } = this.scrollableElement.getScrollPosition(); + if (element.offsetTop + this.iconContainerHeight > scrollTop + height) { + this.scrollableElement.setScrollPosition({ scrollTop: element.offsetTop + this.iconContainerHeight - height }); + } else if (element.offsetTop < scrollTop) { + this.scrollableElement.setScrollPosition({ scrollTop: element.offsetTop }); + } + } + + private matchesContiguous(word: string, wordToMatchAgainst: string): IMatch[] | null { + const matchIndex = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase()); + if (matchIndex !== -1) { + return [{ start: matchIndex, end: matchIndex + word.length }]; + } + return null; + } + + layout(dimension: dom.Dimension): void { + this.domNode.style.width = `${dimension.width}px`; + this.domNode.style.height = `${dimension.height}px`; + + const iconsContainerWidth = dimension.width - 40; + this.numberOfElementsPerRow = Math.floor(iconsContainerWidth / this.iconContainerWidth); + if (this.numberOfElementsPerRow === 0) { + throw new Error('Insufficient width'); + } + + const extraSpace = iconsContainerWidth % this.iconContainerWidth; + const margin = Math.floor(extraSpace / this.numberOfElementsPerRow); + for (const { element } of this.renderedIcons) { + element.style.marginRight = `${margin}px`; + } + + if (this.scrollableElement) { + this.scrollableElement.getDomNode().style.height = `${dimension.height - 80}px`; + this.scrollableElement.scanDomNode(); + } + } + + getFocus(): number[] { + return [this.focusedItemIndex]; + } + + setSelection(index: number): void { + if (index < 0 || index >= this.renderedIcons.length) { + throw new Error(`Invalid index ${index}`); + } + this.focusIcon(index); + this._onDidSelect.fire(this.renderedIcons[index].icon); + } + + focus(): void { + this.inputBox?.focus(); + this.focusIcon(0); + } + + focusNext(): void { + this.focusIcon((this.focusedItemIndex + 1) % this.renderedIcons.length); + } + + focusPrevious(): void { + this.focusIcon((this.focusedItemIndex - 1 + this.renderedIcons.length) % this.renderedIcons.length); + } + + focusNextRow(): void { + let nextRowIndex = this.focusedItemIndex + this.numberOfElementsPerRow; + if (nextRowIndex >= this.renderedIcons.length) { + nextRowIndex = (nextRowIndex + 1) % this.numberOfElementsPerRow; + nextRowIndex = nextRowIndex >= this.renderedIcons.length ? 0 : nextRowIndex; + } + this.focusIcon(nextRowIndex); + } + + focusPreviousRow(): void { + let previousRowIndex = this.focusedItemIndex - this.numberOfElementsPerRow; + if (previousRowIndex < 0) { + const numberOfRows = Math.floor(this.renderedIcons.length / this.numberOfElementsPerRow); + previousRowIndex = this.focusedItemIndex + (this.numberOfElementsPerRow * numberOfRows) - 1; + previousRowIndex = previousRowIndex < 0 + ? this.renderedIcons.length - 1 + : previousRowIndex >= this.renderedIcons.length + ? previousRowIndex - this.numberOfElementsPerRow + : previousRowIndex; + } + this.focusIcon(previousRowIndex); + } + + getFocusedIcon(): ThemeIcon { + return this.renderedIcons[this.focusedItemIndex].icon; + } + +} diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index b82b4508211..a69b863f21d 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -173,7 +173,7 @@ interface IPointerEventFactory { class MouseEventFactory implements IPointerEventFactory { - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); @memoize get onPointerMove(): Event { @@ -192,7 +192,7 @@ class MouseEventFactory implements IPointerEventFactory { class GestureEventFactory implements IPointerEventFactory { - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); @memoize get onPointerMove(): Event { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 5376fe98462..07a22c43e91 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1027,7 +1027,7 @@ class FindController implements IDisposable { private readonly _onDidChangeOpenState = new Emitter(); readonly onDidChangeOpenState = this._onDidChangeOpenState.event; - private enabledDisposables = new DisposableStore(); + private readonly enabledDisposables = new DisposableStore(); private readonly disposables = new DisposableStore(); constructor( @@ -1085,8 +1085,7 @@ class FindController implements IDisposable { this._history = this.widget.getHistory(); this.widget = undefined; - this.enabledDisposables.dispose(); - this.enabledDisposables = new DisposableStore(); + this.enabledDisposables.clear(); this.previousPattern = this.pattern; this.onDidChangeValue(''); diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 35b0620e7b2..bd0f90aa878 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -187,6 +187,12 @@ export function forEachAdjacent(arr: T[], f: (item1: T | undefined, item2: T } } +export function forEachWithNeighbors(arr: T[], f: (before: T | undefined, element: T, after: T | undefined) => void): void { + for (let i = 0; i < arr.length; i++) { + f(i === 0 ? undefined : arr[i - 1], arr[i], i + 1 === arr.length ? undefined : arr[i + 1]); + } +} + interface IMutableSplice extends ISplice { readonly toInsert: T[]; deleteCount: number; diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 0c5fd5a01e8..f45ae37d78f 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -324,7 +324,7 @@ export const Codicon = { note: register('note', 0xeb26), octoface: register('octoface', 0xeb27), openPreview: register('open-preview', 0xeb28), - package_: register('package', 0xeb29), + package: register('package', 0xeb29), paintcan: register('paintcan', 0xeb2a), pin: register('pin', 0xeb2b), play: register('play', 0xeb2c), diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index 22c2f27a678..de578810355 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -120,14 +120,15 @@ export class HistoryNavigator2 { private head: HistoryNode; private tail: HistoryNode; private cursor: HistoryNode; - private size: number; + private _size: number; + get size(): number { return this._size; } constructor(history: readonly T[], private capacity: number = 10) { if (history.length < 1) { throw new Error('not supported'); } - this.size = 1; + this._size = 1; this.head = this.tail = this.cursor = { value: history[0], previous: undefined, @@ -150,7 +151,7 @@ export class HistoryNavigator2 { this.tail.next = node; this.tail = node; this.cursor = this.tail; - this.size++; + this._size++; if (this.valueSet.has(value)) { this._deleteFromList(value); @@ -158,12 +159,12 @@ export class HistoryNavigator2 { this.valueSet.add(value); } - while (this.size > this.capacity) { + while (this._size > this.capacity) { this.valueSet.delete(this.head.value); this.head = this.head.next!; this.head.previous = undefined; - this.size--; + this._size--; } } @@ -188,6 +189,24 @@ export class HistoryNavigator2 { return oldValue; } + prepend(value: T): void { + if (this._size === this.capacity || this.valueSet.has(value)) { + return; + } + + const node: HistoryNode = { + value, + previous: undefined, + next: this.head + }; + + this.head.previous = node; + this.head = node; + this._size++; + + this.valueSet.add(value); + } + isAtEnd(): boolean { return this.cursor === this.tail; } @@ -243,7 +262,7 @@ export class HistoryNavigator2 { temp.next!.previous = temp.previous; } - this.size--; + this._size--; } temp = temp.next!; diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index f06dcf7acbc..c05887f9a70 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -30,6 +30,12 @@ export namespace Iterable { return iterable || _empty; } + export function* reverse(array: Array): Iterable { + for (let i = array.length - 1; i >= 0; i--) { + yield array[i]; + } + } + export function isEmpty(iterable: Iterable | undefined | null): boolean { return !iterable || iterable[Symbol.iterator]().next().done === true; } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 6f85a3111e3..74fd108e399 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -41,6 +41,10 @@ class ResourceMapEntry { constructor(readonly uri: URI, readonly value: T) { } } +function isEntries(arg: ResourceMap | ResourceMapKeyFn | readonly (readonly [URI, T])[] | undefined): arg is readonly (readonly [URI, T])[] { + return Array.isArray(arg); +} + export class ResourceMap implements Map { private static readonly defaultToKey = (resource: URI) => resource.toString(); @@ -63,13 +67,27 @@ export class ResourceMap implements Map { */ constructor(other?: ResourceMap, toKey?: ResourceMapKeyFn); - constructor(mapOrKeyFn?: ResourceMap | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) { - if (mapOrKeyFn instanceof ResourceMap) { - this.map = new Map(mapOrKeyFn.map); + /** + * + * @param other Another resource which this maps is created from + * @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util + */ + constructor(entries?: readonly (readonly [URI, T])[], toKey?: ResourceMapKeyFn); + + constructor(arg?: ResourceMap | ResourceMapKeyFn | readonly (readonly [URI, T])[], toKey?: ResourceMapKeyFn) { + if (arg instanceof ResourceMap) { + this.map = new Map(arg.map); this.toKey = toKey ?? ResourceMap.defaultToKey; + } else if (isEntries(arg)) { + this.map = new Map(); + this.toKey = toKey ?? ResourceMap.defaultToKey; + + for (const [resource, value] of arg) { + this.set(resource, value); + } } else { this.map = new Map(); - this.toKey = mapOrKeyFn ?? ResourceMap.defaultToKey; + this.toKey = arg ?? ResourceMap.defaultToKey; } } diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index ae02b8901fc..de454cf7e73 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - export const enum MarshalledId { Uri = 1, Regexp, @@ -19,6 +18,7 @@ export const enum MarshalledId { TimelineActionContext, NotebookCellActionContext, NotebookActionContext, + TerminalContext, TestItemContext, Date, TestMessageMenuArgs, diff --git a/src/vs/base/common/tfIdf.ts b/src/vs/base/common/tfIdf.ts new file mode 100644 index 00000000000..993f41b5a78 --- /dev/null +++ b/src/vs/base/common/tfIdf.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; + +type SparseEmbedding = Record; +type TermFrequencies = Map; +type DocumentOccurrences = Map; + +function countMapFrom(values: Iterable): Map { + const map = new Map(); + for (const value of values) { + map.set(value, (map.get(value) ?? 0) + 1); + } + return map; +} + +interface DocumentChunkEntry { + readonly text: string; + readonly tf: TermFrequencies; +} + +export interface TfIdfDocument { + readonly key: string; + readonly textChunks: readonly string[]; +} + +export interface TfIdfScore { + readonly key: string; + /** + * An unbounded number. + */ + readonly score: number; +} + +export interface NormalizedTfIdfScore { + readonly key: string; + /** + * A number between 0 and 1. + */ + readonly score: number; +} + +/** + * Implementation of tf-idf (term frequency-inverse document frequency) for a set of + * documents where each document contains one or more chunks of text. + * Each document is identified by a key, and the score for each document is computed + * by taking the max score over all the chunks in the document. + */ +export class TfIdfCalculator { + calculateScores(query: string, token: CancellationToken): TfIdfScore[] { + const embedding = this.computeEmbedding(query); + const idfCache = new Map(); + const scores: TfIdfScore[] = []; + // For each document, generate one score + for (const [key, doc] of this.documents) { + if (token.isCancellationRequested) { + return []; + } + + for (const chunk of doc.chunks) { + const score = this.computeSimilarityScore(chunk, embedding, idfCache); + if (score > 0) { + scores.push({ key, score }); + } + } + } + + return scores; + } + + /** + * Count how many times each term (word) appears in a string. + */ + private static termFrequencies(input: string): TermFrequencies { + return countMapFrom(TfIdfCalculator.splitTerms(input)); + } + + /** + * Break a string into terms (words). + */ + private static *splitTerms(input: string): Iterable { + const normalize = (word: string) => word.toLowerCase(); + + // Only match on words that are at least 3 characters long and start with a letter + for (const [word] of input.matchAll(/\b\p{Letter}[\p{Letter}\d]{2,}\b/gu)) { + yield normalize(word); + + // eslint-disable-next-line local/code-no-look-behind-regex + const camelParts = word.split(/(?<=[a-z])(?=[A-Z])/g); + if (camelParts.length > 1) { + for (const part of camelParts) { + // Require at least 3 letters in the parts of a camel case word + if (part.length > 2 && /\p{Letter}{3,}/gu.test(part)) { + yield normalize(part); + } + } + } + } + } + + /** + * Total number of chunks + */ + private chunkCount = 0; + + private readonly chunkOccurrences: DocumentOccurrences = new Map(); + + private readonly documents = new Map; + }>(); + + updateDocuments(documents: ReadonlyArray): this { + for (const { key } of documents) { + this.deleteDocument(key); + } + + for (const doc of documents) { + const chunks: Array<{ text: string; tf: TermFrequencies }> = []; + for (const text of doc.textChunks) { + // TODO: See if we can compute the tf lazily + // The challenge is that we need to also update the `chunkOccurrences` + // and all of those updates need to get flushed before the real TF-IDF of + // anything is computed. + const tf = TfIdfCalculator.termFrequencies(text); + + // Update occurrences list + for (const term of tf.keys()) { + this.chunkOccurrences.set(term, (this.chunkOccurrences.get(term) ?? 0) + 1); + } + + chunks.push({ text, tf }); + } + + this.chunkCount += chunks.length; + this.documents.set(doc.key, { chunks }); + } + return this; + } + + deleteDocument(key: string) { + const doc = this.documents.get(key); + if (!doc) { + return; + } + + this.documents.delete(key); + this.chunkCount -= doc.chunks.length; + + // Update term occurrences for the document + for (const chunk of doc.chunks) { + for (const term of chunk.tf.keys()) { + const currentOccurrences = this.chunkOccurrences.get(term); + if (typeof currentOccurrences === 'number') { + const newOccurrences = currentOccurrences - 1; + if (newOccurrences <= 0) { + this.chunkOccurrences.delete(term); + } else { + this.chunkOccurrences.set(term, newOccurrences); + } + } + } + } + } + + private computeSimilarityScore(chunk: DocumentChunkEntry, queryEmbedding: SparseEmbedding, idfCache: Map): number { + // Compute the dot product between the chunk's embedding and the query embedding + + // Note that the chunk embedding is computed lazily on a per-term basis. + // This lets us skip a large number of calculations because the majority + // of chunks do not share any terms with the query. + + let sum = 0; + for (const [term, termTfidf] of Object.entries(queryEmbedding)) { + const chunkTf = chunk.tf.get(term); + if (!chunkTf) { + // Term does not appear in chunk so it has no contribution + continue; + } + + let chunkIdf = idfCache.get(term); + if (typeof chunkIdf !== 'number') { + chunkIdf = this.computeIdf(term); + idfCache.set(term, chunkIdf); + } + + const chunkTfidf = chunkTf * chunkIdf; + sum += chunkTfidf * termTfidf; + } + return sum; + } + + private computeEmbedding(input: string): SparseEmbedding { + const tf = TfIdfCalculator.termFrequencies(input); + return this.computeTfidf(tf); + } + + private computeIdf(term: string): number { + const chunkOccurrences = this.chunkOccurrences.get(term) ?? 0; + return chunkOccurrences > 0 + ? Math.log((this.chunkCount + 1) / chunkOccurrences) + : 0; + } + + private computeTfidf(termFrequencies: TermFrequencies): SparseEmbedding { + const embedding = Object.create(null); + for (const [word, occurrences] of termFrequencies) { + const idf = this.computeIdf(word); + if (idf > 0) { + embedding[word] = occurrences * idf; + } + } + return embedding; + } +} + +/** + * Normalize the scores to be between 0 and 1 and sort them decending. + * @param scores array of scores from {@link TfIdfCalculator.calculateScores} + * @returns normalized scores + */ +export function normalizeTfIdfScores(scores: TfIdfScore[]): NormalizedTfIdfScore[] { + + // copy of scores + const result = scores.slice(0) as { score: number }[]; + + // sort descending + result.sort((a, b) => b.score - a.score); + + // normalize + const max = result[0]?.score ?? 0; + if (max > 0) { + for (const score of result) { + score.score /= max; + } + } + + return result as TfIdfScore[]; +} diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index d35246667bc..529d5e4e7be 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -222,3 +222,8 @@ export type OmitOptional = { [K in keyof T as T[K] extends Required[K] ? K export type Mutable = { -readonly [P in keyof T]: T[P] }; + +/** + * A single object or an array of the objects. + */ +export type SingleOrMany = T | T[]; diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index db61aed4ff1..38d7e585713 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -798,7 +798,7 @@ export class IPCServer implements IChannelServer, I private readonly _onDidRemoveConnection = new Emitter>(); readonly onDidRemoveConnection: Event> = this._onDidRemoveConnection.event; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); get connections(): Connection[] { const result: Connection[] = []; diff --git a/src/vs/base/parts/ipc/test/common/ipc.test.ts b/src/vs/base/parts/ipc/test/common/ipc.test.ts index 35184c08217..0515d24a49c 100644 --- a/src/vs/base/parts/ipc/test/common/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/common/ipc.test.ts @@ -113,7 +113,7 @@ interface ITestService { class TestService implements ITestService { - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private readonly _onPong = new Emitter(); readonly onPong = this._onPong.event; diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 03417dea597..86dfd034418 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -102,7 +102,7 @@ const NullAccessor = new NullAccessorClass(); suite('Fuzzy Scorer', () => { test('score (fuzzy)', function () { - const target = 'HeLlo-World'; + const target = 'HelLo-World'; const scores: FuzzyScore[] = []; scores.push(_doScore(target, 'HelLo-World', true)); // direct case match @@ -134,7 +134,7 @@ suite('Fuzzy Scorer', () => { }); test('score (non fuzzy)', function () { - const target = 'HeLlo-World'; + const target = 'HelLo-World'; assert.ok(_doScore(target, 'HelLo-World', false)[0] > 0); assert.strictEqual(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); @@ -1161,10 +1161,10 @@ suite('Fuzzy Scorer', () => { }); test('fuzzyScore2 (matching)', function () { - const target = 'HeLlo-World'; + const target = 'HelLo-World'; for (const offset of [0, 3]) { - let [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HeLlo-World', offset); + let [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HelLo-World', offset); assert.ok(score); assert.strictEqual(matches.length, 1); @@ -1183,7 +1183,7 @@ suite('Fuzzy Scorer', () => { }); test('fuzzyScore2 (multiple queries)', function () { - const target = 'HeLlo-World'; + const target = 'HelLo-World'; const [firstSingleScore, firstSingleMatches] = _doScore2(target, 'HelLo'); const [secondSingleScore, secondSingleMatches] = _doScore2(target, 'World'); diff --git a/src/vs/base/test/common/history.test.ts b/src/vs/base/test/common/history.test.ts index c5cacb494a7..eae79bb5afa 100644 --- a/src/vs/base/test/common/history.test.ts +++ b/src/vs/base/test/common/history.test.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { HistoryNavigator, HistoryNavigator2 } from 'vs/base/common/history'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('History Navigator', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('create reduces the input to limit', () => { const testObject = new HistoryNavigator(['1', '2', '3', '4'], 2); @@ -269,4 +272,21 @@ suite('History Navigator 2', () => { assert.strictEqual(testObject.previous(), '1'); }); + test('prepend', () => { + const testObject = new HistoryNavigator2(['1', '2', '3', '4']); + assert.strictEqual(testObject.current(), '4'); + assert.ok(testObject.isAtEnd()); + assert.deepStrictEqual(Array.from(testObject), ['1', '2', '3', '4']); + + testObject.prepend('0'); + assert.strictEqual(testObject.current(), '4'); + assert.ok(testObject.isAtEnd()); + assert.deepStrictEqual(Array.from(testObject), ['0', '1', '2', '3', '4']); + + testObject.prepend('2'); + assert.strictEqual(testObject.current(), '4'); + assert.ok(testObject.isAtEnd()); + assert.deepStrictEqual(Array.from(testObject), ['0', '1', '2', '3', '4']); + }); + }); diff --git a/src/vs/base/test/common/tfIdf.test.ts b/src/vs/base/test/common/tfIdf.test.ts new file mode 100644 index 00000000000..69c094759eb --- /dev/null +++ b/src/vs/base/test/common/tfIdf.test.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { TfIdfCalculator, TfIdfDocument, TfIdfScore } from 'vs/base/common/tfIdf'; + +/** + * Generates all permutations of an array. + * + * This is useful for testing to make sure order does not effect the result. + */ +function permutate(arr: T[]): T[][] { + if (arr.length === 0) { + return [[]]; + } + + const result: T[][] = []; + + for (let i = 0; i < arr.length; i++) { + const rest = [...arr.slice(0, i), ...arr.slice(i + 1)]; + const permutationsRest = permutate(rest); + for (let j = 0; j < permutationsRest.length; j++) { + result.push([arr[i], ...permutationsRest[j]]); + } + } + + return result; +} + +function assertScoreOrdersEqual(actualScores: TfIdfScore[], expectedScoreKeys: string[]): void { + actualScores.sort((a, b) => (b.score - a.score) || a.key.localeCompare(b.key)); + assert.strictEqual(actualScores.length, expectedScoreKeys.length); + for (let i = 0; i < expectedScoreKeys.length; i++) { + assert.strictEqual(actualScores[i].key, expectedScoreKeys[i]); + } +} + +suite('TF-IDF Calculator', function () { + test('Should return no scores when no documents are given', () => { + const tfidf = new TfIdfCalculator(); + const scores = tfidf.calculateScores('something', CancellationToken.None); + assertScoreOrdersEqual(scores, []); + }); + + test('Should return no scores for term not in document', () => { + const tfidf = new TfIdfCalculator().updateDocuments([ + makeDocument('A', 'cat dog fish'), + ]); + const scores = tfidf.calculateScores('elepant', CancellationToken.None); + assertScoreOrdersEqual(scores, []); + }); + + test('Should return scores for document with exact match', () => { + for (const docs of permutate([ + makeDocument('A', 'cat dog cat'), + makeDocument('B', 'cat fish'), + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('dog', CancellationToken.None); + assertScoreOrdersEqual(scores, ['A']); + } + }); + + test('Should return document with more matches first', () => { + for (const docs of permutate([ + makeDocument('/A', 'cat dog cat'), + makeDocument('/B', 'cat fish'), + makeDocument('/C', 'frog'), + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/B']); + } + }); + + test('Should return document with more matches first when term appears in all documents', () => { + for (const docs of permutate([ + makeDocument('/A', 'cat dog cat cat'), + makeDocument('/B', 'cat fish'), + makeDocument('/C', 'frog cat cat'), + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/C', '/B']); + } + }); + + test('Should weigh less common term higher', () => { + for (const docs of permutate([ + makeDocument('/A', 'cat dog cat'), + makeDocument('/B', 'fish'), + makeDocument('/C', 'cat cat cat cat'), + makeDocument('/D', 'cat fish') + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('cat the dog', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/C', '/D']); + } + }); + + test('Should weigh chunks with less common terms higher', () => { + for (const docs of permutate([ + makeDocument('/A', ['cat dog cat', 'fish']), + makeDocument('/B', ['cat cat cat cat dog', 'dog']) + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/B', '/A']); + } + + for (const docs of permutate([ + makeDocument('/A', ['cat dog cat', 'fish']), + makeDocument('/B', ['cat cat cat cat dog', 'dog']) + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('dog', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/B', '/B']); + } + + for (const docs of permutate([ + makeDocument('/A', ['cat dog cat', 'fish']), + makeDocument('/B', ['cat cat cat cat dog', 'dog']) + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('cat the dog', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/B', '/A', '/B']); + } + + for (const docs of permutate([ + makeDocument('/A', ['cat dog cat', 'fish']), + makeDocument('/B', ['cat cat cat cat dog', 'dog']) + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('lake fish', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A']); + } + }); + + test('Should ignore case and punctuation', () => { + for (const docs of permutate([ + makeDocument('/A', 'Cat doG.cat'), + makeDocument('/B', 'cAt fiSH'), + makeDocument('/C', 'frOg'), + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('. ,CaT! ', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/B']); + } + }); + + test('Should match on camelCase words', () => { + for (const docs of permutate([ + makeDocument('/A', 'catDog cat'), + makeDocument('/B', 'fishCatFish'), + makeDocument('/C', 'frogcat'), + ])) { + const tfidf = new TfIdfCalculator().updateDocuments(docs); + const scores = tfidf.calculateScores('catDOG', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/B']); + } + }); + + test('Should not match document after delete', () => { + const docA = makeDocument('/A', 'cat dog cat'); + const docB = makeDocument('/B', 'cat fish'); + const docC = makeDocument('/C', 'frog'); + + const tfidf = new TfIdfCalculator().updateDocuments([docA, docB, docC]); + let scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/A', '/B']); + + tfidf.deleteDocument(docA.key); + scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/B']); + + tfidf.deleteDocument(docC.key); + scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, ['/B']); + + tfidf.deleteDocument(docB.key); + scores = tfidf.calculateScores('cat', CancellationToken.None); + assertScoreOrdersEqual(scores, []); + }); +}); + +function makeDocument(key: string, content: string | string[]): TfIdfDocument { + return { + key, + textChunks: Array.isArray(content) ? content : [content], + }; +} diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 5307987e657..06ecbdd6396 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -45,7 +45,7 @@ import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/commo import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter'; import { ExtensionHostStarter } from 'vs/platform/extensions/electron-main/extensionHostStarter'; -import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/electron-main/externalTerminal'; import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; import { LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 89fcc9d0e75..e095b3d6d03 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -1052,10 +1052,11 @@ export class IssueReporter extends Disposable { }, extension.name); }; - const extensionsSelector = this.getElementById('extension-selector'); + const extensionsSelector = this.getElementById('extension-selector'); if (extensionsSelector) { const { selectedExtension } = this.issueReporterModel.getData(); - reset(extensionsSelector, $('option'), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + extensionsSelector.selectedIndex = 0; this.addEventListener('extension-selector', 'change', (e: Event) => { const selectedExtensionId = (e.target).value; diff --git a/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts b/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts index 11299d0848e..6f29b8d3a23 100644 --- a/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts +++ b/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts @@ -9,10 +9,12 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IVoiceRecognitionService } from 'vs/platform/voiceRecognition/node/voiceRecognitionService'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { LimitedQueue } from 'vs/base/common/async'; +import { LimitedQueue, Queue } from 'vs/base/common/async'; export class VoiceTranscriptionManager extends Disposable { + private static USE_SLIDING_WINDOW = !!process.env.VSCODE_VOICE_USE_SLIDING_WINDOW; + constructor( private readonly onDidWindowConnectRaw: Event, @IVoiceRecognitionService private readonly voiceRecognitionService: IVoiceRecognitionService, @@ -25,23 +27,25 @@ export class VoiceTranscriptionManager extends Disposable { private registerListeners(): void { this._register(this.onDidWindowConnectRaw(port => { - this._register(new VoiceTranscriber(port, this.voiceRecognitionService, this.logService)); + this.logService.info(`[voice] transcriber: new connection (sliding window: ${VoiceTranscriptionManager.USE_SLIDING_WINDOW})`); + + if (VoiceTranscriptionManager.USE_SLIDING_WINDOW) { + this._register(new SlidingWindowVoiceTranscriber(port, this.voiceRecognitionService, this.logService)); + } else { + this._register(new FullWindowVoiceTranscriber(port, this.voiceRecognitionService, this.logService)); + } })); } } -class VoiceTranscriber extends Disposable { +abstract class VoiceTranscriber extends Disposable { - private static MAX_DATA_LENGTH = 30 /* seconds */ * 16000 /* sampling rate */ * 16 /* bith depth */ * 1 /* channels */ / 8; - - private readonly transcriptionQueue = new LimitedQueue(); - - private data: Float32Array | undefined = undefined; + protected static MAX_DATA_LENGTH = 30 /* seconds */ * 16000 /* sampling rate */ * 16 /* bith depth */ * 1 /* channels */ / 8; constructor( - private readonly port: MessagePortMain, - private readonly voiceRecognitionService: IVoiceRecognitionService, - private readonly logService: ILogService + protected readonly port: MessagePortMain, + protected readonly voiceRecognitionService: IVoiceRecognitionService, + protected readonly logService: ILogService ) { super(); @@ -49,39 +53,115 @@ class VoiceTranscriber extends Disposable { } private registerListeners(): void { - this.logService.info(`[voice] transcriber: new connection`); - const cts = new CancellationTokenSource(); this._register(toDisposable(() => cts.dispose(true))); - const requestHandler = (e: MessageEvent) => this.handleRequest(e, cts.token); + const requestHandler = (e: MessageEvent) => { + if (!(e.data instanceof Float32Array)) { + return; + } + + this.handleRequest(e.data, cts.token); + }; this.port.on('message', requestHandler); this._register(toDisposable(() => this.port.off('message', requestHandler))); this.port.start(); - this._register(toDisposable(() => this.port.close())); + let closed = false; this.port.on('close', () => { this.logService.info(`[voice] transcriber: closed connection`); - cts.dispose(true); + closed = true; + this.dispose(); }); + + this._register(toDisposable(() => { + if (!closed) { + this.port.close(); + } + })); } - private async handleRequest(e: MessageEvent, cancellation: CancellationToken): Promise { - if (!(Array.isArray(e.data))) { + protected abstract handleRequest(data: Float32Array, cancellation: CancellationToken): Promise; + + protected joinFloat32Arrays(float32Arrays: Float32Array[]): Float32Array { + const result = new Float32Array(float32Arrays.reduce((prev, curr) => prev + curr.length, 0)); + + let offset = 0; + for (const float32Array of float32Arrays) { + result.set(float32Array, offset); + offset += float32Array.length; + } + + return result; + } +} + +class SlidingWindowVoiceTranscriber extends VoiceTranscriber { + + private readonly transcriptionQueue = this._register(new Queue()); + + private transcribedResults: string[] = []; + private data: Float32Array = new Float32Array(0); + + protected async handleRequest(data: Float32Array, cancellation: CancellationToken): Promise { + if (data.length > 0) { + this.logService.info(`[voice] transcriber: voice detected, storing in buffer`); + + this.data = this.data ? this.joinFloat32Arrays([this.data, data]) : data; + } else { + this.logService.info(`[voice] transcriber: silence detected, transcribing window...`); + + const data = this.data.slice(0); + this.data = new Float32Array(0); + + this.transcriptionQueue.queue(() => this.transcribe(data, cancellation)); + } + } + + private async transcribe(data: Float32Array, cancellation: CancellationToken): Promise { + if (cancellation.isCancellationRequested) { return; } - const newData: Float32Array[] = []; - for (const channelData of e.data) { - if (channelData instanceof Float32Array) { - newData.push(channelData); + if (data.length > VoiceTranscriber.MAX_DATA_LENGTH) { + this.logService.warn(`[voice] transcriber: refusing to accept more than 30s of audio data`); + return; + } + + if (data.length !== 0) { + const result = await this.voiceRecognitionService.transcribe(data, cancellation); + if (result) { + this.transcribedResults.push(result); } } - const dataCandidate = this.joinFloat32Arrays(this.data ? [this.data, ...newData] : newData); + if (cancellation.isCancellationRequested) { + return; + } + this.port.postMessage(this.transcribedResults.join(' ')); + } + + override dispose(): void { + super.dispose(); + + this.data = new Float32Array(0); + } +} + +class FullWindowVoiceTranscriber extends VoiceTranscriber { + + private readonly transcriptionQueue = new LimitedQueue(); + + private data: Float32Array | undefined = undefined; + + private transcribedDataLength = 0; + private transcribedResult = ''; + + protected async handleRequest(data: Float32Array, cancellation: CancellationToken): Promise { + const dataCandidate = this.data ? this.joinFloat32Arrays([this.data, data]) : data; if (dataCandidate.length > VoiceTranscriber.MAX_DATA_LENGTH) { this.logService.warn(`[voice] transcriber: refusing to accept more than 30s of audio data`); return; @@ -102,7 +182,20 @@ class VoiceTranscriber extends Disposable { return; } - const result = await this.voiceRecognitionService.transcribe(data, cancellation); + let result: string; + if (data.length === this.transcribedDataLength) { + // Optimization: if the data is the same as the last time + // we transcribed, don't transcribe again, just return the + // same result as we had last time. + this.logService.info(`[voice] transcriber: silence detected, reusing previous transcription result`); + result = this.transcribedResult; + } else { + this.logService.info(`[voice] transcriber: voice detected, transcribing everything...`); + result = await this.voiceRecognitionService.transcribe(data, cancellation); + } + + this.transcribedResult = result; + this.transcribedDataLength = data.length; if (cancellation.isCancellationRequested) { return; @@ -111,15 +204,9 @@ class VoiceTranscriber extends Disposable { this.port.postMessage(result); } - private joinFloat32Arrays(float32Arrays: Float32Array[]): Float32Array { - const result = new Float32Array(float32Arrays.reduce((prev, curr) => prev + curr.length, 0)); + override dispose(): void { + super.dispose(); - let offset = 0; - for (const float32Array of float32Arrays) { - result.set(float32Array, offset); - offset += float32Array.length; - } - - return result; + this.data = undefined; } } diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 5bfec5deba4..c2cc7511079 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -98,7 +98,7 @@ export class MouseHandler extends ViewEventHandler { // remove this listener if (!this._mouseLeaveMonitor) { - this._mouseLeaveMonitor = dom.addDisposableListener(document, 'mousemove', (e) => { + this._mouseLeaveMonitor = dom.addDisposableListener(this.viewHelper.viewDomNode.ownerDocument, 'mousemove', (e) => { if (!this.viewHelper.viewDomNode.contains(e.target as Node | null)) { // went outside the editor! this._onMouseLeave(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode)); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index b39b8f95ba6..4c57874ba42 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -336,7 +336,7 @@ export class HitTestContext { } private static _findAttribute(element: Element, attr: string, stopAt: Element): string | null { - while (element && element !== document.body) { + while (element && element !== element.ownerDocument.body) { if (element.hasAttribute && element.hasAttribute(attr)) { return element.getAttribute(attr); } @@ -917,7 +917,7 @@ export class MouseTargetFactory { range = (shadowRoot).caretRangeFromPoint(coords.clientX, coords.clientY); } } else { - range = (document).caretRangeFromPoint(coords.clientX, coords.clientY); + range = (ctx.viewDomNode.ownerDocument).caretRangeFromPoint(coords.clientX, coords.clientY); } if (!range || !range.startContainer) { @@ -959,7 +959,7 @@ export class MouseTargetFactory { * Most probably Gecko */ private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult { - const hitResult: { offsetNode: Node; offset: number } = (document).caretPositionFromPoint(coords.clientX, coords.clientY); + const hitResult: { offsetNode: Node; offset: number } = (ctx.viewDomNode.ownerDocument).caretPositionFromPoint(coords.clientX, coords.clientY); if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) { // offsetNode is expected to be the token text @@ -1011,9 +1011,9 @@ export class MouseTargetFactory { private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { let result: HitTestResult = new UnknownHitTestResult(); - if (typeof (document).caretRangeFromPoint === 'function') { + if (typeof (ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') { result = this._doHitTestWithCaretRangeFromPoint(ctx, request); - } else if ((document).caretPositionFromPoint) { + } else if ((ctx.viewDomNode.ownerDocument).caretPositionFromPoint) { result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates()); } if (result.type === HitTestResultType.Content) { diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index d1db497e636..8be2c4e4baa 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -391,7 +391,7 @@ export class TextAreaHandler extends ViewPart { const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount; const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount); const { tabSize } = this._context.viewModel.model.getOptions(); - const widthOfHiddenTextBefore = measureText(hiddenLineTextBefore, this._fontInfo, tabSize); + const widthOfHiddenTextBefore = measureText(this.textArea.domNode.ownerDocument, hiddenLineTextBefore, this._fontInfo, tabSize); return { distanceToModelLineStart, widthOfHiddenTextBefore }; })(); @@ -927,7 +927,7 @@ interface IRenderData { strikethrough?: boolean; } -function measureText(text: string, fontInfo: FontInfo, tabSize: number): number { +function measureText(document: Document, text: string, fontInfo: FontInfo, tabSize: number): number { if (text.length === 0) { return 0; } diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 36da11ef183..929df3c5d84 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -110,6 +110,8 @@ export interface ICompleteTextAreaWrapper extends ITextAreaWrapper { readonly onBlur: Event; readonly onSyntheticTap: Event; + readonly ownerDocument: Document; + setIgnoreSelectionChangeTime(reason: string): void; getIgnoreSelectionChangeTime(): number; resetSelectionChangeTime(): void; @@ -494,7 +496,7 @@ export class TextAreaInput extends Disposable { // `selectionchange` events often come multiple times for a single logical change // so throttle multiple `selectionchange` events that burst in a short period of time. let previousSelectionChangeEventTime = 0; - return dom.addDisposableListener(document, 'selectionchange', (e) => { + return dom.addDisposableListener(this._textArea.ownerDocument, 'selectionchange', (e) => {//todo inputLatency.onSelectionChange(); if (!this._hasFocus) { @@ -701,6 +703,10 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap public readonly onFocus = this._register(new DomEmitter(this._actual, 'focus')).event; public readonly onBlur = this._register(new DomEmitter(this._actual, 'blur')).event; + public get ownerDocument(): Document { + return this._actual.ownerDocument; + } + private _onSyntheticTap = this._register(new Emitter()); public readonly onSyntheticTap: Event = this._onSyntheticTap.event; @@ -725,7 +731,7 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap if (shadowRoot) { return shadowRoot.activeElement === this._actual; } else if (dom.isInDOM(this._actual)) { - return document.activeElement === this._actual; + return this._actual.ownerDocument.activeElement === this._actual; } else { return false; } @@ -775,7 +781,7 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap if (shadowRoot) { activeElement = shadowRoot.activeElement; } else { - activeElement = document.activeElement; + activeElement = textArea.ownerDocument.activeElement; } const currentIsFocused = (activeElement === textArea); diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index 7a3452d8461..44fc8eb9ab9 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -29,6 +29,7 @@ import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/co import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel'; import { ISelection } from 'vs/editor/common/core/selection'; +import { getActiveElement } from 'vs/base/browser/dom'; const CORE_WEIGHT = KeybindingWeight.EditorCore; @@ -315,9 +316,9 @@ abstract class EditorOrNativeTextInputCommand { // 2. handle case when focus is in some other `input` / `textarea`. target.addImplementation(1000, 'generic-dom-input-textarea', (accessor: ServicesAccessor, args: unknown) => { // Only if focused on an element that allows for entering text - const activeElement = document.activeElement; + const activeElement = getActiveElement(); if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { - this.runDOMCommand(); + this.runDOMCommand(activeElement); return true; } return false; @@ -343,7 +344,7 @@ abstract class EditorOrNativeTextInputCommand { return true; } - public abstract runDOMCommand(): void; + public abstract runDOMCommand(activeElement: Element): void; public abstract runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise; } @@ -1875,13 +1876,13 @@ export namespace CoreNavigationCommands { constructor() { super(SelectAllCommand); } - public runDOMCommand(): void { + public runDOMCommand(activeElement: Element): void { if (isFirefox) { - (document.activeElement).focus(); - (document.activeElement).select(); + (activeElement).focus(); + (activeElement).select(); } - document.execCommand('selectAll'); + activeElement.ownerDocument.execCommand('selectAll'); } public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void { const viewModel = editor._getViewModel(); @@ -2090,8 +2091,8 @@ export namespace CoreEditingCommands { constructor() { super(UndoCommand); } - public runDOMCommand(): void { - document.execCommand('undo'); + public runDOMCommand(activeElement: Element): void { + activeElement.ownerDocument.execCommand('undo'); } public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise { if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) { @@ -2105,8 +2106,8 @@ export namespace CoreEditingCommands { constructor() { super(RedoCommand); } - public runDOMCommand(): void { - document.execCommand('redo'); + public runDOMCommand(activeElement: Element): void { + activeElement.ownerDocument.execCommand('redo'); } public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise { if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) { diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 4af00e67b13..60379467426 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -241,7 +241,7 @@ export class GlobalEditorPointerMoveMonitor extends Disposable { // Add a <> keydown event listener that will cancel the monitoring // if something other than a modifier key is pressed - this._keydownListener = dom.addStandardDisposableListener(document, 'keydown', (e) => { + this._keydownListener = dom.addStandardDisposableListener(initialElement.ownerDocument, 'keydown', (e) => { const chord = e.toKeyCodeChord(); if (chord.isModifierKey()) { // Allow modifier keys diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 75562c53821..f84a017b141 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -24,6 +24,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IDisposable } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ILogService } from 'vs/platform/log/common/log'; +import { getActiveElement } from 'vs/base/browser/dom'; export type ServicesAccessor = InstantiationServicesAccessor; export type EditorContributionCtor = IConstructorSignature; @@ -219,7 +220,7 @@ export class MultiCommand extends Command { logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`); for (const impl of this._implementations) { if (impl.when) { - const context = contextKeyService.getContext(document.activeElement); + const context = contextKeyService.getContext(getActiveElement()); const value = impl.when.evaluate(context); if (!value) { continue; diff --git a/src/vs/editor/browser/view/viewPart.ts b/src/vs/editor/browser/view/viewPart.ts index 14a46ce44fe..ad88b9f9551 100644 --- a/src/vs/editor/browser/view/viewPart.ts +++ b/src/vs/editor/browser/view/viewPart.ts @@ -57,7 +57,7 @@ export class PartFingerprints { const result: PartFingerprint[] = []; let resultLen = 0; - while (child && child !== document.body) { + while (child && child !== child.ownerDocument.body) { if (child === stopAt) { break; } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index b494ba60cc2..3112c19c324 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -266,9 +266,11 @@ class Widget { } private _getMaxWidth(): number { + const elDocument = this.domNode.domNode.ownerDocument; + const elWindow = elDocument.defaultView; return ( this.allowEditorOverflow - ? window.innerWidth || document.documentElement!.offsetWidth || document.body.offsetWidth + ? elWindow?.innerWidth || elDocument.documentElement.offsetWidth || elDocument.body.offsetWidth : this._contentWidth ); } @@ -326,7 +328,9 @@ class Widget { const MIN_LIMIT = Math.max(LEFT_PADDING, domNodePosition.left - width); const MAX_LIMIT = Math.min(domNodePosition.left + domNodePosition.width + width, windowSize.width - RIGHT_PADDING); - let absoluteLeft = domNodePosition.left + left - window.scrollX; + const elDocument = this._viewDomNode.domNode.ownerDocument; + const elWindow = elDocument.defaultView; + let absoluteLeft = domNodePosition.left + left - (elWindow?.scrollX ?? 0); if (absoluteLeft + width > MAX_LIMIT) { const delta = absoluteLeft - (MAX_LIMIT - width); @@ -348,10 +352,12 @@ class Widget { const belowTop = anchor.top + anchor.height; const domNodePosition = dom.getDomNodePagePosition(this._viewDomNode.domNode); - const absoluteAboveTop = domNodePosition.top + aboveTop - window.scrollY; - const absoluteBelowTop = domNodePosition.top + belowTop - window.scrollY; + const elDocument = this._viewDomNode.domNode.ownerDocument; + const elWindow = elDocument.defaultView; + const absoluteAboveTop = domNodePosition.top + aboveTop - (elWindow?.scrollY ?? 0); + const absoluteBelowTop = domNodePosition.top + belowTop - (elWindow?.scrollY ?? 0); - const windowSize = dom.getClientArea(document.body); + const windowSize = dom.getClientArea(elDocument.body); const [left, absoluteAboveLeft] = this._layoutHorizontalSegmentInPage(windowSize, domNodePosition, anchor.left - ctx.scrollLeft + this._contentLeft, width); // Leave some clearance to the top/bottom diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts b/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts index 9ccc307b2cd..f17051a0364 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveElement } from 'vs/base/browser/dom'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -290,10 +291,11 @@ export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | } } - if (document.activeElement) { + const activeElement = getActiveElement(); + if (activeElement) { for (const d of diffEditors) { const container = d.getContainerDomNode(); - if (isElementOrParentOf(container, document.activeElement)) { + if (isElementOrParentOf(container, activeElement)) { return d; } } diff --git a/src/vs/editor/common/core/offsetRange.ts b/src/vs/editor/common/core/offsetRange.ts index 64c29063206..02053528c4c 100644 --- a/src/vs/editor/common/core/offsetRange.ts +++ b/src/vs/editor/common/core/offsetRange.ts @@ -103,6 +103,12 @@ export class OffsetRange { return undefined; } + public intersectsOrTouches(other: OffsetRange): boolean { + const start = Math.max(this.start, other.start); + const end = Math.min(this.endExclusive, other.endExclusive); + return start <= end; + } + public slice(arr: T[]): T[] { return arr.slice(this.start, this.endExclusive); } diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts index 2d095c6209b..95cc0f22054 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts @@ -16,11 +16,11 @@ export interface IDiffAlgorithm { export class DiffAlgorithmResult { static trivial(seq1: ISequence, seq2: ISequence): DiffAlgorithmResult { - return new DiffAlgorithmResult([new SequenceDiff(new OffsetRange(0, seq1.length), new OffsetRange(0, seq2.length))], false); + return new DiffAlgorithmResult([new SequenceDiff(OffsetRange.ofLength(seq1.length), OffsetRange.ofLength(seq2.length))], false); } static trivialTimedOut(seq1: ISequence, seq2: ISequence): DiffAlgorithmResult { - return new DiffAlgorithmResult([new SequenceDiff(new OffsetRange(0, seq1.length), new OffsetRange(0, seq2.length))], true); + return new DiffAlgorithmResult([new SequenceDiff(OffsetRange.ofLength(seq1.length), OffsetRange.ofLength(seq2.length))], true); } constructor( @@ -36,21 +36,22 @@ export class DiffAlgorithmResult { export class SequenceDiff { public static invert(sequenceDiffs: SequenceDiff[], doc1Length: number): SequenceDiff[] { const result: SequenceDiff[] = []; - forEachAdjacent(sequenceDiffs, (a, b) => { - const seq1Start = a ? a.seq1Range.endExclusive : 0; - const seq2Start = a ? a.seq2Range.endExclusive : 0; - const seq1EndEx = b ? b.seq1Range.start : doc1Length; - const seq2EndEx = b ? b.seq2Range.start : (a ? a.seq2Range.endExclusive - a.seq1Range.endExclusive : 0) + doc1Length; - result.push(new SequenceDiff( - new OffsetRange(seq1Start, seq1EndEx), - new OffsetRange(seq2Start, seq2EndEx), + result.push(SequenceDiff.fromOffsetPairs( + a ? a.getEndExclusives() : OffsetPair.zero, + b ? b.getStarts() : new OffsetPair(doc1Length, (a ? a.seq2Range.endExclusive - a.seq1Range.endExclusive : 0) + doc1Length) )); }); - return result; } + public static fromOffsetPairs(start: OffsetPair, endExclusive: OffsetPair): SequenceDiff { + return new SequenceDiff( + new OffsetRange(start.offset1, endExclusive.offset1), + new OffsetRange(start.offset2, endExclusive.offset2), + ); + } + constructor( public readonly seq1Range: OffsetRange, public readonly seq2Range: OffsetRange, @@ -74,6 +75,56 @@ export class SequenceDiff { } return new SequenceDiff(this.seq1Range.delta(offset), this.seq2Range.delta(offset)); } + + public deltaStart(offset: number): SequenceDiff { + if (offset === 0) { + return this; + } + return new SequenceDiff(this.seq1Range.deltaStart(offset), this.seq2Range.deltaStart(offset)); + } + + public deltaEnd(offset: number): SequenceDiff { + if (offset === 0) { + return this; + } + return new SequenceDiff(this.seq1Range.deltaEnd(offset), this.seq2Range.deltaEnd(offset)); + } + + public intersectsOrTouches(other: SequenceDiff): boolean { + return this.seq1Range.intersectsOrTouches(other.seq1Range) || this.seq2Range.intersectsOrTouches(other.seq2Range); + } + + public intersect(other: SequenceDiff): SequenceDiff | undefined { + const i1 = this.seq1Range.intersect(other.seq1Range); + const i2 = this.seq2Range.intersect(other.seq2Range); + if (!i1 || !i2) { + return undefined; + } + return new SequenceDiff(i1, i2); + } + + public getStarts(): OffsetPair { + return new OffsetPair(this.seq1Range.start, this.seq2Range.start); + } + + public getEndExclusives(): OffsetPair { + return new OffsetPair(this.seq1Range.endExclusive, this.seq2Range.endExclusive); + } +} + +export class OffsetPair { + public static readonly zero = new OffsetPair(0, 0); + public static readonly max = new OffsetPair(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + + constructor( + public readonly offset1: number, + public readonly offset2: number, + ) { + } + + public toString(): string { + return `${this.offset1} <-> ${this.offset2}`; + } } export interface ISequence { diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts index 6436ad0d9e0..2d25234c4e5 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts @@ -196,6 +196,9 @@ function computeUnchangedMoves( for (extendToTop = 0; extendToTop < linesAbove; extendToTop++) { const origLine = move.original.startLineNumber - extendToTop - 1; const modLine = move.modified.startLineNumber - extendToTop - 1; + if (origLine > originalLines.length || modLine > modifiedLines.length) { + break; + } if (modifiedSet.contains(modLine) || originalSet.contains(origLine)) { break; } @@ -213,6 +216,9 @@ function computeUnchangedMoves( for (extendToBottom = 0; extendToBottom < linesBelow; extendToBottom++) { const origLine = move.original.endLineNumberExclusive + extendToBottom; const modLine = move.modified.endLineNumberExclusive + extendToBottom; + if (origLine > originalLines.length || modLine > modifiedLines.length) { + break; + } if (modifiedSet.contains(modLine) || originalSet.contains(origLine)) { break; } diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts index b288c473874..eb9c36b1668 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { forEachWithNeighbors } from 'vs/base/common/arrays'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; -import { ISequence, SequenceDiff } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm'; +import { ISequence, OffsetPair, SequenceDiff } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm'; import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence'; import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; @@ -425,28 +426,33 @@ export function removeVeryShortMatchingTextBetweenLongDiffs(sequence1: LinesSlic diffs = result; } while (counter++ < 10 && shouldRepeat); - // Remove short suffixes/prefixes - for (let i = 0; i < diffs.length; i++) { - const cur = diffs[i]; + const newDiffs: SequenceDiff[] = []; - let range1 = cur.seq1Range; - let range2 = cur.seq2Range; + // Remove short suffixes/prefixes + forEachWithNeighbors(diffs, (prev, cur, next) => { + let newDiff = cur; + + function shouldMarkAsChanged(text: string): boolean { + return text.length > 0 && text.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 100; + } const fullRange1 = sequence1.extendToFullLines(cur.seq1Range); const prefix = sequence1.getText(new OffsetRange(fullRange1.start, cur.seq1Range.start)); - if (prefix.length > 0 && prefix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 100) { - range1 = cur.seq1Range.deltaStart(-prefix.length); - range2 = cur.seq2Range.deltaStart(-prefix.length); + if (shouldMarkAsChanged(prefix)) { + newDiff = newDiff.deltaStart(-prefix.length); } - const suffix = sequence1.getText(new OffsetRange(cur.seq1Range.endExclusive, fullRange1.endExclusive)); - if (suffix.length > 0 && (suffix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 150)) { - range1 = range1.deltaEnd(suffix.length); - range2 = range2.deltaEnd(suffix.length); + if (shouldMarkAsChanged(suffix)) { + newDiff = newDiff.deltaEnd(suffix.length); } - diffs[i] = new SequenceDiff(range1, range2); - } + const availableSpace = SequenceDiff.fromOffsetPairs( + prev ? prev.getEndExclusives() : OffsetPair.zero, + next ? next.getStarts() : OffsetPair.max, + ); + const result = newDiff.intersect(availableSpace)!; + newDiffs.push(result); + }); - return diffs; + return newDiffs; } diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts index ca515f2cbbe..822a6310bf0 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts @@ -96,7 +96,7 @@ export class LinesSliceCharSequence implements ISequence { let score = 0; if (prevCategory !== nextCategory) { score += 10; - if (nextCategory === CharBoundaryCategory.WordUpper) { + if (prevCategory === CharBoundaryCategory.WordLower && nextCategory === CharBoundaryCategory.WordUpper) { score += 1; } } diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 3d9f01f7dfa..b5cfb6cf6c7 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -840,6 +840,13 @@ export interface ITextModel { */ isTooLargeForTokenization(): boolean; + /** + * The file is so large, that operations on it might be too large for heap + * and can lead to OOM crashes so they should be disabled. + * @internal + */ + isTooLargeForHeapOperation(): boolean; + /** * Search the model. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index b600bd7e3bb..ea5f3ffea5e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -177,6 +177,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati static _MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB, // used in tests private static readonly LARGE_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20 MB; private static readonly LARGE_FILE_LINE_COUNT_THRESHOLD = 300 * 1000; // 300K lines + private static readonly LARGE_FILE_HEAP_OPERATION_THRESHOLD = 256 * 1024 * 1024; // 256M characters, usually ~> 512MB memory usage public static DEFAULT_CREATION_OPTIONS: model.ITextModelCreationOptions = { isForSimpleWidget: false, @@ -257,6 +258,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _initialUndoRedoSnapshot: ResourceEditStackSnapshot | null; private readonly _isTooLargeForSyncing: boolean; private readonly _isTooLargeForTokenization: boolean; + private readonly _isTooLargeForHeapOperation: boolean; //#region Editing private readonly _commandManager: EditStack; @@ -345,8 +347,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati (bufferTextLength > TextModel.LARGE_FILE_SIZE_THRESHOLD) || (bufferLineCount > TextModel.LARGE_FILE_LINE_COUNT_THRESHOLD) ); + + this._isTooLargeForHeapOperation = bufferTextLength > TextModel.LARGE_FILE_HEAP_OPERATION_THRESHOLD; } else { this._isTooLargeForTokenization = false; + this._isTooLargeForHeapOperation = false; } this._isTooLargeForSyncing = (bufferTextLength > TextModel._MODEL_SYNC_LIMIT); @@ -587,6 +592,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._isTooLargeForTokenization; } + public isTooLargeForHeapOperation(): boolean { + return this._isTooLargeForHeapOperation; + } + public isDisposed(): boolean { return this._isDisposed; } @@ -743,6 +752,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public getValue(eol?: model.EndOfLinePreference, preserveBOM: boolean = false): string { this._assertNotDisposed(); + if (this.isTooLargeForHeapOperation()) { + throw new BugIndicatingError('Operation would exceed heap memory limits'); + } + const fullModelRange = this.getFullModelRange(); const fullModelValue = this.getValueInRange(fullModelRange, eol); @@ -809,6 +822,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public getLinesContent(): string[] { this._assertNotDisposed(); + if (this.isTooLargeForHeapOperation()) { + throw new BugIndicatingError('Operation would exceed heap memory limits'); + } + return this._buffer.getLinesContent(); } diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index ef8fb85da14..12b28f16713 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -574,7 +574,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { return result; } - public async computeHumanReadableDiff(modelUrl: string, edits: TextEdit[], options: ILinesDiffComputerOptions): Promise { + public computeHumanReadableDiff(modelUrl: string, edits: TextEdit[], options: ILinesDiffComputerOptions): TextEdit[] { const model = this._getModel(modelUrl); if (!model) { return edits; diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index ec6cf691492..44f9fb9b434 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -572,28 +572,28 @@ export enum KeyCode { * Either the angle bracket key or the backslash key on the RT 102-key keyboard. */ IntlBackslash = 97, - Numpad0 = 98, - Numpad1 = 99, - Numpad2 = 100, - Numpad3 = 101, - Numpad4 = 102, - Numpad5 = 103, - Numpad6 = 104, - Numpad7 = 105, - Numpad8 = 106, - Numpad9 = 107, - NumpadMultiply = 108, - NumpadAdd = 109, - NUMPAD_SEPARATOR = 110, - NumpadSubtract = 111, - NumpadDecimal = 112, - NumpadDivide = 113, + Numpad0 = 98,// VK_NUMPAD0, 0x60, Numeric keypad 0 key + Numpad1 = 99,// VK_NUMPAD1, 0x61, Numeric keypad 1 key + Numpad2 = 100,// VK_NUMPAD2, 0x62, Numeric keypad 2 key + Numpad3 = 101,// VK_NUMPAD3, 0x63, Numeric keypad 3 key + Numpad4 = 102,// VK_NUMPAD4, 0x64, Numeric keypad 4 key + Numpad5 = 103,// VK_NUMPAD5, 0x65, Numeric keypad 5 key + Numpad6 = 104,// VK_NUMPAD6, 0x66, Numeric keypad 6 key + Numpad7 = 105,// VK_NUMPAD7, 0x67, Numeric keypad 7 key + Numpad8 = 106,// VK_NUMPAD8, 0x68, Numeric keypad 8 key + Numpad9 = 107,// VK_NUMPAD9, 0x69, Numeric keypad 9 key + NumpadMultiply = 108,// VK_MULTIPLY, 0x6A, Multiply key + NumpadAdd = 109,// VK_ADD, 0x6B, Add key + NUMPAD_SEPARATOR = 110,// VK_SEPARATOR, 0x6C, Separator key + NumpadSubtract = 111,// VK_SUBTRACT, 0x6D, Subtract key + NumpadDecimal = 112,// VK_DECIMAL, 0x6E, Decimal key + NumpadDivide = 113,// VK_DIVIDE, 0x6F, /** * Cover all key codes when IME is processing input. */ KEY_IN_COMPOSITION = 114, - ABNT_C1 = 115, - ABNT_C2 = 116, + ABNT_C1 = 115,// Brazilian (ABNT) Keyboard + ABNT_C2 = 116,// Brazilian (ABNT) Keyboard AudioVolumeMute = 117, AudioVolumeUp = 118, AudioVolumeDown = 119, diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index 420abfa16a4..1aa2c9f3611 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as browser from 'vs/base/browser/browser'; +import { getActiveDocument } from 'vs/base/browser/dom'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; import { CopyOptions, InMemoryClipboardMetadataManager } from 'vs/editor/browser/controller/textAreaInput'; @@ -179,7 +180,7 @@ class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction { CopyOptions.forceCopyWithSyntaxHighlighting = true; editor.focus(); - document.execCommand('copy'); + editor.getContainerDomNode().ownerDocument.execCommand('copy'); CopyOptions.forceCopyWithSyntaxHighlighting = false; } } @@ -200,7 +201,7 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman if (selection && selection.isEmpty() && !emptySelectionClipboard) { return true; } - document.execCommand(browserCommand); + focusedEditor.getContainerDomNode().ownerDocument.execCommand(browserCommand); return true; } return false; @@ -208,7 +209,7 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // 2. (default) handle case when focus is somewhere else. target.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: any) => { - document.execCommand(browserCommand); + getActiveDocument().execCommand(browserCommand); return true; }); } @@ -225,7 +226,7 @@ if (PasteAction) { // Only if editor text focus (i.e. not if editor has widget focus). const focusedEditor = codeEditorService.getFocusedCodeEditor(); if (focusedEditor && focusedEditor.hasTextFocus()) { - const result = document.execCommand('paste'); + const result = focusedEditor.getContainerDomNode().ownerDocument.execCommand('paste'); // Use the clipboard service if document.execCommand('paste') was not successful if (!result && platform.isWeb) { return (async () => { @@ -256,7 +257,7 @@ if (PasteAction) { // 2. Paste: (default) handle case when focus is somewhere else. PasteAction.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: any) => { - document.execCommand('paste'); + getActiveDocument().execCommand('paste'); return true; }); } diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index 9ce2ced9889..9a203b85257 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { coalesce, equals, isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { illegalArgument, isCancellationError, onUnexpectedExternalError } from 'vs/base/common/errors'; @@ -18,7 +19,6 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IModelService } from 'vs/editor/common/services/model'; import { TextModelCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; -import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -89,6 +89,10 @@ export async function getCodeActions( token: CancellationToken, ): Promise { const filter = trigger.filter || {}; + const notebookFilter: CodeActionFilter = { + ...filter, + excludes: [...(filter.excludes || []), CodeActionKind.Notebook], + }; const codeActionContext: languages.CodeActionContext = { only: filter.include?.value, @@ -96,7 +100,9 @@ export async function getCodeActions( }; const cts = new TextModelCancellationTokenSource(model, token); - const providers = getCodeActionProviders(registry, model, filter); + // if the trigger is auto (autosave, lightbulb, etc), we should exclude notebook codeActions + const excludeNotebookCodeActions = (trigger.type === languages.CodeActionTriggerType.Auto); + const providers = getCodeActionProviders(registry, model, (excludeNotebookCodeActions) ? notebookFilter : filter); const disposables = new DisposableStore(); const promises = providers.map(async provider => { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 14307f031e5..a56fb161ee2 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -30,6 +30,7 @@ import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types'; import { CodeActionModel, CodeActionsState } from './codeActionModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; interface IActionShowOptions { @@ -251,6 +252,13 @@ export class CodeActionController extends Disposable implements IEditorContribut }, onHide: () => { this._editor?.focus(); + }, + onFocus: async (action: CodeActionItem, token: CancellationToken) => { + await action.resolve(token); + if (token.isCancellationRequested) { + return; + } + return { canPreview: !!action.action.edit?.edits.length }; } }; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts index 8108d979327..7d15a54b44e 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts @@ -44,7 +44,8 @@ export function toMenuItems( item: action, group: uncategorizedCodeActionGroup, disabled: !!action.action.disabled, - label: action.action.disabled || action.action.title + label: action.action.disabled || action.action.title, + canPreview: !!action.action.edit?.edits.length, }; }); } diff --git a/src/vs/editor/contrib/codeAction/common/types.ts b/src/vs/editor/contrib/codeAction/common/types.ts index e1e8d835765..123b4c24c3d 100644 --- a/src/vs/editor/contrib/codeAction/common/types.ts +++ b/src/vs/editor/contrib/codeAction/common/types.ts @@ -20,6 +20,7 @@ export class CodeActionKind { public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); public static readonly RefactorMove = CodeActionKind.Refactor.append('move'); public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + public static readonly Notebook = new CodeActionKind('notebook'); public static readonly Source = new CodeActionKind('source'); public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll'); diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index f260a892cca..2cba641d116 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -39,7 +39,7 @@ export class CodeLensContribution implements IEditorContribution { private readonly _resolveCodeLensesScheduler: RunOnceScheduler; private _getCodeLensModelPromise: CancelablePromise | undefined; - private _oldCodeLensModels = new DisposableStore(); + private readonly _oldCodeLensModels = new DisposableStore(); private _currentCodeLensModel: CodeLensModel | undefined; private _resolveCodeLensesPromise: CancelablePromise | undefined; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts index 2f25409976f..5e245dfb4a2 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts @@ -264,7 +264,7 @@ class SaturationBox extends Disposable { this.monitor.startMonitoring(e.target, e.pointerId, e.buttons, event => this.onDidChangePosition(event.pageX - origin.left, event.pageY - origin.top), () => null); - const pointerUpListener = dom.addDisposableListener(document, dom.EventType.POINTER_UP, () => { + const pointerUpListener = dom.addDisposableListener(e.target.ownerDocument, dom.EventType.POINTER_UP, () => { this._onColorFlushed.fire(); pointerUpListener.dispose(); if (this.monitor) { @@ -387,7 +387,7 @@ abstract class Strip extends Disposable { monitor.startMonitoring(e.target, e.pointerId, e.buttons, event => this.onDidChangeTop(event.pageY - origin.top), () => null); - const pointerUpListener = dom.addDisposableListener(document, dom.EventType.POINTER_UP, () => { + const pointerUpListener = dom.addDisposableListener(e.target.ownerDocument, dom.EventType.POINTER_UP, () => { this._onColorFlushed.fire(); pointerUpListener.dispose(); monitor.stopMonitoring(true); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index f85e595c4ff..08ad5897812 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener, getActiveDocument } from 'vs/base/browser/dom'; import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -99,7 +99,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._editor.focus(); try { this._pasteAsActionContext = { preferredId }; - document.execCommand('paste'); + getActiveDocument().execCommand('paste'); } finally { this._pasteAsActionContext = undefined; } diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 168d7e5c230..ce1cdb9084e 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -97,6 +97,7 @@ export class CommonFindController extends Disposable implements IEditorContribut protected readonly _storageService: IStorageService; private readonly _clipboardService: IClipboardService; protected readonly _contextKeyService: IContextKeyService; + protected readonly _notificationService: INotificationService; get editor() { return this._editor; @@ -110,7 +111,8 @@ export class CommonFindController extends Disposable implements IEditorContribut editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService storageService: IStorageService, - @IClipboardService clipboardService: IClipboardService + @IClipboardService clipboardService: IClipboardService, + @INotificationService notificationService: INotificationService ) { super(); this._editor = editor; @@ -118,6 +120,7 @@ export class CommonFindController extends Disposable implements IEditorContribut this._contextKeyService = contextKeyService; this._storageService = storageService; this._clipboardService = clipboardService; + this._notificationService = notificationService; this._updateHistoryDelayer = new Delayer(500); this._state = this._register(new FindReplaceState()); @@ -390,6 +393,10 @@ export class CommonFindController extends Disposable implements IEditorContribut public replaceAll(): boolean { if (this._model) { + if (this._editor.getModel()?.isTooLargeForHeapOperation()) { + this._notificationService.warn(nls.localize('too.large.for.replaceall', "The file is too large to perform a replace all operation.")); + return false; + } this._model.replaceAll(); return true; } @@ -437,11 +444,11 @@ export class FindController extends CommonFindController implements IFindControl @IContextKeyService _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, - @INotificationService private readonly _notificationService: INotificationService, + @INotificationService notificationService: INotificationService, @IStorageService _storageService: IStorageService, @IClipboardService clipboardService: IClipboardService, ) { - super(editor, _contextKeyService, _storageService, clipboardService); + super(editor, _contextKeyService, _storageService, clipboardService, notificationService); this._widget = null; this._findOptionsWidget = null; } diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index 96505ecc752..27717765fa3 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -20,6 +20,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; class TestFindController extends CommonFindController { @@ -33,9 +34,10 @@ class TestFindController extends CommonFindController { editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService storageService: IStorageService, - @IClipboardService clipboardService: IClipboardService + @IClipboardService clipboardService: IClipboardService, + @INotificationService notificationService: INotificationService ) { - super(editor, contextKeyService, storageService, clipboardService); + super(editor, contextKeyService, storageService, clipboardService, notificationService); this._findInputFocused = CONTEXT_FIND_INPUT_FOCUSED.bindTo(contextKeyService); this._updateHistoryDelayer = new Delayer(50); this.hasFocus = false; diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index c06e6bac4a7..00b575b5c89 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -675,7 +675,7 @@ export class ContentHoverWidget extends ResizableContentWidget { ); if (overflowing || this._hover.containerDomNode.clientWidth < initialWidth) { - const bodyBoxWidth = dom.getClientArea(document.body).width; + const bodyBoxWidth = dom.getClientArea(this._hover.containerDomNode.ownerDocument.body).width; const horizontalPadding = 14; return bodyBoxWidth - horizontalPadding; } else { diff --git a/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts b/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts index 0243d9e88bf..72736b8ab6e 100644 --- a/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts +++ b/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts @@ -78,7 +78,7 @@ export abstract class ResizableContentWidget extends Disposable implements ICont return; } const editorBox = dom.getDomNodePagePosition(editorDomNode); - const bodyBox = dom.getClientArea(document.body); + const bodyBox = dom.getClientArea(editorDomNode.ownerDocument.body); const mouseBottom = editorBox.top + mouseBox.top + mouseBox.height; return bodyBox.height - mouseBottom - BOTTOM_HEIGHT; } diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 11a5c6c4f27..74d7849587e 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -708,7 +708,7 @@ export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction { if (selection.isEmpty()) { if (selection.startColumn === 1) { const deleteFromLine = Math.max(1, selection.startLineNumber - 1); - const deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineContent(deleteFromLine).length + 1; + const deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineLength(deleteFromLine) + 1; return new Range(deleteFromLine, deleteFromColumn, selection.startLineNumber, 1); } else { return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn); @@ -864,7 +864,7 @@ export class JoinLinesAction extends EditorAction { let endLineNumber: number, endColumn: number; - const selectionEndPositionOffset = model.getLineContent(selection.endLineNumber).length - selection.endColumn; + const selectionEndPositionOffset = model.getLineLength(selection.endLineNumber) - selection.endColumn; if (selection.isEmpty() || selection.startLineNumber === selection.endLineNumber) { const position = selection.getStartPosition(); diff --git a/src/vs/editor/contrib/message/browser/messageController.ts b/src/vs/editor/contrib/message/browser/messageController.ts index e4466162dc4..6e13775d651 100644 --- a/src/vs/editor/contrib/message/browser/messageController.ts +++ b/src/vs/editor/contrib/message/browser/messageController.ts @@ -82,7 +82,7 @@ export class MessageController implements IEditorContribution { return; // override when mouse over message } - if (this._messageWidget.value && dom.isAncestor(document.activeElement, this._messageWidget.value.getDomNode())) { + if (this._messageWidget.value && dom.isAncestor(dom.getActiveElement(), this._messageWidget.value.getDomNode())) { return; // override when focus is inside the message } diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index 2ea6da1d7f8..e00d68cd368 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -192,7 +192,7 @@ export class RenameInputField implements IContentWidget { }; disposeOnDone.add(token.onCancellationRequested(() => this.cancelInput(true))); - disposeOnDone.add(this._editor.onDidBlurEditorWidget(() => this.cancelInput(!document.hasFocus()))); + disposeOnDone.add(this._editor.onDidBlurEditorWidget(() => this.cancelInput(!this._domNode?.ownerDocument.hasFocus()))); this._show(); diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index 87d27b48d5c..0290c450113 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -62,7 +62,7 @@ export class SnippetController2 implements IEditorContribution { private readonly _hasPrevTabstop: IContextKey; private _session?: SnippetSession; - private _snippetListener = new DisposableStore(); + private readonly _snippetListener = new DisposableStore(); private _modelVersionId: number = -1; private _currentChoice?: Choice; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 6a03536715e..b86e2fd94ef 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -787,7 +787,7 @@ export class SuggestWidget implements IDisposable { return; } - const bodyBox = dom.getClientArea(document.body); + const bodyBox = dom.getClientArea(this.element.domNode.ownerDocument.body); const info = this.getLayoutInfo(); if (!size) { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts index d6e95dd5a71..7f4646be053 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts @@ -372,7 +372,7 @@ export class SuggestDetailsOverlay implements IOverlayWidget { } _placeAtAnchor(anchorBox: dom.IDomNodePagePosition, size: dom.Dimension, preferAlignAtTop: boolean) { - const bodyBox = dom.getClientArea(document.body); + const bodyBox = dom.getClientArea(this.getDomNode().ownerDocument.body); const info = this.widget.getLayoutInfo(); diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index f66d1bbaa65..6874020e739 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -91,6 +91,8 @@ suite('TextAreaInput', () => { private _state: IRecordedTextareaState; private _currDispatchingEvent: IRecordedEvent | null; + public ownerDocument = document; + constructor() { super(); this._state = { diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 28a17729768..418f0b990c9 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -100,6 +100,7 @@ export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { class TestEditorDomElement { parentElement: IContextKeyServiceTarget | null = null; + ownerDocument = document; setAttribute(attr: string, value: string): void { } removeAttribute(attr: string): void { } hasAttribute(attr: string): boolean { return false; } diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 15accef1657..5dc2dd11f16 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -257,7 +257,7 @@ suite('EditorSimpleWorker', () => { ); }); - test.skip('[Bug] Getting Message "Overlapping ranges are not allowed" and nothing happens with Inline-Chat ', async function () { + test('[Bug] Getting Message "Overlapping ranges are not allowed" and nothing happens with Inline-Chat ', async function () { await testEdits(("const API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const req = {};\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === '/books') {\n handler(req, res);\n }\n },\n });\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n author: mockAuthor,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/author/${mockAuthor}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n title: mockTitle,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/title/${mockTitle}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n});\n").split('\n'), [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 96, endColumn: 1 }, @@ -337,7 +337,7 @@ function applyEdits(text: string, edits: { range: IRange; text: string }[]): str class PositionOffsetTransformer { private readonly lineStartOffsetByLineIdx: number[]; - constructor(text: string) { + constructor(private readonly text: string) { this.lineStartOffsetByLineIdx = []; this.lineStartOffsetByLineIdx.push(0); for (let i = 0; i < text.length; i++) { @@ -349,7 +349,7 @@ class PositionOffsetTransformer { } getOffset(position: Position): number { - const nextLineOffset = this.lineStartOffsetByLineIdx[position.lineNumber]; - return Math.min(this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1, nextLineOffset - 1); + const maxLineOffset = position.lineNumber >= this.lineStartOffsetByLineIdx.length ? this.text.length : (this.lineStartOffsetByLineIdx[position.lineNumber] - 1); + return Math.min(this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1, maxLineOffset); } } diff --git a/src/vs/editor/test/node/diffing/fixtures.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts index a7ff5dbe5a1..18a949c6538 100644 --- a/src/vs/editor/test/node/diffing/fixtures.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -11,6 +11,7 @@ import { FileAccess } from 'vs/base/common/network'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { LegacyLinesDiffComputer } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; +import { Range } from 'vs/editor/common/core/range'; suite('diffing fixtures', () => { setup(() => { @@ -48,12 +49,18 @@ suite('diffing fixtures', () => { originalRange: c.original.toString(), modifiedRange: c.modified.toString(), innerChanges: c.innerChanges?.map(c => ({ - originalRange: c.originalRange.toString(), - modifiedRange: c.modifiedRange.toString(), + originalRange: formatRange(c.originalRange, firstContentLines), + modifiedRange: formatRange(c.modifiedRange, secondContentLines), })) || null })); } + function formatRange(range: Range, lines: string[]): string { + const toLastChar = range.endColumn === lines[range.endLineNumber - 1].length + 1; + + return '[' + range.startLineNumber + ',' + range.startColumn + ' -> ' + range.endLineNumber + ',' + range.endColumn + (toLastChar ? ' EOL' : '') + ']'; + } + const actualDiffingResult: DiffingResult = { original: { content: firstContent, fileName: `./${firstFileName}` }, modified: { content: secondContent, fileName: `./${secondFileName}` }, diff --git a/src/vs/editor/test/node/diffing/fixtures/class-replacement/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/class-replacement/legacy.expected.diff.json index ec12e5d7a02..8f07edb769f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/class-replacement/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/class-replacement/legacy.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[29,31)", "innerChanges": [ { - "originalRange": "[29,1 -> 33,41]", - "modifiedRange": "[29,1 -> 30,54]" + "originalRange": "[29,1 -> 33,41 EOL]", + "modifiedRange": "[29,1 -> 30,54 EOL]" } ] }, @@ -59,8 +59,8 @@ "modifiedRange": "[36,42 -> 36,43]" }, { - "originalRange": "[41,29 -> 45,24]", - "modifiedRange": "[36,46 -> 36,66]" + "originalRange": "[41,29 -> 45,24 EOL]", + "modifiedRange": "[36,46 -> 36,66 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json index 77b3553a99c..c1c5787e10f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json @@ -14,7 +14,7 @@ "innerChanges": [ { "originalRange": "[1,1 -> 29,64]", - "modifiedRange": "[1,1 -> 1,1]" + "modifiedRange": "[1,1 -> 1,1 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json index b2155f7f625..7686d146b16 100644 --- a/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json @@ -33,8 +33,8 @@ "modifiedRange": "[156,163)", "innerChanges": [ { - "originalRange": "[159,1 -> 159,1]", - "modifiedRange": "[156,1 -> 163,1]" + "originalRange": "[159,1 -> 159,1 EOL]", + "modifiedRange": "[156,1 -> 163,1 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/difficult-move/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/difficult-move/legacy.expected.diff.json index 54e5d6610b0..6b0e1568dc0 100644 --- a/src/vs/editor/test/node/diffing/fixtures/difficult-move/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/difficult-move/legacy.expected.diff.json @@ -28,8 +28,8 @@ "modifiedRange": "[226,234)", "innerChanges": [ { - "originalRange": "[222,17 -> 222,19]", - "modifiedRange": "[226,17 -> 226,17]" + "originalRange": "[222,17 -> 222,19 EOL]", + "modifiedRange": "[226,17 -> 226,17 EOL]" }, { "originalRange": "[223,4 -> 223,28]", @@ -40,8 +40,8 @@ "modifiedRange": "[227,41 -> 227,48]" }, { - "originalRange": "[223,54 -> 223,65]", - "modifiedRange": "[227,53 -> 227,62]" + "originalRange": "[223,54 -> 223,65 EOL]", + "modifiedRange": "[227,53 -> 227,62 EOL]" }, { "originalRange": "[224,4 -> 224,21]", @@ -60,8 +60,8 @@ "modifiedRange": "[228,123 -> 228,152]" }, { - "originalRange": "[226,22 -> 226,25]", - "modifiedRange": "[228,166 -> 228,169]" + "originalRange": "[226,22 -> 226,25 EOL]", + "modifiedRange": "[228,166 -> 228,169 EOL]" }, { "originalRange": "[227,5 -> 227,30]", @@ -72,16 +72,16 @@ "modifiedRange": "[229,28 -> 229,32]" }, { - "originalRange": "[227,45 -> 227,93]", - "modifiedRange": "[229,35 -> 229,67]" + "originalRange": "[227,45 -> 227,93 EOL]", + "modifiedRange": "[229,35 -> 229,67 EOL]" }, { - "originalRange": "[228,5 -> 228,51]", - "modifiedRange": "[230,5 -> 230,30]" + "originalRange": "[228,5 -> 228,51 EOL]", + "modifiedRange": "[230,5 -> 230,30 EOL]" }, { - "originalRange": "[229,6 -> 230,6]", - "modifiedRange": "[231,6 -> 231,40]" + "originalRange": "[229,6 -> 230,6 EOL]", + "modifiedRange": "[231,6 -> 231,40 EOL]" }, { "originalRange": "[231,4 -> 232,42]", @@ -92,8 +92,8 @@ "modifiedRange": "[232,25 -> 233,32]" }, { - "originalRange": "[232,72 -> 233,4]", - "modifiedRange": "[233,35 -> 233,60]" + "originalRange": "[232,72 -> 233,4 EOL]", + "modifiedRange": "[233,35 -> 233,60 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/fuzzy-matching/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/fuzzy-matching/advanced.expected.diff.json index 4059da96da8..30bf62d99e9 100644 --- a/src/vs/editor/test/node/diffing/fixtures/fuzzy-matching/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/fuzzy-matching/advanced.expected.diff.json @@ -17,32 +17,32 @@ "modifiedRange": "[1,1 -> 1,1]" }, { - "originalRange": "[2,15 -> 2,15]", - "modifiedRange": "[1,15 -> 1,16]" + "originalRange": "[2,15 -> 2,15 EOL]", + "modifiedRange": "[1,15 -> 1,16 EOL]" }, { - "originalRange": "[3,15 -> 3,15]", - "modifiedRange": "[2,15 -> 2,16]" + "originalRange": "[3,15 -> 3,15 EOL]", + "modifiedRange": "[2,15 -> 2,16 EOL]" }, { - "originalRange": "[4,15 -> 4,15]", - "modifiedRange": "[3,15 -> 3,16]" + "originalRange": "[4,15 -> 4,15 EOL]", + "modifiedRange": "[3,15 -> 3,16 EOL]" }, { - "originalRange": "[5,15 -> 5,15]", - "modifiedRange": "[4,15 -> 5,1]" + "originalRange": "[5,15 -> 5,15 EOL]", + "modifiedRange": "[4,15 -> 5,1 EOL]" }, { - "originalRange": "[6,15 -> 6,15]", - "modifiedRange": "[6,15 -> 6,16]" + "originalRange": "[6,15 -> 6,15 EOL]", + "modifiedRange": "[6,15 -> 6,16 EOL]" }, { - "originalRange": "[7,15 -> 7,15]", - "modifiedRange": "[7,15 -> 7,16]" + "originalRange": "[7,15 -> 7,15 EOL]", + "modifiedRange": "[7,15 -> 7,16 EOL]" }, { - "originalRange": "[8,15 -> 8,15]", - "modifiedRange": "[8,15 -> 9,1]" + "originalRange": "[8,15 -> 8,15 EOL]", + "modifiedRange": "[8,15 -> 9,1 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/import-shifting/1.tst b/src/vs/editor/test/node/diffing/fixtures/import-shifting/1.tst new file mode 100644 index 00000000000..ff1f7ccada9 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/import-shifting/1.tst @@ -0,0 +1,3 @@ +import { RuntimeMode } from './runtimeMode'; +import { PromiseQueue } from './telemetry'; +import { TestNotificationSender, TestUrlOpener } from './testHelpers'; diff --git a/src/vs/editor/test/node/diffing/fixtures/import-shifting/2.tst b/src/vs/editor/test/node/diffing/fixtures/import-shifting/2.tst new file mode 100644 index 00000000000..3955d074aa9 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/import-shifting/2.tst @@ -0,0 +1,3 @@ +import { RuntimeMode } from './runtimeMode'; +import { PromiseQueue, TestPromiseQueue } from './telemetry'; +import { TestNotificationSender, TestUrlOpener } from './testHelpers'; diff --git a/src/vs/editor/test/node/diffing/fixtures/import-shifting/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/import-shifting/advanced.expected.diff.json new file mode 100644 index 00000000000..adbfd5208d5 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/import-shifting/advanced.expected.diff.json @@ -0,0 +1,22 @@ +{ + "original": { + "content": "import { RuntimeMode } from './runtimeMode';\nimport { PromiseQueue } from './telemetry';\nimport { TestNotificationSender, TestUrlOpener } from './testHelpers';\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "import { RuntimeMode } from './runtimeMode';\nimport { PromiseQueue, TestPromiseQueue } from './telemetry';\nimport { TestNotificationSender, TestUrlOpener } from './testHelpers';\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[2,3)", + "modifiedRange": "[2,3)", + "innerChanges": [ + { + "originalRange": "[2,22 -> 2,22]", + "modifiedRange": "[2,22 -> 2,40]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/import-shifting/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/import-shifting/legacy.expected.diff.json new file mode 100644 index 00000000000..adbfd5208d5 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/import-shifting/legacy.expected.diff.json @@ -0,0 +1,22 @@ +{ + "original": { + "content": "import { RuntimeMode } from './runtimeMode';\nimport { PromiseQueue } from './telemetry';\nimport { TestNotificationSender, TestUrlOpener } from './testHelpers';\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "import { RuntimeMode } from './runtimeMode';\nimport { PromiseQueue, TestPromiseQueue } from './telemetry';\nimport { TestNotificationSender, TestUrlOpener } from './testHelpers';\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[2,3)", + "modifiedRange": "[2,3)", + "innerChanges": [ + { + "originalRange": "[2,22 -> 2,22]", + "modifiedRange": "[2,22 -> 2,40]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/intra-block-align/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/intra-block-align/advanced.expected.diff.json index 84c7fde1881..25b767b28eb 100644 --- a/src/vs/editor/test/node/diffing/fixtures/intra-block-align/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/intra-block-align/advanced.expected.diff.json @@ -13,16 +13,16 @@ "modifiedRange": "[1,4)", "innerChanges": [ { - "originalRange": "[1,15 -> 1,16]", - "modifiedRange": "[1,15 -> 1,15]" + "originalRange": "[1,15 -> 1,16 EOL]", + "modifiedRange": "[1,15 -> 1,15 EOL]" }, { - "originalRange": "[2,15 -> 3,16]", - "modifiedRange": "[2,15 -> 2,15]" + "originalRange": "[2,15 -> 3,16 EOL]", + "modifiedRange": "[2,15 -> 2,15 EOL]" }, { - "originalRange": "[4,15 -> 4,16]", - "modifiedRange": "[3,15 -> 3,15]" + "originalRange": "[4,15 -> 4,16 EOL]", + "modifiedRange": "[3,15 -> 3,15 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/intra-block-align/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/intra-block-align/legacy.expected.diff.json index 84c7fde1881..25b767b28eb 100644 --- a/src/vs/editor/test/node/diffing/fixtures/intra-block-align/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/intra-block-align/legacy.expected.diff.json @@ -13,16 +13,16 @@ "modifiedRange": "[1,4)", "innerChanges": [ { - "originalRange": "[1,15 -> 1,16]", - "modifiedRange": "[1,15 -> 1,15]" + "originalRange": "[1,15 -> 1,16 EOL]", + "modifiedRange": "[1,15 -> 1,15 EOL]" }, { - "originalRange": "[2,15 -> 3,16]", - "modifiedRange": "[2,15 -> 2,15]" + "originalRange": "[2,15 -> 3,16 EOL]", + "modifiedRange": "[2,15 -> 2,15 EOL]" }, { - "originalRange": "[4,15 -> 4,16]", - "modifiedRange": "[3,15 -> 3,15]" + "originalRange": "[4,15 -> 4,16 EOL]", + "modifiedRange": "[3,15 -> 3,15 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/1.tst b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/1.tst new file mode 100644 index 00000000000..8a882b0f3d7 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/1.tst @@ -0,0 +1,94 @@ +const API = require('../src/api'); + +describe('API', () => { + let api; + let database; + + beforeAll(() => { + database = { + getAllBooks: jest.fn(), + getBooksByAuthor: jest.fn(), + getBooksByTitle: jest.fn(), + }; + api = new API(database); + }); + + describe('GET /books', () => { + it('should return all books', async () => { + const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }]; + database.getAllBooks.mockResolvedValue(mockBooks); + + const req = {}; + const res = { + json: jest.fn(), + }; + + await api.register({ + get: (path, handler) => { + if (path === '/books') { + handler(req, res); + } + }, + }); + + expect(database.getAllBooks).toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith(mockBooks); + }); + }); + + describe('GET /books/author/:author', () => { + it('should return books by author', async () => { + const mockAuthor = 'John Doe'; + const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }]; + database.getBooksByAuthor.mockResolvedValue(mockBooks); + + const req = { + params: { + author: mockAuthor, + }, + }; + const res = { + json: jest.fn(), + }; + + await api.register({ + get: (path, handler) => { + if (path === `/books/author/${mockAuthor}`) { + handler(req, res); + } + }, + }); + + expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor); + expect(res.json).toHaveBeenCalledWith(mockBooks); + }); + }); + + describe('GET /books/title/:title', () => { + it('should return books by title', async () => { + const mockTitle = 'Book 1'; + const mockBooks = [{ title: mockTitle, author: 'John Doe' }]; + database.getBooksByTitle.mockResolvedValue(mockBooks); + + const req = { + params: { + title: mockTitle, + }, + }; + const res = { + json: jest.fn(), + }; + + await api.register({ + get: (path, handler) => { + if (path === `/books/title/${mockTitle}`) { + handler(req, res); + } + }, + }); + + expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle); + expect(res.json).toHaveBeenCalledWith(mockBooks); + }); + }); +}); diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/2.tst b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/2.tst new file mode 100644 index 00000000000..b688d2706ed --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/2.tst @@ -0,0 +1,57 @@ +const request = require('supertest'); +const API = require('../src/api'); + +describe('API', () => { + let api; + let database; + + beforeAll(() => { + database = { + getAllBooks: jest.fn(), + getBooksByAuthor: jest.fn(), + getBooksByTitle: jest.fn(), + }; + api = new API(database); + }); + + describe('GET /books', () => { + it('should return all books', async () => { + const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }]; + database.getAllBooks.mockResolvedValue(mockBooks); + + const response = await request(api.app).get('/books'); + + expect(database.getAllBooks).toHaveBeenCalled(); + expect(response.status).toBe(200); + expect(response.body).toEqual(mockBooks); + }); + }); + + describe('GET /books/author/:author', () => { + it('should return books by author', async () => { + const mockAuthor = 'John Doe'; + const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }]; + database.getBooksByAuthor.mockResolvedValue(mockBooks); + + const response = await request(api.app).get(`/books/author/${mockAuthor}`); + + expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor); + expect(response.status).toBe(200); + expect(response.body).toEqual(mockBooks); + }); + }); + + describe('GET /books/title/:title', () => { + it('should return books by title', async () => { + const mockTitle = 'Book 1'; + const mockBooks = [{ title: mockTitle, author: 'John Doe' }]; + database.getBooksByTitle.mockResolvedValue(mockBooks); + + const response = await request(api.app).get(`/books/title/${mockTitle}`); + + expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle); + expect(response.status).toBe(200); + expect(response.body).toEqual(mockBooks); + }); + }); +}); diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/advanced.expected.diff.json new file mode 100644 index 00000000000..131a397b9a5 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/advanced.expected.diff.json @@ -0,0 +1,130 @@ +{ + "original": { + "content": "const API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const req = {};\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === '/books') {\n handler(req, res);\n }\n },\n });\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n author: mockAuthor,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/author/${mockAuthor}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n title: mockTitle,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/title/${mockTitle}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n});\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "const request = require('supertest');\nconst API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get('/books');\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get(`/books/author/${mockAuthor}`);\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get(`/books/title/${mockTitle}`);\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n});\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[1,1)", + "modifiedRange": "[1,2)", + "innerChanges": [ + { + "originalRange": "[1,1 -> 1,1]", + "modifiedRange": "[1,1 -> 2,1]" + } + ] + }, + { + "originalRange": "[21,33)", + "modifiedRange": "[22,23)", + "innerChanges": [ + { + "originalRange": "[21,1 -> 22,1]", + "modifiedRange": "[22,1 -> 22,1]" + }, + { + "originalRange": "[22,13 -> 26,6]", + "modifiedRange": "[22,13 -> 22,23]" + }, + { + "originalRange": "[26,13 -> 26,13]", + "modifiedRange": "[22,30 -> 22,38]" + }, + { + "originalRange": "[26,17 -> 27,9]", + "modifiedRange": "[22,42 -> 22,47]" + }, + { + "originalRange": "[27,9 -> 33,1 EOL]", + "modifiedRange": "[22,47 -> 23,1 EOL]" + } + ] + }, + { + "originalRange": "[35,36)", + "modifiedRange": "[25,27)", + "innerChanges": [ + { + "originalRange": "[35,14 -> 35,44]", + "modifiedRange": "[25,14 -> 26,36]" + } + ] + }, + { + "originalRange": "[45,61)", + "modifiedRange": "[36,37)", + "innerChanges": [ + { + "originalRange": "[45,1 -> 54,6]", + "modifiedRange": "[36,1 -> 36,23]" + }, + { + "originalRange": "[54,13 -> 54,13]", + "modifiedRange": "[36,30 -> 36,38]" + }, + { + "originalRange": "[54,17 -> 55,9]", + "modifiedRange": "[36,42 -> 36,47]" + }, + { + "originalRange": "[55,12 -> 56,24]", + "modifiedRange": "[36,50 -> 36,51]" + }, + { + "originalRange": "[56,54 -> 60,9]", + "modifiedRange": "[36,81 -> 36,81]" + } + ] + }, + { + "originalRange": "[63,64)", + "modifiedRange": "[39,41)", + "innerChanges": [ + { + "originalRange": "[63,14 -> 63,44]", + "modifiedRange": "[39,14 -> 40,36]" + } + ] + }, + { + "originalRange": "[73,89)", + "modifiedRange": "[50,51)", + "innerChanges": [ + { + "originalRange": "[73,1 -> 82,6]", + "modifiedRange": "[50,1 -> 50,23]" + }, + { + "originalRange": "[82,13 -> 82,13]", + "modifiedRange": "[50,30 -> 50,38]" + }, + { + "originalRange": "[82,17 -> 83,9]", + "modifiedRange": "[50,42 -> 50,47]" + }, + { + "originalRange": "[83,12 -> 84,24]", + "modifiedRange": "[50,50 -> 50,51]" + }, + { + "originalRange": "[84,52 -> 88,9]", + "modifiedRange": "[50,79 -> 50,79]" + } + ] + }, + { + "originalRange": "[91,92)", + "modifiedRange": "[53,55)", + "innerChanges": [ + { + "originalRange": "[91,14 -> 91,44]", + "modifiedRange": "[53,14 -> 54,36]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/legacy.expected.diff.json new file mode 100644 index 00000000000..9a4b104d1f2 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-bug/legacy.expected.diff.json @@ -0,0 +1,145 @@ +{ + "original": { + "content": "const API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const req = {};\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === '/books') {\n handler(req, res);\n }\n },\n });\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n author: mockAuthor,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/author/${mockAuthor}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n title: mockTitle,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/title/${mockTitle}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n});\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "const request = require('supertest');\nconst API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get('/books');\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get(`/books/author/${mockAuthor}`);\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get(`/books/title/${mockTitle}`);\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n});\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[1,1)", + "modifiedRange": "[1,2)", + "innerChanges": null + }, + { + "originalRange": "[21,33)", + "modifiedRange": "[22,23)", + "innerChanges": [ + { + "originalRange": "[21,15 -> 22,8]", + "modifiedRange": "[22,15 -> 22,17]" + }, + { + "originalRange": "[22,11 -> 22,16]", + "modifiedRange": "[22,20 -> 22,21]" + }, + { + "originalRange": "[22,19 -> 26,7]", + "modifiedRange": "[22,24 -> 22,24]" + }, + { + "originalRange": "[26,13 -> 27,9]", + "modifiedRange": "[22,30 -> 22,47]" + }, + { + "originalRange": "[27,12 -> 28,24]", + "modifiedRange": "[22,50 -> 22,51]" + }, + { + "originalRange": "[28,33 -> 32,9]", + "modifiedRange": "[22,60 -> 22,60]" + } + ] + }, + { + "originalRange": "[35,36)", + "modifiedRange": "[25,27)", + "innerChanges": [ + { + "originalRange": "[35,17 -> 35,22]", + "modifiedRange": "[25,17 -> 25,29]" + }, + { + "originalRange": "[35,26 -> 35,44]", + "modifiedRange": "[25,33 -> 26,36]" + } + ] + }, + { + "originalRange": "[45,61)", + "modifiedRange": "[36,37)", + "innerChanges": [ + { + "originalRange": "[45,15 -> 50,8]", + "modifiedRange": "[36,15 -> 36,17]" + }, + { + "originalRange": "[50,11 -> 50,16]", + "modifiedRange": "[36,20 -> 36,21]" + }, + { + "originalRange": "[50,19 -> 54,7]", + "modifiedRange": "[36,24 -> 36,24]" + }, + { + "originalRange": "[54,13 -> 56,24]", + "modifiedRange": "[36,30 -> 36,51]" + }, + { + "originalRange": "[56,54 -> 60,9]", + "modifiedRange": "[36,81 -> 36,81]" + } + ] + }, + { + "originalRange": "[63,64)", + "modifiedRange": "[39,41)", + "innerChanges": [ + { + "originalRange": "[63,17 -> 63,22]", + "modifiedRange": "[39,17 -> 39,29]" + }, + { + "originalRange": "[63,26 -> 63,44]", + "modifiedRange": "[39,33 -> 40,36]" + } + ] + }, + { + "originalRange": "[73,89)", + "modifiedRange": "[50,51)", + "innerChanges": [ + { + "originalRange": "[73,15 -> 73,16]", + "modifiedRange": "[50,15 -> 50,21]" + }, + { + "originalRange": "[73,19 -> 78,12]", + "modifiedRange": "[50,24 -> 50,29]" + }, + { + "originalRange": "[78,15 -> 79,16]", + "modifiedRange": "[50,32 -> 50,34]" + }, + { + "originalRange": "[79,19 -> 83,9]", + "modifiedRange": "[50,37 -> 50,47]" + }, + { + "originalRange": "[83,12 -> 84,24]", + "modifiedRange": "[50,50 -> 50,51]" + }, + { + "originalRange": "[84,52 -> 88,9]", + "modifiedRange": "[50,79 -> 50,79]" + } + ] + }, + { + "originalRange": "[91,92)", + "modifiedRange": "[53,55)", + "innerChanges": [ + { + "originalRange": "[91,17 -> 91,22]", + "modifiedRange": "[53,17 -> 53,29]" + }, + { + "originalRange": "[91,26 -> 91,44]", + "modifiedRange": "[53,33 -> 54,36]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json index 6bfa1f36e92..bdaa293acd4 100644 --- a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json @@ -33,8 +33,8 @@ "modifiedRange": "[745,4 -> 745,8]" }, { - "originalRange": "[744,59 -> 745,6]", - "modifiedRange": "[745,56 -> 745,59]" + "originalRange": "[744,59 -> 745,6 EOL]", + "modifiedRange": "[745,56 -> 745,59 EOL]" }, { "originalRange": "[746,24 -> 746,25]", diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/legacy.expected.diff.json index e48b9d56df1..924e83b900e 100644 --- a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/legacy.expected.diff.json @@ -33,16 +33,16 @@ "modifiedRange": "[745,4 -> 745,8]" }, { - "originalRange": "[744,59 -> 745,6]", - "modifiedRange": "[745,56 -> 745,59]" + "originalRange": "[744,59 -> 745,6 EOL]", + "modifiedRange": "[745,56 -> 745,59 EOL]" }, { "originalRange": "[746,24 -> 746,25]", "modifiedRange": "[746,26 -> 746,26]" }, { - "originalRange": "[746,37 -> 746,37]", - "modifiedRange": "[747,4 -> 750,20]" + "originalRange": "[746,37 -> 746,37 EOL]", + "modifiedRange": "[747,4 -> 750,20 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/just-whitespace/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/just-whitespace/advanced.expected.diff.json index a24c28bdcfa..1bff77e1739 100644 --- a/src/vs/editor/test/node/diffing/fixtures/just-whitespace/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/just-whitespace/advanced.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[1,2)", "innerChanges": [ { - "originalRange": "[1,20 -> 1,21]", - "modifiedRange": "[1,20 -> 1,20]" + "originalRange": "[1,20 -> 1,21 EOL]", + "modifiedRange": "[1,20 -> 1,20 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/just-whitespace/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/just-whitespace/legacy.expected.diff.json index a24c28bdcfa..1bff77e1739 100644 --- a/src/vs/editor/test/node/diffing/fixtures/just-whitespace/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/just-whitespace/legacy.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[1,2)", "innerChanges": [ { - "originalRange": "[1,20 -> 1,21]", - "modifiedRange": "[1,20 -> 1,20]" + "originalRange": "[1,20 -> 1,21 EOL]", + "modifiedRange": "[1,20 -> 1,20 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/method-splitting/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/method-splitting/legacy.expected.diff.json index e1f4edb8a10..a5e2847e3b9 100644 --- a/src/vs/editor/test/node/diffing/fixtures/method-splitting/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/method-splitting/legacy.expected.diff.json @@ -57,8 +57,8 @@ "modifiedRange": "[7,148 -> 7,170]" }, { - "originalRange": "[8,62 -> 9,3]", - "modifiedRange": "[7,181 -> 7,183]" + "originalRange": "[8,62 -> 9,3 EOL]", + "modifiedRange": "[7,181 -> 7,183 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/move-1/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/move-1/advanced.expected.diff.json index 73f6bfc728f..ad809ac7fe0 100644 --- a/src/vs/editor/test/node/diffing/fixtures/move-1/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/move-1/advanced.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[24,24)", "innerChanges": [ { - "originalRange": "[24,1 -> 28,1]", - "modifiedRange": "[24,1 -> 24,1]" + "originalRange": "[24,1 -> 28,1 EOL]", + "modifiedRange": "[24,1 -> 24,1 EOL]" } ] }, @@ -23,8 +23,8 @@ "modifiedRange": "[70,74)", "innerChanges": [ { - "originalRange": "[74,1 -> 74,1]", - "modifiedRange": "[70,1 -> 74,1]" + "originalRange": "[74,1 -> 74,1 EOL]", + "modifiedRange": "[70,1 -> 74,1 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json index 20190a83798..36bbcbebac9 100644 --- a/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json @@ -21,8 +21,8 @@ "modifiedRange": "[53,6 -> 53,45]" }, { - "originalRange": "[52,77 -> 56,1]", - "modifiedRange": "[53,98 -> 66,1]" + "originalRange": "[52,77 -> 56,1 EOL]", + "modifiedRange": "[53,98 -> 66,1 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/random-match-2/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/random-match-2/legacy.expected.diff.json index 7d0d8ed1629..b9a24abd3bd 100644 --- a/src/vs/editor/test/node/diffing/fixtures/random-match-2/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/random-match-2/legacy.expected.diff.json @@ -37,8 +37,8 @@ "modifiedRange": "[3,78 -> 3,82]" }, { - "originalRange": "[5,123 -> 6,3]", - "modifiedRange": "[3,120 -> 3,120]" + "originalRange": "[5,123 -> 6,3 EOL]", + "modifiedRange": "[3,120 -> 3,120 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/random-match-3/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/random-match-3/advanced.expected.diff.json index 869c81350b7..f690b15dd1d 100644 --- a/src/vs/editor/test/node/diffing/fixtures/random-match-3/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/random-match-3/advanced.expected.diff.json @@ -17,8 +17,8 @@ "modifiedRange": "[1,6 -> 2,18]" }, { - "originalRange": "[2,1 -> 2,1]", - "modifiedRange": "[3,1 -> 4,1]" + "originalRange": "[2,1 -> 2,1 EOL]", + "modifiedRange": "[3,1 -> 4,1 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/random-match-3/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/random-match-3/legacy.expected.diff.json index a1f2e8db4a5..328fc5656a4 100644 --- a/src/vs/editor/test/node/diffing/fixtures/random-match-3/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/random-match-3/legacy.expected.diff.json @@ -21,8 +21,8 @@ "modifiedRange": "[2,5 -> 2,18]" }, { - "originalRange": "[1,98 -> 1,98]", - "modifiedRange": "[2,71 -> 3,39]" + "originalRange": "[1,98 -> 1,98 EOL]", + "modifiedRange": "[2,71 -> 3,39 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/subword/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/subword/advanced.expected.diff.json index c8a5000a54c..d22795d014f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/subword/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/subword/advanced.expected.diff.json @@ -23,8 +23,8 @@ "modifiedRange": "[6,7)", "innerChanges": [ { - "originalRange": "[5,92 -> 5,92]", - "modifiedRange": "[5,92 -> 6,31]" + "originalRange": "[5,92 -> 5,92 EOL]", + "modifiedRange": "[5,92 -> 6,31 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/trivial/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/trivial/advanced.expected.diff.json index b88bd08c021..a92226e91aa 100644 --- a/src/vs/editor/test/node/diffing/fixtures/trivial/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/trivial/advanced.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[1,2)", "innerChanges": [ { - "originalRange": "[1,1 -> 1,1]", - "modifiedRange": "[1,1 -> 1,2]" + "originalRange": "[1,1 -> 1,1 EOL]", + "modifiedRange": "[1,1 -> 1,2 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/advanced.expected.diff.json index a1f9f55cf07..59ce8640941 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/advanced.expected.diff.json @@ -21,16 +21,16 @@ "modifiedRange": "[4,17 -> 4,28]" }, { - "originalRange": "[2,39 -> 2,40]", - "modifiedRange": "[4,50 -> 4,50]" + "originalRange": "[2,39 -> 2,40 EOL]", + "modifiedRange": "[4,50 -> 4,50 EOL]" }, { "originalRange": "[3,5 -> 3,9]", "modifiedRange": "[5,5 -> 5,5]" }, { - "originalRange": "[4,1 -> 5,1]", - "modifiedRange": "[6,1 -> 6,1]" + "originalRange": "[4,1 -> 5,1 EOL]", + "modifiedRange": "[6,1 -> 6,1 EOL]" } ] }, @@ -39,8 +39,8 @@ "modifiedRange": "[9,10)", "innerChanges": [ { - "originalRange": "[8,35 -> 8,35]", - "modifiedRange": "[9,35 -> 9,36]" + "originalRange": "[8,35 -> 8,35 EOL]", + "modifiedRange": "[9,35 -> 9,36 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/legacy.expected.diff.json index 3411d7fbc1a..ee4907cc31f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-advanced-bug/legacy.expected.diff.json @@ -21,16 +21,16 @@ "modifiedRange": "[4,20 -> 4,31]" }, { - "originalRange": "[2,39 -> 2,40]", - "modifiedRange": "[4,50 -> 4,50]" + "originalRange": "[2,39 -> 2,40 EOL]", + "modifiedRange": "[4,50 -> 4,50 EOL]" }, { "originalRange": "[3,5 -> 3,9]", "modifiedRange": "[5,5 -> 5,5]" }, { - "originalRange": "[3,57 -> 4,36]", - "modifiedRange": "[5,53 -> 5,53]" + "originalRange": "[3,57 -> 4,36 EOL]", + "modifiedRange": "[5,53 -> 5,53 EOL]" } ] }, @@ -39,8 +39,8 @@ "modifiedRange": "[9,10)", "innerChanges": [ { - "originalRange": "[8,35 -> 8,35]", - "modifiedRange": "[9,35 -> 9,36]" + "originalRange": "[8,35 -> 8,35 EOL]", + "modifiedRange": "[9,35 -> 9,36 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json index 0b00ea98beb..7e9d27ae2e0 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json @@ -37,8 +37,8 @@ "modifiedRange": "[6,7)", "innerChanges": [ { - "originalRange": "[6,85 -> 9,1]", - "modifiedRange": "[6,85 -> 6,140]" + "originalRange": "[6,85 -> 9,1 EOL]", + "modifiedRange": "[6,85 -> 6,140 EOL]" } ] }, @@ -47,8 +47,8 @@ "modifiedRange": "[8,12)", "innerChanges": [ { - "originalRange": "[11,10 -> 21,1]", - "modifiedRange": "[8,10 -> 12,1]" + "originalRange": "[11,10 -> 21,1 EOL]", + "modifiedRange": "[8,10 -> 12,1 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-class/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-class/legacy.expected.diff.json index bfb1d8aa5dc..ea38da72c7a 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-class/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-class/legacy.expected.diff.json @@ -36,8 +36,8 @@ "modifiedRange": "[6,85 -> 6,123]" }, { - "originalRange": "[8,57 -> 9,1]", - "modifiedRange": "[6,127 -> 6,140]" + "originalRange": "[8,57 -> 9,1 EOL]", + "modifiedRange": "[6,127 -> 6,140 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json index 3835a5ceb3c..dea51256ca6 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json @@ -65,8 +65,8 @@ "modifiedRange": "[21,18 -> 21,43]" }, { - "originalRange": "[17,71 -> 24,1]", - "modifiedRange": "[21,74 -> 22,1]" + "originalRange": "[17,71 -> 24,1 EOL]", + "modifiedRange": "[21,74 -> 22,1 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/legacy.expected.diff.json index f011be41a70..be3f0413bf3 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/legacy.expected.diff.json @@ -71,8 +71,8 @@ "modifiedRange": "[16,40 -> 16,47]" }, { - "originalRange": "[14,196 -> 14,211]", - "modifiedRange": "[16,51 -> 17,1]" + "originalRange": "[14,196 -> 14,211 EOL]", + "modifiedRange": "[16,51 -> 17,1 EOL]" } ] }, @@ -101,8 +101,8 @@ "modifiedRange": "[21,39 -> 21,43]" }, { - "originalRange": "[17,71 -> 17,72]", - "modifiedRange": "[21,74 -> 22,1]" + "originalRange": "[17,71 -> 17,72 EOL]", + "modifiedRange": "[21,74 -> 22,1 EOL]" }, { "originalRange": "[18,3 -> 19,26]", @@ -152,8 +152,8 @@ "modifiedRange": "[34,12 -> 36,16]" }, { - "originalRange": "[30,30 -> 30,31]", - "modifiedRange": "[36,20 -> 37,9]" + "originalRange": "[30,30 -> 30,31 EOL]", + "modifiedRange": "[36,20 -> 37,9 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-example2-ts/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-example2-ts/advanced.expected.diff.json index 647fee613e5..e4262043f06 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-example2-ts/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-example2-ts/advanced.expected.diff.json @@ -31,8 +31,8 @@ "modifiedRange": "[19,26)", "innerChanges": [ { - "originalRange": "[8,2 -> 8,2]", - "modifiedRange": "[18,2 -> 25,2]" + "originalRange": "[8,2 -> 8,2 EOL]", + "modifiedRange": "[18,2 -> 25,2 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/advanced.expected.diff.json index 93657f71909..5000ede1044 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/advanced.expected.diff.json @@ -27,8 +27,8 @@ "modifiedRange": "[7,1 -> 7,1]" }, { - "originalRange": "[7,9 -> 7,15]", - "modifiedRange": "[7,4 -> 7,4]" + "originalRange": "[7,9 -> 7,15 EOL]", + "modifiedRange": "[7,4 -> 7,4 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/legacy.expected.diff.json index 2c2c64d2b9c..1e160d871d4 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-fragmented-eager-diffing3/legacy.expected.diff.json @@ -27,8 +27,8 @@ "modifiedRange": "[7,1 -> 7,1]" }, { - "originalRange": "[7,9 -> 7,15]", - "modifiedRange": "[7,4 -> 7,4]" + "originalRange": "[7,9 -> 7,15 EOL]", + "modifiedRange": "[7,4 -> 7,4 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-methods/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-methods/legacy.expected.diff.json index 5034a4b336a..4f1a7a5647d 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-methods/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-methods/legacy.expected.diff.json @@ -21,8 +21,8 @@ "modifiedRange": "[3,12 -> 3,20]" }, { - "originalRange": "[3,91 -> 3,91]", - "modifiedRange": "[3,102 -> 4,73]" + "originalRange": "[3,91 -> 3,91 EOL]", + "modifiedRange": "[3,102 -> 4,73 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/legacy.expected.diff.json index aa65f823a85..3f20554ee6e 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/legacy.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[9,15)", "innerChanges": [ { - "originalRange": "[9,124 -> 9,124]", - "modifiedRange": "[9,124 -> 14,143]" + "originalRange": "[9,124 -> 9,124 EOL]", + "modifiedRange": "[9,124 -> 14,143 EOL]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/advanced.expected.diff.json index 5742291665d..23feeaa2119 100644 --- a/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/advanced.expected.diff.json @@ -83,8 +83,8 @@ "modifiedRange": "[43,44)", "innerChanges": [ { - "originalRange": "[43,5 -> 43,16]", - "modifiedRange": "[43,5 -> 43,11]" + "originalRange": "[43,5 -> 43,16 EOL]", + "modifiedRange": "[43,5 -> 43,11 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/legacy.expected.diff.json index 8f9a20db404..5cf3d49bc8f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/word-shared-letters/legacy.expected.diff.json @@ -95,8 +95,8 @@ "modifiedRange": "[43,8 -> 43,8]" }, { - "originalRange": "[43,14 -> 43,16]", - "modifiedRange": "[43,11 -> 43,11]" + "originalRange": "[43,14 -> 43,16 EOL]", + "modifiedRange": "[43,11 -> 43,11 EOL]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ws-alignment/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ws-alignment/legacy.expected.diff.json index c248859e098..0fff59a0214 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ws-alignment/legacy.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ws-alignment/legacy.expected.diff.json @@ -27,28 +27,28 @@ "modifiedRange": "[7,5 -> 8,5]" }, { - "originalRange": "[7,14 -> 7,43]", - "modifiedRange": "[8,8 -> 11,140]" + "originalRange": "[7,14 -> 7,43 EOL]", + "modifiedRange": "[8,8 -> 11,140 EOL]" }, { "originalRange": "[8,5 -> 8,6]", "modifiedRange": "[12,5 -> 12,25]" }, { - "originalRange": "[8,9 -> 9,12]", - "modifiedRange": "[12,28 -> 12,131]" + "originalRange": "[8,9 -> 9,12 EOL]", + "modifiedRange": "[12,28 -> 12,131 EOL]" }, { "originalRange": "[10,7 -> 10,22]", "modifiedRange": "[13,7 -> 13,17]" }, { - "originalRange": "[10,30 -> 10,48]", - "modifiedRange": "[13,25 -> 14,8]" + "originalRange": "[10,30 -> 10,48 EOL]", + "modifiedRange": "[13,25 -> 14,8 EOL]" }, { - "originalRange": "[11,6 -> 11,13]", - "modifiedRange": "[15,6 -> 15,7]" + "originalRange": "[11,6 -> 11,13 EOL]", + "modifiedRange": "[15,6 -> 15,7 EOL]" }, { "originalRange": "[12,5 -> 12,17]", diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2a6218c8de5..ab0e838f808 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -419,28 +419,28 @@ declare namespace monaco { * Either the angle bracket key or the backslash key on the RT 102-key keyboard. */ IntlBackslash = 97, - Numpad0 = 98, - Numpad1 = 99, - Numpad2 = 100, - Numpad3 = 101, - Numpad4 = 102, - Numpad5 = 103, - Numpad6 = 104, - Numpad7 = 105, - Numpad8 = 106, - Numpad9 = 107, - NumpadMultiply = 108, - NumpadAdd = 109, - NUMPAD_SEPARATOR = 110, - NumpadSubtract = 111, - NumpadDecimal = 112, - NumpadDivide = 113, + Numpad0 = 98,// VK_NUMPAD0, 0x60, Numeric keypad 0 key + Numpad1 = 99,// VK_NUMPAD1, 0x61, Numeric keypad 1 key + Numpad2 = 100,// VK_NUMPAD2, 0x62, Numeric keypad 2 key + Numpad3 = 101,// VK_NUMPAD3, 0x63, Numeric keypad 3 key + Numpad4 = 102,// VK_NUMPAD4, 0x64, Numeric keypad 4 key + Numpad5 = 103,// VK_NUMPAD5, 0x65, Numeric keypad 5 key + Numpad6 = 104,// VK_NUMPAD6, 0x66, Numeric keypad 6 key + Numpad7 = 105,// VK_NUMPAD7, 0x67, Numeric keypad 7 key + Numpad8 = 106,// VK_NUMPAD8, 0x68, Numeric keypad 8 key + Numpad9 = 107,// VK_NUMPAD9, 0x69, Numeric keypad 9 key + NumpadMultiply = 108,// VK_MULTIPLY, 0x6A, Multiply key + NumpadAdd = 109,// VK_ADD, 0x6B, Add key + NUMPAD_SEPARATOR = 110,// VK_SEPARATOR, 0x6C, Separator key + NumpadSubtract = 111,// VK_SUBTRACT, 0x6D, Subtract key + NumpadDecimal = 112,// VK_DECIMAL, 0x6E, Decimal key + NumpadDivide = 113,// VK_DIVIDE, 0x6F, /** * Cover all key codes when IME is processing input. */ KEY_IN_COMPOSITION = 114, - ABNT_C1 = 115, - ABNT_C2 = 116, + ABNT_C1 = 115,// Brazilian (ABNT) Keyboard + ABNT_C2 = 116,// Brazilian (ABNT) Keyboard AudioVolumeMute = 117, AudioVolumeUp = 118, AudioVolumeDown = 119, diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index e4422c393d6..6a4c9402ca2 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -6,6 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -24,6 +25,7 @@ export const previewSelectedActionCommand = 'previewSelectedCodeAction'; export interface IActionListDelegate { onHide(didCancel?: boolean): void; onSelect(action: T, preview?: boolean): void; + onFocus?(action: T, cancellationToken: CancellationToken): Promise<{ canPreview: boolean } | void>; } export interface IActionListItem { @@ -32,8 +34,8 @@ export interface IActionListItem { readonly group?: { kind?: any; icon?: ThemeIcon; title: string }; readonly disabled?: boolean; readonly label?: string; - readonly keybinding?: ResolvedKeybinding; + canPreview?: boolean | undefined; } interface IActionMenuTemplateData { @@ -126,7 +128,7 @@ class ActionItemRenderer implements IListRenderer, IAction if (element.disabled) { data.container.title = element.label; } else if (actionTitle && previewTitle) { - if (this._supportsPreview) { + if (this._supportsPreview && element.canPreview) { data.container.title = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to apply, Shift+F2 to preview"'] }, "{0} to apply, {1} to preview", actionTitle, previewTitle); } else { data.container.title = localize({ key: 'label', comment: ['placeholder is a keybinding, e.g "F2 to apply"'] }, "{0} to apply", actionTitle); @@ -168,6 +170,8 @@ export class ActionList extends Disposable { private readonly _allMenuItems: readonly IActionListItem[]; + private readonly cts = this._register(new CancellationTokenSource()); + constructor( user: string, preview: boolean, @@ -230,6 +234,7 @@ export class ActionList extends Disposable { hide(didCancel?: boolean): void { this._delegate.onHide(didCancel); + this.cts.cancel(); this._contextViewService.hideContextView(); } @@ -302,7 +307,18 @@ export class ActionList extends Disposable { } } - private onListHover(e: IListMouseEvent>): void { + private async onListHover(e: IListMouseEvent>) { + const element = e.element; + if (element && element.item && this.focusCondition(element)) { + if (this._delegate.onFocus && !element.disabled && element.kind === ActionListItemKind.Action) { + const result = await this._delegate.onFocus(element.item, this.cts.token); + element.canPreview = result ? result.canPreview : undefined; + } + if (e.index) { + this._list.splice(e.index, 1, [element]); + } + } + this._list.setFocus(typeof e.index === 'number' ? [e.index] : []); } diff --git a/src/vs/platform/externalTerminal/common/externalTerminal.ts b/src/vs/platform/externalTerminal/common/externalTerminal.ts index 290bb20fca3..6d8da336419 100644 --- a/src/vs/platform/externalTerminal/common/externalTerminal.ts +++ b/src/vs/platform/externalTerminal/common/externalTerminal.ts @@ -35,9 +35,3 @@ export interface IExternalTerminalConfiguration { } export const DEFAULT_TERMINAL_OSX = 'Terminal.app'; - -export const IExternalTerminalMainService = createDecorator('externalTerminal'); - -export interface IExternalTerminalMainService extends IExternalTerminalService { - readonly _serviceBrand: undefined; -} diff --git a/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts b/src/vs/platform/externalTerminal/electron-main/externalTerminal.ts similarity index 81% rename from src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts rename to src/vs/platform/externalTerminal/electron-main/externalTerminal.ts index 4f2ec5e3941..af6925563c7 100644 --- a/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts +++ b/src/vs/platform/externalTerminal/electron-main/externalTerminal.ts @@ -5,12 +5,9 @@ import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; export const IExternalTerminalMainService = createDecorator('externalTerminal'); export interface IExternalTerminalMainService extends IExternalTerminalService { readonly _serviceBrand: undefined; } - -registerMainProcessRemoteService(IExternalTerminalMainService, 'externalTerminal'); diff --git a/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalService.ts b/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalService.ts new file mode 100644 index 00000000000..209c24d072b --- /dev/null +++ b/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExternalTerminalService as ICommonExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; + +export const IExternalTerminalService = createDecorator('externalTerminal'); + +export interface IExternalTerminalService extends ICommonExternalTerminalService { + readonly _serviceBrand: undefined; +} + +registerMainProcessRemoteService(IExternalTerminalService, 'externalTerminal'); diff --git a/src/vs/platform/externalTerminal/node/externalTerminalService.ts b/src/vs/platform/externalTerminal/node/externalTerminalService.ts index 9b0b2ba548c..3dcc1c49d0a 100644 --- a/src/vs/platform/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -11,7 +11,7 @@ import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; import * as pfs from 'vs/base/node/pfs'; import * as processes from 'vs/base/node/processes'; import * as nls from 'vs/nls'; -import { DEFAULT_TERMINAL_OSX, IExternalTerminalMainService, IExternalTerminalSettings, ITerminalForPlatform } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { DEFAULT_TERMINAL_OSX, IExternalTerminalService, IExternalTerminalSettings, ITerminalForPlatform } from 'vs/platform/externalTerminal/common/externalTerminal'; import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); @@ -28,7 +28,7 @@ abstract class ExternalTerminalService { } } -export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { +export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService { private static readonly CMD = 'cmd.exe'; private static _DEFAULT_TERMINAL_WINDOWS: string; @@ -125,7 +125,7 @@ export class WindowsExternalTerminalService extends ExternalTerminalService impl } } -export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { +export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService { private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { @@ -215,7 +215,7 @@ export class MacExternalTerminalService extends ExternalTerminalService implemen } } -export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { +export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService { private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue..."); diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index b65c4e8705a..8a8d6a57bda 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -18,7 +18,7 @@ import { basename, dirname, extUri, extUriIgnorePathCase, IExtUri, isAbsolutePat import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError } from 'vs/platform/files/common/files'; +import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability } from 'vs/platform/files/common/files'; import { readFileIntoStream } from 'vs/platform/files/common/io'; import { ILogService } from 'vs/platform/log/common/log'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; @@ -359,10 +359,18 @@ export class FileService extends Disposable implements IFileService { const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); const { providerExtUri } = this.getExtUri(provider); + let writeFileOptions = options; + if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) { + const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource); + if (enforcedAtomicWrite) { + writeFileOptions = { ...options, atomic: enforcedAtomicWrite }; + } + } + try { // validate write - const stat = await this.validateWriteFile(provider, resource, options); + const stat = await this.validateWriteFile(provider, resource, writeFileOptions); // mkdir recursively as needed if (!stat) { @@ -389,9 +397,13 @@ export class FileService extends Disposable implements IFileService { bufferOrReadableOrStreamOrBufferedStream = bufferOrReadableOrStream; } - // write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability) - if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer)) { - await this.doWriteUnbuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream); + // write file: unbuffered + if ( + !hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported + (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already + (hasReadWriteCapability(provider) && hasFileAtomicWriteCapability(provider) && writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it + ) { + await this.doWriteUnbuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream); } // write file: buffered @@ -399,20 +411,20 @@ export class FileService extends Disposable implements IFileService { const contents = bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream; // atomic write - if (options?.atomic !== false && options?.atomic?.postfix) { - await this.doWriteBufferedAtomic(provider, resource, joinPath(dirname(resource), `${basename(resource)}${options.atomic.postfix}`), options, contents); + if (writeFileOptions?.atomic !== false && writeFileOptions?.atomic?.postfix) { + await this.doWriteBufferedAtomic(provider, resource, joinPath(dirname(resource), `${basename(resource)}${writeFileOptions.atomic.postfix}`), writeFileOptions, contents); } // non-atomic write else { - await this.doWriteBuffered(provider, resource, options, contents); + await this.doWriteBuffered(provider, resource, writeFileOptions, contents); } } // events this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE)); } catch (error) { - throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), writeFileOptions); } return this.resolve(resource, { resolveMetadata: true }); @@ -535,8 +547,13 @@ export class FileService extends Disposable implements IFileService { // cancellation of the read operation. const cancellableSource = new CancellationTokenSource(token); + let readFileOptions = options; + if (hasFileAtomicReadCapability(provider) && provider.enforceAtomicReadFile?.(resource)) { + readFileOptions = { ...options, atomic: true }; + } + // validate read operation - const statPromise = this.validateReadFile(resource, options).then(stat => stat, error => { + const statPromise = this.validateReadFile(resource, readFileOptions).then(stat => stat, error => { cancellableSource.dispose(true); throw error; @@ -549,27 +566,27 @@ export class FileService extends Disposable implements IFileService { // due to the likelihood of hitting a NOT_MODIFIED_SINCE result. // otherwise, we let it run in parallel to the file reading for // optimal startup performance. - if (typeof options?.etag === 'string' && options.etag !== ETAG_DISABLED) { + if (typeof readFileOptions?.etag === 'string' && readFileOptions.etag !== ETAG_DISABLED) { await statPromise; } // read unbuffered if ( - (options?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered + (readFileOptions?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered !(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || // provider has no buffered capability - (hasReadWriteCapability(provider) && options?.preferUnbuffered) // unbuffered read is preferred + (hasReadWriteCapability(provider) && readFileOptions?.preferUnbuffered) // unbuffered read is preferred ) { - fileStream = this.readFileUnbuffered(provider, resource, options); + fileStream = this.readFileUnbuffered(provider, resource, readFileOptions); } // read streamed (always prefer over primitive buffered read) else if (hasFileReadStreamCapability(provider)) { - fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, options); + fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, readFileOptions); } // read buffered else { - fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options); + fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, readFileOptions); } fileStream.on('end', () => cancellableSource.dispose()); @@ -592,7 +609,7 @@ export class FileService extends Disposable implements IFileService { // Re-throw errors as file operation errors but preserve // specific errors (such as not modified since) - throw this.restoreReadError(error, resource, options); + throw this.restoreReadError(error, resource, readFileOptions); } } @@ -1023,9 +1040,17 @@ export class FileService extends Disposable implements IFileService { async del(resource: URI, options?: Partial): Promise { const provider = await this.doValidateDelete(resource, options); - const useTrash = !!options?.useTrash; - const recursive = !!options?.recursive; - const atomic = options?.atomic ?? false; + let deleteFileOptions = options; + if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) { + const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource); + if (enforcedAtomicDelete) { + deleteFileOptions = { ...options, atomic: enforcedAtomicDelete }; + } + } + + const useTrash = !!deleteFileOptions?.useTrash; + const recursive = !!deleteFileOptions?.recursive; + const atomic = deleteFileOptions?.atomic ?? false; // Delete through provider await provider.delete(resource, { recursive, useTrash, atomic }); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4f7c149b6d6..0913cb6eaa8 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -657,6 +657,7 @@ export function hasFileReadStreamCapability(provider: IFileSystemProvider): prov export interface IFileSystemProviderWithFileAtomicReadCapability extends IFileSystemProvider { readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise; + enforceAtomicReadFile?(resource: URI): boolean; } export function hasFileAtomicReadCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicReadCapability { @@ -669,6 +670,7 @@ export function hasFileAtomicReadCapability(provider: IFileSystemProvider): prov export interface IFileSystemProviderWithFileAtomicWriteCapability extends IFileSystemProvider { writeFile(resource: URI, contents: Uint8Array, opts?: IFileAtomicWriteOptions): Promise; + enforceAtomicWriteFile?(resource: URI): IFileAtomicOptions | false; } export function hasFileAtomicWriteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicWriteCapability { @@ -681,6 +683,7 @@ export function hasFileAtomicWriteCapability(provider: IFileSystemProvider): pro export interface IFileSystemProviderWithFileAtomicDeleteCapability extends IFileSystemProvider { delete(resource: URI, opts: IFileAtomicDeleteOptions): Promise; + enforceAtomicDelete?(resource: URI): IFileAtomicOptions | false; } export function hasFileAtomicDeleteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicDeleteCapability { diff --git a/src/vs/platform/files/common/inMemoryFilesystemProvider.ts b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts index 9402533647e..f875c3be628 100644 --- a/src/vs/platform/files/common/inMemoryFilesystemProvider.ts +++ b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as resources from 'vs/base/common/resources'; +import { ReadableStreamEvents, newWriteableStream } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; -import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError } from 'vs/platform/files/common/files'; +import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability } from 'vs/platform/files/common/files'; class File implements IStat { - type: FileType.File; - ctime: number; + readonly type: FileType.File; + readonly ctime: number; mtime: number; size: number; @@ -30,13 +32,13 @@ class File implements IStat { class Directory implements IStat { - type: FileType.Directory; - ctime: number; + readonly type: FileType.Directory; + readonly ctime: number; mtime: number; size: number; name: string; - entries: Map; + readonly entries: Map; constructor(name: string) { this.type = FileType.Directory; @@ -50,8 +52,16 @@ class Directory implements IStat { type Entry = File | Directory; -export class InMemoryFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { +export class InMemoryFileSystemProvider extends Disposable implements + IFileSystemProviderWithFileReadWriteCapability, + IFileSystemProviderWithOpenReadWriteCloseCapability, + IFileSystemProviderWithFileReadStreamCapability, + IFileSystemProviderWithFileAtomicReadCapability, + IFileSystemProviderWithFileAtomicWriteCapability, + IFileSystemProviderWithFileAtomicDeleteCapability { + private memoryFdCounter = 0; + private readonly fdMemory = new Map(); private _onDidChangeCapabilities = this._register(new Emitter()); readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; @@ -92,6 +102,15 @@ export class InMemoryFileSystemProvider extends Disposable implements IFileSyste throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); } + readFileStream(resource: URI): ReadableStreamEvents { + const data = this._lookupAsFile(resource, false).data; + + const stream = newWriteableStream(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer); + stream.end(data); + + return stream; + } + async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { const basename = resources.basename(resource); const parent = this._lookupParentDirectory(resource); @@ -117,6 +136,44 @@ export class InMemoryFileSystemProvider extends Disposable implements IFileSyste this._fireSoon({ type: FileChangeType.UPDATED, resource }); } + // file open/read/write/close + open(resource: URI, opts: IFileOpenOptions): Promise { + const data = this._lookupAsFile(resource, false).data; + if (data) { + const fd = this.memoryFdCounter++; + this.fdMemory.set(fd, data); + return Promise.resolve(fd); + } + throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); + } + + close(fd: number): Promise { + this.fdMemory.delete(fd); + return Promise.resolve(); + } + + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const memory = this.fdMemory.get(fd); + if (!memory) { + throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable); + } + + const toWrite = VSBuffer.wrap(memory).slice(pos, pos + length); + data.set(toWrite.buffer, offset); + return Promise.resolve(toWrite.byteLength); + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const memory = this.fdMemory.get(fd); + if (!memory) { + throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable); + } + + const toWrite = VSBuffer.wrap(data).slice(offset, offset + length); + memory.set(toWrite.buffer, pos); + return Promise.resolve(toWrite.byteLength); + } + // --- manage files/folders async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { @@ -241,4 +298,10 @@ export class InMemoryFileSystemProvider extends Disposable implements IFileSyste this._bufferedChanges.length = 0; }, 5); } + + override dispose(): void { + super.dispose(); + + this.fdMemory.clear(); + } } diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 5b32db14d64..5d0b16250ed 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -5,12 +5,14 @@ import * as assert from 'assert'; import { DeferredPromise, timeout } from 'vs/base/common/async'; +import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat } from 'vs/platform/files/common/files'; +import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat, IFileAtomicReadOptions, IFileAtomicWriteOptions, IFileAtomicDeleteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileAtomicOptions } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -177,7 +179,7 @@ suite('File Service', () => { throw new Error('failed'); } - readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { + override readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { if (async) { const stream = newWriteableStream(chunk => chunk[0]); timeout(5, CancellationToken.None).then(() => stream.error(new Error('failed'))); @@ -232,7 +234,7 @@ suite('File Service', () => { }; } - readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { + override readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { const stream = newWriteableStream(chunk => chunk[0]); disposables.add(token.onCancellationRequested(() => { stream.error(new Error('Expected cancellation')); @@ -245,7 +247,7 @@ suite('File Service', () => { } }; - const disposable = service.registerProvider('test', provider); + disposables.add(service.registerProvider('test', provider)); provider.setCapabilities(FileSystemProviderCapabilities.FileReadStream); @@ -272,8 +274,96 @@ suite('File Service', () => { } assert.ok(e2); + }); - disposable.dispose(); + test('enforced atomic read/write/delete', async () => { + const service = disposables.add(new FileService(new NullLogService())); + + const atomicResource = URI.parse('test://foo/bar/atomic'); + const nonAtomicResource = URI.parse('test://foo/nonatomic'); + + let atomicReadCounter = 0; + let atomicWriteCounter = 0; + let atomicDeleteCounter = 0; + + const provider = new class extends NullFileSystemProvider implements IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability { + + override async stat(resource: URI): Promise { + return { + type: FileType.File, + ctime: Date.now(), + mtime: Date.now(), + size: 0 + }; + } + + override async readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise { + if (opts?.atomic) { + atomicReadCounter++; + } + return new Uint8Array(); + } + + override readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { + return newWriteableStream(chunk => chunk[0]); + } + + enforceAtomicReadFile(resource: URI): boolean { + return isEqual(resource, atomicResource); + } + + override async writeFile(resource: URI, content: Uint8Array, opts: IFileAtomicWriteOptions): Promise { + if (opts.atomic) { + atomicWriteCounter++; + } + } + + enforceAtomicWriteFile(resource: URI): IFileAtomicOptions | false { + return isEqual(resource, atomicResource) ? { postfix: '.tmp' } : false; + } + + override async delete(resource: URI, opts: IFileAtomicDeleteOptions): Promise { + if (opts.atomic) { + atomicDeleteCounter++; + } + } + + enforceAtomicDelete(resource: URI): IFileAtomicOptions | false { + return isEqual(resource, atomicResource) ? { postfix: '.tmp' } : false; + } + }; + + provider.setCapabilities( + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.FileOpenReadWriteClose | + FileSystemProviderCapabilities.FileReadStream | + FileSystemProviderCapabilities.FileAtomicRead | + FileSystemProviderCapabilities.FileAtomicWrite | + FileSystemProviderCapabilities.FileAtomicDelete + ); + + disposables.add(service.registerProvider('test', provider)); + + await service.readFile(atomicResource); + await service.readFile(nonAtomicResource); + await service.readFileStream(atomicResource); + await service.readFileStream(nonAtomicResource); + + await service.writeFile(atomicResource, VSBuffer.fromString('')); + await service.writeFile(nonAtomicResource, VSBuffer.fromString('')); + + await service.writeFile(atomicResource, bufferToStream(VSBuffer.fromString(''))); + await service.writeFile(nonAtomicResource, bufferToStream(VSBuffer.fromString(''))); + + await service.writeFile(atomicResource, bufferToReadable(VSBuffer.fromString(''))); + await service.writeFile(nonAtomicResource, bufferToReadable(VSBuffer.fromString(''))); + + await service.del(atomicResource); + await service.del(nonAtomicResource); + + assert.strictEqual(atomicReadCounter, 2); + assert.strictEqual(atomicWriteCounter, 3); + assert.strictEqual(atomicDeleteCounter, 1); }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/platform/files/test/common/nullFileSystemProvider.ts b/src/vs/platform/files/test/common/nullFileSystemProvider.ts index 291682d58c1..59862eaf63f 100644 --- a/src/vs/platform/files/test/common/nullFileSystemProvider.ts +++ b/src/vs/platform/files/test/common/nullFileSystemProvider.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; -import { IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileWriteOptions, IFileChange, IFileSystemProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileWriteOptions, IFileChange, IFileSystemProvider, IStat, IWatchOptions, IFileReadStreamOptions } from 'vs/platform/files/common/files'; export class NullFileSystemProvider implements IFileSystemProvider { @@ -36,11 +38,12 @@ export class NullFileSystemProvider implements IFileSystemProvider { async readdir(resource: URI): Promise<[string, FileType][]> { return undefined!; } async delete(resource: URI, opts: IFileDeleteOptions): Promise { return undefined; } async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { return undefined; } - async copy?(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { return undefined; } - async readFile?(resource: URI): Promise { return undefined!; } - async writeFile?(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { return undefined; } - async open?(resource: URI, opts: IFileOpenOptions): Promise { return undefined!; } - async close?(fd: number): Promise { return undefined; } - async read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return undefined!; } - async write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return undefined!; } + async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { return undefined; } + async readFile(resource: URI): Promise { return undefined!; } + readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { return undefined!; } + async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { return undefined; } + async open(resource: URI, opts: IFileOpenOptions): Promise { return undefined!; } + async close(fd: number): Promise { return undefined; } + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return undefined!; } + async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return undefined!; } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 75b13f0a8fc..f0d944ddf06 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -54,7 +54,7 @@ export class ListService implements IListService { declare readonly _serviceBrand: undefined; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private lists: IRegisteredList[] = []; private _lastFocusedWidget: WorkbenchListWidget | undefined = undefined; private _hasCreatedStyleController: boolean = false; diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 49e70c41c70..2ff14dbaa44 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -8,8 +8,10 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; import { matchesContiguousSubString, matchesPrefix, matchesWords, or } from 'vs/base/common/filters'; +import { once } from 'vs/base/common/functional'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; +import { TfIdfCalculator, normalizeTfIdfScores } from 'vs/base/common/tfIdf'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -25,6 +27,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export interface ICommandQuickPick extends IPickerQuickAccessItem { readonly commandId: string; readonly commandAlias?: string; + tfIdfScore?: number; readonly args?: any[]; } @@ -37,6 +40,9 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc static PREFIX = '>'; + private static readonly TFIDF_THRESHOLD = 0.5; + private static readonly TFIDF_MAX_RESULTS = 5; + private static WORD_FILTER = or(matchesPrefix, matchesWords, matchesContiguousSubString); private readonly commandsHistory = this._register(this.instantiationService.createInstance(CommandsHistory)); @@ -65,6 +71,19 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc return []; } + const runTfidf = once(() => { + const tfidf = new TfIdfCalculator(); + tfidf.updateDocuments(allCommandPicks.map(commandPick => ({ + key: commandPick.commandId, + textChunks: [commandPick.label + (commandPick.commandAlias ? ` ${commandPick.commandAlias}` : '')] + }))); + const result = tfidf.calculateScores(filter, token); + + return normalizeTfIdfScores(result) + .filter(score => score.score > AbstractCommandsQuickAccessProvider.TFIDF_THRESHOLD) + .slice(0, AbstractCommandsQuickAccessProvider.TFIDF_MAX_RESULTS); + }); + // Filter const filteredCommandPicks: ICommandQuickPick[] = []; for (const commandPick of allCommandPicks) { @@ -85,6 +104,21 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc else if (filter === commandPick.commandId) { filteredCommandPicks.push(commandPick); } + + // Handle tf-idf scoring for the rest if there's a filter + else if (filter.length >= 3) { + const tfidf = runTfidf(); + if (token.isCancellationRequested) { + return []; + } + + // Add if we have a tf-idf score + const tfidfScore = tfidf.find(score => score.key === commandPick.commandId); + if (tfidfScore) { + commandPick.tfIdfScore = tfidfScore.score; + filteredCommandPicks.push(commandPick); + } + } } // Add description to commands that have duplicate labels @@ -101,6 +135,18 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc // Sort by MRU order and fallback to name otherwise filteredCommandPicks.sort((commandPickA, commandPickB) => { + // If a result came from tf-idf, we want to put that towards the bottom + if (commandPickA.tfIdfScore && commandPickB.tfIdfScore) { + if (commandPickA.tfIdfScore === commandPickB.tfIdfScore) { + return commandPickA.label.localeCompare(commandPickB.label); // prefer lexicographically smaller command + } + return commandPickB.tfIdfScore - commandPickA.tfIdfScore; // prefer higher tf-idf score + } else if (commandPickA.tfIdfScore) { + return 1; // first command has a score but other doesn't so other wins + } else if (commandPickB.tfIdfScore) { + return -1; // other command has a score but first doesn't so first wins + } + const commandACounter = this.commandsHistory.peek(commandPickA.commandId); const commandBCounter = this.commandsHistory.peek(commandPickB.commandId); @@ -139,6 +185,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc const commandPicks: Array = []; let addOtherSeparator = false; + let addSuggestedSeparator = true; let addCommonlyUsedSeparator = !!this.options.suggestedCommandIds; for (let i = 0; i < filteredCommandPicks.length; i++) { const commandPick = filteredCommandPicks[i]; @@ -149,15 +196,20 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc addOtherSeparator = true; } + if (addSuggestedSeparator && commandPick.tfIdfScore !== undefined) { + commandPicks.push({ type: 'separator', label: localize('suggested', "similar commands") }); + addSuggestedSeparator = false; + } + // Separator: commonly used - if (addCommonlyUsedSeparator && !this.commandsHistory.peek(commandPick.commandId) && this.options.suggestedCommandIds?.has(commandPick.commandId)) { + if (addCommonlyUsedSeparator && commandPick.tfIdfScore === undefined && !this.commandsHistory.peek(commandPick.commandId) && this.options.suggestedCommandIds?.has(commandPick.commandId)) { commandPicks.push({ type: 'separator', label: localize('commonlyUsed', "commonly used") }); addOtherSeparator = true; addCommonlyUsedSeparator = false; } // Separator: other commands - if (addOtherSeparator && !this.commandsHistory.peek(commandPick.commandId) && !this.options.suggestedCommandIds?.has(commandPick.commandId)) { + if (addOtherSeparator && commandPick.tfIdfScore === undefined && !this.commandsHistory.peek(commandPick.commandId) && !this.options.suggestedCommandIds?.has(commandPick.commandId)) { commandPicks.push({ type: 'separator', label: localize('morecCommands', "other commands") }); addOtherSeparator = false; } @@ -178,7 +230,13 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc return []; } - return additionalCommandPicks.map(commandPick => this.toCommandPick(commandPick, runOptions)); + const commandPicks: Array = additionalCommandPicks.map(commandPick => this.toCommandPick(commandPick, runOptions)); + // Basically, if we haven't already added a separator, we add one before the additional picks so long + // as one hasn't been added to the start of the array. + if (addSuggestedSeparator && commandPicks[0]?.type !== 'separator') { + commandPicks.unshift({ type: 'separator', label: localize('suggested', "similar commands") }); + } + return commandPicks; })() }; } diff --git a/src/vs/platform/userData/common/fileUserDataProvider.ts b/src/vs/platform/userData/common/fileUserDataProvider.ts index 47239b78561..3cddb5514a3 100644 --- a/src/vs/platform/userData/common/fileUserDataProvider.ts +++ b/src/vs/platform/userData/common/fileUserDataProvider.ts @@ -2,16 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IFileSystemProviderWithFileReadWriteCapability, IFileChange, IWatchOptions, IStat, IFileOverwriteOptions, FileType, IFileWriteOptions, IFileDeleteOptions, FileSystemProviderCapabilities, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileFolderCopyCapability, hasFileFolderCopyCapability, hasFileAtomicWriteCapability } from 'vs/platform/files/common/files'; +import { IFileSystemProviderWithFileReadWriteCapability, IFileChange, IWatchOptions, IStat, IFileOverwriteOptions, FileType, IFileWriteOptions, IFileDeleteOptions, FileSystemProviderCapabilities, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IFileSystemProviderWithFileAtomicReadCapability, hasFileFolderCopyCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileCloneCapability, hasFileCloneCapability, IFileAtomicReadOptions, IFileAtomicOptions } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; +import { ReadableStreamEvents } from 'vs/base/common/stream'; import { ILogService } from 'vs/platform/log/common/log'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { isObject } from 'vs/base/common/types'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ResourceSet } from 'vs/base/common/map'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -23,44 +21,63 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' */ export class FileUserDataProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, + IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, + IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, - IFileSystemProviderWithFileFolderCopyCapability { + IFileSystemProviderWithFileAtomicWriteCapability, + IFileSystemProviderWithFileAtomicDeleteCapability, + IFileSystemProviderWithFileCloneCapability { - get capabilities() { return this.fileSystemProvider.capabilities & ~FileSystemProviderCapabilities.FileOpenReadWriteClose; } - readonly onDidChangeCapabilities: Event = this.fileSystemProvider.onDidChangeCapabilities; + readonly capabilities = this.fileSystemProvider.capabilities; + readonly onDidChangeCapabilities = this.fileSystemProvider.onDidChangeCapabilities; private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile: Event = this._onDidChangeFile.event; + readonly onDidChangeFile = this._onDidChangeFile.event; private readonly watchResources = TernarySearchTree.forUris(() => !(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive)); - private readonly atomicWritesResources: ResourceSet; + private readonly atomicReadWriteResources = new ResourceSet((uri) => this.uriIdentityService.extUri.getComparisonKey(this.toFileSystemResource(uri))); constructor( private readonly fileSystemScheme: string, - private readonly fileSystemProvider: IFileSystemProviderWithFileReadWriteCapability & (IFileSystemProviderWithFileReadStreamCapability | IFileSystemProviderWithFileAtomicReadCapability | IFileSystemProviderWithFileFolderCopyCapability), + private readonly fileSystemProvider: IFileSystemProviderWithFileReadWriteCapability & IFileSystemProviderWithOpenReadWriteCloseCapability & IFileSystemProviderWithFileReadStreamCapability & IFileSystemProviderWithFileAtomicReadCapability & IFileSystemProviderWithFileAtomicWriteCapability & IFileSystemProviderWithFileAtomicDeleteCapability, private readonly userDataScheme: string, private readonly userDataProfilesService: IUserDataProfilesService, - uriIdentityService: IUriIdentityService, + private readonly uriIdentityService: IUriIdentityService, private readonly logService: ILogService, ) { super(); - this.atomicWritesResources = new ResourceSet((uri) => uriIdentityService.extUri.getComparisonKey(this.toFileSystemResource(uri))); - this.updateAtomicWritesResources(); - this._register(userDataProfilesService.onDidChangeProfiles(() => this.updateAtomicWritesResources())); + this.updateAtomicReadWritesResources(); + this._register(userDataProfilesService.onDidChangeProfiles(() => this.updateAtomicReadWritesResources())); this._register(this.fileSystemProvider.onDidChangeFile(e => this.handleFileChanges(e))); } - private updateAtomicWritesResources(): void { - this.atomicWritesResources.clear(); + private updateAtomicReadWritesResources(): void { + this.atomicReadWriteResources.clear(); for (const profile of this.userDataProfilesService.profiles) { - this.atomicWritesResources.add(profile.settingsResource); - this.atomicWritesResources.add(profile.keybindingsResource); - this.atomicWritesResources.add(profile.tasksResource); - this.atomicWritesResources.add(profile.extensionsResource); + this.atomicReadWriteResources.add(profile.settingsResource); + this.atomicReadWriteResources.add(profile.keybindingsResource); + this.atomicReadWriteResources.add(profile.tasksResource); + this.atomicReadWriteResources.add(profile.extensionsResource); } } + open(resource: URI, opts: IFileOpenOptions): Promise { + return this.fileSystemProvider.open(this.toFileSystemResource(resource), opts); + } + + close(fd: number): Promise { + return this.fileSystemProvider.close(fd); + } + + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this.fileSystemProvider.read(fd, pos, data, offset, length); + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this.fileSystemProvider.write(fd, pos, data, offset, length); + } + watch(resource: URI, opts: IWatchOptions): IDisposable { this.watchResources.set(resource, resource); const disposable = this.fileSystemProvider.watch(this.toFileSystemResource(resource), opts); @@ -82,35 +99,34 @@ export class FileUserDataProvider extends Disposable implements return this.fileSystemProvider.rename(this.toFileSystemResource(from), this.toFileSystemResource(to), opts); } - readFile(resource: URI): Promise { - return this.fileSystemProvider.readFile(this.toFileSystemResource(resource), { atomic: true }); + readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise { + return this.fileSystemProvider.readFile(this.toFileSystemResource(resource), opts); } readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { - const stream = newWriteableStream(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer); - (async () => { - try { - const contents = await this.readFile(resource); - stream.end(contents); - } catch (error) { - stream.error(error); - stream.end(); - } - })(); - return stream; + return this.fileSystemProvider.readFileStream(this.toFileSystemResource(resource), opts, token); } readdir(resource: URI): Promise<[string, FileType][]> { return this.fileSystemProvider.readdir(this.toFileSystemResource(resource)); } + enforceAtomicReadFile(resource: URI): boolean { + return this.atomicReadWriteResources.has(resource); + } + writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { - if (this.atomicWritesResources.has(resource) && !isObject(opts.atomic) && hasFileAtomicWriteCapability(this.fileSystemProvider)) { - opts = { ...opts, atomic: { postfix: '.vsctmp' } }; - } return this.fileSystemProvider.writeFile(this.toFileSystemResource(resource), content, opts); } + enforceAtomicWriteFile(resource: URI): IFileAtomicOptions | false { + if (this.atomicReadWriteResources.has(resource)) { + return { postfix: '.vsctmp' }; + } + + return false; + } + delete(resource: URI, opts: IFileDeleteOptions): Promise { return this.fileSystemProvider.delete(this.toFileSystemResource(resource), opts); } @@ -122,6 +138,13 @@ export class FileUserDataProvider extends Disposable implements throw new Error('copy not supported'); } + cloneFile(from: URI, to: URI): Promise { + if (hasFileCloneCapability(this.fileSystemProvider)) { + return this.fileSystemProvider.cloneFile(this.toFileSystemResource(from), this.toFileSystemResource(to)); + } + throw new Error('clone not supported'); + } + private handleFileChanges(changes: readonly IFileChange[]): void { const userDataChanges: IFileChange[] = []; for (const change of changes) { diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index 2a88a1d2c5f..ef65f7c57e5 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IFileService, FileChangeType, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, FileType, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService, FileChangeType, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, FileType, FileSystemProviderCapabilities, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; @@ -21,6 +21,8 @@ import product from 'vs/platform/product/common/product'; import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ReadableStreamEvents } from 'vs/base/common/stream'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -278,10 +280,11 @@ suite('FileUserDataProvider', () => { }); }); -class TestFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability { +class TestFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability { constructor(readonly onDidChangeFile: Event) { } + readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite; readonly onDidChangeCapabilities: Event = Event.None; @@ -301,7 +304,12 @@ class TestFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapa writeFile(): Promise { throw new Error('Not Supported'); } delete(): Promise { throw new Error('Not Supported'); } + open(resource: URI, opts: IFileOpenOptions): Promise { throw new Error('Not Supported'); } + close(fd: number): Promise { throw new Error('Not Supported'); } + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { throw new Error('Not Supported'); } + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { throw new Error('Not Supported'); } + readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { throw new Error('Method not implemented.'); } } suite('FileUserDataProvider - Watching', () => { diff --git a/src/vs/platform/voiceRecognition/node/voiceRecognitionService.ts b/src/vs/platform/voiceRecognition/node/voiceRecognitionService.ts index a63b58f6224..03f7a16ca95 100644 --- a/src/vs/platform/voiceRecognition/node/voiceRecognitionService.ts +++ b/src/vs/platform/voiceRecognition/node/voiceRecognitionService.ts @@ -37,8 +37,6 @@ export class VoiceRecognitionService implements IVoiceRecognitionService { ) { } async transcribe(channelData: Float32Array, cancellation: CancellationToken): Promise { - this.logService.info(`[voice] transcribe(${channelData.length}): Begin`); - const modulePath = process.env.VSCODE_VOICE_MODULE_PATH; // TODO@bpasero package if (!modulePath || this.productService.quality === 'stable') { this.logService.error(`[voice] transcribe(${channelData.length}): Voice recognition not yet supported`); @@ -47,15 +45,12 @@ export class VoiceRecognitionService implements IVoiceRecognitionService { const now = Date.now(); - this.logService.info(`[voice] transcribe(${channelData.length}): Getting module from ${modulePath}`); - try { const voiceModule: { transcribe: ( audioBuffer: { channelCount: 1; samplingRate: 16000; bitDepth: 16; channelData: Float32Array }, options: { language: string | 'auto'; - suppressNonSpeechTokens: boolean; signal: AbortSignal; } ) => Promise; @@ -71,15 +66,14 @@ export class VoiceRecognitionService implements IVoiceRecognitionService { channelData }, { language: 'en', - suppressNonSpeechTokens: true, signal: abortController.signal }); - this.logService.info(`[voice] transcribe(${channelData.length}): End (text: "${text}", took: ${Date.now() - now}ms)`); + this.logService.info(`[voice] transcribe(${channelData.length}): Text "${text}", took ${Date.now() - now}ms)`); return text; } catch (error) { - this.logService.error(`[voice] transcribe(${channelData.length}): Failed (error: "${error}", took: ${Date.now() - now}ms)`); + this.logService.error(`[voice] transcribe(${channelData.length}): Failed width "${error}", took ${Date.now() - now}ms)`); throw error; } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index abb8b45a8ad..1abc2a81ab3 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -21,6 +21,7 @@ import './mainThreadLocalization'; import './mainThreadBulkEdits'; import './mainThreadChatProvider'; import './mainThreadChatSlashCommands'; +import './mainThreadChatAgents'; import './mainThreadChatVariables'; import './mainThreadCodeInsets'; import './mainThreadCLICommands'; diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents.ts b/src/vs/workbench/api/browser/mainThreadChatAgents.ts new file mode 100644 index 00000000000..52b8106cba8 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadChatAgents.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableMap } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { ExtHostChatAgentsShape, ExtHostContext, MainContext, MainThreadChatAgentsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IChatAgentMetadata, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; + + +@extHostNamedCustomer(MainContext.MainThreadChatAgents) +export class MainThreadChatAgents implements MainThreadChatAgentsShape { + + private readonly _agents = new DisposableMap; + private readonly _pendingProgress = new Map>(); + private readonly _proxy: ExtHostChatAgentsShape; + + constructor( + extHostContext: IExtHostContext, + @IChatAgentService private readonly _chatAgentService: IChatAgentService + ) { + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents); + } + + $unregisterAgent(handle: number): void { + this._agents.deleteAndDispose(handle); + } + + dispose(): void { + this._agents.clearAndDisposeAll(); + } + + $registerAgent(handle: number, name: string, metadata: IChatAgentMetadata): void { + if (!this._chatAgentService.hasAgent(name)) { + // dynamic! + this._chatAgentService.registerAgentData({ + id: name, + metadata: revive(metadata) + }); + } + + const d = this._chatAgentService.registerAgentCallback(name, async (prompt, progress, history, token) => { + const requestId = Math.random(); + this._pendingProgress.set(requestId, progress); + try { + return await this._proxy.$invokeAgent(handle, requestId, prompt, { history }, token); + } finally { + this._pendingProgress.delete(requestId); + } + }); + this._agents.set(handle, d); + } + + async $handleProgressChunk(requestId: number, chunk: IChatSlashFragment): Promise { + this._pendingProgress.get(requestId)?.report(revive(chunk)); + } + + $unregisterCommand(handle: number): void { + this._agents.deleteAndDispose(handle); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 8f7c4742fea..5ddf13e573e 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 } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ISplice, Sequence } from 'vs/base/common/sequence'; @@ -16,6 +16,20 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; + +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) }; + } +} class MainThreadSCMResourceGroup implements ISCMResourceGroup { @@ -90,6 +104,55 @@ class MainThreadSCMResource implements ISCMResource { } } +class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { + + private _onDidChangeActionButton = new Emitter(); + readonly onDidChangeActionButton = this._onDidChangeActionButton.event; + + private _onDidChangeCurrentHistoryItemGroup = new Emitter(); + readonly onDidChangeCurrentHistoryItemGroup = this._onDidChangeCurrentHistoryItemGroup.event; + + private _actionButton: ISCMActionButtonDescriptor | undefined; + get actionButton(): ISCMActionButtonDescriptor | undefined { return this._actionButton; } + set actionButton(actionButton: ISCMActionButtonDescriptor | undefined) { + this._actionButton = actionButton; + this._onDidChangeActionButton.fire(); + } + + private _currentHistoryItemGroup: ISCMHistoryItemGroup | undefined; + get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } + set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined) { + this._currentHistoryItemGroup = historyItemGroup; + this._onDidChangeCurrentHistoryItemGroup.fire(); + } + + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } + + async resolveHistoryItemGroupBase(historyItemGroupId: string): Promise { + return this.proxy.$resolveHistoryItemGroupBase(this.handle, historyItemGroupId, CancellationToken.None); + } + + async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string): Promise<{ id: string; ahead: number; behind: number } | undefined> { + return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None); + } + + 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), })); + } + + async provideHistoryItemChanges(historyItemId: string): Promise { + const changes = await this.proxy.$provideHistoryItemChanges(this.handle, historyItemId, CancellationToken.None); + return changes?.map(change => ({ + uri: URI.revive(change.uri), + originalUri: change.originalUri && URI.revive(change.originalUri), + modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri), + renameUri: change.renameUri && URI.revive(change.renameUri) + })); + } + +} + class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private static ID_HANDLE = 0; @@ -121,6 +184,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get contextValue(): string { return this._contextValue; } get commitTemplate(): string { return this.features.commitTemplate || ''; } + get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; } get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; } @@ -132,12 +196,17 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _onDidChangeStatusBarCommands = new Emitter(); get onDidChangeStatusBarCommands(): Event { return this._onDidChangeStatusBarCommands.event; } + private readonly _onDidChangeHistoryProvider = new Emitter(); + readonly onDidChangeHistoryProvider: Event = this._onDidChangeHistoryProvider.event; + private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; private _quickDiff: IDisposable | undefined; public readonly isSCM: boolean = true; + private _historyProvider: ISCMHistoryProvider | undefined; + constructor( private readonly proxy: ExtHostSCMShape, private readonly _handle: number, @@ -171,6 +240,14 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._quickDiff.dispose(); this._quickDiff = undefined; } + + if (features.hasHistoryProvider && !this._historyProvider) { + this._historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); + this._onDidChangeHistoryProvider.fire(); + } else if (features.hasHistoryProvider === false && this._historyProvider) { + this._historyProvider = undefined; + this._onDidChangeHistoryProvider.fire(); + } } $registerGroups(_groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][]): void { @@ -280,6 +357,22 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { return result && URI.revive(result); } + $onDidChangeHistoryProviderActionButton(actionButton?: SCMActionButtonDto | null): void { + if (!this._historyProvider) { + return; + } + + this._historyProvider.actionButton = actionButton ?? undefined; + } + + $onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void { + if (!this._historyProvider) { + return; + } + + this._historyProvider.currentHistoryItemGroup = currentHistoryItemGroup ?? undefined; + } + toJSON(): any { return { $mid: MarshalledId.ScmProvider, @@ -486,4 +579,26 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.validateInput = async () => undefined; } } + + $onDidChangeHistoryProviderActionButton(sourceControlHandle: number, actionButton?: SCMActionButtonDto | null | undefined): void { + const repository = this._repositories.get(sourceControlHandle); + + if (!repository) { + return; + } + + const provider = repository.provider as MainThreadSCMProvider; + provider.$onDidChangeHistoryProviderActionButton(actionButton); + } + + $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void { + const repository = this._repositories.get(sourceControlHandle); + + if (!repository) { + return; + } + + const provider = repository.provider as MainThreadSCMProvider; + provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7e68b4409db..948c269fa21 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -108,6 +108,7 @@ import { ExtHostChatSlashCommands } from 'vs/workbench/api/common/extHostChatSla import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables'; import { ExtHostRelatedInformation } from 'vs/workbench/api/common/extHostAiRelatedInformation'; import { ExtHostAiEmbeddingVector } from 'vs/workbench/api/common/extHostEmbeddingVector'; +import { ExtHostChatAgents } from 'vs/workbench/api/common/extHostChatAgents'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -209,6 +210,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInlineChat, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService)); const extHostChatProvider = rpcProtocol.set(ExtHostContext.ExtHostChatProvider, new ExtHostChatProvider(rpcProtocol, extHostLogService)); const extHostChatSlashCommands = rpcProtocol.set(ExtHostContext.ExtHostChatSlashCommands, new ExtHostChatSlashCommands(rpcProtocol, extHostChatProvider, extHostLogService)); + const extHostChatAgents = rpcProtocol.set(ExtHostContext.ExtHostChatAgents, new ExtHostChatAgents(rpcProtocol, extHostChatProvider, extHostLogService)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol, extHostLogService)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); @@ -1360,7 +1362,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); + }, + registerAgent(name: string, agent: vscode.ChatAgent, metadata: vscode.ChatAgentMetadata) { + checkProposedApiEnabled(extension, 'chatAgents'); + return extHostChatAgents.registerAgent(extension.identifier, name, agent, metadata); } + }; return { @@ -1502,6 +1509,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextEditorLineNumbersStyle: extHostTypes.TextEditorLineNumbersStyle, TextEditorRevealType: extHostTypes.TextEditorRevealType, TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind, + SyntaxTokenType: extHostTypes.SyntaxTokenType, TextDocumentChangeReason: extHostTypes.TextDocumentChangeReason, ThemeColor: extHostTypes.ThemeColor, ThemeIcon: extHostTypes.ThemeIcon, @@ -1577,7 +1585,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, StackFrameFocus: extHostTypes.StackFrameFocus, ThreadFocus: extHostTypes.ThreadFocus, - NotebookCodeActionKind: extHostTypes.NotebookCodeActionKind, RelatedInformationType: extHostTypes.RelatedInformationType }; }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index eaf7745216b..ea5ccab44fc 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -26,7 +26,6 @@ import * as languages from 'vs/editor/common/languages'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/languages/languageConfiguration'; import { EndOfLineSequence } from 'vs/editor/common/model'; import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; -import { IInlineChatRequest, IInlineChatResponse, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -50,8 +49,13 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider'; +import { IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatResponseErrorDetails, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; -import { IChatResponseErrorDetails, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatMessageResponse, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -61,7 +65,8 @@ import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; -import { IAuthenticationCreateSessionOptions, AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication'; +import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; @@ -73,10 +78,6 @@ 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 { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider'; -import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -1154,6 +1155,16 @@ export interface ExtHostChatSlashCommandsShape { $executeCommand(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise; } +export interface MainThreadChatAgentsShape extends IDisposable { + $registerAgent(handle: number, name: string, metadata: IChatAgentMetadata): void; + $unregisterAgent(handle: number): void; + $handleProgressChunk(requestId: number, chunk: IChatSlashFragment): Promise; +} + +export interface ExtHostChatAgentsShape { + $invokeAgent(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise; +} + export interface MainThreadChatVariablesShape extends IDisposable { $registerVariable(handle: number, data: IChatVariableData): void; $unregisterVariable(handle: number): void; @@ -1169,7 +1180,7 @@ export interface MainThreadInlineChatShape extends IDisposable { $unregisterInteractiveEditorProvider(handle: number): Promise; } -export type IInlineChatResponseDto = Dto; +export type IInlineChatResponseDto = Dto & { edits: IWorkspaceEditDto } | IInlineChatMessageResponse>; export interface ExtHostInlineChatShape { $prepareSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise; @@ -1348,6 +1359,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { } export interface SCMProviderFeatures { + hasHistoryProvider?: boolean; hasQuickDiffProvider?: boolean; quickDiffLabel?: string; count?: number; @@ -1390,6 +1402,33 @@ export type SCMRawResourceSplices = [ SCMRawResourceSplice[] ]; +export interface SCMHistoryItemGroupDto { + readonly id: string; + readonly label: string; + readonly upstream?: SCMRemoteHistoryItemGroupDto; +} + +export interface SCMRemoteHistoryItemGroupDto { + readonly id: string; + readonly label: string; +} + +export interface SCMHistoryItemDto { + readonly id: string; + readonly parentIds: string[]; + readonly label: string; + readonly description?: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + readonly timestamp?: number; +} + +export interface SCMHistoryItemChangeDto { + readonly uri: UriComponents; + readonly originalUri: UriComponents | undefined; + readonly modifiedUri: UriComponents | undefined; + readonly renameUri: UriComponents | undefined; +} + export interface MainThreadSCMShape extends IDisposable { $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; @@ -1408,6 +1447,9 @@ export interface MainThreadSCMShape extends IDisposable { $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void; $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void; $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void; + + $onDidChangeHistoryProviderActionButton(sourceControlHandle: number, actionButton?: SCMActionButtonDto | null): void; + $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void; } export interface MainThreadQuickDiffShape extends IDisposable { @@ -2115,6 +2157,10 @@ export interface ExtHostSCMShape { $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise; $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; + $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise; + $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise; + $resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise; + $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; } export interface ExtHostQuickDiffShape { @@ -2605,6 +2651,7 @@ export const MainContext = { MainThreadBulkEdits: createProxyIdentifier('MainThreadBulkEdits'), MainThreadChatProvider: createProxyIdentifier('MainThreadChatProvider'), MainThreadChatSlashCommands: createProxyIdentifier('MainThreadChatSlashCommands'), + MainThreadChatAgents: createProxyIdentifier('MainThreadChatAgents'), MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), MainThreadCommands: createProxyIdentifier('MainThreadCommands'), @@ -2725,6 +2772,7 @@ export const ExtHostContext = { ExtHostInlineChat: createProxyIdentifier('ExtHostInlineChatShape'), ExtHostChat: createProxyIdentifier('ExtHostChat'), ExtHostChatSlashCommands: createProxyIdentifier('ExtHostChatSlashCommands'), + ExtHostChatAgents: createProxyIdentifier('ExtHostChatAgents'), ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), diff --git a/src/vs/workbench/api/common/extHostChatAgents.ts b/src/vs/workbench/api/common/extHostChatAgents.ts new file mode 100644 index 00000000000..1fc42c5a393 --- /dev/null +++ b/src/vs/workbench/api/common/extHostChatAgents.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DeferredPromise, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { ExtHostChatAgentsShape, IMainContext, MainContext, MainThreadChatAgentsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { ChatMessageRole } from 'vs/workbench/api/common/extHostTypes'; +import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; +import type * as vscode from 'vscode'; + +export class ExtHostChatAgents implements ExtHostChatAgentsShape { + + private static _idPool = 0; + + private readonly _agents = new Map(); + private readonly _proxy: MainThreadChatAgentsShape; + + constructor( + mainContext: IMainContext, + private readonly _extHostChatProvider: ExtHostChatProvider, + private readonly _logService: ILogService, + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents); + } + + registerAgent(extension: ExtensionIdentifier, name: string, agent: vscode.ChatAgent, metadata: vscode.ChatAgentMetadata): IDisposable { + const handle = ExtHostChatAgents._idPool++; + this._agents.set(handle, { extension, agent }); + this._proxy.$registerAgent(handle, name, metadata); + + return toDisposable(() => { + this._proxy.$unregisterAgent(handle); + this._agents.delete(handle); + }); + } + + async $invokeAgent(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise { + const data = this._agents.get(handle); + if (!data) { + this._logService.warn(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); + return; + } + + let done = false; + function throwIfDone() { + if (done) { + throw new Error('Only valid while executing the command'); + } + } + + const commandExecution = new DeferredPromise(); + token.onCancellationRequested(() => commandExecution.complete()); + setTimeout(() => commandExecution.complete(), 3 * 1000); + this._extHostChatProvider.allowListExtensionWhile(data.extension, commandExecution.p); + + const task = data.agent( + { role: ChatMessageRole.User, content: prompt }, + { history: context.history.map(typeConvert.ChatMessage.to) }, + new Progress(p => { + throwIfDone(); + this._proxy.$handleProgressChunk(requestId, { content: isInteractiveProgressFileTree(p.message) ? p.message : p.message.value }); + }), + token + ); + + try { + return await raceCancellation(Promise.resolve(task).then((v) => { + if (v && 'followUp' in v) { + const convertedFollowup = v?.followUp?.map(f => typeConvert.ChatFollowup.from(f)); + return { followUp: convertedFollowup }; + } + return undefined; + }), token); + } finally { + done = true; + commandExecution.complete(); + } + } +} + +function isInteractiveProgressFileTree(thing: unknown): thing is vscode.InteractiveProgressFileTree { + return !!thing && typeof thing === 'object' && 'treeData' in thing; +} diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 79f86de8eb8..bf6758e93ae 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -15,7 +15,7 @@ import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _handlePool = 0; - private _disposables = new DisposableStore(); + private readonly _disposables = new DisposableStore(); private _insets = new Map }>(); constructor( diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 99fccebaee0..404f8f7d9c0 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -157,9 +157,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToStartExtensionHost = new Barrier(); this._readyToRunExtensions = new Barrier(); this._eagerExtensionsActivated = new Barrier(); - this._activationEventsReader = new SyncedActivationEventsReader(this._initData.activationEvents); - this._globalRegistry = new ExtensionDescriptionRegistry(this._activationEventsReader, this._initData.allExtensions); - const myExtensionsSet = new ExtensionIdentifierSet(this._initData.myExtensions); + this._activationEventsReader = new SyncedActivationEventsReader(this._initData.extensions.activationEvents); + this._globalRegistry = new ExtensionDescriptionRegistry(this._activationEventsReader, this._initData.extensions.allExtensions); + const myExtensionsSet = new ExtensionIdentifierSet(this._initData.extensions.myExtensions); this._myRegistry = new ExtensionDescriptionRegistry( this._activationEventsReader, filterExtensions(this._globalRegistry, myExtensionsSet) diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ec2fbcebeef..bead4ad7611 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { equals, mixin } from 'vs/base/common/objects'; import type * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKind, InternalDataTransferItem, CodeActionTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKind, InternalDataTransferItem, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import * as languages from 'vs/editor/common/languages'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -33,9 +33,10 @@ import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; import { isCancellationError, NotImplementedError } from 'vs/base/common/errors'; import { raceCancellationError } from 'vs/base/common/async'; -import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { localize } from 'vs/nls'; +import { IAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; // --- adapter @@ -376,7 +377,6 @@ class CodeActionAdapter { private readonly _cache = new Cache('CodeAction'); private readonly _disposables = new Map(); - private readonly nbKind = new CodeActionKind('notebook'); constructor( private readonly _documents: ExtHostDocuments, @@ -436,10 +436,6 @@ class CodeActionAdapter { command: this._commands.toInternal(candidate, disposables), }); } else { - if (codeActionContext.triggerKind !== CodeActionTriggerKind.Invoke && candidate.kind && this.nbKind.contains(candidate.kind)) { - continue; - } - if (codeActionContext.only) { if (!candidate.kind) { this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); @@ -2564,6 +2560,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return onEnterRules.map(ExtHostLanguageFeatures._serializeOnEnterRule); } + private static _serializeAutoClosingPair(autoClosingPair: vscode.AutoClosingPair): IAutoClosingPairConditional { + return { + open: autoClosingPair.open, + close: autoClosingPair.close, + notIn: autoClosingPair.notIn ? autoClosingPair.notIn.map(v => SyntaxTokenType.toString(v)) : undefined, + }; + } + + private static _serializeAutoClosingPairs(autoClosingPairs: vscode.AutoClosingPair[]): IAutoClosingPairConditional[] { + return autoClosingPairs.map(ExtHostLanguageFeatures._serializeAutoClosingPair); + } + setLanguageConfiguration(extension: IExtensionDescription, languageId: string, configuration: vscode.LanguageConfiguration): vscode.Disposable { const { wordPattern } = configuration; @@ -2589,10 +2597,6 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF `Do not use.`); } - if (configuration.autoClosingPairs) { - checkProposedApiEnabled(extension, 'languageConfigurationAutoClosingPairs'); - } - const handle = this._nextHandle(); const serializedConfiguration: extHostProtocol.ILanguageConfigurationDto = { comments: configuration.comments, @@ -2602,7 +2606,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF onEnterRules: configuration.onEnterRules ? ExtHostLanguageFeatures._serializeOnEnterRules(configuration.onEnterRules) : undefined, __electricCharacterSupport: configuration.__electricCharacterSupport, __characterPairSupport: configuration.__characterPairSupport, - autoClosingPairs: configuration.autoClosingPairs + autoClosingPairs: configuration.autoClosingPairs ? ExtHostLanguageFeatures._serializeAutoClosingPairs(configuration.autoClosingPairs) : undefined, }; this._proxy.$setLanguageConfiguration(handle, languageId, serializedConfiguration); diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index a8d22102648..561592baf84 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { asPromise } from 'vs/base/common/async'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures } from './extHost.protocol'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemGroupDto } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; import type * as vscode from 'vscode'; @@ -45,6 +45,19 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor } } +function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { + if (!historyItem.icon) { + return undefined; + } else if (URI.isUri(historyItem.icon)) { + return historyItem.icon; + } else if (ThemeIcon.isThemeIcon(historyItem.icon)) { + return historyItem.icon; + } else { + const icon = historyItem.icon as { light: URI; dark: URI }; + return { light: icon.light, dark: icon.dark }; + } +} + function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { if (!a.iconPath && !b.iconPath) { return 0; @@ -197,6 +210,18 @@ function commandListEquals(a: readonly vscode.Command[], b: readonly vscode.Comm return equals(a, b, commandEquals); } +function historyItemGroupEquals(a: vscode.SourceControlHistoryItemGroup | undefined, b: vscode.SourceControlHistoryItemGroup | undefined): boolean { + if (a === b) { + return true; + } + + if (!a || !b) { + return false; + } + + return a.id === b.id && a.label === b.label && a.upstream?.id === b.upstream?.id && a.upstream?.label === b.upstream?.label; +} + export interface IValidateInput { (value: string, cursorPosition: number): vscode.ProviderResult; } @@ -507,6 +532,50 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel }); } + private _historyProvider: vscode.SourceControlHistoryProvider | undefined; + private _historyProviderDisposable = new MutableDisposable(); + private _historyProviderCurrentHistoryItemGroup: vscode.SourceControlHistoryItemGroup | undefined; + private _historyProviderActionButtonDisposable = new MutableDisposable(); + + get historyProvider(): vscode.SourceControlHistoryProvider | undefined { + checkProposedApiEnabled(this._extension, 'scmHistoryProvider'); + return this._historyProvider; + } + + set historyProvider(historyProvider: vscode.SourceControlHistoryProvider | undefined) { + checkProposedApiEnabled(this._extension, 'scmHistoryProvider'); + + this._historyProvider = historyProvider; + this._historyProviderDisposable.value = new DisposableStore(); + + this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider }); + + if (historyProvider) { + this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => { + if (historyItemGroupEquals(this._historyProviderCurrentHistoryItemGroup, historyProvider?.currentHistoryItemGroup)) { + return; + } + + this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); + })); + + this._historyProviderDisposable.value.add(historyProvider.onDidChangeActionButton(() => { + checkProposedApiEnabled(this._extension, 'scmActionButton'); + + this._historyProviderActionButtonDisposable.value = new DisposableStore(); + const internal = historyProvider.actionButton !== undefined ? + { + command: this._commands.converter.toInternal(historyProvider.actionButton.command, this._historyProviderActionButtonDisposable.value), + description: historyProvider.actionButton.description, + enabled: historyProvider.actionButton.enabled + } : undefined; + + this.#proxy.$onDidChangeHistoryProviderActionButton(this.handle, internal ?? null); + })); + } + } + private _commitTemplate: string | undefined = undefined; get commitTemplate(): string | undefined { @@ -882,4 +951,33 @@ export class ExtHostSCM implements ExtHostSCMShape { this._selectedSourceControlHandle = selectedSourceControlHandle; return Promise.resolve(undefined); } + + async $resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.resolveHistoryItemGroupBase(historyItemGroupId, token) ?? undefined; + } + + async $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined> { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined; + } + + async $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); + + return historyItems?.map(item => ({ + id: item.id, + parentIds: item.parentIds, + label: item.label, + description: item.description, + icon: getHistoryItemIconDto(item), + timestamp: item.timestamp, + })) ?? undefined; + } + + async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.provideHistoryItemChanges(historyItemId, token) ?? undefined; + } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 58fd62eb8fb..0fd8310eebd 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -25,6 +25,8 @@ import { Promises } from 'vs/base/common/async'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { TerminalQuickFix, ViewColumn } from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ISerializedTerminalInstanceContext } from 'vs/workbench/contrib/terminal/common/terminal'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -416,6 +418,31 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._proxy = extHostRpc.getProxy(MainContext.MainThreadTerminalService); this._bufferer = new TerminalDataBufferer(this._proxy.$sendProcessData); this._proxy.$registerProcessSupport(supportsProcesses); + this._extHostCommands.registerArgumentProcessor({ + processArgument: arg => { + const deserialize = (arg: any) => { + const cast = arg as ISerializedTerminalInstanceContext; + return this._getTerminalById(cast.instanceId)?.value; + }; + switch (arg?.$mid) { + case MarshalledId.TerminalContext: return deserialize(arg); + default: { + // Do array transformation in place as this is a hot path + if (Array.isArray(arg)) { + for (let i = 0; i < arg.length; i++) { + if (arg[i].$mid === MarshalledId.TerminalContext) { + arg[i] = deserialize(arg[i]); + } else { + // Probably something else, so exit early + break; + } + } + } + return arg; + } + } + } + }); this._register({ dispose: () => { for (const [_, terminalProcess] of this._terminalProcesses) { diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 8ab93b18b21..60d75433f9c 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -165,10 +165,10 @@ export class ExtHostTextEditorOptions { set tabSize(value: number | string) { that._setTabSize(value); }, - get indentSize(): number | 'tabSize' { + get indentSize(): number | string { return that._indentSize; }, - set indentSize(value: number | 'tabSize') { + set indentSize(value: number | string) { that._setIndentSize(value); }, get insertSpaces(): boolean | string { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0f4dcb21281..670dffdcbb4 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1397,16 +1397,6 @@ export class CodeActionKind { } } -export class NotebookCodeActionKind extends CodeActionKind { - public static override Notebook: CodeActionKind; - - constructor( - public override readonly value: string - ) { - super(value); - } -} - CodeActionKind.Empty = new CodeActionKind(''); CodeActionKind.QuickFix = CodeActionKind.Empty.append('quickfix'); CodeActionKind.Refactor = CodeActionKind.Empty.append('refactor'); @@ -1872,6 +1862,24 @@ export namespace TextEditorSelectionChangeKind { } } +export enum SyntaxTokenType { + Other = 0, + Comment = 1, + String = 2, + RegEx = 3 +} +export namespace SyntaxTokenType { + export function toString(v: SyntaxTokenType | unknown): 'other' | 'comment' | 'string' | 'regex' { + switch (v) { + case SyntaxTokenType.Other: return 'other'; + case SyntaxTokenType.Comment: return 'comment'; + case SyntaxTokenType.String: return 'string'; + case SyntaxTokenType.RegEx: return 'regex'; + } + return 'other'; + } +} + @es5ClassCompat export class DocumentLink { diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 40f1cdaf146..2bd275cbc21 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -194,7 +194,7 @@ export class ExtensionHostMain { } private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { - initData.allExtensions.forEach((ext) => { + initData.extensions.allExtensions.forEach((ext) => { (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); }); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index ef4f2b68e69..45cab9925c5 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -29,7 +29,7 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { private _registeredEHSearchProvider = false; - private _disposables = new DisposableStore(); + private readonly _disposables = new DisposableStore(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 6c0cd980ced..b8813437578 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -14,11 +14,11 @@ import { MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protoc import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, LogLevel as LogServiceLevel } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch } from '@vscode/proxy-agent'; -const systemCertificatesV2Default = false; +const systemCertificatesV2Default = true; export function connectProxyResolver( extHostWorkspace: IExtHostWorkspaceProvider, @@ -53,8 +53,22 @@ export function connectProxyResolver( extHostLogService.error(message, ...args); } }, - getLogLevel: () => extHostLogService.getLevel(), - // TODO @chrmarti Remove this from proxy agent + getLogLevel: () => { + const level = extHostLogService.getLevel(); + switch (level) { + case LogServiceLevel.Trace: return LogLevel.Trace; + case LogServiceLevel.Debug: return LogLevel.Debug; + case LogServiceLevel.Info: return LogLevel.Info; + case LogServiceLevel.Warning: return LogLevel.Warning; + case LogServiceLevel.Error: return LogLevel.Error; + case LogServiceLevel.Off: return LogLevel.Off; + default: return never(level); + } + function never(level: never) { + extHostLogService.error('Unknown log level', level); + return LogLevel.Debug; + } + }, proxyResolveTelemetry: () => { }, useHostProxy: doUseHostProxy, addCertificates: [], diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 52afb8083d8..03550d2d646 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -294,8 +294,8 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito editor.encoding = textFileModel.getEncoding(); } - // contents (only if dirty) - if (typeof editor.contents !== 'string' && textFileModel.isDirty()) { + // contents (only if dirty and not too large) + if (typeof editor.contents !== 'string' && textFileModel.isDirty() && !textFileModel.textEditorModel.isTooLargeForHeapOperation()) { editor.contents = textFileModel.textEditorModel.getValue(); } } diff --git a/src/vs/workbench/browser/iconSelectBox.ts b/src/vs/workbench/browser/iconSelectBox.ts new file mode 100644 index 00000000000..e74f52687cd --- /dev/null +++ b/src/vs/workbench/browser/iconSelectBox.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IIconSelectBoxOptions, IconSelectBox } from 'vs/base/browser/ui/icons/iconSelectBox'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import * as dom from 'vs/base/browser/dom'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; + +export const WorkbenchIconSelectBoxFocusContextKey = new RawContextKey('iconSelectBoxFocus', true); +export const WorkbenchIconSelectBoxInputFocusContextKey = new RawContextKey('iconSelectBoxInputFocus', true); +export const WorkbenchIconSelectBoxInputEmptyContextKey = new RawContextKey('iconSelectBoxInputEmpty', true); + +export class WorkbenchIconSelectBox extends IconSelectBox { + + private static focusedWidget: WorkbenchIconSelectBox | undefined; + static getFocusedWidget(): WorkbenchIconSelectBox | undefined { + return WorkbenchIconSelectBox.focusedWidget; + } + + private readonly contextKeyService: IContextKeyService; + private readonly inputFocusContextKey: IContextKey; + private readonly inputEmptyContextKey: IContextKey; + + constructor( + options: IIconSelectBoxOptions, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(options); + this.contextKeyService = this._register(contextKeyService.createScoped(this.domNode)); + WorkbenchIconSelectBoxFocusContextKey.bindTo(this.contextKeyService); + this.inputFocusContextKey = WorkbenchIconSelectBoxInputFocusContextKey.bindTo(this.contextKeyService); + this.inputEmptyContextKey = WorkbenchIconSelectBoxInputEmptyContextKey.bindTo(this.contextKeyService); + if (this.inputBox) { + const focusTracker = this._register(dom.trackFocus(this.inputBox.inputElement)); + this._register(focusTracker.onDidFocus(() => this.inputFocusContextKey.set(true))); + this._register(focusTracker.onDidBlur(() => this.inputFocusContextKey.set(false))); + this._register(this.inputBox.onDidChange(() => this.inputEmptyContextKey.set(this.inputBox?.value.length === 0))); + } + } + + override focus(): void { + super.focus(); + WorkbenchIconSelectBox.focusedWidget = this; + } + +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'iconSelectBox.focusUp', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchIconSelectBoxFocusContextKey, + primary: KeyCode.UpArrow, + handler: () => { + const selectBox = WorkbenchIconSelectBox.getFocusedWidget(); + if (selectBox) { + selectBox.focusPreviousRow(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'iconSelectBox.focusDown', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchIconSelectBoxFocusContextKey, + primary: KeyCode.DownArrow, + handler: () => { + const selectBox = WorkbenchIconSelectBox.getFocusedWidget(); + if (selectBox) { + selectBox.focusNextRow(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'iconSelectBox.focusNext', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchIconSelectBoxFocusContextKey, ContextKeyExpr.or(WorkbenchIconSelectBoxInputEmptyContextKey, WorkbenchIconSelectBoxInputFocusContextKey.toNegated())), + primary: KeyCode.RightArrow, + handler: () => { + const selectBox = WorkbenchIconSelectBox.getFocusedWidget(); + if (selectBox) { + selectBox.focusNext(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'iconSelectBox.focusPrevious', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchIconSelectBoxFocusContextKey, ContextKeyExpr.or(WorkbenchIconSelectBoxInputEmptyContextKey, WorkbenchIconSelectBoxInputFocusContextKey.toNegated())), + primary: KeyCode.LeftArrow, + handler: () => { + const selectBox = WorkbenchIconSelectBox.getFocusedWidget(); + if (selectBox) { + selectBox.focusPrevious(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'iconSelectBox.selectFocused', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchIconSelectBoxFocusContextKey, + primary: KeyCode.Enter, + handler: () => { + const selectBox = WorkbenchIconSelectBox.getFocusedWidget(); + if (selectBox) { + selectBox.setSelection(selectBox.getFocus()[0]); + } + } +}); diff --git a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css index ad0d6936909..d4db8eaff00 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css +++ b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css @@ -44,6 +44,8 @@ max-width: 450px; text-align: center; word-break: break-word; + user-select: text; + -webkit-user-select: text; } .monaco-editor-pane-placeholder .editor-placeholder-buttons-container { diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 1306b617009..30a4e589a06 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -7,7 +7,6 @@ import { reset } from 'vs/base/browser/dom'; import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; @@ -41,7 +40,7 @@ export class CommandCenterControl { const titleToolbar = instantiationService.createInstance(MenuWorkbenchToolBar, this.element, MenuId.CommandCenter, { contextMenu: MenuId.TitleBarContext, - hiddenItemStrategy: HiddenItemStrategy.Ignore, + hiddenItemStrategy: HiddenItemStrategy.NoHide, toolbarOptions: { primaryGroup: () => true, }, @@ -183,13 +182,6 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { }); toolbar.setActions(group); this._store.add(toolbar); - - // spacer - if (i < groups.length - 1) { - const icon = renderIcon(Codicon.circleSmallFilled); - icon.classList.add('spacer'); - container.appendChild(icon); - } } } diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index b0c29642ec2..44a1a6aa908 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -178,10 +178,9 @@ text-overflow: ellipsis; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple .spacer { - height: 100%; - padding: 0 6px; - opacity: 0.5; +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple { + justify-content: space-between; + padding: 0 12px; } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:only-child { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 62ca965951c..24231e4dd00 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -129,7 +129,7 @@ export class TreeViewPane extends ViewPane { } override shouldShowWelcome(): boolean { - return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && (this.treeView.message === undefined); + return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && ((this.treeView.message === undefined) || (this.treeView.message === '')); } protected override layoutBody(height: number, width: number): void { @@ -950,7 +950,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { if (item) { this.focus(true, item); this.tree.setFocus([item]); - } else { + } else if (this.tree.getFocus().length === 0) { this.tree.setFocus([]); } } diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 11f2a5447c0..4f5e7f34ed2 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -105,7 +105,7 @@ class ViewWelcomeController { return visibleItems.map(v => v.descriptor); } - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); constructor( private id: string, diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 1d703966a89..642bfeb1004 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -101,7 +101,7 @@ export interface IWorkbench { * @param options The definition of the terminal, this is similar to * `ExtensionTerminalOptions` in the extension API. */ - createTerminal(options: IEmbedderTerminalOptions): void; + createTerminal(options: IEmbedderTerminalOptions): Promise; }; workspace: { diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index a39b1f28c33..5fab020f5de 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -14,6 +14,7 @@ import { DeferredPromise } from 'vs/base/common/async'; import { asArray } from 'vs/base/common/arrays'; import { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; import { LogLevel } from 'vs/platform/log/common/log'; +import { IEmbedderTerminalOptions } from 'vs/workbench/services/terminal/common/embedderTerminalService'; let created = false; const workbenchPromise = new DeferredPromise(); @@ -144,6 +145,11 @@ export namespace window { return workbench.window.withProgress(options, task); } + + export async function createTerminal(options: IEmbedderTerminalOptions): Promise { + const workbench = await workbenchPromise.p; + workbench.window.createTerminal(options); + } } export namespace workspace { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 580e3ac1026..03a5a2f4d7e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -187,7 +187,7 @@ export class BrowserMain extends Disposable { }, window: { withProgress: (options, task) => progressService.withProgress(options, task), - createTerminal: (options) => embedderTerminalService.createTerminal(options), + createTerminal: async (options) => embedderTerminalService.createTerminal(options), }, workspace: { openTunnel: async tunnelOptions => { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts index 90544aabea6..11bf6795196 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts @@ -96,7 +96,7 @@ class AccessibilityHelpProvider implements IAccessibleContentProvider { } } - if (options.get(EditorOption.stickyScroll)) { + if (options.get(EditorOption.stickyScroll).enabled) { content.push(this._descriptionForCommand('editor.action.focusStickyScroll', AccessibilityHelpNLS.stickScrollKb, AccessibilityHelpNLS.stickScrollNoKb)); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index bbde0f86f2c..568d0719d21 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -101,6 +101,7 @@ export interface IAccessibleViewOptions { */ language?: string; type: AccessibleViewType; + positionBottom?: boolean; } export class AccessibleView extends Disposable { @@ -412,6 +413,14 @@ export class AccessibleView extends Disposable { } this._editorWidget.updateOptions({ ariaLabel }); this._editorWidget.focus(); + if (this._currentProvider?.options.positionBottom) { + const lastLine = this.editorWidget.getModel()?.getLineCount(); + const position = lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined; + if (position) { + this._editorWidget.setPosition(position); + this._editorWidget.revealLine(position.lineNumber); + } + } }); this._updateToolbar(provider.actions, provider.options.type); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index a2e7aa52b62..8899be6939b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -3,12 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_IN_CHAT_LIST } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; export function registerChatCopyActions() { @@ -57,14 +60,23 @@ export function registerChatCopyActions() { category: CHAT_CATEGORY, menu: { id: MenuId.ChatContext + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, + when: CONTEXT_IN_CHAT_LIST } }); } run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; + let item = args[0]; if (!isRequestVM(item) && !isResponseVM(item)) { - return; + const widgetService = accessor.get(IChatWidgetService); + item = widgetService.lastFocusedWidget?.getFocus(); + if (!isRequestVM(item) && !isResponseVM(item)) { + return; + } } const clipboardService = accessor.get(IClipboardService); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9e6ad4404f1..facf315c8c8 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -54,6 +54,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ChatVariablesService, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -253,4 +254,5 @@ registerSingleton(IChatAccessibilityService, ChatAccessibilityService, Instantia registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed); registerSingleton(IChatProviderService, ChatProviderService, InstantiationType.Delayed); registerSingleton(IChatSlashCommandService, ChatSlashCommandService, InstantiationType.Delayed); +registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index aa774249863..a16591501b5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -9,10 +9,10 @@ import * as resources from 'vs/base/common/resources'; import { localize } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views'; import { getHistoryAction, getOpenChatEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { getClearAction } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; @@ -21,6 +21,8 @@ import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'interactiveSession', @@ -59,23 +61,28 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi }, }); -export class ChatContributionService implements IChatContributionService { - declare _serviceBrand: undefined; +export class ChatExtensionPointHandler implements IWorkbenchContribution { + private _viewContainer: ViewContainer; private _registrationDisposables = new Map(); - private _registeredProviders = new Map(); constructor( + @IChatContributionService readonly _chatContributionService: IChatContributionService ) { + this._viewContainer = this.registerViewContainer(); + this.handleAndRegisterChatExtensions(); + } + + private handleAndRegisterChatExtensions(): void { chatExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { const extensionDisposable = new DisposableStore(); for (const providerDescriptor of extension.value) { - this.registerChatProvider(extension.description, providerDescriptor); + this.registerChatProvider(providerDescriptor); const extensionIcon = extension.description.icon ? resources.joinPath(extension.description.extensionLocation, extension.description.icon) : undefined; - this._registeredProviders.set(providerDescriptor.id, { + this._chatContributionService.registerChatProvider({ ...providerDescriptor, extensionIcon }); @@ -91,26 +98,17 @@ export class ChatContributionService implements IChatContributionService { } for (const providerDescriptor of extension.value) { - this._registeredProviders.delete(providerDescriptor.id); + this._chatContributionService.deregisterChatProvider(providerDescriptor.id); } } }); } - public get registeredProviders(): IChatProviderContribution[] { - return Array.from(this._registeredProviders.values()); - } - - public getViewIdForProvider(providerId: string): string { - return ChatViewPane.ID + '.' + providerId; - } - - private registerChatProvider(extension: Readonly, providerDescriptor: IRawChatProviderContribution): IDisposable { - const icon = providerDescriptor.icon ? resources.joinPath(extension.extensionLocation, providerDescriptor.icon) : Codicon.commentDiscussion; - const title = localize('chat.viewContainer.label', "Chat"); - + private registerViewContainer(): ViewContainer { // Register View Container - const viewContainerId = CHAT_SIDEBAR_PANEL_ID + '.' + providerDescriptor.id; + const title = localize('chat.viewContainer.label', "Chat"); + const icon = Codicon.commentDiscussion; + const viewContainerId = CHAT_SIDEBAR_PANEL_ID; const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: viewContainerId, title: { value: title, original: 'Chat' }, @@ -121,19 +119,23 @@ export class ChatContributionService implements IChatContributionService { order: 100, }, ViewContainerLocation.Sidebar); + return viewContainer; + } + + private registerChatProvider(providerDescriptor: IRawChatProviderContribution): IDisposable { // Register View - const viewId = this.getViewIdForProvider(providerDescriptor.id); + const viewId = this._chatContributionService.getViewIdForProvider(providerDescriptor.id); const viewDescriptor: IViewDescriptor[] = [{ id: viewId, - containerIcon: icon, - containerTitle: title, + containerIcon: this._viewContainer.icon, + containerTitle: this._viewContainer.title.value, name: providerDescriptor.label, canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: providerDescriptor.id }]), when: ContextKeyExpr.deserialize(providerDescriptor.when) }]; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); + Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); // Per-provider actions @@ -149,10 +151,39 @@ export class ChatContributionService implements IChatContributionService { return { dispose: () => { - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, viewContainer); - Registry.as(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(viewContainer); + Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); + Registry.as(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(this._viewContainer); disposables.dispose(); } }; } } + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(ChatExtensionPointHandler, LifecyclePhase.Starting); + + +export class ChatContributionService implements IChatContributionService { + declare _serviceBrand: undefined; + + private _registeredProviders = new Map(); + + constructor( + ) { } + + public getViewIdForProvider(providerId: string): string { + return ChatViewPane.ID + '.' + providerId; + } + + public registerChatProvider(provider: IChatProviderContribution): void { + this._registeredProviders.set(provider.id, provider); + } + + public deregisterChatProvider(providerId: string): void { + this._registeredProviders.delete(providerId); + } + + public get registeredProviders(): IChatProviderContribution[] { + return Array.from(this._registeredProviders.values()); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 97a2b1ee4ea..eacb6649ac4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -225,7 +225,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Only allow history navigation when the input is empty. // (If this model change happened as a result of a history navigation, this is canceled out by a call in this.navigateHistory) const model = this._inputEditor.getModel(); - const inputHasText = !!model && model.getValue() !== ''; + const inputHasText = !!model && model.getValueLength() > 0; this.setHistoryNavigationEnablement(!inputHasText); this.inputEditorHasText.set(inputHasText); })); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index ebb17cc541f..9a48e8ea78c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -14,6 +14,7 @@ import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTre import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; +import { distinct } from 'vs/base/common/arrays'; import { IntervalTimer } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; @@ -31,7 +32,7 @@ import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/ed import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ITextModel } from 'vs/editor/common/model'; +import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/browser/bracketMatching'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; @@ -52,6 +53,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; @@ -61,7 +63,9 @@ import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/a import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { fixVariableReferences, walkTreeAndAnnotateResourceLinks } from 'vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer'; import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IPlaceholderMarkdownString } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, 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'; @@ -70,9 +74,6 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { distinct } from 'vs/base/common/arrays'; -import { IPlaceholderMarkdownString } from 'vs/workbench/contrib/chat/common/chatModel'; const $ = dom.$; @@ -137,7 +138,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { @@ -642,6 +648,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.codeBlocksByResponseId.delete(element.id))); } + if (isRequestVM(element)) { + walkTreeAndAnnotateResourceLinks(result.element); + } + if (usedSlashCommand) { const slashCommandElement = $('span.interactive-slash-command', { title: usedSlashCommand.detail }, `/${usedSlashCommand.command} `); if (result.element.firstChild?.nodeName.toLowerCase() === 'p') { @@ -996,7 +1006,7 @@ class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart { } private setText(newText: string): void { - const currentText = this.textModel.getLinesContent().join('\n'); + const currentText = this.textModel.getValue(EndOfLinePreference.LF); if (newText === currentText) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts new file mode 100644 index 00000000000..1a7ad12b7b8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; + +const variableRefUrlPrefix = 'http://vscodeVar_'; + +export function fixVariableReferences(markdown: IMarkdownString): IMarkdownString { + const fixedMarkdownSource = markdown.value.replace(/\]\(values:(.*)/g, `](${variableRefUrlPrefix}_$1`); + return new MarkdownString(fixedMarkdownSource, { isTrusted: markdown.isTrusted, supportThemeIcons: markdown.supportThemeIcons, supportHtml: markdown.supportHtml }); +} + +export function walkTreeAndAnnotateResourceLinks(element: HTMLElement): void { + element.querySelectorAll('a').forEach(a => { + const href = a.getAttribute('data-href'); + if (href) { + if (href.startsWith(variableRefUrlPrefix)) { + a.parentElement!.replaceChild( + renderResourceWidget(a.textContent!), + a); + } + } + + walkTreeAndAnnotateResourceLinks(a as HTMLElement); + }); +} + +function renderResourceWidget(name: string): HTMLElement { + const container = dom.$('span.chat-resource-widget'); + const alias = dom.$('span', undefined, name); + container.appendChild(alias); + return container; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 91c15635f2e..7948c9dd8fc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -25,7 +25,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_LIST, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatReplyFollowup, IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; @@ -119,6 +119,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private lastSlashCommands: ISlashCommand[] | undefined; private slashCommandsPromise: Promise | undefined; + private readonly chatListFocused: IContextKey; + constructor( readonly viewContext: IChatWidgetViewContext, private readonly styles: IChatWidgetStyles, @@ -132,6 +134,7 @@ export class ChatWidget extends Disposable implements IChatWidget { ) { super(); CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); + this.chatListFocused = CONTEXT_IN_CHAT_LIST.bindTo(contextKeyService); this.requestInProgress = CONTEXT_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); this._register((chatWidgetService as ChatWidgetService).register(this)); @@ -351,7 +354,9 @@ export class ChatWidget extends Disposable implements IChatWidget { })); this._register(this.tree.onDidFocus(() => { this._onDidFocus.fire(); + this.chatListFocused.set(this.tree.isDOMFocused()); })); + this._register(this.tree.onDidBlur(() => this.chatListFocused.set(false))); } private onContextMenu(e: ITreeContextMenuEvent): void { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index e110d6ad3cc..f9c312fa4ce 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -23,8 +23,8 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -32,13 +32,12 @@ import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const decorationDescription = 'chat'; -const slashCommandPlaceholderDecorationType = 'chat-session-detail'; +const placeholderDecorationType = 'chat-session-detail'; const slashCommandTextDecorationType = 'chat-session-text'; const variableTextDecorationType = 'chat-variable-text'; class InputEditorDecorations extends Disposable { - private _slashCommandContentWidget: SlashCommandContentWidget | undefined; private _previouslyUsedSlashCommands = new Set(); constructor( @@ -47,10 +46,11 @@ class InputEditorDecorations extends Disposable { @IThemeService private readonly themeService: IThemeService, @IChatService private readonly chatService: IChatService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); - this.codeEditorService.registerDecorationType(decorationDescription, slashCommandPlaceholderDecorationType, {}); + this.codeEditorService.registerDecorationType(decorationDescription, placeholderDecorationType, {}); this._register(this.themeService.onDidColorThemeChange(() => this.updateRegisteredDecorationTypes())); this.updateRegisteredDecorationTypes(); @@ -71,14 +71,12 @@ class InputEditorDecorations extends Disposable { private updateRegisteredDecorationTypes() { this.codeEditorService.removeDecorationType(variableTextDecorationType); this.codeEditorService.removeDecorationType(slashCommandTextDecorationType); - this._slashCommandContentWidget?.hide(); - this.codeEditorService.registerDecorationType(decorationDescription, slashCommandTextDecorationType, { - opacity: '0', - after: { - contentText: ' ', - } - }); + const theme = this.themeService.getColorTheme(); + this.codeEditorService.registerDecorationType(decorationDescription, slashCommandTextDecorationType, { + color: theme.getColor(chatSlashCommandForeground)?.toString(), + backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString() + }); this.codeEditorService.registerDecorationType(decorationDescription, variableTextDecorationType, { color: theme.getColor(chatSlashCommandForeground)?.toString(), backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), @@ -96,11 +94,12 @@ class InputEditorDecorations extends Disposable { private async updateInputEditorDecorations() { const inputValue = this.widget.inputEditor.getValue(); const slashCommands = await this.widget.getSlashCommands(); // TODO this async call can lead to a flicker of the placeholder text when switching editor tabs + const agents = this.chatAgentService.getAgents(); if (!inputValue) { const extensionPlaceholder = this.widget.viewModel?.inputPlaceholder; const defaultPlaceholder = slashCommands?.length ? - localize('interactive.input.placeholderWithCommands', "Ask a question or type '/' for topics") : + localize('interactive.input.placeholderWithCommands', "Ask a question or type '@' or '/'") : localize('interactive.input.placeholderNoCommands', "Ask a question"); const placeholder = extensionPlaceholder ?? defaultPlaceholder; const decoration: IDecorationOptions[] = [ @@ -119,18 +118,57 @@ class InputEditorDecorations extends Disposable { } } ]; - this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandPlaceholderDecorationType, decoration); - this._slashCommandContentWidget?.hide(); + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, decoration); return; } - let slashCommandPlaceholderDecoration: IDecorationOptions[] | undefined; - const command = inputValue && slashCommands?.find(c => inputValue.startsWith(`/${c.command} `)); + // TODO@roblourens need some kind of parser for queries + + let placeholderDecoration: IDecorationOptions[] | undefined; + const usedAgent = inputValue && agents.find(a => inputValue.startsWith(`@${a.id} `)); + + let usedSubcommand: string | undefined; + let subCommandPosition: number | undefined; + if (usedAgent) { + const subCommandReg = /\/(\w+)(\s|$)/g; + let subCommandMatch: RegExpExecArray | null; + while (subCommandMatch = subCommandReg.exec(inputValue)) { + const maybeCommand = subCommandMatch[1]; + usedSubcommand = usedAgent.metadata.subCommands.find(agentCommand => maybeCommand === agentCommand.name)?.name; + if (usedSubcommand) { + subCommandPosition = subCommandMatch.index; + break; + } + } + } + + if (usedAgent && inputValue === `@${usedAgent.id} `) { + // Agent reference with no other text - show the placeholder + if (usedAgent.metadata.description) { + placeholderDecoration = [{ + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: usedAgent.id.length, + endColumn: 1000 + }, + renderOptions: { + after: { + contentText: usedAgent.metadata.description, + color: this.getPlaceholderColor(), + } + } + }]; + } + } + + const command = !usedAgent && inputValue && slashCommands?.find(c => inputValue.startsWith(`/${c.command} `)); if (command && inputValue === `/${command.command} `) { + // Command reference with no other text - show the placeholder const isFollowupSlashCommand = this._previouslyUsedSlashCommands.has(command.command); const shouldRenderFollowupPlaceholder = command.followupPlaceholder && isFollowupSlashCommand; if (shouldRenderFollowupPlaceholder || command.detail) { - slashCommandPlaceholderDecoration = [{ + placeholderDecoration = [{ range: { startLineNumber: 1, endLineNumber: 1, @@ -144,26 +182,40 @@ class InputEditorDecorations extends Disposable { } } }]; - this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandPlaceholderDecorationType, slashCommandPlaceholderDecoration); } } - if (!slashCommandPlaceholderDecoration) { - this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandPlaceholderDecorationType, []); - } - if (command && inputValue.startsWith(`/${command.command} `)) { - if (!this._slashCommandContentWidget) { - this._slashCommandContentWidget = new SlashCommandContentWidget(this.widget.inputEditor); - this._store.add(this._slashCommandContentWidget); + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, placeholderDecoration ?? []); + + // TODO@roblourens The way these numbers are computed aren't totally correct... + const textDecorations: IDecorationOptions[] | undefined = []; + if (usedAgent) { + textDecorations.push( + { + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: usedAgent.id.length + 2 + } + } + ); + if (usedSubcommand) { + textDecorations.push( + { + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: subCommandPosition! + 1, + endColumn: subCommandPosition! + usedSubcommand.length + 2 + } + } + ); } - this._slashCommandContentWidget.setCommandText(command.command); - this._slashCommandContentWidget.show(); - } else { - this._slashCommandContentWidget?.hide(); } - if (command && command.detail) { - const textDecoration: IDecorationOptions[] = [ + if (command) { + textDecorations.push( { range: { startLineNumber: 1, @@ -172,12 +224,11 @@ class InputEditorDecorations extends Disposable { endColumn: command.command.length + 2 } } - ]; - this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecoration); - } else { - this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, []); + ); } + this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations); + const variables = this.chatVariablesService.getVariables(); const variableReg = /(^|\s)@(\w+)(:\d+)?(?=(\s|$))/ig; let match: RegExpMatchArray | null; @@ -235,6 +286,7 @@ class SlashCommandCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -247,6 +299,15 @@ class SlashCommandCompletions extends Disposable { return null; } + const firstLine = model.getLineContent(1).trim(); + + const agents = this.chatAgentService.getAgents(); + const usedAgent = firstLine.startsWith('@') && agents.find(a => firstLine.startsWith(`@${a.id}`)); + if (usedAgent) { + // No (classic) global slash commands when an agent is used + return; + } + if (model.getValueInRange(new Range(1, 1, 1, 2)) !== '/' && model.getValueLength() > 0) { return null; } @@ -275,6 +336,125 @@ class SlashCommandCompletions extends Disposable { } } +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); + +class AgentCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAgentService private readonly chatAgentService: IChatAgentService + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgent', + triggerCharacters: ['@'], + provideCompletionItems: async (model: ITextModel, _position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return null; + } + + if (model.getValueInRange(new Range(1, 1, 1, 2)) !== '@' && model.getValueLength() > 0) { + return null; + } + + const agents = this.chatAgentService.getAgents(); + return { + suggestions: agents.map((c, i) => { + const withAt = `@${c.id}`; + return { + label: withAt, + insertText: `${withAt} `, + detail: c.metadata.description, + range: new Range(1, 1, 1, 1), + // sortText: 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway + // command: c.executeImmediately ? { id: SubmitAction.ID, title: withAt, arguments: [{ widget, inputValue: `${withAt} ` }] } : undefined, + }; + }) + }; + } + })); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentSubcommand', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return; + } + + const firstLine = model.getLineContent(1).trim(); + + if (!firstLine.startsWith('@')) { + return; + } + + const agents = this.chatAgentService.getAgents(); + const usedAgent = agents.find(a => firstLine.startsWith(`@${a.id}`)); + if (!usedAgent) { + return; + } + + const maybeCommands = model.getValue().split(/\s+/).filter(w => w.startsWith('/')); + const usedSubcommand = usedAgent.metadata.subCommands.find(agentCommand => maybeCommands.some(c => c === `/${agentCommand.name}`)); + if (usedSubcommand) { + // Only one allowed + return; + } + + return { + suggestions: usedAgent.metadata.subCommands.map((c, i) => { + const withSlash = `/${c.name}`; + return { + label: withSlash, + insertText: `${withSlash} `, + detail: c.description, + range: new Range(1, position.column - 1, 1, position.column - 1), + // sortText: 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway + // command: c.executeImmediately ? { id: SubmitAction.ID, title: withAt, arguments: [{ widget, inputValue: `${withAt} ` }] } : undefined, + }; + }) + }; + } + })); + + // list subcommands when the query is empty, insert agent+subcommand + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentAndSubcommand', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return; + } + + if (model.getValue().trim() !== '/') { + // Only when the input only contains a slash + return; + } + + const agents = this.chatAgentService.getAgents(); + return { + suggestions: agents.flatMap(a => a.metadata.subCommands.map((c, i) => { + const withSlash = `/${c.name}`; + return { + label: withSlash, + insertText: `@${a.id} ${withSlash} `, + detail: `(@${a.id}) ${c.description}`, + range: new Range(1, 1, 1, 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway + }; + })) + }; + } + })); + } +} + interface SlashCommandYieldTo { command: string; } @@ -348,7 +528,7 @@ function sortSlashCommandsByYieldTo(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); class VariableCompletions extends Disposable { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index eb1c33b9fa6..4283d10304e 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -469,3 +469,11 @@ border-radius: 4px; width: auto; } + +.interactive-item-container .chat-resource-widget { + background-color: var(--vscode-chat-slashCommandBackground); + color: var(--vscode-chat-slashCommandForeground); + border-radius: 3px; + white-space: nowrap; + padding: 1px; +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts new file mode 100644 index 00000000000..e7e4c4cfa83 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; +import { IChatFollowup, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +//#region extension point + +const agentItem: IJSONSchema = { + type: 'object', + required: ['agent', 'detail'], + properties: { + agent: { + type: 'string', + markdownDescription: localize('agent', "The name of the agent which will be used as prefix.") + }, + detail: { + type: 'string', + markdownDescription: localize('details', "The details of the agent.") + }, + } +}; + +const agentItems: IJSONSchema = { + description: localize('vscode.extension.contributes.slashes', "Contributes agents to chat"), + oneOf: [ + agentItem, + { + type: 'array', + items: agentItem + } + ] +}; + +export const agentsExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'agents', + jsonSchema: agentItems +}); + +//#region agent service, commands etc + +export interface IChatAgentData { + id: string; + metadata: IChatAgentMetadata; +} + +function isAgentData(data: any): data is IChatAgentData { + return typeof data === 'object' && data && + typeof data.id === 'string' && + typeof data.detail === 'string'; + // (typeof data.sortText === 'undefined' || typeof data.sortText === 'string') && + // (typeof data.executeImmediately === 'undefined' || typeof data.executeImmediately === 'boolean'); +} + +export interface IChatAgentFragment { + content: string | { treeData: IChatResponseProgressFileTreeData }; +} + +export interface IChatAgentCommand { + name: string; + description: string; +} + +export interface IChatAgentMetadata { + description: string; + subCommands: IChatAgentCommand[]; + requireCommand?: boolean; // Do some agents not have a default action? + isImplicit?: boolean; // Only @workspace. slash commands get promoted to the top-level and this agent is invoked when those are used + fullName?: string; + icon?: URI; +} + +export type IChatAgentCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; + +export const IChatAgentService = createDecorator('chatAgentService'); + +export interface IChatAgentService { + _serviceBrand: undefined; + readonly onDidChangeAgents: Event; + registerAgentData(data: IChatAgentData): IDisposable; + registerAgentCallback(id: string, callback: IChatAgentCallback): IDisposable; + registerAgent(data: IChatAgentData, callback: IChatAgentCallback): IDisposable; + invokeAgent(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; + getAgents(): Array; + hasAgent(id: string): boolean; +} + +type Tuple = { data: IChatAgentData; callback?: IChatAgentCallback }; + +export class ChatAgentService extends Disposable implements IChatAgentService { + + public static readonly AGENT_LEADER = '@'; + + declare _serviceBrand: undefined; + + private readonly _agents = new Map(); + + private readonly _onDidChangeAgents = this._register(new Emitter()); + readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + + constructor(@IExtensionService private readonly _extensionService: IExtensionService) { + super(); + } + + override dispose(): void { + super.dispose(); + this._agents.clear(); + } + + registerAgentData(data: IChatAgentData): IDisposable { + if (this._agents.has(data.id)) { + throw new Error(`Already registered an agent with id ${data.id}}`); + } + this._agents.set(data.id, { data }); + this._onDidChangeAgents.fire(); + + return toDisposable(() => { + if (this._agents.delete(data.id)) { + this._onDidChangeAgents.fire(); + } + }); + } + + registerAgentCallback(id: string, agentCallback: IChatAgentCallback): IDisposable { + const data = this._agents.get(id); + if (!data) { + throw new Error(`No agent with id ${id} registered`); + } + data.callback = agentCallback; + return toDisposable(() => data.callback = undefined); + } + + registerAgent(data: IChatAgentData, callback: IChatAgentCallback): IDisposable { + return combinedDisposable( + this.registerAgentData(data), + this.registerAgentCallback(data.id, callback) + ); + } + + getAgents(): Array { + return Array.from(this._agents.values(), v => v.data); + } + + hasAgent(id: string): boolean { + return this._agents.has(id); + } + + async invokeAgent(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { + const data = this._agents.get(id); + if (!data) { + throw new Error('No agent with id ${id} NOT registered'); + } + if (!data.callback) { + await this._extensionService.activateByEvent(`onChatAgent:${id}`); + } + if (!data.callback) { + throw new Error(`No agent with id ${id} NOT resolved`); + } + + return await data.callback(prompt, progress, history, token); + } +} + +class ChatAgentContribution implements IWorkbenchContribution { + constructor(@IChatAgentService chatAgentService: IChatAgentService) { + const contributions = new DisposableStore(); + + agentsExtPoint.setHandler(extensions => { + contributions.clear(); + + for (const entry of extensions) { + if (!isProposedApiEnabled(entry.description, 'chatAgents')) { + entry.collector.error(`The ${agentsExtPoint.name} is proposed API`); + continue; + } + + const { value } = entry; + + for (const candidate of Iterable.wrap(value)) { + + if (!isAgentData(candidate)) { + entry.collector.error(localize('invalid', "Invalid {0}: {1}", agentsExtPoint.name, JSON.stringify(candidate))); + continue; + } + + contributions.add(chatAgentService.registerAgentData({ ...candidate })); + } + } + }); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ChatAgentContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 631f0af7d00..95dd8d6e825 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -17,5 +17,6 @@ export const CONTEXT_REQUEST = new RawContextKey('chatRequest', false, export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); +export const CONTEXT_IN_CHAT_LIST = new RawContextKey('chatListFocused', false, { type: 'boolean', description: localize('chatListFocused', "True when a row of the chat list is focused, but not when focus is on a different element inside the chat row.") }); export const CONTEXT_PROVIDER_EXISTS = new RawContextKey('hasChatProvider', false, { type: 'boolean', description: localize('hasChatProvider', "True when some chat provider has been registered.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index 722183a5998..24720409f21 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -18,6 +18,8 @@ export interface IChatContributionService { _serviceBrand: undefined; registeredProviders: IChatProviderContribution[]; + registerChatProvider(provider: IChatProviderContribution): void; + deregisterChatProvider(providerId: string): void; getViewIdForProvider(providerId: string): string; } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 5f51abedb9a..26c82c87dd2 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,6 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; +import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatRequestModel { @@ -232,16 +233,17 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } public get username(): string { - return this.session.responderUsername; + return this.agent?.metadata.fullName ?? this.session.responderUsername; } public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + return this.agent?.metadata.icon ?? this.session.responderAvatarIconUri; } constructor( _response: IMarkdownString | (IMarkdownString | IChatResponseProgressFileTreeData)[], public readonly session: ChatModel, + public readonly agent: IChatAgentData | undefined, private _isComplete: boolean = false, private _isCanceled = false, private _vote?: InteractiveSessionVoteDirection, @@ -309,10 +311,18 @@ export interface ISerializableChatsData { [sessionId: string]: ISerializableChatData; } +export interface ISerializableChatAgentData { + id: string; + description: string; + fullName?: string; + icon?: UriComponents; +} + export interface ISerializableChatRequestData { providerRequestId: string | undefined; message: string; response: (IMarkdownString | IChatResponseProgressFileTreeData)[] | undefined; + agent?: ISerializableChatAgentData; responseErrorDetails: IChatResponseErrorDetails | undefined; followups: IChatFollowup[] | undefined; isCanceled: boolean | undefined; @@ -455,7 +465,8 @@ export class ChatModel extends Disposable implements IChatModel { constructor( public readonly providerId: string, private readonly initialData: ISerializableChatData | IExportableChatData | undefined, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -484,7 +495,8 @@ export class ChatModel extends Disposable implements IChatModel { return requests.map((raw: ISerializableChatRequestData) => { const request = new ChatRequestModel(this, raw.message, raw.providerRequestId); if (raw.response || raw.responseErrorDetails) { - request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); + const agent = raw.agent && this.chatAgentService.getAgents().find(a => a.id === raw.agent!.id); // TODO do something reasonable if this agent has disappeared since the last session + request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); } return request; }); @@ -531,13 +543,13 @@ export class ChatModel extends Disposable implements IChatModel { return this._requests; } - addRequest(message: string | IChatReplyFollowup): ChatRequestModel { + addRequest(message: string | IChatReplyFollowup, chatAgent?: IChatAgentData): ChatRequestModel { if (!this._session) { throw new Error('addRequest: No session'); } const request = new ChatRequestModel(this, message); - request.response = new ChatResponseModel(new MarkdownString(''), this); + request.response = new ChatResponseModel(new MarkdownString(''), this, chatAgent); this._requests.push(request); this._onDidChange.fire({ kind: 'addRequest', request }); @@ -550,7 +562,7 @@ export class ChatModel extends Disposable implements IChatModel { } if (!request.response) { - request.response = new ChatResponseModel(new MarkdownString(''), this); + request.response = new ChatResponseModel(new MarkdownString(''), this, undefined); } if (request.response.isComplete) { @@ -593,7 +605,7 @@ export class ChatModel extends Disposable implements IChatModel { } if (!request.response) { - request.response = new ChatResponseModel(new MarkdownString(''), this); + request.response = new ChatResponseModel(new MarkdownString(''), this, undefined); } request.response.setErrorDetails(rawResponse.errorDetails); @@ -642,7 +654,13 @@ export class ChatModel extends Disposable implements IChatModel { responseErrorDetails: r.response?.errorDetails, followups: r.response?.followups, isCanceled: r.response?.isCanceled, - vote: r.response?.vote + vote: r.response?.vote, + agent: r.response?.agent ? { + id: r.response.agent.id, + description: r.response.agent.metadata.description, + fullName: r.response.agent.metadata.fullName, + icon: r.response.agent.metadata.icon + } : undefined, }; }), providerId: this.providerId, diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 361d4d08407..8bdc8b21459 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -21,8 +21,9 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData, isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData, isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatRequest, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -152,7 +153,8 @@ export class ChatService extends Disposable implements IChatService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -433,10 +435,12 @@ export class ChatService extends Disposable implements IChatService { } private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { - const request = model.addRequest(message); + const resolvedAgent = typeof message === 'string' ? this.resolveAgent(message) : undefined; + let request: ChatRequestModel; const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + let gotProgress = false; const requestType = typeof message === 'string' ? (message.startsWith('/') ? 'slashCommand' : 'string') : @@ -487,7 +491,27 @@ export class ChatService extends Disposable implements IChatService { let rawResponse: IChatResponse | null | undefined; let slashCommandFollowups: IChatFollowup[] | void = []; - if ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) { + if (typeof message === 'string' && resolvedAgent) { + request = model.addRequest(message); + const history: IChatMessage[] = []; + for (const request of model.getRequests()) { + if (typeof request.message !== 'string' || !request.response) { + continue; + } + if (isMarkdownString(request.response.response.value)) { + history.push({ role: ChatMessageRole.User, content: request.message }); + history.push({ role: ChatMessageRole.Assistant, content: request.response.response.value.value }); + } + } + const agentResult = await this.chatAgentService.invokeAgent(resolvedAgent.id, message.substring(resolvedAgent.id.length + 1).trimStart(), new Progress(p => { + const { content } = p; + const data = isCompleteInteractiveProgressTreeData(content) ? content : { content }; + progressCallback(data); + }), history, token); + slashCommandFollowups = agentResult?.followUp; + rawResponse = { session: model.session! }; + } else if ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) { + request = model.addRequest(message); // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; @@ -509,19 +533,20 @@ export class ChatService extends Disposable implements IChatService { rawResponse = { session: model.session! }; } else { - const request: IChatRequest = { + const requestProps: IChatRequest = { session: model.session!, message: resolvedCommand, variables: {} }; - if (typeof request.message === 'string') { - const varResult = await this.chatVariablesService.resolveVariables(request.message, model, token); - request.variables = varResult.variables; - request.message = varResult.prompt; + if (typeof requestProps.message === 'string') { + const varResult = await this.chatVariablesService.resolveVariables(requestProps.message, model, token); + requestProps.variables = varResult.variables; + requestProps.message = varResult.prompt; } + request = model.addRequest(requestProps.message); - rawResponse = await provider.provideReply(request, progressCallback, token); + rawResponse = await provider.provideReply(requestProps, progressCallback, token); } if (token.isCancellationRequested) { @@ -598,6 +623,16 @@ export class ChatService extends Disposable implements IChatService { return command; } + private resolveAgent(prompt: string): IChatAgentData | undefined { + prompt = prompt.trim(); + const agents = this.chatAgentService.getAgents(); + if (!prompt.startsWith('@')) { + return; + } + + return agents.find(a => prompt.match(new RegExp(`@${a.id}($|\\s)`))); + } + async getSlashCommands(sessionId: string, token: CancellationToken): Promise { const model = this._sessionModels.get(sessionId); if (!model) { @@ -681,7 +716,7 @@ export class ChatService extends Disposable implements IChatService { } await model.waitForInitialization(); - const request = model.addRequest(message); + const request = model.addRequest(message, undefined); if (typeof response.message === 'string') { model.acceptResponseProgress(request, { content: response.message }); } else { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index fadb4edc028..7e891ea7be6 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -10,8 +10,8 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IChatRequestModel, IChatResponseModel, IChatModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatResponseErrorDetails, IChatReplyFollowup, IChatResponseCommandFollowup, InteractiveSessionVoteDirection, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatReplyFollowup, IChatResponseCommandFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 8cb6844d4f2..3a6bbd8843f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -9,6 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -23,10 +24,11 @@ suite('ChatModel', () => { instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); }); test('Waits for initialization', async () => { - const model = testDisposables.add(new ChatModel('provider', undefined, new NullLogService())); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); let hasInitialized = false; model.waitForInitialization().then(() => { @@ -42,7 +44,7 @@ suite('ChatModel', () => { }); test('Initialization fails when model is disposed', async () => { - const model = testDisposables.add(new ChatModel('provider', undefined, new NullLogService())); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); model.dispose(); await assert.rejects(() => model.waitForInitialization()); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index de602fdfa98..b3ccd1a70a6 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -20,6 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewsService } from 'vs/workbench/common/views'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IChat, IChatProgress, IChatProvider, IChatRequest, IChatResponse, IPersistedChatState, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; @@ -81,6 +82,7 @@ suite('Chat', () => { instantiationService.stub(IChatContributionService, new TestExtensionService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); }); test('retrieveSession', async () => { diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index e8bfcc38559..bd0c4c4393c 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -17,11 +17,18 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { CodeActionsExtensionPoint, ContributedCodeAction } from 'vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +const createCodeActionsAutoSave = (description: string): IJSONSchema => { + return { + type: 'string', + enum: ['always', 'never', 'explicit'], + enumDescriptions: [nls.localize('alwaysSave', 'Always triggers Code Actions on save'), nls.localize('neverSave', 'Never triggers Code Actions on save'), nls.localize('explicitSave', 'Triggers Code Actions only when explicitly saved')], + default: 'explicit', + description: description + }; +}; + const codeActionsOnSaveDefaultProperties = Object.freeze({ - 'source.fixAll': { - type: 'boolean', - description: nls.localize('codeActionsOnSave.fixAll', "Controls whether auto fix action should be run on file save.") - } + 'source.fixAll': createCodeActionsAutoSave(nls.localize('codeActionsOnSave.fixAll', "Controls whether auto fix action should be run on file save.")), }); const codeActionsOnSaveSchema: IConfigurationPropertySchema = { @@ -30,7 +37,7 @@ const codeActionsOnSaveSchema: IConfigurationPropertySchema = { type: 'object', properties: codeActionsOnSaveDefaultProperties, additionalProperties: { - type: 'boolean' + type: 'string' }, }, { @@ -38,8 +45,12 @@ const codeActionsOnSaveSchema: IConfigurationPropertySchema = { items: { type: 'string' } } ], + markdownDescription: nls.localize('editor.codeActionsOnSave', 'Run CodeActions for the editor on save. CodeActions must be specified and the editor must not be shutting down. Example: `"source.organizeImports": "explicit" `'), + type: 'object', + additionalProperties: { + type: 'string' + }, default: {}, - description: nls.localize('codeActionsOnSave', "Code Action kinds to be run on save."), scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, }; @@ -77,10 +88,7 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon private updateConfigurationSchema(codeActionContributions: readonly CodeActionsExtensionPoint[]) { const newProperties: IJSONSchemaMap = { ...codeActionsOnSaveDefaultProperties }; for (const [sourceAction, props] of this.getSourceActions(codeActionContributions)) { - newProperties[sourceAction] = { - type: 'boolean', - description: nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title) - }; + newProperties[sourceAction] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); } codeActionsOnSaveSchema.properties = newProperties; Registry.as(Extensions.Configuration) diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index 987bcadb81f..13ae0ab4aed 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -34,7 +34,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common/output'; import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; -import { GHOST_TEXT_DESCRIPTION } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; const $ = dom.$; @@ -75,6 +74,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.toDispose = []; this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); + this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.update())); this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(emptyTextEditorHintSetting)) { this.update(); @@ -102,14 +102,19 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { return false; } - const hasGhostText = this.editor.getLineDecorations(0)?.find((d) => d.options.description === GHOST_TEXT_DESCRIPTION); - if (hasGhostText) { + const model = this.editor.getModel(); + const languageId = model?.getLanguageId(); + if (!model || languageId === OUTPUT_MODE_ID || languageId === LOG_MODE_ID || languageId === SEARCH_RESULT_LANGUAGE_ID) { return false; } - const model = this.editor.getModel(); - const languageId = model?.getLanguageId(); - if (languageId === OUTPUT_MODE_ID || languageId === LOG_MODE_ID || languageId === SEARCH_RESULT_LANGUAGE_ID) { + const conflictingDecoration = this.editor.getLineDecorations(1)?.find((d) => + d.options.beforeContentClassName + || d.options.afterContentClassName + || d.options.before?.content + || d.options.after?.content + ); + if (conflictingDecoration) { return false; } @@ -178,7 +183,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } private onDidChangeModelContent(): void { - if (this.editor.getValue() === '') { + if (!this.editor.getModel()?.getValueLength()) { this.editor.addContentWidget(this); this.isVisible = true; } else { diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 91aafac3650..b5d88f047c5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -112,7 +112,7 @@ class DocumentSymbolsOutline implements IOutline { readonly onDidChange: Event = this._onDidChange.event; private _outlineModel?: OutlineModel; - private _outlineDisposables = new DisposableStore(); + private readonly _outlineDisposables = new DisposableStore(); private readonly _breadcrumbsDataSource: DocumentSymbolBreadcrumbsSource; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 939c8c05661..d29cbedb139 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -12,25 +13,24 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { CodeActionProvider, CodeActionTriggerType } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeActionTriggerType, CodeActionProvider } from 'vs/editor/common/languages'; -import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; -import { formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/browser/format'; +import { FormattingMode, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider } from 'vs/editor/contrib/format/browser/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProgressStep, IProgress, Progress } from 'vs/platform/progress/common/progress'; -import { ITextFileService, ITextFileSaveParticipant, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { SaveReason } from 'vs/workbench/common/editor'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ITextFileEditorModel, ITextFileSaveParticipant, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class TrimWhitespaceParticipant implements ITextFileSaveParticipant { @@ -277,24 +277,29 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { return; } - // Do not run code actions on auto save - if (env.reason !== SaveReason.EXPLICIT) { - return undefined; - } - const textEditorModel = model.textEditorModel; + const settingsOverrides = { overrideIdentifier: textEditorModel.getLanguageId(), resource: textEditorModel.uri }; - const settingsOverrides = { overrideIdentifier: textEditorModel.getLanguageId(), resource: model.resource }; - const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>('editor.codeActionsOnSave', settingsOverrides); + // Convert boolean values to strings + const setting = this.configurationService.getValue<{ [kind: string]: string | boolean }>('editor.codeActionsOnSave', settingsOverrides); if (!setting) { return undefined; } - const settingItems: string[] = Array.isArray(setting) - ? setting - : Object.keys(setting).filter(x => setting[x]); + if (env.reason === SaveReason.AUTO) { + return undefined; + } - const codeActionsOnSave = this.createCodeActionsOnSave(settingItems); + const convertedSetting: { [kind: string]: string } = {}; + for (const key in setting) { + if (typeof setting[key] === 'boolean') { + convertedSetting[key] = setting[key] ? 'explicit' : 'never'; + } else if (typeof setting[key] === 'string') { + convertedSetting[key] = setting[key] as string; + } + } + + const codeActionsOnSave = this.createCodeActionsOnSave(Object.keys(convertedSetting)); if (!Array.isArray(setting)) { codeActionsOnSave.sort((a, b) => { @@ -315,14 +320,15 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { return undefined; } - const excludedActions = Array.isArray(setting) - ? [] - : Object.keys(setting) - .filter(x => setting[x] === false) - .map(x => new CodeActionKind(x)); + const excludedActions = Object.keys(setting) + .filter(x => convertedSetting[x] === 'never' || false) + .map(x => new CodeActionKind(x)); progress.report({ message: localize('codeaction', "Quick Fixes") }); - await this.applyOnSaveActions(textEditorModel, codeActionsOnSave, excludedActions, progress, token); + + const filteredSaveList = codeActionsOnSave.filter(x => convertedSetting[x.value] === 'always' || (convertedSetting[x.value] === 'explicit') && env.reason === SaveReason.EXPLICIT); + + await this.applyOnSaveActions(textEditorModel, filteredSaveList, excludedActions, progress, token); } private createCodeActionsOnSave(settingItems: readonly string[]): CodeActionKind[] { diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 85bb0e71779..32ac5cb4dec 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -444,7 +444,7 @@ export class ContextScopedSuggestEnabledInputWithHistory extends SuggestEnabledI this._register(this.inputWidget.onDidChangeCursorPosition(({ position }) => { const viewModel = this.inputWidget._getViewModel()!; const lastLineNumber = viewModel.getLineCount(); - const lastLineCol = viewModel.getLineContent(lastLineNumber).length + 1; + const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); historyNavigationBackwardsEnablement.set(viewPosition.lineNumber === 1 && viewPosition.column === 1); historyNavigationForwardsEnablement.set(viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 4a859f036a4..7c98a5622c1 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -488,7 +488,7 @@ export class CommentNode extends Disposable { }); const lastLine = this._commentEditorModel.getLineCount(); - const lastColumn = this._commentEditorModel.getLineContent(lastLine).length + 1; + const lastColumn = this._commentEditorModel.getLineLength(lastLine) + 1; this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn)); const commentThread = this.commentThread; diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 2dbc44ada62..8e5f5e4de7d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread, CommentOptions, PendingCommentThread } from 'vs/editor/common/languages'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; @@ -17,16 +16,12 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; export const ICommentService = createDecorator('commentService'); -export const WorkspaceHasCommenting = new RawContextKey('workspaceHasCommenting', false, { - description: nls.localize('hasCommentingProvider', "Whether the open workspace has either comments or commenting ranges."), - type: 'boolean' -}); - interface IResourceCommentThreadEvent { resource: URI; commentInfos: ICommentInfo[]; @@ -174,7 +169,7 @@ export class CommentService extends Disposable implements ICommentService { super(); this._handleConfiguration(); this._handleZenMode(); - this._workspaceHasCommenting = WorkspaceHasCommenting.bindTo(contextKeyService); + this._workspaceHasCommenting = CommentContextKeys.WorkspaceHasCommenting.bindTo(contextKeyService); const storageListener = this._register(new DisposableStore()); storageListener.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, CONTINUE_ON_COMMENTS, storageListener)((v) => { @@ -370,6 +365,13 @@ export class CommentService extends Disposable implements ICommentService { for (const control of this._commentControls.values()) { commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None) .then(documentComments => { + // Check that there aren't any continue on comments in the provided comments + // This can happen because continue on comments are stored separately from local un-submitted comments. + for (const documentCommentThread of documentComments.threads) { + if (documentCommentThread.comments?.length === 0 && documentCommentThread.range) { + this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, owner: documentComments.owner }); + } + } const pendingComments = this._continueOnComments.get(documentComments.owner); documentComments.pendingCommentThreads = pendingComments?.filter(pendingComment => pendingComment.uri.toString() === resource.toString()); return documentComments; @@ -428,7 +430,7 @@ export class CommentService extends Disposable implements ICommentService { changedOwners.add(pendingComment.owner); } else { const commentsForOwner = this._continueOnComments.get(pendingComment.owner)!; - if (commentsForOwner.every(comment => (comment.uri.toString() !== pendingComment.uri.toString()) || !Range.equalsRange(comment.range, pendingComment.range) || (comment.body !== pendingComment.body))) { + if (commentsForOwner.every(comment => (comment.uri.toString() !== pendingComment.uri.toString()) || !Range.equalsRange(comment.range, pendingComment.range))) { commentsForOwner.push(pendingComment); changedOwners.add(pendingComment.owner); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index aff152a1a9c..0b4723b0c50 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -200,7 +200,7 @@ export class CommentThreadWidget extends } display(lineHeight: number) { - const headHeight = Math.ceil(lineHeight * 1.2); + const headHeight = Math.max(23, Math.ceil(lineHeight * 1.2)); // 23 is the value of `Math.ceil(lineHeight * 1.2)` with the default editor font size this._header.updateHeight(headHeight); this._body.display(); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 74590df5c4c..b9da89e5985 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -195,6 +195,9 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; } this.editor.setScrollTop(scrollTop); + if (focus) { + this._commentThreadWidget.focus(); + } return; } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 636de0344f6..23eea520440 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -33,11 +33,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; import { COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/browser/commentReply'; import { Emitter } from 'vs/base/common/event'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Position } from 'vs/editor/common/core/position'; import { CommentThreadRangeDecorator } from 'vs/workbench/contrib/comments/browser/commentThreadRangeDecorator'; import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; +import { status } from 'vs/base/browser/ui/aria/aria'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; export const ID = 'editor.contrib.review'; @@ -240,6 +242,21 @@ class CommentingRangeDecorator { } } + private areRangesIntersectingOrTouchingByLine(a: Range, b: Range) { + // Check if `a` is before `b` + if (a.endLineNumber < (b.startLineNumber - 1)) { + return false; + } + + // Check if `b` is before `a` + if ((b.endLineNumber + 1) < a.startLineNumber) { + return false; + } + + // These ranges must intersect + return true; + } + public getMatchedCommentAction(commentRange: Range | undefined): CommentRangeAction[] { if (commentRange === undefined) { const foundInfos = this._infos?.filter(info => info.commentingRanges.fileComments); @@ -260,7 +277,7 @@ class CommentingRangeDecorator { const foundHoverActions = new Map(); for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); - if (range && ((range.startLineNumber <= commentRange.startLineNumber) || (commentRange.endLineNumber <= range.endLineNumber))) { + if (range && this.areRangesIntersectingOrTouchingByLine(range, commentRange)) { // We can have several commenting ranges that match from the same owner because of how // the line hover and selection decoration is done. // The ranges must be merged so that we can see if the new commentRange fits within them. @@ -286,16 +303,51 @@ class CommentingRangeDecorator { }).map(actions => actions.action); } + public getNearestCommentingRange(findPosition: Position, reverse?: boolean): Range | undefined { + let findPositionContainedWithin: Range | undefined; + let decorations: CommentingRangeDecoration[]; + if (reverse) { + decorations = []; + for (let i = this.commentingRangeDecorations.length - 1; i >= 0; i--) { + decorations.push(this.commentingRangeDecorations[i]); + } + } else { + decorations = this.commentingRangeDecorations; + } + for (const decoration of decorations) { + const range = decoration.getActiveRange(); + if (!range) { + continue; + } + + if (findPositionContainedWithin && this.areRangesIntersectingOrTouchingByLine(range, findPositionContainedWithin)) { + findPositionContainedWithin = Range.plusRange(findPositionContainedWithin, range); + continue; + } + + if (range.startLineNumber <= findPosition.lineNumber && findPosition.lineNumber <= range.endLineNumber) { + findPositionContainedWithin = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + continue; + } + + if (!reverse && range.endLineNumber < findPosition.lineNumber) { + continue; + } + + if (reverse && range.startLineNumber > findPosition.lineNumber) { + continue; + } + + return range; + } + return decorations[0].getActiveRange() ?? undefined; + } + public dispose(): void { this.commentingRangeDecorations = []; } } -export const ActiveCursorHasCommentingRange = new RawContextKey('activeCursorHasCommentingRange', false, { - description: nls.localize('hasCommentingRange', "Whether the position at the active cursor has a commenting range"), - type: 'boolean' -}); - export class CommentController implements IEditorContribution { private readonly globalToDispose = new DisposableStore(); private readonly localToDispose = new DisposableStore(); @@ -315,6 +367,8 @@ export class CommentController implements IEditorContribution { private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // owner -> threadId -> uniqueIdInThread -> pending comment private _editorDisposables: IDisposable[] = []; private _activeCursorHasCommentingRange: IContextKey; + private _activeEditorHasCommentingRange: IContextKey; + private _hasRespondedToEditorChange: boolean = false; constructor( editor: ICodeEditor, @@ -333,7 +387,8 @@ export class CommentController implements IEditorContribution { this._pendingNewCommentCache = {}; this._pendingEditsCache = {}; this._computePromise = null; - this._activeCursorHasCommentingRange = ActiveCursorHasCommentingRange.bindTo(contextKeyService); + this._activeCursorHasCommentingRange = CommentContextKeys.activeCursorHasCommentingRange.bindTo(contextKeyService); + this._activeEditorHasCommentingRange = CommentContextKeys.activeEditorHasCommentingRange.bindTo(contextKeyService); if (editor instanceof EmbeddedCodeEditorWidget) { return; @@ -362,8 +417,8 @@ export class CommentController implements IEditorContribution { } this.beginCompute(); })); - this.globalToDispose.add(this.commentService.onDidSetDataProvider(_ => this.beginCompute())); - this.globalToDispose.add(this.commentService.onDidUpdateCommentingRanges(_ => this.beginCompute())); + this.globalToDispose.add(this.commentService.onDidSetDataProvider(_ => this.beginComputeAndHandleEditorChange())); + this.globalToDispose.add(this.commentService.onDidUpdateCommentingRanges(_ => this.beginComputeAndHandleEditorChange())); this.globalToDispose.add(this.commentService.onDidSetResourceCommentInfos(e => { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; @@ -543,18 +598,18 @@ export class CommentController implements IEditorContribution { return editor.getContribution(ID); } - public revealCommentThread(threadId: string, commentUniqueId: number, fetchOnceIfNotExist: boolean): void { + public revealCommentThread(threadId: string, commentUniqueId: number, fetchOnceIfNotExist: boolean, focus: boolean): void { const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId); if (commentThreadWidget.length === 1) { - commentThreadWidget[0].reveal(commentUniqueId); + commentThreadWidget[0].reveal(commentUniqueId, focus); } else if (fetchOnceIfNotExist) { if (this._computePromise) { this._computePromise.then(_ => { - this.revealCommentThread(threadId, commentUniqueId, false); + this.revealCommentThread(threadId, commentUniqueId, false, focus); }); } else { this.beginCompute().then(_ => { - this.revealCommentThread(threadId, commentUniqueId, false); + this.revealCommentThread(threadId, commentUniqueId, false, focus); }); } } @@ -654,6 +709,28 @@ export class CommentController implements IEditorContribution { this._findNearestCommentThread(true); } + private _findNearestCommentingRange(reverse?: boolean): void { + if (!this.editor?.hasModel()) { + return; + } + + const after = this.editor.getSelection().getEndPosition(); + const range = this._commentingRangeDecorator.getNearestCommentingRange(after, reverse); + if (range) { + const position = reverse ? range.getEndPosition() : range.getStartPosition(); + this.editor.setPosition(position); + this.editor.revealLineInCenterIfOutsideViewport(position.lineNumber); + } + } + + public nextCommentingRange(): void { + this._findNearestCommentingRange(); + } + + public previousCommentingRange(): void { + this._findNearestCommentingRange(true); + } + public dispose(): void { this.globalToDispose.dispose(); this.localToDispose.dispose(); @@ -671,6 +748,8 @@ export class CommentController implements IEditorContribution { return; } + this._hasRespondedToEditorChange = false; + this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); if (this._editorDisposables.length) { @@ -760,7 +839,21 @@ export class CommentController implements IEditorContribution { this._commentThreadRangeDecorator.update(this.editor, commentInfo); })); - this.beginCompute(); + this.beginComputeAndHandleEditorChange(); + } + + private beginComputeAndHandleEditorChange(): void { + this.beginCompute().then(() => { + if (!this._hasRespondedToEditorChange) { + if (this._commentInfos.some(commentInfo => commentInfo.commentingRanges.ranges.length > 0 || commentInfo.commentingRanges.fileComments)) { + this._hasRespondedToEditorChange = true; + this._activeEditorHasCommentingRange.set(true); + status(nls.localize('hasCommentRanges', "Editor has commenting ranges.")); + } else { + this._activeEditorHasCommentingRange.set(false); + } + } + }); } private async openCommentsView(thread: languages.CommentThread) { @@ -863,6 +956,9 @@ export class CommentController implements IEditorContribution { const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(range); if (!newCommentInfos.length || !this.editor?.hasModel()) { this._addInProgress = false; + if (!newCommentInfos.length) { + throw new Error('There are no commenting ranges at the current position.'); + } return Promise.resolve(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 116782e4364..6384fe1e2cd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import 'vs/css!./media/review'; import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -12,13 +12,15 @@ import * as nls from 'vs/nls'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { ICommentService, WorkspaceHasCommenting } from 'vs/workbench/contrib/comments/browser/commentService'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ActiveCursorHasCommentingRange, CommentController, ID } from 'vs/workbench/contrib/comments/browser/commentsController'; +import { CommentController, ID } from 'vs/workbench/contrib/comments/browser/commentsController'; import { IRange, Range } from 'vs/editor/common/core/range'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; export class NextCommentThreadAction extends EditorAction { constructor() { @@ -62,11 +64,55 @@ export class PreviousCommentThreadAction extends EditorAction { } } - registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender); registerEditorAction(NextCommentThreadAction); registerEditorAction(PreviousCommentThreadAction); +export class NextCommentingRangeAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.goToNextCommentingRange', + label: nls.localize('goToNextCommentingRange', "Go to Next Commenting Range"), + alias: 'Go to Next Commenting Range', + precondition: CommentContextKeys.WorkspaceHasCommenting, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.DownArrow), + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = CommentController.get(editor); + controller?.nextCommentingRange(); + } +} + +export class PreviousCommentingRangeAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.goToPreviousCommentingRange', + label: nls.localize('goToPreviousCommentingRange', "Go to Previous Commenting Range"), + alias: 'Go to Next Commenting Range', + precondition: CommentContextKeys.WorkspaceHasCommenting, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.UpArrow), + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = CommentController.get(editor); + controller?.previousCommentingRange(); + } +} + +registerEditorAction(NextCommentingRangeAction); +registerEditorAction(PreviousCommentingRangeAction); + const TOGGLE_COMMENTING_COMMAND = 'workbench.action.toggleCommenting'; CommandsRegistry.registerCommand({ id: TOGGLE_COMMENTING_COMMAND, @@ -83,13 +129,13 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: nls.localize('comments.toggleCommenting', "Toggle Editor Commenting"), category: 'Comments', }, - when: WorkspaceHasCommenting + when: CommentContextKeys.WorkspaceHasCommenting }); const ADD_COMMENT_COMMAND = 'workbench.action.addComment'; -CommandsRegistry.registerCommand({ +KeybindingsRegistry.registerCommandAndKeybindingRule({ id: ADD_COMMENT_COMMAND, - handler: (accessor, args?: { range: IRange; fileComment: boolean }) => { + handler: async (accessor, args?: { range: IRange; fileComment: boolean }) => { const activeEditor = getActiveEditor(accessor); if (!activeEditor) { return Promise.resolve(); @@ -102,8 +148,15 @@ CommandsRegistry.registerCommand({ const position = args?.range ? new Range(args.range.startLineNumber, args.range.startLineNumber, args.range.endLineNumber, args.range.endColumn) : (args?.fileComment ? undefined : activeEditor.getSelection()); - return controller.addOrToggleCommentAtLine(position, undefined); - } + const notificationService = accessor.get(INotificationService); + try { + await controller.addOrToggleCommentAtLine(position, undefined); + } catch (e) { + notificationService.error(nls.localize('comments.addCommand.error', "The cursor must be within a commenting range to add a comment")); // TODO: Once we have commands to go to next commenting range they should be included as buttons in the error. + } + }, + weight: KeybindingWeight.EditorContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC), }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -112,7 +165,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: nls.localize('comments.addCommand', "Add Comment on Current Selection"), category: 'Comments' }, - when: ActiveCursorHasCommentingRange + when: CommentContextKeys.activeCursorHasCommentingRange }); const COLLAPSE_ALL_COMMENT_COMMAND = 'workbench.action.collapseAllComments'; @@ -129,7 +182,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: nls.localize('comments.collapseAll', "Collapse All Comments"), category: 'Comments' }, - when: WorkspaceHasCommenting + when: CommentContextKeys.WorkspaceHasCommenting }); const EXPAND_ALL_COMMENT_COMMAND = 'workbench.action.expandAllComments'; @@ -146,7 +199,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: nls.localize('comments.expandAll', "Expand All Comments"), category: 'Comments' }, - when: WorkspaceHasCommenting + when: CommentContextKeys.WorkspaceHasCommenting }); const EXPAND_UNRESOLVED_COMMENT_COMMAND = 'workbench.action.expandUnresolvedComments'; @@ -163,7 +216,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: nls.localize('comments.expandUnresolved', "Expand Unresolved Comments"), category: 'Comments' }, - when: WorkspaceHasCommenting + when: CommentContextKeys.WorkspaceHasCommenting }); KeybindingsRegistry.registerCommandAndKeybindingRule({ diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 4be02c8c76c..582b6877ec5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -399,7 +399,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; if (threadToReveal && isCodeEditor(editor)) { const controller = CommentController.get(editor); - controller?.revealCommentThread(threadToReveal, commentToReveal, true); + controller?.revealCommentThread(threadToReveal, commentToReveal, true, !preserveFocus); } return true; @@ -421,7 +421,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { const control = editor.getControl(); if (threadToReveal && isCodeEditor(control)) { const controller = CommentController.get(control); - controller?.revealCommentThread(threadToReveal, commentToReveal.uniqueIdInThread, true); + controller?.revealCommentThread(threadToReveal, commentToReveal.uniqueIdInThread, true, !preserveFocus); } } }); diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index f087642f26b..0ff28e47388 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -62,12 +62,12 @@ export class SimpleCommentEditor extends CodeEditorWidget { this._commentEditorFocused = ctxCommentEditorFocused.bindTo(scopedContextKeyService); this._commentEditorEmpty = CommentContextKeys.commentIsEmpty.bindTo(scopedContextKeyService); - this._commentEditorEmpty.set(!this.getValue()); + this._commentEditorEmpty.set(!this.getModel()?.getValueLength()); this._parentThread = parentThread; this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true))); - this._register(this.onDidChangeModelContent(e => this._commentEditorEmpty.set(!this.getValue()))); + this._register(this.onDidChangeModelContent(e => this._commentEditorEmpty.set(!this.getModel()?.getValueLength()))); this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset())); } diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts index c4b32556b31..f792163c118 100644 --- a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -8,6 +8,31 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export namespace CommentContextKeys { + + /** + * A context key that is set when the active cursor is in a commenting range. + */ + export const activeCursorHasCommentingRange = new RawContextKey('activeCursorHasCommentingRange', false, { + description: nls.localize('hasCommentingRange', "Whether the position at the active cursor has a commenting range"), + type: 'boolean' + }); + + /** + * A context key that is set when the active editor has commenting ranges. + */ + export const activeEditorHasCommentingRange = new RawContextKey('activeEditorHasCommentingRange', false, { + description: nls.localize('editorHasCommentingRange', "Whether the active editor has a commenting range"), + type: 'boolean' + }); + + /** + * A context key that is set when the workspace has either comments or commenting ranges. + */ + export const WorkspaceHasCommenting = new RawContextKey('workspaceHasCommenting', false, { + description: nls.localize('hasCommentingProvider', "Whether the open workspace has either comments or commenting ranges."), + type: 'boolean' + }); + /** * A context key that is set when the comment thread has no comments. */ diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 1084c3707d6..55314e58a47 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -364,6 +364,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { this.initExtensionActivationsIfNeeded(); candidates.sort((first, second) => first.label.localeCompare(second.label)); + candidates = candidates.filter(a => !a.isHiddenFromDropdown); const suggestedCandidates: Debugger[] = []; const otherCandidates: Debugger[] = []; diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 1fecc75d4f9..0a74560e1b4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -704,8 +704,26 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: SELECT_AND_START_ID, - handler: async (accessor: ServicesAccessor) => { + handler: async (accessor: ServicesAccessor, debugType: string | unknown) => { const quickInputService = accessor.get(IQuickInputService); + const debugService = accessor.get(IDebugService); + + if (debugType) { + const configManager = debugService.getConfigurationManager(); + const dynamicProviders = await configManager.getDynamicProviders(); + for (const provider of dynamicProviders) { + if (provider.type === debugType) { + const pick = await provider.pick(); + if (pick) { + await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: provider.type }); + debugService.startDebugging(pick.launch, pick.config, { startedByUser: true }); + + return; + } + } + } + } + quickInputService.quickAccess.show(DEBUG_QUICK_ACCESS_PREFIX); } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 1bb8acebf52..ad2783183b9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -70,7 +70,7 @@ export class DebugService implements IDebugService { private taskRunner: DebugTaskRunner; private configurationManager: ConfigurationManager; private adapterManager: AdapterManager; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private debugType!: IContextKey; private debugState!: IContextKey; private inDebugMode!: IContextKey; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 01c4ef4ce44..ef7b5d7b28c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -52,7 +52,7 @@ export class DebugSession implements IDebugSession, IDisposable { private threads = new Map(); private threadIds: number[] = []; private cancellationMap = new Map(); - private rawListeners = new DisposableStore(); + private readonly rawListeners = new DisposableStore(); private fetchThreadsScheduler: RunOnceScheduler | undefined; private passFocusScheduler: RunOnceScheduler; private lastContinuedThreadId: number | undefined; diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 7c720d874d3..1859e2dc31b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -354,7 +354,7 @@ registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, un registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 55, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, undefined, CONTEXT_MULTI_SESSION_DEBUG); +registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, Codicon.listTree, CONTEXT_MULTI_SESSION_DEBUG); MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { group: 'navigation', diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 3620976fcbc..d55bda40b26 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -839,6 +839,7 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut configurationSnippets?: IJSONSchemaSnippet[]; variables?: { [key: string]: string }; when?: string; + hiddenWhen?: string; deprecated?: string; strings?: { [key in DebuggerString]: string }; } diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index 7363af0723c..ac868c2e5ca 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -72,6 +72,11 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE type: 'string', default: '' }, + hiddenWhen: { + description: nls.localize('vscode.extension.contributes.debuggers.hiddenWhen', "When this condition is true, this debugger type is hidden from the debugger list, but is still enabled."), + type: 'string', + default: '' + }, deprecated: { description: nls.localize('vscode.extension.contributes.debuggers.deprecated', "Optional message to mark this debug type as being deprecated."), type: 'string', diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 1f8faf2fae0..e18a400fe91 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -29,6 +29,7 @@ export class Debugger implements IDebugger, IDebuggerMetadata { private mainExtensionDescription: IExtensionDescription | undefined; private debuggerWhen: ContextKeyExpression | undefined; + private debuggerHiddenWhen: ContextKeyExpression | undefined; constructor( private adapterManager: IAdapterManager, @@ -45,6 +46,7 @@ export class Debugger implements IDebugger, IDebuggerMetadata { this.merge(dbgContribution, extensionDescription); this.debuggerWhen = typeof this.debuggerContribution.when === 'string' ? ContextKeyExpr.deserialize(this.debuggerContribution.when) : undefined; + this.debuggerHiddenWhen = typeof this.debuggerContribution.hiddenWhen === 'string' ? ContextKeyExpr.deserialize(this.debuggerContribution.hiddenWhen) : undefined; } merge(otherDebuggerContribution: IDebuggerContribution, extensionDescription: IExtensionDescription): void { @@ -147,10 +149,21 @@ export class Debugger implements IDebugger, IDebuggerMetadata { return this.debuggerWhen; } + get hiddenWhen(): ContextKeyExpression | undefined { + return this.debuggerHiddenWhen; + } + get enabled() { return !this.debuggerWhen || this.contextKeyService.contextMatchesRules(this.debuggerWhen); } + get isHiddenFromDropdown() { + if (!this.debuggerHiddenWhen) { + return false; + } + return this.contextKeyService.contextMatchesRules(this.debuggerHiddenWhen); + } + get strings() { return this.debuggerContribution.strings ?? (this.debuggerContribution as any).uiMessages; } diff --git a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts index 92be8da593c..7b36ebba216 100644 --- a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/path'; -import { DEFAULT_TERMINAL_OSX, IExternalTerminalService, IExternalTerminalSettings } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { DEFAULT_TERMINAL_OSX, IExternalTerminalSettings } from 'vs/platform/externalTerminal/common/externalTerminal'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -14,7 +14,7 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/electron-sandbox/externalTerminalService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; @@ -84,7 +84,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { export class ExternalTerminalContribution implements IWorkbenchContribution { public _serviceBrand: undefined; - constructor(@IExternalTerminalMainService private readonly _externalTerminalService: IExternalTerminalMainService) { + constructor(@IExternalTerminalService private readonly _externalTerminalService: IExternalTerminalService) { this._updateConfiguration(); } diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index faf84eafbf9..d6c151043f0 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -433,8 +433,8 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements untypedInput.languageId = this.getLanguageId(); untypedInput.contents = (() => { const model = this.textFileService.files.get(this.resource); - if (model?.isDirty()) { - return model.textEditorModel.getValue(); // only if dirty + if (model?.isDirty() && !model.textEditorModel.isTooLargeForHeapOperation()) { + return model.textEditorModel.getValue(); // only if dirty and not too large } return undefined; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index f6de1a29bc4..0a6f9369ba4 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -130,7 +130,7 @@ const hotExitConfiguration: IConfigurationPropertySchema = isNative ? nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`'), nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`') ], - 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) + 'markdownDescription': nls.localize('hotExit', "[Hot Exit](https://aka.ms/vscode-hot-exit) controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) } : { 'type': 'string', 'scope': ConfigurationScope.APPLICATION, @@ -140,7 +140,7 @@ const hotExitConfiguration: IConfigurationPropertySchema = isNative ? nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with editors that have unsaved changes.'), nls.localize('hotExit.onExitAndWindowCloseBrowser', 'Hot exit will be triggered when the browser quits or the window or tab is closed.') ], - 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) + 'markdownDescription': nls.localize('hotExit', "[Hot Exit](https://aka.ms/vscode-hot-exit) controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) }; configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index 7bd383b7932..3f5e22cb5cc 100644 --- a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts +++ b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -35,7 +35,7 @@ export class InlayHintsAccessibility implements IEditorContribution { private readonly _ariaElement: HTMLSpanElement; private readonly _ctxIsReading: IContextKey; - private _sessionDispoosables = new DisposableStore(); + private readonly _sessionDispoosables = new DisposableStore(); constructor( private readonly _editor: ICodeEditor, diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts index 9211cf9643f..ea29b76b146 100644 --- a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -35,7 +35,7 @@ class LanguageDetectionStatusContribution implements IWorkbenchContribution { private readonly _disposables = new DisposableStore(); private _combinedEntry?: IStatusbarEntryAccessor; private _delayer = new ThrottledDelayer(1000); - private _renderDisposables = new DisposableStore(); + private readonly _renderDisposables = new DisposableStore(); constructor( @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 3dd004897d2..c5900bbf721 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -72,7 +72,7 @@ class EditorStatusContribution implements IWorkbenchContribution { private _model?: LanguageStatusViewModel; private _combinedEntry?: IStatusbarEntryAccessor; private _dedicatedEntries = new Map(); - private _renderDisposables = new DisposableStore(); + private readonly _renderDisposables = new DisposableStore(); constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 96903926179..4305c75e301 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -211,7 +211,7 @@ export class NotebookCellOutline implements IOutline { } private _outlineProvider: NotebookCellOutlineProvider | undefined; - private _localDisposables = new DisposableStore(); + private readonly _localDisposables = new DisposableStore(); constructor( private readonly _editor: INotebookEditorPane, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 89989e87bfa..dc42987bbce 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -6,32 +6,37 @@ import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { isEqual } from 'vs/base/common/resources'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CodeActionProvider, CodeActionTriggerType, IWorkspaceTextEdit } from 'vs/editor/common/languages'; +import { IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; -import { CodeActionTriggerType, CodeActionProvider, IWorkspaceTextEdit } from 'vs/editor/common/languages'; -import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { isEqual } from 'vs/base/common/resources'; - -const NotebookCodeAction = new CodeActionKind('notebook'); - class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { constructor( @@ -86,7 +91,199 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { return []; })); - await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); + await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('formatNotebook', "Format Notebook"), code: 'undoredo.formatNotebook', }); + + } finally { + progress.report({ increment: 100 }); + disposable.dispose(); + } + } +} + +class TrimWhitespaceParticipant implements IStoredFileWorkingCopySaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + @ITextModelService private readonly textModelService: ITextModelService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + ) { } + + async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, _token: CancellationToken): Promise { + if (this.configurationService.getValue('files.trimTrailingWhitespace')) { + await this.doTrimTrailingWhitespace(workingCopy, context.reason === SaveReason.AUTO, progress); + } + } + + private async doTrimTrailingWhitespace(workingCopy: IStoredFileWorkingCopy, isAutoSaved: boolean, progress: IProgress) { + if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) { + return; + } + + const disposable = new DisposableStore(); + const notebook = workingCopy.model.notebookModel; + const activeCellEditor = getActiveCellCodeEditor(this.editorService); + + let cursors: Position[] = []; + let prevSelection: Selection[] = []; + try { + const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => { + if (cell.cellKind !== CellKind.Code) { + return []; + } + + const ref = await this.textModelService.createModelReference(cell.uri); + disposable.add(ref); + const model = ref.object.textEditorModel; + + const isActiveCell = (activeCellEditor && cell.uri.toString() === activeCellEditor.getModel()?.uri.toString()); + if (isActiveCell) { + prevSelection = activeCellEditor.getSelections() ?? []; + if (isAutoSaved) { + cursors = prevSelection.map(s => s.getPosition()); // get initial cursor positions + const snippetsRange = SnippetController2.get(activeCellEditor)?.getSessionEnclosingRange(); + if (snippetsRange) { + for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) { + cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber))); + } + } + } + } + + const ops = trimTrailingWhitespace(model, cursors); + if (!ops.length) { + return []; // Nothing to do + } + + return ops.map(op => new ResourceTextEdit(model.uri, { ...op, text: op.text || '' }, model.getVersionId())); + })); + + const filteredEdits = allCellEdits.flat().filter(edit => edit !== undefined) as ResourceEdit[]; + await this.bulkEditService.apply(filteredEdits, { label: localize('trimNotebookWhitespace', "Notebook Trim Trailing Whitespace"), code: 'undoredo.notebookTrimTrailingWhitespace' }); + + } finally { + progress.report({ increment: 100 }); + disposable.dispose(); + } + } +} + +class TrimFinalNewLinesParticipant implements IStoredFileWorkingCopySaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + ) { } + + async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, _token: CancellationToken): Promise { + if (this.configurationService.getValue('files.trimTrailingWhitespace')) { + this.doTrimFinalNewLines(workingCopy, context.reason === SaveReason.AUTO, progress); + } + } + + /** + * returns 0 if the entire file is empty + */ + private findLastNonEmptyLine(textBuffer: IReadonlyTextBuffer): number { + for (let lineNumber = textBuffer.getLineCount(); lineNumber >= 1; lineNumber--) { + const lineLength = textBuffer.getLineLength(lineNumber); + if (lineLength) { + // this line has content + return lineNumber; + } + } + // no line has content + return 0; + } + + private async doTrimFinalNewLines(workingCopy: IStoredFileWorkingCopy, isAutoSaved: boolean, progress: IProgress): Promise { + if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) { + return; + } + + const disposable = new DisposableStore(); + const notebook = workingCopy.model.notebookModel; + const activeCellEditor = getActiveCellCodeEditor(this.editorService); + + try { + const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => { + if (cell.cellKind !== CellKind.Code) { + return; + } + + // autosave -- don't trim every trailing line, just up to the cursor line + let cannotTouchLineNumber = 0; + const isActiveCell = (activeCellEditor && cell.uri.toString() === activeCellEditor.getModel()?.uri.toString()); + if (isAutoSaved && isActiveCell) { + const selections = activeCellEditor.getSelections() ?? []; + for (const sel of selections) { + cannotTouchLineNumber = Math.max(cannotTouchLineNumber, sel.selectionStartLineNumber); + } + } + + const textBuffer = cell.textBuffer; + const lastNonEmptyLine = this.findLastNonEmptyLine(textBuffer); + const deleteFromLineNumber = Math.max(lastNonEmptyLine + 1, cannotTouchLineNumber + 1); + const deletionRange = new Range(deleteFromLineNumber, 1, textBuffer.getLineCount(), textBuffer.getLineLastNonWhitespaceColumn(textBuffer.getLineCount())); + + if (deletionRange.isEmpty()) { + return; + } + + // create the edit to delete all lines in deletionRange + return new ResourceTextEdit(cell.uri, { range: deletionRange, text: '' }, cell.textModel?.getVersionId()); + })); + + const filteredEdits = allCellEdits.flat().filter(edit => edit !== undefined) as ResourceEdit[]; + await this.bulkEditService.apply(filteredEdits, { label: localize('trimNotebookNewlines', "Trim Final New Lines"), code: 'undoredo.trimFinalNewLines' }); + + } finally { + progress.report({ increment: 100 }); + disposable.dispose(); + } + } +} + +class FinalNewLineParticipant implements IStoredFileWorkingCopySaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + ) { } + + async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, _token: CancellationToken): Promise { + if (this.configurationService.getValue('files.insertFinalNewline')) { + this.doInsertFinalNewLine(workingCopy, context, progress); + } + } + + private async doInsertFinalNewLine(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress): Promise { + if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) { + return; + } + + const disposable = new DisposableStore(); + const notebook = workingCopy.model.notebookModel; + + try { + const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => { + if (cell.cellKind !== CellKind.Code) { + return; + } + + const lineCount = cell.textBuffer.getLineCount(); + const lastLineIsEmptyOrWhitespace = cell.textBuffer.getLineFirstNonWhitespaceColumn(lineCount) === 0; + + if (!lineCount || lastLineIsEmptyOrWhitespace) { + return; + } + + return new ResourceTextEdit(cell.uri, { range: new Range(lineCount + 1, cell.textBuffer.getLineLength(lineCount), lineCount + 1, cell.textBuffer.getLineLength(lineCount)), text: cell.textBuffer.getEOL() }, cell.textModel?.getVersionId()); + })); + + const filteredEdits = allCellEdits.filter(edit => edit !== undefined) as ResourceEdit[]; + await this.bulkEditService.apply(filteredEdits, { label: localize('insertFinalNewLine', "Insert Final New Line"), code: 'undoredo.insertFinalNewLine' }); } finally { progress.report({ increment: 100 }); @@ -107,6 +304,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise { + const nbDisposable = new DisposableStore(); const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); if (!isTrusted) { return; @@ -116,31 +314,46 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa return; } + let saveTrigger = ''; if (context.reason === SaveReason.AUTO) { - return undefined; - } - - const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>(NotebookSetting.codeActionsOnSave); - if (!setting) { + // currently this won't happen, as vs/editor/contrib/codeAction/browser/codeAction.ts L#104 filters out codeactions on autosave. Just future-proofing + // ? notebook CodeActions on autosave seems dangerous (perf-wise) + saveTrigger = 'always'; + } else if (context.reason === SaveReason.EXPLICIT) { + saveTrigger = 'explicit'; + } else { + // SaveReason.FOCUS_CHANGE, WINDOW_CHANGE need to be addressed when autosaves are enabled return undefined; } const notebookModel = workingCopy.model.notebookModel; + const setting = this.configurationService.getValue<{ [kind: string]: string }>(NotebookSetting.codeActionsOnSave); + if (!setting) { + return undefined; + } const settingItems: string[] = Array.isArray(setting) ? setting : Object.keys(setting).filter(x => setting[x]); - if (!settingItems.length) { return undefined; } - const codeActionsOnSave = this.createCodeActionsOnSave(settingItems).filter(x => !NotebookCodeAction.contains(x)); - const notebookCodeActionsOnSave = this.createCodeActionsOnSave(settingItems).filter(x => NotebookCodeAction.contains(x)); + const allCodeActions = this.createCodeActionsOnSave(settingItems); + const excludedActions = allCodeActions + .filter(x => setting[x.value] === 'never'); + const includedActions = allCodeActions + .filter(x => setting[x.value] === saveTrigger); + + const editorCodeActionsOnSave = includedActions.filter(x => !CodeActionKind.Notebook.contains(x)); + const notebookCodeActionsOnSave = includedActions.filter(x => CodeActionKind.Notebook.contains(x)); + if (!editorCodeActionsOnSave.length && !notebookCodeActionsOnSave.length) { + return undefined; + } // prioritize `source.fixAll` code actions if (!Array.isArray(setting)) { - codeActionsOnSave.sort((a, b) => { + editorCodeActionsOnSave.sort((a, b) => { if (CodeActionKind.SourceFixAll.contains(a)) { if (CodeActionKind.SourceFixAll.contains(b)) { return 0; @@ -154,21 +367,6 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa }); } - - - - if (!codeActionsOnSave.length && !notebookCodeActionsOnSave.length) { - return undefined; - } - - const excludedActions = Array.isArray(setting) - ? [] - : Object.keys(setting) - .filter(x => setting[x] === false) - .map(x => new CodeActionKind(x)); - - const nbDisposable = new DisposableStore(); - // run notebook code actions progress.report({ message: localize('notebookSaveParticipants.notebookCodeActions', "Running 'Notebook' code actions") }); try { @@ -188,7 +386,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa // run cell level code actions const disposable = new DisposableStore(); - progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running code actions") }); + progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running 'Cell' code actions") }); try { await Promise.all(notebookModel.cells.map(async cell => { const ref = await this.textModelService.createModelReference(cell.uri); @@ -196,7 +394,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa const textEditorModel = ref.object.textEditorModel; - await this.applyOnSaveActions(textEditorModel, codeActionsOnSave, excludedActions, progress, token); + await this.applyOnSaveActions(textEditorModel, editorCodeActionsOnSave, excludedActions, progress, token); })); } catch { this.logService.error('Failed to apply code action on save'); @@ -248,7 +446,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa for (const action of actionsToRun.validActions) { const codeActionEdits = action.action.edit?.edits; let breakFlag = false; - if (!action.action.kind?.includes('notebook')) { + if (!action.action.kind?.startsWith('notebook')) { for (const edit of codeActionEdits ?? []) { const workspaceTextEdit = edit as IWorkspaceTextEdit; if (workspaceTextEdit.resource && isEqual(workspaceTextEdit.resource, model.uri)) { @@ -287,7 +485,12 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } - +function getActiveCellCodeEditor(editorService: IEditorService): ICodeEditor | undefined { + const activePane = editorService.activeEditorPane; + const notebookEditor = getNotebookEditorFromEditorPane(activePane); + const activeCodeEditor = notebookEditor?.activeCodeEditor; + return activeCodeEditor; +} export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution { constructor( @@ -299,8 +502,11 @@ export class SaveParticipantsContribution extends Disposable implements IWorkben } private registerSaveParticipants(): void { + this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(TrimWhitespaceParticipant))); this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(CodeActionOnSaveParticipant))); this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FormatOnSaveParticipant))); + this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FinalNewLineParticipant))); + this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(TrimFinalNewLinesParticipant))); } } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 093970cad14..73480164384 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -12,6 +12,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { EditorsOrder } from 'vs/workbench/common/editor'; @@ -20,7 +21,7 @@ import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/in import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CELL_TITLE_CELL_GROUP_ID, CellToolbarOrder, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NotebookAction, NotebookCellAction, NotebookMultiCellAction, cellExecutionArgs, executeNotebookCondition, getContextFromActiveEditor, getContextFromUri, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, IFocusNotebookCellOptions, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellKind, CellUri, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_HAS_SOMETHING_RUNNING, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT, NOTEBOOK_LAST_CELL_FAILED, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; @@ -455,29 +456,39 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { } const languageService = accessor.get(ILanguageService); + const config = accessor.get(IConfigurationService); + const scrollBehavior = config.getValue(NotebookSetting.cellExecutionScroll); + let focusOptions: IFocusNotebookCellOptions; + if (scrollBehavior === 'none') { + focusOptions = { skipReveal: true }; + } else { + focusOptions = { + revealBehavior: scrollBehavior === 'fullCell' ? ScrollToRevealBehavior.fullCell : ScrollToRevealBehavior.firstLine + }; + } + if (context.cell.cellKind === CellKind.Markup) { const nextCell = context.notebookEditor.cellAt(idx + 1); context.cell.updateEditState(CellEditState.Preview, EXECUTE_CELL_SELECT_BELOW); if (nextCell) { - await context.notebookEditor.focusNotebookCell(nextCell, 'container', { minimalScrolling: true }); + await context.notebookEditor.focusNotebookCell(nextCell, 'container', focusOptions); } else { const newCell = insertCell(languageService, context.notebookEditor, idx, CellKind.Markup, 'below'); if (newCell) { - await context.notebookEditor.focusNotebookCell(newCell, 'editor', { minimalScrolling: true }); + await context.notebookEditor.focusNotebookCell(newCell, 'editor', focusOptions); } } return; } else { - // Try to select below, fall back on inserting const nextCell = context.notebookEditor.cellAt(idx + 1); if (nextCell) { - await context.notebookEditor.focusNotebookCell(nextCell, 'container', { minimalScrolling: true }); + await context.notebookEditor.focusNotebookCell(nextCell, 'container', focusOptions); } else { const newCell = insertCell(languageService, context.notebookEditor, idx, CellKind.Code, 'below'); if (newCell) { - await context.notebookEditor.focusNotebookCell(newCell, 'editor', { minimalScrolling: true }); + await context.notebookEditor.focusNotebookCell(newCell, 'editor', focusOptions); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 6372ee6dada..c4628df2fd4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -964,10 +964,14 @@ configurationRegistry.registerConfiguration({ default: false }, [NotebookSetting.codeActionsOnSave]: { - markdownDescription: nls.localize('notebook.codeActionsOnSave', "Experimental. Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `source.fixAll: true`"), + markdownDescription: nls.localize('notebook.codeActionsOnSave', "Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `source.fixAll: true`"), type: 'object', additionalProperties: { - type: 'boolean' + type: 'string', + enum: ['explicit', 'never'], + // enum: ['explicit', 'always', 'never'], -- autosave support needs to be built first + // nls.localize('always', 'Always triggers Code Actions on save, including autosave, focus, and window change events.'), + enumDescriptions: [nls.localize('never', 'Never triggers Code Actions on save.'), nls.localize('explicit', 'Triggers Code Actions only when explicitly saved.')], }, default: {} }, @@ -1014,6 +1018,17 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks in Remote environment. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), type: 'boolean', default: typeof product.quality === 'string' && product.quality !== 'stable' // only enable as default in insiders + }, + [NotebookSetting.cellExecutionScroll]: { + markdownDescription: nls.localize('notebook.revealNextOnExecuteBehavior.description', "How far to scroll when revealing the next cell upon exectuting {0}.", 'notebook.cell.executeAndSelectBelow'), + type: 'string', + enum: ['fullCell', 'firstLine', 'none'], + markdownEnumDescriptions: [ + nls.localize('notebook.revealNextOnExecuteBehavior.fullCell.description', 'Scroll to fully reveal the next cell.'), + nls.localize('notebook.revealNextOnExecuteBehavior.firstLine.description', 'Scroll to reveal the first line of the next cell.'), + nls.localize('notebook.revealNextOnExecuteBehavior.nonedescription', 'Do not scroll to reveal the next cell.'), + ], + default: 'fullCell' } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 158b98692fe..f525ec94a5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -147,10 +147,15 @@ export interface ICommonCellInfo { readonly executionId?: string; } +export enum ScrollToRevealBehavior { + fullCell, + firstLine +} + export interface IFocusNotebookCellOptions { readonly skipReveal?: boolean; readonly focusEditorLine?: number; - readonly minimalScrolling?: boolean; + readonly revealBehavior?: ScrollToRevealBehavior | undefined; readonly outputId?: string; readonly altOutputId?: string; } @@ -325,7 +330,8 @@ export const enum CellRevealSyncType { Default = 1, Top = 2, Center = 3, - CenterIfOutsideViewport = 4 + CenterIfOutsideViewport = 4, + FirstLineIfOutsideViewport = 5 } export enum CellRevealRangeType { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 9269abcf748..c089810d23f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -52,7 +52,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { contrastBorder, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_PANE_BACKGROUND, PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealSyncType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealSyncType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger'; @@ -889,7 +889,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD NotebookCellList, 'NotebookCellList', this._body, - this._viewContext, + this._viewContext.notebookOptions, this._listDelegate, renderers, this.scopedContextKeyService, @@ -1512,7 +1512,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); if (this._dimension) { - this._list.layout(this._dimension.height, this._dimension.width); + this._list.layout(this.getBodyHeight(this._dimension.height), this._dimension.width); } else { this._list.layout(); } @@ -1779,6 +1779,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return this._scrollBeyondLastLine && !this.isEmbedded; } + private getBodyHeight(dimensionHeight: number) { + return Math.max(dimensionHeight - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0), 0); + } + layout(dimension: DOM.Dimension, shadowElement?: HTMLElement, position?: DOM.IDomPosition): void { if (!shadowElement && this._shadowElementViewInfo === null) { this._dimension = dimension; @@ -1802,7 +1806,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._dimension = dimension; this._position = position; - const newBodyHeight = Math.max(dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0), 0); + const newBodyHeight = this.getBodyHeight(dimension.height); DOM.size(this._body, dimension.width, newBodyHeight); const topInserToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType); @@ -2073,6 +2077,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._list.revealCell(cell, CellRevealSyncType.CenterIfOutsideViewport); } + revealFirstLineIfOutsideViewport(cell: ICellViewModel) { + this._list.revealCell(cell, CellRevealSyncType.FirstLineIfOutsideViewport); + } + async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Default); } @@ -2360,6 +2368,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.revealInCenterIfOutsideViewport(cell); } } else { + // focus container const itemDOM = this._list.domElementOfElement(cell); if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { (document.activeElement as HTMLElement).blur(); @@ -2373,8 +2382,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD if (typeof options?.focusEditorLine === 'number') { this._cursorNavMode.set(true); this.revealInView(cell); - } else if (options?.minimalScrolling) { + } else if (options?.revealBehavior === ScrollToRevealBehavior.fullCell) { this.revealInView(cell); + } else if (options?.revealBehavior === ScrollToRevealBehavior.firstLine) { + this.revealFirstLineIfOutsideViewport(cell); } else { this.revealInCenterIfOutsideViewport(cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 5700dc24c05..0f2c80929bb 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -25,13 +25,13 @@ import { ICellRange, cellRangesToIndexes, reduceCellRanges, cellRangesEqual } fr import { NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { clamp } from 'vs/base/common/numbers'; import { ISplice } from 'vs/base/common/sequence'; -import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { BaseCellRenderTemplate, INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IListViewOptions, IListView } from 'vs/base/browser/ui/list/listView'; import { NotebookCellListView } from 'vs/workbench/contrib/notebook/browser/view/notebookCellListView'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; const enum CellEditorRevealType { Line, @@ -148,7 +148,7 @@ export class NotebookCellList extends WorkbenchList implements ID constructor( private listUser: string, container: HTMLElement, - viewContext: ViewContext, + private readonly notebookOptions: NotebookOptions, delegate: IListVirtualDelegate, renderers: IListRenderer[], contextKeyService: IContextKeyService, @@ -828,12 +828,12 @@ export class NotebookCellList extends WorkbenchList implements ID this._revealInViewWithMinimalScrolling(startIndex); } - private _revealInViewWithMinimalScrolling(viewIndex: number) { + private _revealInViewWithMinimalScrolling(viewIndex: number, firstLine?: boolean) { const firstIndex = this.view.firstVisibleIndex; if (viewIndex <= firstIndex) { - this._revealInternal(viewIndex, true, CellRevealPosition.Top); + this._revealInternal(viewIndex, true, CellRevealPosition.Top, firstLine); } else { - this._revealInternal(viewIndex, true, CellRevealPosition.Bottom); + this._revealInternal(viewIndex, true, CellRevealPosition.Bottom, firstLine); } } @@ -863,13 +863,16 @@ export class NotebookCellList extends WorkbenchList implements ID case CellRevealSyncType.CenterIfOutsideViewport: this._revealInternal(index, true, CellRevealPosition.Center); break; + case CellRevealSyncType.FirstLineIfOutsideViewport: + this._revealInViewWithMinimalScrolling(index, true); + break; case CellRevealSyncType.Default: this._revealInViewWithMinimalScrolling(index); break; } } - private _revealInternal(viewIndex: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) { + private _revealInternal(viewIndex: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition, firstLine?: boolean) { if (viewIndex >= this.view.length) { return; } @@ -879,15 +882,9 @@ export class NotebookCellList extends WorkbenchList implements ID const elementTop = this.view.elementTop(viewIndex); const elementBottom = this.view.elementHeight(viewIndex) + elementTop; - if (ignoreIfInsideViewport - && elementTop >= scrollTop - && elementBottom < wrapperBottom) { - - if (revealPosition === CellRevealPosition.Center - && elementBottom > wrapperBottom - && elementTop > (scrollTop + wrapperBottom) / 2) { - // the element is partially visible and it's below the center of the viewport - } else { + if (ignoreIfInsideViewport) { + if (elementTop >= scrollTop && elementBottom < wrapperBottom) { + // element is already fully visible return; } } @@ -917,6 +914,18 @@ export class NotebookCellList extends WorkbenchList implements ID } break; case CellRevealPosition.Bottom: + if (firstLine) { + const lineHeight = this.viewModel?.layoutInfo?.fontInfo.lineHeight ?? 15; + const padding = this.notebookOptions.getLayoutConfiguration().cellTopMargin + this.notebookOptions.getLayoutConfiguration().editorTopPadding; + const firstLineLocation = elementTop + lineHeight + padding; + if (firstLineLocation < wrapperBottom) { + // first line is already visible + return; + } + + this.view.setScrollTop(this.scrollTop + (firstLineLocation - wrapperBottom)); + break; + } this.view.setScrollTop(this.scrollTop + (elementBottom - wrapperBottom)); this.view.setScrollTop(this.scrollTop + (this.view.elementTop(viewIndex) + this.view.elementHeight(viewIndex) - this.getViewScrollBottom())); break; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index cdd7b0fc31d..5408c07d002 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -963,7 +963,8 @@ export const NotebookSetting = { logging: 'notebook.logging', confirmDeleteRunningCell: 'notebook.confirmDeleteRunningCell', remoteSaving: 'notebook.experimental.remoteSave', - gotoSymbolsAllSymbols: 'notebook.gotoSymbols.showAllSymbols' + gotoSymbolsAllSymbols: 'notebook.gotoSymbols.showAllSymbols', + cellExecutionScroll: 'notebook.revealNextOnExecuteBehavior' } as const; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index ab178faad20..b67c8750640 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -429,11 +429,13 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe disposeTemplate() { } }; + const notebookOptions = !!viewContext ? viewContext.notebookOptions + : disposables.add(new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false)); const cellList: NotebookCellList = disposables.add(instantiationService.createInstance( NotebookCellList, 'NotebookCellList', DOM.$('container'), - viewContext ?? new ViewContext(disposables.add(new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false)), disposables.add(new NotebookEventDispatcher()), () => ({} as IBaseCellEditorOptions)), + notebookOptions, delegate, [renderer], instantiationService.get(IContextKeyService), diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index ab3b550f8b1..6bf05c15a82 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -204,11 +204,11 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon menu: [{ id: MenuId.GlobalActivity, group: '2_configuration', - order: 1 + order: 2 }, { id: MenuId.MenubarPreferencesMenu, group: '2_configuration', - order: 1 + order: 2 }], }); } @@ -773,7 +773,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon { id: MenuId.GlobalActivity, group: '2_configuration', - order: 3 + order: 4 } ] }); @@ -789,7 +789,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon title: nls.localize('keyboardShortcuts', "Keyboard Shortcuts"), }, group: '2_configuration', - order: 3 + order: 4 })); registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 8c9da329807..91eed4d33fc 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1103,18 +1103,19 @@ export class SettingsEditor2 extends EditorPane { const reportModifiedProps = { key, query, - searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(), - rawResults: this.searchResultModel && this.searchResultModel.getRawResults(), + searchResults: this.searchResultModel?.getUniqueResults() ?? null, + rawResults: this.searchResultModel?.getRawResults() ?? null, showConfiguredOnly: !!this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG), isReset: typeof value === 'undefined', settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget }; + this.pendingSettingUpdate = null; return this.reportModifiedSetting(reportModifiedProps); }); } - private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult[] | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void { + private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void { type SettingsEditorModifiedSettingEvent = { key: string; groupId: string | undefined; @@ -1133,29 +1134,21 @@ export class SettingsEditor2 extends EditorPane { isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Identifies whether a setting was reset to its default value.' }; target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The scope of the setting, such as user or workspace.' }; owner: 'rzhao271'; - comment: 'Event which fires when the user modifies a setting in the settings editor'; + comment: 'Event emitted when the user modifies a setting in the Settings editor'; }; - this.pendingSettingUpdate = null; - let groupId: string | undefined = undefined; let nlpIndex: number | undefined = undefined; let displayIndex: number | undefined = undefined; if (props.searchResults) { - const remoteResult = props.searchResults[SearchResultIdx.Remote]; - const localResult = props.searchResults[SearchResultIdx.Local]; - - const localIndex = localResult!.filterMatches.findIndex(m => m.setting.key === props.key); - groupId = localIndex >= 0 ? - 'local' : - 'remote'; - - displayIndex = localIndex >= 0 ? - localIndex : - remoteResult && (remoteResult.filterMatches.findIndex(m => m.setting.key === props.key) + localResult.filterMatches.length); + displayIndex = props.searchResults.filterMatches.findIndex(m => m.setting.key === props.key); if (this.searchResultModel) { const rawResults = this.searchResultModel.getRawResults(); + if (rawResults[SearchResultIdx.Local] && displayIndex >= 0) { + const settingInLocalResults = rawResults[SearchResultIdx.Local].filterMatches.some(m => m.setting.key === props.key); + groupId = settingInLocalResults ? 'local' : 'remote'; + } if (rawResults[SearchResultIdx.Remote]) { const _nlpIndex = rawResults[SearchResultIdx.Remote].filterMatches.findIndex(m => m.setting.key === props.key); nlpIndex = _nlpIndex >= 0 ? _nlpIndex : undefined; @@ -1487,7 +1480,7 @@ export class SettingsEditor2 extends EditorPane { await this.triggerSearch(query.replace(/\u203A/g, ' ')); if (query && this.searchResultModel) { - this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(this.searchResultModel!.getUniqueResults())); + this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(this.searchResultModel)); } } @@ -1578,49 +1571,41 @@ export class SettingsEditor2 extends EditorPane { return filterModel; } - private reportFilteringUsed(results: ISearchResult[]): void { + private reportFilteringUsed(searchResultModel: SearchResultModel | null): void { + if (!searchResultModel) { + return; + } + type SettingsEditorFilterEvent = { - 'durations.nlpResult': number | undefined; 'counts.nlpResult': number | undefined; 'counts.filterResult': number | undefined; - 'requestCount': number | undefined; + 'counts.uniqueResultsCount': number | undefined; }; type SettingsEditorFilterClassification = { - 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'How long the remote search provider took, if applicable.' }; 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of matches found by the local search provider, if applicable.' }; - 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of requests sent to Bing, if applicable.' }; + 'counts.uniqueResultsCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of unique matches over both search providers, if applicable.' }; owner: 'rzhao271'; - comment: 'Tracks the number of requests and performance of the built-in search providers'; + comment: 'Tracks the performance of the built-in search providers'; }; - - const nlpResult = results[SearchResultIdx.Remote]; - const nlpMetadata = nlpResult?.metadata; - - const duration = { - nlpResult: nlpMetadata?.duration - }; - // Count unique results const counts: { nlpResult?: number; filterResult?: number } = {}; - const filterResult = results[SearchResultIdx.Local]; + const rawResults = searchResultModel.getRawResults(); + const filterResult = rawResults[SearchResultIdx.Local]; if (filterResult) { counts['filterResult'] = filterResult.filterMatches.length; } - + const nlpResult = rawResults[SearchResultIdx.Remote]; if (nlpResult) { counts['nlpResult'] = nlpResult.filterMatches.length; } - const requestCount = nlpMetadata?.requestCount; - + const uniqueResults = searchResultModel.getUniqueResults(); const data = { - 'durations.nlpResult': duration.nlpResult, 'counts.nlpResult': counts['nlpResult'], 'counts.filterResult': counts['filterResult'], - requestCount + 'counts.uniqueResultsCount': uniqueResults?.filterMatches.length }; - this.telemetryService.publicLog2('settingsEditor.filter', data); } @@ -1815,7 +1800,8 @@ class SyncControls extends Disposable { container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService + @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @ITelemetryService telemetryService: ITelemetryService, ) { super(); @@ -1826,10 +1812,14 @@ class SyncControls extends Disposable { DOM.hide(this.lastSyncedLabel); this.turnOnSyncButton.enabled = true; - this.turnOnSyncButton.label = localize('turnOnSyncButton', "Turn on Settings Sync"); + this.turnOnSyncButton.label = localize('turnOnSyncButton', "Backup and Sync Settings"); DOM.hide(this.turnOnSyncButton.element); this._register(this.turnOnSyncButton.onDidClick(async () => { + telemetryService.publicLog2<{}, { + owner: 'sandy081'; + comment: 'This event tracks whenever settings sync is turned on from settings editor.'; + }>('sync/turnOnSyncFromSettings'); await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn'); })); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 180f990b5a5..7e30259a1bb 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -11,7 +11,7 @@ import { ConfigurationTarget, IConfigurationValue } from 'vs/platform/configurat import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { ITOCEntry, knownAcronyms, knownTermMappings, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; -import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; +import { IExtensionSetting, ISearchResult, ISetting, ISettingMatch, SettingMatchType, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -831,12 +831,12 @@ function isObjectSetting({ } // Flatten anyof schemas - const flatSchemas = arrays.flatten(schemas.map((schema): IJSONSchema[] => { + const flatSchemas = schemas.map((schema): IJSONSchema[] => { if (Array.isArray(schema.anyOf)) { return schema.anyOf; } return [schema]; - })); + }).flat(); return flatSchemas.every(isObjectRenderableSchema); } @@ -855,7 +855,7 @@ export const enum SearchResultIdx { export class SearchResultModel extends SettingsTreeModel { private rawSearchResults: ISearchResult[] | null = null; - private cachedUniqueSearchResults: ISearchResult[] | null = null; + private cachedUniqueSearchResults: ISearchResult | null = null; private newExtensionSearchResults: ISearchResult | null = null; private searchResultCount: number | null = null; @@ -874,29 +874,86 @@ export class SearchResultModel extends SettingsTreeModel { this.update({ id: 'searchResultModel', label: '' }); } - getUniqueResults(): ISearchResult[] { + private compareTwoNullableNumbers(a: number | undefined, b: number | undefined): number { + const aOrMax = a ?? Number.MAX_SAFE_INTEGER; + const bOrMax = b ?? Number.MAX_SAFE_INTEGER; + if (aOrMax < bOrMax) { + return -1; + } else if (aOrMax > bOrMax) { + return 1; + } else { + return 0; + } + } + + private sortResults(filterMatches: ISettingMatch[]): ISettingMatch[] { + filterMatches.sort((a, b) => { + if (a.matchType !== b.matchType) { + // Sort by match type if the match types are not the same. + // The priority of the match type is given by the SettingMatchType enum. + return b.matchType - a.matchType; + } else if (a.matchType === SettingMatchType.RemoteMatch) { + // The match types are the same and are RemoteMatch. + // Sort by score. + return b.score - a.score; + } else { + // The match types are the same. + if (a.setting.extensionInfo && b.setting.extensionInfo + && a.setting.extensionInfo.id === b.setting.extensionInfo.id) { + // These settings belong to the same extension. + if (a.setting.categoryLabel !== b.setting.categoryLabel + && (a.setting.categoryOrder !== undefined || b.setting.categoryOrder !== undefined) + && a.setting.categoryOrder !== b.setting.categoryOrder) { + // These two settings don't belong to the same category and have different category orders. + return this.compareTwoNullableNumbers(a.setting.categoryOrder, b.setting.categoryOrder); + } else if (a.setting.categoryLabel === b.setting.categoryLabel + && (a.setting.order !== undefined || b.setting.order !== undefined) + && a.setting.order !== b.setting.order) { + // These two settings belong to the same category, but have different orders. + return this.compareTwoNullableNumbers(a.setting.order, b.setting.order); + } + } + // In the worst case, go back to lexicographical order. + return b.score - a.score; + } + }); + return filterMatches; + } + + getUniqueResults(): ISearchResult | null { if (this.cachedUniqueSearchResults) { return this.cachedUniqueSearchResults; } if (!this.rawSearchResults) { - return []; + return null; } + let combinedFilterMatches: ISettingMatch[] = []; + const localMatchKeys = new Set(); const localResult = this.rawSearchResults[SearchResultIdx.Local]; - localResult?.filterMatches.forEach(m => localMatchKeys.add(m.setting.key)); + if (localResult) { + localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key)); + combinedFilterMatches = localResult.filterMatches; + } const remoteResult = this.rawSearchResults[SearchResultIdx.Remote]; if (remoteResult) { remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key)); - } + combinedFilterMatches = combinedFilterMatches.concat(remoteResult.filterMatches); - if (remoteResult) { this.newExtensionSearchResults = this.rawSearchResults[SearchResultIdx.NewExtensions]; } - this.cachedUniqueSearchResults = [localResult, remoteResult]; + // Combine and sort results. + combinedFilterMatches = this.sortResults(combinedFilterMatches); + + this.cachedUniqueSearchResults = { + filterMatches: combinedFilterMatches, + exactMatch: localResult?.exactMatch || remoteResult?.exactMatch + }; + return this.cachedUniqueSearchResults; } @@ -956,14 +1013,7 @@ export class SearchResultModel extends SettingsTreeModel { } private getFlatSettings(): ISetting[] { - const flatSettings: ISetting[] = []; - arrays.coalesce(this.getUniqueResults()) - .forEach(r => { - flatSettings.push( - ...r.filterMatches.map(m => m.setting)); - }); - - return flatSettings; + return this.getUniqueResults()?.filterMatches.map(m => m.setting) ?? []; } } diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 1dc6c5919bf..7f00f9f2e9c 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -40,7 +40,7 @@ import { CHAT_OPEN_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/c export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { - private static AI_RELATED_INFORMATION_MAX_PICKS = 3; + private static AI_RELATED_INFORMATION_MAX_PICKS = 5; private static AI_RELATED_INFORMATION_THRESHOLD = 0.8; private static AI_RELATED_INFORMATION_DEBOUNCE = 200; @@ -165,13 +165,6 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce return []; } - if (additionalPicks.length) { - additionalPicks.unshift({ - type: 'separator', - label: localize('similarCommands', "similar commands") - }); - } - if (picksSoFar.length || additionalPicks.length) { additionalPicks.push({ type: 'separator' diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 400c9af2d00..b33088511af 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -33,7 +33,7 @@ export class SCMStatusController implements IWorkbenchContribution { private focusDisposable: IDisposable = Disposable.None; private focusedRepository: ISCMRepository | undefined = undefined; private readonly badgeDisposable = new MutableDisposable(); - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private repositoryDisposables = new Set(); constructor( @@ -207,7 +207,7 @@ export class SCMStatusController implements IWorkbenchContribution { this.focusDisposable.dispose(); this.statusBarDisposable.dispose(); this.badgeDisposable.dispose(); - this.disposables = dispose(this.disposables); + this.disposables.dispose(); dispose(this.repositoryDisposables.values()); this.repositoryDisposables.clear(); } @@ -217,7 +217,7 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu private activeResourceHasChangesContextKey: IContextKey; private activeResourceRepositoryContextKey: IContextKey; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private repositoryDisposables = new Set(); constructor( @@ -281,7 +281,7 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu } dispose(): void { - this.disposables = dispose(this.disposables); + this.disposables.dispose(); dispose(this.repositoryDisposables.values()); this.repositoryDisposables.clear(); } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index b858e0fc16c..f278f782373 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -714,7 +714,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu private session: IDisposable = Disposable.None; private mouseDownInfo: { lineNumber: number } | null = null; private enabled = false; - private gutterActionDisposables = new DisposableStore(); + private readonly gutterActionDisposables = new DisposableStore(); private stylesheet: HTMLStyleElement; constructor( @@ -741,8 +741,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu private onDidChangeGutterAction(): void { const gutterAction = this.configurationService.getValue<'diff' | 'none'>('scm.diffDecorationsGutterAction'); - this.gutterActionDisposables.dispose(); - this.gutterActionDisposables = new DisposableStore(); + this.gutterActionDisposables.clear(); if (gutterAction === 'diff') { this.gutterActionDisposables.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 2478d508c61..f92c54db113 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -110,12 +110,54 @@ line-height: 22px; } +.scm-view .monaco-list-row .history, +.scm-view .monaco-list-row .history-item-group, .scm-view .monaco-list-row .resource-group { display: flex; height: 100%; 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; + font-size: 14px; + padding-right: 4px; +} + +.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .icon-container { + display: flex; + font-size: 14px; + padding-right: 4px; +} + +.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .avatar { + width: 14px; + height: 14px; + border-radius: 14px; +} + +.scm-view .monaco-list-row .history > .name, +.scm-view .monaco-list-row .history-item-group > .name, .scm-view .monaco-list-row .resource-group > .name { flex: 1; overflow: hidden; diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index f1a041e1a7a..9918414d4c0 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -33,7 +33,7 @@ export class SCMTitleMenu implements IDisposable { readonly onDidChangeTitle = this._onDidChangeTitle.event; readonly menu: IMenu; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); constructor( @IMenuService menuService: IMenuService, diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index f78bcd673ab..eeffaa40d44 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 } 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 } from 'vs/workbench/contrib/scm/common/scm'; +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 { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; @@ -32,6 +32,7 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/contrib/workspace/common/workspace'; import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff'; import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService'; +import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -79,7 +80,7 @@ viewsRegistry.registerViews([{ ctorDescriptor: new SyncDescriptor(SCMViewPane), canToggleVisibility: true, canMoveView: true, - weight: 80, + weight: 60, order: -999, containerIcon: sourceControlViewIcon, openCommandActionDescriptor: { @@ -109,6 +110,17 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon }], viewContainer); +viewsRegistry.registerViews([{ + id: SYNC_VIEW_PANE_ID, + name: localize('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); @@ -279,6 +291,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."), default: true + }, + 'scm.experimental.showSyncView': { + type: 'boolean', + description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."), + default: false } } }); diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts new file mode 100644 index 00000000000..5e0db441a80 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts @@ -0,0 +1,668 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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 { IOpenEvent, WorkbenchAsyncDataTree } 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, 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 } from 'vs/workbench/contrib/scm/common/scm'; +import { comparePaths } from 'vs/base/common/comparers'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; +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'; + +type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement; + +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 { + throw new Error('Invalid tree element'); + } +} + +interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { + readonly description?: string; + readonly ancestor?: string; + readonly count?: number; + readonly repository: ISCMRepository; + readonly type: 'historyItemGroup'; +} + +interface SCMHistoryItemTreeElement extends ISCMHistoryItem { + readonly historyItemGroup: SCMHistoryItemGroupTreeElement; + readonly type: 'historyItem'; +} + +interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange { + readonly historyItem: SCMHistoryItemTreeElement; + readonly type: 'historyItemChange'; +} + +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 { + throw new Error('Invalid tree element'); + } + } +} + +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 ITreeRenderer { + + static readonly TEMPLATE_ID = 'historyItemChange'; + get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; } + + constructor(private labels: ResourceLabels) { } + + 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 { + templateData.fileLabel.setFile(node.element.uri, { + fileDecorations: { colors: false, badges: true }, + fileKind: FileKind.FILE, + hidePath: false, + }); + } + + 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: string[] = []; + + result.push(basename(element.uri)); + + // TODO - add decoration + // if (element.decorations.tooltip) { + // result.push(element.decorations.tooltip); + // } + + 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!: WorkbenchAsyncDataTree; + + 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); + + this._tree = this.instantiationService.createInstance( + WorkbenchAsyncDataTree, + 'SCM Sync View', + this.treeContainer, + new ListDelegate(), + [ + this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ActionButtonRenderer), + this.instantiationService.createInstance(HistoryItemGroupRenderer), + this.instantiationService.createInstance(HistoryItemRenderer), + this.instantiationService.createInstance(HistoryItemChangeRenderer, this.listLabels), + ], + this.instantiationService.createInstance(SCMSyncDataSource), + { + horizontalScrolling: false, + accessibilityProvider: this.instantiationService.createInstance(SCMSyncViewPaneAccessibilityProvider), + identityProvider: this.instantiationService.createInstance(SCMSyncViewPaneTreeIdentityProvider), + sorter: this.instantiationService.createInstance(SCMSyncViewPaneTreeSorter), + }) as WorkbenchAsyncDataTree; + + 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)); + } + + protected override layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this._tree.layout(height, width); + } + + private async onDidOpen(e: IOpenEvent): Promise { + if (!e.element) { + return; + } else 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); + } + } + } + + private updateIndentStyles(theme: any): void { + this.treeContainer.classList.toggle('align-icons-and-twisties', theme.hasFileIcons || (theme.hasFileIcons && !theme.hasFolderIcons)); + } + + override dispose(): void { + this.disposables.dispose(); + super.dispose(); + } +} + +class SCMSyncPaneViewModel { + + private repositories = new Map(); + private historyProviders = new Map(); + + private alwaysShowRepositories = false; + + private readonly disposables = new DisposableStore(); + + constructor( + private readonly tree: WorkbenchAsyncDataTree, + @ISCMViewService scmViewService: ISCMViewService, + @IConfigurationService private readonly configurationService: IConfigurationService, + + ) { + configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); + this.onDidChangeConfiguration(); + + scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.disposables); + this._onDidChangeVisibleRepositories({ added: scmViewService.visibleRepositories, removed: [] }); + } + + 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 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 { + + 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 { + 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 children; + } + + // Action Button + const actionButton = historyProvider.actionButton; + if (actionButton) { + children.push({ + type: 'actionButton', + repository: element, + button: actionButton + } as ISCMActionButton); + } + + // History item group base + const historyItemGroupBase = await historyProvider.resolveHistoryItemGroupBase(historyItemGroup.id); + if (!historyItemGroupBase) { + return children; + } + + // Common ancestor, ahead, behind + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(historyItemGroup.id, historyItemGroupBase.id); + + // 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 + if (historyItemGroup) { + children.push({ + id: historyItemGroup.id, + label: `$(cloud-upload) ${historyItemGroup.label}`, + description: localize('outgoing', "Outgoing Changes"), + ancestor: ancestor?.id, + count: ancestor?.ahead ?? 0, + repository: element, + type: 'historyItemGroup' + } as SCMHistoryItemGroupTreeElement); + } + } else if (isSCMHistoryItemGroupTreeElement(element)) { + const scmProvider = element.repository.provider; + const historyProvider = scmProvider.historyProvider; + + if (!historyProvider) { + return children; + } + + const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? []; + children.push(...historyItems.map(historyItem => ({ + id: historyItem.id, + label: historyItem.label, + description: historyItem.description, + icon: historyItem.icon, + historyItemGroup: element, + type: 'historyItem' + } as SCMHistoryItemTreeElement))); + } else if (isSCMHistoryItemTreeElement(element)) { + const repository = element.historyItemGroup.repository; + const historyProvider = repository.provider.historyProvider; + + if (!historyProvider) { + return children; + } + + // History Item Changes + const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? []; + children.push(...changes.map(change => ({ + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement))); + } else { + throw new Error('getChildren Method not implemented.'); + } + + return children; + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 9060e9dbdca..47801632b7d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -112,7 +112,7 @@ interface ActionButtonTemplate { readonly templateDisposable: IDisposable; } -class ActionButtonRenderer implements ICompressibleTreeRenderer { +export class ActionButtonRenderer implements ICompressibleTreeRenderer { static readonly DEFAULT_HEIGHT = 30; static readonly TEMPLATE_ID = 'actionButton'; @@ -453,7 +453,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer(); constructor( @@ -1036,7 +1036,7 @@ class RepositoryVisibilityActionController { private items = new Map(); private repositoryCountContextKey: IContextKey; private repositoryVisibilityCountContextKey: IContextKey; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); constructor( @ISCMViewService private scmViewService: ISCMViewService, @@ -1178,12 +1178,12 @@ class ViewModel { } private items = new Map(); - private visibilityDisposables = new DisposableStore(); + private readonly visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; private alwaysShowRepositories = false; private showActionButton = false; private firstVisible = true; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private modeContextKey: IContextKey; private sortKeyContextKey: IContextKey; @@ -1352,7 +1352,6 @@ class ViewModel { setVisible(visible: boolean): void { if (visible) { - this.visibilityDisposables = new DisposableStore(); this.scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.visibilityDisposables); this._onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); @@ -1366,7 +1365,7 @@ class ViewModel { } else { this.updateViewState(); - this.visibilityDisposables.dispose(); + this.visibilityDisposables.clear(); this._onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); this.scrollTop = this.tree.scrollTop; } @@ -1807,11 +1806,11 @@ class SCMInputWidget { private editorContainer: HTMLElement; private placeholderTextContainer: HTMLElement; private inputEditor: CodeEditorWidget; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private model: { readonly input: ISCMInput; textModelRef?: IReference } | undefined; private repositoryIdContextKey: IContextKey; - private repositoryDisposables = new DisposableStore(); + private readonly repositoryDisposables = new DisposableStore(); private validation: IInputValidation | undefined; private validationDisposable: IDisposable = Disposable.None; @@ -1837,8 +1836,7 @@ class SCMInputWidget { this.clearValidation(); this.editorContainer.classList.remove('synthetic-focus'); - this.repositoryDisposables.dispose(); - this.repositoryDisposables = new DisposableStore(); + this.repositoryDisposables.clear(); this.repositoryIdContextKey.set(input?.repository.id); if (!input) { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 67ff52a110e..e931a6d4fd9 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -57,7 +57,7 @@ export class SCMViewService implements ISCMViewService { private didFinishLoading: boolean = false; private didSelectRepository: boolean = false; private previousState: ISCMViewServiceState | undefined; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private _repositories: ISCMRepositoryView[] = []; diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index f27763e9c26..34b7170c71e 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -17,6 +17,10 @@ import { Command } from 'vs/editor/common/languages'; import { reset } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +export function isSCMRepositoryArray(element: any): element is ISCMRepository[] { + return Array.isArray(element) && element.every(r => isSCMRepository(r)); +} + export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && !!(element as ISCMRepository).input; } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts new file mode 100644 index 00000000000..b9f6792c9ea --- /dev/null +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; + +export interface ISCMHistoryProvider { + + readonly onDidChangeActionButton: Event; + readonly onDidChangeCurrentHistoryItemGroup: Event; + + get actionButton(): ISCMActionButtonDescriptor | undefined; + set actionButton(button: ISCMActionButtonDescriptor | undefined); + + get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined; + set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined); + + provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; + provideHistoryItemChanges(historyItemId: string): Promise; + resolveHistoryItemGroupBase(historyItemGroupId: string): Promise; + resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string): Promise<{ id: string; ahead: number; behind: number } | undefined>; +} + +export interface ISCMHistoryOptions { + readonly cursor?: string; + readonly limit?: number | { id?: string }; +} + +export interface ISCMHistoryItemGroup { + readonly id: string; + readonly label: string; + readonly upstream?: ISCMRemoteHistoryItemGroup; +} + +export interface ISCMRemoteHistoryItemGroup { + readonly id: string; + readonly label: string; +} + +export interface ISCMHistoryItem { + readonly id: string; + readonly parentIds: string[]; + readonly label: string; + readonly description?: string; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; + readonly timestamp?: number; +} + +export interface ISCMHistoryItemChange { + readonly uri: URI; + readonly originalUri?: URI; + readonly modifiedUri?: URI; + readonly renameUri?: URI; +} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 4c4c3cc3729..e7d39bfecbb 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -13,10 +13,12 @@ import { IAction } from 'vs/base/common/actions'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; 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; @@ -63,7 +65,9 @@ export interface ISCMProvider extends IDisposable { readonly inputBoxDocumentUri: URI; readonly count?: number; readonly commitTemplate: string; + readonly historyProvider?: ISCMHistoryProvider; readonly onDidChangeCommitTemplate: Event; + readonly onDidChangeHistoryProvider: Event; readonly onDidChangeStatusBarCommands?: Event; readonly acceptInputCommand?: Command; readonly actionButton?: ISCMActionButtonDescriptor; diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 38867b0e96a..b8a34af87b1 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; @@ -11,7 +11,10 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { HistoryNavigator2 } from 'vs/base/common/history'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; import { Iterable } from 'vs/base/common/iterator'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; class SCMInput implements ISCMInput { @@ -94,108 +97,31 @@ class SCMInput implements ISCMInput { private readonly _onDidChangeValidateInput = new Emitter(); readonly onDidChangeValidateInput: Event = this._onDidChangeValidateInput.event; - private historyNavigator: HistoryNavigator2; - private didChangeHistory: boolean; - - private static didGarbageCollect = false; - private static migrateAndGarbageCollectStorage(storageService: IStorageService): void { - if (SCMInput.didGarbageCollect) { - return; - } - - // Migrate from old format // TODO@joao: remove this migration code a few releases - const userKeys = Iterable.filter(storageService.keys(StorageScope.APPLICATION, StorageTarget.USER), key => key.startsWith('scm/input:')); - - for (const key of userKeys) { - try { - const rawHistory = storageService.get(key, StorageScope.APPLICATION, ''); - const history = JSON.parse(rawHistory); - - if (Array.isArray(history)) { - if (history.length === 0 || (history.length === 1 && history[0] === '')) { - // remove empty histories - storageService.remove(key, StorageScope.APPLICATION); - } else { - // migrate existing histories to have a timestamp - storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.APPLICATION, StorageTarget.MACHINE); - } - } else { - // move to MACHINE target - storageService.store(key, rawHistory, StorageScope.APPLICATION, StorageTarget.MACHINE); - } - } catch { - // remove unparseable entries - storageService.remove(key, StorageScope.APPLICATION); - } - } - - // Garbage collect - const machineKeys = Iterable.filter(storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE), key => key.startsWith('scm/input:')); - - for (const key of machineKeys) { - try { - const history = JSON.parse(storageService.get(key, StorageScope.APPLICATION, '')); - - if (Array.isArray(history?.history) && Number.isInteger(history?.timestamp) && new Date().getTime() - history?.timestamp > 2592000000) { - // garbage collect after 30 days - storageService.remove(key, StorageScope.APPLICATION); - } - } catch { - // remove unparseable entries - storageService.remove(key, StorageScope.APPLICATION); - } - } - - SCMInput.didGarbageCollect = true; - } + private readonly historyNavigator: HistoryNavigator2; + private didChangeHistory: boolean = false; constructor( readonly repository: ISCMRepository, - @IStorageService private storageService: IStorageService + private readonly history: SCMInputHistory ) { - SCMInput.migrateAndGarbageCollectStorage(storageService); - - const key = this.repository.provider.rootUri ? `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri?.path}` : undefined; - let history: string[] | undefined; - - if (key) { - try { - history = JSON.parse(this.storageService.get(key, StorageScope.APPLICATION, '')).history; - history = history?.map(s => s ?? ''); - } catch { - // noop - } - } - - if (!Array.isArray(history) || history.length === 0) { - history = [this._value]; - } else { - this._value = history[history.length - 1]; - } - - this.historyNavigator = new HistoryNavigator2(history, 50); - this.didChangeHistory = false; - - if (key) { - this.storageService.onWillSaveState(_ => { + if (this.repository.provider.rootUri) { + this.historyNavigator = history.getHistory(this.repository.provider.label, this.repository.provider.rootUri); + this.history.onWillSaveHistory(event => { if (this.historyNavigator.isAtEnd()) { this.saveValue(); } - if (!this.didChangeHistory) { - return; + if (this.didChangeHistory) { + event.historyDidIndeedChange(); } - const history = [...this.historyNavigator].map(s => s ?? ''); - - if (history.length === 0 || (history.length === 1 && history[0] === '')) { - storageService.remove(key, StorageScope.APPLICATION); - } else { - storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.APPLICATION, StorageTarget.MACHINE); - } this.didChangeHistory = false; }); + } else { // in memory only + this.historyNavigator = new HistoryNavigator2([''], 100); } + + this._value = this.historyNavigator.current(); } setValue(value: string, transient: boolean, reason?: SCMInputChangeReason) { @@ -253,14 +179,16 @@ class SCMRepository implements ISCMRepository { private readonly _onDidChangeSelection = new Emitter(); readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; - readonly input: ISCMInput = new SCMInput(this, this.storageService); + readonly input: ISCMInput; constructor( public readonly id: string, public readonly provider: ISCMProvider, private disposable: IDisposable, - @IStorageService private storageService: IStorageService - ) { } + inputHistory: SCMInputHistory + ) { + this.input = new SCMInput(this, inputHistory); + } setSelected(selected: boolean): void { if (this._selected === selected) { @@ -277,6 +205,142 @@ class SCMRepository implements ISCMRepository { } } +class WillSaveHistoryEvent { + private _didChangeHistory = false; + get didChangeHistory() { return this._didChangeHistory; } + historyDidIndeedChange() { this._didChangeHistory = true; } +} + +class SCMInputHistory { + + private readonly disposables = new DisposableStore(); + private readonly histories = new Map>>(); + + private readonly _onWillSaveHistory = this.disposables.add(new Emitter()); + readonly onWillSaveHistory = this._onWillSaveHistory.event; + + constructor( + @IStorageService private storageService: IStorageService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + ) { + this.histories = new Map(); + + const entries = this.storageService.getObject<[string, URI, string[]][]>('scm.history', StorageScope.WORKSPACE, []); + + for (const [providerLabel, rootUri, history] of entries) { + let providerHistories = this.histories.get(providerLabel); + + if (!providerHistories) { + providerHistories = new ResourceMap(); + this.histories.set(providerLabel, providerHistories); + } + + providerHistories.set(rootUri, new HistoryNavigator2(history, 100)); + } + + if (this.migrateStorage()) { + this.saveToStorage(); + } + + this.disposables.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, 'scm.history', this.disposables)(e => { + if (e.external && e.key === 'scm.history') { + const raw = this.storageService.getObject<[string, URI, string[]][]>('scm.history', StorageScope.WORKSPACE, []); + + for (const [providerLabel, uri, rawHistory] of raw) { + const history = this.getHistory(providerLabel, uri); + + for (const value of Iterable.reverse(rawHistory)) { + history.prepend(value); + } + } + } + })); + + this.disposables.add(this.storageService.onWillSaveState(_ => { + const event = new WillSaveHistoryEvent(); + this._onWillSaveHistory.fire(event); + + if (event.didChangeHistory) { + this.saveToStorage(); + } + })); + } + + private saveToStorage(): void { + const raw: [string, URI, string[]][] = []; + + for (const [providerLabel, providerHistories] of this.histories) { + for (const [rootUri, history] of providerHistories) { + if (!(history.size === 1 && history.current() === '')) { + raw.push([providerLabel, rootUri, [...history]]); + } + } + } + + this.storageService.store('scm.history', raw, StorageScope.WORKSPACE, StorageTarget.USER); + } + + getHistory(providerLabel: string, rootUri: URI): HistoryNavigator2 { + let providerHistories = this.histories.get(providerLabel); + + if (!providerHistories) { + providerHistories = new ResourceMap(); + this.histories.set(providerLabel, providerHistories); + } + + let history = providerHistories.get(rootUri); + + if (!history) { + history = new HistoryNavigator2([''], 100); + providerHistories.set(rootUri, history); + } + + return history; + } + + // Migrates from Application scope storage to Workspace scope. + // TODO@joaomoreno: Change from January 2024 onwards such that the only code is to remove all `scm/input:` storage keys + private migrateStorage(): boolean { + let didSomethingChange = false; + const machineKeys = Iterable.filter(this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE), key => key.startsWith('scm/input:')); + + for (const key of machineKeys) { + try { + const legacyHistory = JSON.parse(this.storageService.get(key, StorageScope.APPLICATION, '')); + const match = /^scm\/input:([^:]+):(.+)$/.exec(key); + + if (!match || !Array.isArray(legacyHistory?.history) || !Number.isInteger(legacyHistory?.timestamp)) { + this.storageService.remove(key, StorageScope.APPLICATION); + continue; + } + + const [, providerLabel, rootPath] = match; + const rootUri = URI.file(rootPath); + + if (this.workspaceContextService.getWorkspaceFolder(rootUri)) { + const history = this.getHistory(providerLabel, rootUri); + + for (const entry of Iterable.reverse(legacyHistory.history as string[])) { + history.prepend(entry); + } + + didSomethingChange = true; + this.storageService.remove(key, StorageScope.APPLICATION); + } + } catch { + this.storageService.remove(key, StorageScope.APPLICATION); + } + } + + return didSomethingChange; + } + + dispose() { + this.disposables.dispose(); + } +} + + export class SCMService implements ISCMService { declare readonly _serviceBrand: undefined; @@ -285,6 +349,7 @@ export class SCMService implements ISCMService { get repositories(): Iterable { return this._repositories.values(); } get repositoryCount(): number { return this._repositories.size; } + private inputHistory: SCMInputHistory; private providerCount: IContextKey; private readonly _onDidAddProvider = new Emitter(); @@ -295,9 +360,11 @@ export class SCMService implements ISCMService { constructor( @ILogService private readonly logService: ILogService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, - @IStorageService private storageService: IStorageService + @IStorageService storageService: IStorageService ) { + this.inputHistory = new SCMInputHistory(storageService, workspaceContextService); this.providerCount = contextKeyService.createKey('scm.providerCount', 0); } @@ -314,7 +381,7 @@ export class SCMService implements ISCMService { this.providerCount.set(this._repositories.size); }); - const repository = new SCMRepository(provider.id, provider, disposable, this.storageService); + const repository = new SCMRepository(provider.id, provider, disposable, this.inputHistory); this._repositories.set(provider.id, repository); this._onDidAddProvider.fire(repository); diff --git a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts index 9d2a199fe18..396482865cc 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts @@ -236,8 +236,8 @@ export class ConfigureSnippetsAction extends SnippetsAction { }, f1: true, menu: [ - { id: MenuId.MenubarPreferencesMenu, group: '2_configuration', order: 4 }, - { id: MenuId.GlobalActivity, group: '2_configuration', order: 4 }, + { id: MenuId.MenubarPreferencesMenu, group: '2_configuration', order: 5 }, + { id: MenuId.GlobalActivity, group: '2_configuration', order: 5 }, ] }); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index a252d8e4814..81cb217e4b6 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -231,6 +231,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer public onDidChangeTaskSystemInfo: Event = this._onDidChangeTaskSystemInfo.event; private _onDidReconnectToTasks: Emitter = new Emitter(); public onDidReconnectToTasks: Event = this._onDidReconnectToTasks.event; + public get isReconnected(): boolean { return this._tasksReconnected; } constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -349,6 +350,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._attemptTaskReconnection(); } else { this._tasksReconnected = true; + this._onDidReconnectToTasks.fire(); } }); } diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 55ec14e22a0..a4b4239b43c 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -28,19 +28,18 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, @ILogService private readonly _logService: ILogService) { super(); - this._taskService.onDidReconnectToTasks((() => { - if (this._workspaceTrustManagementService.isWorkspaceTrusted()) { - this._tryRunTasks(); - } - })); - this._register(this._workspaceTrustManagementService.onDidChangeTrust(async trusted => { - if (trusted) { - await this._tryRunTasks(); - } - })); + if (this._taskService.isReconnected) { + this._tryRunTasks(); + } else { + this._register(Event.once(this._taskService.onDidReconnectToTasks)(async () => await this._tryRunTasks())); + } + this._register(this._workspaceTrustManagementService.onDidChangeTrust(async () => await this._tryRunTasks())); } private async _tryRunTasks() { + if (!this._workspaceTrustManagementService.isWorkspaceTrusted()) { + return; + } if (this._hasRunTasks || this._configurationService.getValue(ALLOW_AUTOMATIC_TASKS) === 'off') { return; } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 8ab974d4cbe..f4388813f7c 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -365,7 +365,7 @@ class UserTasksGlobalActionContribution extends Disposable implements IWorkbench }, when: TaskExecutionSupportedContext, group: '2_configuration', - order: 4 + order: 6 })); this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { command: { @@ -374,7 +374,7 @@ class UserTasksGlobalActionContribution extends Disposable implements IWorkbench }, when: TaskExecutionSupportedContext, group: '2_configuration', - order: 4 + order: 6 })); } } diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index b2331ad6bfd..a83ec7b1be1 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -55,18 +55,18 @@ export class TaskTerminalStatus extends Disposable { addTerminal(task: Task, terminal: ITerminalInstance, problemMatcher: AbstractProblemCollector) { const status: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, severity: Severity.Info }; terminal.statusList.add(status); - problemMatcher.onDidFindFirstMatch(() => { + this._register(problemMatcher.onDidFindFirstMatch(() => { this._marker = terminal.registerMarker(); - }); - problemMatcher.onDidFindErrors(() => { + })); + this._register(problemMatcher.onDidFindErrors(() => { if (this._marker) { terminal.addBufferMarker({ marker: this._marker, hoverMessage: nls.localize('task.watchFirstError', "Beginning of detected errors for this run"), disableCommandStorage: true }); } - }); - problemMatcher.onDidRequestInvalidateLastMarker(() => { + })); + this._register(problemMatcher.onDidRequestInvalidateLastMarker(() => { this._marker?.dispose(); this._marker = undefined; - }); + })); this.terminalMap.set(terminal.instanceId, { terminal, task, status, problemMatcher, taskRunEnded: false }); } diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index 0087529e25f..00121e6a40c 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -6,7 +6,7 @@ import { IStringDictionary, INumberDictionary } from 'vs/base/common/collections'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/model'; @@ -35,7 +35,7 @@ export interface IProblemMatcher { processLine(line: string): void; } -export abstract class AbstractProblemCollector implements IDisposable { +export abstract class AbstractProblemCollector extends Disposable implements IDisposable { private matchers: INumberDictionary; private activeMatcher: ILineMatcher | null; @@ -68,6 +68,7 @@ export abstract class AbstractProblemCollector implements IDisposable { readonly onDidRequestInvalidateLastMarker = this._onDidRequestInvalidateLastMarker.event; constructor(public readonly problemMatchers: ProblemMatcher[], protected markerService: IMarkerService, protected modelService: IModelService, fileService?: IFileService) { + super(); this.matchers = Object.create(null); this.bufferLength = 1; problemMatchers.map(elem => createLineMatcher(elem, fileService)).forEach((matcher) => { @@ -99,12 +100,12 @@ export abstract class AbstractProblemCollector implements IDisposable { this.resourcesToClean = new Map>(); this.markers = new Map>>(); this.deliveredMarkers = new Map>(); - this.modelService.onModelAdded((model) => { + this._register(this.modelService.onModelAdded((model) => { this.openModels[model.uri.toString()] = true; - }, this, this.modelListeners); - this.modelService.onModelRemoved((model) => { + }, this, this.modelListeners)); + this._register(this.modelService.onModelRemoved((model) => { delete this.openModels[model.uri.toString()]; - }, this, this.modelListeners); + }, this, this.modelListeners)); this.modelService.getModels().forEach(model => this.openModels[model.uri.toString()] = true); this._onDidStateChange = new Emitter(); @@ -127,7 +128,8 @@ export abstract class AbstractProblemCollector implements IDisposable { protected abstract processLineInternal(line: string): Promise; - public dispose() { + public override dispose() { + super.dispose(); this.modelListeners.dispose(); } diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index c96cba1125d..5799f23fd49 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -64,6 +64,7 @@ export interface IWorkspaceFolderTaskResult extends IWorkspaceTaskResult { export interface ITaskService { readonly _serviceBrand: undefined; onDidStateChange: Event; + isReconnected: boolean; onDidReconnectToTasks: Event; supportsMultipleTaskExecutions: boolean; diff --git a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts index 1e3880420fd..6de24a38863 100644 --- a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts +++ b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts @@ -5,6 +5,8 @@ import { ok } from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -60,6 +62,7 @@ class TestProblemCollector implements Partial { } suite('Task Terminal Status', () => { + let store: DisposableStore; let instantiationService: TestInstantiationService; let taskService: TestTaskService; let taskTerminalStatus: TaskTerminalStatus; @@ -68,6 +71,7 @@ suite('Task Terminal Status', () => { let problemCollector: AbstractProblemCollector; let audioCueService: TestAudioCueService; setup(() => { + store = new DisposableStore(); instantiationService = new TestInstantiationService(); taskService = new TestTaskService(); audioCueService = new TestAudioCueService(); @@ -75,10 +79,13 @@ suite('Task Terminal Status', () => { testTerminal = instantiationService.createInstance(TestTerminal) as any; testTask = instantiationService.createInstance(TestTask) as unknown as Task; problemCollector = instantiationService.createInstance(TestProblemCollector) as any; + store.add(instantiationService); + store.add(taskTerminalStatus); }); teardown(() => { - instantiationService.dispose(); + store.clear(); }); + ensureNoDisposablesAreLeakedInTestSuite(); test('Should add failed status when there is an exit code on task end', async () => { taskTerminalStatus.addTerminal(testTask, testTerminal, problemCollector); taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted }); diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index c32f1c421b0..a11d5cc80b7 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -285,10 +285,6 @@ border-style: solid; } -.xterm-screen .xterm-decoration-container .xterm-decoration.quick-fix { - z-index: 7; -} - .monaco-workbench .part.sidebar > .title > .title-actions .switch-terminal { display: flex; align-items: center; @@ -484,15 +480,6 @@ pointer-events: none; color: var(--vscode-terminalCommandDecoration-defaultBackground); } -.monaco-workbench .terminal .terminal-command-decoration.quick-fix { - color: var(--vscode-editorLightBulb-foreground) !important; - background-color: var(--vscode-terminal-background, var(--vscode-panel-background)); -} -.monaco-workbench .terminal .terminal-command-decoration.quick-fix.explainOnly { - /* Use success background to blend in with the terminal better as it's lower priority. We will - * probably want to add an explicit color for this eventually. */ - color: var(--vscode-terminalCommandDecoration-successBackground) !important; -} .terminal-scroll-highlight { left: 0; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 3b01c31dff1..d14a577769d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -491,7 +491,7 @@ export function registerTerminalActions() { return; } c.service.setActiveInstance(instance); - return c.groupService.showPanel(true); + focusActiveTerminal(instance, c); } }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts index d3ec1a63d6b..4c36da7dd25 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts @@ -4,25 +4,62 @@ *--------------------------------------------------------------------------------------------*/ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction } from 'vs/base/common/actions'; +import { ActionRunner, IAction } from 'vs/base/common/actions'; +import { asArray } from 'vs/base/common/arrays'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { SingleOrMany } from 'vs/base/common/types'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ISerializedTerminalInstanceContext } from 'vs/workbench/contrib/terminal/common/terminal'; -export function openContextMenu(event: MouseEvent, parent: HTMLElement, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void { +class InstanceContext { + private _instanceId: number; + + constructor(instance: ITerminalInstance) { + this._instanceId = instance.instanceId; + } + + toJSON(): ISerializedTerminalInstanceContext { + return { + $mid: MarshalledId.TerminalContext, + instanceId: this._instanceId + }; + } +} + +class TerminalContextActionRunner extends ActionRunner { + + // eslint-disable-next-line @typescript-eslint/naming-convention + protected override async runAction(action: IAction, context?: InstanceContext): Promise { + if (Array.isArray(context) && context.every(e => e instanceof InstanceContext)) { + // arg1: The (first) focused instance + // arg2: All selected instances + await action.run(context?.[0], context); + return; + } + return super.runAction(action, context); + } +} + +export function openContextMenu(event: MouseEvent, contextInstances: SingleOrMany | undefined, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void { const standardEvent = new StandardMouseEvent(event); const actions: IAction[] = []; - createAndFillInContextMenuActions(menu, undefined, actions); + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, actions); if (extraActions) { actions.push(...extraActions); } + const context: InstanceContext[] = contextInstances ? asArray(contextInstances).map(e => new InstanceContext(e)) : []; + contextMenuService.showContextMenu({ + actionRunner: new TerminalContextActionRunner(), getAnchor: () => standardEvent, getActions: () => actions, - getActionsContext: () => parent, + getActionsContext: () => context, }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index a7bd0423aad..24e872ca88e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -138,7 +138,7 @@ export class TerminalEditor extends EditorPane { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { - openContextMenu(event, this._editorInstanceElement!, this._instanceMenu, this._contextMenuService); + openContextMenu(event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); return; } @@ -176,7 +176,7 @@ export class TerminalEditor extends EditorPane { else if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { if (!this._cancelContextMenu) { - openContextMenu(event, this._editorInstanceElement!, this._instanceMenu, this._contextMenuService); + openContextMenu(event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); } event.preventDefault(); event.stopImmediatePropagation(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index d1365b440f2..797498b8dca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1935,13 +1935,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // to show just the file name. This should only happen if the title looks like an // absolute Windows file path this._sequence = title; - if (this._processManager.os === OperatingSystem.Windows) { - if (title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) { - title = path.win32.parse(title).name; - this._sequence = title; - } else { - this._sequence = undefined; - } + if (this._processManager.os === OperatingSystem.Windows && + title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) { + this._sequence = path.win32.parse(title).name; } break; } @@ -2181,7 +2177,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { items.push({ label: `$(${icon.id})`, description: `${icon.id}`, icon }); } const result = await this._quickInputService.pick(items, { - matchOnDescription: true + matchOnDescription: true, + placeHolder: nls.localize('changeIcon', 'Select an icon for the terminal') }); if (result) { this._icon = result.icon; @@ -2212,6 +2209,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const quickPick = this._quickInputService.createQuickPick(); quickPick.items = items; quickPick.matchOnDescription = true; + quickPick.placeholder = nls.localize('changeColor', 'Select a color for the terminal'); quickPick.show(); const disposables: IDisposable[] = []; const result = await new Promise(r => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 24930c1e40b..9df0f04db1e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -43,7 +43,6 @@ export class TerminalTabbedView extends Disposable { private _terminalContainer: HTMLElement; private _tabListElement: HTMLElement; - private _parentElement: HTMLElement; private _tabContainer: HTMLElement; private _tabList: TerminalTabList; @@ -83,8 +82,6 @@ export class TerminalTabbedView extends Disposable { ) { super(); - this._parentElement = parentElement; - this._tabContainer = $('.tabs-container'); const tabListContainer = $('.tabs-list-container'); this._tabListElement = $('.tabs-list'); @@ -357,7 +354,7 @@ export class TerminalTabbedView extends Disposable { else if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { - openContextMenu(event, this._parentElement, this._instanceMenu, this._contextMenuService); + openContextMenu(event, terminal, this._instanceMenu, this._contextMenuService); return; } @@ -389,8 +386,9 @@ export class TerminalTabbedView extends Disposable { if (rightClickBehavior === 'nothing' && !event.shiftKey) { this._cancelContextMenu = true; } + terminalContainer.focus(); if (!this._cancelContextMenu) { - openContextMenu(event, this._parentElement, this._instanceMenu, this._contextMenuService); + openContextMenu(event, this._terminalGroupService.activeInstance!, this._instanceMenu, this._contextMenuService); } event.preventDefault(); event.stopImmediatePropagation(); @@ -406,7 +404,16 @@ export class TerminalTabbedView extends Disposable { if (!emptyList) { this._terminalGroupService.lastAccessedMenu = 'tab-list'; } - openContextMenu(event, this._parentElement, emptyList ? this._tabsListEmptyMenu : this._tabsListMenu, this._contextMenuService, emptyList ? this._getTabActions() : undefined); + + // Put the focused item first as it's used as the first positional argument + const selectedInstances = this._tabList.getSelectedElements(); + const focusedInstance = this._tabList.getFocusedElements()?.[0]; + if (focusedInstance) { + selectedInstances.splice(selectedInstances.findIndex(e => e.instanceId === focusedInstance.instanceId), 1); + selectedInstances.unshift(focusedInstance); + } + + openContextMenu(event, selectedInstances, emptyList ? this._tabsListEmptyMenu : this._tabsListMenu, this._contextMenuService, emptyList ? this._getTabActions() : undefined); } event.preventDefault(); event.stopImmediatePropagation(); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 59898ee1b40..7755dceb67a 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -6,26 +6,26 @@ import * as dom from 'vs/base/browser/dom'; import { IAction, Separator } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CommandInvalidationReason, ICommandDetectionCapability, IMarkProperties, ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; import { terminalDecorationError, terminalDecorationIncomplete, terminalDecorationMark, terminalDecorationSuccess } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { DecorationSelector, TerminalDecorationHoverManager, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles'; import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import type { IDecoration, ITerminalAddon, Terminal } from 'xterm'; -import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number; markProperties?: IMarkProperties } @@ -371,6 +371,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this._onDidRequestRunCommand.fire({ command }); } }); + // The second section is the clipboard section + actions.push(new Separator()); const labelCopy = localize("terminal.copyCommand", 'Copy Command'); actions.push({ class: undefined, tooltip: labelCopy, id: 'terminal.copyCommand', label: labelCopy, enabled: true, @@ -378,9 +380,16 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { }); } if (command.hasOutput()) { - if (actions.length > 0) { - actions.push(new Separator()); - } + const labelCopyCommandAndOutput = localize("terminal.copyCommandAndOutput", 'Copy Command and Output'); + actions.push({ + class: undefined, tooltip: labelCopyCommandAndOutput, id: 'terminal.copyCommandAndOutput', label: labelCopyCommandAndOutput, enabled: true, + run: () => { + const output = command.getOutput(); + if (typeof output === 'string') { + this._clipboardService.writeText(`${command.command !== '' ? command.command + '\n' : ''}${output}`); + } + } + }); const labelText = localize("terminal.copyOutput", 'Copy Output'); actions.push({ class: undefined, tooltip: labelText, id: 'terminal.copyOutput', label: labelText, enabled: true, diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index efe0d3be971..03979a19aa9 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -5,6 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -379,6 +380,14 @@ export interface ITerminalStatusHoverAction { run: () => void; } +/** + * Context for actions taken on terminal instances. + */ +export interface ISerializedTerminalInstanceContext { + $mid: MarshalledId.TerminalContext; + instanceId: number; +} + export const QUICK_LAUNCH_PROFILE_CHOICE = 'workbench.action.terminal.profile.choice'; export const enum TerminalCommandId { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index cdb90775373..41d07a65710 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -422,7 +422,7 @@ const terminalConfiguration: IConfigurationNode = { default: 'warnonly' }, [TerminalSettingId.EnvironmentChangesRelaunch]: { - markdownDescription: localize('terminal.integrated.environmentChangesRelaunch', "Whether to relaunch terminals automatically if extension want to contribute to their environment and have not been interacted with yet."), + markdownDescription: localize('terminal.integrated.environmentChangesRelaunch', "Whether to relaunch terminals automatically if extensions want to contribute to their environment and have not been interacted with yet."), type: 'boolean', default: true }, diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 29783e7170b..95d437c7ee2 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -8,7 +8,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; @@ -28,6 +28,9 @@ import { TextAreaSyncAddon } from 'vs/workbench/contrib/terminalContrib/accessib import type { Terminal } from 'xterm'; import { Position } from 'vs/editor/common/core/position'; import { ICommandWithEditorLine, TerminalAccessibleBufferProvider } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { Event } from 'vs/base/common/event'; class TextAreaSyncContribution extends DisposableStore implements ITerminalContribution { static readonly ID = 'terminal.textAreaSync'; @@ -57,6 +60,7 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT return instance.getContribution(TerminalAccessibleViewContribution.ID); } private _bufferTracker: BufferContentTracker | undefined; + private _bufferProvider: TerminalAccessibleBufferProvider | undefined; private _xterm: Pick & { raw: Terminal } | undefined; constructor( private readonly _instance: ITerminalInstance, @@ -64,7 +68,9 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT widgetManager: TerminalWidgetManager, @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalService private readonly _terminalService: ITerminalService) { + @ITerminalService private readonly _terminalService: ITerminalService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService) { super(); this._register(AccessibleViewAction.addImplementation(90, 'terminal', () => { if (this._terminalService.activeInstance !== this._instance) { @@ -73,13 +79,38 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT this.show(); return true; }, TerminalContextKeys.focus)); + this._register(_instance.onDidRunText(() => { + const focusAfterRun = configurationService.getValue(TerminalSettingId.FocusAfterRun); + if (focusAfterRun === 'terminal') { + _instance.focus(true); + } else if (focusAfterRun === 'accessible-buffer') { + this.show(); + } + })); } xtermReady(xterm: IXtermTerminal & { raw: Terminal }): void { const addon = this._instantiationService.createInstance(TextAreaSyncAddon, this._instance.capabilities); xterm.raw.loadAddon(addon); addon.activate(xterm.raw); this._xterm = xterm; + this._register(this._xterm.raw.onWriteParsed(async () => { + if (this._isTerminalAccessibleViewOpen() && this._xterm!.raw.buffer.active.baseY === 0) { + this.show(); + } + })); + + const onRequestUpdateEditor = Event.latch(this._xterm.raw.onScroll); + this._register(onRequestUpdateEditor(() => { + if (this._isTerminalAccessibleViewOpen()) { + this.show(); + } + })); } + + private _isTerminalAccessibleViewOpen(): boolean { + return accessibleViewCurrentProviderId.getValue(this._contextKeyService) === AccessibleViewProviderId.Terminal; + } + show(): void { if (!this._xterm) { return; @@ -87,15 +118,10 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT if (!this._bufferTracker) { this._bufferTracker = this._register(this._instantiationService.createInstance(BufferContentTracker, this._xterm)); } - this._accessibleViewService.show(this._instantiationService.createInstance(TerminalAccessibleBufferProvider, this._instance, this._bufferTracker)); - // wait for the render to happen so that the line count is correct and - // the cursor is at the bottom of the buffer - setTimeout(() => { - const lastPosition = this._accessibleViewService.getLastPosition(); - if (lastPosition) { - this._accessibleViewService.setPosition(lastPosition, true); - } - }, 50); + if (!this._bufferProvider) { + this._bufferProvider = this._register(this._instantiationService.createInstance(TerminalAccessibleBufferProvider, this._instance, this._bufferTracker)); + } + this._accessibleViewService.show(this._bufferProvider); } navigateToCommand(type: NavigationType): void { const currentLine = this._accessibleViewService.getPosition()?.lineNumber || this._accessibleViewService.getLastPosition()?.lineNumber; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts index c4ba54d12a6..5e0a5b0fbe5 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts @@ -9,16 +9,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TerminalCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; -import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewSymbol } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { IXtermTerminal, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BufferContentTracker } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker'; import type { Terminal } from 'xterm'; -import { Event } from 'vs/base/common/event'; export class TerminalAccessibleBufferProvider extends DisposableStore implements IAccessibleContentProvider { - options: IAccessibleViewOptions = { type: AccessibleViewType.View, language: 'terminal' }; + options: IAccessibleViewOptions = { type: AccessibleViewType.View, language: 'terminal', positionBottom: true }; verbositySettingKey = AccessibilityVerbositySettingId.Terminal; private _xterm: IXtermTerminal & { raw: Terminal } | undefined; constructor( @@ -28,19 +26,9 @@ export class TerminalAccessibleBufferProvider extends DisposableStore implements @IConfigurationService _configurationService: IConfigurationService, @IContextKeyService _contextKeyService: IContextKeyService, @ITerminalService _terminalService: ITerminalService, - @IConfigurationService configurationService: IConfigurationService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + @IConfigurationService configurationService: IConfigurationService ) { super(); - this.add(_instance.onDidRunText(() => { - const focusAfterRun = configurationService.getValue(TerminalSettingId.FocusAfterRun); - if (focusAfterRun === 'terminal') { - _instance.focus(true); - } else if (focusAfterRun === 'accessible-buffer') { - _accessibleViewService.show(this); - } - })); - this.registerListeners(); } onClose() { @@ -50,14 +38,6 @@ export class TerminalAccessibleBufferProvider extends DisposableStore implements if (!this._xterm) { return; } - this._xterm.raw.onWriteParsed(async () => { - if (this._xterm!.raw.buffer.active.baseY === 0) { - this._bufferTracker.update(); - this._accessibleViewService.show(this); - } - }); - const onRequestUpdateEditor = Event.latch(this._xterm.raw.onScroll); - this.add(onRequestUpdateEditor(() => this._accessibleViewService.show(this))); } provideContent(): string { diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/media/terminalQuickFix.css b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/media/terminalQuickFix.css new file mode 100644 index 00000000000..11a54028fe5 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/media/terminalQuickFix.css @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.xterm-screen .xterm-decoration-container .xterm-decoration.quick-fix { + z-index: 7; +} + +.monaco-workbench .terminal .terminal-command-decoration.quick-fix { + color: var(--vscode-editorLightBulb-foreground) !important; + background-color: var(--vscode-terminal-background, var(--vscode-panel-background)); +} + +.monaco-workbench .terminal .terminal-command-decoration.quick-fix.explainOnly { + /* Use success background to blend in with the terminal better as it's lower priority. We will + * probably want to add an explicit color for this eventually. */ + color: var(--vscode-terminalCommandDecoration-successBackground) !important; +} diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts index 365dd9f00d1..697f6db5009 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts @@ -283,7 +283,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, updateLayout(this._configurationService, e); this._audioCueService.playAudioCue(AudioCue.terminalQuickFix); - const parentElement = e.parentElement?.parentElement?.parentElement?.parentElement; + const parentElement = e.closest('.xterm') as HTMLElement; if (!parentElement) { return; } diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts index 96f2fccd74f..4fca04ec9a9 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/terminalQuickFix'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 2dcb37944d7..090221014d8 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -723,13 +723,13 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { title: localize('themes', "Themes"), submenu: ThemesSubMenu, group: '2_configuration', - order: 6 + order: 7 }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { title: localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Theme"), submenu: ThemesSubMenu, group: '2_configuration', - order: 6 + order: 7 }); MenuRegistry.appendMenuItem(ThemesSubMenu, { diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 044a7a24227..411c6105a73 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -37,7 +37,7 @@ export class ReleaseNotesManager { private _currentReleaseNotes: WebviewInput | undefined = undefined; private _lastText: string | undefined; - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); public constructor( @IEnvironmentService private readonly _environmentService: IEnvironmentService, diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 769878ced7d..bf6cc44b591 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -95,7 +95,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements return getProfilesTitle(); }, submenu: ProfilesMenu, - group: '1_profiles', + group: '2_configuration', order: 1, }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { @@ -103,7 +103,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements return getProfilesTitle(); }, submenu: ProfilesMenu, - group: '1_profiles', + group: '2_configuration', order: 1, when: PROFILES_ENABLEMENT_CONTEXT, }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 1549fc0631e..215e53b2f9b 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -727,20 +727,20 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: 'workbench.userDataSync.actions.turnOn', - title: { value: localize('global activity turn on sync', "Turn on Settings Sync..."), original: 'Turn on Settings Sync...' }, + title: { value: localize('global activity turn on sync', "Backup and Sync Settings..."), original: 'Backup and Sync Settings...' }, category: { value: SYNC_TITLE, original: `Settings Sync` }, f1: true, precondition: when, menu: [{ - group: '1_profiles', + group: '3_settings_sync', id: MenuId.GlobalActivity, when, - order: 2 + order: 1 }, { - group: '1_profiles', + group: '3_settings_sync', id: MenuId.MenubarPreferencesMenu, when, - order: 2 + order: 1 }, { group: '1_settings', id: MenuId.AccountsContext, @@ -764,7 +764,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo title: localize('turnin on sync', "Turning on Settings Sync..."), precondition: ContextKeyExpr.false(), menu: [{ - group: '1_profiles', + group: '3_settings_sync', id: MenuId.GlobalActivity, when, order: 2 @@ -811,7 +811,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userData.actions.signin', title: localize('sign in global', "Sign in to Sync Settings"), menu: { - group: '1_profiles', + group: '3_settings_sync', id: MenuId.GlobalActivity, when, order: 2 @@ -851,12 +851,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo f1: true, precondition: CONTEXT_HAS_CONFLICTS, menu: [{ - group: '1_profiles', + group: '3_settings_sync', id: MenuId.GlobalActivity, when: CONTEXT_HAS_CONFLICTS, order: 2 }, { - group: '1_profiles', + group: '3_settings_sync', id: MenuId.MenubarPreferencesMenu, when: CONTEXT_HAS_CONFLICTS, order: 2 @@ -881,13 +881,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo menu: [ { id: MenuId.GlobalActivity, - group: '1_profiles', + group: '3_settings_sync', when, order: 2 }, { id: MenuId.MenubarPreferencesMenu, - group: '1_profiles', + group: '3_settings_sync', when, order: 2, }, @@ -1150,6 +1150,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const uriIdentityService = accessor.get(IUriIdentityService); const fileService = accessor.get(IFileService); const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); + const notificationService = accessor.get(INotificationService); const result = await fileDialogService.showOpenDialog({ title: localize('download sync activity dialog title', "Select folder to download Settings Sync activity"), @@ -1185,6 +1186,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return userDataSyncWorkbenchService.downloadSyncActivity(uriIdentityService.extUri.joinPath(result[0], indexes[0] !== 0 ? name : `${name} ${indexes[indexes.length - 1] + 1}`)); }); + + notificationService.info(localize('download sync activity complete', "Successfully downloaded Settings Sync activity.")); } })); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index e2fb27d6d8b..bb1993e9d95 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -153,14 +153,12 @@ const apiMenus: IAPIMenu[] = [ { key: 'terminal/context', id: MenuId.TerminalInstanceContext, - description: localize('menus.terminalContext', "The terminal context menu"), - proposed: 'terminalContextMenu' + description: localize('menus.terminalContext', "The terminal context menu") }, { key: 'terminal/title/context', id: MenuId.TerminalTabContext, - description: localize('menus.terminalTabContext', "The terminal tabs context menu"), - proposed: 'terminalContextMenu' + description: localize('menus.terminalTabContext', "The terminal tabs context menu") }, { key: 'view/title', diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index fc6b15a756b..9d3f6a883d2 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -999,26 +999,12 @@ export class SimpleFileDialog implements ISimpleFileDialog { return sorted; } - private extname(file: URI): string { - const ext = resources.extname(file); - if (ext.length === 0) { - const basename = resources.basename(file); - if (basename.startsWith('.')) { - return basename; - } - } else { - return ext; - } - return ''; - } - private filterFile(file: URI): boolean { if (this.options.filters) { - const ext = this.extname(file); for (let i = 0; i < this.options.filters.length; i++) { for (let j = 0; j < this.options.filters[i].extensions.length; j++) { const testExt = this.options.filters[i].extensions[j]; - if ((testExt === '*') || (ext === ('.' + testExt))) { + if ((testExt === '*') || (file.path.endsWith('.' + testExt))) { return true; } } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 0317de83928..25885c7c4f9 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -26,14 +26,15 @@ import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWo import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider'; import { AbstractExtensionService, IExtensionHostFactory, ResolvedExtensions, checkEnabledAndProposedAPI } from 'vs/workbench/services/extensions/common/abstractExtensionService'; +import { ExtensionDescriptionRegistrySnapshot } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionRunningLocationTracker, filterExtensionDescriptions } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; -import { ExtensionHostStartup, IExtensionHost, IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsProposedApi } from 'vs/workbench/services/extensions/common/extensionsProposedApi'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IRemoteExtensionHostDataProvider, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; +import { IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; @@ -70,7 +71,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const extensionHostFactory = new BrowserExtensionHostFactory( extensionsProposedApi, () => this._scanWebExtensions(), - () => this._getExtensions(), + () => this._getExtensionRegistrySnapshotWhenReady(), instantiationService, remoteAgentService, remoteAuthorityResolverService, @@ -216,7 +217,7 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory { constructor( private readonly _extensionsProposedApi: ExtensionsProposedApi, private readonly _scanWebExtensions: () => Promise, - private readonly _getExtensions: () => Promise, + private readonly _getExtensionRegistrySnapshotWhenReady: () => Promise, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @@ -255,18 +256,14 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory { const localExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, await this._scanWebExtensions(), /* ignore workspace trust */true); const runningLocation = runningLocations.computeRunningLocation(localExtensions, [], false); const myExtensions = filterExtensionDescriptions(localExtensions, runningLocation, extRunningLocation => desiredRunningLocation.equals(extRunningLocation)); - return { - allExtensions: localExtensions, - myExtensions: myExtensions.map(extension => extension.identifier) - }; + const extensions = new ExtensionHostExtensions(0, localExtensions, myExtensions.map(extension => extension.identifier)); + return { extensions }; } else { // restart case - const allExtensions = await this._getExtensions(); - const myExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation); - return { - allExtensions: allExtensions, - myExtensions: myExtensions.map(extension => extension.identifier) - }; + const snapshot = await this._getExtensionRegistrySnapshotWhenReady(); + const myExtensions = runningLocations.filterByRunningLocation(snapshot.extensions, desiredRunningLocation); + const extensions = new ExtensionHostExtensions(snapshot.versionId, snapshot.extensions, myExtensions.map(extension => extension.identifier)); + return { extensions }; } } }; @@ -275,28 +272,26 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory { private _createRemoteExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, remoteAuthority: string): IRemoteExtensionHostDataProvider { return { remoteAuthority: remoteAuthority, - getInitData: async () => { - const allExtensions = await this._getExtensions(); + getInitData: async (): Promise => { + const snapshot = await this._getExtensionRegistrySnapshotWhenReady(); const remoteEnv = await this._remoteAgentService.getEnvironment(); if (!remoteEnv) { throw new Error('Cannot provide init data for remote extension host!'); } - const myExtensions = runningLocations.filterByExtensionHostKind(allExtensions, ExtensionHostKind.Remote); + const myExtensions = runningLocations.filterByExtensionHostKind(snapshot.extensions, ExtensionHostKind.Remote); + const extensions = new ExtensionHostExtensions(snapshot.versionId, snapshot.extensions, myExtensions.map(extension => extension.identifier)); - const initData = { + return { connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAuthority), pid: remoteEnv.pid, appRoot: remoteEnv.appRoot, extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - allExtensions: allExtensions, - myExtensions: myExtensions.map(extension => extension.identifier), + extensions, }; - - return initData; } }; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 5ec7f0690c6..a7ce017ab9f 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { parentOriginHash } from 'vs/base/browser/iframe'; import { Barrier } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { canceled, onUnexpectedError } from 'vs/base/common/errors'; @@ -15,7 +16,6 @@ import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; @@ -25,15 +25,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isLoggingOnly } from 'vs/platform/telemetry/common/telemetryUtils'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { parentOriginHash } from 'vs/base/browser/iframe'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ExtensionHostExitCode, IExtensionHostInitData, MessageType, UIKind, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; export interface IWebWorkerExtensionHostInitData { - readonly allExtensions: IExtensionDescription[]; - readonly myExtensions: ExtensionIdentifier[]; + readonly extensions: ExtensionHostExtensions; } export interface IWebWorkerExtensionHostDataProvider { @@ -43,7 +41,7 @@ export interface IWebWorkerExtensionHostDataProvider { export class WebWorkerExtensionHost extends Disposable implements IExtensionHost { public readonly remoteAuthority = null; - public readonly extensions = new ExtensionHostExtensions(); + public extensions: ExtensionHostExtensions | null = null; private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; @@ -267,8 +265,8 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost private async _createExtHostInitData(): Promise { const initData = await this._initDataProvider.getInitData(); + this.extensions = initData.extensions; const workspace = this._contextService.getWorkspace(); - const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl; let nlsUrlWithDetails: URI | undefined = undefined; // Only use the nlsBaseUrl if we are using a language other than the default, English. @@ -304,9 +302,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost includeStack: false, logNative: this._environmentService.debugRenderer }, - allExtensions: deltaExtensions.toAdd, - activationEvents: deltaExtensions.addActivationEvents, - myExtensions: deltaExtensions.myToAdd, + extensions: this.extensions.toSnapshot(), nlsBaseUrl: nlsUrlWithDetails, telemetryInfo: { sessionId: this._telemetryService.sessionId, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index ccb729ce206..e1308458a40 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -31,7 +31,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionDescriptionRegistryLock, IActivationEventsReader, LockableExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { ExtensionDescriptionRegistryLock, ExtensionDescriptionRegistrySnapshot, IActivationEventsReader, LockableExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionHostManager, createExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; @@ -298,22 +298,22 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._doHandleExtensionPoints(([]).concat(toAdd).concat(toRemove)); // Update the extension host - await this._updateExtensionsOnExtHosts(toAdd, toRemove.map(e => e.identifier)); + await this._updateExtensionsOnExtHosts(result.versionId, toAdd, toRemove.map(e => e.identifier)); for (let i = 0; i < toAdd.length; i++) { this._activateAddedExtensionIfNeeded(toAdd[i]); } } - private async _updateExtensionsOnExtHosts(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { + private async _updateExtensionsOnExtHosts(versionId: number, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { const removedRunningLocation = this._runningLocations.deltaExtensions(toAdd, toRemove); const promises = this._extensionHostManagers.map( - extHostManager => this._updateExtensionsOnExtHost(extHostManager, toAdd, toRemove, removedRunningLocation) + extHostManager => this._updateExtensionsOnExtHost(extHostManager, versionId, toAdd, toRemove, removedRunningLocation) ); await Promise.all(promises); } - private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[], removedRunningLocation: ExtensionIdentifierMap): Promise { + private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, versionId: number, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[], removedRunningLocation: ExtensionIdentifierMap): Promise { const myToAdd = this._runningLocations.filterByExtensionHostManager(toAdd, extensionHostManager); const myToRemove = filterExtensionIdentifiers(toRemove, removedRunningLocation, extRunningLocation => extensionHostManager.representsRunningLocation(extRunningLocation)); const addActivationEvents = ImplicitActivationEvents.createActivationEventsMap(toAdd); @@ -322,7 +322,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx const printIds = (extensions: ExtensionIdentifier[]) => extensions.map(e => e.value).join(','); this._logService.info(`AbstractExtensionService: Calling deltaExtensions: toRemove: [${printIds(toRemove)}], toAdd: [${printExtIds(toAdd)}], myToRemove: [${printIds(myToRemove)}], myToAdd: [${printExtIds(myToAdd)}],`); } - await extensionHostManager.deltaExtensions({ toRemove, toAdd, addActivationEvents, myToRemove, myToAdd: myToAdd.map(extension => extension.identifier) }); + await extensionHostManager.deltaExtensions({ versionId, toRemove, toAdd, addActivationEvents, myToRemove, myToAdd: myToAdd.map(extension => extension.identifier) }); } public canAddExtension(extension: IExtensionDescription): boolean { @@ -440,11 +440,11 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._processExtensions(lock, resolvedExtensions); // Start extension hosts which are not automatically started - const allExtensions = this._registry.getAllExtensionDescriptions(); + const snapshot = this._registry.getSnapshot(); for (const extHostManager of this._extensionHostManagers) { if (extHostManager.startup !== ExtensionHostStartup.EagerAutoStart) { - const extensions = this._runningLocations.filterByExtensionHostManager(allExtensions, extHostManager); - extHostManager.start(allExtensions, extensions.map(extension => extension.identifier)); + const extensions = this._runningLocations.filterByExtensionHostManager(snapshot.extensions, extHostManager); + extHostManager.start(snapshot.versionId, snapshot.extensions, extensions.map(extension => extension.identifier)); } } } finally { @@ -934,8 +934,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return this._registry.getAllExtensionDescriptions(); } - protected _getExtensions(): Promise { - return this._installedExtensionsReady.wait().then(() => this.extensions); + protected _getExtensionRegistrySnapshotWhenReady(): Promise { + return this._installedExtensionsReady.wait().then(() => this._registry.getSnapshot()); } public getExtension(id: string): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts index 16365d94990..96b940c9ef0 100644 --- a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts @@ -10,6 +10,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' export class DeltaExtensionsResult { constructor( + public readonly versionId: number, public readonly removedDueToLooping: IExtensionDescription[] ) { } } @@ -45,6 +46,7 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti private readonly _onDidChange = new Emitter(); public readonly onDidChange = this._onDidChange.event; + private _versionId: number = 0; private _extensionDescriptions: IExtensionDescription[]; private _extensionsMap!: ExtensionIdentifierMap; private _extensionsArr!: IExtensionDescription[]; @@ -93,10 +95,14 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti } } - public set(extensionDescriptions: IExtensionDescription[]): void { + public set(extensionDescriptions: IExtensionDescription[]): { versionId: number } { this._extensionDescriptions = extensionDescriptions; this._initialize(); + this._versionId++; this._onDidChange.fire(undefined); + return { + versionId: this._versionId + }; } public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult { @@ -112,8 +118,9 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti this._extensionDescriptions = removeExtensions(this._extensionDescriptions, looping.map(ext => ext.identifier)); this._initialize(); + this._versionId++; this._onDidChange.fire(undefined); - return new DeltaExtensionsResult(looping); + return new DeltaExtensionsResult(this._versionId, looping); } private static _findLoopingExtensions(extensionDescriptions: IExtensionDescription[]): IExtensionDescription[] { @@ -217,6 +224,13 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti return this._extensionsArr.slice(0); } + public getSnapshot(): ExtensionDescriptionRegistrySnapshot { + return new ExtensionDescriptionRegistrySnapshot( + this._versionId, + this.getAllExtensionDescriptions() + ); + } + public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined { const extension = this._extensionsMap.get(extensionId); return extension ? extension : undefined; @@ -239,6 +253,13 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti } } +export class ExtensionDescriptionRegistrySnapshot { + constructor( + public readonly versionId: number, + public readonly extensions: readonly IExtensionDescription[] + ) { } +} + export interface IActivationEventsReader { readActivationEvents(extensionDescription: IExtensionDescription): string[] | undefined; } @@ -282,6 +303,9 @@ export class LockableExtensionDescriptionRegistry implements IReadOnlyExtensionD public getAllExtensionDescriptions(): IExtensionDescription[] { return this._actual.getAllExtensionDescriptions(); } + public getSnapshot(): ExtensionDescriptionRegistrySnapshot { + return this._actual.getSnapshot(); + } public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined { return this._actual.getExtensionDescription(extensionId); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 59c293fa8b1..0980a41d10c 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -53,7 +53,7 @@ export interface IExtensionHostManager { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise; - start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise; + start(extensionRegistryVersionId: number, allExtensions: readonly IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise; extensionTestsExecute(): Promise; setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } @@ -436,12 +436,12 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return proxy.getCanonicalURI(remoteAuthority, uri); } - public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise { + public async start(extensionRegistryVersionId: number, allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise { const proxy = await this._proxy; if (!proxy) { return; } - const deltaExtensions = this._extensionHost.extensions.set(allExtensions, myExtensions); + const deltaExtensions = this._extensionHost.extensions!.set(extensionRegistryVersionId, allExtensions, myExtensions); return proxy.startExtensionHost(deltaExtensions); } @@ -457,17 +457,21 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise { + public async deltaExtensions(incomingExtensionsDelta: IExtensionDescriptionDelta): Promise { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.delta(extensionsDelta); - return proxy.deltaExtensions(extensionsDelta); + const outgoingExtensionsDelta = this._extensionHost.extensions!.delta(incomingExtensionsDelta); + if (!outgoingExtensionsDelta) { + // The extension host already has this version of the extensions. + return; + } + return proxy.deltaExtensions(outgoingExtensionsDelta); } public containsExtension(extensionId: ExtensionIdentifier): boolean { - return this._extensionHost.extensions.containsExtension(extensionId); + return this._extensionHost.extensions?.containsExtension(extensionId) ?? false; } public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise { @@ -529,7 +533,7 @@ class LazyCreateExtensionHostManager extends Disposable implements IExtensionHos return this._actual; } const actual = this._createActual(reason); - await actual.start([], []); + await actual.start(this._lazyStartExtensions!.versionId, this._lazyStartExtensions!.allExtensions, this._lazyStartExtensions!.myExtensions); return actual; } @@ -550,13 +554,12 @@ class LazyCreateExtensionHostManager extends Disposable implements IExtensionHos this._lazyStartExtensions!.delta(extensionsDelta); if (extensionsDelta.myToAdd.length > 0) { const actual = this._createActual(`contains ${extensionsDelta.myToAdd.length} new extension(s) (installed or enabled): ${extensionsDelta.myToAdd.map(extId => extId.value)}`); - const { toAdd, myToAdd } = this._lazyStartExtensions!.toDelta(); - actual.start(toAdd, myToAdd); + await actual.start(this._lazyStartExtensions!.versionId, this._lazyStartExtensions!.allExtensions, this._lazyStartExtensions!.myExtensions); return; } } public containsExtension(extensionId: ExtensionIdentifier): boolean { - return this._extensionHost.extensions.containsExtension(extensionId); + return this._extensionHost.extensions?.containsExtension(extensionId) ?? false; } public async activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { await this._startCalled.wait(); @@ -615,17 +618,16 @@ class LazyCreateExtensionHostManager extends Disposable implements IExtensionHos } throw new Error(`Cannot resolve canonical URI`); } - public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise { + public async start(extensionRegistryVersionId: number, allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise { if (myExtensions.length > 0) { // there are actual extensions, so let's launch the extension host const actual = this._createActual(`contains ${myExtensions.length} extension(s): ${myExtensions.map(extId => extId.value)}.`); - const result = actual.start(allExtensions, myExtensions); + const result = actual.start(extensionRegistryVersionId, allExtensions, myExtensions); this._startCalled.open(); return result; } // there are no actual extensions running, store extensions in `this._lazyStartExtensions` - this._lazyStartExtensions = new ExtensionHostExtensions(); - this._lazyStartExtensions.set(allExtensions, myExtensions); + this._lazyStartExtensions = new ExtensionHostExtensions(extensionRegistryVersionId, allExtensions, myExtensions); this._startCalled.open(); } public async extensionTestsExecute(): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index 4472c9b7bdb..87b031a1c56 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -9,7 +9,15 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ILoggerResource, LogLevel } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; +export interface IExtensionDescriptionSnapshot { + readonly versionId: number; + readonly allExtensions: IExtensionDescription[]; + readonly activationEvents: { [extensionId: string]: string[] }; + readonly myExtensions: ExtensionIdentifier[]; +} + export interface IExtensionDescriptionDelta { + readonly versionId: number; readonly toRemove: ExtensionIdentifier[]; readonly toAdd: IExtensionDescription[]; readonly addActivationEvents: { [extensionId: string]: string[] }; @@ -27,9 +35,7 @@ export interface IExtensionHostInitData { parentPid: number | 0; environment: IEnvironment; workspace?: IStaticWorkspaceData | null; - activationEvents: { [extensionId: string]: string[] }; - allExtensions: IExtensionDescription[]; - myExtensions: ExtensionIdentifier[]; + extensions: IExtensionDescriptionSnapshot; nlsBaseUrl?: URI; telemetryInfo: { readonly sessionId: string; diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 436f780bd00..65d59f165b5 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -13,7 +13,7 @@ import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, Ex import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind'; -import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IExtensionDescriptionSnapshot } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -120,7 +120,7 @@ export interface IExtensionHost { * extension will execute or is executing on this extension host. * **NOTE**: this will reflect extensions correctly only after `start()` resolves. */ - readonly extensions: ExtensionHostExtensions; + readonly extensions: ExtensionHostExtensions | null; readonly onExit: Event<[number, string | null]>; start(): Promise; @@ -130,26 +130,41 @@ export interface IExtensionHost { } export class ExtensionHostExtensions { - + private _versionId: number; private _allExtensions: IExtensionDescription[]; private _myExtensions: ExtensionIdentifier[]; - constructor() { - this._allExtensions = []; - this._myExtensions = []; + public get versionId(): number { + return this._versionId; } - public toDelta(): IExtensionDescriptionDelta { + public get allExtensions(): IExtensionDescription[] { + return this._allExtensions; + } + + public get myExtensions(): ExtensionIdentifier[] { + return this._myExtensions; + } + + constructor(versionId: number, allExtensions: readonly IExtensionDescription[], myExtensions: ExtensionIdentifier[]) { + this._versionId = versionId; + this._allExtensions = allExtensions.slice(0); + this._myExtensions = myExtensions.slice(0); + } + + toSnapshot(): IExtensionDescriptionSnapshot { return { - toRemove: [], - toAdd: this._allExtensions, - addActivationEvents: ImplicitActivationEvents.createActivationEventsMap(this._allExtensions), - myToRemove: [], - myToAdd: this._myExtensions + versionId: this._versionId, + allExtensions: this._allExtensions, + myExtensions: this._myExtensions, + activationEvents: ImplicitActivationEvents.createActivationEventsMap(this._allExtensions) }; } - public set(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): IExtensionDescriptionDelta { + public set(versionId: number, allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): IExtensionDescriptionDelta { + if (this._versionId > versionId) { + throw new Error(`ExtensionHostExtensions: invalid versionId ${versionId} (current: ${this._versionId})`); + } const toRemove: ExtensionIdentifier[] = []; const toAdd: IExtensionDescription[] = []; const myToRemove: ExtensionIdentifier[] = []; @@ -210,12 +225,17 @@ export class ExtensionHostExtensions { } const addActivationEvents = ImplicitActivationEvents.createActivationEventsMap(toAdd); - const delta = { toRemove, toAdd, addActivationEvents, myToRemove, myToAdd }; + const delta = { versionId, toRemove, toAdd, addActivationEvents, myToRemove, myToAdd }; this.delta(delta); return delta; } - public delta(extensionsDelta: IExtensionDescriptionDelta): void { + public delta(extensionsDelta: IExtensionDescriptionDelta): IExtensionDescriptionDelta | null { + if (this._versionId >= extensionsDelta.versionId) { + // ignore older deltas + return null; + } + const { toRemove, toAdd, myToRemove, myToAdd } = extensionsDelta; // First handle removals const toRemoveSet = new ExtensionIdentifierSet(toRemove); @@ -239,6 +259,8 @@ export class ExtensionHostExtensions { for (const extensionId of myToAdd) { this._myExtensions.push(extensionId); } + + return extensionsDelta; } public containsExtension(extensionId: ExtensionIdentifier): boolean { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index ad2039a68bf..58141dd1bc6 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -11,6 +11,7 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', chat: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chat.d.ts', + chatAgents: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatAgents.d.ts', chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', chatRequestAccess: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatRequestAccess.d.ts', chatSlashCommands: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSlashCommands.d.ts', @@ -49,16 +50,13 @@ export const allApiProposals = Object.freeze({ fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', handleIssueUri: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.handleIssueUri.d.ts', idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', - indentSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.indentSize.d.ts', inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', interactive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', interactiveUserActions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveUserActions.d.ts', interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', - languageConfigurationAutoClosingPairs: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts', mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - notebookCodeActions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts', notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', notebookExecution: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookExecution.d.ts', @@ -75,6 +73,7 @@ export const allApiProposals = Object.freeze({ resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', saveEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.saveEditor.d.ts', scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', + scmHistoryProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts', scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', @@ -83,7 +82,6 @@ export const allApiProposals = Object.freeze({ tabInputTextMerge: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', - terminalContextMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalContextMenu.d.ts', terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalExecuteCommandEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 7ac17418689..a4036f94bd2 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -12,7 +12,6 @@ import { URI } from 'vs/base/common/uri'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -36,8 +35,7 @@ export interface IRemoteExtensionHostInitData { readonly extensionHostLogsPath: URI; readonly globalStorageHome: URI; readonly workspaceStorageHome: URI; - readonly allExtensions: IExtensionDescription[]; - readonly myExtensions: ExtensionIdentifier[]; + readonly extensions: ExtensionHostExtensions; } export interface IRemoteExtensionHostDataProvider { @@ -49,7 +47,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { public readonly remoteAuthority: string; public readonly startup = ExtensionHostStartup.EagerAutoStart; - public readonly extensions = new ExtensionHostExtensions(); + public extensions: ExtensionHostExtensions | null = null; private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onExit.event; @@ -203,8 +201,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise { const remoteInitData = await this._initDataProvider.getInitData(); + this.extensions = remoteInitData.extensions; const workspace = this._contextService.getWorkspace(); - const deltaExtensions = this.extensions.set(remoteInitData.allExtensions, remoteInitData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -240,9 +238,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { includeStack: false, logNative: Boolean(this._environmentService.debugExtensionHost.debugId) }, - allExtensions: deltaExtensions.toAdd, - activationEvents: deltaExtensions.addActivationEvents, - myExtensions: deltaExtensions.myToAdd, + extensions: this.extensions.toSnapshot(), telemetryInfo: { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index b72e19b9ec6..c635003cea3 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -21,7 +21,6 @@ import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; import { INativeHostService } from 'vs/platform/native/common/native'; @@ -42,8 +41,7 @@ import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/life import { parseExtensionDevOptions } from '../common/extensionDevOptions'; export interface ILocalProcessExtensionHostInitData { - readonly allExtensions: IExtensionDescription[]; - readonly myExtensions: ExtensionIdentifier[]; + readonly extensions: ExtensionHostExtensions; } export interface ILocalProcessExtensionHostDataProvider { @@ -93,7 +91,7 @@ export class ExtensionHostProcess { export class NativeLocalProcessExtensionHost implements IExtensionHost { public readonly remoteAuthority = null; - public readonly extensions = new ExtensionHostExtensions(); + public extensions: ExtensionHostExtensions | null = null; private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>(); public readonly onExit: Event<[number, string]> = this._onExit.event; @@ -416,8 +414,8 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { private async _createExtHostInitData(): Promise { const initData = await this._initDataProvider.getInitData(); + this.extensions = initData.extensions; const workspace = this._contextService.getWorkspace(); - const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -454,9 +452,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { includeStack: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose), logNative: !this._isExtensionDevTestFromCli && this._isExtensionDevHost }, - allExtensions: deltaExtensions.toAdd, - activationEvents: deltaExtensions.addActivationEvents, - myExtensions: deltaExtensions.myToAdd, + extensions: this.extensions.toSnapshot(), telemetryInfo: { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index d63228db9d2..c5adae5f0d2 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -40,6 +40,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { EnablementState, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { AbstractExtensionService, ExtensionHostCrashTracker, IExtensionHostFactory, ResolvedExtensions, checkEnabledAndProposedAPI, extensionIsEnabled } from 'vs/workbench/services/extensions/common/abstractExtensionService'; +import { ExtensionDescriptionRegistrySnapshot } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; @@ -47,9 +48,9 @@ import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/e import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionRunningLocationTracker, filterExtensionDescriptions } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; -import { ExtensionHostStartup, IExtensionHost, IExtensionService, WebWorkerExtHostConfigValue, toExtension, webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IExtensionService, WebWorkerExtHostConfigValue, toExtension, webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsProposedApi } from 'vs/workbench/services/extensions/common/extensionsProposedApi'; -import { IRemoteExtensionHostDataProvider, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; +import { IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner'; import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, NativeLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -91,7 +92,7 @@ export class NativeExtensionService extends AbstractExtensionService implements const extensionHostFactory = new NativeExtensionHostFactory( extensionsProposedApi, extensionScanner, - () => this._getExtensions(), + () => this._getExtensionRegistrySnapshotWhenReady(), instantiationService, environmentService, extensionEnablementService, @@ -521,7 +522,7 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { constructor( private readonly _extensionsProposedApi: ExtensionsProposedApi, private readonly _extensionScanner: CachedExtensionScanner, - private readonly _getExtensions: () => Promise, + private readonly _getExtensionRegistrySnapshotWhenReady: () => Promise, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -581,22 +582,17 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { const runningLocation = runningLocations.computeRunningLocation(localExtensions, [], false); const myExtensions = filterExtensionDescriptions(localExtensions, runningLocation, extRunningLocation => desiredRunningLocation.equals(extRunningLocation)); + const extensions = new ExtensionHostExtensions(0, localExtensions, myExtensions.map(extension => extension.identifier)); if (isCI) { this._logService.info(`NativeExtensionHostFactory._createLocalProcessExtensionHostDataProvider.myExtensions: ${myExtensions.map(ext => ext.identifier.value).join(',')}`); } - - return { - allExtensions: localExtensions, - myExtensions: myExtensions.map(extension => extension.identifier) - }; + return { extensions }; } else { // restart case - const allExtensions = await this._getExtensions(); - const myExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation); - return { - allExtensions: allExtensions, - myExtensions: myExtensions.map(extension => extension.identifier) - }; + const snapshot = await this._getExtensionRegistrySnapshotWhenReady(); + const myExtensions = runningLocations.filterByRunningLocation(snapshot.extensions, desiredRunningLocation); + const extensions = new ExtensionHostExtensions(snapshot.versionId, snapshot.extensions, myExtensions.map(extension => extension.identifier)); + return { extensions }; } } }; @@ -605,12 +601,10 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { private _createWebWorkerExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, desiredRunningLocation: LocalWebWorkerRunningLocation): IWebWorkerExtensionHostDataProvider { return { getInitData: async (): Promise => { - const allExtensions = await this._getExtensions(); - const myExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation); - return { - allExtensions: allExtensions, - myExtensions: myExtensions.map(extension => extension.identifier) - }; + const snapshot = await this._getExtensionRegistrySnapshotWhenReady(); + const myExtensions = runningLocations.filterByRunningLocation(snapshot.extensions, desiredRunningLocation); + const extensions = new ExtensionHostExtensions(snapshot.versionId, snapshot.extensions, myExtensions.map(extension => extension.identifier)); + return { extensions }; } }; } @@ -618,28 +612,26 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { private _createRemoteExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, remoteAuthority: string): IRemoteExtensionHostDataProvider { return { remoteAuthority: remoteAuthority, - getInitData: async () => { - const allExtensions = await this._getExtensions(); + getInitData: async (): Promise => { + const snapshot = await this._getExtensionRegistrySnapshotWhenReady(); const remoteEnv = await this._remoteAgentService.getEnvironment(); if (!remoteEnv) { throw new Error('Cannot provide init data for remote extension host!'); } - const myExtensions = runningLocations.filterByExtensionHostKind(allExtensions, ExtensionHostKind.Remote); + const myExtensions = runningLocations.filterByExtensionHostKind(snapshot.extensions, ExtensionHostKind.Remote); + const extensions = new ExtensionHostExtensions(snapshot.versionId, snapshot.extensions, myExtensions.map(extension => extension.identifier)); - const initData = { + return { connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAuthority), pid: remoteEnv.pid, appRoot: remoteEnv.appRoot, extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - allExtensions: allExtensions, - myExtensions: myExtensions.map(extension => extension.identifier), + extensions, }; - - return initData; } }; } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index e6112182d25..10e58f90efe 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -18,7 +18,7 @@ import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { Keybinding, KeyCodeChord, ResolvedKeybinding, ScanCodeChord } from 'vs/base/common/keybindings'; import { IMMUTABLE_CODE_TO_KEY_CODE, KeyCode, KeyCodeUtils, KeyMod, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import { isMacintosh, OperatingSystem, OS } from 'vs/base/common/platform'; import { dirname } from 'vs/base/common/resources'; @@ -237,28 +237,9 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { this.updateKeybindingsJsonSchema(); this._register(extensionService.onDidRegisterExtensions(() => this.updateKeybindingsJsonSchema())); - // for standard keybindings - this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - this.isComposingGlobalContextKey.set(e.isComposing); - const keyEvent = new StandardKeyboardEvent(e); - this._log(`/ Received keydown event - ${printKeyboardEvent(e)}`); - this._log(`| Converted keydown event - ${printStandardKeyboardEvent(keyEvent)}`); - const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); - if (shouldPreventDefault) { - keyEvent.preventDefault(); - } - this.isComposingGlobalContextKey.set(false); - })); - - // for single modifier chord keybindings (e.g. shift shift) - this._register(dom.addDisposableListener(window, dom.EventType.KEY_UP, (e: KeyboardEvent) => { - this.isComposingGlobalContextKey.set(e.isComposing); - const keyEvent = new StandardKeyboardEvent(e); - const shouldPreventDefault = this._singleModifierDispatch(keyEvent, keyEvent.target); - if (shouldPreventDefault) { - keyEvent.preventDefault(); - } - this.isComposingGlobalContextKey.set(false); + this._register(this._registerKeyListeners(window)); + this._register(dom.onDidCreateWindow(({ window, disposableStore }) => { + disposableStore.add(this._registerKeyListeners(window)); })); this._register(browser.onDidChangeFullscreen(() => { @@ -280,6 +261,36 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { })); } + private _registerKeyListeners(window: Window): IDisposable { + const disposables = new DisposableStore(); + + // for standard keybindings + disposables.add(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + this.isComposingGlobalContextKey.set(e.isComposing); + const keyEvent = new StandardKeyboardEvent(e); + this._log(`/ Received keydown event - ${printKeyboardEvent(e)}`); + this._log(`| Converted keydown event - ${printStandardKeyboardEvent(keyEvent)}`); + const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); + if (shouldPreventDefault) { + keyEvent.preventDefault(); + } + this.isComposingGlobalContextKey.set(false); + })); + + // for single modifier chord keybindings (e.g. shift shift) + disposables.add(dom.addDisposableListener(window, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + this.isComposingGlobalContextKey.set(e.isComposing); + const keyEvent = new StandardKeyboardEvent(e); + const shouldPreventDefault = this._singleModifierDispatch(keyEvent, keyEvent.target); + if (shouldPreventDefault) { + keyEvent.preventDefault(); + } + this.isComposingGlobalContextKey.set(false); + })); + + return disposables; + } + public registerSchemaContribution(contribution: KeybindingsSchemaContribution): void { this._contributions.push(contribution); if (contribution.onDidChange) { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index fa940fa4782..070eab8f0b2 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -56,18 +56,6 @@ abstract class AbstractSettingsModel extends EditorModel { }); } - private compareTwoNullableNumbers(a: number | undefined, b: number | undefined): number { - const aOrMax = a ?? Number.MAX_SAFE_INTEGER; - const bOrMax = b ?? Number.MAX_SAFE_INTEGER; - if (aOrMax < bOrMax) { - return -1; - } else if (aOrMax > bOrMax) { - return 1; - } else { - return 0; - } - } - filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] { const allGroups = this.filterGroups; @@ -90,36 +78,6 @@ abstract class AbstractSettingsModel extends EditorModel { } } - filterMatches.sort((a, b) => { - if (a.matchType !== b.matchType) { - // Sort by match type if the match types are not the same. - // The priority of the match type is given by the SettingMatchType enum. - return b.matchType - a.matchType; - } else if (a.matchType === SettingMatchType.RemoteMatch) { - // The match types are the same and are RemoteMatch. - // Sort by score. - return b.score - a.score; - } else { - // The match types are the same. - if (a.setting.extensionInfo && b.setting.extensionInfo - && a.setting.extensionInfo.id === b.setting.extensionInfo.id) { - // These settings belong to the same extension. - if (a.setting.categoryLabel !== b.setting.categoryLabel - && (a.setting.categoryOrder !== undefined || b.setting.categoryOrder !== undefined) - && a.setting.categoryOrder !== b.setting.categoryOrder) { - // These two settings don't belong to the same category and have different category orders. - return this.compareTwoNullableNumbers(a.setting.categoryOrder, b.setting.categoryOrder); - } else if (a.setting.categoryLabel === b.setting.categoryLabel - && (a.setting.order !== undefined || b.setting.order !== undefined) - && a.setting.order !== b.setting.order) { - // These two settings belong to the same category, but have different orders. - return this.compareTwoNullableNumbers(a.setting.order, b.setting.order); - } - } - // In the worst case, go back to lexicographical order. - return b.score - a.score; - } - }); return filterMatches; } diff --git a/src/vs/workbench/services/voiceRecognition/electron-sandbox/voiceTranscriptionWorklet.ts b/src/vs/workbench/services/voiceRecognition/electron-sandbox/voiceTranscriptionWorklet.ts index fc3acd9eb6a..cb0083c81eb 100644 --- a/src/vs/workbench/services/voiceRecognition/electron-sandbox/voiceTranscriptionWorklet.ts +++ b/src/vs/workbench/services/voiceRecognition/electron-sandbox/voiceTranscriptionWorklet.ts @@ -13,6 +13,7 @@ declare class AudioWorkletProcessor { interface IVoiceTranscriptionWorkletOptions extends AudioWorkletNodeOptions { processorOptions: { readonly bufferTimespan: number; + readonly vadThreshold: number; }; } @@ -76,16 +77,50 @@ class VoiceTranscriptionWorklet extends AudioWorkletProcessor { this.buffer.push(inputChannelData.slice(0)); if (Date.now() - this.startTime > this.options.processorOptions.bufferTimespan && this.sharedProcessConnection) { - const buffer = this.buffer; + const buffer = this.joinFloat32Arrays(this.buffer); this.buffer = []; - this.sharedProcessConnection.postMessage(buffer); + // Send buffer to shared process for transcription. + // Send an empty buffer if it appears to be silence + // so that we can still trigger the transcription + // service and let it know about this. + + this.sharedProcessConnection.postMessage(this.appearsToBeSilence(buffer) ? new Float32Array(0) : buffer); this.startTime = Date.now(); } return !this.stopped; } + + private appearsToBeSilence(data: Float32Array): boolean { + + // This is the most simple Voice Activity Detection (VAD) + // and it is based on the Root Mean Square (RMS) of the signal + // with a certain threshold. Good for testing but probably + // not suitable for shipping to stable (TODO@bpasero). + + let sum = 0; + for (let i = 0; i < data.length; i++) { + sum += data[i] * data[i]; + } + + const rms = Math.sqrt(sum / data.length); + + return rms < this.options.processorOptions.vadThreshold; + } + + private joinFloat32Arrays(float32Arrays: Float32Array[]): Float32Array { + const result = new Float32Array(float32Arrays.reduce((prev, curr) => prev + curr.length, 0)); + + let offset = 0; + for (const float32Array of float32Arrays) { + result.set(float32Array, offset); + offset += float32Array.length; + } + + return result; + } } // @ts-ignore diff --git a/src/vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService.ts b/src/vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService.ts index 11aa0dae3e1..0f279ac26ea 100644 --- a/src/vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService.ts +++ b/src/vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService.ts @@ -41,6 +41,7 @@ export interface IWorkbenchVoiceRecognitionService { interface IVoiceTranscriptionWorkletOptions extends AudioWorkletNodeOptions { processorOptions: { readonly bufferTimespan: number; + readonly vadThreshold: number; }; } @@ -93,6 +94,7 @@ export class WorkbenchVoiceRecognitionService implements IWorkbenchVoiceRecognit private static readonly AUDIO_CHANNELS = 1; private static readonly BUFFER_TIMESPAN = 1000; + private static readonly VAD_THRESHOLD = 0.02; constructor( @IProgressService private readonly progressService: IProgressService, @@ -172,7 +174,8 @@ export class WorkbenchVoiceRecognitionService implements IWorkbenchVoiceRecognit channelCount: WorkbenchVoiceRecognitionService.AUDIO_CHANNELS, channelCountMode: 'explicit', processorOptions: { - bufferTimespan: WorkbenchVoiceRecognitionService.BUFFER_TIMESPAN + bufferTimespan: WorkbenchVoiceRecognitionService.BUFFER_TIMESPAN, + vadThreshold: WorkbenchVoiceRecognitionService.VAD_THRESHOLD } }, onDidTranscribe, this.sharedProcessService); await voiceTranscriptionTarget.start(cts.token); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ab2bc3c34ea..1a62f5f9cd6 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1396,7 +1396,7 @@ export class TestInMemoryFileSystemProvider extends InMemoryFileSystemProvider i | FileSystemProviderCapabilities.FileReadStream; } - readFileStream(resource: URI): ReadableStreamEvents { + override readFileStream(resource: URI): ReadableStreamEvents { const BUFFER_SIZE = 64 * 1024; const stream = newWriteableStream(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 1f6253a29b6..b4ad16270af 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -47,6 +47,7 @@ import 'vs/workbench/browser/parts/paneCompositePart'; import 'vs/workbench/browser/parts/banner/bannerPart'; import 'vs/workbench/browser/parts/statusbar/statusbarPart'; import 'vs/workbench/browser/parts/views/viewsService'; +import 'vs/workbench/browser/iconSelectBox'; //#endregion diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index b5c09fe1617..53e139a1671 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -668,13 +668,22 @@ declare module 'vscode' { /** * The size in spaces a tab takes. This is used for two purposes: * - the rendering width of a tab character; - * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true. + * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true + * and `indentSize` is set to `"tabSize"`. * * When getting a text editor's options, this property will always be a number (resolved). * When setting a text editor's options, this property is optional and it can be a number or `"auto"`. */ tabSize?: number | string; + /** + * The number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true. + * + * When getting a text editor's options, this property will always be a number (resolved). + * When setting a text editor's options, this property is optional and it can be a number or `"tabSize"`. + */ + indentSize?: number | string; + /** * When pressing Tab insert {@link TextEditorOptions.tabSize n} spaces. * When getting a text editor's options, this property will always be a boolean (resolved). @@ -2465,6 +2474,24 @@ declare module 'vscode' { */ static readonly SourceFixAll: CodeActionKind; + /** + * Base kind for all code actions applying to the enitre notebook's scope. CodeActionKinds using + * this should always begin with `notebook.` + * + * This requires that new CodeActions be created for it and contributed via extensions. + * Pre-existing kinds can not just have the new `notebook.` prefix added to them, as the functionality + * is unique to the full-notebook scope. + * + * Notebook CodeActionKinds can be initialized as either of the following (both resulting in `notebook.source.xyz`): + * - `const newKind = CodeActionKind.Notebook.append(CodeActionKind.Source.append('xyz').value)` + * - `const newKind = CodeActionKind.Notebook.append('source.xyz')` + * + * Example Kinds/Actions: + * - `notebook.source.organizeImports` (might move all imports to a new top cell) + * - `notebook.source.normalizeVariableNames` (might rename all variables to a standardized casing format) + */ + static readonly Notebook: CodeActionKind; + /** * Private constructor, use statix `CodeActionKind.XYZ` to derive from an existing code action kind. * @@ -6189,6 +6216,46 @@ declare module 'vscode' { action: EnterAction; } + /** + * Enumeration of commonly encountered syntax token types. + */ + export enum SyntaxTokenType { + /** + * Everything except tokens that are part of comments, string literals and regular expressions. + */ + Other = 0, + /** + * A comment. + */ + Comment = 1, + /** + * A string literal. + */ + String = 2, + /** + * A regular expression. + */ + RegEx = 3 + } + + /** + * Describes pairs of strings where the close string will be automatically inserted when typing the opening string. + */ + export interface AutoClosingPair { + /** + * The string that will trigger the automatic insertion of the closing string. + */ + open: string; + /** + * The closing string that will be automatically inserted when typing the opening string. + */ + close: string; + /** + * A set of tokens where the pair should not be auto closed. + */ + notIn?: SyntaxTokenType[]; + } + /** * The language configuration interfaces defines the contract between extensions * and various editor features, like automatic bracket insertion, automatic indentation etc. @@ -6219,6 +6286,10 @@ declare module 'vscode' { * The language's rules to be evaluated when pressing Enter. */ onEnterRules?: OnEnterRule[]; + /** + * The language's auto closing pairs. + */ + autoClosingPairs?: AutoClosingPair[]; /** * **Deprecated** Do not use. diff --git a/src/vscode-dts/vscode.proposed.chatAgents.d.ts b/src/vscode-dts/vscode.proposed.chatAgents.d.ts new file mode 100644 index 00000000000..6f52c6498df --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatAgents.d.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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 ChatAgentContext { + history: ChatMessage[]; + } + + export interface ChatAgentResponse { + message: MarkdownString | InteractiveProgressFileTree; + } + + export interface ChatAgentResult { + followUp?: InteractiveSessionFollowup[]; + } + + export interface ChatAgentCommand { + name: string; + description: string; + } + + export interface ChatAgentMetadata { + description: string; + fullName?: string; + icon?: Uri; + subCommands: ChatAgentCommand[]; + } + + export interface ChatAgent { + (prompt: ChatMessage, context: ChatAgentContext, progress: Progress, token: CancellationToken): Thenable; + } + + export namespace chat { + export function registerAgent(id: string, agent: ChatAgent, metadata: ChatAgentMetadata): Disposable; + } +} diff --git a/src/vscode-dts/vscode.proposed.indentSize.d.ts b/src/vscode-dts/vscode.proposed.indentSize.d.ts deleted file mode 100644 index 2a490dd2742..00000000000 --- a/src/vscode-dts/vscode.proposed.indentSize.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // https://github.com/microsoft/vscode/issues/193077 @alexdima - - /** - * Represents a {@link TextEditor text editor}'s {@link TextEditor.options options}. - */ - export interface TextEditorOptions { - /** - * The size in spaces a tab takes. This is used for two purposes: - * - the rendering width of a tab character; - * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true - * and `indentSize` is set to `"tabSize"`. - * - * When getting a text editor's options, this property will always be a number (resolved). - * When setting a text editor's options, this property is optional and it can be a number or `"auto"`. - */ - tabSize?: number | string; - /** - * The number of spaces to insert when [insertSpaces](#TextEditorOptions.insertSpaces) is true. - * - * When getting a text editor's options, this property will always be a number (resolved). - * When setting a text editor's options, this property is optional and it can be a number or `"tabSize"`. - */ - indentSize?: number | 'tabSize'; - } -} diff --git a/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts b/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts deleted file mode 100644 index 3cea4fdf090..00000000000 --- a/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // https://github.com/microsoft/vscode/issues/173738 @alexdima - - export interface LanguageConfiguration { - autoClosingPairs?: { - open: string; - close: string; - notIn?: string[]; - }[]; - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts b/src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts deleted file mode 100644 index 0768e7c2d48..00000000000 --- a/src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // https://github.com/microsoft/vscode/issues/179213 - - export class NotebookCodeActionKind { - // can only return MULTI CELL workspaceEdits - // ex: notebook.organizeImprots - static readonly Notebook: CodeActionKind; - - constructor(value: string); - } -} diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts new file mode 100644 index 00000000000..5d81cf2a1f4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + // https://github.com/microsoft/vscode/issues/185269 + + export interface SourceControl { + historyProvider?: SourceControlHistoryProvider; + } + + export interface SourceControlHistoryProvider { + actionButton?: SourceControlActionButton; + currentHistoryItemGroup?: SourceControlHistoryItemGroup; + + /** + * Fires when the action button changes + */ + onDidChangeActionButton: Event; + + /** + * Fires when the current history item group changes (ex: checkout) + */ + onDidChangeCurrentHistoryItemGroup: Event; + + /** + * Fires when the history item groups change (ex: commit, push, fetch) + */ + // onDidChangeHistoryItemGroups: Event; + + provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; + provideHistoryItemChanges(historyItemId: string, token: CancellationToken): ProviderResult; + + resolveHistoryItemGroupBase(historyItemGroupId: string, token: CancellationToken): ProviderResult; + resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId: string, token: CancellationToken): ProviderResult<{ id: string; ahead: number; behind: number }>; + } + + export interface SourceControlHistoryOptions { + readonly cursor?: string; + readonly limit?: number | { id?: string }; + } + + export interface SourceControlHistoryItemGroup { + readonly id: string; + readonly label: string; + readonly upstream?: SourceControlRemoteHistoryItemGroup; + } + + export interface SourceControlRemoteHistoryItemGroup { + readonly id: string; + readonly label: string; + } + + export interface SourceControlHistoryItem { + readonly id: string; + readonly parentIds: string[]; + readonly label: string; + readonly description?: string; + readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + readonly timestamp?: number; + } + + export interface SourceControlHistoryItemChange { + readonly uri: Uri; + readonly originalUri: Uri | undefined; + readonly modifiedUri: Uri | undefined; + readonly renameUri: Uri | undefined; + } + + // export interface SourceControlHistoryChangeEvent { + // readonly added: Iterable; + // readonly removed: Iterable; + // readonly modified: Iterable; + // } + +} diff --git a/src/vscode-dts/vscode.proposed.terminalContextMenu.d.ts b/src/vscode-dts/vscode.proposed.terminalContextMenu.d.ts deleted file mode 100644 index 768600e79a9..00000000000 --- a/src/vscode-dts/vscode.proposed.terminalContextMenu.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // https://github.com/microsoft/vscode/issues/79034 - - // This proposal doesn't have any API changes, only contributions - -} diff --git a/yarn.lock b/yarn.lock index cf5b0450563..924eaed8282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1304,10 +1304,10 @@ bindings "^1.5.0" node-addon-api "^6.0.0" -"@vscode/proxy-agent@^0.17.2": - version "0.17.2" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.17.2.tgz#0e0dac24478e2d71a4fd1b2bb5f84dc61add79e2" - integrity sha512-aKRo1YfUCsgEjHvr2HXfM6dwHhieyO6G+WHly7jewyyTJ1nANWEocS3JRnRbC4KjlajKhSUEOx838cdnY/vRtA== +"@vscode/proxy-agent@^0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.17.4.tgz#e3ffb63357353a428436f15a69de3453a5061f0c" + integrity sha512-tX8eidofoJlZFRWzdiiW3wyu26hgIRk8HvM/RoP1wVSu3U/As36EgGIZYG6pPnqiythRqTcsddniVNA5M39g4w== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -10008,10 +10008,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.3.0-dev.20230911: - version "5.3.0-dev.20230911" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.0-dev.20230911.tgz#7f60e82ee86e381655ddc63141408eb90b6ca31d" - integrity sha512-2iI2l7OuGvU668gBje+JQKE8bsf7SH8w8ScwUkENHCcrbaDpXa/Oqfuwq5gdFM7SfVfp5p6c8kHZRMvL+kabJg== +typescript@^5.3.0-dev.20230919: + version "5.3.0-dev.20230919" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.0-dev.20230919.tgz#ee93ccb19d16a89b562bcd851d31e20e545b2ed0" + integrity sha512-FU6DZhzId38aY/dX2gHp7phaYkbNJkCx8G//VVs0nVzZv0qjWGggLkMXoMipphO8Hv0TvZu30Zwdt6nzFIbcBQ== typical@^4.0.0: version "4.0.0"