diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6b833803b2e34..6f20c66c400e5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,137 +55,3 @@ for (let i = 0, n = str.length; i < 10; i++) { function f(x: number, y: string): void { } ``` - -# Extension API Guidelines - -## Overview - -The following guidelines only apply to files in the `src/vscode-dts` folder. - -This is a loose collection of guidelines that you should be following when proposing API. The process for adding API is described here: [Extension API Process](https://github.com/Microsoft/vscode/wiki/Extension-API-process). - -## Core Principles - -### Avoid Breakage - -We DO NOT want to break API. Therefore be careful and conservative when proposing new API. It needs to hold up in the long term. Expose only the minimum but still try to anticipate potential future requests. - -### Namespaces - -The API is structured into different namespaces, like `commands`, `window`, `workspace` etc. Namespaces contain functions, constants, and events. All types (classes, enum, interfaces) are defined in the global, `vscode`, namespace. - -### JavaScript Feel - -The API should have a JavaScript’ish feel. While that is harder to put in rules, it means we use namespaces, properties, functions, and globals instead of object-factories and services. Also take inspiration from popular existing JS API, for instance `window.createStatusBarItem` is like `document.createElement`, the members of `DiagnosticsCollection` are similar to ES6 maps etc. - -### Global Events - -Events aren’t defined on the types they occur on but in the best matching namespace. For instance, document changes aren't sent by a document but via the `workspace.onDidChangeTextDocument` event. The event will contain the document in question. This **global event** pattern makes it easier to manage event subscriptions because changes happen less frequently. - -### Private Events - -Private or instance events aren't accessible via globals but exist on objects, e.g., `FileSystemWatcher#onDidCreate`. *Don't* use private events unless the sender of the event is private. The rule of thumb is: 'Objects that can be accessed globally (editors, tasks, terminals, documents, etc)' should not have private events, objects that are private (only known by its creators, like tree views, web views) can send private events' - -### Event Naming - -Events follow the `on[Did|Will]VerbSubject` patterns, like `onDidChangeActiveEditor` or `onWillSaveTextDocument`. It doesn’t hurt to use explicit names. - -### Creating Objects - -Objects that live in the main thread but can be controlled/instantiated by extensions are declared as interfaces, e.g. `TextDocument` or `StatusBarItem`. When you allow creating such objects your API must follow the `createXYZ(args): XYZ` pattern. Because this is a constructor-replacement, the call must return synchronously. - -### Shy Objects - -Objects the API hands out to extensions should not contain more than what the API defines. Don’t expect everyone to read `vscode.d.ts` but also expect folks to use debugging-aided-intellisense, meaning whatever the debugger shows developers will program against. We don’t want to appear as making false promises. Prefix your private members with `_` as that is a common rule or, even better, use function-scopes to hide information. - -### Sync vs. Async - -Reading data, like an editor selection, a configuration value, etc. is synchronous. Setting a state that reflects on the main side is asynchronous. Despite updates being async your ‘extension host object’ should reflect the new state synchronously. This happens when setting an editor selection - -``` - editor.selection = newSelection - - | - | - V - - 1. On the API object set the value as given - 2. Make an async-call to the main side ala `trySetSelection` - 3. The async-call returns with the actual selection (it might have changed in the meantime) - 4. On the API object set the value again -``` - -We usually don’t expose the fact that setting state is asynchronous. We try to have API that feels sync -`editor.selection` is a getter/setter and not a method. - -### Data Driven - -Whenever possible, you should define a data model and define provider-interfaces. This puts VS Code into control as we can decide when to ask those providers, how to deal with multiple providers etc. The `ReferenceProvider` interface is a good sample for this. - -### Enrich Data Incrementally - -Sometimes it is expensive for a provider to compute parts of its data. For instance, creating a full `CompletionItem` (with all the documentation and symbols resolved) conflicts with being able to compute a large list of them quickly. In those cases, providers should return a lightweight version and offer a `resolve` method that allows extensions to enrich data. The `CodeLensProvider` and `CompletionItemProvider` interfaces are good samples for this. - -### Cancellation - -Calls into a provider should always include a `CancellationToken` as the last parameter. With that, the main thread can signal to the provider that its result won’t be needed anymore. When adding new parameters to provider-functions, it is OK to have the token not at the end anymore. - -### Objects vs. Interfaces - -Objects that should be returned by a provider are usually represented by a class that extensions can instantiate, e.g. `CodeLens`. We do that to provide convenience constructors and to be able to populate default values. - -Data that we accept in methods calls, i.e., parameter types, like in `registerRenameProvider` or `showQuickPick`, are declared as interfaces. That makes it easy to fulfill the API contract using class-instances or plain object literals. - -### Strict and Relaxed Data - -Data the API returns is strict, e.g. `activeTextEditor` is an editor or `undefined`, but not `null`. On the other side, providers can return relaxed data. We usually accept 4 types: The actual type, like `Hover`, a `Thenable` of that type, `undefined` or `null`. With that we want to make it easy to implement a provider, e.g., if you can compute results synchronous you don’t need to wrap things into a promise or if a certain condition isn’t met simple return, etc. - -### Validate Data - -Although providers can return ‘relaxed’ data, you need to verify it. The same is true for arguments etc. Throw validation errors when possible, drop data object when invalid. - -### Copy Data - -Don’t send the data that a provider returned over the wire. Often it contains more information than we need and often there are cyclic dependencies. Use the provider data to create objects that your protocol speaks. - -### Enums - -When API-work started only numeric-enums were supported, today TypeScript supports string-or-types and string-enums. Because fewer concepts are better, we stick to numeric-enums. - -### Strict Null - -We define the API with strictNull-checks in mind. That means we use the optional annotation `foo?: number` and `null` or `undefined` in type annotations. For instance, its `activeTextEditor: TextEditor | undefined`. Again, be strict for types we define and relaxed when accepting data. - -### Undefined is False - -The default value of an optional, boolean property is `false`. This is for consistency with JS where undefined never evaluates to `true`. - -### JSDoc - -We add JSDoc for all parts of the API. The doc is supported by markdown syntax. When document string-datatypes that end up in the UI, use the phrase ‘Human-readable string…’ - -## Optional Parameters (`?` vs `| undefined`) - -* For implementation, treat omitting a parameter with `?` the same as explicitly passing in `undefined` -* Use `| undefined` when you want to callers to always have to consider the parameter. -* Use `?` when you want to allow callers to omit the parameter. -* Never use `?` and `| undefined` on a parameter. Instead follow the two rules above to decide which version to use. -* If adding a new parameter to an existing function, use `?` as this allows the new signature to be backwards compatible with the old version. -* Do not add an overload to add an optional parameter to the end of the function. Instead use `?`. - -## Optional Properties (`?` vs `| undefined`) - -* Do not write code that treats the absence of a property differently than a property being present but set to `undefined` - * This can sometimes hit you on spreads or iterating through objects, so just something to be aware of - -* For readonly properties on interfaces that VS Code exposes to extensions (this include managed objects, as well as the objects passed to events): - * Use `| undefined` as this makes it clear the property exists but has the value `undefined`. - -* For readonly properties on options bag type objects passed from extensions to VS Code: - * Use `?` when it is ok to omit the property - * Use `| undefined` when you want the user to have to pass in the property but `undefined` signals that you will fall back to some default - * Try to avoid `?` + `| undefined` in most cases. Instead use `?`. Using both `?` + `| undefined` isn't wrong, but it's often more clear to treat omitting the property as falling back to the default rather than passing in `undefined` - -* For unmanaged, writable objects: - * If using `?`, always also add `| undefined` unless want to allow the property to be omitted during initialization, but never allow users to explicitly set it to `undefined` afterwards. I don't think we have many cases where this will be needed - * In these cases, you may want to try changing the api to avoid this potential confusion - * If adding a new property to an unmanaged object, use `?` as this ensures the type is backwards compatible with the old version diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index 102c5518d9b78..742db9d9e9781 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -18,6 +18,7 @@ const node_worker_threads_1 = require("node:worker_threads"); const msal_node_1 = require("@azure/msal-node"); const storage_blob_1 = require("@azure/storage-blob"); const jws = require("jws"); +const node_timers_1 = require("node:timers"); function e(name) { const result = process.env[name]; if (typeof result !== 'string') { @@ -37,6 +38,7 @@ function hashStream(hashName, stream) { var StatusCode; (function (StatusCode) { StatusCode["Pass"] = "pass"; + StatusCode["Aborted"] = "aborted"; StatusCode["Inprogress"] = "inprogress"; StatusCode["FailCanRetry"] = "failCanRetry"; StatusCode["FailDoNotRetry"] = "failDoNotRetry"; @@ -136,8 +138,13 @@ class ESRPReleaseService { if (releaseStatus.status === 'pass') { break; } + else if (releaseStatus.status === 'aborted') { + this.log(JSON.stringify(releaseStatus)); + throw new Error(`Release was aborted`); + } else if (releaseStatus.status !== 'inprogress') { - throw new Error(`Failed to submit release: ${JSON.stringify(releaseStatus)}`); + this.log(JSON.stringify(releaseStatus)); + throw new Error(`Unknown error when polling for release`); } } const releaseDetails = await this.getReleaseDetails(submitReleaseResult.operationId); @@ -470,50 +477,95 @@ function getRealType(type) { return type; } } +async function withLease(client, fn) { + const lease = client.getBlobLeaseClient(); + for (let i = 0; i < 360; i++) { // Try to get lease for 30 minutes + try { + await client.uploadData(new ArrayBuffer()); // blob needs to exist for lease to be acquired + await lease.acquireLease(60); + try { + const abortController = new AbortController(); + const refresher = new Promise((c, e) => { + abortController.signal.onabort = () => { + (0, node_timers_1.clearInterval)(interval); + c(); + }; + const interval = (0, node_timers_1.setInterval)(() => { + lease.renewLease().catch(err => { + (0, node_timers_1.clearInterval)(interval); + e(new Error('Failed to renew lease ' + err)); + }); + }, 30_000); + }); + const result = await Promise.race([fn(), refresher]); + abortController.abort(); + return result; + } + finally { + await lease.releaseLease(); + } + } + catch (err) { + if (err.statusCode !== 409 && err.statusCode !== 412) { + throw err; + } + await new Promise(c => setTimeout(c, 5000)); + } + } + throw new Error('Failed to acquire lease on blob after 30 minutes'); +} async function processArtifact(artifact, filePath) { + const log = (...args) => console.log(`[${artifact.name}]`, ...args); const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { throw new Error(`Invalid artifact name: ${artifact.name}`); } - // getPlatform needs the unprocessedType const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); const quality = e('VSCODE_QUALITY'); const version = e('BUILD_SOURCEVERSION'); - const { product, os, arch, unprocessedType } = match.groups; - const isLegacy = artifact.name.includes('_legacy'); - const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); - const type = getRealType(unprocessedType); - const size = fs.statSync(filePath).size; - const stream = fs.createReadStream(filePath); - const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 - const log = (...args) => console.log(`[${artifact.name}]`, ...args); - const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); - const containerClient = blobServiceClient.getContainerClient('staging'); - const releaseService = await ESRPReleaseService.create(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT'), e('RELEASE_REQUEST_SIGNING_CERT'), containerClient); const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; - const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; - const res = await (0, retry_1.retry)(() => fetch(url)); - if (res.status === 200) { - log(`Already released and provisioned: ${url}`); - } - else { - await releaseService.createRelease(version, filePath, friendlyFileName); - } - const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; - log('Creating asset...'); - const result = await (0, retry_1.retry)(async (attempt) => { - log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); - const scripts = client.database('builds').container(quality).scripts; - const { resource: result } = await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); - return result; + const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); + const leasesContainerClient = blobServiceClient.getContainerClient('leases'); + await leasesContainerClient.createIfNotExists(); + const leaseBlobClient = leasesContainerClient.getBlockBlobClient(friendlyFileName); + log(`Acquiring lease for: ${friendlyFileName}`); + await withLease(leaseBlobClient, async () => { + log(`Successfully acquired lease for: ${friendlyFileName}`); + const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; + const res = await (0, retry_1.retry)(() => fetch(url)); + if (res.status === 200) { + log(`Already released and provisioned: ${url}`); + } + else { + const stagingContainerClient = blobServiceClient.getContainerClient('staging'); + await stagingContainerClient.createIfNotExists(); + const releaseService = await ESRPReleaseService.create(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT'), e('RELEASE_REQUEST_SIGNING_CERT'), stagingContainerClient); + await (0, retry_1.retry)(() => releaseService.createRelease(version, filePath, friendlyFileName)); + } + const { product, os, arch, unprocessedType } = match.groups; + const isLegacy = artifact.name.includes('_legacy'); + const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); + const type = getRealType(unprocessedType); + const size = fs.statSync(filePath).size; + const stream = fs.createReadStream(filePath); + const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 + const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; + log('Creating asset...'); + const result = await (0, retry_1.retry)(async (attempt) => { + log(`Creating asset in Cosmos DB (attempt ${attempt})...`); + const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); + const scripts = client.database('builds').container(quality).scripts; + const { resource: result } = await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); + return result; + }); + if (result === 'already exists') { + log('Asset already exists!'); + } + else { + log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); + } }); - if (result === 'already exists') { - log('Asset already exists!'); - } - else { - log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); - } + log(`Successfully released lease for: ${friendlyFileName}`); } // It is VERY important that we don't download artifacts too much too fast from AZDO. // AZDO throttles us SEVERELY if we do. Not just that, but they also close open diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index ab9270e177df7..0987502432bc2 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -16,8 +16,9 @@ import * as cp from 'child_process'; import * as os from 'os'; import { Worker, isMainThread, workerData } from 'node:worker_threads'; import { ConfidentialClientApplication } from '@azure/msal-node'; -import { BlobClient, BlobServiceClient, ContainerClient } from '@azure/storage-blob'; +import { BlobClient, BlobServiceClient, BlockBlobClient, ContainerClient } from '@azure/storage-blob'; import * as jws from 'jws'; +import { clearInterval, setInterval } from 'node:timers'; function e(name: string): string { const result = process.env[name]; @@ -74,6 +75,7 @@ interface ReleaseError { const enum StatusCode { Pass = 'pass', + Aborted = 'aborted', Inprogress = 'inprogress', FailCanRetry = 'failCanRetry', FailDoNotRetry = 'failDoNotRetry', @@ -376,8 +378,12 @@ class ESRPReleaseService { if (releaseStatus.status === 'pass') { break; + } else if (releaseStatus.status === 'aborted') { + this.log(JSON.stringify(releaseStatus)); + throw new Error(`Release was aborted`); } else if (releaseStatus.status !== 'inprogress') { - throw new Error(`Failed to submit release: ${JSON.stringify(releaseStatus)}`); + this.log(JSON.stringify(releaseStatus)); + throw new Error(`Unknown error when polling for release`); } } @@ -788,67 +794,121 @@ function getRealType(type: string) { } } +async function withLease(client: BlockBlobClient, fn: () => Promise) { + const lease = client.getBlobLeaseClient(); + + for (let i = 0; i < 360; i++) { // Try to get lease for 30 minutes + try { + await client.uploadData(new ArrayBuffer()); // blob needs to exist for lease to be acquired + await lease.acquireLease(60); + + try { + const abortController = new AbortController(); + const refresher = new Promise((c, e) => { + abortController.signal.onabort = () => { + clearInterval(interval); + c(); + }; + + const interval = setInterval(() => { + lease.renewLease().catch(err => { + clearInterval(interval); + e(new Error('Failed to renew lease ' + err)); + }); + }, 30_000); + }); + + const result = await Promise.race([fn(), refresher]); + abortController.abort(); + return result; + } finally { + await lease.releaseLease(); + } + } catch (err) { + if (err.statusCode !== 409 && err.statusCode !== 412) { + throw err; + } + + await new Promise(c => setTimeout(c, 5000)); + } + } + + throw new Error('Failed to acquire lease on blob after 30 minutes'); +} + async function processArtifact( artifact: Artifact, filePath: string ) { + const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { throw new Error(`Invalid artifact name: ${artifact.name}`); } - // getPlatform needs the unprocessedType const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); const quality = e('VSCODE_QUALITY'); const version = e('BUILD_SOURCEVERSION'); - const { product, os, arch, unprocessedType } = match.groups!; - const isLegacy = artifact.name.includes('_legacy'); - const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); - const type = getRealType(unprocessedType); - const size = fs.statSync(filePath).size; - const stream = fs.createReadStream(filePath); - const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 + const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; - const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); const blobServiceClient = new BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); - const containerClient = blobServiceClient.getContainerClient('staging'); + const leasesContainerClient = blobServiceClient.getContainerClient('leases'); + await leasesContainerClient.createIfNotExists(); + const leaseBlobClient = leasesContainerClient.getBlockBlobClient(friendlyFileName); - const releaseService = await ESRPReleaseService.create( - log, - e('RELEASE_TENANT_ID'), - e('RELEASE_CLIENT_ID'), - e('RELEASE_AUTH_CERT'), - e('RELEASE_REQUEST_SIGNING_CERT'), - containerClient - ); + log(`Acquiring lease for: ${friendlyFileName}`); - const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; - const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; - const res = await retry(() => fetch(url)); + await withLease(leaseBlobClient, async () => { + log(`Successfully acquired lease for: ${friendlyFileName}`); - if (res.status === 200) { - log(`Already released and provisioned: ${url}`); - } else { - await releaseService.createRelease(version, filePath, friendlyFileName); - } + const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; + const res = await retry(() => fetch(url)); - const asset: Asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; - log('Creating asset...'); + if (res.status === 200) { + log(`Already released and provisioned: ${url}`); + } else { + const stagingContainerClient = blobServiceClient.getContainerClient('staging'); + await stagingContainerClient.createIfNotExists(); + + const releaseService = await ESRPReleaseService.create( + log, + e('RELEASE_TENANT_ID'), + e('RELEASE_CLIENT_ID'), + e('RELEASE_AUTH_CERT'), + e('RELEASE_REQUEST_SIGNING_CERT'), + stagingContainerClient + ); + + await retry(() => releaseService.createRelease(version, filePath, friendlyFileName)); + } - const result = await retry(async (attempt) => { - log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT')!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); - const scripts = client.database('builds').container(quality).scripts; - const { resource: result } = await scripts.storedProcedure('createAsset').execute<'ok' | 'already exists'>('', [version, asset, true]); - return result; + const { product, os, arch, unprocessedType } = match.groups!; + const isLegacy = artifact.name.includes('_legacy'); + const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); + const type = getRealType(unprocessedType); + const size = fs.statSync(filePath).size; + const stream = fs.createReadStream(filePath); + const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 + const asset: Asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; + log('Creating asset...'); + + const result = await retry(async (attempt) => { + log(`Creating asset in Cosmos DB (attempt ${attempt})...`); + const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT')!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); + const scripts = client.database('builds').container(quality).scripts; + const { resource: result } = await scripts.storedProcedure('createAsset').execute<'ok' | 'already exists'>('', [version, asset, true]); + return result; + }); + + if (result === 'already exists') { + log('Asset already exists!'); + } else { + log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); + } }); - if (result === 'already exists') { - log('Asset already exists!'); - } else { - log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); - } + log(`Successfully released lease for: ${friendlyFileName}`); } // It is VERY important that we don't download artifacts too much too fast from AZDO. diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index 0bf8df9dc0106..cb1fb733b9998 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -1,29 +1,94 @@ { "comments": { "lineComment": "//", - "blockComment": ["/*", "*/"] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - { "open": "[", "close": "]" }, - { "open": "{", "close": "}" }, - { "open": "(", "close": ")" }, - { "open": "'", "close": "'", "notIn": ["string", "comment"] }, - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "/*", "close": "*/", "notIn": ["string", "comment"] }, - { "open": "/**", "close": " */", "notIn": ["string"] } + { + "open": "[", + "close": "]" + }, + { + "open": "{", + "close": "}" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "/*", + "close": "*/", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"], - ["<", ">"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ], + [ + "<", + ">" + ] ], "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)", "folding": { @@ -32,6 +97,14 @@ "end": "^\\s*#pragma\\s+endregion\\b" } }, + "indentationRules": { + "decreaseIndentPattern": { + "pattern": "^\\s*[\\}\\]\\)].*$" + }, + "increaseIndentPattern": { + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" + }, + }, "onEnterRules": [ { // Decrease indentation after single line if/else if/else, for, or while @@ -41,6 +114,19 @@ "action": { "indent": "outdent" } - } + }, + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, ] } diff --git a/extensions/csharp/language-configuration.json b/extensions/csharp/language-configuration.json index d8698b46c0906..60814ae02f4ae 100644 --- a/extensions/csharp/language-configuration.json +++ b/extensions/csharp/language-configuration.json @@ -1,32 +1,100 @@ { "comments": { "lineComment": "//", - "blockComment": ["/*", "*/"] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "'", "close": "'", "notIn": ["string", "comment"] }, - { "open": "\"", "close": "\"", "notIn": ["string", "comment"] } + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string", + "comment" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["<", ">"], - ["'", "'"], - ["\"", "\""] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "<", + ">" + ], + [ + "'", + "'" + ], + [ + "\"", + "\"" + ] ], "folding": { "markers": { "start": "^\\s*#region\\b", "end": "^\\s*#endregion\\b" } - } + }, + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, + ] } diff --git a/extensions/dart/cgmanifest.json b/extensions/dart/cgmanifest.json index da493cafa700c..5558b78af1d9b 100644 --- a/extensions/dart/cgmanifest.json +++ b/extensions/dart/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dart-lang/dart-syntax-highlight", "repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight", - "commitHash": "e8b053f9834cb44db0f49ac4a4567177bd943dbf" + "commitHash": "e1ac5c446c2531343393adbe8fff9d45d8a7c412" } }, "licenseDetail": [ diff --git a/extensions/dart/syntaxes/dart.tmLanguage.json b/extensions/dart/syntaxes/dart.tmLanguage.json index 32ea3f5b0c340..b4f80b680bd8f 100644 --- a/extensions/dart/syntaxes/dart.tmLanguage.json +++ b/extensions/dart/syntaxes/dart.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/dart-lang/dart-syntax-highlight/commit/e8b053f9834cb44db0f49ac4a4567177bd943dbf", + "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/e1ac5c446c2531343393adbe8fff9d45d8a7c412", "name": "Dart", "scopeName": "source.dart", "patterns": [ @@ -66,6 +66,16 @@ } ], "repository": { + "dartdoc-codeblock-triple": { + "begin": "^\\s*///\\s*(?!\\s*```)", + "end": "\n", + "contentName": "variable.other.source.dart" + }, + "dartdoc-codeblock-block": { + "begin": "^\\s*\\*\\s*(?!(\\s*```|/))", + "end": "\n", + "contentName": "variable.other.source.dart" + }, "dartdoc": { "patterns": [ { @@ -77,30 +87,31 @@ } }, { - "match": "^ {4,}(?![ \\*]).*", - "captures": { - "0": { - "name": "variable.name.source.dart" + "begin": "^\\s*///\\s*(```)", + "end": "^\\s*///\\s*(```)|^(?!\\s*///)", + "patterns": [ + { + "include": "#dartdoc-codeblock-triple" } - } + ] }, { - "contentName": "variable.other.source.dart", - "begin": "```.*?$", - "end": "```" + "begin": "^\\s*\\*\\s*(```)", + "end": "^\\s*\\*\\s*(```)|^(?=\\s*\\*/)", + "patterns": [ + { + "include": "#dartdoc-codeblock-block" + } + ] }, { - "match": "(`[^`]+?`)", - "captures": { - "0": { - "name": "variable.other.source.dart" - } - } + "match": "`[^`\n]+`", + "name": "variable.other.source.dart" }, { - "match": "(\\* (( ).*))$", + "match": "(?:\\*|\\/\\/)\\s{4,}(.*?)(?=($|\\*\\/))", "captures": { - "2": { + "1": { "name": "variable.other.source.dart" } } @@ -154,7 +165,7 @@ { "name": "comment.block.documentation.dart", "begin": "///", - "while": "^\\s*///", + "end": "^(?!\\s*///)", "patterns": [ { "include": "#dartdoc" diff --git a/extensions/docker/cgmanifest.json b/extensions/docker/cgmanifest.json index 4f568542aed88..8462de7dd7283 100644 --- a/extensions/docker/cgmanifest.json +++ b/extensions/docker/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-docker", "repositoryUrl": "https://github.com/moby/moby", - "commitHash": "abd39744c6f3ed854500e423f5fabf952165161f" + "commitHash": "c2029cb2574647e4bc28ed58486b8e85883eedb9" } }, "license": "Apache-2.0", @@ -15,4 +15,4 @@ } ], "version": 1 -} +} \ No newline at end of file diff --git a/extensions/docker/syntaxes/docker.tmLanguage.json b/extensions/docker/syntaxes/docker.tmLanguage.json index f7f414636c496..aa5223a31eae9 100644 --- a/extensions/docker/syntaxes/docker.tmLanguage.json +++ b/extensions/docker/syntaxes/docker.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/moby/moby/commit/abd39744c6f3ed854500e423f5fabf952165161f", + "version": "https://github.com/moby/moby/commit/c2029cb2574647e4bc28ed58486b8e85883eedb9", "name": "Dockerfile", "scopeName": "source.dockerfile", "patterns": [ @@ -41,6 +41,9 @@ }, "match": "^\\s*(?i:(ONBUILD)\\s+)?(?i:(CMD|ENTRYPOINT))\\s" }, + { + "include": "#string-character-escape" + }, { "begin": "\"", "beginCaptures": { @@ -57,8 +60,7 @@ "name": "string.quoted.double.dockerfile", "patterns": [ { - "match": "\\\\.", - "name": "constant.character.escaped.dockerfile" + "include": "#string-character-escape" } ] }, @@ -78,8 +80,7 @@ "name": "string.quoted.single.dockerfile", "patterns": [ { - "match": "\\\\.", - "name": "constant.character.escaped.dockerfile" + "include": "#string-character-escape" } ] }, @@ -98,5 +99,11 @@ "comment": "comment.line", "match": "^(\\s*)((#).*$\\n?)" } - ] + ], + "repository": { + "string-character-escape": { + "name": "constant.character.escaped.dockerfile", + "match": "\\\\." + } + } } \ No newline at end of file diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 1c2e6ca45c2a6..ef7997c7542ea 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -986,7 +986,7 @@ export class Repository implements Disposable { const actionButton = new ActionButton(this, this.commitCommandCenter, this.logger); this.disposables.push(actionButton); - actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button); + actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button, this, this.disposables); this._sourceControl.actionButton = actionButton.button; const progressManager = new ProgressManager(this); diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index 5276d2824a718..a6dbd5d1bf015 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "32bbaebcf218fa552e8f0397401e12f6e94fa3c5" + "commitHash": "fbdaec061157e98dda185c0ce771ce6a2c793045" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.7.8" + "version": "0.7.9" } ], "version": 1 diff --git a/extensions/go/language-configuration.json b/extensions/go/language-configuration.json index a5e06a56bad12..9238bf3529b04 100644 --- a/extensions/go/language-configuration.json +++ b/extensions/go/language-configuration.json @@ -1,28 +1,86 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "`", "close": "`", "notIn": ["string"]}, - { "open": "\"", "close": "\"", "notIn": ["string"]}, - { "open": "'", "close": "'", "notIn": ["string", "comment"]} + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "`", + "close": "`", + "notIn": [ + "string" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"], - ["`", "`"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ], + [ + "`", + "`" + ] ], "indentationRules": { "increaseIndentPattern": "^.*(\\bcase\\b.*:|\\bdefault\\b:|(\\b(func|if|else|switch|select|for|struct)\\b.*)?{[^}\"'`]*|\\([^)\"'`]*)$", @@ -33,5 +91,20 @@ "start": "^\\s*//\\s*#?region\\b", "end": "^\\s*//\\s*#?endregion\\b" } - } -} \ No newline at end of file + }, + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, + ] +} diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index ed6ead03480da..db17cad3f9119 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.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/worlpaker/go-syntax/commit/32bbaebcf218fa552e8f0397401e12f6e94fa3c5", + "version": "https://github.com/worlpaker/go-syntax/commit/fbdaec061157e98dda185c0ce771ce6a2c793045", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -97,7 +97,10 @@ "comment": "all statements related to variables", "patterns": [ { - "include": "#var_const_assignment" + "include": "#const_assignment" + }, + { + "include": "#var_assignment" }, { "include": "#variable_assignment" @@ -2645,12 +2648,12 @@ } ] }, - "var_const_assignment": { - "comment": "variable assignment with var and const keyword", + "var_assignment": { + "comment": "variable assignment with var keyword", "patterns": [ { - "comment": "var and const with single type assignment", - "match": "(?:(?<=\\bvar\\b|\\bconst\\b)(?:\\s*)(\\b[\\w\\.]+(?:\\,\\s*[\\w\\.]+)*)(?:\\s*)((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?!(?:[\\[\\]\\*]+)?\\b(?:struct|func|map)\\b)(?:[\\w\\.\\[\\]\\*]+(?:\\,\\s*[\\w\\.\\[\\]\\*]+)*)?(?:\\s*)(?:\\=)?)?)", + "comment": "single assignment", + "match": "(?:(?<=\\bvar\\b)(?:\\s*)(\\b[\\w\\.]+(?:\\,\\s*[\\w\\.]+)*)(?:\\s*)((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?!(?:[\\[\\]\\*]+)?\\b(?:struct|func|map)\\b)(?:[\\w\\.\\[\\]\\*]+(?:\\,\\s*[\\w\\.\\[\\]\\*]+)*)?(?:\\s*)(?:\\=)?)?)", "captures": { "1": { "patterns": [ @@ -2696,8 +2699,8 @@ } }, { - "comment": "var and const with multi type assignment", - "begin": "(?:(?<=\\bvar\\b|\\bconst\\b)(?:\\s*)(\\())", + "comment": "multi assignment", + "begin": "(?:(?<=\\bvar\\b)(?:\\s*)(\\())", "beginCaptures": { "1": { "name": "punctuation.definition.begin.bracket.round.go" @@ -2763,6 +2766,124 @@ } ] }, + "const_assignment": { + "comment": "constant assignment with const keyword", + "patterns": [ + { + "comment": "single assignment", + "match": "(?:(?<=\\bconst\\b)(?:\\s*)(\\b[\\w\\.]+(?:\\,\\s*[\\w\\.]+)*)(?:\\s*)((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?!(?:[\\[\\]\\*]+)?\\b(?:struct|func|map)\\b)(?:[\\w\\.\\[\\]\\*]+(?:\\,\\s*[\\w\\.\\[\\]\\*]+)*)?(?:\\s*)(?:\\=)?)?)", + "captures": { + "1": { + "patterns": [ + { + "include": "#delimiters" + }, + { + "match": "\\w+", + "name": "variable.other.constant.go" + } + ] + }, + "2": { + "patterns": [ + { + "include": "#type-declarations-without-brackets" + }, + { + "include": "#generic_types" + }, + { + "match": "\\(", + "name": "punctuation.definition.begin.bracket.round.go" + }, + { + "match": "\\)", + "name": "punctuation.definition.end.bracket.round.go" + }, + { + "match": "\\[", + "name": "punctuation.definition.begin.bracket.square.go" + }, + { + "match": "\\]", + "name": "punctuation.definition.end.bracket.square.go" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] + } + } + }, + { + "comment": "multi assignment", + "begin": "(?:(?<=\\bconst\\b)(?:\\s*)(\\())", + "beginCaptures": { + "1": { + "name": "punctuation.definition.begin.bracket.round.go" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.end.bracket.round.go" + } + }, + "patterns": [ + { + "match": "(?:(?:^\\s*)(\\b[\\w\\.]+(?:\\,\\s*[\\w\\.]+)*)(?:\\s*)((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?!(?:[\\[\\]\\*]+)?\\b(?:struct|func|map)\\b)(?:[\\w\\.\\[\\]\\*]+(?:\\,\\s*[\\w\\.\\[\\]\\*]+)*)?(?:\\s*)(?:\\=)?)?)", + "captures": { + "1": { + "patterns": [ + { + "include": "#delimiters" + }, + { + "match": "\\w+", + "name": "variable.other.constant.go" + } + ] + }, + "2": { + "patterns": [ + { + "include": "#type-declarations-without-brackets" + }, + { + "include": "#generic_types" + }, + { + "match": "\\(", + "name": "punctuation.definition.begin.bracket.round.go" + }, + { + "match": "\\)", + "name": "punctuation.definition.end.bracket.round.go" + }, + { + "match": "\\[", + "name": "punctuation.definition.begin.bracket.square.go" + }, + { + "match": "\\]", + "name": "punctuation.definition.end.bracket.square.go" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] + } + } + }, + { + "include": "$self" + } + ] + } + ] + }, "variable_assignment": { "comment": "variable assignment", "patterns": [ diff --git a/extensions/groovy/language-configuration.json b/extensions/groovy/language-configuration.json index a81a8864a5127..39e5fd4092c05 100644 --- a/extensions/groovy/language-configuration.json +++ b/extensions/groovy/language-configuration.json @@ -1,25 +1,88 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] } + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ] + ], + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, ] } diff --git a/extensions/java/language-configuration.json b/extensions/java/language-configuration.json index 610adc686b45c..6ba09bbd15c01 100644 --- a/extensions/java/language-configuration.json +++ b/extensions/java/language-configuration.json @@ -1,28 +1,85 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] }, - { "open": "/**", "close": " */", "notIn": ["string"] } + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"], - ["<", ">"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ], + [ + "<", + ">" + ] ], "folding": { "markers": { @@ -97,6 +154,19 @@ "action": { "indent": "indent" } - } + }, + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, ] } diff --git a/extensions/json/language-configuration.json b/extensions/json/language-configuration.json index f9ec3fec78102..d47efe2587edb 100644 --- a/extensions/json/language-configuration.json +++ b/extensions/json/language-configuration.json @@ -1,22 +1,84 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"] + [ + "{", + "}" + ], + [ + "[", + "]" + ] ], "autoClosingPairs": [ - { "open": "{", "close": "}", "notIn": ["string"] }, - { "open": "[", "close": "]", "notIn": ["string"] }, - { "open": "(", "close": ")", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] }, - { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, - { "open": "`", "close": "`", "notIn": ["string", "comment"] } + { + "open": "{", + "close": "}", + "notIn": [ + "string" + ] + }, + { + "open": "[", + "close": "]", + "notIn": [ + "string" + ] + }, + { + "open": "(", + "close": ")", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "`", + "close": "`", + "notIn": [ + "string", + "comment" + ] + } ], "indentationRules": { "increaseIndentPattern": "({+(?=((\\\\.|[^\"\\\\])*\"(\\\\.|[^\"\\\\])*\")*[^\"}]*)$)|(\\[+(?=((\\\\.|[^\"\\\\])*\"(\\\\.|[^\"\\\\])*\")*[^\"\\]]*)$)", "decreaseIndentPattern": "^\\s*[}\\]],?\\s*$" - } + }, + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, + ] } diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index d937ba4f4304f..25b52bf3787e7 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "df6ef817c932d24da5cc72927344a547e463cc65" + "commitHash": "59971565a7065dbb617576c04add9d891b056319" } }, "license": "MIT", diff --git a/extensions/less/language-configuration.json b/extensions/less/language-configuration.json index 7325d05270449..71e155ddfcc3e 100644 --- a/extensions/less/language-configuration.json +++ b/extensions/less/language-configuration.json @@ -1,26 +1,88 @@ { "comments": { - "blockComment": ["/*", "*/"], + "blockComment": [ + "/*", + "*/" + ], "lineComment": "//" }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - { "open": "{", "close": "}", "notIn": ["string", "comment"] }, - { "open": "[", "close": "]", "notIn": ["string", "comment"] }, - { "open": "(", "close": ")", "notIn": ["string", "comment"] }, - { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, - { "open": "'", "close": "'", "notIn": ["string", "comment"] } + { + "open": "{", + "close": "}", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "[", + "close": "]", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "(", + "close": ")", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ] ], "folding": { "markers": { @@ -32,5 +94,20 @@ "increaseIndentPattern": "(^.*\\{[^}]*$)", "decreaseIndentPattern": "^\\s*\\}" }, - "wordPattern": "(#?-?\\d*\\.\\d\\w*%?)|(::?[\\w-]+(?=[^,{;]*[,{]))|(([@#.!])?[\\w-?]+%?|[@#!.])" + "wordPattern": "(#?-?\\d*\\.\\d\\w*%?)|(::?[\\w-]+(?=[^,{;]*[,{]))|(([@#.!])?[\\w-?]+%?|[@#!.])", + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, + ] } diff --git a/extensions/objective-c/language-configuration.json b/extensions/objective-c/language-configuration.json index a81a8864a5127..39e5fd4092c05 100644 --- a/extensions/objective-c/language-configuration.json +++ b/extensions/objective-c/language-configuration.json @@ -1,25 +1,88 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] } + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ] + ], + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, ] } diff --git a/extensions/perl/cgmanifest.json b/extensions/perl/cgmanifest.json index cd175abe37de7..b7c850dd1aab8 100644 --- a/extensions/perl/cgmanifest.json +++ b/extensions/perl/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "textmate/perl.tmbundle", "repositoryUrl": "https://github.com/textmate/perl.tmbundle", - "commitHash": "a85927a902d6e5d7805f56a653f324d34dfad53a" + "commitHash": "d9841a0878239fa43f88c640f8d458590f97e8f5" } }, "licenseDetail": [ diff --git a/extensions/php/language-configuration.json b/extensions/php/language-configuration.json index f44d7a25cb618..d696ffa29503a 100644 --- a/extensions/php/language-configuration.json +++ b/extensions/php/language-configuration.json @@ -1,28 +1,96 @@ { "comments": { "lineComment": "//", // "#" - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - { "open": "{", "close": "}", "notIn": ["string"] }, - { "open": "[", "close": "]", "notIn": ["string"] }, - { "open": "(", "close": ")", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string", "comment"] }, - { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, - { "open": "/**", "close": " */", "notIn": ["string"] } + { + "open": "{", + "close": "}", + "notIn": [ + "string" + ] + }, + { + "open": "[", + "close": "]", + "notIn": [ + "string" + ] + }, + { + "open": "(", + "close": ")", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["'", "'"], - ["\"", "\""], - ["`", "`"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "'", + "'" + ], + [ + "\"", + "\"" + ], + [ + "`", + "`" + ] ], "indentationRules": { "increaseIndentPattern": "({(?!.*}).*|\\(|\\[|((else(\\s)?)?if|else|for(each)?|while|switch|case).*:)\\s*((/[/*].*|)?$|\\?>)", @@ -85,6 +153,19 @@ "action": { "indent": "outdent" } - } + }, + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, ] } diff --git a/extensions/rust/language-configuration.json b/extensions/rust/language-configuration.json index ecb0007f6ea2c..490f4409c652c 100644 --- a/extensions/rust/language-configuration.json +++ b/extensions/rust/language-configuration.json @@ -1,25 +1,67 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "\"", "close": "\"", "notIn": ["string"] } + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["<", ">"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "<", + ">" + ] ], "indentationRules": { "increaseIndentPattern": "^.*\\{[^}\"']*$|^.*\\([^\\)\"']*$", @@ -30,5 +72,20 @@ "start": "^\\s*//\\s*#?region\\b", "end": "^\\s*//\\s*#?endregion\\b" } - } + }, + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, + ] } diff --git a/extensions/swift/language-configuration.json b/extensions/swift/language-configuration.json index 54095ef5212e2..e1ceb1f6bc6fc 100644 --- a/extensions/swift/language-configuration.json +++ b/extensions/swift/language-configuration.json @@ -1,27 +1,99 @@ { "comments": { "lineComment": "//", - "blockComment": [ "/*", "*/" ] + "blockComment": [ + "/*", + "*/" + ] }, "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] ], "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] }, - { "open": "`", "close": "`", "notIn": ["string"] } + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string" + ] + }, + { + "open": "`", + "close": "`", + "notIn": [ + "string" + ] + } ], "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"], - ["`", "`"] + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ], + [ + "`", + "`" + ] + ], + "onEnterRules": [ + // Add // when pressing enter from inside line comment + { + "beforeText": { + "pattern": "\/\/.*" + }, + "afterText": { + "pattern": "^(?!\\s*$).+" + }, + "action": { + "indent": "none", + "appendText": "// " + } + }, ] } diff --git a/package-lock.json b/package-lock.json index b4c9991af6b16..a16b3ecf06f5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-oss-dev", - "version": "1.96.0", + "version": "1.97.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.96.0", + "version": "1.97.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f7a7f68cde781..f283587b8c5f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "1.96.0", + "version": "1.97.0", "distro": "c883c91dadf5f063b26c86ffe01851acee3747c6", "author": { "name": "Microsoft Corporation" diff --git a/src/main.ts b/src/main.ts index 2d9c977a3bdb3..1504375283f39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -288,8 +288,9 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // Following features are disabled from the runtime: // `CalculateNativeWinOcclusion` - Disable native window occlusion tracker (https://groups.google.com/a/chromium.org/g/embedder-dev/c/ZF3uHHyWLKw/m/VDN2hDXMAAAJ) + // `PlzDedicatedWorker` - Refs https://github.com/microsoft/vscode/issues/233060#issuecomment-2523212427 const featuresToDisable = - `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; + `CalculateNativeWinOcclusion,PlzDedicatedWorker,${app.commandLine.getSwitchValue('disable-features')}`; app.commandLine.appendSwitch('disable-features', featuresToDisable); // Blink features to configure. diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 4a4d15cc2df09..01f7648ad16f1 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index b1bc28386575d..c578d85ee4e0f 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -584,4 +584,6 @@ export const codiconsLibrary = { codeReview: register('code-review', 0xec37), copilotWarning: register('copilot-warning', 0xec38), python: register('python', 0xec39), + copilotLarge: register('copilot-large', 0xec3a), + copilotWarningLarge: register('copilot-warning-large', 0xec3b), } as const; diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index 09021d118b747..f6565bdc4e2f7 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -383,28 +383,22 @@ export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: A return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); } -export function lineRangeMappingFromChanges(changes: IChange[]): LineRangeMapping[] { - const lineRangeMapping: LineRangeMapping[] = []; - - for (const change of changes) { - let originalRange: LineRange; - if (change.originalEndLineNumber === 0) { - // Insertion - originalRange = new LineRange(change.originalStartLineNumber + 1, change.originalStartLineNumber + 1); - } else { - originalRange = new LineRange(change.originalStartLineNumber, change.originalEndLineNumber + 1); - } - - let modifiedRange: LineRange; - if (change.modifiedEndLineNumber === 0) { - // Deletion - modifiedRange = new LineRange(change.modifiedStartLineNumber + 1, change.modifiedStartLineNumber + 1); - } else { - modifiedRange = new LineRange(change.modifiedStartLineNumber, change.modifiedEndLineNumber + 1); - } +export function lineRangeMappingFromChange(change: IChange): LineRangeMapping { + let originalRange: LineRange; + if (change.originalEndLineNumber === 0) { + // Insertion + originalRange = new LineRange(change.originalStartLineNumber + 1, change.originalStartLineNumber + 1); + } else { + originalRange = new LineRange(change.originalStartLineNumber, change.originalEndLineNumber + 1); + } - lineRangeMapping.push(new LineRangeMapping(originalRange, modifiedRange)); + let modifiedRange: LineRange; + if (change.modifiedEndLineNumber === 0) { + // Deletion + modifiedRange = new LineRange(change.modifiedStartLineNumber + 1, change.modifiedStartLineNumber + 1); + } else { + modifiedRange = new LineRange(change.modifiedStartLineNumber, change.modifiedEndLineNumber + 1); } - return lineRangeMapping; + return new LineRangeMapping(originalRange, modifiedRange); } diff --git a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts index 6f6f8323e022f..70c093ee9d8f3 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts @@ -196,7 +196,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri return; } - this.textModelResolverService.createModelReference(result.uri).then(ref => { + return this.textModelResolverService.createModelReference(result.uri).then(ref => { if (!ref.object || !ref.object.textEditorModel) { ref.dispose(); diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index ca0677d32066a..9b8252eec14cd 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -244,10 +244,16 @@ export class ContentHoverController extends Disposable implements IEditorContrib return; } const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e); - const isModifierKeyPressed = this._isModifierKeyPressed(e); - if (isPotentialKeyboardShortcut || isModifierKeyPressed) { + if (isPotentialKeyboardShortcut) { return; } + const isModifierKeyPressed = this._isModifierKeyPressed(e); + if (isModifierKeyPressed && this._mouseMoveEvent) { + const contentWidget: ContentHoverWidgetWrapper = this._getOrCreateContentWidget(); + if (contentWidget.showsOrWillShow(this._mouseMoveEvent)) { + return; + } + } this.hideContentHover(); } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index ad27c1544d58a..ffb8aaa9f115d 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -19,7 +19,6 @@ import { Emitter } from '../../../../base/common/event.js'; import { RenderedContentHover } from './contentHoverRendered.js'; const HORIZONTAL_SCROLLING_BY = 30; -const CONTAINER_HEIGHT_PADDING = 6; export class ContentHoverWidget extends ResizableContentWidget { @@ -68,6 +67,7 @@ export class ContentHoverWidget extends ResizableContentWidget { dom.append(this._resizableNode.domNode, this._hover.containerDomNode); this._resizableNode.domNode.style.zIndex = '50'; + this._resizableNode.domNode.className = 'monaco-resizable-hover'; this._register(this._editor.onDidLayoutChange(() => { if (this.isVisible) { @@ -117,9 +117,15 @@ export class ContentHoverWidget extends ResizableContentWidget { return ContentHoverWidget._applyDimensions(containerDomNode, width, height); } + private _setScrollableElementDimensions(width: number | string, height: number | string): void { + const scrollbarDomElement = this._hover.scrollbar.getDomNode(); + return ContentHoverWidget._applyDimensions(scrollbarDomElement, width, height); + } + private _setHoverWidgetDimensions(width: number | string, height: number | string): void { - this._setContentsDomNodeDimensions(width, height); this._setContainerDomNodeDimensions(width, height); + this._setScrollableElementDimensions(width, height); + this._setContentsDomNodeDimensions(width, height); this._layoutContentWidget(); } @@ -176,12 +182,11 @@ export class ContentHoverWidget extends ResizableContentWidget { if (!availableSpace) { return; } - // Padding needed in order to stop the resizing down to a smaller height - let maximumHeight = CONTAINER_HEIGHT_PADDING; + const children = this._hover.contentsDomNode.children; + let maximumHeight = children.length - 1; Array.from(this._hover.contentsDomNode.children).forEach((hoverPart) => { maximumHeight += hoverPart.clientHeight; }); - return Math.min(availableSpace, maximumHeight); } @@ -209,7 +214,7 @@ export class ContentHoverWidget extends ResizableContentWidget { const initialWidth = ( typeof this._contentWidth === 'undefined' ? 0 - : this._contentWidth - 2 // - 2 for the borders + : this._contentWidth ); if (overflowing || this._hover.containerDomNode.clientWidth < initialWidth) { @@ -217,7 +222,7 @@ export class ContentHoverWidget extends ResizableContentWidget { const horizontalPadding = 14; return bodyBoxWidth - horizontalPadding; } else { - return this._hover.containerDomNode.clientWidth + 2; + return this._hover.containerDomNode.clientWidth; } } @@ -389,16 +394,16 @@ export class ContentHoverWidget extends ResizableContentWidget { public onContentsChanged(): void { this._removeConstraintsRenderNormally(); - const containerDomNode = this._hover.containerDomNode; + const contentsDomNode = this._hover.contentsDomNode; - let height = dom.getTotalHeight(containerDomNode); - let width = dom.getTotalWidth(containerDomNode); + let height = dom.getTotalHeight(contentsDomNode); + let width = dom.getTotalWidth(contentsDomNode) + 2; this._resizableNode.layout(height, width); this._setHoverWidgetDimensions(width, height); - height = dom.getTotalHeight(containerDomNode); - width = dom.getTotalWidth(containerDomNode); + height = dom.getTotalHeight(contentsDomNode); + width = dom.getTotalWidth(contentsDomNode); this._contentWidth = width; this._updateMinimumWidth(); this._resizableNode.layout(height, width); diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 958f1ee9cf426..b9da587c91840 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -7,17 +7,15 @@ background-color: var(--vscode-editor-hoverHighlightBackground); } -.monaco-editor .monaco-hover-content { - padding-right: 2px; - padding-bottom: 2px; - box-sizing: border-box; +.monaco-editor .monaco-resizable-hover { + border: 1px solid var(--vscode-editorHoverWidget-border); + border-radius: 3px; + box-sizing: content-box; } .monaco-editor .monaco-hover { color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); - border: 1px solid var(--vscode-editorHoverWidget-border); - border-radius: 3px; } .monaco-editor .monaco-hover a { diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 93af5bc077d59..73d81e729cbbc 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -166,7 +166,12 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } return observableFromEvent(this, dirtyDiffModel.onDidChange, () => { - return dirtyDiffModel.getQuickDiffResults(); + return dirtyDiffModel.getQuickDiffResults() + .map(result => ({ + original: result.original, + modified: result.modified, + changes: result.changes2 + })); }); } @@ -180,7 +185,12 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } return observableFromEvent(Event.any(dirtyDiffModel.onDidChange, diffEditor.onDidUpdateDiff), () => { - const dirtyDiffInformation = dirtyDiffModel.getQuickDiffResults(); + const dirtyDiffInformation = dirtyDiffModel.getQuickDiffResults() + .map(result => ({ + original: result.original, + modified: result.modified, + changes: result.changes2 + })); const diffChanges = diffEditor.getDiffComputationResult()?.changes2 ?? []; const diffInformation = [{ diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 6aa44168c7b95..0264e5460d4d3 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -207,10 +207,6 @@ border-color: var(--vscode-commandCenter-inactiveBorder) !important; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .codicon-copilot { - font-size: 14px; -} - .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:HOVER { color: var(--vscode-commandCenter-activeForeground); background-color: var(--vscode-commandCenter-activeBackground); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index ce421fb04911a..cca372c610304 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -20,7 +20,7 @@ import { ILocalizedString, localize, localize2 } from '../../../../../nls.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -571,9 +571,12 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench @IChatAgentService agentService: IChatAgentService, @IChatQuotasService chatQuotasService: IChatQuotasService, @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); + const contextKeySet = new Set([ChatContextKeys.Setup.signedOut.key]); + actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { if (!(action instanceof SubmenuItemAction)) { return undefined; @@ -587,29 +590,39 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench const chatExtensionInstalled = agentService.getAgents().some(agent => agent.isDefault); const { chatQuotaExceeded, completionsQuotaExceeded } = chatQuotasService.quotas; - - let primaryAction: MenuItemAction; - if (chatExtensionInstalled && !chatQuotaExceeded && !completionsQuotaExceeded) { - primaryAction = instantiationService.createInstance(MenuItemAction, { - id: CHAT_OPEN_ACTION_ID, - title: OpenChatGlobalAction.TITLE, - icon: Codicon.copilot, - }, undefined, undefined, undefined, undefined); - } else if (!chatExtensionInstalled) { - primaryAction = instantiationService.createInstance(MenuItemAction, { - id: 'workbench.action.chat.triggerSetup', - title: localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."), - icon: Codicon.copilot, - }, undefined, undefined, undefined, undefined); + const signedOut = contextKeyService.getContextKeyValue(ChatContextKeys.Setup.signedOut.key) ?? false; + + let primaryActionId: string; + let primaryActionTitle: string; + let primaryActionIcon: ThemeIcon; + if (!chatExtensionInstalled) { + primaryActionId = 'workbench.action.chat.triggerSetup'; + primaryActionTitle = localize('triggerChatSetup', "Use AI Features with Copilot for Free..."); + primaryActionIcon = Codicon.copilot; } else { - primaryAction = instantiationService.createInstance(MenuItemAction, { - id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, - title: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }), - icon: Codicon.copilotWarning, - }, undefined, undefined, undefined, undefined); + if (signedOut) { + primaryActionId = CHAT_OPEN_ACTION_ID; + primaryActionTitle = localize('signInToChatSetup', "Sign in to Use Copilot..."); + primaryActionIcon = Codicon.copilotWarning; + } else if (chatQuotaExceeded || completionsQuotaExceeded) { + primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; + primaryActionTitle = quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }); + primaryActionIcon = Codicon.copilotWarning; + } else { + primaryActionId = CHAT_OPEN_ACTION_ID; + primaryActionTitle = OpenChatGlobalAction.TITLE.value; + primaryActionIcon = Codicon.copilot; + } } - - return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, primaryAction, dropdownAction, action.actions, '', { ...options, skipTelemetry: true }); - }, Event.any(agentService.onDidChangeAgents, chatQuotasService.onDidChangeQuotas)); + return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, instantiationService.createInstance(MenuItemAction, { + id: primaryActionId, + title: primaryActionTitle, + icon: primaryActionIcon, + }, undefined, undefined, undefined, undefined), dropdownAction, action.actions, '', { ...options, skipTelemetry: true }); + }, Event.any( + agentService.onDidChangeAgents, + chatQuotasService.onDidChangeQuotas, + Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(contextKeySet)) + )); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index a7ce13ef1d6ad..c812ec0d053d2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -43,7 +43,7 @@ import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../searc import { SearchContext } from '../../../search/common/constants.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatEditingService } from '../../common/chatEditingService.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariableData, IChatVariablesService } from '../../common/chatVariables.js'; @@ -794,8 +794,10 @@ export class AttachContextAction extends Action2 { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); if (chatEditingService) { - for (const file of chatEditingService.currentEditingSessionObs.get()?.workingSet.keys() ?? []) { - attachedContext.add(this._getFileContextId({ resource: file })); + for (const [file, state] of chatEditingService.currentEditingSessionObs.get()?.workingSet.entries() ?? []) { + if (state.state !== WorkingSetEntryState.Suggested) { + attachedContext.add(this._getFileContextId({ resource: file })); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 7d395f051d875..10dd97aff86ac 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; -import { Event } from '../../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { marked } from '../../../../../base/common/marked/marked.js'; +import { waitForState } from '../../../../../base/common/observable.js'; import { basename } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; @@ -465,9 +465,7 @@ export function registerChatTitleActions() { let editingSession = chatEditingService.currentEditingSessionObs.get(); if (!editingSession) { - await Event.toPromise(chatEditingService.onDidCreateEditingSession); - editingSession = chatEditingService.currentEditingSessionObs.get(); - return; + editingSession = await waitForState(chatEditingService.currentEditingSessionObs); } if (!editingSession) { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9506286866508..2aa243649de88 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -124,6 +124,7 @@ configurationRegistry.registerConfiguration({ 'chat.experimental.offerSetup': { type: 'boolean', default: false, + scope: ConfigurationScope.APPLICATION, markdownDescription: nls.localize('chat.experimental.offerSetup', "Controls whether setup is offered for Chat if not done already."), tags: ['experimental', 'onExP'] }, diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index d0ac0fec70166..a281df1c40cee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -35,7 +35,6 @@ import { IChatMarkdownContent } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; import { IChatCodeBlockInfo, IChatListItemRendererOptions } from '../chat.js'; -import { AnimatedValue, ObservableAnimatedValue } from '../chatEditorOverlay.js'; import { IChatRendererDelegate } from '../chatListRenderer.js'; import { ChatMarkdownDecorationsRenderer } from '../chatMarkdownDecorationsRenderer.js'; import { ChatEditorOptions } from '../chatOptions.js'; @@ -351,23 +350,15 @@ class CollapsedCodeBlock extends Disposable { this.element.title = this.labelService.getUriLabel(uri, { relative: false }); // Show a percentage progress that is driven by the rewrite - const slickRatio = ObservableAnimatedValue.const(0); - let t = Date.now(); + this._progressStore.add(autorun(r => { const rewriteRatio = modifiedEntry?.rewriteRatio.read(r); - if (rewriteRatio) { - slickRatio.changeAnimation(prev => { - const result = new AnimatedValue(prev.getValue(), rewriteRatio, Date.now() - t); - t = Date.now(); - return result; - }, undefined); - } const labelDetail = this.element.querySelector('.label-detail'); const isComplete = !modifiedEntry?.isCurrentlyBeingModified.read(r); if (labelDetail && !isStreaming && !isComplete) { - const value = slickRatio.getValue(undefined); - labelDetail.textContent = value === 0 ? localize('chat.codeblock.applying', "Applying edits...") : localize('chat.codeblock.applyingPercentage', "Applying edits ({0}%)...", Math.round(value * 100)); + const value = rewriteRatio; + labelDetail.textContent = value === 0 || !value ? localize('chat.codeblock.applying', "Applying edits...") : localize('chat.codeblock.applyingPercentage', "Applying edits ({0}%)...", Math.round(value * 100)); } else if (labelDetail && !isStreaming && isComplete) { iconEl.classList.remove(...iconClasses); const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE; @@ -388,8 +379,8 @@ class CollapsedCodeBlock extends Disposable { } labelAdded.textContent = `+${addedLines}`; labelRemoved.textContent = `-${removedLines}`; - const insertionsFragment = addedLines === 1 ? localize('chat.codeblock.insertions.one', "{0} insertion") : localize('chat.codeblock.insertions', "{0} insertions", addedLines); - const deletionsFragment = removedLines === 1 ? localize('chat.codeblock.deletions.one', "{0} deletion") : localize('chat.codeblock.deletions', "{0} deletions", removedLines); + const insertionsFragment = addedLines === 1 ? localize('chat.codeblock.insertions.one', "1 insertion") : localize('chat.codeblock.insertions', "{0} insertions", addedLines); + const deletionsFragment = removedLines === 1 ? localize('chat.codeblock.deletions.one', "1 deletion") : localize('chat.codeblock.deletions', "{0} deletions", removedLines); this.element.ariaLabel = this.element.title = localize('summary', 'Edited {0}, {1}, {2}', iconText, insertionsFragment, deletionsFragment); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 5a067cee72f27..8a2ac519e78dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -109,6 +109,13 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { for (const uri of uris) { chatWidget.attachmentModel.delete(uri.toString()); } + + // If there are now only suggested files in the working set, also clear those + const entries = [...currentEditingSession.workingSet.entries()]; + const suggestedFiles = entries.filter(([_, state]) => state.state === WorkingSetEntryState.Suggested); + if (suggestedFiles.length === entries.length && !chatWidget.attachmentModel.attachments.find((v) => v.isFile && URI.isUri(v.value))) { + currentEditingSession.remove(WorkingSetEntryRemovalReason.Programmatic, ...entries.map(([uri,]) => uri)); + } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index f1204bd40ed3d..34b24c1dbb8e4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -61,11 +61,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._currentSessionObs; } - private readonly _onDidCreateEditingSession = this._register(new Emitter()); - get onDidCreateEditingSession() { - return this._onDidCreateEditingSession.event; - } - private readonly _onDidChangeEditingSession = this._register(new Emitter()); public readonly onDidChangeEditingSession = this._onDidChangeEditingSession.event; @@ -220,7 +215,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic })); this._currentSessionObs.set(session, undefined); - this._onDidCreateEditingSession.fire(session); this._onDidChangeEditingSession.fire(); return session; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 978b13b59a11a..6a09f5ef2751f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -5,7 +5,7 @@ import './media/chatEditorOverlay.css'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, IReader, ISettableObservable, ITransaction, observableFromEvent, observableSignal, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; @@ -17,7 +17,7 @@ import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionView import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IActionRunner } from '../../../../base/common/actions.js'; -import { $, append, EventLike, getWindow, reset, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { $, append, EventLike, reset } from '../../../../base/browser/dom.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -25,7 +25,6 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { assertType } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { ctxNotebookHasEditorModification } from '../../notebook/browser/contrib/chatEdit/notebookChatEditController.js'; import { AcceptAction, RejectAction } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; @@ -195,22 +194,11 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._domNode.classList.toggle('busy', busy); })); - const slickRatio = ObservableAnimatedValue.const(0); - let t = Date.now(); this._showStore.add(autorun(r => { const value = activeEntry.rewriteRatio.read(r); - - slickRatio.changeAnimation(prev => { - const result = new AnimatedValue(prev.getValue(), value, Date.now() - t); - t = Date.now(); - return result; - }, undefined); - - const value2 = slickRatio.getValue(r); - reset(this._progressNode, (value === 0 ? localize('generating', "Generating edits...") - : localize('applyingPercentage', "{0}% Applying edits...", Math.round(value2 * 100)))); + : localize('applyingPercentage', "{0}% Applying edits...", Math.round(value * 100)))); })); this._showStore.add(autorun(r => { @@ -269,101 +257,10 @@ MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { title: localize('label', "Navigation Status"), precondition: ContextKeyExpr.false(), }, - when: ctxNotebookHasEditorModification.negate(), group: 'navigate', order: -1 }); - -export class ObservableAnimatedValue { - public static const(value: number): ObservableAnimatedValue { - return new ObservableAnimatedValue(AnimatedValue.const(value)); - } - - private readonly _value: ISettableObservable; - - constructor( - initialValue: AnimatedValue, - ) { - this._value = observableValue(this, initialValue); - } - - setAnimation(value: AnimatedValue, tx: ITransaction | undefined): void { - this._value.set(value, tx); - } - - changeAnimation(fn: (prev: AnimatedValue) => AnimatedValue, tx: ITransaction | undefined): void { - const value = fn(this._value.get()); - this._value.set(value, tx); - } - - getValue(reader: IReader | undefined): number { - const value = this._value.read(reader); - if (!value.isFinished()) { - Scheduler.instance.invalidateOnNextAnimationFrame(reader); - } - return value.getValue(); - } -} - -class Scheduler { - static instance = new Scheduler(); - - private readonly _signal = observableSignal(this); - - private _isScheduled = false; - - invalidateOnNextAnimationFrame(reader: IReader | undefined): void { - this._signal.read(reader); - if (!this._isScheduled) { - this._isScheduled = true; - scheduleAtNextAnimationFrame(getWindow(undefined), () => { - this._isScheduled = false; - this._signal.trigger(undefined); - }); - } - } -} - -export class AnimatedValue { - - static const(value: number): AnimatedValue { - return new AnimatedValue(value, value, 0); - } - - readonly startTimeMs = Date.now(); - - constructor( - readonly startValue: number, - readonly endValue: number, - readonly durationMs: number, - ) { - if (startValue === endValue) { - this.durationMs = 0; - } - } - - isFinished(): boolean { - return Date.now() >= this.startTimeMs + this.durationMs; - } - - getValue(): number { - const timePassed = Date.now() - this.startTimeMs; - if (timePassed >= this.durationMs) { - return this.endValue; - } - const value = easeOutExpo(timePassed, this.startValue, this.endValue - this.startValue, this.durationMs); - return value; - } -} - -function easeOutExpo(passedTime: number, start: number, length: number, totalDuration: number): number { - return passedTime === totalDuration - ? start + length - : length * (-Math.pow(2, -10 * passedTime / totalDuration) + 1) + start; -} - - export class ChatEditorOverlayController implements IEditorContribution { static readonly ID = 'editor.contrib.chatOverlayController'; diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 7d86a35629735..efa9304c9742f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -32,12 +32,13 @@ export interface IChatQuotasService { readonly quotas: IChatQuotas; acceptQuotas(quotas: IChatQuotas): void; + clearQuotas(): void; } export interface IChatQuotas { - readonly chatQuotaExceeded: boolean; - readonly completionsQuotaExceeded: boolean; - readonly quotaResetDate: Date; + chatQuotaExceeded: boolean; + completionsQuotaExceeded: boolean; + quotaResetDate: Date | undefined; } export const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog'; @@ -49,7 +50,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService private readonly _onDidChangeQuotas = this._register(new Emitter()); readonly onDidChangeQuotas: Event = this._onDidChangeQuotas.event; - private _quotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: new Date(0) }; + private _quotas: IChatQuotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }; get quotas(): IChatQuotas { return this._quotas; } private readonly chatQuotaExceededContextKey = ChatContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService); @@ -113,9 +114,12 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService id: MenuId.ChatCommandCenter, group: 'a_first', order: 1, - when: ContextKeyExpr.or( - ChatContextKeys.chatQuotaExceeded, - ChatContextKeys.completionsQuotaExceeded + when: ContextKeyExpr.and( + ChatContextKeys.Setup.installed, + ContextKeyExpr.or( + ChatContextKeys.chatQuotaExceeded, + ChatContextKeys.completionsQuotaExceeded + ) ) } }); @@ -169,7 +173,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService ], custom: { closeOnLinkClick: true, - icon: Codicon.copilotWarning, + icon: Codicon.copilotWarningLarge, markdownDetails: [ { markdown: new MarkdownString(message, true) }, { markdown: new MarkdownString(upgradeToPro, true) } @@ -221,6 +225,12 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService this._onDidChangeQuotas.fire(); } + clearQuotas(): void { + if (this.quotas.chatQuotaExceeded || this.quotas.completionsQuotaExceeded) { + this.acceptQuotas({ chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }); + } + } + private updateContextKeys(): void { this.chatQuotaExceededContextKey.set(this._quotas.chatQuotaExceeded); this.completionsQuotaExceededContextKey.set(this._quotas.completionsQuotaExceeded); @@ -231,6 +241,8 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo static readonly ID = 'chat.quotasStatusBarEntry'; + private static readonly COPILOT_STATUS_ID = 'GitHub.copilot.status'; // TODO@bpasero unify into 1 core indicator + private readonly _entry = this._register(new MutableDisposable()); constructor( @@ -239,24 +251,44 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo ) { super(); - this._register(this.chatQuotasService.onDidChangeQuotas(() => this.updateStatusbarEntry())); + this._register(Event.runAndSubscribe(this.chatQuotasService.onDidChangeQuotas, () => this.updateStatusbarEntry())); } private updateStatusbarEntry(): void { const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas; + + // Some quota exceeded, show indicator if (chatQuotaExceeded || completionsQuotaExceeded) { - // Some quota exceeded, show indicator + let text: string; + if (chatQuotaExceeded && !completionsQuotaExceeded) { + text = localize('chatQuotaExceededStatus', "Chat limit reached"); + } else if (completionsQuotaExceeded && !chatQuotaExceeded) { + text = localize('completionsQuotaExceededStatus', "Completions limit reached"); + } else { + text = localize('chatAndCompletionsQuotaExceededStatus', "Copilot limit reached"); + } + + const isCopilotStatusVisible = this.statusbarService.isEntryVisible(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID); + if (!isCopilotStatusVisible) { + text = `$(copilot-warning) ${text}`; + } + this._entry.value = this.statusbarService.addEntry({ - name: localize('indicator', "Copilot Quota Indicator"), - text: localize('limitReached', "Copilot Limit Reached"), - ariaLabel: localize('copilotQuotaExceeded', "Copilot Limit Reached"), + name: localize('indicator', "Copilot Limit Indicator"), + text, + ariaLabel: text, command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, - kind: 'prominent', showInAllWindows: true, - tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }), - }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { id: 'GitHub.copilot.status', alignment: StatusbarAlignment.RIGHT }); // TODO@bpasero unify into 1 core indicator - } else { - // No quota exceeded, remove indicator + tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }) + }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { + id: ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, + alignment: StatusbarAlignment.RIGHT, + compact: isCopilotStatusVisible + }); + } + + // No quota exceeded, remove indicator + else { this._entry.clear(); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 916bb9b66ee31..27c17e0617254 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -56,6 +56,7 @@ import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatView import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; import { IChatQuotasService } from './chatQuotasService.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { mainWindow } from '../../../../base/browser/window.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -94,6 +95,25 @@ const ASK_FOR_PUBLIC_CODE_MATCHES = false; // TODO@bpasero revisit this const TRIGGER_SETUP_COMMAND_ID = 'workbench.action.chat.triggerSetup'; const TRIGGER_SETUP_COMMAND_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."); +export const SetupWelcomeViewKeys = new Set([ChatContextKeys.Setup.triggered.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); +export const SetupWelcomeViewCondition = ContextKeyExpr.and( + ContextKeyExpr.has('config.chat.experimental.offerSetup'), + ContextKeyExpr.or( + ContextKeyExpr.and( + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.installed.negate() + ), + ContextKeyExpr.and( + ChatContextKeys.Setup.canSignUp, + ChatContextKeys.Setup.installed + ), + ContextKeyExpr.and( + ChatContextKeys.Setup.signedOut, + ChatContextKeys.Setup.installed + ) + ) +)!; + export class ChatSetupContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.chat.setup'; @@ -119,24 +139,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr private registerChatWelcome(): void { Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ title: localize('welcomeChat', "Welcome to Copilot"), - when: ContextKeyExpr.and( - ContextKeyExpr.has('config.chat.experimental.offerSetup'), - ContextKeyExpr.or( - ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed.negate() - ), - ContextKeyExpr.and( - ChatContextKeys.Setup.canSignUp, - ChatContextKeys.Setup.installed - ), - ContextKeyExpr.and( - ChatContextKeys.Setup.signedOut, - ChatContextKeys.Setup.installed - ) - ) - )!, - icon: Codicon.copilot, + when: SetupWelcomeViewCondition, + icon: Codicon.copilotLarge, content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.controller.value, this.context)).element, }); } @@ -176,13 +180,12 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr await that.context.update({ triggered: true }); - showCopilotView(viewsService); + showCopilotView(viewsService, layoutService); ensureSideBarChatViewSize(400, viewDescriptorService, layoutService); - // Setup should be kicked off immediately - if (typeof startSetup === 'boolean' && startSetup) { + if (startSetup === true && !ASK_FOR_PUBLIC_CODE_MATCHES) { const controller = that.controller.value; - controller.setup({ publicCodeSuggestions: true }); // TODO@sbatten pass in as argument + controller.setup({ publicCodeSuggestions: true }); } configurationService.updateValue('chat.commandCenter.enabled', true); @@ -339,6 +342,15 @@ class ChatSetupRequests extends Disposable { this.resolve(); } })); + + this._register(this.context.onDidChange(() => { + if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Unknown) { + // When the extension is not installed or the user is not entitled + // make sure to clear quotas so that any indicators are also gone + this.state = { entitlement: this.state.entitlement, quotas: undefined }; + this.chatQuotasService.clearQuotas(); + } + })); } private async resolve(): Promise { @@ -500,7 +512,7 @@ class ChatSetupRequests extends Disposable { this.chatQuotasService.acceptQuotas({ chatQuotaExceeded: typeof state.quotas.chat === 'number' ? state.quotas.chat <= 0 : false, completionsQuotaExceeded: typeof state.quotas.completions === 'number' ? state.quotas.completions <= 0 : false, - quotaResetDate: state.quotas.resetDate ? new Date(state.quotas.resetDate) : new Date(0) + quotaResetDate: state.quotas.resetDate ? new Date(state.quotas.resetDate) : undefined }); } } @@ -604,7 +616,8 @@ class ChatSetupController extends Disposable { @IProgressService private readonly progressService: IProgressService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IActivityService private readonly activityService: IActivityService, - @ICommandService private readonly commandService: ICommandService + @ICommandService private readonly commandService: ICommandService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(); @@ -680,7 +693,7 @@ class ChatSetupController extends Disposable { let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; try { - showCopilotView(this.viewsService); + showCopilotView(this.viewsService, this.layoutService); session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes[0]); entitlement = await this.requests.forceResolveEntitlement(session); } catch (error) { @@ -702,7 +715,7 @@ class ChatSetupController extends Disposable { const wasInstalled = this.context.state.installed; let didSignUp = false; try { - showCopilotView(this.viewsService); + showCopilotView(this.viewsService, this.layoutService); if (entitlement !== ChatEntitlement.Limited && entitlement !== ChatEntitlement.Pro && entitlement !== ChatEntitlement.Unavailable) { didSignUp = await this.requests.signUpLimited(session, options); @@ -735,8 +748,9 @@ class ChatSetupController extends Disposable { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); - if (activeElement === getActiveElement()) { - (await showCopilotView(this.viewsService))?.focusInput(); + const currentActiveElement = getActiveElement(); + if (activeElement === currentActiveElement || currentActiveElement === mainWindow.document.body) { + (await showCopilotView(this.viewsService, this.layoutService))?.focusInput(); } } } @@ -940,7 +954,7 @@ class ChatSetupContext extends Disposable { super(); this.checkExtensionInstallation(); - this.updateContext(); + this.updateContextSync(); } private async checkExtensionInstallation(): Promise { @@ -1001,6 +1015,10 @@ class ChatSetupContext extends Disposable { private async updateContext(): Promise { await this.updateBarrier?.wait(); + this.updateContextSync(); + } + + private updateContextSync(): void { this.logService.trace(`[chat setup] updateContext(): ${JSON.stringify(this._state)}`); if (this._state.triggered && !this._state.installed) { @@ -1047,7 +1065,14 @@ function isCopilotEditsViewActive(viewsService: IViewsService): boolean { return viewsService.getFocusedView()?.id === EditsViewId; } -function showCopilotView(viewsService: IViewsService): Promise { +function showCopilotView(viewsService: IViewsService, layoutService: IWorkbenchLayoutService): Promise { + + // Ensure main window is in front + if (layoutService.activeContainer !== layoutService.mainContainer) { + layoutService.mainContainer.focus(); + } + + // Bring up the correct view if (isCopilotEditsViewActive(viewsService)) { return showEditsView(viewsService); } else { diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index d4ea33c62ab18..485adf05ab9dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $ } from '../../../../base/browser/dom.js'; +import { $, getWindow } from '../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; @@ -14,6 +14,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -29,6 +30,7 @@ import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatModelInitState, IChatModel } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; import { IChatService } from '../common/chatService.js'; +import { SetupWelcomeViewCondition, SetupWelcomeViewKeys } from './chatSetup.js'; import { ChatWidget, IChatViewState } from './chatWidget.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js'; @@ -66,6 +68,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { @IChatService private readonly chatService: IChatService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @ILogService private readonly logService: ILogService, + @ILayoutService private readonly layoutService: ILayoutService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); @@ -99,6 +102,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._onDidChangeViewWelcomeState.fire(); })); + + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(SetupWelcomeViewKeys)) { + this._onDidChangeViewWelcomeState.fire(); + } + })); } override getActionsContext(): IChatViewTitleActionContext | undefined { @@ -130,10 +139,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } override shouldShowWelcome(): boolean { + const showSetup = this.contextKeyService.contextMatchesRules(SetupWelcomeViewCondition); const noPersistedSessions = !this.chatService.hasSessions(); - const shouldShow = this.didUnregisterProvider || !this._widget?.viewModel && noPersistedSessions || this.defaultParticipantRegistrationFailed; - this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: didUnregister=${this.didUnregisterProvider} || noViewModel:${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions} || defaultParticipantRegistrationFailed=${this.defaultParticipantRegistrationFailed}`); - return shouldShow; + const shouldShow = this.didUnregisterProvider || !this._widget?.viewModel && noPersistedSessions || this.defaultParticipantRegistrationFailed || showSetup; + this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: didUnregister=${this.didUnregisterProvider} || noViewModel:${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions} || defaultParticipantRegistrationFailed=${this.defaultParticipantRegistrationFailed} || showSetup=${showSetup}`); + return !!shouldShow; } private getSessionId() { @@ -155,7 +165,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); const locationBasedColors = this.getLocationBasedColors(); - const editorOverflowNode = $('.chat-editor-overflow-widgets'); + const editorOverflowNode = this.layoutService.getContainer(getWindow(parent)).appendChild($('.chat-editor-overflow.monaco-editor')); + this._register({ dispose: () => editorOverflowNode.remove() }); + this._widget = this._register(scopedInstantiationService.createInstance( ChatWidget, this.chatOptions.location, @@ -172,7 +184,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { }, }, enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, - // editorOverflowWidgetsDomNode: editorOverflowNode, + editorOverflowWidgetsDomNode: editorOverflowNode, }, { listForeground: SIDE_BAR_FOREGROUND, @@ -187,7 +199,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { })); this._register(this._widget.onDidClear(() => this.clear())); this._widget.render(parent); - parent.appendChild(editorOverflowNode); const sessionId = this.getSessionId(); const disposeListener = this._register(this.chatService.onDidDisposeSession((e) => { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index a8831878ddb8a..800e82d952eb1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -15,6 +15,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; +import { autorun } from '../../../../base/common/observable.js'; import { extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -250,7 +251,11 @@ export class ChatWidget extends Disposable implements IChatWidget { const chatEditingSessionDisposables = this._register(new DisposableStore()); - this._register(this.chatEditingService.onDidCreateEditingSession((session) => { + this._register(autorun(r => { + const session = this.chatEditingService.currentEditingSessionObs.read(r); + if (!session) { + return; + } if (session.chatSessionId !== this.viewModel?.sessionId) { // this chat editing session is for a different chat widget return; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index ff1938c5b7b04..3f4045b9ce099 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -7,10 +7,11 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; +import { autorun } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ChatEditingSessionChangeType, IChatEditingService, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionChangeType, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatWidgetService } from '../chat.js'; export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution { @@ -25,10 +26,12 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben ) { super(); - this._handleNewEditingSession(); - this._register(this.chatEditingService.onDidCreateEditingSession(() => { + this._register(autorun(r => { this.chatEditingSessionDisposables.clear(); - this._handleNewEditingSession(); + const session = this.chatEditingService.currentEditingSessionObs.read(r); + if (session) { + this._handleNewEditingSession(session); + } })); } @@ -95,11 +98,8 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben } - private _handleNewEditingSession() { - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); - if (!currentEditingSession) { - return; - } + private _handleNewEditingSession(currentEditingSession: IChatEditingSession) { + const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); if (!widget || widget.viewModel?.sessionId !== currentEditingSession.chatSessionId) { return; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index fc60c5f5dbb06..502ef69725e48 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -312,8 +312,11 @@ top: 2px; } - /* But codicons in toolbars and other widgets assume the natural position of the codicon */ - .chat-codeblock-pill-widget .codicon, + .chat-codeblock-pill-widget .codicon { + top: -1px; + } + + /* But codicons in toolbars assume the natural position of the codicon */ .monaco-toolbar .codicon { position: initial; top: initial; @@ -696,12 +699,16 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-button-secondaryForeground); } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:hover, .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary:hover { background-color: var(--vscode-button-secondaryHoverBackground); color: var(--vscode-button-secondaryForeground); } +/* The Add Files button is currently implemented as a secondary button but should not have the secondary button background */ +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary.monaco-text-button.codicon:not(.disabled):hover, .interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button:hover { background-color: var(--vscode-toolbar-hoverBackground); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index e102d7525e95a..c0598e39ebabd 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -23,7 +23,6 @@ export interface IChatEditingService { _serviceBrand: undefined; - readonly onDidCreateEditingSession: Event; /** * emitted when a session is created, changed or disposed */ diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 5951803d0ae1d..4de8cc1c24455 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -562,7 +562,9 @@ export class ExplorerFindProvider implements IAsyncFindProvider { const tree = this.treeProvider(); for (const directory of highlightedDirectories) { - tree.rerender(directory); + if (tree.hasNode(directory)) { + tree.rerender(directory); + } } } @@ -602,7 +604,10 @@ export class ExplorerFindProvider implements IAsyncFindProvider { const searchExcludePattern = getExcludes(this.configurationService.getValue({ resource: root.resource })) || {}; const searchOptions: IFileQuery = { - folderQueries: [{ folder: root.resource }], + folderQueries: [{ + folder: root.resource, + disregardIgnoreFiles: !this.configurationService.getValue('explorer.excludeGitIgnore'), + }], type: QueryType.File, shouldGlobMatchFilePattern: true, cacheKey: `explorerfindprovider:${root.name}:${rootIndex}:${this.sessionId}`, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index 5f48988ff0626..43e73454efd11 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -16,7 +16,7 @@ import { Range } from '../../../../editor/common/core/range.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { AbstractInlineChatAction } from './inlineChatActions.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { InjectedTextCursorStops, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; +import { IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; import { URI } from '../../../../base/common/uri.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js'; @@ -33,9 +33,11 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { toAction } from '../../../../base/common/actions.js'; import { IMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { Event } from '../../../../base/common/event.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js'; +import { createStyleSheet2 } from '../../../../base/browser/domStylesheets.js'; +import { stringValue } from '../../../../base/browser/cssValue.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); @@ -149,12 +151,6 @@ export class ShowInlineChatHintAction extends EditorAction2 { } } -class HintData { - constructor( - readonly setting: string - ) { } -} - export class InlineChatHintsController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.inlineChatHints'; @@ -193,8 +189,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont if (e.target.type !== MouseTargetType.CONTENT_TEXT) { return; } - const attachedData = e.target.detail.injectedText?.options.attachedData; - if (!(attachedData instanceof HintData)) { + if (!e.target.element?.classList.contains('inline-chat-hint-text')) { return; } if (e.event.leftButton) { @@ -202,7 +197,10 @@ export class InlineChatHintsController extends Disposable implements IEditorCont this.hide(); } else if (e.event.rightButton) { e.event.preventDefault(); - this._showContextMenu(e.event, attachedData.setting); + this._showContextMenu(e.event, e.target.element?.classList.contains('whitespace') + ? InlineChatConfigKeys.LineEmptyHint + : InlineChatConfigKeys.LineNLHint + ); } })); @@ -210,10 +208,9 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const decos = this._editor.createDecorationsCollection(); const editorObs = observableCodeEditor(editor); - const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel()); - - const configSignal = observableFromEvent(Event.filter(_configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(InlineChatConfigKeys.LineEmptyHint) || e.affectsConfiguration(InlineChatConfigKeys.LineNLHint)), () => undefined); + const configHintEmpty = observableConfigValue(InlineChatConfigKeys.LineEmptyHint, false, this._configurationService); + const configHintNL = observableConfigValue(InlineChatConfigKeys.LineNLHint, false, this._configurationService); const showDataObs = derived(r => { const ghostState = ghostCtrl?.model.read(r)?.state.read(r); @@ -224,8 +221,6 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const kb = keyObs.read(r); - configSignal.read(r); - if (ghostState !== undefined || !kb || !position || !model || !textFocus) { return undefined; } @@ -239,18 +234,21 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0 && position.column > 1; if (isWhitespace) { - return _configurationService.getValue(InlineChatConfigKeys.LineEmptyHint) + return configHintEmpty.read(r) ? { isEol, isWhitespace, kb, position, model } : undefined; } - if (visible && isEol && _configurationService.getValue(InlineChatConfigKeys.LineNLHint)) { + if (visible && isEol && configHintNL.read(r)) { return { isEol, isWhitespace, kb, position, model }; } return undefined; }); + const style = createStyleSheet2(); + this._store.add(style); + this._store.add(autorun(r => { const showData = showDataObs.read(r); @@ -264,7 +262,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.name ?? localize('defaultTitle', "Chat"); const { position, isEol, isWhitespace, kb, model } = showData; - const inlineClassName: string[] = ['inline-chat-hint']; + const inlineClassName: string[] = ['a' /*HACK but sorts as we want*/, 'inline-chat-hint', 'inline-chat-hint-text']; let content: string; if (isWhitespace) { content = '\u00a0' + localize('title2', "{0} to edit with {1}", kb, agentName); @@ -275,6 +273,11 @@ export class InlineChatHintsController extends Disposable implements IEditorCont inlineClassName.push('embedded'); } + style.setStyle(`.inline-chat-hint-text::after { content: ${stringValue(content)} }`); + if (isWhitespace) { + inlineClassName.push('whitespace'); + } + this._ctxShowingHint.set(true); decos.set([{ @@ -283,13 +286,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont description: 'inline-chat-hint-line', showIfCollapsed: true, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - after: { - content, - inlineClassName: inlineClassName.join(' '), - inlineClassNameAffectsLetterSpacing: true, - cursorStops: InjectedTextCursorStops.None, - attachedData: new HintData(isWhitespace ? InlineChatConfigKeys.LineEmptyHint : InlineChatConfigKeys.LineNLHint) - } + afterContentClassName: inlineClassName.join(' '), } }]); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index d82d9e99ddd32..131cb75876625 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, Dimension, getActiveElement, getTotalHeight, h, reset, trackFocus } from '../../../../base/browser/dom.js'; +import { $, Dimension, getActiveElement, getTotalHeight, getWindow, h, reset, trackFocus } from '../../../../base/browser/dom.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; @@ -520,7 +520,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IHoverService hoverService: IHoverService, @ILayoutService layoutService: ILayoutService ) { - const overflowWidgetsNode = layoutService.mainContainer.appendChild($('.inline-chat-overflow.monaco-editor')); + const overflowWidgetsNode = layoutService.getContainer(getWindow(_parentEditor.getContainerDomNode())).appendChild($('.inline-chat-overflow.monaco-editor')); super(location, { ...options, chatWidgetViewOptions: { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index feaba4ca832c7..7d07547770ee6 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -70,6 +70,7 @@ import { IChatEditingService, IChatEditingSession } from '../../../chat/common/c import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js'; +import { IObservable, observableValue } from '../../../../../base/common/observable.js'; suite('InlineChatController', function () { @@ -162,7 +163,7 @@ suite('InlineChatController', function () { [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], [IChatEditingService, new class extends mock() { - override onDidCreateEditingSession: Event = Event.None; + override currentEditingSessionObs: IObservable = observableValue(this, null); }], [IInlineChatSavingService, new class extends mock() { override markChanged(session: Session): void { diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 098df9c8c1b72..7ca28f94265d1 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js'; import './media/dirtydiffDecorator.css'; import { ThrottledDelayer } from '../../../../base/common/async.js'; -import { IDisposable, dispose, toDisposable, Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { IDisposable, dispose, toDisposable, Disposable, DisposableStore, DisposableMap } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import * as ext from '../../../common/contributions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -57,12 +57,13 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IQuickDiffService, QuickDiff, QuickDiffResult } from '../common/quickDiff.js'; +import { IQuickDiffService, QuickDiff, QuickDiffChange, QuickDiffResult } from '../common/quickDiff.js'; import { IQuickDiffSelectItem, SwitchQuickDiffBaseAction, SwitchQuickDiffViewItem } from './dirtyDiffSwitcher.js'; import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; -import { lineRangeMappingFromChanges } from '../../../../editor/common/diff/rangeMapping.js'; +import { LineRangeMapping, lineRangeMappingFromChange } from '../../../../editor/common/diff/rangeMapping.js'; import { DiffState } from '../../../../editor/browser/widget/diffEditor/diffEditorViewModel.js'; import { toLineChanges } from '../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; +import { Iterable } from '../../../../base/common/iterator.js'; class DiffActionRunner extends ActionRunner { @@ -204,8 +205,10 @@ class DirtyDiffWidget extends PeekViewWidget { this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this)); this._applyTheme(themeService.getColorTheme()); - if (this.model.original.length > 0) { - contextKeyService = contextKeyService.createOverlay([['originalResourceScheme', this.model.original[0].uri.scheme], ['originalResourceSchemes', this.model.original.map(original => original.uri.scheme)]]); + if (!Iterable.isEmpty(this.model.originalTextModels)) { + contextKeyService = contextKeyService.createOverlay([ + ['originalResourceScheme', Iterable.first(this.model.originalTextModels)?.uri.scheme], + ['originalResourceSchemes', Iterable.map(this.model.originalTextModels, textModel => textModel.uri.scheme)]]); } this.create(); @@ -234,15 +237,13 @@ class DirtyDiffWidget extends PeekViewWidget { const labeledChange = this.model.changes[index]; const change = labeledChange.change; this._index = index; - this.contextKeyService.createKey('originalResourceScheme', this.model.changes[index].uri.scheme); + this.contextKeyService.createKey('originalResourceScheme', this.model.changes[index].original.scheme); this.updateActions(); this._provider = labeledChange.label; this.change = change; - const originalModel = this.model.original; - - if (!originalModel) { + if (Iterable.isEmpty(this.model.originalTextModels)) { return; } @@ -252,7 +253,7 @@ class DirtyDiffWidget extends PeekViewWidget { // non-side-by-side diff still hasn't created the view zones onFirstDiffUpdate(() => setTimeout(() => this.revealChange(change), 0)); - const diffEditorModel = this.model.getDiffEditorModel(labeledChange.uri.toString()); + const diffEditorModel = this.model.getDiffEditorModel(labeledChange.original); if (!diffEditorModel) { return; } @@ -291,7 +292,7 @@ class DirtyDiffWidget extends PeekViewWidget { } private renderTitle(label: string): void { - const providerChanges = this.model.mapChanges.get(label)!; + const providerChanges = this.model.quickDiffChanges.get(label)!; const providerIndex = providerChanges.indexOf(this._index); let detail: string; @@ -336,16 +337,8 @@ class DirtyDiffWidget extends PeekViewWidget { } private shouldUseDropdown(): boolean { - let providersWithChangesCount = 0; - if (this.model.mapChanges.size > 1) { - const keys = Array.from(this.model.mapChanges.keys()); - for (let i = 0; (i < keys.length) && (providersWithChangesCount <= 1); i++) { - if (this.model.mapChanges.get(keys[i])!.length > 0) { - providersWithChangesCount++; - } - } - } - return providersWithChangesCount >= 2; + return this.model.getQuickDiffResults() + .filter(quickDiff => quickDiff.changes.length > 0).length > 1; } private updateActions(): void { @@ -787,7 +780,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu if (this.editor.hasModel() && (typeof lineNumber === 'number' || !this.widget.provider)) { index = this.model.findNextClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber, true, this.widget.provider); } else { - const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value!; + const providerChanges: number[] = this.model.quickDiffChanges.get(this.widget.provider) ?? this.model.quickDiffChanges.values().next().value!; const mapIndex = providerChanges.findIndex(value => value === this.widget!.index); index = providerChanges[rot(mapIndex + 1, providerChanges.length)]; } @@ -807,7 +800,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu if (this.editor.hasModel() && (typeof lineNumber === 'number')) { index = this.model.findPreviousClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber, true, this.widget.provider); } else { - const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value!; + const providerChanges: number[] = this.model.quickDiffChanges.get(this.widget.provider) ?? this.model.quickDiffChanges.values().next().value!; const mapIndex = providerChanges.findIndex(value => value === this.widget!.index); index = providerChanges[rot(mapIndex - 1, providerChanges.length)]; } @@ -879,7 +872,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu return true; } - private onDidModelChange(splices: ISplice[]): void { + private onDidModelChange(splices: ISplice[]): void { if (!this.model || !this.widget || this.widget.hasFocus()) { return; } @@ -1210,29 +1203,34 @@ export async function getOriginalResource(quickDiffService: IQuickDiffService, u return quickDiffs.length > 0 ? quickDiffs[0].originalResource : null; } -type LabeledChange = { change: IChange; label: string; uri: URI }; - export class DirtyDiffModel extends Disposable { - private _quickDiffs: QuickDiff[] = []; - private _originalModels: Map = new Map(); // key is uri.toString() - private _originalTextModels: ITextModel[] = []; private _model: ITextFileEditorModel; - get original(): ITextModel[] { return this._originalTextModels; } - private diffDelayer = new ThrottledDelayer(200); - private _quickDiffsPromise?: Promise; - private repositoryDisposables = new Set(); - private readonly originalModelDisposables = this._register(new DisposableStore()); + private readonly _originalEditorModels = new ResourceMap(); + private readonly _originalEditorModelsDisposables = this._register(new DisposableStore()); + get originalTextModels(): Iterable { + return Iterable.map(this._originalEditorModels.values(), editorModel => editorModel.textEditorModel); + } + private _disposed = false; + private _quickDiffs: QuickDiff[] = []; + private _quickDiffsPromise?: Promise; + private _diffDelayer = new ThrottledDelayer(200); + + private readonly _onDidChange = new Emitter<{ changes: QuickDiffChange[]; diff: ISplice[] }>(); + readonly onDidChange: Event<{ changes: QuickDiffChange[]; diff: ISplice[] }> = this._onDidChange.event; - private readonly _onDidChange = new Emitter<{ changes: LabeledChange[]; diff: ISplice[] }>(); - readonly onDidChange: Event<{ changes: LabeledChange[]; diff: ISplice[] }> = this._onDidChange.event; + private _changes: QuickDiffChange[] = []; + get changes(): QuickDiffChange[] { return this._changes; } - private _changes: LabeledChange[] = []; - get changes(): LabeledChange[] { return this._changes; } - private _mapChanges: Map = new Map(); // key is the quick diff name, value is the index of the change in this._changes - get mapChanges(): Map { return this._mapChanges; } + /** + * Map of quick diff name to the index of the change in `this.changes` + */ + private _quickDiffChanges: Map = new Map(); + get quickDiffChanges(): Map { return this._quickDiffChanges; } + + private readonly _repositoryDisposables = new DisposableMap(); constructor( textFileModel: IResolvedTextFileEditorModel, @@ -1260,10 +1258,9 @@ export class DirtyDiffModel extends Disposable { } this._register(this._model.onDidChangeEncoding(() => { - this.diffDelayer.cancel(); + this._diffDelayer.cancel(); this._quickDiffs = []; - this._originalModels.clear(); - this._originalTextModels = []; + this._originalEditorModels.clear(); this._quickDiffsPromise = undefined; this.setChanges([], new Map()); this.triggerDiff(); @@ -1280,65 +1277,56 @@ export class DirtyDiffModel extends Disposable { public getQuickDiffResults(): QuickDiffResult[] { return this._quickDiffs.map(quickDiff => { - const changes = this._changes - .filter(change => change.label === quickDiff.label) - .map(change => change.change); + const changes = this.changes + .filter(change => change.label === quickDiff.label); - // Convert IChange[] to LineRangeMapping[] - const lineRangeMappings = lineRangeMappingFromChanges(changes); return { + label: quickDiff.label, original: quickDiff.originalResource, modified: this._model.resource, - changes: lineRangeMappings + changes: changes.map(change => change.change), + changes2: changes.map(change => change.change2) }; }); } - public getDiffEditorModel(originalUri: string): IDiffEditorModel | undefined { - if (!this._originalModels.has(originalUri)) { - return; - } - const original = this._originalModels.get(originalUri)!; - - return { - modified: this._model.textEditorModel!, - original: original.textEditorModel - }; + public getDiffEditorModel(originalUri: URI): IDiffEditorModel | undefined { + const editorModel = this._originalEditorModels.get(originalUri); + return editorModel ? + { + modified: this._model.textEditorModel!, + original: editorModel.textEditorModel + } : undefined; } private onDidAddRepository(repository: ISCMRepository): void { const disposables = new DisposableStore(); - this.repositoryDisposables.add(disposables); - disposables.add(toDisposable(() => this.repositoryDisposables.delete(disposables))); - disposables.add(repository.provider.onDidChangeResources(this.triggerDiff, this)); - const onDidRemoveThis = Event.filter(this.scmService.onDidRemoveRepository, r => r === repository); - disposables.add(onDidRemoveThis(() => dispose(disposables), null)); + const onDidRemoveRepository = Event.filter(this.scmService.onDidRemoveRepository, r => r === repository); + disposables.add(onDidRemoveRepository(() => this._repositoryDisposables.deleteAndDispose(repository))); + + this._repositoryDisposables.set(repository, disposables); this.triggerDiff(); } private triggerDiff(): void { - if (!this.diffDelayer) { + if (!this._diffDelayer) { return; } - this.diffDelayer + this._diffDelayer .trigger(async () => { - const result: { changes: LabeledChange[]; mapChanges: Map } | null = await this.diff(); + const result: { changes: QuickDiffChange[]; mapChanges: Map } | null = await this.diff(); - const originalModels = Array.from(this._originalModels.values()); - if (!result || this._disposed || this._model.isDisposed() || originalModels.some(originalModel => originalModel.isDisposed())) { + const editorModels = Array.from(this._originalEditorModels.values()); + if (!result || this._disposed || this._model.isDisposed() || editorModels.some(editorModel => editorModel.isDisposed())) { return; // disposed } - if (originalModels.every(originalModel => originalModel.textEditorModel.getValueLength() === 0)) { - result.changes = []; - } - - if (!result.changes) { + if (editorModels.every(editorModel => editorModel.textEditorModel.getValueLength() === 0)) { result.changes = []; } @@ -1347,14 +1335,14 @@ export class DirtyDiffModel extends Disposable { .catch(err => onUnexpectedError(err)); } - private setChanges(changes: LabeledChange[], mapChanges: Map): void { - const diff = sortedDiff(this._changes, changes, (a, b) => compareChanges(a.change, b.change)); + private setChanges(changes: QuickDiffChange[], mapChanges: Map): void { + const diff = sortedDiff(this.changes, changes, (a, b) => compareChanges(a.change, b.change)); this._changes = changes; - this._mapChanges = mapChanges; + this._quickDiffChanges = mapChanges; this._onDidChange.fire({ changes, diff }); } - private diff(): Promise<{ changes: LabeledChange[]; mapChanges: Map } | null> { + private diff(): Promise<{ changes: QuickDiffChange[]; mapChanges: Map } | null> { return this.progressService.withProgress({ location: ProgressLocation.Scm, delay: 250 }, async () => { const originalURIs = await this.getQuickDiffsPromise(); if (this._disposed || this._model.isDisposed() || (originalURIs.length === 0)) { @@ -1371,14 +1359,18 @@ export class DirtyDiffModel extends Disposable { ? this.configurationService.getValue('diffEditor.ignoreTrimWhitespace') : ignoreTrimWhitespaceSetting !== 'false'; - const allDiffs: LabeledChange[] = []; + const allDiffs: QuickDiffChange[] = []; for (const quickDiff of filteredToDiffable) { const dirtyDiff = await this._diff(quickDiff.originalResource, this._model.resource, ignoreTrimWhitespace); - if (dirtyDiff) { - for (const diff of dirtyDiff) { - if (diff) { - allDiffs.push({ change: diff, label: quickDiff.label, uri: quickDiff.originalResource }); - } + if (dirtyDiff.changes && dirtyDiff.changes2 && dirtyDiff.changes.length === dirtyDiff.changes2.length) { + for (let index = 0; index < dirtyDiff.changes.length; index++) { + allDiffs.push({ + label: quickDiff.label, + original: quickDiff.originalResource, + modified: this._model.resource, + change: dirtyDiff.changes[index], + change2: dirtyDiff.changes2[index] + }); } } } @@ -1395,22 +1387,19 @@ export class DirtyDiffModel extends Disposable { }); } - private async _diff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { + private async _diff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<{ changes: readonly IChange[] | null; changes2: readonly LineRangeMapping[] | null }> { if (this.algorithm === undefined) { - return this.editorWorkerService.computeDirtyDiff(original, modified, ignoreTrimWhitespace); + const changes = await this.editorWorkerService.computeDirtyDiff(original, modified, ignoreTrimWhitespace); + return { changes, changes2: changes?.map(change => lineRangeMappingFromChange(change)) ?? null }; } - const diffResult = await this.editorWorkerService.computeDiff(original, modified, { + const result = await this.editorWorkerService.computeDiff(original, modified, { computeMoves: false, ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER }, this.algorithm); - if (!diffResult) { - return null; - } - - return toLineChanges(DiffState.fromDiffResult(diffResult)); + return { changes: result ? toLineChanges(DiffState.fromDiffResult(result)) : null, changes2: result?.changes ?? null }; } private getQuickDiffsPromise(): Promise { @@ -1425,8 +1414,7 @@ export class DirtyDiffModel extends Disposable { if (quickDiffs.length === 0) { this._quickDiffs = []; - this._originalModels.clear(); - this._originalTextModels = []; + this._originalEditorModels.clear(); return []; } @@ -1434,10 +1422,10 @@ export class DirtyDiffModel extends Disposable { return quickDiffs; } - this.originalModelDisposables.clear(); - this._originalModels.clear(); - this._originalTextModels = []; this._quickDiffs = quickDiffs; + + this._originalEditorModels.clear(); + this._originalEditorModelsDisposables.clear(); return (await Promise.all(quickDiffs.map(async (quickDiff) => { try { const ref = await this.textModelResolverService.createModelReference(quickDiff.originalResource); @@ -1446,8 +1434,7 @@ export class DirtyDiffModel extends Disposable { return []; } - this._originalModels.set(quickDiff.originalResource.toString(), ref.object); - this._originalTextModels.push(ref.object.textEditorModel); + this._originalEditorModels.set(quickDiff.originalResource, ref.object); if (isTextFileEditorModel(ref.object)) { const encoding = this._model.getEncoding(); @@ -1457,8 +1444,8 @@ export class DirtyDiffModel extends Disposable { } } - this.originalModelDisposables.add(ref); - this.originalModelDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => this.triggerDiff())); + this._originalEditorModelsDisposables.add(ref); + this._originalEditorModelsDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => this.triggerDiff())); return quickDiff; } catch (error) { @@ -1553,15 +1540,14 @@ export class DirtyDiffModel extends Disposable { } override dispose(): void { - super.dispose(); - this._disposed = true; + this._quickDiffs = []; - this._originalModels.clear(); - this._originalTextModels = []; - this.diffDelayer.cancel(); - this.repositoryDisposables.forEach(d => dispose(d)); - this.repositoryDisposables.clear(); + this._diffDelayer.cancel(); + this._originalEditorModels.clear(); + this._repositoryDisposables.dispose(); + + super.dispose(); } } diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 486f978b768c2..f14d8ea79f67c 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -20,18 +20,6 @@ function actionEquals(a: IAction, b: IAction): boolean { return a.id === b.id; } -const repositoryMenuDisposables = new DisposableStore(); - -MenuRegistry.onDidChangeMenu(e => { - if (e.has(MenuId.SCMTitle)) { - repositoryMenuDisposables.clear(); - - for (const menuItem of MenuRegistry.getMenuItems(MenuId.SCMTitle)) { - repositoryMenuDisposables.add(MenuRegistry.appendMenuItem(MenuId.SCMSourceControlInline, menuItem)); - } - } -}); - export class SCMTitleMenu implements IDisposable { private _actions: IAction[] = []; @@ -244,6 +232,7 @@ export class SCMMenus implements ISCMMenus, IDisposable { readonly titleMenu: SCMTitleMenu; private readonly disposables = new DisposableStore(); + private readonly repositoryMenuDisposables = new DisposableStore(); private readonly menus = new Map void }>(); constructor( @@ -252,6 +241,20 @@ export class SCMMenus implements ISCMMenus, IDisposable { ) { this.titleMenu = instantiationService.createInstance(SCMTitleMenu); scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + + // Duplicate the `SCMTitle` menu items to the `SCMSourceControlInline` menu. We do this + // so that menu items can be independently hidden/shown in the "Source Control" and the + // "Source Control Repositories" views. + MenuRegistry.onDidChangeMenu(e => { + if (!e.has(MenuId.SCMTitle)) { + return; + } + + this.repositoryMenuDisposables.clear(); + for (const menuItem of MenuRegistry.getMenuItems(MenuId.SCMTitle)) { + this.repositoryMenuDisposables.add(MenuRegistry.appendMenuItem(MenuId.SCMSourceControlInline, menuItem)); + } + }, this, this.disposables); } private onDidRemoveRepository(repository: ISCMRepository): void { diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index d6057c14ecb0c..af17ed451bd09 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -1372,7 +1372,7 @@ export class SCMHistoryViewPane extends ViewPane { } isFirstRun = false; })); - }); + }, this, this._store); } protected override layoutBody(height: number, width: number): void { diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 0720dc9f5ff41..3770f071c7219 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -9,6 +9,7 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { LanguageSelector } from '../../../../editor/common/languageSelector.js'; import { Event } from '../../../../base/common/event.js'; import { LineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; +import { IChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; export const IQuickDiffService = createDecorator('quickDiff'); @@ -28,10 +29,20 @@ export interface QuickDiff { visible: boolean; } +export interface QuickDiffChange { + readonly label: string; + readonly original: URI; + readonly modified: URI; + readonly change: IChange; + readonly change2: LineRangeMapping; +} + export interface QuickDiffResult { + readonly label: string; readonly original: URI; readonly modified: URI; - readonly changes: readonly LineRangeMapping[]; + readonly changes: IChange[]; + readonly changes2: LineRangeMapping[]; } export interface IQuickDiffService { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 850d0cb66505c..02350e772581c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -211,7 +211,7 @@ export const startEntries: GettingStartedStartEntryContent = [ const Button = (title: string, href: string) => `[${title}](${href})`; const CopilotStepTitle = localize('gettingStarted.copilotSetup.title', "Use AI features with Copilot for free"); -const CopilotDescription = localize({ key: 'gettingStarted.copilotSetup.description', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "Write code faster and smarter with [Copilot]({0}) for free.", product.defaultChatAgent?.documentationUrl ?? ''); +const CopilotDescription = localize({ key: 'gettingStarted.copilotSetup.description', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "Write code faster and smarter with [Copilot]({0}) for free with your GitHub account.", product.defaultChatAgent?.documentationUrl ?? ''); const CopilotTermsString = localize({ key: 'copilotTerms', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to Copilot [Terms]({0}) and [Privacy Policy]({1}).", product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''); const CopilotSignedOutButton = Button(localize('setupCopilotButton.signIn', "Sign in to use Copilot"), `command:workbench.action.chat.triggerSetup?${encodeURIComponent(JSON.stringify([true]))}`); const CopilotSignedInButton = Button(localize('setupCopilotButton.setup', "Setup Copilot"), `command:workbench.action.chat.triggerSetup?${encodeURIComponent(JSON.stringify([true]))}`); @@ -283,6 +283,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'settings', title: localize('gettingStarted.settings.title', "Tune your settings"), description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), + when: '!config.chat.experimental.offerSetup', media: { type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' }, @@ -291,12 +292,22 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'settingsSync', title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"), description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')), - when: 'syncStatus != uninitialized', + when: '!config.chat.experimental.offerSetup && syncStatus != uninitialized', completionEvents: ['onEvent:sync-enabled'], media: { type: 'svg', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: 'settingsSync.svg' }, }, + { + id: 'settingsAndSync', + title: localize('gettingStarted.settings.title', "Tune your settings"), + description: localize('gettingStarted.settingsAndSync.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. [Back up and sync](command:workbench.userDataSync.actions.turnOn) your essential customizations across all your devices.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), + when: 'config.chat.experimental.offerSetup && syncStatus != uninitialized', + completionEvents: ['onEvent:sync-enabled'], + media: { + type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' + }, + }, { id: 'commandPaletteTask', title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "), @@ -307,7 +318,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'pickAFolderTask-Mac', title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), - when: 'isMac && workspaceFolderCount == 0', + when: '!config.chat.experimental.offerSetup && isMac && workspaceFolderCount == 0', media: { type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' } @@ -316,7 +327,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'pickAFolderTask-Other', title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), - when: '!isMac && workspaceFolderCount == 0', + when: '!config.chat.experimental.offerSetup && !isMac && workspaceFolderCount == 0', media: { type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 5a52c698e2d58..4a850d040d8d2 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -642,7 +642,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { hasMainWindowBorder(): boolean { return false; } getMainWindowBorderRadius(): string | undefined { return undefined; } isVisible(_part: Parts): boolean { return true; } - getContainer(): HTMLElement { return null!; } + getContainer(): HTMLElement { return mainWindow.document.body; } whenContainerStylesLoaded() { return undefined; } isTitleBarHidden(): boolean { return false; } isStatusBarHidden(): boolean { return false; } diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 7c28a98930cf7..b93d91a78e943 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -5,6 +5,8 @@ /*eslint-env mocha*/ +// @ts-check + const fs = require('fs'); (function () { @@ -24,7 +26,7 @@ const fs = require('fs'); function createSpy(element, cnt) { return function (...args) { if (logging) { - console.log(`calling ${element}: ` + args.slice(0, cnt).join(',') + (withStacks ? (`\n` + new Error().stack.split('\n').slice(2).join('\n')) : '')); + console.log(`calling ${element}: ` + args.slice(0, cnt).join(',') + (withStacks ? (`\n` + new Error().stack?.split('\n').slice(2).join('\n')) : '')); } return originals[element].call(this, ...args); }; @@ -88,9 +90,18 @@ Object.assign(globalThis, { const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY; const _tests_glob = '**/test/**/*.test.js'; -let loader; + + +/** + * Loads one or N modules. + * @type {{ + * (module: string|string[]): Promise|Promise; + * _out: string; + * }} + */ +let loadFn; + const _loaderErrors = []; -let _out; function initNls(opts) { if (opts.build) { @@ -101,20 +112,17 @@ function initNls(opts) { } } -function initLoader(opts) { +function initLoadFn(opts) { const outdir = opts.build ? 'out-build' : 'out'; - _out = path.join(__dirname, `../../../${outdir}`); + const out = path.join(__dirname, `../../../${outdir}`); const baseUrl = pathToFileURL(path.join(__dirname, `../../../${outdir}/`)); globalThis._VSCODE_FILE_ROOT = baseUrl.href; // set loader - /** - * @param {string[]} modules - * @param {(...args:any[]) => void} callback - */ - function esmRequire(modules, callback, errorback) { - const tasks = modules.map(mod => { + function importModules(modules) { + const moduleArray = Array.isArray(modules) ? modules : [modules]; + const tasks = moduleArray.map(mod => { const url = new URL(`./${mod}.js`, baseUrl).href; return import(url).catch(err => { console.log(mod, url); @@ -124,35 +132,33 @@ function initLoader(opts) { }); }); - Promise.all(tasks).then(modules => callback(...modules)).catch(errorback); + return Array.isArray(modules) + ? Promise.all(tasks) + : tasks[0]; } - - loader = { require: esmRequire }; + importModules._out = out; + loadFn = importModules; } -function createCoverageReport(opts) { - if (opts.coverage) { - return coverage.createReport(opts.run || opts.runGlob); +async function createCoverageReport(opts) { + if (!opts.coverage) { + return undefined; } - return Promise.resolve(undefined); -} - -function loadWorkbenchTestingUtilsModule() { - return new Promise((resolve, reject) => { - loader.require(['vs/workbench/test/common/utils'], resolve, reject); - }); + return coverage.createReport(opts.run || opts.runGlob); } async function loadModules(modules) { for (const file of modules) { mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha); - const m = await new Promise((resolve, reject) => loader.require([file], resolve, reject)); + const m = await loadFn(file); mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha); mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha); } } -function loadTestModules(opts) { +const globAsync = util.promisify(glob); + +async function loadTestModules(opts) { if (opts.run) { const files = Array.isArray(opts.run) ? opts.run : [opts.run]; @@ -164,17 +170,9 @@ function loadTestModules(opts) { } const pattern = opts.runGlob || _tests_glob; - - return new Promise((resolve, reject) => { - glob(pattern, { cwd: _out }, (err, files) => { - if (err) { - reject(err); - return; - } - const modules = files.map(file => file.replace(/\.js$/, '')); - resolve(modules); - }); - }).then(loadModules); + const files = await globAsync(pattern, { cwd: loadFn._out }); + const modules = files.map(file => file.replace(/\.js$/, '')); + return loadModules(modules); } /** @type Mocha.Test */ @@ -220,7 +218,7 @@ async function loadTests(opts) { console[consoleFn.name] = function (msg) { if (!currentTest) { consoleFn.apply(console, arguments); - } else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title)) { + } else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title ?? '')) { _testsWithUnexpectedOutput = true; consoleFn.apply(console, arguments); } @@ -242,79 +240,74 @@ async function loadTests(opts) { 'Search Model: Search reports timed telemetry on search when error is called' ]); - loader.require(['vs/base/common/errors'], function (errors) { - - const onUnexpectedError = function (err) { - if (err.name === 'Canceled') { - return; // ignore canceled errors that are common - } - - let stack = (err ? err.stack : null); - if (!stack) { - stack = new Error().stack; - } + const errors = await loadFn('vs/base/common/errors'); + const onUnexpectedError = function (err) { + if (err.name === 'Canceled') { + return; // ignore canceled errors that are common + } - _unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack); - }; + let stack = (err ? err.stack : null); + if (!stack) { + stack = new Error().stack; + } - process.on('uncaughtException', error => onUnexpectedError(error)); - process.on('unhandledRejection', (reason, promise) => { - onUnexpectedError(reason); - promise.catch(() => { }); - }); - window.addEventListener('unhandledrejection', event => { - event.preventDefault(); // Do not log to test output, we show an error later when test ends - event.stopPropagation(); + _unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack); + }; - if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) { - onUnexpectedError(event.reason); - } - }); + process.on('uncaughtException', error => onUnexpectedError(error)); + process.on('unhandledRejection', (reason, promise) => { + onUnexpectedError(reason); + promise.catch(() => { }); + }); + window.addEventListener('unhandledrejection', event => { + event.preventDefault(); // Do not log to test output, we show an error later when test ends + event.stopPropagation(); - errors.setUnexpectedErrorHandler(onUnexpectedError); + if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) { + onUnexpectedError(event.reason); + } }); + errors.setUnexpectedErrorHandler(onUnexpectedError); //#endregion - return loadWorkbenchTestingUtilsModule().then((workbenchTestingModule) => { - const assertCleanState = workbenchTestingModule.assertCleanState; + const { assertCleanState } = await loadFn('vs/workbench/test/common/utils'); - suite('Tests are using suiteSetup and setup correctly', () => { - test('assertCleanState - check that registries are clean at the start of test running', () => { - assertCleanState(); - }); + suite('Tests are using suiteSetup and setup correctly', () => { + test('assertCleanState - check that registries are clean at the start of test running', () => { + assertCleanState(); }); + }); - setup(async () => { - await perTestCoverage?.startTest(); - }); + setup(async () => { + await perTestCoverage?.startTest(); + }); - teardown(async () => { - await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle()); + teardown(async () => { + await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle()); - // should not have unexpected output - if (_testsWithUnexpectedOutput && !opts.dev) { - assert.ok(false, 'Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.'); - } + // should not have unexpected output + if (_testsWithUnexpectedOutput && !opts.dev) { + assert.ok(false, 'Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.'); + } - // should not have unexpected errors - const errors = _unexpectedErrors.concat(_loaderErrors); - if (errors.length) { - for (const error of errors) { - console.error(`Error: Test run should not have unexpected errors:\n${error}`); - } - assert.ok(false, 'Error: Test run should not have unexpected errors.'); + // should not have unexpected errors + const errors = _unexpectedErrors.concat(_loaderErrors); + if (errors.length) { + for (const error of errors) { + console.error(`Error: Test run should not have unexpected errors:\n${error}`); } - }); - - suiteTeardown(() => { // intentionally not in teardown because some tests only cleanup in suiteTeardown + assert.ok(false, 'Error: Test run should not have unexpected errors.'); + } + }); - // should have cleaned up in registries - assertCleanState(); - }); + suiteTeardown(() => { // intentionally not in teardown because some tests only cleanup in suiteTeardown - return loadTestModules(opts); + // should have cleaned up in registries + assertCleanState(); }); + + return loadTestModules(opts); } function serializeSuite(suite) { @@ -403,42 +396,41 @@ class IPCReporter { } } -function runTests(opts) { +async function runTests(opts) { // this *must* come before loadTests, or it doesn't work. if (opts.timeout !== undefined) { mocha.timeout(opts.timeout); } - return loadTests(opts).then(() => { + await loadTests(opts); - if (opts.grep) { - mocha.grep(opts.grep); - } + if (opts.grep) { + mocha.grep(opts.grep); + } - if (!opts.dev) { - mocha.reporter(IPCReporter); - } + if (!opts.dev) { + // @ts-expect-error + mocha.reporter(IPCReporter); + } - const runner = mocha.run(() => { - createCoverageReport(opts).then(() => { - ipcRenderer.send('all done'); - }); - }); + const runner = mocha.run(async () => { + await createCoverageReport(opts) + ipcRenderer.send('all done'); + }); - runner.on('test', test => currentTest = test); + runner.on('test', test => currentTest = test); - if (opts.dev) { - runner.on('fail', (test, err) => { - console.error(test.fullTitle()); - console.error(err.stack); - }); - } - }); + if (opts.dev) { + runner.on('fail', (test, err) => { + console.error(test.fullTitle()); + console.error(err.stack); + }); + } } ipcRenderer.on('run', async (_e, opts) => { initNls(opts); - initLoader(opts); + initLoadFn(opts); await Promise.resolve(globalThis._VSCODE_TEST_INIT);